@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.
Files changed (88) hide show
  1. package/package.json +47 -49
  2. package/src/assets/stardust-base64.ts +1 -1
  3. package/src/errors.ts +14 -14
  4. package/src/fonts/index.ts +23 -24
  5. package/src/fonts/load-bun.ts +11 -11
  6. package/src/index.ts +28 -28
  7. package/src/links.ts +11 -11
  8. package/src/meta.ts +25 -25
  9. package/src/payload/base64url.ts +21 -21
  10. package/src/payload/codec.ts +17 -17
  11. package/src/primitives/category-icon.ts +90 -67
  12. package/src/primitives/compact-row.ts +32 -32
  13. package/src/primitives/divider.ts +14 -14
  14. package/src/primitives/icon-hex.ts +36 -34
  15. package/src/primitives/module-slot.ts +131 -131
  16. package/src/primitives/panel.ts +18 -18
  17. package/src/primitives/quantity-badge.ts +32 -32
  18. package/src/primitives/span-paragraph.ts +55 -57
  19. package/src/primitives/stat-bar.ts +71 -71
  20. package/src/primitives/svg.ts +15 -15
  21. package/src/primitives/text.ts +33 -33
  22. package/src/primitives/wrap.ts +19 -19
  23. package/src/render.ts +21 -25
  24. package/src/templates/_shared.ts +5 -6
  25. package/src/templates/component.ts +123 -121
  26. package/src/templates/index.ts +23 -23
  27. package/src/templates/item-cell.ts +84 -81
  28. package/src/templates/module.ts +177 -174
  29. package/src/templates/packed-entity.ts +22 -24
  30. package/src/templates/resource.ts +134 -134
  31. package/src/templates/ship-panel.ts +120 -121
  32. package/src/templates/social-card.ts +28 -26
  33. package/src/tokens/colors.ts +38 -38
  34. package/src/tokens/index.ts +5 -5
  35. package/src/tokens/spacing.ts +8 -8
  36. package/src/tokens/typography.ts +17 -17
  37. package/.github/workflows/ci.yml +0 -14
  38. package/.gitignore +0 -6
  39. package/Makefile +0 -50
  40. package/biome.json +0 -18
  41. package/bun.lock +0 -123
  42. package/scripts/check-bundle-size.ts +0 -37
  43. package/scripts/copy-fonts.ts +0 -41
  44. package/scripts/preview.ts +0 -43
  45. package/test/__image_snapshots__/.gitkeep +0 -0
  46. package/test/__image_snapshots__/component-hull-plates.diff.png +0 -0
  47. package/test/__image_snapshots__/component-hull-plates.png +0 -0
  48. package/test/__image_snapshots__/module-engine-t1.diff.png +0 -0
  49. package/test/__image_snapshots__/module-engine-t1.png +0 -0
  50. package/test/__image_snapshots__/module-storage-t1.diff.png +0 -0
  51. package/test/__image_snapshots__/module-storage-t1.png +0 -0
  52. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.diff.png +0 -0
  53. package/test/__image_snapshots__/packed-entity-ship-t1-only-engine.png +0 -0
  54. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.diff.png +0 -0
  55. package/test/__image_snapshots__/packed-entity-ship-t1-two-modules.png +0 -0
  56. package/test/__image_snapshots__/resource-ore-t1.diff.png +0 -0
  57. package/test/__image_snapshots__/resource-ore-t1.png +0 -0
  58. package/test/__snapshots__/templates-component.test.ts.snap +0 -5
  59. package/test/__snapshots__/templates-item-cell.test.ts.snap +0 -9
  60. package/test/__snapshots__/templates-module.test.ts.snap +0 -17
  61. package/test/__snapshots__/templates-packed-entity.test.ts.snap +0 -5
  62. package/test/__snapshots__/templates-resource.test.ts.snap +0 -7
  63. package/test/base64url.test.ts +0 -33
  64. package/test/codec.test.ts +0 -43
  65. package/test/errors.test.ts +0 -24
  66. package/test/fixtures/cargo-items.ts +0 -122
  67. package/test/fonts.test.ts +0 -28
  68. package/test/links-meta.test.ts +0 -43
  69. package/test/pixel.test.ts +0 -66
  70. package/test/primitives-category-icon.test.ts +0 -79
  71. package/test/primitives-compact-row.test.ts +0 -44
  72. package/test/primitives-domain.test.ts +0 -72
  73. package/test/primitives-layout.test.ts +0 -56
  74. package/test/primitives-module-slot.test.ts +0 -88
  75. package/test/render.test.ts +0 -40
  76. package/test/sanity.test.ts +0 -6
  77. package/test/sdk-link.test.ts +0 -19
  78. package/test/snapshots/.gitkeep +0 -0
  79. package/test/svg.test.ts +0 -28
  80. package/test/templates-component.test.ts +0 -36
  81. package/test/templates-dispatch.test.ts +0 -35
  82. package/test/templates-item-cell.test.ts +0 -94
  83. package/test/templates-module.test.ts +0 -63
  84. package/test/templates-packed-entity.test.ts +0 -47
  85. package/test/templates-resource.test.ts +0 -71
  86. package/test/templates-ship-panel.test.ts +0 -87
  87. package/test/tokens.test.ts +0 -32
  88. 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 &quot;T1&quot;')
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
- })
@@ -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
- })
@@ -1,6 +0,0 @@
1
- import { expect, test } from 'bun:test'
2
- import { VERSION } from '../src/index.ts'
3
-
4
- test('VERSION is a semver string', () => {
5
- expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/)
6
- })
@@ -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
- })
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 &gt; 3 &amp; 2 &lt; 4 &quot;hi&quot; &apos;yo&apos;',
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 &quot;fast&quot;"')
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
- })