Back to Black Site Map Editor
SOURCE / PINNED RELEASE

Made of little things.

Black Site Map Editor

Release
32a57733b7e9…
Author-recorded commit
3f54890f542f…
License
LICENSE
Author’s source reference
nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/black-site-map-editor

Archive hash verified: 7a4b1abbac7db5b0…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

tests/editor-ui.mjs
// Browser regression evidence. Fake domains belong to this test host, never the napplet.
// Run after pnpm build: node tests/editor-ui.mjs. SOYLI_PREVIEW_URL optionally checks a running real host.
import assert from 'node:assert/strict';
import { readFile, mkdir, realpath, writeFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { createRequire } from 'node:module';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const require = createRequire(
    await realpath(resolve(root, 'node_modules/@napplet/conformance-cli/package.json')),
);
const { chromium } = require('playwright');
const html = await readFile(resolve(root, 'dist/index.html'), 'utf8');
const fixture = JSON.parse(await readFile(resolve(root, 'src/vendor/outpost.json'), 'utf8'));
const shots = resolve(root, '.napplet-space/promotion/editor-tests');
await mkdir(shots, { recursive: true });
function installHost(map, cold = false, withKeys = false) {
    const state = (window.__host = {
        viewer: '',
        events: [],
        blobs: {},
        calls: [],
        failPublish: false,
        failResource: false,
        failStorage: false,
        handlers: [],
        saved: {},
        id: 0,
    });
    const digest = async (bytes) =>
        Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), (x) =>
            x.toString(16).padStart(2, '0'),
        ).join('');
    state.intentHandlers = [];
    state.pendingDelivery = cold
        ? [
              {
                  sender: 'test-game',
                  archetype: 'black-site',
                  action: 'map-new',
                  convention: 'napplet:black-site/map-new',
                  payload: { map: { ...map, id: 'cold-start', name: 'COLD START' } },
              },
          ]
        : [];
    state.deliver = (delivery) => {
        if (state.intentHandlers.length) for (const h of state.intentHandlers) h(delivery);
        else state.pendingDelivery.push(delivery);
    };
    state.switch = (pubkey) => {
        state.viewer = pubkey;
        for (const h of state.handlers) h(pubkey);
    };
    state.seed = async (pubkey = 'a'.repeat(64)) => {
        const bytes = new TextEncoder().encode(JSON.stringify(map));
        const hash = await digest(bytes);
        state.blobs[hash] = Array.from(bytes);
        const event = {
            id: (++state.id).toString(16).padStart(64, '0'),
            pubkey,
            sig: '0'.repeat(128),
            kind: 35858,
            content: '',
            created_at: 1000 + state.id,
            tags: [
                ['d', map.id],
                ['title', map.name],
                ['version', '1'],
                ['x', hash],
                ['size', String(bytes.length)],
                ['m', 'application/json'],
                ['server', 'https://maps.example'],
            ],
        };
        state.events.push(event);
        return event;
    };
    window.napplet = {
        intent: {
            onDelivery: (handler) => {
                state.intentHandlers.push(handler);
                queueMicrotask(() => {
                    for (const delivery of state.pendingDelivery.splice(0)) handler(delivery);
                });
                return {
                    close() {
                        state.intentHandlers = state.intentHandlers.filter((h) => h !== handler);
                    },
                };
            },
        },
        identity: {
            getPublicKey: async () => state.viewer,
            onChanged: (h) => {
                state.handlers.push(h);
                return { close() {} };
            },
        },
        outbox: {
            query: async (filters) => {
                state.calls.push(['query', filters]);
                const list = Array.isArray(filters) ? filters : [filters];
                return {
                    events: state.events
                        .filter((e) =>
                            list.some(
                                (f) =>
                                    (!f.kinds || f.kinds.includes(e.kind)) &&
                                    (!f.authors || f.authors.includes(e.pubkey)) &&
                                    (f.until === undefined || e.created_at <= f.until) &&
                                    Object.entries(f)
                                        .filter(([key]) => key.startsWith('#'))
                                        .every(([key, values]) =>
                                            e.tags.some(
                                                (t) =>
                                                    t[0] === key.slice(1) && values.includes(t[1]),
                                            ),
                                        ),
                            ),
                        )
                        .map((event) => ({ event, sidecar: { relayHints: [] } })),
                    incomplete: false,
                };
            },
            publish: async (template) => {
                state.calls.push(['publish', template]);
                if (state.failPublish)
                    return { ok: false, error: 'Fixture host denied publication' };
                const event = {
                    ...template,
                    pubkey: state.viewer,
                    id: (++state.id).toString(16).padStart(64, '0'),
                    sig: '0'.repeat(128),
                };
                if (event.kind === 5) {
                    const address = event.tags.find((t) => t[0] === 'a')?.[1];
                    state.events = state.events.filter(
                        (e) =>
                            `35858:${e.pubkey}:${e.tags.find((t) => t[0] === 'd')?.[1]}` !==
                            address,
                    );
                } else {
                    state.events = state.events.filter(
                        (e) =>
                            e.pubkey !== event.pubkey ||
                            e.tags.find((t) => t[0] === 'd')?.[1] !==
                                event.tags.find((t) => t[0] === 'd')?.[1],
                    );
                    state.events.push(event);
                }
                return { ok: true, event, relays: { 'wss://relay.example': true } };
            },
        },
        resource: {
            bytes: async (url) => {
                state.calls.push(['resource', url]);
                if (url === 'https://profiles.example/avatar.png')
                    return new Blob(
                        [
                            Uint8Array.from(
                                atob(
                                    'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aX1sAAAAASUVORK5CYII=',
                                ),
                                (c) => c.charCodeAt(0),
                            ),
                        ],
                        { type: 'image/png' },
                    );
                if (state.delayResource)
                    await new Promise((resolve) => {
                        const previous = state.releaseResource;
                        state.releaseResource = () => {
                            previous?.();
                            resolve();
                        };
                    });
                if (state.failResource)
                    throw Object.assign(new Error('Fixture unavailable'), {
                        code: 'network-error',
                    });
                const hash = url.startsWith('blossom:')
                    ? url.replace(/^blossom:(sha256:)?/, '')
                    : url.split('/').pop().split('.')[0];
                if (!state.blobs[hash])
                    throw Object.assign(new Error('Missing fixture'), { code: 'not-found' });
                return new Blob([new Uint8Array(state.blobs[hash])], { type: 'application/json' });
            },
        },
        upload: {
            info: async () => ({ rails: [{ rail: 'blossom', enabled: true }], maxBytes: 262144 }),
            upload: async (req) => {
                state.calls.push([
                    'upload',
                    {
                        mimeType: req.mimeType,
                        noTransform: req.noTransform,
                        filename: req.filename,
                    },
                ]);
                if (state.delayUpload)
                    await new Promise((resolve) => {
                        state.releaseUpload = resolve;
                    });
                const bytes = new Uint8Array(await req.data.arrayBuffer());
                const hash = await digest(bytes);
                state.blobs[hash] = Array.from(bytes);
                return {
                    ok: true,
                    status: 'complete',
                    uploadId: 'test-upload',
                    sha256: hash,
                    size: bytes.length,
                    url: `https://maps.example/${hash}.json`,
                };
            },
        },
        ...(withKeys
            ? {
                  keys: {
                      registerAction: async (action) => {
                          state.calls.push(['keys.registerAction', action]);
                          return {
                              actionId: action.id,
                              binding: action.id === 'view.grid' ? 'Alt+G' : undefined,
                          };
                      },
                      onAction: (id, cb) => {
                          (state.keyHandlers ??= {})[id] = cb;
                          return {
                              close() {
                                  delete state.keyHandlers[id];
                              },
                          };
                      },
                      unregisterAction: (id) => state.calls.push(['keys.unregisterAction', id]),
                  },
              }
            : {}),
        storage: {
            getItem: async (key) => state.saved[key] ?? null,
            setItem: async (key, value) => {
                if (state.failStorage) throw new Error('Fixture storage denied');
                state.saved[key] = value;
            },
            removeItem: async (key) => {
                delete state.saved[key];
            },
        },
    };
}
const server = createServer((req, res) => {
    res.setHeader('content-type', 'text/html');
    res.end(
        '<!doctype html><meta name="viewport" content="width=device-width,initial-scale=1"><body style="margin:0"><iframe id="editor" sandbox="allow-scripts" style="border:0;width:100vw;height:100vh"></iframe>',
    );
});
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const browser = await chromium.launch({ headless: true });
const report = { passed: [], gaps: [], screenshots: [] };
try {
    const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
    page.setDefaultTimeout(15000);
    const errors = [];
    page.on('pageerror', (e) => errors.push(e.message));
    await page.goto(`http://127.0.0.1:${server.address().port}`);
    const script = `(${installHost.toString()})(${JSON.stringify(fixture)});`;
    await page.locator('#editor').evaluate(
        (el, source) => {
            el.srcdoc = source;
        },
        html.replace(
            /<head[^>]*>/,
            `<head><script>${script.replaceAll('</script>', '<\\/script>')}</script>`,
        ),
    );
    const frame = page.frameLocator('#editor');
    await frame.locator('#map-canvas').waitFor();
    // First launch with storage: the tutorial opens once, in vertical step tabs.
    const help = frame.locator('#help');
    await help.waitFor();
    assert.equal(
        await frame.locator('#help [role="tablist"]').getAttribute('aria-orientation'),
        'vertical',
    );
    const helpTabs = frame.locator('#help [role="tab"]');
    assert.ok((await helpTabs.count()) >= 10);
    assert.equal(await helpTabs.first().getAttribute('aria-selected'), 'true');
    assert.equal(await frame.locator('#help-back').isDisabled(), true);
    await frame.locator('#help-next').click();
    assert.equal(await helpTabs.nth(1).getAttribute('aria-selected'), 'true');
    await helpTabs.nth(1).focus();
    await page.keyboard.press('ArrowDown');
    assert.equal(await helpTabs.nth(2).getAttribute('aria-selected'), 'true');
    assert.equal(await frame.locator('#help [role="tabpanel"]:not([hidden])').count(), 1);
    await frame.locator('#help [role="tab"]').nth(5).click();
    await helpTabs.nth(5).focus();
    await page.screenshot({ path: resolve(shots, 'help-desktop.png') });
    report.screenshots.push(resolve(shots, 'help-desktop.png'));
    await page.keyboard.press('End');
    assert.equal(await frame.locator('#help-next').textContent(), 'Start editing');
    const helpText = await help.evaluate((el) =>
        [...el.querySelectorAll('[role="tabpanel"]')].map((p) => p.textContent).join('\n'),
    );
    assert.doesNotMatch(
        helpText,
        /JSON|schema|SHA-?256|NAP-|OUTBOX|Blossom|35858|relay|hash|operation/i,
        'Tutorial uses player language',
    );
    await page.setViewportSize({ width: 320, height: 560 });
    await page.waitForTimeout(80);
    await page.screenshot({ path: resolve(shots, 'help-narrow.png') });
    report.screenshots.push(resolve(shots, 'help-narrow.png'));
    const helpBox = await frame.locator('#help-next').boundingBox();
    assert.ok(
        helpBox && helpBox.y + helpBox.height <= 560,
        'Tutorial footer stays reachable at 320×560',
    );
    assert.equal(
        await frame.locator('#help .help-step-title').first().isVisible(),
        false,
        'Narrow frames collapse step tabs to numbers',
    );
    await page.setViewportSize({ width: 1280, height: 800 });
    await helpTabs.last().click();
    await frame.locator('#help-next').click();
    await help.waitFor({ state: 'hidden' });
    await page.waitForTimeout(50);
    assert.equal(
        await page
            .frames()
            .find((f) => f !== page.mainFrame())
            .evaluate(() => window.__host.saved['black-site-editor:tutorial-seen:v1']),
        '1',
    );
    await frame.locator('#help-open').click();
    await help.waitFor();
    assert.equal(await helpTabs.first().getAttribute('aria-selected'), 'true');
    await frame.locator('#help-close').click();
    await help.waitFor({ state: 'hidden' });
    await frame.locator('#map-canvas').focus();
    await page.keyboard.press('F1');
    await help.waitFor();
    await page.keyboard.press('Escape');
    await help.waitFor({ state: 'hidden' });
    report.passed.push(
        'Tutorial auto-opens once with vertical step tabs, keyboard navigation, player language, narrow layout, Help button and F1',
    );
    assert.equal(await frame.locator('#publish').isDisabled(), true);
    report.passed.push('Signed-out publication disabled');
    for (const [width, height, label] of [
        [200, 160, 'tiny'],
        [320, 560, 'narrow'],
        [900, 600, 'desktop'],
        [1280, 800, 'full'],
        [390, 844, 'phone-portrait'],
        [844, 390, 'phone-landscape'],
    ]) {
        await page.setViewportSize({ width, height });
        await page.waitForTimeout(80);
        const path = resolve(shots, `${label}.png`);
        await page.screenshot({ path });
        report.screenshots.push(path);
        const bounds = await frame
            .locator('body')
            .evaluate((el) => ({ scroll: el.scrollWidth, width: innerWidth }));
        assert.ok(
            bounds.scroll <= bounds.width + 2,
            `${label}: body horizontal overflow ${bounds.scroll}/${bounds.width}`,
        );
    }
    report.passed.push('Six frame sizes render without body horizontal overflow');
    await page.setViewportSize({ width: 1280, height: 800 });
    const child = page.frames().find((f) => f !== page.mainFrame());
    await child.evaluate(async () => {
        await window.__host.seed();
        window.__host.events.push({
            kind: 0,
            pubkey: 'a'.repeat(64),
            id: 'e'.repeat(64),
            created_at: 1,
            tags: [],
            content: JSON.stringify({
                display_name: 'Map Maker',
                name: 'maker',
                picture: 'https://profiles.example/avatar.png',
            }),
        });
        window.__host.switch('a'.repeat(64));
    });
    await page.waitForTimeout(100);
    report.passed.push('Identity change delivered through host subscription');
    await frame.locator('#identity .account-name').filter({ hasText: 'Map Maker' }).waitFor();
    await frame.locator('#identity img').waitFor();
    assert.ok(
        await frame
            .locator('#identity img')
            .evaluate((img) => img.complete && img.naturalWidth > 0),
    );
    report.passed.push('Signed-in account shows profile display name and host-loaded avatar');
    const doc = async () => JSON.parse(await frame.locator('#json-editor').inputValue());
    const click = async (id) => frame.locator(`#${id}`).click();
    const accept = async () => frame.locator('#confirm-ok').click();
    const draw = async (layer, a, b = a) => {
        await frame.locator(`[data-tool="${layer}"]`).click();
        await click('fit');
        const rect = await frame.locator('#map-canvas').boundingBox();
        const [w, h] = (await doc()).size;
        const tile = Math.max(1, Math.min((rect.width - 24) / w, (rect.height - 24) / h));
        const pos = (p) => ({
            x: rect.x + (rect.width - w * tile) / 2 + (p[0] + 0.5) * tile,
            y: rect.y + (rect.height - h * tile) / 2 + (p[1] + 0.5) * tile,
        });
        const start = pos(a),
            end = pos(b);
        await page.mouse.move(start.x, start.y);
        await page.mouse.down();
        await page.mouse.move(end.x, end.y, { steps: 5 });
        await page.mouse.up();
    };
    const terrainCategory = frame
        .locator('.tool-category')
        .filter({ has: frame.getByText('Terrain', { exact: true }) });
    await terrainCategory.locator('summary').click();
    assert.equal(await terrainCategory.locator('[data-tool="roads"]').isVisible(), false);
    await terrainCategory.locator('summary').click();
    assert.equal(await terrainCategory.locator('[data-tool="roads"]').isVisible(), true);
    report.passed.push('Tool groups collapse and reopen without losing tools');
    await draw('roads', [8, 12], [15, 13]);
    assert.deepEqual((await doc()).roads, [[8, 12, 15, 13]]);
    await click('undo');
    assert.equal((await doc()).roads, undefined);
    await click('redo');
    assert.equal((await doc()).roads.length, 1);
    // Built-in keyboard shortcuts (this fixture host has no keys domain).
    await frame.locator('#map-canvas').focus();
    await frame.locator('#map-canvas').press('w');
    assert.equal(await frame.locator('[data-tool="water"]').getAttribute('aria-pressed'), 'true');
    await frame.locator('#map-canvas').press('Control+z');
    assert.equal((await doc()).roads, undefined);
    await frame.locator('#map-canvas').press('Control+Shift+Z');
    assert.equal((await doc()).roads.length, 1);
    assert.equal(await frame.locator('#undo').getAttribute('title'), 'Undo (Ctrl+Z)');
    assert.equal(await frame.locator('#undo').getAttribute('aria-keyshortcuts'), 'Control+Z');
    await frame.locator('#map-canvas').press('Shift+?');
    await frame.locator('#shortcuts').waitFor();
    assert.match(await frame.locator('#shortcuts-list').innerText(), /Undo\s+Ctrl\+Z/);
    assert.match(await frame.locator('#shortcuts-source').innerText(), /Built-in keys/);
    await page.screenshot({ path: resolve(shots, 'shortcuts-desktop.png') });
    await page.keyboard.press('g');
    assert.equal(
        await frame.locator('#grid-toggle').getAttribute('aria-pressed'),
        'true',
        'Shortcuts pause while a window is open',
    );
    await page.keyboard.press('Shift+?');
    assert.equal(await frame.locator('#shortcuts').isVisible(), false);
    await click('settings-open');
    await frame.locator('#name').press('v');
    assert.equal(
        await frame.locator('[data-tool="water"]').getAttribute('aria-pressed'),
        'true',
        'Typing in fields never triggers tools',
    );
    await frame.locator('#name').press('Backspace');
    await click('settings-close');
    await frame.locator('#map-canvas').press('g');
    assert.equal(await frame.locator('#grid-toggle').getAttribute('aria-pressed'), 'false');
    await frame.locator('#map-canvas').press('g');
    await frame.locator('#map-canvas').press('v');
    report.passed.push(
        'Built-in shortcuts switch tools, undo/redo, toggle the grid and open help; fields and dialogs keep their keystrokes',
    );
    await draw('buildings', [8, 5], [13, 9]);
    await draw('doors', [9, 5]);
    await draw('interiorWalls', [10, 6], [10, 8]);
    await draw('containers', [18, 6], [22, 7]);
    await draw('tanks', [25, 9]);
    await draw('enemies', [24, 18]);
    await draw('caches', [6, 20]);
    const placed = await doc();
    assert.equal(placed.buildings[0].doors.length, 2);
    assert.equal(placed.buildings[0].walls.length, 1);
    assert.equal(placed.containers.length, 1);
    assert.equal(placed.tanks.length, 1);
    assert.equal(placed.enemies.length, 1);
    report.passed.push(
        'Rectangle authoring, buildings/doors/interior walls, containers/tanks/guards/caches and undo/redo',
    );
    // Observe the internal snapshot only; all edits use player-facing controls.
    for (const selector of [
        '#json-editor',
        '#object-json',
        '#advanced-json',
        '#id',
        '#publish-details',
    ])
        assert.equal(
            await frame.locator(selector).isVisible(),
            false,
            `${selector} is hidden from map authors`,
        );
    assert.doesNotMatch(
        await frame.locator('body').innerText(),
        /JSON|schema|SHA-?256|NAP-|OUTBOX|Blossom|35858|operation/i,
    );
    await draw('enemies', [26, 18]);
    await frame.locator('#object-controls [data-field="patrol"]').check();
    await click('apply-object');
    assert.equal((await doc()).enemies.at(-1)[2], 'p');
    await click('settings-open');
    assert.equal(
        await frame.getByRole('tab', { name: 'Map', exact: true }).getAttribute('aria-selected'),
        'true',
    );
    await frame.getByRole('tab', { name: 'Map', exact: true }).press('ArrowRight');
    assert.equal(
        await frame
            .getByRole('tab', { name: 'Appearance', exact: true })
            .getAttribute('aria-selected'),
        'true',
    );
    assert.equal(await frame.locator('#name').isVisible(), false);
    await frame.getByRole('tab', { name: 'Appearance', exact: true }).press('End');
    assert.equal(
        await frame
            .getByRole('tab', { name: 'Sharing', exact: true })
            .getAttribute('aria-selected'),
        'true',
    );
    await frame.getByRole('tab', { name: 'Sharing', exact: true }).press('Home');
    assert.equal(await frame.locator('#name').isVisible(), true);
    for (const title of ['Appearance', 'Gameplay', 'Files', 'Sharing', 'Map']) {
        await frame.getByRole('tab', { name: title, exact: true }).click();
        assert.equal(await frame.locator('#settings [role="tabpanel"]:visible').count(), 1);
    }
    report.passed.push(
        'Five settings tabs isolate related controls and support keyboard navigation',
    );
    await page.screenshot({ path: resolve(shots, 'settings-desktop.png') });
    await page.setViewportSize({ width: 320, height: 560 });
    await frame.locator('#name').fill('TEST MAP');
    await frame.locator('#name').blur();
    assert.equal((await doc()).name, 'TEST MAP');
    await frame.locator('#settings-close').scrollIntoViewIfNeeded();
    assert.equal(await frame.locator('#settings-close').isVisible(), true);
    await page.screenshot({ path: resolve(shots, 'settings-narrow.png') });
    await frame.getByRole('tab', { name: 'Gameplay', exact: true }).click();
    await frame.locator('#deployment-controls [data-field="buildings"]').fill('2');
    await frame.locator('#deployment-controls [data-field="enemies"]').fill('50');
    await click('apply-advanced');
    await page.screenshot({ path: resolve(shots, 'settings-narrow-extras.png') });
    await page.setViewportSize({ width: 1280, height: 800 });
    assert.equal((await doc()).generate.buildings, 2);
    assert.equal((await doc()).spawns.extraEnemies, 0.5);
    await frame.locator('#deployment-controls [data-field="buildings"]').fill('-1');
    await click('apply-advanced');
    assert.equal(
        (await doc()).generate.buildings,
        2,
        'Invalid player setting leaves map unchanged',
    );
    await frame.locator('#deployment-controls [data-field="buildings"]').fill('2');
    await frame.getByRole('tab', { name: 'Files', exact: true }).click();
    await frame.getByRole('tab', { name: 'Gameplay', exact: true }).click();
    assert.equal(
        await frame.locator('#deployment-controls [data-field="buildings"]').inputValue(),
        '2',
        'Tab switching preserves pending values',
    );
    await click('settings-close');
    assert.equal(await frame.locator('#settings').isVisible(), false);
    report.passed.push('Map settings open in a dedicated dialog and close back to editing');
    report.passed.push(
        'Plain-language guard and deployment settings preserve maps and hide internal formats',
    );
    await click('validate');
    assert.match(await frame.locator('#status').innerText(), /ready to play/i);
    await click('save-draft');
    const saved = await doc();
    await click('new-map');
    await click('library-open');
    await click('restore-draft');
    assert.deepEqual(await doc(), saved);
    report.passed.push('Identity-scoped draft save and restore');
    await child.evaluate(() => {
        window.__host.failPublish = true;
        window.__host.delayUpload = true;
    });
    await click('publish');
    await accept();
    await frame.locator('#publication-message').filter({ hasText: 'Uploading your map' }).waitFor();
    assert.equal(await frame.locator('#publication progress').isVisible(), true);
    assert.equal(await frame.locator('#publication-close').isDisabled(), true);
    await page.screenshot({ path: resolve(shots, 'publish-progress.png') });
    await child.evaluate(() => {
        window.__host.delayUpload = false;
        window.__host.releaseUpload();
    });
    await frame
        .locator('#status')
        .filter({ hasText: /denied|service could not/i })
        .waitFor();
    await frame
        .locator('#publication-title')
        .filter({ hasText: 'could not be published' })
        .waitFor();
    await frame.locator('.publish-error summary').click();
    assert.match(
        await frame.locator('.publish-error pre').textContent(),
        /Fixture host denied publication/,
    );
    await page.screenshot({ path: resolve(shots, 'publish-failed.png') });
    assert.deepEqual(await doc(), saved);
    const uploadCount = await child.evaluate(
        () => window.__host.calls.filter((c) => c[0] === 'upload').length,
    );
    assert.equal(uploadCount, 1);
    assert.equal(
        await frame.locator('#publish-details').isVisible(),
        false,
        'Upload receipt stays out of the player interface',
    );
    await child.evaluate(() => {
        window.__host.failPublish = false;
    });
    await click('publication-retry');
    await accept();
    await frame
        .locator('#status')
        .filter({ hasText: /published/i })
        .waitFor();
    await frame.locator('#publication-title').filter({ hasText: 'Map published' }).waitFor();
    await click('publication-close');
    assert.equal(
        await child.evaluate(() => window.__host.calls.filter((c) => c[0] === 'upload').length),
        uploadCount,
        'Retry reuses verified upload',
    );
    report.passed.push(
        'Publishing modal shows upload progress, retains service errors and retries successfully without duplicate upload',
    );
    await click('library-open');
    const ownCard = frame.locator('.map-card').filter({ hasText: saved.name }).first();
    await ownCard.getByRole('button').click();
    await frame.getByRole('heading', { name: 'Reactions', exact: true }).waitFor();
    await frame.locator('.reaction-counts').waitFor();
    assert.equal(
        await frame
            .locator('#library')
            .evaluate((el) => Math.round(el.getBoundingClientRect().width)),
        await child.evaluate(() => innerWidth),
    );
    await frame.locator('#map-comment').fill('A map worth remixing.');
    await frame.getByRole('button', { name: 'Post comment', exact: true }).click();
    await frame.locator('.map-comment').filter({ hasText: 'A map worth remixing.' }).waitFor();
    await page.screenshot({ path: resolve(shots, 'map-details.png') });
    for (const [width, height] of [
        [200, 160],
        [320, 560],
        [900, 600],
        [1440, 900],
        [844, 390],
    ]) {
        await page.setViewportSize({ width, height });
        await page.waitForTimeout(80);
        assert.ok(
            await frame
                .locator('.map-browser-scroll')
                .evaluate((el) => el.scrollWidth <= el.clientWidth + 1),
        );
        await frame
            .locator('#library')
            .screenshot({ path: resolve(shots, `map-details-${width}.png`) });
    }
    await page.setViewportSize({ width: 1100, height: 760 });

    await frame.getByRole('button', { name: '← Back to maps', exact: true }).click();
    await click('following-maps');
    await frame.locator('#library-status').filter({ hasText: 'not following anyone' }).waitFor();
    await child.evaluate(async () => {
        const state = window.__host;
        await state.seed('c'.repeat(64));
        state.events.push({
            kind: 3,
            pubkey: state.viewer,
            id: '9'.repeat(64),
            created_at: 1,
            tags: [['p', 'c'.repeat(64)]],
            content: '',
        });
    });
    await click('community-maps');
    await frame.locator('.map-card').first().waitFor();
    await click('following-maps');
    await frame.locator('#library-status').filter({ hasText: '1 maps' }).waitFor();
    assert.equal(await frame.locator('.map-card').count(), 1);
    assert.match(await frame.locator('.map-card').textContent(), /cccccccccccc/);
    await click('community-maps');
    await frame
        .locator('#library-status')
        .filter({ hasText: /^\d+ maps/ })
        .waitFor();
    assert.ok((await frame.locator('.map-card').count()) >= 2);
    await frame.locator('.map-card canvas').first().waitFor();
    await frame.locator('#library').screenshot({ path: resolve(shots, 'map-gallery.png') });
    await click('my-maps');
    await frame
        .locator('.map-card')
        .filter({ hasText: saved.name })
        .first()
        .getByRole('button')
        .click();
    report.passed.push(
        'Full-frame map browser, details, comment publication and followed-author filtering',
    );
    await frame.getByRole('button', { name: 'Edit map', exact: true }).click();
    await frame.locator('#status').filter({ hasText: 'Map opened.' }).waitFor();
    await frame.locator('#library').waitFor({ state: 'hidden' });
    assert.deepEqual(await doc(), saved);
    assert.ok(await child.evaluate(() => window.__host.calls.some((c) => c[0] === 'resource')));
    report.passed.push('Own map discovery and hash-verified RESOURCE load');
    await child.evaluate(() => window.__host.switch('b'.repeat(64)));
    await frame.locator('#identity .account-name').filter({ hasText: 'bbbbbbbb' }).waitFor();
    assert.equal(await frame.locator('#identity img').count(), 0);
    assert.equal(await frame.locator('#publish').isDisabled(), true);
    assert.equal(await frame.locator('#save-draft').isDisabled(), true);
    report.passed.push('Account switch prevents former owner updates and draft saves');
    await click('settings-open');
    await frame.getByRole('tab', { name: 'Sharing', exact: true }).click();
    await frame.locator('#remix-current').click();
    await accept();
    await click('settings-close');
    assert.notEqual((await doc()).id, saved.id);
    assert.match(await frame.locator('#lineage').textContent(), /Remix of/);
    assert.equal(await frame.locator('#save-draft').isDisabled(), false);
    await click('save-draft');
    report.passed.push(
        'Remix current workspace creates new ID under current identity and retains parent credit',
    );
    await click('library-open');
    await click('community-maps');
    await frame
        .locator('.map-card')
        .filter({ hasText: saved.name })
        .first()
        .getByRole('button')
        .click();
    await frame.getByRole('button', { name: 'Remix map', exact: true }).click();
    const remix = await doc();
    assert.notEqual(remix.id, saved.id);
    await click('publish');
    await accept();
    await frame
        .locator('#status')
        .filter({ hasText: /published/i })
        .waitFor();
    await frame.locator('#publication-title').filter({ hasText: 'Map published' }).waitFor();
    await click('publication-close');
    const remixEvent = await child.evaluate(() =>
        window.__host.events.find((e) => e.pubkey === 'b'.repeat(64)),
    );
    assert.ok(remixEvent.tags.some((t) => t[0] === 'A'));
    assert.ok(remixEvent.tags.some((t) => t[0] === 'p' && t[1] === 'a'.repeat(64)));
    report.passed.push('Community remix publishes new author address and original/parent credits');
    await click('library-open');
    await frame.locator('.map-card').first().getByRole('button').click();
    await frame.getByRole('button', { name: 'Delete map', exact: true }).click();
    await accept();
    await frame.locator('#status').filter({ hasText: 'Deletion request published' }).waitFor();
    assert.equal(
        await child.evaluate(
            () => window.__host.events.filter((e) => e.pubkey === 'b'.repeat(64)).length,
        ),
        0,
    );
    await click('library-close');
    report.passed.push('Owner map deletion publishes native kind 5 request');
    await child.evaluate(() => window.__host.switch('a'.repeat(64)));
    await click('new-map');
    await click('library-open');
    await child.evaluate(() => {
        window.__host.failResource = true;
    });
    await frame.locator('.map-card').first().getByRole('button').click();
    await frame.getByRole('button', { name: 'Edit map', exact: true }).click();
    await frame
        .locator('#status')
        .filter({ hasText: /could not|unavailable|try again/i })
        .waitFor();
    await click('library-close');
    assert.notEqual((await doc()).id, saved.id);
    report.passed.push('Resource failure preserves current workspace');
    await child.evaluate((id) => {
        window.__host.failResource = false;
        const hash = window.__host.events
            .find(
                (e) =>
                    e.pubkey === 'a'.repeat(64) && e.tags.some((t) => t[0] === 'd' && t[1] === id),
            )
            .tags.find((t) => t[0] === 'x')[1];
        window.__host.corruptHash = hash;
        window.__host.originalBytes = window.__host.blobs[hash];
        window.__host.blobs[hash] = window.__host.blobs[hash].map((v, i) => (i === 10 ? v ^ 1 : v));
    }, saved.id);
    await click('library-open');
    await frame.locator('.map-card').first().getByRole('button').click();
    await frame.getByRole('button', { name: 'Edit map', exact: true }).click();
    await frame
        .locator('#status')
        .filter({ hasText: /damaged|changed|download|try again|could not|not valid/i })
        .waitFor();
    await click('library-close');
    report.passed.push('Corrupted map bytes fail SHA-256 verification and preserve workspace');
    await child.evaluate(() => {
        window.__host.blobs[window.__host.corruptHash] = window.__host.originalBytes;
        window.__host.failStorage = true;
    });
    await click('save-draft');
    await frame
        .locator('#status')
        .filter({ hasText: /save|storage|denied/i })
        .waitFor();
    report.passed.push('Optional storage rejection is visible');
    await child.evaluate(() => {
        window.__host.failStorage = false;
        window.__host.delayResource = true;
    });
    await click('library-open');
    await frame.locator('.map-card').first().getByRole('button').click();
    await frame.getByRole('button', { name: 'Edit map', exact: true }).click();
    await click('library-close');
    await click('new-map');
    const newId = (await doc()).id;
    await child.evaluate(() => {
        window.__host.delayResource = false;
        window.__host.releaseResource();
    });
    await page.waitForTimeout(100);
    assert.equal((await doc()).id, newId);
    report.passed.push('Delayed published-map load cannot replace a newer workspace');
    await child.evaluate(() => {
        window.__host.failResource = false;
        delete window.napplet.storage;
        delete window.napplet.theme;
        window.__host.switch('b'.repeat(64));
    });
    assert.equal(await frame.locator('#save-draft').isDisabled(), true);
    await draw('crates', [6, 8]);
    assert.equal((await doc()).crates.length, 1);
    report.passed.push('Missing optional storage/theme leaves map editing usable');
    // Exercise a real touchscreen event, rather than mouse-only mobile screenshots.
    const touchContext = await browser.newContext({
        viewport: { width: 390, height: 844 },
        hasTouch: true,
        isMobile: true,
    });
    const phone = await touchContext.newPage();
    await phone.goto(`http://127.0.0.1:${server.address().port}`);
    await phone.locator('#editor').evaluate(
        (el, source) => {
            el.srcdoc = source;
        },
        html.replace(
            /<head[^>]*>/,
            `<head><script>${script.replaceAll('</script>', '<\\/script>')}delete window.napplet.intent;window.__host.saved['black-site-editor:tutorial-seen:v1']='1';</script>`,
        ),
    );
    const pf = phone.frameLocator('#editor');
    await pf.locator('#map-canvas').waitFor();
    await phone.waitForTimeout(150);
    assert.equal(
        await pf.locator('#help').isVisible(),
        false,
        'Seen tutorial does not auto-open again',
    );
    await pf.locator('button[data-tab="tools"]').tap();
    await pf.locator('[data-tool="enemies"]').tap();
    await pf.locator('button[data-tab="map"]').tap();
    await pf.locator('#fit').tap();
    const cr = await pf.locator('#map-canvas').boundingBox();
    await phone.touchscreen.tap(cr.x + cr.width / 2, cr.y + cr.height / 2);
    assert.equal(JSON.parse(await pf.locator('#json-editor').inputValue()).enemies.length, 1);
    report.passed.push(
        'Phone portrait touch tabs and canvas guard placement with optional INTENT absent',
    );
    await phone.setViewportSize({ width: 844, height: 390 });
    await pf.locator('[data-tool="caches"]').tap();
    const land = await pf.locator('#map-canvas').boundingBox();
    await phone.touchscreen.tap(land.x + land.width * 0.6, land.y + land.height * 0.5);
    assert.equal(JSON.parse(await pf.locator('#json-editor').inputValue()).caches.length, 2);
    report.passed.push('Phone landscape touch cache placement');
    await touchContext.close();
    await frame.locator('[data-tool="select"]').click();
    const current = await doc();
    await frame.locator('#map-canvas').focus();
    await frame.locator('#map-canvas').press('ArrowRight');
    assert.deepEqual((await doc()).crates[0], [
        current.crates[0][0] + 1,
        current.crates[0][1],
        current.crates[0][2] + 1,
        current.crates[0][3],
    ]);
    report.passed.push('Focused canvas keyboard moves selected object');
    const ipage = await browser.newPage({ viewport: { width: 1200, height: 750 } });
    ipage.setDefaultTimeout(15000);
    await ipage.goto(`http://127.0.0.1:${server.address().port}`);
    await ipage.locator('#editor').evaluate(
        (el, source) => {
            el.srcdoc = source;
        },
        html.replace(
            /<head[^>]*>/,
            `<head><script>(${installHost.toString()})(${JSON.stringify(fixture)},true);</script>`,
        ),
    );
    const iframe = ipage.frames().find((f) => f !== ipage.mainFrame());
    const ui = ipage.frameLocator('#editor');
    await ui.locator('#map-canvas').waitFor();
    const idoc = async () => JSON.parse(await ui.locator('#json-editor').inputValue());
    try {
        await ui.locator('#mission').filter({ hasText: 'COLD START' }).waitFor();
    } catch (error) {
        console.log(
            'Cold intent diagnostics',
            await iframe.evaluate(() => ({
                status: document.querySelector('#status').textContent,
                mission: document.querySelector('#mission').textContent,
                handlers: window.__host.intentHandlers.length,
                pending: window.__host.pendingDelivery.length,
            })),
        );
        throw error;
    }
    assert.equal((await idoc()).id, 'cold-start');
    report.passed.push('Cold-start buffered NAP-INTENT delivery waits for receiver boot');
    await ipage.waitForTimeout(150);
    assert.equal(
        await ui.locator('#help').isVisible(),
        false,
        'Tutorial never covers a delivered map request',
    );
    await ui.locator('#save-draft').click();
    await iframe.evaluate(async () => {
        await window.__host.seed();
        window.__host.deliver({
            sender: 'test-game',
            archetype: 'black-site',
            action: 'map-edit',
            convention: 'napplet:black-site/map-edit',
            payload: {
                address: '35858:' + 'a'.repeat(64) + ':outpost',
                relays: ['wss://relay.example'],
            },
        });
    });
    await ui
        .locator('#status')
        .filter({ hasText: /connect|sign in|identity/i })
        .waitFor();
    assert.equal((await idoc()).id, 'cold-start');
    await iframe.evaluate(() => window.__host.switch('a'.repeat(64)));
    await ui.locator('#mission').filter({ hasText: 'OUTPOST' }).waitFor();
    assert.equal((await idoc()).id, 'outpost');
    assert.equal(await ui.locator('#id').isDisabled(), true);
    report.passed.push(
        'Signed-out edit delivery queues until identity login then loads OUTBOX/RESOURCE',
    );
    const deliver = async (action, payload, convention = `napplet:black-site/map-${action}`) =>
        iframe.evaluate(
            ({ action, payload, convention }) =>
                window.__host.deliver({
                    sender: 'test-game',
                    archetype: 'black-site',
                    action: `map-${action}`,
                    convention,
                    payload,
                }),
            { action, payload, convention },
        );
    await deliver('open', { address: '35858:' + 'a'.repeat(64) + ':outpost' });
    await ui.locator('#status').filter({ hasText: 'Your map is open for editing.' }).waitFor();
    assert.equal((await idoc()).id, 'outpost');
    report.passed.push('Inbound open convention resolves own map for editing');
    await iframe.evaluate(() => window.__host.switch('b'.repeat(64)));
    await deliver('edit', { address: '35858:' + 'a'.repeat(64) + ':outpost' });
    await ui
        .locator('#status')
        .filter({ hasText: /remix|author|owner/i })
        .waitFor();
    assert.equal(await ui.locator('#publish').isDisabled(), true);
    report.passed.push('Inbound edit refuses other-author update');
    await deliver('remix', { address: '35858:' + 'a'.repeat(64) + ':outpost' });
    await ui.locator('#lineage').filter({ hasText: 'Remix of' }).waitFor({ state: 'attached' });
    await ipage.waitForTimeout(80);
    assert.notEqual((await idoc()).id, 'outpost');
    assert.match(await ui.locator('#lineage').textContent(), /Remix of/);
    report.passed.push('Inbound remix loads parent map and creates distinct credited document');
    const dirtyIntentDoc = await idoc();
    await deliver('new', {});
    await ui.locator('#confirm-cancel').click();
    assert.deepEqual(await idoc(), dirtyIntentDoc);
    report.passed.push('Cancelled dirty-workspace intent preserves document');
    for (const [action, payload, convention] of [
        [
            'edit',
            { address: '35858:' + 'a'.repeat(64) + ':outpost' },
            'napplet:black-site/map-edit?x=1',
        ],
        [
            'edit',
            { address: '35859:' + 'a'.repeat(64) + ':outpost' },
            'napplet:black-site/map-edit',
        ],
        [
            'edit',
            { address: '35858:' + 'a'.repeat(64) + ':outpost', relays: ['https://wrong.example'] },
            'napplet:black-site/map-edit',
        ],
    ]) {
        await deliver(action, payload, convention);
        await ipage.waitForTimeout(40);
        assert.deepEqual(await idoc(), dirtyIntentDoc);
    }
    report.passed.push(
        'Intent convention query, wrong event kind and invalid relay payload rejected without replacement',
    );
    await ipage.close();
    const kpage = await browser.newPage({ viewport: { width: 1200, height: 750 } });
    kpage.setDefaultTimeout(15000);
    await kpage.goto(`http://127.0.0.1:${server.address().port}`);
    await kpage.locator('#editor').evaluate(
        (el, source) => {
            el.srcdoc = source;
        },
        html.replace(
            /<head[^>]*>/,
            `<head><script>(${installHost.toString()})(${JSON.stringify(fixture)},false,true);</script>`,
        ),
    );
    const kui = kpage.frameLocator('#editor');
    await kui.locator('#map-canvas').waitFor();
    await kui.locator('#help').waitFor();
    await kui.locator('#help-close').click();
    await kui.locator('#help').waitFor({ state: 'hidden' });
    const kframe = kpage.frames().find((f) => f !== kpage.mainFrame());
    await kui
        .locator('#grid-toggle[title="Show or hide grid (Alt+G)"]')
        .waitFor({ state: 'attached' });
    const registered = await kframe.evaluate(() =>
        window.__host.calls.filter((c) => c[0] === 'keys.registerAction').map((c) => c[1]),
    );
    assert.ok(registered.length > 30);
    assert.ok(
        registered.every((a) => /^[a-z]+\.[a-z0-9-]+$/.test(a.id) && a.label && a.defaultKey),
    );
    assert.equal(new Set(registered.map((a) => a.id)).size, registered.length);
    await kui.locator('#map-canvas').focus();
    await kui.locator('#map-canvas').press('g');
    assert.equal(
        await kui.locator('#grid-toggle').getAttribute('aria-pressed'),
        'true',
        'Host-bound action ignores its built-in key',
    );
    await kframe.evaluate(() => window.__host.keyHandlers['view.grid']());
    assert.equal(
        await kui.locator('#grid-toggle').getAttribute('aria-pressed'),
        'false',
        'Host-delivered action runs',
    );
    await kui.locator('#map-canvas').press('w');
    assert.equal(
        await kui.locator('[data-tool="water"]').getAttribute('aria-pressed'),
        'true',
        'Unbound host registrations keep built-in keys',
    );
    await kui.locator('#shortcuts-open').click();
    assert.match(await kui.locator('#shortcuts-source').innerText(), /host manages/);
    assert.match(await kui.locator('#shortcuts-list').innerText(), /Show or hide grid\s+Alt\+G/);
    await kframe.evaluate(() => window.__host.keyHandlers['help.shortcuts']());
    assert.equal(await kui.locator('#shortcuts').isVisible(), false);
    await kframe.evaluate(() => window.dispatchEvent(new Event('pagehide')));
    assert.ok(
        (await kframe.evaluate(
            () => window.__host.calls.filter((c) => c[0] === 'keys.unregisterAction').length,
        )) > 30,
        'Teardown unregisters actions',
    );
    report.passed.push(
        'Keys host: actions registered with stable ids, host bindings replace built-in keys, host-delivered actions run, teardown unregisters',
    );
    await kpage.close();
    const themeDoc = await doc();
    const canvasBefore = await frame.locator('#map-canvas').evaluate((c) => c.toDataURL());
    const palette = await frame
        .locator('body')
        .evaluate((el) => getComputedStyle(el).backgroundColor);
    await child.evaluate(() => {
        window.napplet.theme = { get: async () => ({ colorScheme: 'light' }) };
    });
    assert.equal(
        await frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor),
        palette,
    );
    await child.evaluate(() => {
        window.napplet.theme = { get: async () => ({ colorScheme: 'dark' }) };
    });
    assert.equal(
        await frame.locator('body').evaluate((el) => getComputedStyle(el).backgroundColor),
        palette,
    );
    assert.deepEqual(await doc(), themeDoc);
    assert.equal(await frame.locator('#map-canvas').evaluate((c) => c.toDataURL()), canvasBefore);
    report.passed.push('App-owned palette unchanged with absent, light, and dark optional theme');
    assert.deepEqual(errors, [], 'No uncaught app errors');
    report.passed.push('Sandbox iframe boot with injected SDK domains and no uncaught errors');
    if (process.env.SOYLI_PREVIEW_URL) {
        const hosted = await browser.newPage();
        await hosted.goto(process.env.SOYLI_PREVIEW_URL);
        await hosted.waitForTimeout(1200);
        const app = hosted.frames().find((f) => f !== hosted.mainFrame());
        assert.ok(app, 'Real host iframe exists');
        await app.locator('#map-canvas').waitFor({ timeout: 15000 });
        await hosted.waitForTimeout(500);
        if (await app.locator('#help').isVisible()) await app.locator('#help-close').click();
        await app.locator('#save-draft').click();
        await app.locator('#status').filter({ hasText: 'Draft saved' }).waitFor();
        const domains = await app.evaluate(() => Object.keys(window.napplet));
        assert.ok(
            domains.includes('storage') &&
                domains.includes('outbox') &&
                domains.includes('resource'),
        );
        report.passed.push(
            'Real soyLI hosted iframe loads with injected domains and user-triggered STORAGE operation',
        );
        await hosted.screenshot({ path: resolve(shots, 'soyli-host.png') });
        await hosted.setViewportSize({ width: 1200, height: 930 });
        await hosted
            .locator('iframe')
            .first()
            .evaluate((el) => {
                el.style.height = '750px';
                el.style.width = '1200px';
            });
        await app.locator('#library-open').click();
        await app.locator('#templates').click();
        await app
            .locator('.map-card')
            .filter({ has: app.getByRole('heading', { name: 'DOCKYARD', exact: true }) })
            .getByRole('button', { name: 'Use template' })
            .click();
        await app.locator('#fit').click();
        await app.locator('#inspector summary').filter({ hasText: 'Objects & layers' }).click();
        await app.locator('#object-layer').selectOption('containers');
        await app.locator('#object-list button').first().click();
        await hosted
            .locator('iframe')
            .first()
            .screenshot({ path: resolve(root, 'preview.png') });
        report.screenshots.push(resolve(root, 'preview.png'));
        report.passed.push('Representative populated DOCKYARD editor capture from real hosted app');
        await hosted.setViewportSize({ width: 390, height: 1200 });
        await hosted
            .locator('iframe')
            .first()
            .evaluate((el) => {
                el.style.height = '844px';
                el.style.width = '390px';
            });
        await app.locator('button[data-tab="map"]').click();
        await app.locator('#fit').click();
        await hosted
            .locator('iframe')
            .first()
            .screenshot({ path: resolve(shots, 'populated-phone.png') });
    } else
        report.gaps.push(
            'Real soyLI host not requested: provide SOYLI_PREVIEW_URL from owned preview session.',
        );
    report.gaps.push(
        'This browser suite uses controlled SDK fixtures for native signed operations. The separate source-host service suite verifies real signatures, consent, upload, map lifecycle and game-to-editor routing on local relay/Blossom services; released/deployed hosts remain separate.',
    );
    await writeFile(resolve(shots, 'report.json'), JSON.stringify(report, null, 2) + '\n');
    console.log(JSON.stringify(report, null, 2));
} finally {
    await browser.close();
    await new Promise((r) => server.close(r));
}