@shipload/item-renderer 0.2.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +6 -0
- package/bun.lock +2 -2
- package/package.json +2 -2
- package/scripts/check-bundle-size.ts +21 -21
- package/scripts/copy-fonts.ts +19 -19
- package/scripts/preview.ts +13 -15
- package/src/assets/stardust-base64.ts +1 -1
- package/src/errors.ts +8 -8
- package/src/fonts/index.ts +25 -26
- package/src/fonts/load-bun.ts +9 -9
- package/src/index.ts +21 -21
- package/src/links.ts +11 -11
- package/src/meta.ts +16 -16
- package/src/payload/base64url.ts +16 -16
- package/src/payload/codec.ts +13 -13
- package/src/primitives/category-icon.ts +69 -48
- package/src/primitives/compact-row.ts +13 -13
- package/src/primitives/divider.ts +9 -9
- package/src/primitives/icon-hex.ts +18 -16
- package/src/primitives/module-slot.ts +73 -73
- package/src/primitives/panel.ts +10 -10
- package/src/primitives/quantity-badge.ts +13 -13
- package/src/primitives/span-paragraph.ts +48 -50
- package/src/primitives/stat-bar.ts +24 -24
- package/src/primitives/svg.ts +13 -13
- package/src/primitives/text.ts +25 -25
- package/src/primitives/wrap.ts +12 -12
- package/src/render.ts +15 -19
- package/src/templates/_shared.ts +5 -6
- package/src/templates/component.ts +67 -65
- package/src/templates/index.ts +17 -17
- package/src/templates/item-cell.ts +48 -45
- package/src/templates/module.ts +83 -81
- package/src/templates/packed-entity.ts +12 -14
- package/src/templates/resource.ts +63 -65
- package/src/templates/ship-panel.ts +66 -71
- package/src/templates/social-card.ts +27 -25
- package/src/tokens/colors.ts +29 -29
- package/src/tokens/index.ts +6 -6
- package/src/tokens/spacing.ts +1 -1
- package/src/tokens/typography.ts +1 -1
- package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
- package/test/base64url.test.ts +22 -22
- package/test/codec.test.ts +26 -35
- package/test/errors.test.ts +21 -21
- package/test/fixtures/cargo-items.ts +43 -43
- package/test/fonts.test.ts +23 -23
- package/test/links-meta.test.ts +37 -37
- package/test/pixel.test.ts +44 -41
- package/test/primitives-category-icon.test.ts +74 -67
- package/test/primitives-compact-row.test.ts +29 -29
- package/test/primitives-domain.test.ts +61 -50
- package/test/primitives-layout.test.ts +47 -47
- package/test/primitives-module-slot.test.ts +58 -58
- package/test/render.test.ts +38 -35
- package/test/sanity.test.ts +5 -5
- package/test/sdk-link.test.ts +13 -13
- package/test/svg.test.ts +24 -22
- package/test/templates-component.test.ts +32 -32
- package/test/templates-dispatch.test.ts +29 -29
- package/test/templates-item-cell.test.ts +79 -79
- package/test/templates-module.test.ts +52 -52
- package/test/templates-packed-entity.test.ts +42 -42
- package/test/templates-resource.test.ts +61 -61
- package/test/templates-ship-panel.test.ts +65 -61
- package/test/tokens.test.ts +28 -26
package/test/codec.test.ts
CHANGED
|
@@ -1,43 +1,34 @@
|
|
|
1
|
-
import { expect, test } from
|
|
2
|
-
import { encodePayload, decodePayload } from
|
|
3
|
-
import { InvalidPayloadError } from
|
|
4
|
-
import { FIXTURES } from
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { encodePayload, decodePayload } from "../src/payload/codec.ts";
|
|
3
|
+
import { InvalidPayloadError } from "../src/errors.ts";
|
|
4
|
+
import { FIXTURES } from "./fixtures/cargo-items.ts";
|
|
5
5
|
|
|
6
|
-
test(
|
|
6
|
+
test("round-trips every fixture exactly", () => {
|
|
7
7
|
for (const [name, item] of Object.entries(FIXTURES)) {
|
|
8
|
-
const encoded = encodePayload(item)
|
|
9
|
-
const decoded = decodePayload(encoded)
|
|
10
|
-
expect(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
).toBe(
|
|
14
|
-
expect(
|
|
15
|
-
decoded.quantity.equals(item.quantity),
|
|
16
|
-
`${name} quantity`,
|
|
17
|
-
).toBe(true)
|
|
18
|
-
expect(
|
|
19
|
-
decoded.stats.equals(item.stats),
|
|
20
|
-
`${name} stats`,
|
|
21
|
-
).toBe(true)
|
|
22
|
-
expect(decoded.modules.length, `${name} modules length`).toBe(item.modules.length)
|
|
8
|
+
const encoded = encodePayload(item);
|
|
9
|
+
const decoded = decodePayload(encoded);
|
|
10
|
+
expect(decoded.item_id.equals(item.item_id), `${name} item_id`).toBe(true);
|
|
11
|
+
expect(decoded.quantity.equals(item.quantity), `${name} quantity`).toBe(true);
|
|
12
|
+
expect(decoded.stats.equals(item.stats), `${name} stats`).toBe(true);
|
|
13
|
+
expect(decoded.modules.length, `${name} modules length`).toBe(item.modules.length);
|
|
23
14
|
}
|
|
24
|
-
})
|
|
15
|
+
});
|
|
25
16
|
|
|
26
|
-
test(
|
|
17
|
+
test("encoded payload is URL-safe", () => {
|
|
27
18
|
for (const item of Object.values(FIXTURES)) {
|
|
28
|
-
const encoded = encodePayload(item)
|
|
29
|
-
expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/)
|
|
19
|
+
const encoded = encodePayload(item);
|
|
20
|
+
expect(encoded).toMatch(/^[A-Za-z0-9_-]+$/);
|
|
30
21
|
}
|
|
31
|
-
})
|
|
22
|
+
});
|
|
32
23
|
|
|
33
|
-
test(
|
|
34
|
-
expect(() => decodePayload(
|
|
35
|
-
expect(() => decodePayload(
|
|
36
|
-
expect(() => decodePayload(
|
|
37
|
-
})
|
|
24
|
+
test("decodePayload throws InvalidPayloadError on malformed input", () => {
|
|
25
|
+
expect(() => decodePayload("!!!")).toThrow(InvalidPayloadError);
|
|
26
|
+
expect(() => decodePayload("AAA")).toThrow(InvalidPayloadError);
|
|
27
|
+
expect(() => decodePayload("")).toThrow(InvalidPayloadError);
|
|
28
|
+
});
|
|
38
29
|
|
|
39
|
-
test(
|
|
40
|
-
expect(encodePayload(FIXTURES.oreT1).length).toBeLessThan(30)
|
|
41
|
-
expect(encodePayload(FIXTURES.shipT1NoModules).length).toBeLessThan(30)
|
|
42
|
-
expect(encodePayload(FIXTURES.shipT1TwoModules).length).toBeLessThan(110)
|
|
43
|
-
})
|
|
30
|
+
test("payload sizes are within expected ranges", () => {
|
|
31
|
+
expect(encodePayload(FIXTURES.oreT1).length).toBeLessThan(30);
|
|
32
|
+
expect(encodePayload(FIXTURES.shipT1NoModules).length).toBeLessThan(30);
|
|
33
|
+
expect(encodePayload(FIXTURES.shipT1TwoModules).length).toBeLessThan(110);
|
|
34
|
+
});
|
package/test/errors.test.ts
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import { expect, test } from
|
|
2
|
-
import { InvalidPayloadError, UnknownItemError, RenderError } from
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { InvalidPayloadError, UnknownItemError, RenderError } from "../src/errors.ts";
|
|
3
3
|
|
|
4
|
-
test(
|
|
5
|
-
const e = new InvalidPayloadError(
|
|
6
|
-
expect(e).toBeInstanceOf(Error)
|
|
7
|
-
expect(e).toBeInstanceOf(InvalidPayloadError)
|
|
8
|
-
expect(e.message).toBe(
|
|
9
|
-
expect(e.name).toBe(
|
|
10
|
-
})
|
|
4
|
+
test("InvalidPayloadError preserves the message and is an Error", () => {
|
|
5
|
+
const e = new InvalidPayloadError("bad base64");
|
|
6
|
+
expect(e).toBeInstanceOf(Error);
|
|
7
|
+
expect(e).toBeInstanceOf(InvalidPayloadError);
|
|
8
|
+
expect(e.message).toBe("bad base64");
|
|
9
|
+
expect(e.name).toBe("InvalidPayloadError");
|
|
10
|
+
});
|
|
11
11
|
|
|
12
|
-
test(
|
|
13
|
-
const e = new UnknownItemError(9999)
|
|
14
|
-
expect(e).toBeInstanceOf(UnknownItemError)
|
|
15
|
-
expect(e.itemId).toBe(9999)
|
|
16
|
-
expect(e.message).toContain(
|
|
17
|
-
})
|
|
12
|
+
test("UnknownItemError carries the item id", () => {
|
|
13
|
+
const e = new UnknownItemError(9999);
|
|
14
|
+
expect(e).toBeInstanceOf(UnknownItemError);
|
|
15
|
+
expect(e.itemId).toBe(9999);
|
|
16
|
+
expect(e.message).toContain("9999");
|
|
17
|
+
});
|
|
18
18
|
|
|
19
|
-
test(
|
|
20
|
-
const cause = new Error(
|
|
21
|
-
const e = new RenderError(
|
|
22
|
-
expect(e).toBeInstanceOf(RenderError)
|
|
23
|
-
expect(e.cause).toBe(cause)
|
|
24
|
-
})
|
|
19
|
+
test("RenderError wraps a cause", () => {
|
|
20
|
+
const cause = new Error("inner");
|
|
21
|
+
const e = new RenderError("render failed", { cause });
|
|
22
|
+
expect(e).toBeInstanceOf(RenderError);
|
|
23
|
+
expect(e.cause).toBe(cause);
|
|
24
|
+
});
|
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
import { ServerContract } from
|
|
1
|
+
import { ServerContract } from "@shipload/sdk";
|
|
2
2
|
|
|
3
|
-
export const ITEM_ORE_T1 = 101
|
|
4
|
-
export const ITEM_GAS_T2 = 302
|
|
5
|
-
export const ITEM_ENGINE_T1 = 10100
|
|
6
|
-
export const ITEM_GENERATOR_T1 = 10101
|
|
7
|
-
export const ITEM_GATHERER_T1 = 10102
|
|
8
|
-
export const ITEM_LOADER_T1 = 10103
|
|
9
|
-
export const ITEM_MANUFACTURING_T1 = 10104
|
|
10
|
-
export const ITEM_STORAGE_T1 = 10105
|
|
11
|
-
export const ITEM_HAULER_T1 = 10106
|
|
12
|
-
export const ITEM_SHIP_T1_PACKED = 10201
|
|
3
|
+
export const ITEM_ORE_T1 = 101;
|
|
4
|
+
export const ITEM_GAS_T2 = 302;
|
|
5
|
+
export const ITEM_ENGINE_T1 = 10100;
|
|
6
|
+
export const ITEM_GENERATOR_T1 = 10101;
|
|
7
|
+
export const ITEM_GATHERER_T1 = 10102;
|
|
8
|
+
export const ITEM_LOADER_T1 = 10103;
|
|
9
|
+
export const ITEM_MANUFACTURING_T1 = 10104;
|
|
10
|
+
export const ITEM_STORAGE_T1 = 10105;
|
|
11
|
+
export const ITEM_HAULER_T1 = 10106;
|
|
12
|
+
export const ITEM_SHIP_T1_PACKED = 10201;
|
|
13
13
|
|
|
14
|
-
export const MODULE_ENGINE = 1
|
|
15
|
-
export const MODULE_GENERATOR = 2
|
|
14
|
+
export const MODULE_ENGINE = 1;
|
|
15
|
+
export const MODULE_GENERATOR = 2;
|
|
16
16
|
|
|
17
|
-
export function cargoOreT1(stats =
|
|
17
|
+
export function cargoOreT1(stats = "0x123456789ABCDEF", quantity = 1) {
|
|
18
18
|
return ServerContract.Types.cargo_item.from({
|
|
19
19
|
item_id: ITEM_ORE_T1,
|
|
20
20
|
quantity,
|
|
21
21
|
stats,
|
|
22
22
|
modules: [],
|
|
23
|
-
})
|
|
23
|
+
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function cargoShipT1Packed(opts?: {
|
|
27
|
-
stats?: string
|
|
28
|
-
engineStats?: string
|
|
29
|
-
generatorStats?: string
|
|
30
|
-
onlyEngine?: boolean
|
|
27
|
+
stats?: string;
|
|
28
|
+
engineStats?: string;
|
|
29
|
+
generatorStats?: string;
|
|
30
|
+
onlyEngine?: boolean;
|
|
31
31
|
}) {
|
|
32
|
-
const o = opts ?? {}
|
|
33
|
-
const modules: unknown[] = []
|
|
32
|
+
const o = opts ?? {};
|
|
33
|
+
const modules: unknown[] = [];
|
|
34
34
|
modules.push({
|
|
35
35
|
type: MODULE_ENGINE,
|
|
36
|
-
installed: { item_id: ITEM_ENGINE_T1, stats: o.engineStats ??
|
|
37
|
-
})
|
|
36
|
+
installed: { item_id: ITEM_ENGINE_T1, stats: o.engineStats ?? "0x2A4F6B8C" },
|
|
37
|
+
});
|
|
38
38
|
if (!o.onlyEngine) {
|
|
39
39
|
modules.push({
|
|
40
40
|
type: MODULE_GENERATOR,
|
|
41
|
-
installed: { item_id: ITEM_GENERATOR_T1, stats: o.generatorStats ??
|
|
42
|
-
})
|
|
41
|
+
installed: { item_id: ITEM_GENERATOR_T1, stats: o.generatorStats ?? "0x1B2D4F" },
|
|
42
|
+
});
|
|
43
43
|
}
|
|
44
44
|
return ServerContract.Types.cargo_item.from({
|
|
45
45
|
item_id: ITEM_SHIP_T1_PACKED,
|
|
46
46
|
quantity: 1,
|
|
47
|
-
stats: o.stats ??
|
|
47
|
+
stats: o.stats ?? "0",
|
|
48
48
|
modules,
|
|
49
|
-
})
|
|
49
|
+
});
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export const ITEM_HULL_PLATES = 10001
|
|
52
|
+
export const ITEM_HULL_PLATES = 10001;
|
|
53
53
|
|
|
54
54
|
export const FIXTURES = {
|
|
55
|
-
oreT1: cargoOreT1(
|
|
56
|
-
oreT1StackOf50: cargoOreT1(
|
|
57
|
-
oreT1ZeroStats: cargoOreT1(
|
|
55
|
+
oreT1: cargoOreT1("0x123456789ABCDEF"),
|
|
56
|
+
oreT1StackOf50: cargoOreT1("0x123456789ABCDEF", 50),
|
|
57
|
+
oreT1ZeroStats: cargoOreT1("0"),
|
|
58
58
|
gasT2: ServerContract.Types.cargo_item.from({
|
|
59
59
|
item_id: ITEM_GAS_T2,
|
|
60
60
|
quantity: 1,
|
|
61
|
-
stats:
|
|
61
|
+
stats: "0xDEADBEEF1234",
|
|
62
62
|
modules: [],
|
|
63
63
|
}),
|
|
64
64
|
hullPlates: ServerContract.Types.cargo_item.from({
|
|
65
65
|
item_id: ITEM_HULL_PLATES,
|
|
66
66
|
quantity: 1,
|
|
67
|
-
stats:
|
|
67
|
+
stats: "0x7FFF",
|
|
68
68
|
modules: [],
|
|
69
69
|
}),
|
|
70
70
|
engineT1: ServerContract.Types.cargo_item.from({
|
|
71
71
|
item_id: ITEM_ENGINE_T1,
|
|
72
72
|
quantity: 1,
|
|
73
|
-
stats:
|
|
73
|
+
stats: "358800",
|
|
74
74
|
modules: [],
|
|
75
75
|
}),
|
|
76
76
|
generatorT1: ServerContract.Types.cargo_item.from({
|
|
77
77
|
item_id: ITEM_GENERATOR_T1,
|
|
78
78
|
quantity: 1,
|
|
79
|
-
stats:
|
|
79
|
+
stats: "683908",
|
|
80
80
|
modules: [],
|
|
81
81
|
}),
|
|
82
82
|
gathererT1: ServerContract.Types.cargo_item.from({
|
|
83
83
|
item_id: ITEM_GATHERER_T1,
|
|
84
84
|
quantity: 1,
|
|
85
|
-
stats:
|
|
85
|
+
stats: "138255128433040",
|
|
86
86
|
modules: [],
|
|
87
87
|
}),
|
|
88
88
|
loaderT1: ServerContract.Types.cargo_item.from({
|
|
89
89
|
item_id: ITEM_LOADER_T1,
|
|
90
90
|
quantity: 1,
|
|
91
|
-
stats:
|
|
91
|
+
stats: "512750",
|
|
92
92
|
modules: [],
|
|
93
93
|
}),
|
|
94
94
|
crafterT1: ServerContract.Types.cargo_item.from({
|
|
95
95
|
item_id: ITEM_MANUFACTURING_T1,
|
|
96
96
|
quantity: 1,
|
|
97
|
-
stats:
|
|
97
|
+
stats: "512600",
|
|
98
98
|
modules: [],
|
|
99
99
|
}),
|
|
100
100
|
storageT1: ServerContract.Types.cargo_item.from({
|
|
101
101
|
item_id: ITEM_STORAGE_T1,
|
|
102
102
|
quantity: 1,
|
|
103
|
-
stats:
|
|
103
|
+
stats: "537605632700",
|
|
104
104
|
modules: [],
|
|
105
105
|
}),
|
|
106
106
|
haulerT1: ServerContract.Types.cargo_item.from({
|
|
107
107
|
item_id: ITEM_HAULER_T1,
|
|
108
108
|
quantity: 1,
|
|
109
|
-
stats:
|
|
109
|
+
stats: "0x3E8",
|
|
110
110
|
modules: [],
|
|
111
111
|
}),
|
|
112
112
|
shipT1NoModules: ServerContract.Types.cargo_item.from({
|
|
113
113
|
item_id: ITEM_SHIP_T1_PACKED,
|
|
114
114
|
quantity: 1,
|
|
115
|
-
stats:
|
|
115
|
+
stats: "0",
|
|
116
116
|
modules: [],
|
|
117
117
|
}),
|
|
118
118
|
shipT1TwoModules: cargoShipT1Packed(),
|
|
119
119
|
shipT1OnlyEngine: cargoShipT1Packed({ onlyEngine: true }),
|
|
120
|
-
} as const
|
|
120
|
+
} as const;
|
|
121
121
|
|
|
122
|
-
export type FixtureName = keyof typeof FIXTURES
|
|
122
|
+
export type FixtureName = keyof typeof FIXTURES;
|
package/test/fonts.test.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import { expect, test } from
|
|
2
|
-
import { embedFontsInSvg } from
|
|
3
|
-
import { loadFontData } from
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { embedFontsInSvg } from "../src/fonts/index.ts";
|
|
3
|
+
import { loadFontData } from "../src/fonts/load-bun.ts";
|
|
4
4
|
|
|
5
|
-
test(
|
|
6
|
-
const data = await loadFontData()
|
|
5
|
+
test("loadFontData returns all four faces with non-zero bytes", async () => {
|
|
6
|
+
const data = await loadFontData();
|
|
7
7
|
expect(Object.keys(data).sort()).toEqual([
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
])
|
|
8
|
+
"inter-400",
|
|
9
|
+
"inter-600",
|
|
10
|
+
"jetbrains-500",
|
|
11
|
+
"orbitron-700",
|
|
12
|
+
]);
|
|
13
13
|
for (const [key, bytes] of Object.entries(data)) {
|
|
14
|
-
expect(bytes.byteLength, `${key} bytes`).toBeGreaterThan(2000)
|
|
15
|
-
expect(bytes.byteLength, `${key} bytes`).toBeLessThan(120_000)
|
|
14
|
+
expect(bytes.byteLength, `${key} bytes`).toBeGreaterThan(2000);
|
|
15
|
+
expect(bytes.byteLength, `${key} bytes`).toBeLessThan(120_000);
|
|
16
16
|
}
|
|
17
|
-
})
|
|
17
|
+
});
|
|
18
18
|
|
|
19
|
-
test(
|
|
20
|
-
const data = await loadFontData()
|
|
21
|
-
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>'
|
|
22
|
-
const out = embedFontsInSvg(svg, data)
|
|
23
|
-
expect(out).toContain(
|
|
24
|
-
expect(out).toContain('font-family: "Orbitron"')
|
|
25
|
-
expect(out).toContain('font-family: "Inter"')
|
|
26
|
-
expect(out).toContain('font-family: "JetBrains Mono"')
|
|
27
|
-
expect(out).toContain(
|
|
28
|
-
})
|
|
19
|
+
test("embedFontsInSvg inlines @font-face blocks", async () => {
|
|
20
|
+
const data = await loadFontData();
|
|
21
|
+
const svg = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>';
|
|
22
|
+
const out = embedFontsInSvg(svg, data);
|
|
23
|
+
expect(out).toContain("@font-face");
|
|
24
|
+
expect(out).toContain('font-family: "Orbitron"');
|
|
25
|
+
expect(out).toContain('font-family: "Inter"');
|
|
26
|
+
expect(out).toContain('font-family: "JetBrains Mono"');
|
|
27
|
+
expect(out).toContain("data:font/woff2;base64,");
|
|
28
|
+
});
|
package/test/links-meta.test.ts
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { expect, test } from
|
|
2
|
-
import { resolveItem } from
|
|
3
|
-
import { linkToItemImage, linkToItemPage, linkToItemSocial } from
|
|
4
|
-
import { itemPageMeta } from
|
|
5
|
-
import { FIXTURES } from
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { resolveItem } from "@shipload/sdk";
|
|
3
|
+
import { linkToItemImage, linkToItemPage, linkToItemSocial } from "../src/links.ts";
|
|
4
|
+
import { itemPageMeta } from "../src/meta.ts";
|
|
5
|
+
import { FIXTURES } from "./fixtures/cargo-items.ts";
|
|
6
6
|
|
|
7
|
-
test(
|
|
8
|
-
const url = linkToItemPage(FIXTURES.oreT1)
|
|
9
|
-
expect(url).toMatch(/^https:\/\/shiploadgame\.com\/guide\/item\/[A-Za-z0-9_-]+$/)
|
|
10
|
-
})
|
|
7
|
+
test("linkToItemPage defaults to shiploadgame.com", () => {
|
|
8
|
+
const url = linkToItemPage(FIXTURES.oreT1);
|
|
9
|
+
expect(url).toMatch(/^https:\/\/shiploadgame\.com\/guide\/item\/[A-Za-z0-9_-]+$/);
|
|
10
|
+
});
|
|
11
11
|
|
|
12
|
-
test(
|
|
13
|
-
const url = linkToItemPage(FIXTURES.oreT1,
|
|
14
|
-
expect(url.startsWith(
|
|
15
|
-
})
|
|
12
|
+
test("linkToItemPage accepts a custom base URL", () => {
|
|
13
|
+
const url = linkToItemPage(FIXTURES.oreT1, "http://localhost:5173");
|
|
14
|
+
expect(url.startsWith("http://localhost:5173/guide/item/")).toBe(true);
|
|
15
|
+
});
|
|
16
16
|
|
|
17
|
-
test(
|
|
18
|
-
const url = linkToItemImage(FIXTURES.oreT1,
|
|
19
|
-
expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/item\/[A-Za-z0-9_-]+\.png$/)
|
|
20
|
-
})
|
|
17
|
+
test("linkToItemImage builds a PNG URL", () => {
|
|
18
|
+
const url = linkToItemImage(FIXTURES.oreT1, "png");
|
|
19
|
+
expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/item\/[A-Za-z0-9_-]+\.png$/);
|
|
20
|
+
});
|
|
21
21
|
|
|
22
|
-
test(
|
|
23
|
-
const url = linkToItemImage(FIXTURES.oreT1,
|
|
24
|
-
expect(url).toMatch(/\.svg$/)
|
|
25
|
-
})
|
|
22
|
+
test("linkToItemImage builds an SVG URL", () => {
|
|
23
|
+
const url = linkToItemImage(FIXTURES.oreT1, "svg");
|
|
24
|
+
expect(url).toMatch(/\.svg$/);
|
|
25
|
+
});
|
|
26
26
|
|
|
27
|
-
test(
|
|
28
|
-
const url = linkToItemSocial(FIXTURES.oreT1)
|
|
29
|
-
expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/)
|
|
30
|
-
})
|
|
27
|
+
test("linkToItemSocial builds a social card URL", () => {
|
|
28
|
+
const url = linkToItemSocial(FIXTURES.oreT1);
|
|
29
|
+
expect(url).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/);
|
|
30
|
+
});
|
|
31
31
|
|
|
32
|
-
test(
|
|
33
|
-
const item = FIXTURES.oreT1
|
|
34
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
35
|
-
const meta = itemPageMeta(item, resolved)
|
|
36
|
-
expect(meta.title).toContain(
|
|
37
|
-
expect(meta.description).toContain(
|
|
38
|
-
expect(meta.description).toMatch(/Strength \d+/)
|
|
39
|
-
expect(meta.description).toMatch(/\d+(\.\d+)? t$/)
|
|
40
|
-
expect(meta.ogImage).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/)
|
|
41
|
-
expect(meta.ogImageWidth).toBe(1200)
|
|
42
|
-
expect(meta.ogImageHeight).toBe(630)
|
|
43
|
-
})
|
|
32
|
+
test("itemPageMeta produces title, description, and ogImage (social card)", () => {
|
|
33
|
+
const item = FIXTURES.oreT1;
|
|
34
|
+
const resolved = resolveItem(item.item_id, item.stats, item.modules);
|
|
35
|
+
const meta = itemPageMeta(item, resolved);
|
|
36
|
+
expect(meta.title).toContain("Crude Ore");
|
|
37
|
+
expect(meta.description).toContain("T1 Ore");
|
|
38
|
+
expect(meta.description).toMatch(/Strength \d+/);
|
|
39
|
+
expect(meta.description).toMatch(/\d+(\.\d+)? t$/);
|
|
40
|
+
expect(meta.ogImage).toMatch(/^https:\/\/item\.shiploadgame\.com\/social\/[A-Za-z0-9_-]+\.png$/);
|
|
41
|
+
expect(meta.ogImageWidth).toBe(1200);
|
|
42
|
+
expect(meta.ogImageHeight).toBe(630);
|
|
43
|
+
});
|
package/test/pixel.test.ts
CHANGED
|
@@ -1,55 +1,58 @@
|
|
|
1
|
-
import { readFile, writeFile, mkdir } from
|
|
2
|
-
import { existsSync } from
|
|
3
|
-
import { resolve } from
|
|
4
|
-
import { expect, test } from
|
|
5
|
-
import { Resvg } from
|
|
6
|
-
import pixelmatch from
|
|
7
|
-
import { PNG } from
|
|
8
|
-
import { resolveItem } from
|
|
9
|
-
import { renderItem } from
|
|
10
|
-
import { embedFontsInSvg } from
|
|
11
|
-
import { loadFontData } from
|
|
12
|
-
import { FIXTURES } from
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { expect, test } from "bun:test";
|
|
5
|
+
import { Resvg } from "@resvg/resvg-js";
|
|
6
|
+
import pixelmatch from "pixelmatch";
|
|
7
|
+
import { PNG } from "pngjs";
|
|
8
|
+
import { resolveItem } from "@shipload/sdk";
|
|
9
|
+
import { renderItem } from "../src/render.ts";
|
|
10
|
+
import { embedFontsInSvg } from "../src/fonts/index.ts";
|
|
11
|
+
import { loadFontData } from "../src/fonts/load-bun.ts";
|
|
12
|
+
import { FIXTURES } from "./fixtures/cargo-items.ts";
|
|
13
13
|
|
|
14
|
-
const SNAP_DIR = resolve(import.meta.dir,
|
|
15
|
-
const UPDATE = process.env.UPDATE_IMAGE_SNAPSHOTS ===
|
|
14
|
+
const SNAP_DIR = resolve(import.meta.dir, "__image_snapshots__");
|
|
15
|
+
const UPDATE = process.env.UPDATE_IMAGE_SNAPSHOTS === "1";
|
|
16
16
|
|
|
17
|
-
await mkdir(SNAP_DIR, { recursive: true })
|
|
18
|
-
const fontData = await loadFontData()
|
|
17
|
+
await mkdir(SNAP_DIR, { recursive: true });
|
|
18
|
+
const fontData = await loadFontData();
|
|
19
19
|
|
|
20
20
|
async function renderPng(svg: string): Promise<Buffer> {
|
|
21
21
|
const resvg = new Resvg(svg, {
|
|
22
|
-
font: {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
font: {
|
|
23
|
+
loadSystemFonts: false,
|
|
24
|
+
fontBuffers: Object.values(fontData).map((b) => Buffer.from(b)),
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return resvg.render().asPng();
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
const CASES = [
|
|
28
|
-
{ name:
|
|
29
|
-
{ name:
|
|
30
|
-
{ name:
|
|
31
|
-
{ name:
|
|
32
|
-
{ name:
|
|
33
|
-
{ name:
|
|
34
|
-
] as const
|
|
31
|
+
{ name: "resource-ore-t1", fixture: FIXTURES.oreT1 },
|
|
32
|
+
{ name: "packed-entity-ship-t1-two-modules", fixture: FIXTURES.shipT1TwoModules },
|
|
33
|
+
{ name: "packed-entity-ship-t1-only-engine", fixture: FIXTURES.shipT1OnlyEngine },
|
|
34
|
+
{ name: "component-hull-plates", fixture: FIXTURES.hullPlates },
|
|
35
|
+
{ name: "module-engine-t1", fixture: FIXTURES.engineT1 },
|
|
36
|
+
{ name: "module-storage-t1", fixture: FIXTURES.storageT1 },
|
|
37
|
+
] as const;
|
|
35
38
|
|
|
36
39
|
for (const c of CASES) {
|
|
37
40
|
test(`pixel golden — ${c.name}`, async () => {
|
|
38
|
-
const resolved = resolveItem(c.fixture.item_id, c.fixture.stats, c.fixture.modules)
|
|
39
|
-
const svg = embedFontsInSvg(renderItem(c.fixture, resolved), fontData)
|
|
40
|
-
const png = await renderPng(svg)
|
|
41
|
-
const goldPath = resolve(SNAP_DIR, `${c.name}.png`)
|
|
41
|
+
const resolved = resolveItem(c.fixture.item_id, c.fixture.stats, c.fixture.modules);
|
|
42
|
+
const svg = embedFontsInSvg(renderItem(c.fixture, resolved), fontData);
|
|
43
|
+
const png = await renderPng(svg);
|
|
44
|
+
const goldPath = resolve(SNAP_DIR, `${c.name}.png`);
|
|
42
45
|
|
|
43
46
|
if (UPDATE || !existsSync(goldPath)) {
|
|
44
|
-
await writeFile(goldPath, png)
|
|
45
|
-
return
|
|
47
|
+
await writeFile(goldPath, png);
|
|
48
|
+
return;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
const actual = PNG.sync.read(png)
|
|
49
|
-
const expected = PNG.sync.read(await readFile(goldPath))
|
|
50
|
-
expect(actual.width).toBe(expected.width)
|
|
51
|
-
expect(actual.height).toBe(expected.height)
|
|
52
|
-
const diff = new PNG({ width: actual.width, height: actual.height })
|
|
51
|
+
const actual = PNG.sync.read(png);
|
|
52
|
+
const expected = PNG.sync.read(await readFile(goldPath));
|
|
53
|
+
expect(actual.width).toBe(expected.width);
|
|
54
|
+
expect(actual.height).toBe(expected.height);
|
|
55
|
+
const diff = new PNG({ width: actual.width, height: actual.height });
|
|
53
56
|
const diffCount = pixelmatch(
|
|
54
57
|
actual.data,
|
|
55
58
|
expected.data,
|
|
@@ -57,10 +60,10 @@ for (const c of CASES) {
|
|
|
57
60
|
actual.width,
|
|
58
61
|
actual.height,
|
|
59
62
|
{ threshold: 0.1 },
|
|
60
|
-
)
|
|
63
|
+
);
|
|
61
64
|
if (diffCount > 10) {
|
|
62
|
-
await writeFile(resolve(SNAP_DIR, `${c.name}.diff.png`), PNG.sync.write(diff))
|
|
65
|
+
await writeFile(resolve(SNAP_DIR, `${c.name}.diff.png`), PNG.sync.write(diff));
|
|
63
66
|
}
|
|
64
|
-
expect(diffCount).toBeLessThanOrEqual(10)
|
|
65
|
-
})
|
|
67
|
+
expect(diffCount).toBeLessThanOrEqual(10);
|
|
68
|
+
});
|
|
66
69
|
}
|