@thermal-label/brother-ql-core 0.3.0 → 0.5.0
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/README.md +1 -1
- package/data/devices.json +823 -0
- package/data/media.json +823 -0
- package/dist/__tests__/devices.test.js +112 -31
- package/dist/__tests__/devices.test.js.map +1 -1
- package/dist/__tests__/media.test.js +251 -1
- package/dist/__tests__/media.test.js.map +1 -1
- package/dist/__tests__/protocol.test.js +168 -1
- package/dist/__tests__/protocol.test.js.map +1 -1
- package/dist/__tests__/status.test.js +71 -0
- package/dist/__tests__/status.test.js.map +1 -1
- package/dist/devices.d.ts +13 -270
- package/dist/devices.d.ts.map +1 -1
- package/dist/devices.generated.d.ts +696 -0
- package/dist/devices.generated.d.ts.map +1 -0
- package/dist/devices.generated.js +831 -0
- package/dist/devices.generated.js.map +1 -0
- package/dist/devices.js +28 -272
- package/dist/devices.js.map +1 -1
- package/dist/index.d.ts +13 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/media.d.ts +37 -22
- package/dist/media.d.ts.map +1 -1
- package/dist/media.generated.d.ts +4 -0
- package/dist/media.generated.d.ts.map +1 -0
- package/dist/media.generated.js +1640 -0
- package/dist/media.generated.js.map +1 -0
- package/dist/media.js +74 -281
- package/dist/media.js.map +1 -1
- package/dist/protocol.d.ts +54 -3
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +117 -18
- package/dist/protocol.js.map +1 -1
- package/dist/status.d.ts +4 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +6 -2
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +92 -27
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -9
- package/src/__tests__/devices.test.ts +122 -32
- package/src/__tests__/media.test.ts +287 -1
- package/src/__tests__/protocol.test.ts +209 -0
- package/src/__tests__/status.test.ts +87 -0
- package/src/devices.generated.ts +840 -0
- package/src/devices.ts +30 -272
- package/src/index.ts +28 -4
- package/src/media.generated.ts +1644 -0
- package/src/media.ts +86 -282
- package/src/protocol.ts +196 -18
- package/src/status.ts +10 -3
- package/src/types.ts +93 -27
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { DEVICES, findDevice, isMassStorageMode } from '../devices.js';
|
|
2
|
+
import { DEVICES, findDevice, getUsbIds, isMassStorageMode } from '../devices.js';
|
|
3
3
|
|
|
4
4
|
describe('findDevice', () => {
|
|
5
|
-
it('returns correct
|
|
6
|
-
const dev = findDevice(0x04f9,
|
|
5
|
+
it('returns correct entry for QL-820NWBc (PID shared with QL-820NWB)', () => {
|
|
6
|
+
const dev = findDevice(0x04f9, 0x209d);
|
|
7
7
|
expect(dev).toBeDefined();
|
|
8
|
-
expect(dev!.name).toBe('QL-
|
|
9
|
-
expect(dev!.twoColor).toBe(true);
|
|
8
|
+
expect(dev!.name).toBe('QL-820NWBc');
|
|
9
|
+
expect(dev!.engines[0]?.capabilities?.twoColor).toBe(true);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
it('returns correct
|
|
13
|
-
const dev = findDevice(0x04f9,
|
|
12
|
+
it('returns correct entry for QL-1100', () => {
|
|
13
|
+
const dev = findDevice(0x04f9, 0x20a7);
|
|
14
14
|
expect(dev).toBeDefined();
|
|
15
|
-
expect(dev!.name).toBe('QL-
|
|
16
|
-
expect(dev!.
|
|
15
|
+
expect(dev!.name).toBe('QL-1100');
|
|
16
|
+
expect(dev!.engines[0]?.headDots).toBe(1296);
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it('returns correct
|
|
19
|
+
it('returns correct entry for QL-500', () => {
|
|
20
20
|
const dev = findDevice(0x04f9, 0x2013);
|
|
21
21
|
expect(dev).toBeDefined();
|
|
22
22
|
expect(dev!.name).toBe('QL-500');
|
|
23
|
-
|
|
23
|
+
// QL-500 has no autocut — the capability flag is absent.
|
|
24
|
+
expect(dev!.engines[0]?.capabilities?.autocut).toBeUndefined();
|
|
24
25
|
});
|
|
25
26
|
|
|
26
27
|
it('returns undefined for unknown PID', () => {
|
|
@@ -28,39 +29,44 @@ describe('findDevice', () => {
|
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
it('returns undefined for unknown VID', () => {
|
|
31
|
-
expect(findDevice(0x1234,
|
|
32
|
+
expect(findDevice(0x1234, 0x209d)).toBeUndefined();
|
|
32
33
|
});
|
|
33
34
|
});
|
|
34
35
|
|
|
35
36
|
describe('isMassStorageMode', () => {
|
|
36
|
-
it('returns true for
|
|
37
|
+
it('returns true for 0x20a9 (QL-1100 mass storage)', () => {
|
|
38
|
+
expect(isMassStorageMode(0x20a9)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns true for 0x20aa (QL-1110NWB mass storage)', () => {
|
|
37
42
|
expect(isMassStorageMode(0x20aa)).toBe(true);
|
|
38
43
|
});
|
|
39
44
|
|
|
40
|
-
it('returns true for
|
|
41
|
-
expect(isMassStorageMode(
|
|
45
|
+
it('returns true for 0x20ac (QL-1115NWB mass storage)', () => {
|
|
46
|
+
expect(isMassStorageMode(0x20ac)).toBe(true);
|
|
42
47
|
});
|
|
43
48
|
|
|
44
49
|
it('returns false for all printer-class PIDs', () => {
|
|
45
50
|
for (const dev of Object.values(DEVICES)) {
|
|
46
|
-
|
|
51
|
+
const ids = getUsbIds(dev);
|
|
52
|
+
if (ids) expect(isMassStorageMode(ids.pid)).toBe(false);
|
|
47
53
|
}
|
|
48
54
|
});
|
|
49
55
|
});
|
|
50
56
|
|
|
51
57
|
describe('Device registry invariants', () => {
|
|
52
|
-
it('every two-color device has
|
|
58
|
+
it('every two-color device has a 720-dot engine', () => {
|
|
53
59
|
for (const dev of Object.values(DEVICES)) {
|
|
54
|
-
if (dev.twoColor) {
|
|
55
|
-
expect(dev.
|
|
60
|
+
if (dev.engines[0]?.capabilities?.twoColor) {
|
|
61
|
+
expect(dev.engines[0].headDots).toBe(720);
|
|
56
62
|
}
|
|
57
63
|
}
|
|
58
64
|
});
|
|
59
65
|
|
|
60
|
-
it('every device with
|
|
66
|
+
it('every device with headDots 1296 belongs to the QL-1xxx series', () => {
|
|
61
67
|
for (const dev of Object.values(DEVICES)) {
|
|
62
|
-
if (dev.
|
|
63
|
-
expect(dev.
|
|
68
|
+
if (dev.engines[0]?.headDots === 1296) {
|
|
69
|
+
expect(dev.name).toMatch(/^QL-1\d{3}/);
|
|
64
70
|
}
|
|
65
71
|
}
|
|
66
72
|
});
|
|
@@ -71,19 +77,103 @@ describe('Device registry invariants', () => {
|
|
|
71
77
|
}
|
|
72
78
|
});
|
|
73
79
|
|
|
74
|
-
it('
|
|
75
|
-
for (const
|
|
76
|
-
|
|
77
|
-
expect(dev.transports).
|
|
78
|
-
expect(dev.transports).
|
|
80
|
+
it('every device declares a USB transport with hex-string vid+pid', () => {
|
|
81
|
+
for (const dev of Object.values(DEVICES)) {
|
|
82
|
+
expect(dev.transports.usb).toBeDefined();
|
|
83
|
+
expect(dev.transports.usb!.vid).toMatch(/^0x[0-9a-f]+$/);
|
|
84
|
+
expect(dev.transports.usb!.pid).toMatch(/^0x[0-9a-f]+$/);
|
|
79
85
|
}
|
|
80
86
|
});
|
|
87
|
+
});
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
describe('PT-* device entries', () => {
|
|
90
|
+
const PT_KEYS = ['PT_E550W', 'PT_P750W', 'PT_P900', 'PT_P900W', 'PT_P950NW', 'PT_P910BT'];
|
|
91
|
+
|
|
92
|
+
it('every PT entry resolves by (vid, pid)', () => {
|
|
93
|
+
const expected: Record<string, number> = {
|
|
94
|
+
PT_E550W: 0x2060,
|
|
95
|
+
PT_P750W: 0x2062,
|
|
96
|
+
PT_P900: 0x2083,
|
|
97
|
+
PT_P900W: 0x2085,
|
|
98
|
+
PT_P950NW: 0x2086,
|
|
99
|
+
PT_P910BT: 0x20c7,
|
|
100
|
+
};
|
|
101
|
+
for (const key of PT_KEYS) {
|
|
102
|
+
const pid = expected[key]!;
|
|
103
|
+
const dev = findDevice(0x04f9, pid);
|
|
104
|
+
expect(dev, key).toBeDefined();
|
|
105
|
+
expect(dev!.key).toBe(key);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('every PT engine uses the pt-raster protocol', () => {
|
|
110
|
+
for (const key of PT_KEYS) {
|
|
111
|
+
const dev = DEVICES[key as keyof typeof DEVICES];
|
|
112
|
+
expect(dev.engines[0]?.protocol, key).toBe('pt-raster');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('every PT engine has headDots in {128, 560}', () => {
|
|
117
|
+
for (const key of PT_KEYS) {
|
|
118
|
+
const dev = DEVICES[key as keyof typeof DEVICES];
|
|
119
|
+
expect([128, 560]).toContain(dev.engines[0]?.headDots);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('PT high-res dpi is exactly 2× the native dpi', () => {
|
|
124
|
+
for (const key of PT_KEYS) {
|
|
125
|
+
const dev = DEVICES[key as keyof typeof DEVICES];
|
|
126
|
+
const engine = dev.engines[0]!;
|
|
127
|
+
const dpi = engine.dpi;
|
|
128
|
+
const highResDpi = engine.capabilities?.highResDpi as number | undefined;
|
|
129
|
+
expect(highResDpi, `${key} highResDpi`).toBeDefined();
|
|
130
|
+
expect(highResDpi).toBe(dpi * 2);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('128-dot family is 180 dpi, 560-dot family is 360 dpi', () => {
|
|
135
|
+
for (const key of PT_KEYS) {
|
|
136
|
+
const dev = DEVICES[key as keyof typeof DEVICES];
|
|
137
|
+
const engine = dev.engines[0]!;
|
|
138
|
+
if (engine.headDots === 128) expect(engine.dpi).toBe(180);
|
|
139
|
+
else if (engine.headDots === 560) expect(engine.dpi).toBe(360);
|
|
87
140
|
}
|
|
88
141
|
});
|
|
142
|
+
|
|
143
|
+
it('every PT entry ships untested', () => {
|
|
144
|
+
for (const key of PT_KEYS) {
|
|
145
|
+
const dev = DEVICES[key as keyof typeof DEVICES];
|
|
146
|
+
expect(dev.support.status).toBe('untested');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('PT-P910BT is TZe-only (no HSe in mediaCompatibility)', () => {
|
|
151
|
+
const dev = DEVICES.PT_P910BT;
|
|
152
|
+
expect(dev.engines[0]?.mediaCompatibility).toEqual(['tze']);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('all other PT models declare TZe + HSe 2:1 + HSe 3:1', () => {
|
|
156
|
+
for (const key of ['PT_E550W', 'PT_P750W', 'PT_P900', 'PT_P900W', 'PT_P950NW']) {
|
|
157
|
+
const dev = DEVICES[key as keyof typeof DEVICES];
|
|
158
|
+
expect(dev.engines[0]?.mediaCompatibility, key).toEqual(['tze', 'hse-2to1', 'hse-3to1']);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('PT-P750W carries both printer PID 0x2062 and mass-storage PID 0x2065', () => {
|
|
163
|
+
const dev = DEVICES.PT_P750W;
|
|
164
|
+
expect(dev.transports.usb?.pid).toBe('0x2062');
|
|
165
|
+
expect(dev.capabilities?.massStoragePid).toBe('0x2065');
|
|
166
|
+
expect(isMassStorageMode(0x2065)).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('PT-P910BT declares bluetooth-spp transport', () => {
|
|
170
|
+
const dev = DEVICES.PT_P910BT;
|
|
171
|
+
expect(dev.transports['bluetooth-spp']).toBeDefined();
|
|
172
|
+
expect(dev.transports['bluetooth-spp']?.namePrefix).toBe('PT-P910');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('PT-P900 is USB-only (no tcp / bluetooth)', () => {
|
|
176
|
+
const dev = DEVICES.PT_P900;
|
|
177
|
+
expect(Object.keys(dev.transports).sort()).toEqual(['usb']);
|
|
178
|
+
});
|
|
89
179
|
});
|
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { mediaCompatibleWith, type PrintEngine } from '@thermal-label/contracts';
|
|
3
|
+
import {
|
|
4
|
+
MEDIA,
|
|
5
|
+
defaultMediaForEngine,
|
|
6
|
+
findMedia,
|
|
7
|
+
findMediaByDimensions,
|
|
8
|
+
findMediaByWidth,
|
|
9
|
+
resolveTapeGeometry,
|
|
10
|
+
} from '../media.js';
|
|
11
|
+
import { DEVICES } from '../devices.js';
|
|
12
|
+
|
|
13
|
+
const NARROW_PT_ENGINE = {
|
|
14
|
+
protocol: 'pt-raster' as const,
|
|
15
|
+
headDots: 128,
|
|
16
|
+
mediaCompatibility: ['tze', 'hse-2to1', 'hse-3to1'],
|
|
17
|
+
};
|
|
18
|
+
const WIDE_PT_ENGINE = {
|
|
19
|
+
protocol: 'pt-raster' as const,
|
|
20
|
+
headDots: 560,
|
|
21
|
+
mediaCompatibility: ['tze', 'hse-2to1', 'hse-3to1'],
|
|
22
|
+
};
|
|
23
|
+
const TZE_ONLY_PT_ENGINE = {
|
|
24
|
+
protocol: 'pt-raster' as const,
|
|
25
|
+
headDots: 560,
|
|
26
|
+
mediaCompatibility: ['tze'],
|
|
27
|
+
};
|
|
28
|
+
const QL_ENGINE = {
|
|
29
|
+
protocol: 'ql-raster' as const,
|
|
30
|
+
headDots: 720,
|
|
31
|
+
mediaCompatibility: ['dk'],
|
|
32
|
+
};
|
|
3
33
|
|
|
4
34
|
describe('findMedia', () => {
|
|
5
35
|
it('returns correct descriptor for 62mm continuous (ID 259)', () => {
|
|
@@ -100,4 +130,260 @@ describe('Media registry invariants', () => {
|
|
|
100
130
|
const unique = new Set(ids);
|
|
101
131
|
expect(unique.size).toBe(ids.length);
|
|
102
132
|
});
|
|
133
|
+
|
|
134
|
+
it('every entry declares a tape system', () => {
|
|
135
|
+
for (const m of Object.values(MEDIA)) {
|
|
136
|
+
expect(m.tapeSystem, `entry ${m.id.toString()}`).toBeDefined();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('TZe ids occupy the 401-419 range, HSe 421-459', () => {
|
|
141
|
+
for (const m of Object.values(MEDIA)) {
|
|
142
|
+
if (m.tapeSystem === 'tze') {
|
|
143
|
+
expect(m.id, m.name).toBeGreaterThanOrEqual(401);
|
|
144
|
+
expect(m.id, m.name).toBeLessThanOrEqual(419);
|
|
145
|
+
} else if (m.tapeSystem === 'hse-2to1') {
|
|
146
|
+
expect(m.id, m.name).toBeGreaterThanOrEqual(421);
|
|
147
|
+
expect(m.id, m.name).toBeLessThanOrEqual(439);
|
|
148
|
+
} else if (m.tapeSystem === 'hse-3to1') {
|
|
149
|
+
expect(m.id, m.name).toBeGreaterThanOrEqual(441);
|
|
150
|
+
expect(m.id, m.name).toBeLessThanOrEqual(459);
|
|
151
|
+
} else {
|
|
152
|
+
expect(m.tapeSystem, m.name).toBe('dk');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('TZe / HSe pin sums equal the head-family pin count', () => {
|
|
158
|
+
for (const m of Object.values(MEDIA)) {
|
|
159
|
+
if (m.tapeSystem === 'dk') continue;
|
|
160
|
+
if (m.geometry?.narrow) {
|
|
161
|
+
const { leftMarginPins, printAreaDots, rightMarginPins } = m.geometry.narrow;
|
|
162
|
+
expect(leftMarginPins + printAreaDots + rightMarginPins, `${m.name} narrow head sum`).toBe(
|
|
163
|
+
128,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (m.geometry?.wide) {
|
|
167
|
+
const { leftMarginPins, printAreaDots, rightMarginPins } = m.geometry.wide;
|
|
168
|
+
expect(leftMarginPins + printAreaDots + rightMarginPins, `${m.name} wide head sum`).toBe(
|
|
169
|
+
560,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('36 mm TZe and 31 mm HSe-3:1 are wide-head only', () => {
|
|
176
|
+
expect(MEDIA[407]!.geometry?.narrow).toBeUndefined();
|
|
177
|
+
expect(MEDIA[407]!.geometry?.wide).toBeDefined();
|
|
178
|
+
expect(MEDIA[445]!.geometry?.narrow).toBeUndefined();
|
|
179
|
+
expect(MEDIA[445]!.geometry?.wide).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('findMediaByDimensions — engine gating', () => {
|
|
184
|
+
it('legacy call (no engine) keeps DK-only behaviour for 12 mm continuous', () => {
|
|
185
|
+
const m = findMediaByDimensions(12, 0);
|
|
186
|
+
expect(m).toBeDefined();
|
|
187
|
+
expect(m!.tapeSystem).toBe('dk');
|
|
188
|
+
expect(m!.id).toBe(257);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('QL engine for 12 mm returns the DK entry, never a TZe entry', () => {
|
|
192
|
+
const m = findMediaByDimensions(12, 0, false, QL_ENGINE);
|
|
193
|
+
expect(m!.tapeSystem).toBe('dk');
|
|
194
|
+
expect(m!.id).toBe(257);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('narrow PT engine for 12 mm returns the 128-dot TZe entry', () => {
|
|
198
|
+
const m = findMediaByDimensions(12, 0, false, NARROW_PT_ENGINE);
|
|
199
|
+
expect(m!.tapeSystem).toBe('tze');
|
|
200
|
+
expect(m!.id).toBe(404);
|
|
201
|
+
expect(m!.geometry?.narrow).toBeDefined();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('wide PT engine for 12 mm returns the same TZe row (geometry resolves later)', () => {
|
|
205
|
+
const m = findMediaByDimensions(12, 0, false, WIDE_PT_ENGINE);
|
|
206
|
+
expect(m!.tapeSystem).toBe('tze');
|
|
207
|
+
expect(m!.id).toBe(404);
|
|
208
|
+
expect(m!.geometry?.wide).toBeDefined();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('narrow PT engine cannot reach 36 mm TZe', () => {
|
|
212
|
+
const m = findMediaByDimensions(36, 0, false, NARROW_PT_ENGINE);
|
|
213
|
+
expect(m).toBeUndefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('wide PT engine resolves 36 mm TZe', () => {
|
|
217
|
+
const m = findMediaByDimensions(36, 0, false, WIDE_PT_ENGINE);
|
|
218
|
+
expect(m!.id).toBe(407);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('narrow PT engine cannot reach 31 mm HSe-3:1', () => {
|
|
222
|
+
const m = findMediaByDimensions(31, 0, false, NARROW_PT_ENGINE);
|
|
223
|
+
expect(m).toBeUndefined();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('wide PT engine resolves 31 mm HSe-3:1', () => {
|
|
227
|
+
const m = findMediaByDimensions(31, 0, false, WIDE_PT_ENGINE);
|
|
228
|
+
expect(m!.id).toBe(445);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('TZe-only PT engine never returns HSe entries', () => {
|
|
232
|
+
expect(findMediaByDimensions(11.7, 0, false, TZE_ONLY_PT_ENGINE)).toBeUndefined();
|
|
233
|
+
expect(findMediaByDimensions(5.2, 0, false, TZE_ONLY_PT_ENGINE)).toBeUndefined();
|
|
234
|
+
expect(findMediaByDimensions(31, 0, false, TZE_ONLY_PT_ENGINE)).toBeUndefined();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('TZe-only PT engine still resolves TZe widths', () => {
|
|
238
|
+
const m = findMediaByDimensions(24, 0, false, TZE_ONLY_PT_ENGINE);
|
|
239
|
+
expect(m!.tapeSystem).toBe('tze');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
describe('resolveTapeGeometry', () => {
|
|
244
|
+
it('returns flat fields for DK entries regardless of engine head dots', () => {
|
|
245
|
+
const dk = MEDIA[259]!;
|
|
246
|
+
const geom = resolveTapeGeometry(dk, { headDots: 720 });
|
|
247
|
+
expect(geom.printAreaDots).toBe(696);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('routes TZe through narrow / wide based on engine.headDots', () => {
|
|
251
|
+
const tze12 = MEDIA[404]!;
|
|
252
|
+
const narrow = resolveTapeGeometry(tze12, { headDots: 128 });
|
|
253
|
+
expect(narrow.printAreaDots).toBe(70);
|
|
254
|
+
const wide = resolveTapeGeometry(tze12, { headDots: 560 });
|
|
255
|
+
expect(wide.printAreaDots).toBe(150);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('throws when the requested head family has no geometry', () => {
|
|
259
|
+
expect(() => resolveTapeGeometry(MEDIA[407]!, { headDots: 128 })).toThrow(/narrow/);
|
|
260
|
+
expect(() => resolveTapeGeometry(MEDIA[445]!, { headDots: 128 })).toThrow(/narrow/);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe('defaultMediaForEngine', () => {
|
|
265
|
+
it('returns DK-22205 for QL engines', () => {
|
|
266
|
+
expect(defaultMediaForEngine({ protocol: 'ql-raster' }).id).toBe(259);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('returns 12 mm TZe for PT engines', () => {
|
|
270
|
+
expect(defaultMediaForEngine({ protocol: 'pt-raster' }).id).toBe(404);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe('Substrate-gate field-shape invariants', () => {
|
|
275
|
+
it('every entry declares a non-empty targetModels', () => {
|
|
276
|
+
for (const m of Object.values(MEDIA)) {
|
|
277
|
+
expect(m.targetModels, m.name).toBeDefined();
|
|
278
|
+
expect(m.targetModels!.length, m.name).toBeGreaterThan(0);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('every entry declares a category', () => {
|
|
283
|
+
for (const m of Object.values(MEDIA)) {
|
|
284
|
+
expect(m.category, m.name).toBeDefined();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('targetModels is consistent with tapeSystem', () => {
|
|
289
|
+
for (const m of Object.values(MEDIA)) {
|
|
290
|
+
// DK rolls allow either 'dk' or 'dk-wide' in targetModels — both
|
|
291
|
+
// are DK-substrate tags. Other tape systems carry the bare value.
|
|
292
|
+
if (m.tapeSystem === 'dk') {
|
|
293
|
+
const ok = m.targetModels!.includes('dk') || m.targetModels!.includes('dk-wide');
|
|
294
|
+
expect(ok, `${m.name} targetModels=${JSON.stringify(m.targetModels)}`).toBe(true);
|
|
295
|
+
} else {
|
|
296
|
+
expect(m.targetModels, m.name).toContain(m.tapeSystem);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('every DK row with widthMm > 62 is tagged dk-wide', () => {
|
|
302
|
+
for (const m of Object.values(MEDIA)) {
|
|
303
|
+
if (m.tapeSystem === 'dk' && m.widthMm > 62) {
|
|
304
|
+
expect(m.targetModels, `${m.name} (id ${m.id.toString()})`).toContain('dk-wide');
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('every QL-1xxx engine accepts both dk and dk-wide', () => {
|
|
310
|
+
const wideKeys = ['QL_1050', 'QL_1060N', 'QL_1100', 'QL_1110NWB', 'QL_1115NWB'] as const;
|
|
311
|
+
for (const key of wideKeys) {
|
|
312
|
+
const compat = DEVICES[key].engines[0]!.mediaCompatibility;
|
|
313
|
+
expect(compat, key).toContain('dk');
|
|
314
|
+
expect(compat, key).toContain('dk-wide');
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('no narrow QL chassis lists dk-wide', () => {
|
|
319
|
+
for (const d of Object.values(DEVICES)) {
|
|
320
|
+
if (!d.key.startsWith('QL_')) continue;
|
|
321
|
+
// The five 1296-dot chassis are the wide tier; everything else is 720-dot.
|
|
322
|
+
const headDots = d.engines[0]!.headDots;
|
|
323
|
+
if (headDots === 720) {
|
|
324
|
+
expect(d.engines[0]!.mediaCompatibility, d.key).not.toContain('dk-wide');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('Substrate-gate enforcement matrix (mediaCompatibleWith)', () => {
|
|
331
|
+
// Representative engines for each compatibility class. The matrix
|
|
332
|
+
// walks every media row against each and asserts the cell — catches
|
|
333
|
+
// a row losing its targetModels (would falsely match every engine),
|
|
334
|
+
// an HSe row mistagged 'tze', a 102 mm row losing 'dk-wide', or a
|
|
335
|
+
// future engine dropping the substrate tag.
|
|
336
|
+
const CLASSES: Record<string, PrintEngine> = {
|
|
337
|
+
qlStandard: DEVICES.QL_700.engines[0]!,
|
|
338
|
+
qlWide: DEVICES.QL_1100.engines[0]!,
|
|
339
|
+
ptTzeHse: DEVICES.PT_P900.engines[0]!,
|
|
340
|
+
ptTzeOnly: DEVICES.PT_P910BT.engines[0]!,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
type ClassKey = keyof typeof CLASSES;
|
|
344
|
+
// For a media row, returns the set of class keys that should accept it.
|
|
345
|
+
function expectedClassesFor(m: {
|
|
346
|
+
tapeSystem: string;
|
|
347
|
+
targetModels?: readonly string[];
|
|
348
|
+
}): Set<ClassKey> {
|
|
349
|
+
const isWide = m.targetModels?.includes('dk-wide') ?? false;
|
|
350
|
+
const set = new Set<ClassKey>();
|
|
351
|
+
if (m.tapeSystem === 'dk') {
|
|
352
|
+
if (isWide) {
|
|
353
|
+
set.add('qlWide');
|
|
354
|
+
} else {
|
|
355
|
+
set.add('qlStandard');
|
|
356
|
+
set.add('qlWide');
|
|
357
|
+
}
|
|
358
|
+
} else if (m.tapeSystem === 'tze') {
|
|
359
|
+
set.add('ptTzeHse');
|
|
360
|
+
set.add('ptTzeOnly');
|
|
361
|
+
} else if (m.tapeSystem === 'hse-2to1' || m.tapeSystem === 'hse-3to1') {
|
|
362
|
+
set.add('ptTzeHse');
|
|
363
|
+
}
|
|
364
|
+
return set;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
it('every (engine class, media row) cell matches expectation', () => {
|
|
368
|
+
for (const m of Object.values(MEDIA)) {
|
|
369
|
+
const expected = expectedClassesFor(m);
|
|
370
|
+
for (const key of Object.keys(CLASSES)) {
|
|
371
|
+
const got = mediaCompatibleWith(m, CLASSES[key]!);
|
|
372
|
+
const want = expected.has(key);
|
|
373
|
+
expect(got, `${m.name} (id ${m.id.toString()}) on ${key}`).toBe(want);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('PT-P910BT does not surface HSe media', () => {
|
|
379
|
+
// Strictly redundant with the matrix above, but worth keeping
|
|
380
|
+
// separately so a future bisect on a "P910BT shows HSe media"
|
|
381
|
+
// report finds a test named after the case.
|
|
382
|
+
const engine = DEVICES.PT_P910BT.engines[0]!;
|
|
383
|
+
for (const m of Object.values(MEDIA)) {
|
|
384
|
+
if (m.tapeSystem === 'hse-2to1' || m.tapeSystem === 'hse-3to1') {
|
|
385
|
+
expect(mediaCompatibleWith(m, engine), m.name).toBe(false);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
103
389
|
});
|