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.
// 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));
}
