'use strict'; // directory-search widget: references a directory-board by id and renders an // interactive walk-up search page. Verifies the source board's entries are // safely inlined for client-side filtering, that a missing/wrong source shows a // friendly fallback (not a 500), and that entry/category text can't break out // of the inlined ', entries: [{ identifier: '', name: '">bold', subtitle: 'amp & lt < gt >', available: false }], }], }); seed('search_xss', 'directory-search', { source_widget_id: 'board_xss' }); const { status, html } = await fetchRender('search_xss'); assert.equal(status, 200); assert.ok(!html.includes(' breakout neutralized'); assert.ok(html.includes('\\u003c/script>'), 'angle brackets escaped in the inlined JSON blob'); }); // ---- live sync: GET /:id/data.json feed the search page polls ---- const fetchData = async (id) => fetch(`${base}/api/widgets/${id}/data.json`); test('data.json returns the source board categories, CORS-open for polling', async () => { const r = await fetchData('board1'); // seeded in the first test assert.equal(r.status, 200); assert.equal(r.headers.get('access-control-allow-origin'), '*', 'readable from a null-origin sandboxed iframe'); assert.equal(r.headers.get('cache-control'), 'no-store'); const body = await r.json(); assert.ok(Array.isArray(body.categories) && body.categories.length === 2, 'returns the categories array'); assert.equal(body.categories[0].entries[0].name, 'Acme Co'); }); test('data.json 404s for a missing widget (poll keeps last-good data)', async () => { assert.equal((await fetchData('does-not-exist')).status, 404); }); test('data.json 404s for a non-directory-board widget', async () => { assert.equal((await fetchData('clockX')).status, 404); // clockX seeded earlier }); test('search page wires the live-sync poll to its source board', async () => { const { html } = await fetchRender('search1'); assert.ok(html.includes('"source_widget_id":"board1"'), 'source board id inlined into the page'); assert.ok(html.includes('/data.json'), 'page polls the data.json feed'); }); // The on-screen keyboard must be sized against the VIEWPORT, not in fixed px. A panel's CSS // viewport is its resolution over its density, so a 1080p screen at 240dpi presents 1280x720 CSS // px — where a keyboard laid out for 1920x1080 ate ~37% of the height instead of ~24%. The clamp() // maxima are the ORIGINAL fixed values, so a 1080-tall viewport must stay pixel-identical. test('the on-screen keyboard scales with the viewport instead of using fixed pixels', async () => { const bid = 'kb-board', sid = 'kb-search'; seed(bid, 'directory-board', BOARD); seed(sid, 'directory-search', { source_widget_id: bid }); const { status, html } = await fetchRender(sid); assert.equal(status, 200); const keyRule = html.match(/\.key\s*\{[^}]*\}/s); assert.ok(keyRule, '.key rule is present'); assert.match(keyRule[0], /height:clamp\(/, 'key height is clamped to the viewport'); assert.match(keyRule[0], /font-size:clamp\(/, 'key font-size is clamped to the viewport'); assert.ok(!/height:\s*56px/.test(keyRule[0]), 'no bare fixed height survives'); // vh terms must exceed their max at 1080 tall, so existing 1080 panels render unchanged. const h = keyRule[0].match(/height:clamp\(([\d.]+)px,\s*([\d.]+)vh,\s*([\d.]+)px\)/); assert.ok(h, 'height clamp is well-formed'); const [, hMin, hVh, hMax] = h.map(Number); assert.ok(hVh * 1080 / 100 >= hMax, 'at a 1080-tall viewport the height clamps to its max (no visual change)'); assert.ok(hVh * 720 / 100 < hMax, 'at a 720-tall viewport the height actually scales down'); assert.ok(hMin >= 30, 'keys stay tappable on very short viewports'); }); test('the narrow breakpoint no longer pins the key size back to fixed pixels', async () => { const bid = 'kb2-board', sid = 'kb2-search'; seed(bid, 'directory-board', BOARD); seed(sid, 'directory-search', { source_widget_id: bid }); const { html } = await fetchRender(sid); const mq = html.match(/@media \(max-width:700px\)\s*\{[^}]*\}[^}]*\}/s); assert.ok(mq, 'the narrow breakpoint still exists'); assert.ok(!/\.key\s*\{[^}]*height:\s*\d+px/.test(mq[0]), 'the breakpoint must not re-pin .key to a fixed height and undo the clamp'); });