import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { chromium } from 'playwright'; // All navigation is intercepted locally. This test never contacts X or publishes content. const observer = await readFile(new URL('../src-tauri/observer.js', import.meta.url), 'utf8'); const chromiumArgs = process.env.CHROMIUM_ARGS_JSON ? JSON.parse(process.env.CHROMIUM_ARGS_JSON) : ['--no-sandbox']; const browser = await chromium.launch({ headless: true, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined, args: chromiumArgs.filter(arg => !['--disable-web-security', '--disable-site-isolation-trials'].includes(arg)), }); const checks = []; function fixture(extra = '', editorTestId = 'tweetTextarea_0') { return `
${extra}
`; } const context = await browser.newContext(); await context.addInitScript(() => { window.__fixtureOriginalFetch = window.fetch; window.__fixtureOriginalPushState = history.pushState; window.__fixtureOriginalReplaceState = history.replaceState; window.__fixtureOriginalXhrOpen = XMLHttpRequest.prototype.open; window.__fixtureCaptures = []; window.__fixtureAcks = []; window.__TAURI__ = { core: { invoke: (command, args) => new Promise((resolve, reject) => { window.__fixtureCaptures.push({ command, ...args.capture }); window.__fixtureAcks.push({ resolve: () => resolve({ sequence: args.capture.sequence, revision: args.capture.sequence }), reject }); }) } }; }); let markup = fixture(); await context.route('**/*', route => { const url = new URL(route.request().url()); return url.origin === 'https://x.com' ? route.fulfill({ status: 200, contentType: 'text/html', body: markup }) : route.abort(); }); const page = await context.newPage(); const editor = () => page.locator('[data-testid="tweetTextarea_0"]'); const badge = () => page.locator('#x-workspace-capture-status'); const calls = () => page.evaluate(() => window.__fixtureCaptures); async function open(path, html = fixture()) { markup = html; await page.goto(`https://x.com${path}`); await page.addScriptTag({ content: observer }); // The status toast stays attached but hidden while idle. await badge().waitFor({ state: 'attached' }); } async function awaitCount(count) { await page.waitForFunction(count => window.__fixtureCaptures.length === count, count); } async function ack() { await page.evaluate(() => window.__fixtureAcks.shift().resolve()); } async function saved() { await page.waitForFunction(() => document.querySelector('#x-workspace-capture-status').textContent.includes('Saved locally')); } try { await open('/home'); await editor().fill('first text'); await awaitCount(1); assert.equal((await calls())[0].command, 'capture_draft'); assert.equal((await calls())[0].kind, 'post'); assert.equal((await calls())[0].contextUrl, 'https://x.com/home'); assert.equal((await badge().innerText()).includes('Saved locally'), false); await ack(); await saved(); await editor().fill('second text'); assert.equal((await badge().innerText()).includes('Saved locally'), false); await awaitCount(2); await editor().fill('latest text'); await page.waitForTimeout(300); assert.equal((await calls()).length, 2); await ack(); await awaitCount(3); assert.equal((await calls())[2].text, 'latest text'); await ack(); await saved(); await editor().fill(''); await awaitCount(4); assert.equal((await calls())[3].text, ''); await ack(); await saved(); checks.push('post capture, delayed ACK, immediate dirty badge, serialized latest edit, empty revision'); await editor().fill('closed modal text'); await page.locator('[role="dialog"]').evaluate(element => element.remove()); await awaitCount(5); assert.equal((await calls())[4].text, 'closed modal text'); await ack(); checks.push('disconnect flushes the pending composer text'); await open('/someone/status/123'); await editor().fill('reply text'); await awaitCount(1); assert.equal((await calls())[0].kind, 'reply'); assert.equal((await calls())[0].contextUrl, 'https://x.com/someone/status/123'); await ack(); checks.push('reply keeps original status context'); await open('/compose/post', fixture('
Quoted post
')); await editor().fill('quote text'); await awaitCount(1); assert.equal((await calls())[0].kind, 'quote'); assert.equal((await calls())[0].contextUrl, 'https://x.com/someone/status/321'); await ack(); checks.push('quote keeps status context without query data'); await open('/messages/123'); await editor().fill('DM text must not be captured'); await page.waitForTimeout(400); assert.deepEqual(await calls(), []); assert.match(await badge().innerText(), /Unsupported surface/); checks.push('DM route refuses capture even with a deceptive tweet selector'); await open('/home', fixture('', 'unknownEditor')); await page.locator('[contenteditable]').fill('unknown markup'); await page.waitForTimeout(400); assert.deepEqual(await calls(), []); assert.match(await badge().textContent(), /No supported composer/); assert.equal(await badge().isVisible(), false, 'idle status must not cover X'); checks.push('unknown markup is visibly unsupported'); await open('/home', fixture('
')); await editor().fill('thread must be unsupported'); await page.waitForTimeout(400); assert.deepEqual(await calls(), []); checks.push('multi-part thread surface is not partially captured'); await open('/home'); await page.addScriptTag({ content: observer }); assert.equal(await badge().count(), 1); await editor().fill('single capture'); await awaitCount(1); await page.waitForTimeout(400); assert.equal((await calls()).length, 1); await ack(); checks.push('re-injection remains idempotent'); await open('/first/status/100'); await editor().fill('first target pending text'); await page.evaluate(() => { const editor = document.querySelector('[data-testid="tweetTextarea_0"]'); editor.textContent = ''; history.pushState({ fixture: true }, '', '/second/status/200'); }); await editor().fill('second target text'); await awaitCount(2); const reused = await calls(); assert.equal(reused[0].text, 'first target pending text'); assert.equal(reused[0].contextUrl, 'https://x.com/first/status/100'); assert.equal(reused[1].text, 'second target text'); assert.equal(reused[1].contextUrl, 'https://x.com/second/status/200'); assert.notEqual(reused[0].composerId, reused[1].composerId); await ack(); await ack(); await saved(); checks.push('reused DOM node across SPA targets gets a fresh composer identity and immutable old context'); const routing = await page.evaluate(() => { const result = history.replaceState({ preserved: true }, '', location.pathname); let errorName; try { history.pushState({}, '', 'https://unrelated.test/'); } catch (error) { errorName = error.name; } return { returnedUndefined: result === undefined, state: history.state, errorName, origin: location.origin }; }); assert.deepEqual(routing, { returnedUndefined: true, state: { preserved: true }, errorName: 'SecurityError', origin: 'https://x.com' }); const restored = await page.evaluate(() => { window[Symbol.for('x-workspace.observer.installation.v1')].dispose(); return { fetch: window.fetch === window.__fixtureOriginalFetch, push: history.pushState === window.__fixtureOriginalPushState, replace: history.replaceState === window.__fixtureOriginalReplaceState, xhr: XMLHttpRequest.prototype.open === window.__fixtureOriginalXhrOpen, panelRemoved: !document.getElementById('x-workspace-capture-status'), }; }); assert.deepEqual(restored, { fetch: true, push: true, replace: true, xhr: true, panelRemoved: true }); checks.push('native history return, state and security errors are unchanged; disposal restores all hooks'); console.log(JSON.stringify({ ok: true, fixtureOnly: true, liveXVerified: false, checks }, null, 2)); } finally { await browser.close(); }