@shipload/item-renderer 0.2.2 → 1.0.0-beta1
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/package.json +47 -49
- package/src/assets/stardust-base64.ts +1 -1
- package/src/errors.ts +14 -14
- package/src/fonts/index.ts +23 -24
- package/src/fonts/load-bun.ts +11 -11
- package/src/index.ts +28 -28
- package/src/links.ts +11 -11
- package/src/meta.ts +25 -25
- package/src/payload/base64url.ts +21 -21
- package/src/payload/codec.ts +17 -17
- package/src/primitives/category-icon.ts +90 -67
- package/src/primitives/compact-row.ts +32 -32
- package/src/primitives/divider.ts +14 -14
- package/src/primitives/icon-hex.ts +36 -34
- package/src/primitives/module-slot.ts +131 -131
- package/src/primitives/panel.ts +18 -18
- package/src/primitives/quantity-badge.ts +32 -32
- package/src/primitives/span-paragraph.ts +55 -57
- package/src/primitives/stat-bar.ts +71 -71
- package/src/primitives/svg.ts +15 -15
- package/src/primitives/text.ts +33 -33
- package/src/primitives/wrap.ts +19 -19
- package/src/render.ts +21 -25
- package/src/templates/_shared.ts +5 -6
- package/src/templates/component.ts +123 -121
- package/src/templates/index.ts +23 -23
- package/src/templates/item-cell.ts +84 -81
- package/src/templates/module.ts +177 -174
- package/src/templates/packed-entity.ts +22 -24
- package/src/templates/resource.ts +134 -134
- package/src/templates/ship-panel.ts +120 -121
- package/src/templates/social-card.ts +28 -26
- package/src/tokens/colors.ts +38 -38
- package/src/tokens/index.ts +5 -5
- package/src/tokens/spacing.ts +8 -8
- package/src/tokens/typography.ts +17 -17
- package/.github/workflows/ci.yml +0 -14
- package/.gitignore +0 -6
- package/Makefile +0 -50
- package/biome.json +0 -18
- package/bun.lock +0 -123
- package/scripts/check-bundle-size.ts +0 -37
- package/scripts/copy-fonts.ts +0 -41
- package/scripts/preview.ts +0 -43
- package/test/__image_snapshots__/.gitkeep +0 -0
- package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
- package/test/__image_snapshots__/component-hull-plates.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-engine-t1.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
- package/test/__image_snapshots__/module-storage-t1.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
- package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
- package/test/__image_snapshots__/resource-ore-t1.png +0 -0
- package/test/__snapshots__/templates-component.test.ts.snap +0 -5
- package/test/__snapshots__/templates-item-cell.test.ts.snap +0 -9
- package/test/__snapshots__/templates-module.test.ts.snap +0 -17
- package/test/__snapshots__/templates-packed-entity.test.ts.snap +0 -5
- package/test/__snapshots__/templates-resource.test.ts.snap +0 -7
- package/test/base64url.test.ts +0 -33
- package/test/codec.test.ts +0 -43
- package/test/errors.test.ts +0 -24
- package/test/fixtures/cargo-items.ts +0 -122
- package/test/fonts.test.ts +0 -28
- package/test/links-meta.test.ts +0 -43
- package/test/pixel.test.ts +0 -66
- package/test/primitives-category-icon.test.ts +0 -79
- package/test/primitives-compact-row.test.ts +0 -44
- package/test/primitives-domain.test.ts +0 -72
- package/test/primitives-layout.test.ts +0 -56
- package/test/primitives-module-slot.test.ts +0 -88
- package/test/render.test.ts +0 -40
- package/test/sanity.test.ts +0 -6
- package/test/sdk-link.test.ts +0 -19
- package/test/snapshots/.gitkeep +0 -0
- package/test/svg.test.ts +0 -28
- package/test/templates-component.test.ts +0 -36
- package/test/templates-dispatch.test.ts +0 -35
- package/test/templates-item-cell.test.ts +0 -94
- package/test/templates-module.test.ts +0 -63
- package/test/templates-packed-entity.test.ts +0 -47
- package/test/templates-resource.test.ts +0 -71
- package/test/templates-ship-panel.test.ts +0 -87
- package/test/tokens.test.ts +0 -32
- package/tsconfig.json +0 -20
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { iconHex } from '../src/primitives/icon-hex.ts'
|
|
3
|
-
import { statBar } from '../src/primitives/stat-bar.ts'
|
|
4
|
-
import { moduleSlot } from '../src/primitives/module-slot.ts'
|
|
5
|
-
import { quantityBadge } from '../src/primitives/quantity-badge.ts'
|
|
6
|
-
|
|
7
|
-
test('iconHex draws a hexagon with the category color and 2-char code', () => {
|
|
8
|
-
const svg = iconHex({ x: 14, y: 14, color: '#58d08c', code: 'FE' })
|
|
9
|
-
expect(svg).toContain('<polygon')
|
|
10
|
-
expect(svg).toContain('stroke="#58d08c"')
|
|
11
|
-
expect(svg).toContain('>FE<')
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('statBar emits a labeled bar with value ∈ [0, 1023]', () => {
|
|
15
|
-
const svg = statBar({
|
|
16
|
-
x: 14,
|
|
17
|
-
y: 100,
|
|
18
|
-
width: 252,
|
|
19
|
-
label: 'Strength',
|
|
20
|
-
abbreviation: 'STR',
|
|
21
|
-
value: 342,
|
|
22
|
-
color: '#58d08c',
|
|
23
|
-
})
|
|
24
|
-
expect(svg).toContain('STR')
|
|
25
|
-
expect(svg).toContain('Strength')
|
|
26
|
-
expect(svg).toContain('342')
|
|
27
|
-
// Fill width proportional to value/1023:
|
|
28
|
-
// expected filled width ≈ 252 * 342 / 1023 ≈ 84.2
|
|
29
|
-
expect(svg).toContain('width="84"')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('statBar inverts the visual fill when inverted is true', () => {
|
|
33
|
-
const hi = statBar({
|
|
34
|
-
x: 0, y: 0, width: 100, label: 'X', abbreviation: 'X',
|
|
35
|
-
value: 900, color: '#fff',
|
|
36
|
-
})
|
|
37
|
-
const lo = statBar({
|
|
38
|
-
x: 0, y: 0, width: 100, label: 'X', abbreviation: 'X',
|
|
39
|
-
value: 900, color: '#fff', inverted: true,
|
|
40
|
-
})
|
|
41
|
-
expect(hi).not.toBe(lo)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test('moduleSlot renders an empty state with "Empty module" label', () => {
|
|
45
|
-
const svg = moduleSlot({ x: 14, y: 200, width: 252, installed: false })
|
|
46
|
-
expect(svg).toContain('Empty module')
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
test('moduleSlot renders an installed state with capability description', () => {
|
|
50
|
-
const svg = moduleSlot({
|
|
51
|
-
x: 14,
|
|
52
|
-
y: 200,
|
|
53
|
-
width: 252,
|
|
54
|
-
installed: true,
|
|
55
|
-
capability: 'Engine',
|
|
56
|
-
description: 'generates 757 thrust for travel while draining 41 energy per distance travelled',
|
|
57
|
-
accentColor: '#58d08c',
|
|
58
|
-
})
|
|
59
|
-
expect(svg).toContain('Engine')
|
|
60
|
-
expect(svg).toContain('757')
|
|
61
|
-
expect(svg).toContain('thrust')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('quantityBadge is empty string when quantity <= 1', () => {
|
|
65
|
-
expect(quantityBadge({ x: 0, y: 0, quantity: 1 })).toBe('')
|
|
66
|
-
expect(quantityBadge({ x: 0, y: 0, quantity: 0 })).toBe('')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('quantityBadge renders ×N chip when quantity > 1', () => {
|
|
70
|
-
const svg = quantityBadge({ x: 250, y: 8, quantity: 50 })
|
|
71
|
-
expect(svg).toContain('×50')
|
|
72
|
-
})
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { panel } from '../src/primitives/panel.ts'
|
|
3
|
-
import { text } from '../src/primitives/text.ts'
|
|
4
|
-
import { divider } from '../src/primitives/divider.ts'
|
|
5
|
-
import { wrapText } from '../src/primitives/wrap.ts'
|
|
6
|
-
|
|
7
|
-
test('panel renders a rect inset by 0.5px so the 1px stroke stays inside the viewBox', () => {
|
|
8
|
-
const svg = panel({ width: 280, height: 120 })
|
|
9
|
-
expect(svg).toContain('<rect')
|
|
10
|
-
expect(svg).toContain('x="0.5"')
|
|
11
|
-
expect(svg).toContain('y="0.5"')
|
|
12
|
-
expect(svg).toContain('width="279"')
|
|
13
|
-
expect(svg).toContain('height="119"')
|
|
14
|
-
expect(svg).toContain('rx="10"')
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
test('panel accepts an optional tier border color', () => {
|
|
18
|
-
const svg = panel({ width: 280, height: 120, borderColor: '#6cb9ff' })
|
|
19
|
-
expect(svg).toContain('stroke="#6cb9ff"')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('text emits a <text> element with escaped content', () => {
|
|
23
|
-
const svg = text({ x: 10, y: 20, value: `Ship "T1"`, size: 14, weight: 600 })
|
|
24
|
-
expect(svg).toContain('<text')
|
|
25
|
-
expect(svg).toContain('x="10"')
|
|
26
|
-
expect(svg).toContain('y="20"')
|
|
27
|
-
expect(svg).toContain('Ship "T1"')
|
|
28
|
-
expect(svg).toContain('font-size="14"')
|
|
29
|
-
expect(svg).toContain('font-weight="600"')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('divider is a horizontal line at y', () => {
|
|
33
|
-
const svg = divider({ x: 14, y: 40, width: 252 })
|
|
34
|
-
expect(svg).toContain('<line')
|
|
35
|
-
expect(svg).toContain('x1="14"')
|
|
36
|
-
expect(svg).toContain('x2="266"')
|
|
37
|
-
expect(svg).toContain('y1="40"')
|
|
38
|
-
expect(svg).toContain('y2="40"')
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('wrapText splits a string into lines that fit within a char budget', () => {
|
|
42
|
-
const lines = wrapText({
|
|
43
|
-
value: 'generates 757 thrust for travel while draining 41 energy per distance travelled',
|
|
44
|
-
charsPerLine: 40,
|
|
45
|
-
})
|
|
46
|
-
for (const line of lines) expect(line.length).toBeLessThanOrEqual(40)
|
|
47
|
-
// No word is broken mid-word:
|
|
48
|
-
expect(lines.join(' ')).toBe(
|
|
49
|
-
'generates 757 thrust for travel while draining 41 energy per distance travelled',
|
|
50
|
-
)
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('wrapText preserves a single unbreakable long token', () => {
|
|
54
|
-
const lines = wrapText({ value: 'supercalifragilisticexpialidocious', charsPerLine: 10 })
|
|
55
|
-
expect(lines).toEqual(['supercalifragilisticexpialidocious'])
|
|
56
|
-
})
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import { test, expect } from 'bun:test'
|
|
2
|
-
import { moduleSlot } from '../src/primitives/module-slot.ts'
|
|
3
|
-
import type { TextSpan } from '@shipload/sdk'
|
|
4
|
-
|
|
5
|
-
test('string description flows inline with the bold capability label', () => {
|
|
6
|
-
const svg = moduleSlot({
|
|
7
|
-
x: 14,
|
|
8
|
-
y: 40,
|
|
9
|
-
width: 252,
|
|
10
|
-
installed: true,
|
|
11
|
-
capability: 'Engine',
|
|
12
|
-
description: 'generates 700 thrust for travel',
|
|
13
|
-
accentColor: '#2fd6d1',
|
|
14
|
-
})
|
|
15
|
-
expect(svg).toContain('<polygon')
|
|
16
|
-
expect(svg).toContain('Engine: ')
|
|
17
|
-
expect(svg).toContain('font-weight="600"')
|
|
18
|
-
expect(svg).toContain('generates 700 thrust for')
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
test('TextSpan[] description renders each span; highlighted spans get accent color', () => {
|
|
22
|
-
const spans: TextSpan[] = [
|
|
23
|
-
{ text: 'generates ' },
|
|
24
|
-
{ text: '700', highlight: true },
|
|
25
|
-
{ text: ' thrust for travel while draining ' },
|
|
26
|
-
{ text: '45', highlight: true },
|
|
27
|
-
{ text: ' energy per distance travelled' },
|
|
28
|
-
]
|
|
29
|
-
const svg = moduleSlot({
|
|
30
|
-
x: 14,
|
|
31
|
-
y: 40,
|
|
32
|
-
width: 252,
|
|
33
|
-
installed: true,
|
|
34
|
-
capability: 'Engine',
|
|
35
|
-
description: spans,
|
|
36
|
-
accentColor: '#2fd6d1',
|
|
37
|
-
})
|
|
38
|
-
expect(svg).toContain('Engine:')
|
|
39
|
-
expect(svg).toContain('>generates <')
|
|
40
|
-
expect(svg).toContain('>700<')
|
|
41
|
-
expect(svg).toContain('>45<')
|
|
42
|
-
expect(svg).toMatch(/fill="#[0-9A-Fa-f]{6}"[^>]*>700</)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('TextSpan[] description wraps across lines preserving highlight boundaries', () => {
|
|
46
|
-
const spans: TextSpan[] = [
|
|
47
|
-
{ text: 'mines resources at ' },
|
|
48
|
-
{ text: '880', highlight: true },
|
|
49
|
-
{ text: ' speed to a max depth of ' },
|
|
50
|
-
{ text: '248', highlight: true },
|
|
51
|
-
{ text: ' with ' },
|
|
52
|
-
{ text: '100', highlight: true },
|
|
53
|
-
{ text: ' gather speed while draining ' },
|
|
54
|
-
{ text: '1,250', highlight: true },
|
|
55
|
-
{ text: ' energy per second' },
|
|
56
|
-
]
|
|
57
|
-
const svg = moduleSlot({
|
|
58
|
-
x: 14,
|
|
59
|
-
y: 40,
|
|
60
|
-
width: 252,
|
|
61
|
-
installed: true,
|
|
62
|
-
capability: 'Gatherer',
|
|
63
|
-
description: spans,
|
|
64
|
-
accentColor: '#f59e0b',
|
|
65
|
-
})
|
|
66
|
-
expect(svg).toContain('880')
|
|
67
|
-
expect(svg).toContain('248')
|
|
68
|
-
expect(svg).toContain('100')
|
|
69
|
-
expect(svg).toContain('1,250')
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
test('Empty description array renders headline only', () => {
|
|
73
|
-
const svg = moduleSlot({
|
|
74
|
-
x: 14,
|
|
75
|
-
y: 40,
|
|
76
|
-
width: 252,
|
|
77
|
-
installed: true,
|
|
78
|
-
capability: 'Engine',
|
|
79
|
-
description: [],
|
|
80
|
-
accentColor: '#2fd6d1',
|
|
81
|
-
})
|
|
82
|
-
expect(svg).toContain('Engine:')
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
test('Empty slot renders unchanged', () => {
|
|
86
|
-
const svg = moduleSlot({ x: 14, y: 40, width: 252, installed: false })
|
|
87
|
-
expect(svg).toContain('Empty module')
|
|
88
|
-
})
|
package/test/render.test.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { resolveItem } from '@shipload/sdk'
|
|
3
|
-
import { renderItem, renderFromPayload } from '../src/render.ts'
|
|
4
|
-
import { RenderError } from '../src/errors.ts'
|
|
5
|
-
import { encodePayload } from '../src/payload/codec.ts'
|
|
6
|
-
import { FIXTURES } from './fixtures/cargo-items.ts'
|
|
7
|
-
|
|
8
|
-
test('renderItem dispatches to resource template for resources', () => {
|
|
9
|
-
const item = FIXTURES.oreT1
|
|
10
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
11
|
-
const svg = renderItem(item, resolved)
|
|
12
|
-
expect(svg).toContain('Crude Ore')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('renderItem dispatches to packed entity template for entities', () => {
|
|
16
|
-
const item = FIXTURES.shipT1TwoModules
|
|
17
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
18
|
-
const svg = renderItem(item, resolved)
|
|
19
|
-
expect(svg).toContain('HULL')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('renderItem dispatches to module template for modules', () => {
|
|
23
|
-
const item = FIXTURES.engineT1
|
|
24
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
25
|
-
const svg = renderItem(item, resolved)
|
|
26
|
-
expect(svg).toContain('MODULE')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
test('renderItem throws RenderError for unknown types', () => {
|
|
30
|
-
const item = FIXTURES.oreT1
|
|
31
|
-
const fake = { ...resolveItem(item.item_id, item.stats, item.modules), itemType: 'unknown' as never }
|
|
32
|
-
expect(() => renderItem(item, fake)).toThrow(RenderError)
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('renderFromPayload round-trips a payload into SVG', async () => {
|
|
36
|
-
const payload = encodePayload(FIXTURES.oreT1)
|
|
37
|
-
const { svg, item } = await renderFromPayload(payload)
|
|
38
|
-
expect(svg).toContain('Crude Ore')
|
|
39
|
-
expect(item.itemType).toBe('resource')
|
|
40
|
-
})
|
package/test/sanity.test.ts
DELETED
package/test/sdk-link.test.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { resolveItem, ServerContract, type ResolvedItem } from '@shipload/sdk'
|
|
3
|
-
|
|
4
|
-
test('sdkv2 is linked and exposes resolveItem + ResolvedItem type', () => {
|
|
5
|
-
const resolved: ResolvedItem = resolveItem(101 /* T1 Ore */, undefined, undefined)
|
|
6
|
-
expect(resolved.itemId).toBe(101)
|
|
7
|
-
expect(resolved.category).toBe('ore')
|
|
8
|
-
expect(resolved.itemType).toBe('resource')
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
test('ServerContract.Types.cargo_item can be constructed', () => {
|
|
12
|
-
const ci = ServerContract.Types.cargo_item.from({
|
|
13
|
-
item_id: 101,
|
|
14
|
-
quantity: 1,
|
|
15
|
-
stats: '0',
|
|
16
|
-
modules: [],
|
|
17
|
-
})
|
|
18
|
-
expect(ci.item_id.equals(101)).toBe(true)
|
|
19
|
-
})
|
package/test/snapshots/.gitkeep
DELETED
|
File without changes
|
package/test/svg.test.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { attr, el, escapeXml } from '../src/primitives/svg.ts'
|
|
3
|
-
|
|
4
|
-
test('escapeXml escapes the five XML entity chars', () => {
|
|
5
|
-
expect(escapeXml(`5 > 3 & 2 < 4 "hi" 'yo'`)).toBe(
|
|
6
|
-
'5 > 3 & 2 < 4 "hi" 'yo'',
|
|
7
|
-
)
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
test('attr emits name=value pairs with quoted values', () => {
|
|
11
|
-
expect(attr({ width: 10, height: 20, fill: '#123456' }))
|
|
12
|
-
.toBe(' width="10" height="20" fill="#123456"')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('attr drops undefined and null values', () => {
|
|
16
|
-
expect(attr({ width: 10, height: undefined, stroke: null as unknown as string }))
|
|
17
|
-
.toBe(' width="10"')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('attr escapes special chars in values', () => {
|
|
21
|
-
expect(attr({ label: `ship "fast"` })).toBe(' label="ship "fast""')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('el builds self-closing and wrapping elements', () => {
|
|
25
|
-
expect(el('rect', { width: 10, height: 10 })).toBe('<rect width="10" height="10"/>')
|
|
26
|
-
expect(el('g', {}, '<rect/><rect/>')).toBe('<g><rect/><rect/></g>')
|
|
27
|
-
expect(el('text', { x: 5 }, 'hi')).toBe('<text x="5">hi</text>')
|
|
28
|
-
})
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { resolveItem } from '@shipload/sdk'
|
|
3
|
-
import { renderComponent } from '../src/templates/component.ts'
|
|
4
|
-
import { FIXTURES } from './fixtures/cargo-items.ts'
|
|
5
|
-
|
|
6
|
-
test('matches the committed Hull Plates snapshot', async () => {
|
|
7
|
-
const item = FIXTURES.hullPlates
|
|
8
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
9
|
-
const svg = renderComponent(item, resolved)
|
|
10
|
-
expect(svg).toMatchSnapshot('component-hull-plates.svg')
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
test('renderComponent ranges mode shows stat abbreviations with no values', () => {
|
|
14
|
-
const item = FIXTURES.hullPlates
|
|
15
|
-
const resolved = resolveItem(item.item_id)
|
|
16
|
-
const svg = renderComponent(item, resolved, { mode: 'ranges' })
|
|
17
|
-
expect(svg).toContain('STR')
|
|
18
|
-
expect(svg).toContain('DEN')
|
|
19
|
-
expect(svg).not.toMatch(/>\d{3}<\/text>/)
|
|
20
|
-
expect(svg).toContain('COMPONENT')
|
|
21
|
-
expect(svg).toContain('Mass')
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
test('renderComponent values mode (default) still shows concrete numbers', () => {
|
|
25
|
-
const item = FIXTURES.hullPlates
|
|
26
|
-
const resolved = resolveItem(item.item_id, item.stats)
|
|
27
|
-
const svg = renderComponent(item, resolved)
|
|
28
|
-
expect(svg).toMatch(/>\d+<\/text>/)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('renderComponent ranges mode matches snapshot', () => {
|
|
32
|
-
const item = FIXTURES.hullPlates
|
|
33
|
-
const resolved = resolveItem(item.item_id)
|
|
34
|
-
const svg = renderComponent(item, resolved, { mode: 'ranges' })
|
|
35
|
-
expect(svg).toMatchSnapshot('component-ranges')
|
|
36
|
-
})
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { test, expect } from 'bun:test'
|
|
2
|
-
import { resolveItem } from '@shipload/sdk'
|
|
3
|
-
import { renderByType } from '../src/templates/index.ts'
|
|
4
|
-
import { FIXTURES } from './fixtures/cargo-items.ts'
|
|
5
|
-
|
|
6
|
-
test('renderByType forwards mode=ranges to resource template', () => {
|
|
7
|
-
const item = FIXTURES.oreT1
|
|
8
|
-
const resolved = resolveItem(item.item_id)
|
|
9
|
-
const svg = renderByType(item, resolved, { mode: 'ranges' })
|
|
10
|
-
// No 3-digit stat values
|
|
11
|
-
expect(svg).not.toMatch(/>\d{3,}<\/text>/)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('renderByType forwards mode=ranges to component template', () => {
|
|
15
|
-
const item = FIXTURES.hullPlates
|
|
16
|
-
const resolved = resolveItem(item.item_id)
|
|
17
|
-
const svg = renderByType(item, resolved, { mode: 'ranges' })
|
|
18
|
-
expect(svg).not.toMatch(/>\d{3,}<\/text>/)
|
|
19
|
-
expect(svg).toContain('COMPONENT')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('renderByType forwards mode=ranges to module template', () => {
|
|
23
|
-
const item = FIXTURES.engineT1
|
|
24
|
-
const resolved = resolveItem(item.item_id)
|
|
25
|
-
const svg = renderByType(item, resolved, { mode: 'ranges' })
|
|
26
|
-
expect(svg).toContain('MODULE')
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
test('renderByType default mode is values (no opts)', () => {
|
|
30
|
-
const item = FIXTURES.oreT1
|
|
31
|
-
const resolved = resolveItem(item.item_id, item.stats)
|
|
32
|
-
const svg = renderByType(item, resolved)
|
|
33
|
-
// At least one numeric stat value
|
|
34
|
-
expect(svg).toMatch(/>\d+<\/text>/)
|
|
35
|
-
})
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { test, expect } from 'bun:test'
|
|
2
|
-
import { resolveItem } from '@shipload/sdk'
|
|
3
|
-
import { ITEM_HULL_PLATES, ITEM_ENGINE_T1, ITEM_SHIP_T1_PACKED } from '@shipload/sdk'
|
|
4
|
-
import { renderItemCell, itemCellGroup } from '../src/templates/item-cell.ts'
|
|
5
|
-
|
|
6
|
-
test('renderItemCell returns a self-contained <svg>', () => {
|
|
7
|
-
const resolved = resolveItem(ITEM_HULL_PLATES)
|
|
8
|
-
const svg = renderItemCell({ resolved, size: 48 })
|
|
9
|
-
expect(svg.startsWith('<svg ')).toBe(true)
|
|
10
|
-
expect(svg).toContain('viewBox="0 0 48 60"')
|
|
11
|
-
expect(svg.endsWith('</svg>')).toBe(true)
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
test('component cell renders abbreviation', () => {
|
|
15
|
-
const resolved = resolveItem(ITEM_HULL_PLATES)
|
|
16
|
-
const svg = renderItemCell({ resolved, size: 48 })
|
|
17
|
-
expect(svg).toContain('>HP<')
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
test('module cell renders abbreviation', () => {
|
|
21
|
-
const resolved = resolveItem(ITEM_ENGINE_T1)
|
|
22
|
-
const svg = renderItemCell({ resolved, size: 48 })
|
|
23
|
-
expect(svg).toContain('>EN<')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('entity cell renders abbreviation', () => {
|
|
27
|
-
const resolved = resolveItem(ITEM_SHIP_T1_PACKED)
|
|
28
|
-
const svg = renderItemCell({ resolved, size: 48 })
|
|
29
|
-
expect(svg).toContain('>SH<')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('resource cell renders category icon (no abbreviation)', () => {
|
|
33
|
-
const resolved = resolveItem(101)
|
|
34
|
-
const svg = renderItemCell({ resolved, size: 48 })
|
|
35
|
-
expect(svg).not.toMatch(/>[A-Z]{2,3}</)
|
|
36
|
-
expect(svg).toMatch(/<(polygon|circle|rect)\b/)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('quantity renders as plain bold number when quantity > 1', () => {
|
|
40
|
-
const resolved = resolveItem(ITEM_HULL_PLATES)
|
|
41
|
-
const svg = renderItemCell({ resolved, quantity: 42, size: 48 })
|
|
42
|
-
expect(svg).toContain('>42<')
|
|
43
|
-
expect(svg).not.toContain('×')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('no quantity text when quantity is 1 or omitted', () => {
|
|
47
|
-
const resolved = resolveItem(ITEM_HULL_PLATES)
|
|
48
|
-
const svgNoQty = renderItemCell({ resolved, size: 48 })
|
|
49
|
-
const svgOne = renderItemCell({ resolved, quantity: 1, size: 48 })
|
|
50
|
-
expect(svgNoQty).not.toContain('>1<')
|
|
51
|
-
expect(svgOne).not.toContain('>1<')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('itemCellGroup returns <g> with translate, no <svg> wrapper', () => {
|
|
55
|
-
const resolved = resolveItem(ITEM_HULL_PLATES)
|
|
56
|
-
const g = itemCellGroup({ resolved, size: 48, x: 100, y: 200 })
|
|
57
|
-
expect(g.startsWith('<g ')).toBe(true)
|
|
58
|
-
expect(g).toContain('transform="translate(100, 200)"')
|
|
59
|
-
expect(g.startsWith('<svg')).toBe(false)
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
test('tier border uses SDK tierColors for the resolved tier', () => {
|
|
63
|
-
const resolved = resolveItem(ITEM_HULL_PLATES)
|
|
64
|
-
const svg = renderItemCell({ resolved, size: 48 })
|
|
65
|
-
expect(svg).toContain('#8b8b8b')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('abbreviation cell uses proportional font size for different sizes', () => {
|
|
69
|
-
const resolved = resolveItem(ITEM_HULL_PLATES)
|
|
70
|
-
const svg28 = renderItemCell({ resolved, size: 28 })
|
|
71
|
-
const svg80 = renderItemCell({ resolved, size: 80 })
|
|
72
|
-
expect(svg28).toContain('font-size="8"')
|
|
73
|
-
expect(svg80).toContain('font-size="22"')
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
test('resource icon renders in stroke-only mode', () => {
|
|
77
|
-
const resolved = resolveItem(101)
|
|
78
|
-
const svg = renderItemCell({ resolved, size: 48 })
|
|
79
|
-
expect(svg).toContain('fill="none"')
|
|
80
|
-
expect(svg).toContain('stroke-width="1.5"')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
test('matches golden SVG snapshot per itemType', () => {
|
|
84
|
-
const cases: [number, string][] = [
|
|
85
|
-
[101, 'item-cell-resource'],
|
|
86
|
-
[ITEM_HULL_PLATES, 'item-cell-component'],
|
|
87
|
-
[ITEM_ENGINE_T1, 'item-cell-module'],
|
|
88
|
-
[ITEM_SHIP_T1_PACKED, 'item-cell-entity'],
|
|
89
|
-
]
|
|
90
|
-
for (const [id, name] of cases) {
|
|
91
|
-
const svg = renderItemCell({ resolved: resolveItem(id), quantity: 3, size: 48 })
|
|
92
|
-
expect(svg).toMatchSnapshot(name)
|
|
93
|
-
}
|
|
94
|
-
})
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { test, expect } from 'bun:test'
|
|
2
|
-
import { resolveItem } from '@shipload/sdk'
|
|
3
|
-
import { renderModule } from '../src/templates/module.ts'
|
|
4
|
-
import { FIXTURES } from './fixtures/cargo-items.ts'
|
|
5
|
-
|
|
6
|
-
const CASES = [
|
|
7
|
-
'engineT1',
|
|
8
|
-
'generatorT1',
|
|
9
|
-
'gathererT1',
|
|
10
|
-
'loaderT1',
|
|
11
|
-
'crafterT1',
|
|
12
|
-
'storageT1',
|
|
13
|
-
'haulerT1',
|
|
14
|
-
] as const
|
|
15
|
-
|
|
16
|
-
for (const name of CASES) {
|
|
17
|
-
test(`matches the committed ${name} snapshot`, async () => {
|
|
18
|
-
const item = FIXTURES[name]
|
|
19
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
20
|
-
const svg = renderModule(item, resolved)
|
|
21
|
-
expect(svg).toMatchSnapshot(`module-${name}.svg`)
|
|
22
|
-
})
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
test('Engine template embeds the narrative description', () => {
|
|
26
|
-
const item = FIXTURES.engineT1
|
|
27
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
28
|
-
const svg = renderModule(item, resolved)
|
|
29
|
-
expect(svg).toContain('generates')
|
|
30
|
-
expect(svg).toContain('thrust for travel')
|
|
31
|
-
expect(svg).toContain('while draining')
|
|
32
|
-
expect(svg).toContain('distance travelled')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
test('Hauler template falls back to compact rows when description is null', () => {
|
|
36
|
-
const item = FIXTURES.haulerT1
|
|
37
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
38
|
-
const svg = renderModule(item, resolved)
|
|
39
|
-
expect(svg).toContain('Hauler')
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('renderModule ranges mode shows capability header without narrative or attribute values', () => {
|
|
43
|
-
const item = FIXTURES.engineT1
|
|
44
|
-
const resolved = resolveItem(item.item_id)
|
|
45
|
-
const svg = renderModule(item, resolved, { mode: 'ranges' })
|
|
46
|
-
expect(svg).toContain('ENGINE')
|
|
47
|
-
expect(svg).not.toMatch(/>\d{3,}<\/(text|tspan)>/)
|
|
48
|
-
expect(svg).toContain('MODULE')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('renderModule values mode (default) still shows narrative', () => {
|
|
52
|
-
const item = FIXTURES.engineT1
|
|
53
|
-
const resolved = resolveItem(item.item_id, item.stats)
|
|
54
|
-
const svg = renderModule(item, resolved)
|
|
55
|
-
expect(svg).toMatch(/thrust|energy|generates/)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
test('renderModule ranges mode matches snapshot', () => {
|
|
59
|
-
const item = FIXTURES.engineT1
|
|
60
|
-
const resolved = resolveItem(item.item_id)
|
|
61
|
-
const svg = renderModule(item, resolved, { mode: 'ranges' })
|
|
62
|
-
expect(svg).toMatchSnapshot('module-ranges')
|
|
63
|
-
})
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { resolveItem } from '@shipload/sdk'
|
|
3
|
-
import { renderPackedEntity } from '../src/templates/packed-entity.ts'
|
|
4
|
-
import { FIXTURES } from './fixtures/cargo-items.ts'
|
|
5
|
-
|
|
6
|
-
test('renders Ship with hull attributes and two modules', () => {
|
|
7
|
-
const item = FIXTURES.shipT1TwoModules
|
|
8
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
9
|
-
const svg = renderPackedEntity(item, resolved)
|
|
10
|
-
expect(svg).toContain('Ship (Packed)')
|
|
11
|
-
expect(svg).toContain('HULL')
|
|
12
|
-
expect(svg).toContain('Mass')
|
|
13
|
-
expect(svg).toContain('Capacity')
|
|
14
|
-
expect(svg).toContain('Engine')
|
|
15
|
-
expect(svg).toContain('Generator')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('renders empty-module rows when slots are unfilled', () => {
|
|
19
|
-
const item = FIXTURES.shipT1NoModules
|
|
20
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
21
|
-
const svg = renderPackedEntity(item, resolved)
|
|
22
|
-
expect(svg.match(/Empty module/g)?.length ?? 0).toBeGreaterThanOrEqual(1)
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('matches the committed Ship T1 (two modules) snapshot', async () => {
|
|
26
|
-
const item = FIXTURES.shipT1TwoModules
|
|
27
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
28
|
-
const svg = renderPackedEntity(item, resolved)
|
|
29
|
-
expect(svg).toMatchSnapshot('packed-entity-ship-t1-two-modules.svg')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('matches the committed Ship T1 (only engine) snapshot', async () => {
|
|
33
|
-
const item = FIXTURES.shipT1OnlyEngine
|
|
34
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
35
|
-
const svg = renderPackedEntity(item, resolved)
|
|
36
|
-
expect(svg).toMatchSnapshot('packed-entity-ship-t1-only-engine.svg')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('ship with two modules renders SDK-sourced narrative descriptions', () => {
|
|
40
|
-
const item = FIXTURES.shipT1TwoModules
|
|
41
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
42
|
-
const svg = renderPackedEntity(item, resolved)
|
|
43
|
-
expect(svg).toContain('Engine: ')
|
|
44
|
-
expect(svg).toContain('generates')
|
|
45
|
-
expect(svg).toContain('Generator: ')
|
|
46
|
-
expect(svg).toContain('holds')
|
|
47
|
-
})
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { expect, test } from 'bun:test'
|
|
2
|
-
import { resolveItem, getStatDefinitions } from '@shipload/sdk'
|
|
3
|
-
import { renderResource } from '../src/templates/resource.ts'
|
|
4
|
-
import { FIXTURES } from './fixtures/cargo-items.ts'
|
|
5
|
-
|
|
6
|
-
test('renders Crude Ore with category, mass, and three stat bars', () => {
|
|
7
|
-
const item = FIXTURES.oreT1
|
|
8
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
9
|
-
const svg = renderResource(item, resolved)
|
|
10
|
-
expect(svg).toContain('Crude Ore')
|
|
11
|
-
expect(svg).toContain('Ore') // category label
|
|
12
|
-
expect(svg).toContain('30,000') // mass
|
|
13
|
-
expect(svg).toContain('STR') // strength abbreviation
|
|
14
|
-
expect(svg).toContain('TOL')
|
|
15
|
-
expect(svg).toContain('DEN')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('renders quantity badge when stack > 1', () => {
|
|
19
|
-
const item = FIXTURES.oreT1StackOf50
|
|
20
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
21
|
-
const svg = renderResource(item, resolved)
|
|
22
|
-
expect(svg).toContain('×50')
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
test('does not render quantity badge when stack == 1', () => {
|
|
26
|
-
const item = FIXTURES.oreT1
|
|
27
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
28
|
-
const svg = renderResource(item, resolved)
|
|
29
|
-
expect(svg).not.toContain('×')
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('matches the committed Crude Ore snapshot', async () => {
|
|
33
|
-
const item = FIXTURES.oreT1
|
|
34
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
35
|
-
const svg = renderResource(item, resolved)
|
|
36
|
-
expect(svg).toMatchSnapshot('resource-ore-t1.svg')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('matches the committed Dense Gas snapshot', async () => {
|
|
40
|
-
const item = FIXTURES.gasT2
|
|
41
|
-
const resolved = resolveItem(item.item_id, item.stats, item.modules)
|
|
42
|
-
const svg = renderResource(item, resolved)
|
|
43
|
-
expect(svg).toMatchSnapshot('resource-gas-t2.svg')
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
test('renderResource ranges mode shows stat abbreviations with no values', () => {
|
|
47
|
-
const item = FIXTURES.oreT1
|
|
48
|
-
const resolved = resolveItem(item.item_id)
|
|
49
|
-
const svg = renderResource(item, resolved, { mode: 'ranges' })
|
|
50
|
-
const defs = getStatDefinitions(resolved.category!)
|
|
51
|
-
for (const def of defs) {
|
|
52
|
-
expect(svg).toContain(def.abbreviation)
|
|
53
|
-
}
|
|
54
|
-
expect(svg).not.toMatch(/>\d{3}<\/text>/)
|
|
55
|
-
expect(svg).toContain('Category')
|
|
56
|
-
expect(svg).toContain('Mass')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
test('renderResource values mode (default) still shows concrete numbers', () => {
|
|
60
|
-
const item = FIXTURES.oreT1
|
|
61
|
-
const resolved = resolveItem(item.item_id, item.stats)
|
|
62
|
-
const svg = renderResource(item, resolved)
|
|
63
|
-
expect(svg).toMatch(/>\d+<\/text>/)
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('renderResource ranges mode matches snapshot', () => {
|
|
67
|
-
const item = FIXTURES.oreT1
|
|
68
|
-
const resolved = resolveItem(item.item_id)
|
|
69
|
-
const svg = renderResource(item, resolved, { mode: 'ranges' })
|
|
70
|
-
expect(svg).toMatchSnapshot('resource-ranges')
|
|
71
|
-
})
|