SOURCE / PINNED RELEASE
Made of little things.
Outbreak: Soycoon City
- Release
- db7750b1d009…
- Author-recorded commit
- 3ee3f5fd27a6…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b87c04571a9
Archive hash verified: dbebdc772b142a1c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { createRequire } from 'node:module';
import { realpath, mkdir, readFile } from 'node:fs/promises';
import { execFileSync } from 'node:child_process';
import assert from 'node:assert/strict';
const url = process.env.SOYINDENT_PREVIEW_URL;
if (!url) throw new Error('Start soyli dev, then run SOYINDENT_PREVIEW_URL=<its printed URL> pnpm test:hosted');
const require = createRequire(await realpath(new URL('../node_modules/@napplet/conformance-cli/package.json', import.meta.url)));
const { chromium } = require('playwright');
const profile = JSON.parse(execFileSync('soyli', ['browser', 'path', '--json'], { encoding: 'utf8' }));
const executablePath = profile.executables.find(e => e.name === 'chromium-headless-shell' && e.installed)?.path;
const browser = await chromium.launch({ executablePath, headless: true, args: ['--enable-webgl', '--use-gl=angle', '--use-angle=swiftshader'] });
await mkdir('.napplet-space/testing', { recursive: true });
const errors = [];
const page = await browser.newPage({ viewport: { width: 1200, height: 900 } });
page.on('pageerror', e => errors.push(e.message));
try {
await page.goto(url);
const f = page.frameLocator('iframe');
await f.locator('#scene canvas').waitFor();
await f.locator('#startButton').click();
await page.keyboard.press('Space');
assert.equal(await f.locator('#ammoCount').innerText(), '05');
await page.keyboard.press('KeyR');
await f.locator('#ammoCount').filter({hasText:'06'}).waitFor();
assert.equal(await f.locator('#ammoCount').innerText(), '06');
assert.equal(await f.locator('#reserveCount').innerText(), '23');
await page.keyboard.down('KeyW'); await f.locator('#cameraLabel').filter({hasText:'02'}).waitFor(); await page.keyboard.up('KeyW');
assert.match(await f.locator('#cameraLabel').innerText(), /02/);
await page.keyboard.press('Escape');
console.log('PASS movement, camera cut, shotgun, reload, pause');
await f.locator('#editorTab').click(); await f.locator('#saveMap').click();
await f.locator('#status').filter({ hasText: 'Draft saved' }).waitFor();
await f.locator('summary').filter({ hasText: 'Map JSON' }).click();
await f.locator('#showJson').click();
const stage = JSON.parse(await f.locator('#mapJson').inputValue());
stage.rooms[0].spawn = { x: 4.3, z: 2.9 };
await f.locator('#mapJson').fill(JSON.stringify(stage)); await f.locator('#applyJson').click();
await f.locator('#testMap').click(); await page.keyboard.press('KeyE');
assert.equal(await f.locator('#roomTitle').innerText(), 'Records Corridor');
await page.keyboard.press('KeyE'); assert.equal(await f.locator('#roomTitle').innerText(), 'Evidence Room');
console.log('PASS SDK draft save and both directions of connected doors');
await f.locator('#editorTab').click(); const cameraCount=await f.locator('#cameraList button').count(); await f.locator('#addCamera').click();
assert.equal(await f.locator('#cameraList button').count(), cameraCount+1);
await f.locator('#deleteCamera').click(); assert.equal(await f.locator('#cameraList button').count(), cameraCount);
await f.locator('#exportSchema').click(); await page.getByRole('button', { name: 'Save file', exact: true }).click();
await f.locator('#status').filter({ hasText: 'Schema PNG exported' }).waitFor();
const [schema] = await Promise.all([page.waitForEvent('download'), page.getByRole('link', { name: /evidence-cam-1-schema.png/ }).click()]);
await schema.saveAs('.napplet-space/testing/exported-schema.png');
const bytes = await readFile('.napplet-space/testing/exported-schema.png');
assert.equal(bytes.readUInt32BE(16), 640); assert.equal(bytes.readUInt32BE(20), 480);
console.log('PASS 640×480 schema PNG through the actual host file export');
await f.locator('#importBackdrop').click();
await page.getByLabel('Choose files for napplet').waitFor({ state: 'visible' });
await page.getByLabel('Choose files for napplet').setInputFiles('.napplet-space/testing/exported-schema.png');
await f.locator('#status').filter({ hasText: 'Background assigned' }).waitFor();
assert.match(await f.locator('#artStatus').innerText(), /Custom art assigned/);
await f.locator('#exportMap').click(); await page.getByRole('button', { name: 'Save file', exact: true }).click();
await f.locator('#status').filter({ hasText: 'Map exported' }).waitFor();
const [mapDownload] = await Promise.all([page.waitForEvent('download'), page.getByRole('link', { name: /soyindent-stage.json/ }).click()]);
await mapDownload.saveAs('.napplet-space/testing/roundtrip-map.json');
const map = JSON.parse(await readFile('.napplet-space/testing/roundtrip-map.json', 'utf8'));
assert.match(map.rooms[0].cameras[0].backdrop, /^data:image\/png;base64,/);
await f.locator('#importMap').click();
await page.getByLabel('Choose files for napplet').waitFor({ state: 'visible' });
await page.getByLabel('Choose files for napplet').setInputFiles('.napplet-space/testing/roundtrip-map.json');
await f.locator('#status').filter({ hasText: 'Map imported' }).waitFor();
assert.match(await f.locator('#artStatus').innerText(), /Custom art assigned/);
await f.locator('#removeBackdrop').click();
console.log('PASS background import and map JSON/art roundtrip');
await f.locator('#loadMap').click();
// Removing custom art leaves unsaved edits; the app must protect that draft.
await f.locator('#status').filter({ hasText: 'Save or export' }).waitFor();
await f.locator('#saveMap').click(); await f.locator('#status').filter({ hasText: 'Draft saved' }).waitFor();
await f.locator('#loadMap').click(); await f.locator('#status').filter({ hasText: 'Draft loaded' }).waitFor();
await f.locator('#playTab').click();
for (const [w, h] of [[200, 160], [320, 560], [900, 600], [1600, 900]]) {
await page.locator('iframe').evaluate((e, [w, h]) => { Object.assign(e.style, { width: `${w}px`, height: `${h}px`, minHeight: '0', flex: 'none' }); }, [w, h]);
await page.waitForTimeout(120);
const actual = await f.locator('body').evaluate(() => ({ w: innerWidth, h: innerHeight, scroll: document.body.scrollWidth, notice: getComputedStyle(document.querySelector('.minimum-notice')).display, stage: document.querySelector('#gameStage').getBoundingClientRect().toJSON() }));
assert.equal(actual.w, w); assert.equal(actual.h, h); assert(actual.scroll <= w);
if (w >= 280 && h >= 300) assert(Math.abs(actual.stage.width / actual.stage.height - 4 / 3) < .015);
else assert.notEqual(actual.notice, 'none');
await page.locator('iframe').screenshot({ path: `.napplet-space/testing/final-${w}x${h}.png` });
if (w >= 280) { await f.locator('#editorTab').click(); assert((await f.locator('body').evaluate(() => document.body.scrollWidth)) <= w); await f.locator('#playTab').click(); }
console.log(`PASS actual frame ${w}×${h}`);
}
for (const scheme of ['dark', 'light']) {
await page.emulateMedia({ colorScheme: scheme });
assert.equal(await f.locator('body').evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--bg')), '#111715');
}
for (const [w, h] of [[390, 844], [360, 640], [844, 390]]) {
const context = await browser.newContext({ viewport: { width: w, height: h }, hasTouch: true, isMobile: true, deviceScaleFactor: 1 });
try {
const touchPage = await context.newPage(); touchPage.on('pageerror', e => errors.push(e.message));
await touchPage.goto(url); const tf = touchPage.frameLocator('iframe');
await tf.locator('#scene canvas').waitFor(); await tf.locator('#startButton').tap();
await tf.locator('[data-action=shoot]').tap(); assert.equal(await tf.locator('#ammoCount').innerText(), '05');
await tf.locator('[data-action=reload]').tap(); await tf.locator('#ammoCount').filter({hasText:'06'}).waitFor();
assert.equal(await tf.locator('#ammoCount').innerText(), '06');
const cdp = await context.newCDPSession(touchPage);
await touchPage.locator('iframe').scrollIntoViewIfNeeded(); await tf.locator('[data-hold=forward]').scrollIntoViewIfNeeded();
const a = await tf.locator('[data-hold=forward]').boundingBox(), b = await tf.locator('[data-hold=aim]').boundingBox();
await cdp.send('Input.dispatchTouchEvent', { type: 'touchStart', touchPoints: [{ x: a.x + a.width / 2, y: a.y + a.height / 2, id: 1 }, { x: b.x + b.width / 2, y: b.y + b.height / 2, id: 2 }] });
await touchPage.waitForTimeout(200);
assert.equal(await tf.locator('.held').count(), 2);
await cdp.send('Input.dispatchTouchEvent', { type: 'touchCancel', touchPoints: [] });
assert.equal(await tf.locator('.held').count(), 0);
const frame = await tf.locator('body').evaluate(() => ({ w: innerWidth, h: innerHeight }));
await touchPage.locator('iframe').screenshot({ path: `.napplet-space/testing/touch-${w}x${h}.png` });
console.log(`PASS touch ${w}×${h}, actual frame ${frame.w}×${frame.h}, fire/reload, simultaneous movement+aim and cancellation`);
} finally { await context.close(); }
}
await f.locator('body').evaluate(() => { delete window.napplet.theme; delete window.napplet.storage; delete window.napplet.fs; });
await f.locator('#editorTab').click(); await f.locator('#saveMap').click();
await f.locator('#status').filter({ hasText: 'Storage is unavailable' }).waitFor();
await f.locator('#exportSchema').click(); await f.locator('#status').filter({ hasText: 'no file exports' }).waitFor();
await f.locator('#playTab').click(); await f.locator('#startButton').click(); await page.keyboard.press('Space');
assert.equal(await f.locator('#ammoCount').innerText(), '05');
console.log('PASS app palette in both schemes and absent optional domains');
assert.deepEqual(errors, []);
} finally { await browser.close(); }
