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 `
`;
}
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(''));
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();
}