coulomb-social/scripts/test-usecase-browser.mjs
tegwick a1989f19db
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m2s
Establish use-case regression collection and real browser gate
Assistant: codex
Assistant-Model: gpt-6.1-sol
Assistant-Session: 01a10e2d-a5ef-7e92-b03d-b1694e915680
2026-10-06 14:02:01 +02:00

141 lines
7.3 KiB
JavaScript

// Real UI traversal with synthetic protocols; no production hosts or email.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { chromium } from 'playwright';
const base = new URL(process.argv[2]);
if (base.protocol !== 'http:' || !['localhost', '127.0.0.1'].includes(base.hostname)) {
throw new Error('Use-case browser runner requires an isolated loopback test server');
}
const scenario = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
assert.equal(scenario.synthetic, true);
const browser = await chromium.launch({headless: true});
try {
const context = await browser.newContext();
// Fail closed on unexpected integrations. Only the isolated app can be contacted.
await context.route('**/*', route => {
if (new URL(route.request().url()).origin === base.origin) return route.continue();
return route.abort();
});
await context.addInitScript(() => {
window.clipboardMode = 'rich';
window.clipboardReceipts = [];
Object.defineProperty(navigator, 'clipboard', {configurable: true, value: {
write: async items => {
if (window.clipboardMode !== 'rich') throw new Error('Rich copy blocked');
const item = items[0];
window.clipboardReceipts.push({
html: await (await item.getType('text/html')).text(),
text: await (await item.getType('text/plain')).text(),
});
},
writeText: async text => {
if (window.clipboardMode === 'denied') throw new Error('Copy blocked');
window.clipboardReceipts.push({text});
},
}});
});
const page = await context.newPage();
page.setDefaultTimeout(10000);
const jsErrors = [];
page.on('pageerror', error => jsErrors.push(error.message));
const detail = `${base.origin}/app/spaces/${scenario.space_slug}/`;
const original = `${detail}?page=${scenario.previous_slug}`;
async function login(role) {
await page.goto(`${base.origin}/auth/dev-login/`);
await page.locator('input[name=subject]').fill(scenario[`${role}_subject`]);
await page.locator('input[name=tenant]').fill(scenario.tenant);
await page.getByRole('button', {name: 'Establish session'}).click();
await page.waitForURL('**/app/');
}
await login('writer');
assert.match(await page.locator('main').innerText(), new RegExp(scenario.space_title));
await page.locator(`a[href="/app/spaces/${scenario.space_slug}/"]`).click();
await page.getByRole('link', {name: scenario.previous_title, exact: true}).click();
await page.getByRole('link', {name: 'Duplicate page', exact: true}).click();
await page.getByLabel('New page slug').fill(scenario.next_slug);
await page.locator('input[name=title]').fill(scenario.next_title);
await page.getByRole('button', {name: 'Create duplicate'}).click();
await page.waitForURL(`**?page=${scenario.next_slug}`);
assert.match(await page.locator('#page-content').innerText(), /Completed point/);
await page.getByRole('link', {name: 'Edit page', exact: true}).click();
assert.equal(await page.getByLabel('Body (markdown)').inputValue(), scenario.previous_body);
await page.getByLabel('Body (markdown)').fill(scenario.next_body);
await page.getByRole('button', {name: 'Save page'}).click();
await page.waitForURL(`**?page=${scenario.next_slug}`);
await page.reload();
assert.match(await page.locator('#page-content').innerText(), /Follow-up point/);
assert.doesNotMatch(await page.locator('#page-content').innerText(), /Completed point/);
// Execute the actual static script loaded by the real detail page, not a fixture DOM.
for (const mode of ['rich', 'plain', 'denied']) {
await page.evaluate(mode => {
window.clipboardMode = mode;
window.clipboardReceipts = [];
}, mode);
await page.getByRole('button', {name: 'Copy content to clipboard'}).click();
await page.waitForFunction(() => !document.getElementById('copy-page-content').disabled);
const result = await page.evaluate(() => ({
receipts: window.clipboardReceipts,
status: document.getElementById('copy-content-status').textContent,
manual: document.getElementById('copy-content-manual').value,
hidden: document.getElementById('copy-content-manual').hidden,
selected: document.getElementById('copy-content-manual').selectionEnd,
}));
const text = mode === 'denied' ? result.manual : result.receipts[0].text;
assert.match(text, /Next weekly call/);
assert.match(text, /- Follow-up point/);
assert.match(text, / - Nested detail/);
assert.match(text, /- Another agenda item/);
assert.match(text, /New notes with emphasis/);
assert.doesNotMatch(text, /Edit page|Duplicate page|Source:|content-plane|Sign out/);
if (mode === 'denied') {
assert.equal(result.receipts.length, 0);
assert.equal(result.hidden, false);
assert.equal(result.selected, text.length);
assert.match(result.status, /Ctrl\+C or Cmd\+C/);
assert.doesNotMatch(result.status, /Content copied/);
} else {
assert.match(result.status, /Content copied/);
if (mode === 'rich') {
const html = result.receipts[0].html;
assert.match(html, /<ul>/);
assert.match(html, /<strong>emphasis<\/strong>/);
// A synthetic rich email editor can consume the same exported HTML.
const email = await context.newPage();
await email.setContent('<div id="email-draft" contenteditable="true"></div>');
await email.locator('#email-draft').evaluate((node, html) => {node.innerHTML = html;}, html);
assert.equal(await email.locator('#email-draft li').count(), 3);
assert.match(await email.locator('#email-draft').innerText(), /New notes with emphasis/);
await email.close();
}
}
}
await page.goto(original);
assert.match(await page.locator('#page-content').innerText(), /Completed point/);
assert.doesNotMatch(await page.locator('#page-content').innerText(), /New notes/);
await login('reader');
await page.goto(`${detail}?page=${scenario.next_slug}`);
assert.match(await page.locator('#page-content').innerText(), /New notes/);
assert.equal(await page.getByRole('link', {name: 'Edit page', exact: true}).count(), 0);
assert.equal(await page.getByRole('button', {name: 'Copy content to clipboard'}).count(), 1);
await page.getByRole('button', {name: 'Copy content to clipboard'}).click();
await page.waitForFunction(() => !document.getElementById('copy-page-content').disabled);
assert.match(await page.locator('#copy-content-status').innerText(), /Content copied/);
const csrf = await page.locator('input[name=csrfmiddlewaretoken]').first().inputValue();
const deniedPost = await page.request.post(`${detail}pages/${scenario.next_slug}/edit/`, {
form: {csrfmiddlewaretoken: csrf, title: 'Unauthorized change',
slug: scenario.next_slug, body: 'Unauthorized reader edit'},
});
assert.equal(deniedPost.status(), 403);
assert.match(await deniedPost.text(), /read-only/);
const denied = await page.goto(`${detail}pages/${scenario.next_slug}/edit/`);
assert.equal(denied.status(), 403);
await login('outsider');
const forbidden = await page.goto(`${detail}?page=${scenario.next_slug}`);
assert.equal(forbidden.status(), 404);
assert.equal(await page.locator('#page-content').count(), 0);
assert.deepEqual(jsErrors, []);
console.log(`${scenario.usecase_id}: real UI duplication/edit/reopen/clipboard/access passed`);
} finally {
await browser.close();
}