Improve L2 market parsing and data handling

This commit is contained in:
2026-08-11 22:24:36 +03:00
parent de41230b9b
commit af9cd1e92d
58 changed files with 2474 additions and 444 deletions
+92
View File
@@ -0,0 +1,92 @@
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));
}
});