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