import test from "node:test"; import assert from "node:assert/strict"; import { access, readdir, readFile } from "node:fs/promises"; const dataUrl = new URL("../data/", import.meta.url); async function loadManifest() { return JSON.parse(await readFile(new URL("fixtures.json", dataUrl), "utf8")); } test("fixture manifest covers every screenshot exactly once", async () => { const manifest = await loadManifest(); const files = (await readdir(dataUrl)).filter((file) => file.toLowerCase().endsWith(".jpg")).sort(); const declared = manifest.cases.map((fixture) => fixture.file).sort(); assert.equal(manifest.version, 1); assert.deepEqual(manifest.frame, { width: 1560, height: 1360 }); assert.equal(new Set(manifest.cases.map((fixture) => fixture.id)).size, manifest.cases.length); assert.deepEqual(declared, files); }); test("fixture groups have deterministic order and reviewed slot transitions", async () => { const { cases } = await loadManifest(); const groups = new Map(); cases.forEach((fixture) => { const group = groups.get(fixture.groupId) || []; group.push(fixture); groups.set(fixture.groupId, group); }); for (const fixtures of groups.values()) { const orders = fixtures.map((fixture) => fixture.order).sort((left, right) => left - right); assert.deepEqual(orders, [...Array(fixtures.length).keys()]); for (const fixture of fixtures) { const expected = fixture.expected; const occupied = expected.occupiedSlots; const classified = [...expected.pendingSlots, ...expected.foundSlots].sort((a, b) => a - b); assert.deepEqual(classified, occupied); assert.equal(new Set(classified).size, classified.length); assert.ok(classified.every((slot) => Number.isInteger(slot) && slot >= 0 && slot < 18)); if (expected.item) { assert.ok(fixture.catalogAssetId); assert.ok(expected.foundSlots.includes(expected.hoveredSlot)); assert.ok(expected.item.name && expected.item.displayName); assert.ok(Number.isSafeInteger(expected.item.quantity) && expected.item.quantity >= 0); assert.ok(Number.isSafeInteger(expected.item.priceAdena) && expected.item.priceAdena > 0); } } } }); test("contextual tooltip fixtures own their initial shop state", async () => { const { cases } = await loadManifest(); const contextual = cases.filter((fixture) => fixture.mode === "contextual"); assert.ok(contextual.length > 0); contextual.forEach((fixture) => { assert.equal(fixture.initialState.side, fixture.expected.side); assert.equal(fixture.initialState.merchant, fixture.expected.merchant); assert.ok(fixture.initialState.activeShopKey); }); }); test("fixture calibration and every declared catalog asset are committed", async () => { const manifest = await loadManifest(); const calibration = JSON.parse(await readFile(new URL(manifest.calibration, dataUrl), "utf8")); const catalog = JSON.parse( await readFile(new URL("fixture-assets/catalog.json", dataUrl), "utf8"), ); assert.deepEqual(calibration.frame, manifest.frame); for (const field of [ "saleAnchor", "storeHeaderRegion", "tooltipSearchRegion", "tooltipAnchor", "itemNameRegion", "itemPriceRegion", "sellItemGridRegion", "buyItemGridRegion", ]) { assert.ok(calibration[field], `missing fixture calibration field: ${field}`); } await access(new URL(calibration.saleAnchor.image, dataUrl)); await access(new URL(calibration.tooltipAnchor.image, dataUrl)); for (const fixture of manifest.cases.filter((entry) => entry.catalogAssetId)) { const item = catalog.items[fixture.catalogAssetId]; assert.ok(item, `missing catalog fixture: ${fixture.catalogAssetId}`); await access(new URL(item.iconUrl.replace(/^\//, "../"), dataUrl)); } });