test(drive): cover the read-only viewer's page
The chrome is gated on MayWrite and nothing was checking what that produces. The failure it guards against is not a crash: it is a viewer handed a toolbar whose every control 404s, which reads as a broken app rather than as a permission. Seven checks in a second browser context with its own session: the grid still works, there is no toolbar, Edit and Format are absent while File remains, the formula bar is read-only, and ā the one that is easy to break while fixing the others ā cells still paint their formats. Being read-only is about writing, not about seeing; a viewer must still get the document as its author wrote it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0182f7QfojuuqdS4Xd2fYFaL
1 file changed,
+43
−0
tools/drive/chrome.mjs+43 −0
diff --git a/tools/drive/chrome.mjs b/tools/drive/chrome.mjs| index 41a5bf1..9f1351c 100644 |
| --- a/tools/drive/chrome.mjs |
| +++ b/tools/drive/chrome.mjs |
| @@ -213,6 +213,49 @@ for (const w of [320, 768]) { |
| check('no console errors', errors.length === 0, errors.slice(0, 3).join(' | ')); |
| +/* A viewer's page, in a second context with its own session. |
| + * |
| + * The chrome is gated on MayWrite, and the failure this guards against |
| + * is not a crash: it is a viewer being handed a toolbar whose every |
| + * control 404s, which looks like a broken app rather than like a |
| + * permission. A control somebody may not use should be absent. */ |
| +console.log('\nā the read-only viewer'); |
| +await page.setViewportSize({ width: 1440, height: 900 }); |
| +await page.goto(`${url}/share`); |
| +await page.fill('input[name=address]', 'ada@elsewhere.example'); |
| +await page.selectOption('select[name=role]', 'viewer').catch(() => {}); |
| +await Promise.all([ |
| + page.waitForLoadState('networkidle'), |
| + page.click('form[action$="/share"] button[type=submit]'), |
| +]); |
| + |
| +const viewerCtx = await browser.newContext(); |
| +const vp = await viewerCtx.newPage(); |
| +const vErrors = []; |
| +vp.on('pageerror', (e) => vErrors.push(String(e))); |
| +await vp.goto(`${base}/signin`); |
| +await vp.fill('input[type=email]', 'ada@elsewhere.example'); |
| +await Promise.all([vp.waitForLoadState('networkidle'), vp.click('button[type=submit]')]); |
| +const vm = [...fs.readFileSync(LOG, 'utf8').matchAll(/http:\/\/127\.0\.0\.1:8799\/auth\/verify\?[^\s\\"]+/g)].pop(); |
| +await vp.goto(vm[0]); |
| +await vp.goto(url); |
| +await vp.waitForFunction(() => window.SheetsGrid && window.SheetsGrid.worksheet(), null, { timeout: 15000 }).catch(() => {}); |
| +await vp.waitForTimeout(600); |
| + |
| +const vMenus = await vp.evaluate(() => |
| + Array.from(document.querySelectorAll('[data-menubar] > details > summary')).map((s) => s.textContent.trim())); |
| +check('a viewer still gets a working grid', await vp.evaluate(() => !!(window.SheetsGrid && window.SheetsGrid.worksheet()))); |
| +check('...with no toolbar', !(await vp.isVisible('[data-toolbar]')), 'a viewer was offered controls that 404'); |
| +check('...no Edit or Format menu', !vMenus.includes('Edit') && !vMenus.includes('Format'), `menus: ${vMenus.join(', ')}`); |
| +check('...but File is still there', vMenus.includes('File')); |
| +check('...a read-only formula bar', await vp.evaluate(() => document.querySelector('[data-formula-input]').readOnly)); |
| +/* Read-only is about writing, not about seeing. A viewer must still get |
| + * the document as authored. */ |
| +check('...and cells that still paint their formats', |
| + await vp.evaluate(() => document.querySelectorAll('[data-sheet-grid] td[data-b],[data-sheet-grid] td[data-fill],[data-sheet-grid] td[data-fx]').length > 0)); |
| +check('...no console errors on the viewer page', vErrors.length === 0, vErrors.slice(0, 2).join(' | ')); |
| +await vp.screenshot({ path: `${OUT}/sheet-viewer.png` }); |
| + |
| await browser.close(); |
| proxy.close(); |
| console.log(fails.length ? `\n${fails.length} FAILED\n` + fails.map((f) => ' ' + f).join('\n') : '\nall checks passed'); |