oficina / sheets Public

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
Paul Campbell pushed by paul@keymail.dev ce9790673b7b408ce3c16016306c0926ab4bdf4a parent 37b90f6
1 file changed, +43 −0
  • tools/drive/chrome.mjs +43 −0

CI cancelled — superseded by a newer push — run details

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');