Feature 12 of the co-design report: a convergence monitor.
- engine::converge:
- history: for each version the person made after a model proposed,
its structural distance from the proposals in its line of work (the
whole proposal) and the model's share of its gates; converging when
most of the program is the model's or the last versions come closer
each time, with why; versions noted "organic exploration" (the
assistant-off probes) set beside the rest;
- cohort: the Vendi score over 1 - distance (Jacobi eigenvalues), each
program's distance to the centroid of the gate-token bags, the medoid
and the outlier, and with times the score in windows and a narrowing;
- call for hosts. novelty::bag is shared.
- The command line: converge history | cohort (a file's time from its
history, else the file's).
- MCP: converge_cohort and converge_history.
- The Studio: "Your own way" under Versions, and "Explore on my own for a
while": the assistant only asks and offers no program, Explore and Ask
the model wait, the person's versions are noted; End lets the model
propose again.
- Bindings: engine_converge in the page, converge in the desktop app.
- CI: wai-web's native engine tests count 13.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
746 lines
63 KiB
JavaScript
746 lines
63 KiB
JavaScript
// The Studio in the installed browser, as a reader uses it:
|
||
//
|
||
// node build.mjs --pkg PATH/TO/ENGINE/PKG && node test/studio.test.mjs
|
||
//
|
||
// Serves dist/ (with the engine package in dist/pkg/); the page's index.html names the
|
||
// live job service, https://wai.transaction.science/quantum-jobs, which the service checks use.
|
||
// - the first example loads; the program is linted, drawn and planned as it is typed;
|
||
// - it runs in this browser: the stabilizer tableau, only 00 and 11, ⟨Z0 Z1⟩ = 1, the
|
||
// receipt checked here and signed by this browser's key;
|
||
// - an error in the program is marked on its line;
|
||
// - a rotation runs on the state vector, ⟨Z0⟩ = cos 0.3;
|
||
// - the same Bell job on the service gives the counts run here, shot for shot, and its
|
||
// receipt checks here as the service's (left out with STUDIO_SKIP_SERVICE=1);
|
||
// - Ctrl+Enter in the editor runs; the program and the history survive a reload;
|
||
// - Versions: each change recorded with who made it; a proposal taken change by
|
||
// change; whose hand each line and gate is in; the receipt binding it; a line
|
||
// of work from an older version merged back; kept and downloaded;
|
||
// - Rules: held lines marked, shaded and drawn; a gate set and a region; a
|
||
// proposal's breaking changes shown with their rule and never taken; the
|
||
// model told the rules;
|
||
// - the critique: its measures as the program changes and their change since the last
|
||
// run; a smell named with its lines, flagged on the diagram, shown, and fixed by the
|
||
// engine's rewrite, checked the same circuit;
|
||
// - Alternatives: Improve offers the engine's rewrite checked equivalent, plotted and
|
||
// applied; Explore without a model says it needs one; a rewrite that reaches the GHZ
|
||
// state by a different route is labelled so and flagged in Found, saved there with a
|
||
// note, and kept with the page; an alternative that is the same circuit, taken whole, is
|
||
// a choice the reader's taste learns from, and the card says how the draft becomes it, as
|
||
// a chain of checked rewrites;
|
||
// - the rewrite lens: each rewrite the program admits, its gates marked on the diagram,
|
||
// one taken as the engine's proposal, and the list following the program;
|
||
// - your own way: a while on one's own, kept in this browser, Explore waiting for it, a
|
||
// version made then noted so, the convergence line, and its end;
|
||
// - the classical-shortcut lens: a Clifford ring with a closing rotation layer priced by
|
||
// the cut-rank sum, a Clifford circuit easy at any width, a contraction searched only
|
||
// when thorough, and a check marked stale once the program changes;
|
||
// - predict first: the form stops a run until the reader predicts, refuses an outcome of
|
||
// the wrong width, and sets the prediction against the counts;
|
||
// - the assistant, against a scripted model server: a tool call run on the engine, a
|
||
// proposal the engine cannot read sent back with its reason, the corrected one checked
|
||
// and run before it is offered, and Apply; it asks questions first by default and speaks
|
||
// only when asked; offering a hint after refused runs only when the reader allows it,
|
||
// and never asking the model itself; Improve and Explore with the model, each candidate
|
||
// read, measured and compared by the engine, a rewrite that is not the same circuit
|
||
// left out with its reason;
|
||
// - generators: a program with an integer input shows it under the editor, is linted and
|
||
// drawn at its value, and runs there; the Workbench starts from it, judges a hundred
|
||
// blind edits, keeps the reader's log-depth tree as the cheapest, opens it in the
|
||
// editor, and comes back with the page; the tree is named a known construction, GHZ by
|
||
// doubling, with what it beats the reference on that the cost does not weigh; on a
|
||
// line, the tree is an aberration, offered with the rule to drop, and flagged; your taste:
|
||
// two kept generators compared and one chosen for its economy, the archive ranked by the
|
||
// profile with each one's typicality, the dial turned, a second profile kept apart, and
|
||
// the profiles kept with the page; with the
|
||
// model, its generators are judged at every size, a wrong one and one with a gate the
|
||
// specification forbids left out (an aberration), and it is told the specification and
|
||
// shown the cheapest found; its CZ idea in Explore is a different route to GHZ;
|
||
// - no console error and no failed request; at a phone's width nothing scrolls sideways.
|
||
// Exit 0 iff every check passes.
|
||
import { createServer } from 'node:http';
|
||
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
|
||
import { dirname, extname, join, normalize, resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const { chromium } = (await import(process.env.WAI_E2E_DRIVER ?? 'playwright')).default;
|
||
const DIST = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
||
const SHOTS = resolve(dirname(fileURLToPath(import.meta.url)), 'screenshots');
|
||
const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.wasm': 'application/wasm', '.json': 'application/json' };
|
||
const server = createServer((req, res) => {
|
||
let file = normalize(join(DIST, decodeURIComponent(new URL(req.url, 'http://x').pathname)));
|
||
if (!file.startsWith(DIST)) { res.writeHead(403).end(); return; }
|
||
if (existsSync(file) && statSync(file).isDirectory()) file = join(file, 'index.html');
|
||
if (!existsSync(file)) { res.writeHead(404).end('not found'); return; }
|
||
res.writeHead(200, { 'content-type': TYPES[extname(file)] ?? 'application/octet-stream' });
|
||
res.end(readFileSync(file));
|
||
});
|
||
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
||
const ORIGIN = `http://127.0.0.1:${server.address().port}`;
|
||
let fails = 0;
|
||
const check = (ok, what) => { console.log(`${ok ? 'PASS' : 'FAIL'} ${what}`); if (!ok) fails += 1; };
|
||
const LONG = 120_000;
|
||
const browser = await chromium.launch({ channel: process.env.WAI_E2E_CHANNEL ?? 'chrome', headless: true });
|
||
|
||
async function open(viewport = { width: 1400, height: 950 }) {
|
||
const ctx = await browser.newContext({ viewport, colorScheme: 'dark' });
|
||
const page = await ctx.newPage();
|
||
const seen = { errors: [], failed: [] };
|
||
page.on('console', (m) => { if (m.type() === 'error') seen.errors.push(m.text()); });
|
||
page.on('pageerror', (e) => seen.errors.push(String(e)));
|
||
page.on('requestfailed', (r) => seen.failed.push(`${r.url()} (${r.failure()?.errorText})`));
|
||
page.on('response', (r) => { if (r.status() >= 400) seen.failed.push(`${r.url()} (${r.status()})`); });
|
||
await page.goto(ORIGIN + '/', { waitUntil: 'domcontentloaded' });
|
||
return { ctx, page, seen };
|
||
}
|
||
const done = (page) => page.waitForFunction(() => !/^Running|^On the service/.test(document.querySelector('.qs-status')?.textContent ?? '') && document.querySelector('#qs-run') && !document.querySelector('#qs-run').disabled, null, { timeout: LONG });
|
||
const verified = (page) => page.waitForFunction(() => /verifies|does not verify|not checked/.test(document.querySelector('#qs-verified')?.textContent ?? ''), null, { timeout: LONG });
|
||
const counts = (page) => page.$$eval('.qs-bar', (bs) => bs.map((b) => [b.querySelector('.qs-key').textContent, b.querySelector('.qs-n').textContent]));
|
||
|
||
const { ctx, page, seen } = await open();
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.textContent('#qs-lint')) === '2 qubits · 2 gates', `the first example is linted as the engine reads it (${await page.textContent('#qs-lint')})`);
|
||
await page.waitForFunction(() => document.querySelector('#qs-circuit')?.textContent.includes('⊕'), null, { timeout: LONG });
|
||
const drawing = await page.textContent('#qs-circuit');
|
||
check(/q0: .*H.*●/.test(drawing) && /q1: .*⊕/.test(drawing), 'the circuit is drawn as the program is typed');
|
||
const svg = await page.$eval('#qs-diagram svg', (g) => ({ label: g.getAttribute('aria-label'), rings: g.querySelectorAll('.qc-ring').length, dots: g.querySelectorAll('.qc-dot').length, joins: g.querySelectorAll('.qc-join').length, meters: g.querySelectorAll('.qc-measure').length }));
|
||
check(svg.rings === 1 && svg.dots === 1 && svg.joins === 1 && svg.meters === 2 && /2 qubits/.test(svg.label), `the circuit is drawn as a diagram with a text alternative (${JSON.stringify(svg)})`);
|
||
await page.click('#qs-astext');
|
||
check(await page.isVisible('#qs-circuit') && await page.isHidden('#qs-diagram'), '“Text” shows the circuit as text to copy');
|
||
await page.click('#qs-astext');
|
||
await page.waitForFunction(() => /Stabilizer tableau/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `the plan is shown before running (${await page.textContent('#qs-plan')})`);
|
||
|
||
await page.click('#qs-run');
|
||
await done(page);
|
||
await verified(page);
|
||
check((await page.textContent('.qs-method')) === 'Stabilizer tableau', 'run in this browser: the stabilizer tableau');
|
||
const here = await counts(page);
|
||
check(here.length === 2 && here.every(([k]) => k === '00' || k === '11'), `only 00 and 11 (${JSON.stringify(here)})`);
|
||
const zz = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
|
||
check(zz.some((t) => t.startsWith('⟨Z0 Z1⟩') && t.includes('1.000000')), `⟨Z0 Z1⟩ = 1 (${zz.slice(0, 2).join(' | ')})`);
|
||
check((await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the receipt verifies, checked here (${await page.textContent('#qs-verified')})`);
|
||
check((await page.textContent('#qs-signed-by')).startsWith('this browser’s key did:key:z6Mk'), `signed by this browser's key (${await page.textContent('#qs-signed-by')})`);
|
||
check((await page.$$('.qs-hitem')).length === 1, 'the run is in the history');
|
||
// What it looks like, for a reader of the change: the dark scheme and the light.
|
||
mkdirSync(SHOTS, { recursive: true });
|
||
await page.screenshot({ path: join(SHOTS, 'studio-dark.png'), fullPage: true });
|
||
await page.emulateMedia({ colorScheme: 'light' });
|
||
await page.screenshot({ path: join(SHOTS, 'studio-light.png'), fullPage: true });
|
||
await page.emulateMedia({ colorScheme: 'dark' });
|
||
|
||
// An error is marked on its line.
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+End');
|
||
await page.keyboard.type('\nfoo q[0];');
|
||
await page.waitForFunction(() => document.querySelector('#qs-lint')?.textContent === 'problem', null, { timeout: LONG });
|
||
await page.waitForSelector('.cm-lintRange-error', { timeout: LONG });
|
||
check(true, 'an unknown gate is marked in the editor');
|
||
for (let i = 0; i < 10; i++) await page.keyboard.press('Backspace');
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
|
||
// A rotation: any angle, on the state vector.
|
||
await page.selectOption('#qs-example', 'rotation');
|
||
await page.waitForFunction(() => /State vector/.test(document.querySelector('#qs-plan')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-run');
|
||
await done(page);
|
||
const z = await page.$$eval('.qs-table tbody tr', (rs) => rs.map((r) => r.textContent));
|
||
check((await page.textContent('.qs-method')) === 'State vector, double precision' && z.some((t) => t.startsWith('⟨Z0⟩') && t.includes(Math.cos(0.3).toFixed(6))), `rx(0.3) on the state vector, ⟨Z0⟩ = cos 0.3 (${z[0]})`);
|
||
|
||
// The same Bell job on the service, and here: the same counts.
|
||
// (STUDIO_SKIP_SERVICE=1 leaves it out, where a build may not reach the service.)
|
||
if (process.env.STUDIO_SKIP_SERVICE) {
|
||
console.log('SKIP the service checks (STUDIO_SKIP_SERVICE)');
|
||
} else {
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.click('#qs-run'); await done(page); await verified(page);
|
||
const local = await counts(page);
|
||
await page.click('.qs-seg[data-where=service]');
|
||
await page.click('#qs-run'); await done(page); await verified(page);
|
||
const remote = await counts(page);
|
||
check(JSON.stringify(remote) === JSON.stringify(local), `the service gives the counts run here, shot for shot (${JSON.stringify(remote)})`);
|
||
check((await page.textContent('#qs-signed-by')).startsWith('the service’s key') && (await page.textContent('#qs-verified')).startsWith('✓ verifies'), `the service's receipt checks here, as the service's (${await page.textContent('#qs-signed-by')})`);
|
||
await page.click('.qs-seg[data-where=local]');
|
||
|
||
}
|
||
|
||
// Ctrl+Enter runs; the program and the history survive a reload. (A job run again
|
||
// unchanged gives the same signed result, which the history keeps once; so this one
|
||
// asks for other shots.)
|
||
const before = (await page.$$('.qs-hitem')).length;
|
||
await page.fill('#qs-shots', '321');
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+Enter');
|
||
await done(page);
|
||
check((await page.$$('.qs-hitem')).length === before + 1, 'Ctrl+Enter in the editor runs the program');
|
||
const program = await page.evaluate(() => window.quantumStudio.view.state.doc.toString());
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.evaluate(() => window.quantumStudio.view.state.doc.toString())) === program, 'the program survives a reload');
|
||
await page.waitForFunction((n) => document.querySelectorAll('.qs-hitem').length >= n, before + 1, { timeout: LONG });
|
||
check(true, 'the history survives a reload');
|
||
|
||
// The critique, as the program changes.
|
||
const doc = () => page.evaluate(() => window.quantumStudio.view.state.doc.toString());
|
||
const setProgram = async (q) => { await page.evaluate((q) => window.quantumStudio.fill({ qasm: q }), q); await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, q); };
|
||
const chip = (k) => page.$eval(`#qs-measures [data-k=${k}]`, (c) => c.textContent).catch(() => '');
|
||
{
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG });
|
||
const m = await page.textContent('#qs-measures');
|
||
check(/2 qubits/.test(m) && /1 two-qubit/.test(m) && await page.isVisible('#qs-clifford') && /fidelity, modelled/.test(m) && (await page.$$('#qs-smells li')).length === 0,
|
||
`the critique gives the Bell pair's measures, that it is Clifford, a modelled fidelity, and no smells (${m})`);
|
||
const SMELLY = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(SMELLY);
|
||
await page.waitForSelector('.qs-smell[data-kind=cancels]', { timeout: LONG });
|
||
const smell = await page.textContent('.qs-smell[data-kind=cancels] .qs-smell-text');
|
||
check(smell === 'x on qubit 1 twice in a row (lines 5 and 6): together they do nothing', `a smell is named with its lines (${smell})`);
|
||
check((await page.$$('#qs-diagram .qc-item.qc-flag')).length === 2, 'its two gates are flagged on the diagram');
|
||
await page.hover('.qs-smell[data-kind=cancels]');
|
||
check((await page.$$('#qs-diagram .qc-item.qc-smell')).length === 2 && (await page.$$('#qs-diagram .qc-wire.qc-smell')).length === 0, 'pointing at the smell marks its gates, and not their wires');
|
||
await page.click('.qs-smell[data-kind=cancels] button >> text=Show');
|
||
const sel = await page.evaluate(() => { const v = window.quantumStudio.view, d = v.state.doc, r = v.state.selection.main; return [r.from === d.line(5).from, r.to === d.line(6).to]; });
|
||
check(sel[0] && sel[1], 'Show selects its lines in the editor');
|
||
await page.click('#qs-run'); await done(page);
|
||
await page.click('.qs-smell[data-kind=cancels] .qs-fix');
|
||
await page.waitForFunction(() => !/x q\[1\];/.test(window.quantumStudio.view.state.doc.toString()) && /checked the result/.test(document.querySelector('#qs-shows')?.textContent ?? ''), null, { timeout: LONG });
|
||
const shows = await page.textContent('#qs-shows');
|
||
check(/^Fixed: x on qubit 1/.test(shows) && /same tableau/.test(shows), `Fix applies the engine's rewrite, checked the same circuit (${shows})`);
|
||
await page.waitForFunction(() => /−2/.test(document.querySelector('#qs-measures [data-k=gates]')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `the measures show their change since the last run (${await chip('gates')})`);
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+z');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, SMELLY, { timeout: LONG });
|
||
check(true, 'Undo brings the fixed lines back');
|
||
await setProgram('OPENQASM 3.0;\nqubit[3] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\n');
|
||
await page.waitForSelector('.qs-smell[data-kind=idle]', { timeout: LONG });
|
||
check((await page.$$('#qs-diagram .qc-wire.qc-flag')).length === 2 && !(await page.$('.qs-smell[data-kind=idle] .qs-fix')), 'qubits nothing uses are flagged on their wires, with no Fix');
|
||
}
|
||
|
||
// Alternatives: Improve, without a model; Explore says it needs one.
|
||
{
|
||
const MERGE = 'OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nrz(0.25) q[0];\nrz(0.5) q[0];\nc[0] = measure q[0];\n';
|
||
await setProgram(MERGE);
|
||
await page.click('#qs-improve');
|
||
await page.waitForSelector('.qs-cand', { timeout: LONG });
|
||
const card = await page.textContent('.qs-cand');
|
||
check(/the engine/.test(card) && /same circuit · on every basis state/.test(card) && /2 gates/.test(card), `Improve offers the engine's rewrite, checked on every basis state (${card.slice(0, 160)})`);
|
||
check(await page.isVisible('#qs-atlas') && (await page.$$('#qs-atlas .qs-dot')).length === (await page.$$('.qs-cand')).length && (await page.$$('#qs-atlas .qs-ring')).length === 1, 'the atlas plots the draft and each alternative');
|
||
check(/Connect a model/.test(await page.textContent('#qs-alt-body')), 'without a model, Improve says the engine’s are the only ones');
|
||
check(/How: 1 rewrite, each checked/.test(card) && card.includes('rz(0.25) q[0] and rz(0.5) q[0] merge into rz(0.75) q[0]'), `the card says how the draft becomes it, a chain of checked rewrites (${card.slice(0, 220)})`);
|
||
check((await doc()) === MERGE, 'nothing changes the program until an alternative is applied');
|
||
await page.click('.qs-cand .qs-apply');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
check((await doc()) === MERGE && /A proposal from the engine \(Alternatives\)/.test(await page.textContent('#qs-review')), 'Apply offers the alternative as the engine’s proposal, to take change by change');
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => /rz\(0\.25 \+ 0\.5\) q\[0\];/.test(window.quantumStudio.view.state.doc.toString()), null, { timeout: LONG });
|
||
check(true, 'taking it puts the alternative in the editor');
|
||
await page.waitForFunction(() => document.querySelector('#qs-taste-count')?.textContent === '1', null, { timeout: LONG });
|
||
check(/^1 choice\b/.test(await page.textContent('#qs-taste-summary')), 'taken whole, an alternative that is the same circuit is a choice the reader’s taste learns from');
|
||
await page.click('#qs-explore');
|
||
await page.waitForSelector('#qs-alt-nomodel', { timeout: LONG });
|
||
check(true, 'Explore without a model says it needs one');
|
||
}
|
||
|
||
// What is new: a rewrite that is the GHZ state, built unlike the known chain, is a
|
||
// different route to it, and Found keeps it until the reader saves or dismisses it.
|
||
{
|
||
check((await page.textContent('#qs-found-count')) === '0 flagged · 0 saved' && await page.isVisible('#qs-found-empty'), 'Found starts empty');
|
||
await setProgram('OPENQASM 3.0;\nqubit[3] q;\nh q[2];\nx q[0];\nx q[0];\ncx q[2], q[1];\ncx q[1], q[0];\n');
|
||
await page.click('#qs-improve');
|
||
await page.waitForSelector('.qs-cand .qs-novelty', { timeout: LONG });
|
||
const labels = await page.$$eval('.qs-cand .qs-novelty', (bs) => bs.map((b) => [b.textContent, b.title]));
|
||
const route = labels.find(([t]) => t === 'a different route to GHZ, by a chain of CNOTs');
|
||
check(route && await page.$('.qs-cand.find'), `a rewrite that makes the GHZ state, built unlike the chain, is labelled a different route (${labels.map(([t]) => t).join(' | ')})`);
|
||
check(/0\.\d\d apart/.test(route?.[1] ?? ''), 'the label says how far apart they are built');
|
||
await page.waitForSelector('#qs-found-list .qs-found-item[data-kind=route]', { timeout: LONG });
|
||
check((await page.textContent('#qs-found-count')) === '1 flagged · 0 saved' && /different route/.test(await page.textContent('#qs-found-list')) && /Improve/.test(await page.textContent('#qs-found-list')), 'Found flags it, and says where it came from');
|
||
await page.click('#qs-found-list .qs-found-save');
|
||
await page.fill('#qs-found-list .qs-found-item input', 'the ladder, read backwards');
|
||
await page.dispatchEvent('#qs-found-list .qs-found-item input', 'change');
|
||
check((await page.textContent('#qs-found-count')) === '1 flagged · 1 saved' && (await page.textContent('#qs-found-list .qs-found-save')) === 'Saved', 'Save keeps it, and the count says so');
|
||
}
|
||
|
||
// The rewrite lens: each rewrite that keeps the meaning, checked, taken one at a time.
|
||
{
|
||
const LENS = 'OPENQASM 3.0;\nqubit[2] q;\nh q[1];\ncx q[0], q[1];\nh q[1];\nt q[0];\nt q[0];\n';
|
||
await setProgram(LENS);
|
||
// The diagram is drawn a moment after the edit; marks go on the one drawn.
|
||
// (The measurements it draws at the end carry no gate.)
|
||
await page.waitForFunction(() => /5 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && [...document.querySelectorAll('#qs-diagram .qc-item')].filter((g) => /^\d+$/.test(g.dataset.op ?? '')).length === 5, null, { timeout: LONG });
|
||
await page.click('#qs-rewrites');
|
||
await page.waitForSelector('#qs-rewrite-list li', { timeout: LONG });
|
||
const rows = await page.$$eval('#qs-rewrite-list li', (ls) => ls.map((l) => [l.dataset.rule, l.textContent]));
|
||
check(rows.some(([r, t]) => r === 'conjugate' && t.includes('cx q[0], q[1] between two h q[1] is cz q[0], q[1]')) && rows.some(([r, t]) => r === 'merge' && t.includes('t q[0] and t q[0] merge into s q[0]')),
|
||
`the lens lists each rewrite the program admits, in words (${rows.map(([r]) => r).join(', ')})`);
|
||
await page.hover('#qs-rewrite-list li[data-rule=conjugate]');
|
||
const marked = await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-rewrite').length === 3, null, { timeout: 5000 }).then(() => true, () => false);
|
||
check(marked, `pointing at one marks its gates on the diagram (${(await page.$$('#qs-diagram .qc-item.qc-rewrite')).length} marked)`);
|
||
await page.click('#qs-rewrite-list li[data-rule=conjugate] .qs-rewrite-take');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
check(/A proposal from the engine \(Rewrite lens\)/.test(await page.textContent('#qs-review')) && (await doc()) === LENS, 'Take offers it as the engine’s proposal, to review');
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => /cz q\[0\], q\[1\];/.test(window.quantumStudio.view.state.doc.toString()) && !/h q\[1\]/.test(window.quantumStudio.view.state.doc.toString()), null, { timeout: LONG });
|
||
await page.waitForFunction(() => !document.querySelector('#qs-rewrite-list li[data-rule=conjugate]') && document.querySelector('#qs-rewrite-list li[data-rule=merge]'), null, { timeout: LONG });
|
||
check(true, 'taken, the lens follows the program: the conjugation is gone and the merge is still offered');
|
||
}
|
||
|
||
// Your own way: whether the versions converge on the model's, and a while on one's own.
|
||
{
|
||
check(/^Explore on my own/.test(await page.textContent('#qs-organic')) && await page.isHidden('#qs-organic-chip'), 'Versions offers a while on one’s own');
|
||
await page.click('#qs-organic');
|
||
check(await page.isVisible('#qs-organic-chip') && (await page.getAttribute('#qs-organic', 'aria-pressed')) === 'true' && (await page.evaluate(() => localStorage.getItem('wai.quantum-studio.organic'))) === '1', 'on one’s own: marked, and kept in this browser');
|
||
await page.click('#qs-explore');
|
||
await page.waitForSelector('#qs-alt-paused', { timeout: LONG });
|
||
check(true, 'Explore, which asks the model, waits');
|
||
await setProgram('OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\nz q[1];\n');
|
||
await page.evaluate(() => window.quantumStudio.versions.record());
|
||
const note = await page.evaluate(() => window.quantumStudio.versions.history().versions.at(-1).note);
|
||
check(note === 'organic exploration', `a version made on one’s own is noted so (${note})`);
|
||
await page.waitForFunction(() => /\.$/.test(document.querySelector('#qs-converge')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `the line says whether the versions converge on the model’s (${await page.textContent('#qs-converge')})`);
|
||
await page.click('#qs-organic');
|
||
check(await page.isHidden('#qs-organic-chip') && /^Explore on my own/.test(await page.textContent('#qs-organic')), 'ended, the model may be asked again');
|
||
}
|
||
|
||
// Versions: each change and who made it; a proposal taken change by change;
|
||
// whose hand each line and gate is in; the run's receipt binding the count; a
|
||
// line of work from an older version, brought back in; and all of it kept.
|
||
{
|
||
const here = () => page.textContent('#qs-vs-list li.here');
|
||
const record = () => page.evaluate(() => window.quantumStudio.versions.record());
|
||
const BASE = 'OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(BASE);
|
||
await record();
|
||
check(/edited by you/.test(await here()), `what the reader types is recorded as theirs (${await here()})`);
|
||
const first = await page.textContent('#qs-vs-list li:last-child');
|
||
check(/^v0started from the example /.test(first), `the first version is named for where the program came from (${first})`);
|
||
const mine = await page.$eval('#qs-vs-list li.here', (l) => l.dataset.id);
|
||
const PROPOSED = 'OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[1];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n';
|
||
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'assistant', name: 'scripted' }, 'entangle the third qubit'); }, PROPOSED);
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-1', { timeout: LONG });
|
||
const offered = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent));
|
||
check(offered.length === 2 && /h q\[0\]/.test(offered[0]) && /\+ cx q\[1\], q\[2\]/.test(offered[1]) && await page.evaluate(() => window.quantumStudio.view.state.readOnly),
|
||
`a proposal is shown change by change, and the program waits (${offered.map((t) => t.replace(/\s+/g, ' ')).join(' | ')})`);
|
||
await page.uncheck('#qs-review-0');
|
||
check((await page.textContent('#qs-review-apply')) === 'Take 1 of 2', 'the reader takes the changes they want');
|
||
await page.click('#qs-review-apply');
|
||
const TAKEN = BASE.replace('cx q[0], q[1];\n', 'cx q[0], q[1];\ncx q[1], q[2];\n');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q && !window.quantumStudio.view.state.readOnly, TAKEN, { timeout: LONG });
|
||
check(/proposed by the assistant \(scripted\)/.test(await here()) && /1 of 2 taken/.test(await here()), `only the change taken reaches the program, recorded as the assistant's (${await here()})`);
|
||
check(/1 of 2 changes taken; the rest were not taken/.test(await page.textContent('#qs-vs-detail')), 'the version keeps what was rejected');
|
||
await page.check('#qs-vs-hands');
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-hand-gutter .qs-hand:not(.qs-hand-spacer)').length === 7, null, { timeout: LONG });
|
||
const gutter = await page.$$eval('.qs-hand-gutter .qs-hand:not(.qs-hand-spacer)', (ms) => ms.map((m) => m.className.replace('qs-hand qs-hand-', '')));
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-by-assistant').length === 1, null, { timeout: LONG });
|
||
check(gutter[5] === 'assistant' && gutter.filter((h) => h === 'assistant').length === 1 && gutter.slice(1, 3).every((h) => h === 'person'),
|
||
`“Who wrote it” marks each line in the gutter and each gate on the diagram (${gutter.join(', ')})`);
|
||
// The reader edits the model's line: in both hands.
|
||
await setProgram(TAKEN.replace('cx q[1], q[2];', 'cx q[0], q[2];'));
|
||
await record();
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-hand-gutter .qs-hand-assistant_then_person').length === 1, null, { timeout: LONG });
|
||
check(/the assistant, then you \(scripted\): 1 gate, 1 line/.test(await page.textContent('#qs-vs-share')), `a model's line the reader edits is in both hands (${await page.textContent('#qs-vs-share')})`);
|
||
await page.click('#qs-run'); await done(page);
|
||
await page.waitForFunction(() => /recomputed/.test(document.querySelector('#qs-account')?.textContent ?? ''), null, { timeout: LONG });
|
||
const wrote = await page.textContent('#qs-written-by');
|
||
check(/1 by the assistant, then you \(scripted\)/.test(wrote) && /of 3 gates/.test(wrote) && (await page.textContent('#qs-verified')).startsWith('✓ verifies'),
|
||
`the run's receipt binds whose hand each gate is in, recomputed here (${wrote}; ${await page.textContent('#qs-account')})`);
|
||
// A line of work from an older version, then brought back in.
|
||
await page.click(`#qs-vs-list li[data-id=${mine}] .qs-vs-item`);
|
||
await page.click('#qs-vs-open');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, BASE, { timeout: LONG });
|
||
await setProgram(BASE.replace('h q[0];\n', 'h q[0];\nx q[2];\n'));
|
||
await record();
|
||
const heads = await page.$$eval('#qs-vs-list li', (ls) => ls.filter((l) => /a head|in the editor/.test(l.textContent)).length);
|
||
check(heads === 2 && (await page.$$('.qs-vs-svg .qs-vs-edge')).length >= 4, `an edit from an older version is a line of work of its own (${heads} heads)`);
|
||
const other = await page.$eval('#qs-vs-list li:not(.here)', (l) => l.dataset.id);
|
||
const otherHead = await page.$$eval('#qs-vs-list li', (ls) => ls.find((l) => /a head/.test(l.textContent))?.dataset.id);
|
||
await page.click(`#qs-vs-list li[data-id=${otherHead}] .qs-vs-item`);
|
||
await page.click('#qs-vs-merge');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
const merging = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent));
|
||
// Keep this line's x q[2]; take the other's edited CX.
|
||
for (const [i, t] of merging.entries()) if (/− x q\[2\]/.test(t)) await page.uncheck(`#qs-review-${i}`);
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => /merged by you/.test(document.querySelector('#qs-vs-list li.here')?.textContent ?? ''), null, { timeout: LONG });
|
||
const merged = await doc();
|
||
check(/x q\[2\];/.test(merged) && /cx q\[0\], q\[2\];/.test(merged) && /, from v\d+ and v\d+/.test(await page.textContent('#qs-vs-detail')),
|
||
`bringing a line of work back in is a merge of both (${other}, ${otherHead})`);
|
||
check(/the assistant, then you \(scripted\): 1 gate/.test(await page.textContent('#qs-vs-share')), 'the merge keeps each line’s author');
|
||
const count = (await page.$$('#qs-vs-list li')).length;
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForSelector('#qs-vs-list li.here', { timeout: LONG });
|
||
const back = (await page.$$('#qs-vs-list li')).length;
|
||
check(back === count && /merged by you/.test(await here()) && (await doc()) === merged, `the versions come back with the page (${back} of ${count}; ${await here()})`);
|
||
const files = [];
|
||
page.on('download', (d) => files.push(d));
|
||
await page.click('#qs-vs-download');
|
||
await page.waitForFunction(() => /program\.history\.json/.test(document.querySelector('.qs-status')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.waitForTimeout(500);
|
||
const hist = files.find((d) => d.suggestedFilename() === 'program.history.json');
|
||
const saved = hist && JSON.parse(readFileSync(await hist.path(), 'utf8'));
|
||
const prog = files.find((d) => d.suggestedFilename() === 'program.qasm');
|
||
check(saved?.versions?.length === count && saved.versions.every((v) => v.seal?.key?.length === 64 && v.digest.length === 64) && prog && readFileSync(await prog.path(), 'utf8') === merged,
|
||
`Download saves the program and its history, every version sealed, as wai-quantum history verify reads them (${files.map((d) => d.suggestedFilename()).join(', ')})`);
|
||
await page.uncheck('#qs-vs-hands');
|
||
}
|
||
|
||
// Rules for proposals: held lines, a gate set and a region bind every proposal,
|
||
// change by change; a change that breaks one is shown with it and cannot be taken.
|
||
{
|
||
const record = () => page.evaluate(() => window.quantumStudio.versions.record());
|
||
const BASE = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(BASE);
|
||
await record();
|
||
await page.evaluate(() => { const v = window.quantumStudio.view, l = v.state.doc.line(4); v.dispatch({ selection: { anchor: l.from, head: l.to } }); });
|
||
await page.click('#qs-rules > summary');
|
||
await page.click('#qs-rule-hold');
|
||
const HELD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\n// @lock\nh q[0];\n// @unlock\ncx q[0], q[1];\nc = measure q;\n';
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, HELD, { timeout: LONG });
|
||
check((await page.$$('.cm-line.qs-held-line')).length === 3, 'holding the selected lines marks them in the program and shades them');
|
||
await page.waitForFunction(() => document.querySelectorAll('#qs-diagram .qc-item.qc-held').length === 1, null, { timeout: LONG });
|
||
check(true, 'the held gate is marked on the diagram');
|
||
await page.fill('#qs-rule-gates', 'h, cx, x');
|
||
await page.dispatchEvent('#qs-rule-gates', 'change');
|
||
await record();
|
||
// Three changes: a cz the gate set does not allow, the held line, and an x it does.
|
||
const PROPOSED = HELD.replace('bit[2] c;\n', 'bit[2] c;\ncz q[0], q[1];\n').replace('h q[0];', 'h q[1];').replace('cx q[0], q[1];\n', 'cx q[0], q[1];\nx q[1];\n');
|
||
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'assistant', name: 'scripted' }, 'three changes'); }, PROPOSED);
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-2', { timeout: LONG });
|
||
const rev = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent.replace(/\s+/g, ' ')));
|
||
check(await page.isDisabled('#qs-review-0') && await page.isDisabled('#qs-review-1') && !(await page.isDisabled('#qs-review-2')) && /only cx, h, x may be used/.test(rev[0]) && /are held/.test(rev[1]),
|
||
`a change that breaks a rule is shown with it and cannot be taken (${rev.map((t) => t.slice(0, 80)).join(' | ')})`);
|
||
check((await page.textContent('#qs-review-apply')) === 'Take 1 of 1', 'only the change the rules allow can be taken');
|
||
await page.click('#qs-review-apply');
|
||
const KEPT = HELD.replace('cx q[0], q[1];\n', 'cx q[0], q[1];\nx q[1];\n');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, KEPT, { timeout: LONG });
|
||
check(/1 of 3 taken, 2 blocked/.test(await page.textContent('#qs-vs-list li.here')), `the version records what the rules blocked (${await page.textContent('#qs-vs-list li.here')})`);
|
||
// A region: the next proposal keeps to the selected line.
|
||
await page.evaluate(() => { const v = window.quantumStudio.view, l = v.state.doc.line(8); v.dispatch({ selection: { anchor: l.from, head: l.to } }); });
|
||
await page.click('#qs-rule-region');
|
||
check(/Only lines 8 to 8/.test(await page.textContent('#qs-rule-region-chip')), 'a region is kept to the selected lines');
|
||
const OUTSIDE = KEPT.replace('qubit[2] q;\nbit[2] c;', 'qubit[3] q;\nbit[3] c;').replace('x q[1];', 'x q[0];');
|
||
await page.evaluate((q) => { window.quantumStudio.propose(q, { kind: 'agent', name: 'test agent' }, 'two changes'); }, OUTSIDE);
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-1', { timeout: LONG });
|
||
const out = await page.$$eval('.qs-review-list > li', (ls) => ls.map((l) => l.textContent.replace(/\s+/g, ' ')));
|
||
check(await page.isDisabled('#qs-review-0') && /outside lines 8 to 8/.test(out[0]) && !(await page.isDisabled('#qs-review-1')), `a change outside the region is blocked (${out[0].slice(0, 90)})`);
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction(() => document.querySelector('#qs-rule-region-chip')?.hidden, null, { timeout: LONG });
|
||
check((await doc()) === KEPT.replace('x q[1];', 'x q[0];'), 'the region holds for that proposal and is then let go');
|
||
await page.fill('#qs-rule-gates', '');
|
||
await page.dispatchEvent('#qs-rule-gates', 'change');
|
||
}
|
||
|
||
// The classical-shortcut lens.
|
||
{
|
||
let RING = 'OPENQASM 3.0;\nqubit[16] q;\nbit[16] c;\nh q;\n';
|
||
for (let i = 0; i < 16; i++) RING += `cz q[${i}], q[${(i + 1) % 16}];\n`;
|
||
RING += 'rz(0.3) q;\nc = measure q;\n';
|
||
await setProgram(RING);
|
||
await page.click('#qs-shortcuts');
|
||
await page.waitForSelector('#qs-shortcuts-summary', { timeout: LONG });
|
||
const summary = await page.textContent('#qs-shortcuts-summary');
|
||
const cut = await page.textContent('#qs-routes tr[data-route=cutrank]');
|
||
check(/cut-rank sum/.test(summary) && /widest cut 2 bits/.test(cut) && await page.$('#qs-routes tr.best[data-route=cutrank]') && /does not apply/.test(await page.textContent('#qs-routes tr[data-route=stabilizer]')),
|
||
`the lens prices a Clifford ring with a closing layer by the cut-rank sum (${summary})`);
|
||
check(/not searched/.test(await page.textContent('#qs-routes tr[data-route=tensor_network]')), 'the quick check leaves the contraction search for a thorough one');
|
||
await page.click('#qs-shortcuts-thorough');
|
||
await page.waitForFunction(() => /2\^/.test(document.querySelector('#qs-routes tr[data-route=tensor_network]')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, `a thorough check searches a contraction tree (${(await page.textContent('#qs-routes tr[data-route=tensor_network]')).trim()})`);
|
||
await page.click('.qs-editor .cm-content');
|
||
await page.keyboard.press('ControlOrMeta+End');
|
||
await page.keyboard.type('\nh q[0];');
|
||
await page.waitForSelector('#qs-shortcuts-panel.stale .qs-stale-note', { timeout: LONG });
|
||
check(true, 'a check is marked stale once the program changes');
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.waitForFunction(() => /2 gates/.test(document.querySelector('#qs-measures')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-shortcuts');
|
||
await page.waitForFunction(() => /^Classically easy at any width/.test(document.querySelector('#qs-shortcuts-summary')?.textContent ?? '') && !document.querySelector('#qs-shortcuts-panel.stale'), null, { timeout: LONG });
|
||
check(true, 'a Clifford circuit is classically easy at any width');
|
||
}
|
||
|
||
// Predict first.
|
||
{
|
||
await page.selectOption('#qs-example', 'bell');
|
||
await page.check('#qs-predict-first');
|
||
const ran = (await page.$$('.qs-hitem')).length;
|
||
await page.click('#qs-run');
|
||
await page.waitForSelector('#qs-predict', { timeout: LONG });
|
||
check((await page.$$('.qs-hitem')).length === ran && !(await page.$('.qs-method')), 'with Predict first, the run waits for a prediction');
|
||
await page.fill('#qs-predict-bits', '000');
|
||
await page.fill('#qs-predict-share', '50');
|
||
await page.click('#qs-predict-run');
|
||
check(/2 classical bits; the outcome has 3/.test(await page.textContent('#qs-predict-note')), 'an outcome of the wrong width is refused, with the width');
|
||
await page.fill('#qs-predict-bits', '00');
|
||
await page.click('#qs-predict-run');
|
||
await done(page);
|
||
await page.waitForSelector('#qs-predicted', { timeout: LONG });
|
||
const pred = await page.textContent('#qs-predicted');
|
||
check(/you said00 in 50%/.test(pred) && /the run00 in \d/.test(pred) && /(came out on top|Another outcome)/.test(pred) && /first prediction/.test(pred), `the prediction is set against the counts (${pred})`);
|
||
await page.uncheck('#qs-predict-first');
|
||
}
|
||
// Generators: a program with integer inputs, read at the inputs bar's values. The
|
||
// Workbench checks a family at every size and keeps the cheapest of each cell.
|
||
const CHAIN = 'OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint i in [0:n-2] {\n cx q[i], q[i + 1];\n}\n';
|
||
const TREE = 'OPENQASM 3.0;\ninput uint n;\nqubit[n] q;\nh q[0];\nfor uint k in [0:n] {\n for uint i in [0:2**k - 1] {\n if (2**k < n && i + 2**k < n) {\n cx q[i], q[i + 2**k];\n }\n }\n}\n';
|
||
const lintSays = (re) => page.waitForFunction((re) => new RegExp(re).test(document.querySelector('#qs-lint')?.textContent ?? ''), re.source, { timeout: LONG });
|
||
const wbLog = () => page.$$eval('#qs-wb-log li', (ls) => ls.map((l) => l.textContent));
|
||
const wbIdle = () => page.waitForFunction(() => !document.querySelector('#qs-wb-start').disabled, null, { timeout: LONG });
|
||
{
|
||
await setProgram(CHAIN.replace('qubit[n] q;\n', 'qubit[n] q;\nbit[n] c;\n') + 'c = measure q;\n');
|
||
await page.waitForSelector('#qs-inputs:not([hidden]) #qs-input-n', { timeout: LONG });
|
||
await lintSays(/4 qubits · 4 gates at n = 4/);
|
||
check(true, 'a generator shows its input under the editor, and the lint reads it at n = 4');
|
||
await page.fill('#qs-input-n', '3');
|
||
await page.dispatchEvent('#qs-input-n', 'change');
|
||
await lintSays(/3 qubits · 3 gates at n = 3/);
|
||
await page.waitForFunction(() => /^The circuit: 3 qubits/.test(document.querySelector('#qs-diagram svg')?.getAttribute('aria-label') ?? ''), null, { timeout: LONG });
|
||
check(true, 'a new value re-reads it: linted and drawn at n = 3');
|
||
await page.click('#qs-run');
|
||
await done(page);
|
||
const got = await counts(page);
|
||
check(got.length === 2 && got.every(([k]) => k === '000' || k === '111'), `it runs at its value: a GHZ state on 3 qubits (${JSON.stringify(got)})`);
|
||
await page.fill('#qs-input-n', '4');
|
||
await page.dispatchEvent('#qs-input-n', 'change');
|
||
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-start');
|
||
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
|
||
const st = await page.textContent('#qs-wb-stats');
|
||
check(/Best e0: cost 17\.3 against the reference’s 17\.3, by reference/.test(st) && (await page.$$('#qs-wb-map .qs-ring')).length === 1,
|
||
`Start checks the reference at n = 3, 5 and 8 and seeds the archive with it (${st})`);
|
||
await page.click('#qs-wb-search');
|
||
await page.waitForFunction(() => /The blind search: 100 edits judged/.test(document.querySelector('#qs-wb-log')?.textContent ?? ''), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const after = await page.textContent('#qs-wb-stats');
|
||
check(/^101 evaluated/.test(after), `the blind search judges a hundred edits, each by the engine (${after})`);
|
||
await setProgram(TREE);
|
||
await page.click('#qs-wb-submit');
|
||
// What is new in it is said after the outcome, above it.
|
||
await page.waitForFunction(() => [...document.querySelectorAll('#qs-wb-log li')].some((l) => /^Your program: /.test(l.textContent)), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const said = (await wbLog()).find((t) => /^Your program: /.test(t));
|
||
check(/^Your program: kept as e\d+, cheaper than every generator before it \(cost 12\.3\)/.test(said) && /by person/.test(await page.textContent('#qs-wb-stats')),
|
||
`the reader's log-depth tree is kept as the cheapest, and named as theirs (${said})`);
|
||
const named = (await wbLog())[0];
|
||
check(/^e\d+: a known construction: GHZ, by doubling\.$/.test(named), `the tree is named for the known construction it is (${named})`);
|
||
await page.click('#qs-wb-map .qs-wb-best');
|
||
await page.waitForSelector('#qs-wb-open', { timeout: LONG });
|
||
const card = await page.textContent('#qs-wb-entry');
|
||
check(/cost 12\.3/.test(card) && /checked by \w+, sealed/.test(card) && /person/.test(card) && !/null/.test(card), `an entry shows its cost, how it was checked, its seal and who wrote it (${card.slice(0, 140)})`);
|
||
await page.waitForFunction(() => /a known construction/.test(document.querySelector('#qs-wb-novelty')?.textContent ?? ''), null, { timeout: LONG });
|
||
const novel = await page.textContent('#qs-wb-novelty');
|
||
check(/GHZ, by doubling/.test(novel) && /Better than the reference on what the cost does not weigh: two-qubit depth \d+ against \d+/.test(novel), `the entry says what it is and what it beats the reference on unasked (${novel})`);
|
||
// Your taste: two kept generators, which the engine proves make the same state, compared.
|
||
if (!(await page.$eval('#qs-taste', (d) => d.open))) await page.click('#qs-taste > summary');
|
||
await page.click('#qs-taste-compare');
|
||
await page.waitForSelector('.qs-taste-pick', { timeout: LONG });
|
||
const pair = await page.$$eval('.qs-taste-side', (ss) => ss.map((s) => s.dataset.name));
|
||
check(pair.length === 2 && pair[0] !== pair[1] && /proves they do the same thing \(state\)/.test(await page.textContent('#qs-taste-pair')), `Compare two offers the pair the profile is least sure of, proved equal (${pair.join(', ')})`);
|
||
await page.selectOption('#qs-taste-part', 'economy');
|
||
await page.click('.qs-taste-side >> nth=0 >> .qs-taste-pick');
|
||
await page.waitForSelector('#qs-taste-chosen', { timeout: LONG });
|
||
check(new RegExp(`^Kept: ${pair[0]} over ${pair[1]}, for its economy`).test(await page.textContent('#qs-taste-chosen')), `the choice is kept, with what it was about (${await page.textContent('#qs-taste-chosen')})`);
|
||
await page.waitForFunction(() => document.querySelector('#qs-taste-count')?.textContent === '2', null, { timeout: LONG });
|
||
check(/^2 choices \(economy 1\)/.test(await page.textContent('#qs-taste-summary')), `the profile counts its choices and their parts (${await page.textContent('#qs-taste-summary')})`);
|
||
await page.click('#qs-taste-rank');
|
||
await page.waitForSelector('#qs-taste-list li', { timeout: LONG });
|
||
const ranked = await page.$$eval('#qs-taste-list li', (ls) => ls.map((l) => l.textContent));
|
||
const kept = await page.evaluate(() => Object.keys(JSON.parse(localStorage.getItem('wai.quantum-studio.workbench')).archive.cells).length);
|
||
check(ranked.length === kept && ranked.every((t) => /typicality \d\.\d\d/.test(t) && /score -?\d/.test(t) && /cost \d/.test(t)), `Rank by my taste orders every kept generator, each with its typicality, score and cost (${ranked.length} of ${kept}: ${ranked[0]})`);
|
||
check(kept <= 3 || /The first 3 are \d\.\d\d apart in how they are built; by cost, \d\.\d\d/.test(await page.textContent('#qs-taste-ranked')), 'the ranking says whether it narrows what comes first, against the cost');
|
||
await page.evaluate(() => { window.qsListBefore = document.querySelector('#qs-taste-list'); });
|
||
await page.$eval('#qs-taste-dial', (d) => { d.value = '1'; d.dispatchEvent(new Event('change')); });
|
||
await page.waitForFunction(() => document.querySelector('#qs-taste-list') && document.querySelector('#qs-taste-list') !== window.qsListBefore, null, { timeout: LONG });
|
||
check((await page.$$('#qs-taste-list li')).length === kept, 'turning the dial ranks them again');
|
||
await page.$eval('#qs-taste-dial', (d) => { d.value = '0'; d.dispatchEvent(new Event('change')); });
|
||
await page.fill('#qs-taste-profile', 'teaching');
|
||
await page.dispatchEvent('#qs-taste-profile', 'change');
|
||
await page.waitForFunction(() => /^No choices yet/.test(document.querySelector('#qs-taste-summary')?.textContent ?? ''), null, { timeout: LONG });
|
||
check((await page.textContent('#qs-taste-count')) === '', 'a second profile is kept apart: it has no choices');
|
||
await page.fill('#qs-taste-profile', 'mine');
|
||
await page.dispatchEvent('#qs-taste-profile', 'change');
|
||
await page.waitForFunction(() => document.querySelector('#qs-taste-count')?.textContent === '2', null, { timeout: LONG });
|
||
check(true, 'and the first comes back with its choices');
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-open');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, TREE, { timeout: LONG });
|
||
check(/proposed by you/.test(await page.textContent('#qs-vs-list li.here')), 'Open in editor offers the kept generator, recorded as its proposer’s, the reader’s');
|
||
await page.click('#qs-wb-ask');
|
||
check(/needs one/.test((await wbLog())[0]), 'Ask the model, with none connected, says it needs one');
|
||
const files = [];
|
||
page.on('download', (d) => files.push(d));
|
||
await page.click('#qs-wb-save');
|
||
await page.waitForFunction(() => /^Saved workbench-/.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.waitForTimeout(500);
|
||
const read = async (d) => JSON.parse(readFileSync(await d.path(), 'utf8'));
|
||
const [specFile, archiveFile] = [files.find((d) => !/archive/.test(d.suggestedFilename())), files.find((d) => /\.archive\.json$/.test(d.suggestedFilename()))];
|
||
const savedSpec = specFile && await read(specFile), savedArchive = archiveFile && await read(archiveFile);
|
||
check(savedSpec?.reference === CHAIN && savedArchive?.spec?.length === 64 && savedArchive.entries.length >= 2 && savedArchive.entries.every((e) => e.sealed?.receipt)
|
||
&& archiveFile.suggestedFilename() === specFile.suggestedFilename().replace(/\.json$/, '.archive.json'),
|
||
`Download saves the specification and its archive side by side, every entry sealed, as the command line reads them (${files.map((d) => d.suggestedFilename()).join(', ')})`);
|
||
await page.reload({ waitUntil: 'domcontentloaded' });
|
||
await page.waitForFunction(() => /Taken up again: 102 evaluated/.test(document.querySelector('#qs-wb-log')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(/by person/.test(await page.textContent('#qs-wb-stats')) && (await page.inputValue('#qs-wb-sizes')) === 'n = 3, 5, 8', 'the archive and its specification come back with the page');
|
||
await lintSays(/qubits/);
|
||
check((await page.textContent('#qs-taste-count')) === '2' && (await page.inputValue('#qs-taste-profile')) === 'mine', 'the taste profiles come back with the page');
|
||
check(/^\d+ flagged · 1 saved$/.test(await page.textContent('#qs-found-count')) && (await page.inputValue('#qs-found-list .qs-found-item.saved input')) === 'the ladder, read backwards', 'what Found keeps comes back with the page, with its note');
|
||
// On a line, the tree breaks only the rule that gates join neighbours: an aberration.
|
||
if (!(await page.$eval('#qs-wb-spec', (d) => d.open))) await page.click('#qs-wb-spec > summary');
|
||
await page.selectOption('#qs-wb-conn', 'line');
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-start');
|
||
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
|
||
await setProgram(TREE);
|
||
await page.click('#qs-wb-submit');
|
||
await page.waitForFunction(() => /an aberration/.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const ab = (await wbLog())[0];
|
||
check(/^Your program: an aberration\. It breaks only one rule \(at n = 3: `cx` acts on qubits \[0, 2\], which are not neighbours on the line\); without it, it would be verified \(\w+\) at cost 12\.3/.test(ab) && /Try without that rule/.test(ab),
|
||
`a generator that breaks only the line's rule is an aberration, with the cost it would have (${ab})`);
|
||
check(/not verified/.test((await wbLog())[1]) && /\b1 kept\b/.test(await page.textContent('#qs-wb-stats')), 'an aberration is never kept');
|
||
check(await page.$('#qs-found-list .qs-found-item[data-kind=aberration]'), 'Found flags the aberration');
|
||
await page.click('#qs-wb-log .qs-wb-try');
|
||
check((await page.inputValue('#qs-wb-conn')) === 'all' && /lets a gate join any pair/.test(await page.textContent('#qs-wb-stats')), 'Try without that rule loosens the specification, to start again under it');
|
||
}
|
||
|
||
// The assistant, against a scripted model server: it calls a tool, proposes a program
|
||
// the engine cannot read, gets the engine's reason back, and proposes a corrected one,
|
||
// which the Studio checks and runs before offering it.
|
||
{
|
||
const asked = [];
|
||
const GOOD = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n';
|
||
const model = createServer((req, res) => {
|
||
const cors = { 'access-control-allow-origin': '*', 'access-control-allow-headers': 'content-type', 'access-control-allow-methods': 'GET, POST, OPTIONS' };
|
||
if (req.method === 'OPTIONS') { res.writeHead(204, cors).end(); return; }
|
||
if (req.url.endsWith('/models')) { res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ data: [{ id: 'scripted' }] })); return; }
|
||
let body = '';
|
||
req.on('data', (c) => (body += c));
|
||
req.on('end', () => {
|
||
const r = JSON.parse(body);
|
||
asked.push(r);
|
||
const msgs = r.messages;
|
||
// Alternatives' calls carry no tools: candidates, one of them wrong on purpose.
|
||
if (!r.tools) {
|
||
const want = msgs[1].content;
|
||
const content = /^Write \d+ generators/.test(want)
|
||
? `Idea: double the qubits that hold the state each layer.\n\`\`\`qasm\n${TREE}\`\`\`\nIdea: forget the Hadamard.\n\`\`\`qasm\n${CHAIN.replace('h q[0];\n', '')}\`\`\`\nIdea: use CZ.\n\`\`\`qasm\n${CHAIN.replace('cx q[i], q[i + 1];', 'h q[i + 1];\n cz q[i], q[i + 1];\n h q[i + 1];')}\`\`\``
|
||
: /^Write \d+ programs that are the same circuit/.test(want)
|
||
? 'Idea: the two Hadamards cancel, and the X moves past the CNOT.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nx q[1];\nc = measure q;\n```\nIdea: drop the X.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\ncx q[0], q[1];\nc = measure q;\n```'
|
||
: 'Idea: a GHZ state on three qubits.\n```qasm\nOPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\nc = measure q;\n```\n**Idea:** entangle with CZ instead.\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[1];\ncz q[0], q[1];\nh q[1];\nc = measure q;\n```';
|
||
res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message: { role: 'assistant', content }, finish_reason: 'stop' }] }));
|
||
return;
|
||
}
|
||
const told = msgs.some((m) => m.role === 'user' && /could not read the program you proposed/.test(m.content));
|
||
const toolDone = msgs.some((m) => m.role === 'tool');
|
||
let message;
|
||
if (!toolDone) message = { role: 'assistant', content: '', tool_calls: [{ id: 'c1', type: 'function', function: { name: 'lint_qasm', arguments: JSON.stringify({ qasm: GOOD }) } }] };
|
||
else if (!told) message = { role: 'assistant', content: 'It entangles two qubits. A variant:\n```qasm\nOPENQASM 3.0;\nqubit[2] q;\nfoo q[0];\n```' };
|
||
else message = { role: 'assistant', content: 'Corrected:\n```qasm\n' + GOOD + '```' };
|
||
res.writeHead(200, { ...cors, 'content-type': 'application/json' }).end(JSON.stringify({ choices: [{ message, finish_reason: message.tool_calls ? 'tool_calls' : 'stop' }], usage: { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120 } }));
|
||
});
|
||
});
|
||
await new Promise((r) => model.listen(0, '127.0.0.1', r));
|
||
await page.click('#qs-ai-gear');
|
||
await page.fill('#qs-ai-endpoint', `http://127.0.0.1:${model.address().port}/v1`);
|
||
await page.click('#qs-ai-connect');
|
||
await page.waitForFunction(() => /connected · scripted/.test(document.querySelector('#qs-ai-state')?.textContent ?? ''), null, { timeout: LONG });
|
||
check(true, 'the assistant connects to a model server and reads its models');
|
||
await page.click('.qs-ai-quick button >> text=Explain this circuit');
|
||
await page.waitForSelector('.qs-ai-apply', { timeout: LONG });
|
||
const log = await page.$$eval('#qs-ai-log > li', (ls) => ls.map((l) => l.textContent));
|
||
check(log.some((t) => t.includes('used lint_qasm')), 'the model\'s tool call ran on the engine, and the log shows it');
|
||
check(log.some((t) => /could not read the proposed program \(line 3: unknown gate foo\)/.test(t)), `a proposal the engine cannot read goes back with the engine's reason (${log.find((t) => /could not read/.test(t)) ?? 'none'})`);
|
||
const proposal = await page.textContent('.qs-ai-proposal');
|
||
check(/checked: 2 qubits, 2 gates/.test(proposal) && /ran here on Stabilizer tableau/.test(proposal), `the corrected proposal is checked and run before it is offered (${proposal})`);
|
||
check(asked.length === 3 && asked.every((r) => r.tools?.some((t) => t.function.name === 'run_job') && r.messages[0].role === 'system' && /Never state a probability/.test(r.messages[0].content)), 'every call to the model carries the engine\'s tools and the rule that numbers come from them');
|
||
const toolMsg = asked[1].messages.find((m) => m.role === 'tool');
|
||
check(toolMsg && JSON.parse(toolMsg.content).qubits === 2, 'the tool\'s answer went back to the model');
|
||
await page.click('.qs-ai-apply');
|
||
await page.waitForSelector('#qs-review:not([hidden]) #qs-review-apply', { timeout: LONG });
|
||
check(/A proposal from the assistant \(scripted\)/.test(await page.textContent('#qs-review')), 'Apply offers the checked program as the assistant’s proposal, named');
|
||
await page.click('#qs-review-apply');
|
||
await page.waitForFunction((q) => window.quantumStudio.view.state.doc.toString() === q, GOOD, { timeout: LONG });
|
||
check(true, 'taken, it is the program in the editor');
|
||
check((await page.inputValue('#qs-ai-stance')) === 'questions' && (await page.inputValue('#qs-ai-initiative')) === 'asked' && /ask one short question/.test(asked[0].messages[0].content),
|
||
'by default the assistant asks questions first and speaks only when asked');
|
||
const quick = await page.$$eval('.qs-ai-quick button', (bs) => bs.map((b) => b.textContent));
|
||
check(quick.includes('Ask me a question') && quick.includes('Hint'), `it can be asked for a question or a hint (${quick.join(', ')})`);
|
||
check(asked.every((r) => !r.tools || r.tools.some((t) => t.function.name === 'critique_qasm') && r.tools.some((t) => t.function.name === 'check_equivalent')), 'its tools include the critique and the equivalence check');
|
||
// Runs refused twice: no hint offered while it speaks only when asked; offered, not sent, when allowed.
|
||
const calls = asked.length;
|
||
await setProgram('OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n');
|
||
for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); }
|
||
check(await page.isHidden('#qs-ai-stuck'), 'while it speaks only when asked, refused runs offer no hint');
|
||
await page.selectOption('#qs-ai-initiative', 'hint');
|
||
for (let i = 0; i < 2; i++) { await page.click('#qs-run'); await done(page); }
|
||
check(await page.isVisible('#qs-ai-stuck') && asked.length === calls, 'allowed to, it offers a hint after refused runs, and asks the model nothing');
|
||
await page.selectOption('#qs-ai-initiative', 'asked');
|
||
// Improve and Explore with the model: the engine reads, measures and compares each.
|
||
// A rule the model is told: text to avoid.
|
||
if (!(await page.$eval('#qs-rules', (d) => d.open))) await page.click('#qs-rules > summary');
|
||
await page.fill('#qs-rule-text', 'measure q[5]');
|
||
await page.dispatchEvent('#qs-rule-text', 'change');
|
||
const DRAFT = 'OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\nh q[0];\nx q[1];\ncx q[0], q[1];\nc = measure q;\n';
|
||
await setProgram(DRAFT);
|
||
await page.click('#qs-improve');
|
||
await page.waitForFunction(() => document.querySelectorAll('.qs-cand').length && !/looking/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG });
|
||
const cards = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent));
|
||
const refused = await page.textContent('#qs-alt-refused').catch(() => '');
|
||
check(cards.some((t) => /the model/.test(t) && /the two Hadamards cancel/.test(t) && /same circuit · equal tableaus/.test(t)) && /drop the X: not the same circuit/.test(refused),
|
||
`Improve with the model: its rewrite checked the same circuit, its wrong one left out with the reason (${refused})`);
|
||
await page.fill('#qs-explore-aim', 'another entangler');
|
||
await page.click('#qs-explore');
|
||
await page.waitForFunction(() => /a GHZ state/.test(document.querySelector('#qs-alt-body')?.textContent ?? ''), null, { timeout: LONG });
|
||
const ideas = await page.$$eval('.qs-cand', (cs) => cs.map((c) => c.textContent));
|
||
const muse = asked.filter((r) => !r.tools).map((r) => r.messages[1].content);
|
||
check(ideas.length === 2 && ideas.some((t) => /3 qubits/.test(t) && /a different circuit/.test(t)) && ideas.some((t) => /entangle with CZ instead/.test(t)) && muse.some((m) => /toward this aim: another entangler/.test(m)),
|
||
`Explore: the model's ideas, each read and measured by the engine (${ideas.map((t) => t.slice(0, 60)).join(' | ')})`);
|
||
check(ideas.some((t) => /a GHZ state on three qubits/.test(t) && /a known construction: GHZ, by a chain of CNOTs/.test(t)) && ideas.some((t) => /entangle with CZ instead/.test(t) && /a different route to GHZ, by a chain of CNOTs/.test(t)),
|
||
'each idea says what is new in it: the chain is a known construction, the CZ one a different route to it');
|
||
check(muse.length >= 2 && muse.every((m) => m.includes('Add no line holding `measure q[5]`.')), 'the model is told the reader’s rules with every request for ideas');
|
||
await page.fill('#qs-rule-text', '');
|
||
await page.dispatchEvent('#qs-rule-text', 'change');
|
||
check((await doc()) === DRAFT, 'neither changes the program until one is applied');
|
||
// The Workbench asks the model for generators, and the engine judges each at every size.
|
||
await setProgram(CHAIN);
|
||
await page.click('#qs-wb-start');
|
||
await page.waitForFunction(() => /^1 evaluated/.test(document.querySelector('#qs-wb-stats')?.textContent ?? ''), null, { timeout: LONG });
|
||
await page.click('#qs-wb-ask');
|
||
await page.waitForFunction(() => /^use CZ: /.test(document.querySelector('#qs-wb-log li')?.textContent ?? ''), null, { timeout: LONG });
|
||
await wbIdle();
|
||
const wlog = await wbLog();
|
||
check(wlog.some((t) => /^double the qubits that hold the state each layer: kept as e\d+, cheaper than every generator before it/.test(t))
|
||
&& wlog.some((t) => /^forget the Hadamard: not verified: at n = 3: different/.test(t))
|
||
&& wlog.some((t) => /^use CZ: not verified: at n = 3: it uses `cz`, which the specification does not allow/.test(t)),
|
||
`the model's generators, each judged at every size: one kept, a wrong one and a forbidden gate left out (${wlog.slice(0, 3).join(' | ')})`);
|
||
check(wlog.some((t) => /^use CZ: an aberration\. It breaks only one rule \(at n = 3: it uses `cz`/.test(t)) && !wlog.some((t) => /^forget the Hadamard: an aberration/.test(t)),
|
||
'the forbidden gate that would be verified without the rule is an aberration; the wrong one is not');
|
||
const brief = asked.filter((r) => !r.tools).map((r) => r.messages[1].content).find((m) => /^Write 3 generators/.test(m)) ?? '';
|
||
check(/prepare the same state from \|0…0⟩/.test(brief) && /n = 3, 5, 8/.test(brief) && /only these gates: h, cx/.test(brief) && brief.includes('cx q[i], q[i + 1];') && /The cheapest found: cost 17\.3, the reference\./.test(brief),
|
||
'the model is told the specification and shown the cheapest found, with the cost the engine measured');
|
||
check(/by model: scripted/.test(await page.textContent('#qs-wb-stats')), 'the best is the model’s, and named as its');
|
||
await setProgram(DRAFT);
|
||
// The candidates come back within the editor's redraw delay: wait for the draft's diagram.
|
||
await page.waitForFunction(() => /4 gates/.test(document.querySelector('#qs-lint')?.textContent ?? '') && document.querySelectorAll('#qs-diagram .qc-flag').length === 2, null, { timeout: LONG });
|
||
check(!/null/.test(await page.textContent('#qs-alt-body')) && !/null/.test(await page.textContent('#qs-measures')), 'no empty part is written out as “null”');
|
||
await page.screenshot({ path: join(SHOTS, 'studio-alternatives.png'), fullPage: true });
|
||
model.close();
|
||
}
|
||
check(seen.errors.length === 0, `no console error${seen.errors.length ? ': ' + seen.errors.join(' | ') : ''}`);
|
||
check(seen.failed.length === 0, `no failed request${seen.failed.length ? ': ' + seen.failed.join(' | ') : ''}`);
|
||
await ctx.close();
|
||
|
||
{
|
||
const { ctx, page } = await open({ width: 390, height: 844 });
|
||
await page.waitForFunction(() => /qubits/.test(document.querySelector('#qs-lint')?.textContent ?? ''), null, { timeout: LONG });
|
||
const over = await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth);
|
||
check(over <= 0, `at 390 px nothing scrolls sideways (overflow ${over} px)`);
|
||
await ctx.close();
|
||
}
|
||
|
||
await browser.close();
|
||
server.close();
|
||
console.log(`\n${fails === 0 ? 'every check passed' : `${fails} check(s) failed`}`);
|
||
process.exit(fails === 0 ? 0 : 1);
|