Skip to content

Commit 97c8af9

Browse files
committed
test(web): the end-to-end check also covers Keep and Undo, and runs against a release
Undo of a file the agent created removes it, and so does everything else still pending from the session; Keep clears the bar. The account card that sign-in leaves open is closed the way a user would, before the chat is clicked.
1 parent 0ebbca1 commit 97c8af9

2 files changed

Lines changed: 30 additions & 3 deletions

File tree

‎web/test/e2e.mjs‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ try {
103103
let hardened = /webEndpointUrlTemplate/.test(args.product || '');
104104
if (DIST) { try { hardened = /\(same origin/.test(fs.readFileSync(path.join(DIST, 'build.json'), 'utf8')) ? false : true; } catch { /* keep */ } }
105105
const scratchFile = hardened ? (async (p) => {
106-
await ask('Please read the greeting');
106+
await ask('Please read ' + p.replace(/^\/scratch\//, ''));
107107
const t = lastToolResult();
108108
return /ERROR/.test(t) ? null : t;
109109
}) : (p) => page.eval(`new Promise((res, rej) => { const r = indexedDB.open('levelcode-scratch'); r.onsuccess = () => { const g = r.result.transaction('nodes').objectStore('nodes').get(${JSON.stringify(p)}); g.onsuccess = () => res(g.result ? new TextDecoder().decode(g.result.data) : null); g.onerror = () => rej(g.error); }; r.onerror = () => rej(r.error); })`);
@@ -146,6 +146,11 @@ try {
146146
/* 3. chat and agent */
147147
await activateTab(/LevelCode AI/);
148148
await page.waitFor(async () => (await inChat('!!d.getElementById("send")')) === true, { ms: 30000, label: 'chat webview' });
149+
// Signing in leaves the account card open over the chat, as it does on the desktop; close it like a user would.
150+
if ((await inChat(`getComputedStyle(d.getElementById('acctClose')).display !== 'none' && d.getElementById('acctClose').offsetParent !== null`)) === true) {
151+
await clickInChat('#acctClose');
152+
await sleep(400);
153+
}
149154
check('the chat webview renders from the editor', true);
150155
/** Send a prompt and wait for the agent to finish its turns (a tool call, then its answer). */
151156
const idle = () => inChat(`!d.getElementById('send').classList.contains('stop')`);
@@ -197,7 +202,25 @@ try {
197202
const entries = await page.eval(`[...document.querySelectorAll('.quick-input-list .monaco-list-row')].map((e) => e.getAttribute('aria-label') || e.textContent)`);
198203
check('Quick Open finds files in the scratch workspace', entries.some((t) => /hello\.txt/.test(t)), JSON.stringify(entries.slice(0, 4)));
199204
await page.key('Escape', 'Escape');
205+
await page.waitFor(() => page.eval(`(() => { const w = document.querySelector('.quick-input-widget'); return !w || getComputedStyle(w).display === 'none'; })()`), { ms: 5000, label: 'quick open to close' }).catch(() => null);
200206
} else { check('Quick Open finds files in the scratch workspace', false, 'the quick open widget did not appear'); }
207+
await activateTab(/LevelCode AI/); // focus goes back to the chat before it is clicked
208+
209+
// Review: an agent edit is applied at once and offered for Keep or Undo. Undo of a file the agent created removes it.
210+
await ask('Please make a notes file');
211+
check('the agent created notes.md', (await scratchFile('/scratch/notes.md')) === 'notes\n');
212+
await page.waitFor(async () => (await inChat(`getComputedStyle(d.getElementById('reviewBar')).display !== 'none'`)) === true, { ms: 10000, label: 'review bar' }).catch(() => null);
213+
check('the chat offers Keep and Undo for it', (await inChat(`getComputedStyle(d.getElementById('reviewBar')).display !== 'none' && !!d.querySelector('#reviewBar .keepall') && !!d.querySelector('#reviewBar .undoall')`)) === true);
214+
await shot(page, '3c-review');
215+
await clickInChat('#reviewBar .undoall');
216+
await sleep(2000);
217+
check('Undo all removes the file the agent created', (await scratchFile('/scratch/notes.md')) === null);
218+
check('and everything else still pending: hello.txt, created and edited earlier in the session, is gone too', (await scratchFile('/scratch/hello.txt')) === null);
219+
await ask('Please make a notes file');
220+
await page.waitFor(async () => (await inChat(`getComputedStyle(d.getElementById('reviewBar')).display !== 'none'`)) === true, { ms: 10000, label: 'review bar' }).catch(() => null);
221+
await clickInChat('#reviewBar .keepall');
222+
await sleep(1500);
223+
check('Keep all keeps it, and the review bar goes away', (await scratchFile('/scratch/notes.md')) === 'notes\n' && (await inChat(`getComputedStyle(d.getElementById('reviewBar')).display === 'none'`)) === true);
201224

202225
/* 4. a reload keeps the session and the files */
203226
const before = stub.state.log.filter((e) => e.path === '/api/levelcode/v1/account/models' || e.path === '/api/levelcode/v1/account/profile').length;
@@ -206,7 +229,7 @@ try {
206229
await page.waitFor(() => stub.state.log.filter((e) => e.path === '/api/levelcode/v1/account/models' || e.path === '/api/levelcode/v1/account/profile').length > before, { ms: 30000, label: 'account call after reload' }).catch(() => null);
207230
const after = stub.state.log.filter((e) => e.path === '/api/levelcode/v1/account/models' || e.path === '/api/levelcode/v1/account/profile').length;
208231
check('after a reload the editor is still signed in', after > before, `${before} -> ${after}`);
209-
check('after a reload the scratch file is still there', (await scratchFile('/scratch/hello.txt')) === 'hello again\n');
232+
check('after a reload the file that was kept is still there', (await scratchFile('/scratch/notes.md')) === 'notes\n');
210233
await shot(page, '4-reload');
211234
} catch (e) {
212235
check('the run completed', false, String((e && e.stack) || e));

‎web/test/stub-backend.mjs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ export function defaultModel(messages) {
5252
if (/\bcreate\b/i.test(text)) { return call('write_file', { path: 'hello.txt', content: 'hello from the browser\n', explanation: 'Create the greeting file' }, 'I will create the file now.'); }
5353
if (/\blist\b/i.test(text)) { return call('list_files', {}, 'Let me look at the files.'); }
5454
if (/\bsearch\b/i.test(text)) { return call('search', { query: 'hello', explanation: 'Find the greeting' }, 'Searching.'); }
55-
if (/\bread\b/i.test(text)) { return call('read_file', { path: 'hello.txt', explanation: 'Read the greeting file' }, 'Reading it.'); }
55+
if (/\bmake\b/i.test(text)) { return call('write_file', { path: 'notes.md', content: 'notes\n', explanation: 'Make the notes file' }, 'Making it.'); }
56+
if (/\bread\b/i.test(text)) {
57+
const named = text.match(/read\s+(?:the\s+)?(\S+\.\w+)/i);
58+
return call('read_file', { path: named ? named[1] : 'hello.txt', explanation: 'Read the file' }, 'Reading it.');
59+
}
5660
if (/\bchange\b/i.test(text)) { return call('edit_file', { path: 'hello.txt', old_str: 'hello from the browser', new_str: 'hello again', explanation: 'Change the greeting' }, 'Changing it.'); }
5761
return { text: 'Hello from the stand-in gateway.' };
5862
}

0 commit comments

Comments
 (0)