import { test, expect } from '@playwright/test'; import * as path from 'path'; // shared/cap.js — the "who can?" helpers (denyHint / whoCan) + handleForbidden // enrichment. cap.js is bundled into every server-mode tool; tables.html is a // convenient host. Pure helpers run fine on a file:// page (cap.at short-circuits // offline, but denyHint/whoCan/handleForbidden don't need the network). const HOST = 'file://' + path.resolve('tables/dist/tables.html'); async function load(page) { await page.goto(HOST, { waitUntil: 'load' }); await page.waitForFunction(() => window.zddc && window.zddc.cap && window.zddc.cap.denyHint); } test.describe('cap.js — who-can hints', () => { test('denyHint is role-first with people as the tooltip detail', async ({ page }) => { await load(page); const h = await page.evaluate(() => { const view = { path_who_can: { c: { roles: ['document_controller'], people: ['alice@example.com', 'bob@example.com'] } } }; return window.zddc.cap.denyHint(view, 'c'); }); expect(h.text).toBe('Only the document controller can create here.'); // role-first, humanized expect(h.title).toBe('alice@example.com, bob@example.com'); // people in the tooltip }); test('denyHint names people when no role grants the verb', async ({ page }) => { await load(page); const h = await page.evaluate(() => window.zddc.cap.denyHint({ path_who_can: { w: { people: ['sam@example.com'] } } }, 'w')); expect(h.text).toBe('Ask sam@example.com to write here.'); }); test('denyHint falls back to "an administrator" when nobody is named', async ({ page }) => { await load(page); const h = await page.evaluate(() => window.zddc.cap.denyHint({ path_who_can: {} }, 'd')); expect(h.text).toBe('Ask an administrator to delete here.'); }); test('whoCan reads either a path view or a 403 body', async ({ page }) => { await load(page); const r = await page.evaluate(() => { const fromView = window.zddc.cap.whoCan({ path_who_can: { c: { roles: ['r1'] } } }, 'c'); const fromBody = window.zddc.cap.whoCan({ missing_verb: 'c', who_can: { roles: ['r2'] } }, 'c'); const miss = window.zddc.cap.whoCan({ path_who_can: { w: {} } }, 'c'); return { fromView: fromView && fromView.roles[0], fromBody: fromBody && fromBody.roles[0], miss }; }); expect(r.fromView).toBe('r1'); expect(r.fromBody).toBe('r2'); expect(r.miss).toBeNull(); }); test('handleForbidden appends the who-can hint from the 403 body', async ({ page }) => { await load(page); const msg = await page.evaluate(async () => { let captured = ''; window.zddc.toast = (m) => { captured = m; return document.createElement('div'); }; const body = JSON.stringify({ error: 'Forbidden', missing_verb: 'c', who_can: { roles: ['document_controller'], people: ['alice@example.com'] } }); const resp = new Response(body, { status: 403, headers: { 'Content-Type': 'application/json' } }); await window.zddc.cap.handleForbidden(resp, { context: 'Create' }); return captured; }); expect(msg).toContain('You do not have create access here.'); expect(msg).toContain('Only the document controller can create here.'); // who-can appended }); });