@flighthq/bitmap 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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 +2 -2
- package/dist/bitmapDraw.d.ts.map +1 -1
- package/dist/bitmapDraw.js +3 -2
- package/dist/bitmapDraw.js.map +1 -1
- package/dist/bitmapEncode.d.ts +3 -2
- package/dist/bitmapEncode.d.ts.map +1 -1
- package/dist/bitmapEncode.js +20 -14
- package/dist/bitmapEncode.js.map +1 -1
- package/dist/bitmapEncodeBackend.d.ts +11 -0
- package/dist/bitmapEncodeBackend.d.ts.map +1 -0
- package/dist/bitmapEncodeBackend.js +58 -0
- package/dist/bitmapEncodeBackend.js.map +1 -0
- package/dist/bitmapFrom.d.ts +6 -8
- package/dist/bitmapFrom.d.ts.map +1 -1
- package/dist/bitmapFrom.js +8 -56
- package/dist/bitmapFrom.js.map +1 -1
- package/dist/bitmapReadbackBackend.d.ts +7 -0
- package/dist/bitmapReadbackBackend.d.ts.map +1 -0
- package/dist/bitmapReadbackBackend.js +21 -0
- package/dist/bitmapReadbackBackend.js.map +1 -0
- package/dist/bitmapReadbackResolver.d.ts +8 -0
- package/dist/bitmapReadbackResolver.d.ts.map +1 -0
- package/dist/bitmapReadbackResolver.js +12 -0
- package/dist/bitmapReadbackResolver.js.map +1 -0
- package/dist/contract.d.ts +2 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +2 -0
- package/dist/contract.js.map +1 -1
- package/dist/explainBitmapReadback.d.ts.map +1 -1
- package/dist/explainBitmapReadback.js +7 -36
- package/dist/explainBitmapReadback.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bitmapDraw.test.ts +111 -16
- package/src/bitmapEncode.test.ts +99 -13
- package/src/bitmapEncodeBackend.test.ts +146 -0
- package/src/bitmapFrom.test.ts +31 -44
- package/src/bitmapReadbackBackend.test.ts +69 -0
- package/src/bitmapReadbackResolver.test.ts +32 -0
- package/src/explainBitmapReadback.test.ts +74 -54
package/src/bitmapDraw.test.ts
CHANGED
|
@@ -12,21 +12,58 @@ function readExtractedPixel(buf: Uint8ClampedArray, regionWidth: number, x: numb
|
|
|
12
12
|
return ((buf[i] << 24) | (buf[i + 1] << 16) | (buf[i + 2] << 8) | buf[i + 3]) >>> 0;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
function guardGlobalImageDataAccess() {
|
|
16
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'ImageData');
|
|
17
|
+
let accesses = 0;
|
|
18
|
+
Object.defineProperty(globalThis, 'ImageData', {
|
|
19
|
+
configurable: true,
|
|
20
|
+
get() {
|
|
21
|
+
accesses++;
|
|
22
|
+
throw new Error('global ImageData must not be accessed');
|
|
23
|
+
},
|
|
22
24
|
});
|
|
25
|
+
return {
|
|
26
|
+
get accesses() {
|
|
27
|
+
return accesses;
|
|
28
|
+
},
|
|
29
|
+
restore() {
|
|
30
|
+
if (descriptor === undefined) Reflect.deleteProperty(globalThis, 'ImageData');
|
|
31
|
+
else Object.defineProperty(globalThis, 'ImageData', descriptor);
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
36
|
+
describe('drawBitmap', () => {
|
|
37
|
+
it('allocates through one destination context and puts the exact extracted pixels at the caller coordinates', () => {
|
|
38
|
+
const src = createBitmap(2, 1);
|
|
39
|
+
setBitmapPixel(src, 0, 0, 0x112233ff);
|
|
40
|
+
setBitmapPixel(src, 1, 0, 0xaabbccdd);
|
|
41
|
+
const imageData = {
|
|
42
|
+
colorSpace: 'srgb',
|
|
43
|
+
data: new Uint8ClampedArray(8),
|
|
44
|
+
height: 1,
|
|
45
|
+
width: 2,
|
|
46
|
+
} as ImageData;
|
|
47
|
+
const createImageData = vi.fn(() => imageData);
|
|
48
|
+
const putImageData = vi.fn();
|
|
49
|
+
const getContext = vi.fn(() => ({ createImageData, putImageData }));
|
|
26
50
|
const canvas = document.createElement('canvas');
|
|
27
|
-
canvas
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
Object.defineProperty(canvas, 'getContext', { configurable: true, value: getContext });
|
|
52
|
+
const globalImageData = guardGlobalImageDataAccess();
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
drawBitmap(canvas, region(src), 7, -3);
|
|
56
|
+
expect(getContext).toHaveBeenCalledOnce();
|
|
57
|
+
expect(getContext).toHaveBeenCalledWith('2d');
|
|
58
|
+
expect(createImageData).toHaveBeenCalledOnce();
|
|
59
|
+
expect(createImageData).toHaveBeenCalledWith(2, 1);
|
|
60
|
+
expect(Array.from(imageData.data)).toEqual([17, 34, 51, 255, 170, 187, 204, 221]);
|
|
61
|
+
expect(putImageData).toHaveBeenCalledOnce();
|
|
62
|
+
expect(putImageData).toHaveBeenCalledWith(imageData, 7, -3);
|
|
63
|
+
expect(globalImageData.accesses).toBe(0);
|
|
64
|
+
} finally {
|
|
65
|
+
globalImageData.restore();
|
|
66
|
+
}
|
|
30
67
|
});
|
|
31
68
|
|
|
32
69
|
it('extracts correct pixels from a filled source', () => {
|
|
@@ -58,11 +95,69 @@ describe('drawBitmap', () => {
|
|
|
58
95
|
expect(readExtractedPixel(buf, r.width, 1, 1)).toBe(0x00000000);
|
|
59
96
|
});
|
|
60
97
|
|
|
61
|
-
it(
|
|
98
|
+
it.each([
|
|
99
|
+
['width', 0, 1],
|
|
100
|
+
['height', 1, 0],
|
|
101
|
+
] as const)('is a no-op for a zero %s before context, allocation, extraction, or put', (_axis, width, height) => {
|
|
62
102
|
const src = createBitmap(2, 2, 0x112233ff);
|
|
103
|
+
const sourceData = src.data;
|
|
104
|
+
let sourceDataAccesses = 0;
|
|
105
|
+
Object.defineProperty(src, 'data', {
|
|
106
|
+
configurable: true,
|
|
107
|
+
get() {
|
|
108
|
+
sourceDataAccesses++;
|
|
109
|
+
return sourceData;
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const createImageData = vi.fn();
|
|
113
|
+
const putImageData = vi.fn();
|
|
114
|
+
const getContext = vi.fn(() => ({ createImageData, putImageData }));
|
|
115
|
+
const canvas = document.createElement('canvas');
|
|
116
|
+
Object.defineProperty(canvas, 'getContext', { configurable: true, value: getContext });
|
|
117
|
+
const globalImageData = guardGlobalImageDataAccess();
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
expect(() => drawBitmap(canvas, region(src, 0, 0, width, height), 0, 0)).not.toThrow();
|
|
121
|
+
expect(getContext).not.toHaveBeenCalled();
|
|
122
|
+
expect(createImageData).not.toHaveBeenCalled();
|
|
123
|
+
expect(putImageData).not.toHaveBeenCalled();
|
|
124
|
+
expect(sourceDataAccesses).toBe(0);
|
|
125
|
+
expect(globalImageData.accesses).toBe(0);
|
|
126
|
+
} finally {
|
|
127
|
+
globalImageData.restore();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('throws from a null positive-size context before allocation, extraction, or put', () => {
|
|
132
|
+
const src = createBitmap(1, 1, 0x112233ff);
|
|
133
|
+
const sourceData = src.data;
|
|
134
|
+
let sourceDataAccesses = 0;
|
|
135
|
+
Object.defineProperty(src, 'data', {
|
|
136
|
+
configurable: true,
|
|
137
|
+
get() {
|
|
138
|
+
sourceDataAccesses++;
|
|
139
|
+
return sourceData;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const getContext = vi.fn(() => null);
|
|
63
143
|
const canvas = document.createElement('canvas');
|
|
64
|
-
canvas
|
|
65
|
-
|
|
66
|
-
|
|
144
|
+
Object.defineProperty(canvas, 'getContext', { configurable: true, value: getContext });
|
|
145
|
+
const globalImageData = guardGlobalImageDataAccess();
|
|
146
|
+
const createImageData = vi.spyOn(CanvasRenderingContext2D.prototype, 'createImageData');
|
|
147
|
+
const putImageData = vi.spyOn(CanvasRenderingContext2D.prototype, 'putImageData');
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
expect(() => drawBitmap(canvas, region(src), 0, 0)).toThrow();
|
|
151
|
+
expect(getContext).toHaveBeenCalledOnce();
|
|
152
|
+
expect(getContext).toHaveBeenCalledWith('2d');
|
|
153
|
+
expect(createImageData).not.toHaveBeenCalled();
|
|
154
|
+
expect(putImageData).not.toHaveBeenCalled();
|
|
155
|
+
expect(sourceDataAccesses).toBe(0);
|
|
156
|
+
expect(globalImageData.accesses).toBe(0);
|
|
157
|
+
} finally {
|
|
158
|
+
createImageData.mockRestore();
|
|
159
|
+
putImageData.mockRestore();
|
|
160
|
+
globalImageData.restore();
|
|
161
|
+
}
|
|
67
162
|
});
|
|
68
163
|
});
|
package/src/bitmapEncode.test.ts
CHANGED
|
@@ -1,18 +1,104 @@
|
|
|
1
|
+
import type { BitmapEncodeBackend, ImageFormat } from '@flighthq/types/contract';
|
|
2
|
+
|
|
1
3
|
import { createBitmap } from './bitmap';
|
|
2
|
-
import { encodeBitmap } from './bitmapEncode';
|
|
4
|
+
import { encodeBitmap, explainBitmapEncodeFailure } from './bitmapEncode';
|
|
5
|
+
import {
|
|
6
|
+
installBitmapEncodeHostBackend,
|
|
7
|
+
resetBitmapEncodeBackendForTest,
|
|
8
|
+
setBitmapEncodeBackend,
|
|
9
|
+
} from './bitmapEncodeBackend';
|
|
10
|
+
|
|
11
|
+
function createBackend(
|
|
12
|
+
supportedFormats: BitmapEncodeBackend['supportedFormats'] = ['jpeg', 'png'],
|
|
13
|
+
bytes: Uint8Array = new Uint8Array([9, 8, 7]),
|
|
14
|
+
) {
|
|
15
|
+
return {
|
|
16
|
+
encodeBitmap: vi.fn(() => bytes),
|
|
17
|
+
supportedFormats,
|
|
18
|
+
} satisfies BitmapEncodeBackend;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
resetBitmapEncodeBackendForTest();
|
|
23
|
+
});
|
|
3
24
|
|
|
4
25
|
describe('encodeBitmap', () => {
|
|
5
|
-
it('
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
expect(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
26
|
+
it('dispatches the default png format and quality to the selected backend', () => {
|
|
27
|
+
const backend = createBackend();
|
|
28
|
+
const source = createBitmap(2, 2, 0x112233ff);
|
|
29
|
+
setBitmapEncodeBackend(backend);
|
|
30
|
+
expect(encodeBitmap(source)).toEqual(new Uint8Array([9, 8, 7]));
|
|
31
|
+
expect(backend.encodeBitmap).toHaveBeenCalledWith(source, 'png', 0.9);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('forwards jpeg and caller quality unchanged', () => {
|
|
35
|
+
const backend = createBackend();
|
|
36
|
+
const source = createBitmap(1, 1);
|
|
37
|
+
setBitmapEncodeBackend(backend);
|
|
38
|
+
encodeBitmap(source, 'jpeg', 1.25);
|
|
39
|
+
expect(backend.encodeBitmap).toHaveBeenCalledWith(source, 'jpeg', 1.25);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('preserves the legacy runtime fallback from every non-jpeg value to png', () => {
|
|
43
|
+
const backend = createBackend();
|
|
44
|
+
const source = createBitmap(1, 1);
|
|
45
|
+
setBitmapEncodeBackend(backend);
|
|
46
|
+
encodeBitmap(source, 'webp' as ImageFormat);
|
|
47
|
+
expect(backend.encodeBitmap).toHaveBeenCalledWith(source, 'png', 0.9);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns null when no backend is installed', () => {
|
|
51
|
+
expect(encodeBitmap(createBitmap(1, 1))).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('returns null without invoking an installed backend that does not support the format', () => {
|
|
55
|
+
const backend = createBackend(['png']);
|
|
56
|
+
setBitmapEncodeBackend(backend);
|
|
57
|
+
expect(encodeBitmap(createBitmap(1, 1), 'jpeg')).toBeNull();
|
|
58
|
+
expect(backend.encodeBitmap).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('keeps a custom backend terminal above an installed host', () => {
|
|
62
|
+
const host = createBackend();
|
|
63
|
+
const custom = createBackend(['png'], new Uint8Array([3]));
|
|
64
|
+
installBitmapEncodeHostBackend(host);
|
|
65
|
+
setBitmapEncodeBackend(custom);
|
|
66
|
+
expect(encodeBitmap(createBitmap(1, 1), 'jpeg')).toBeNull();
|
|
67
|
+
expect(custom.encodeBitmap).not.toHaveBeenCalled();
|
|
68
|
+
expect(host.encodeBitmap).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('propagates genuine supported-format encoding faults', () => {
|
|
72
|
+
const failure = new Error('encode failed');
|
|
73
|
+
const backend: BitmapEncodeBackend = {
|
|
74
|
+
encodeBitmap(): Uint8Array {
|
|
75
|
+
throw failure;
|
|
76
|
+
},
|
|
77
|
+
supportedFormats: ['png'],
|
|
78
|
+
};
|
|
79
|
+
setBitmapEncodeBackend(backend);
|
|
80
|
+
expect(() => encodeBitmap(createBitmap(1, 1))).toThrow(failure);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('explainBitmapEncodeFailure', () => {
|
|
85
|
+
it('reports a missing backend for the normalized requested format', () => {
|
|
86
|
+
expect(explainBitmapEncodeFailure('jpeg')).toEqual({ format: 'jpeg', reason: 'backend-not-installed' });
|
|
87
|
+
expect(explainBitmapEncodeFailure('webp' as ImageFormat)).toEqual({
|
|
88
|
+
format: 'png',
|
|
89
|
+
reason: 'backend-not-installed',
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('reports an unsupported format from the selected backend', () => {
|
|
94
|
+
setBitmapEncodeBackend(createBackend(['png']));
|
|
95
|
+
expect(explainBitmapEncodeFailure('jpeg')).toEqual({ format: 'jpeg', reason: 'format-unsupported' });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('returns null for a supported format without invoking the encoder', () => {
|
|
99
|
+
const backend = createBackend(['png']);
|
|
100
|
+
setBitmapEncodeBackend(backend);
|
|
101
|
+
expect(explainBitmapEncodeFailure('png')).toBeNull();
|
|
102
|
+
expect(backend.encodeBitmap).not.toHaveBeenCalled();
|
|
17
103
|
});
|
|
18
104
|
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { BitmapEncodeBackend } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
explainBitmapEncodeBackend,
|
|
5
|
+
explainBitmapEncodeOperation,
|
|
6
|
+
getBitmapEncodeBackend,
|
|
7
|
+
hasBitmapEncodeHostBackend,
|
|
8
|
+
hasBitmapEncodeOperation,
|
|
9
|
+
installBitmapEncodeHostBackend,
|
|
10
|
+
observeBitmapEncodeHostResult,
|
|
11
|
+
resetBitmapEncodeBackendForTest,
|
|
12
|
+
setBitmapEncodeBackend,
|
|
13
|
+
} from './bitmapEncodeBackend';
|
|
14
|
+
|
|
15
|
+
function createBackend(supportedFormats: BitmapEncodeBackend['supportedFormats'] = ['jpeg', 'png']) {
|
|
16
|
+
return {
|
|
17
|
+
encodeBitmap: vi.fn(() => new Uint8Array([1])),
|
|
18
|
+
supportedFormats,
|
|
19
|
+
} satisfies BitmapEncodeBackend;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
resetBitmapEncodeBackendForTest();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('explainBitmapEncodeBackend', () => {
|
|
27
|
+
it('reports absence, host installation, and custom precedence', () => {
|
|
28
|
+
expect(explainBitmapEncodeBackend()).toEqual({
|
|
29
|
+
conflict: false,
|
|
30
|
+
layer: 'host-not-enabled',
|
|
31
|
+
operation: null,
|
|
32
|
+
viability: 'unobserved',
|
|
33
|
+
});
|
|
34
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
35
|
+
expect(explainBitmapEncodeBackend()).toMatchObject({ layer: 'host' });
|
|
36
|
+
setBitmapEncodeBackend(createBackend());
|
|
37
|
+
expect(explainBitmapEncodeBackend()).toMatchObject({ layer: 'custom' });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('explainBitmapEncodeOperation', () => {
|
|
42
|
+
it('distinguishes sentinel, host, and custom layers', () => {
|
|
43
|
+
expect(explainBitmapEncodeOperation('encodeBitmap')).toEqual({
|
|
44
|
+
implemented: false,
|
|
45
|
+
layer: 'sentinel',
|
|
46
|
+
operation: 'encodeBitmap',
|
|
47
|
+
});
|
|
48
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
49
|
+
expect(explainBitmapEncodeOperation('encodeBitmap')).toEqual({
|
|
50
|
+
implemented: true,
|
|
51
|
+
layer: 'host',
|
|
52
|
+
operation: 'encodeBitmap',
|
|
53
|
+
});
|
|
54
|
+
setBitmapEncodeBackend(createBackend());
|
|
55
|
+
expect(explainBitmapEncodeOperation('encodeBitmap')).toEqual({
|
|
56
|
+
implemented: true,
|
|
57
|
+
layer: 'custom',
|
|
58
|
+
operation: 'encodeBitmap',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('getBitmapEncodeBackend', () => {
|
|
64
|
+
it('returns custom over host and null when neither is installed', () => {
|
|
65
|
+
const host = createBackend();
|
|
66
|
+
const custom = createBackend();
|
|
67
|
+
expect(getBitmapEncodeBackend()).toBeNull();
|
|
68
|
+
installBitmapEncodeHostBackend(host);
|
|
69
|
+
expect(getBitmapEncodeBackend()).toBe(host);
|
|
70
|
+
setBitmapEncodeBackend(custom);
|
|
71
|
+
expect(getBitmapEncodeBackend()).toBe(custom);
|
|
72
|
+
setBitmapEncodeBackend(null);
|
|
73
|
+
expect(getBitmapEncodeBackend()).toBe(host);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('hasBitmapEncodeHostBackend', () => {
|
|
78
|
+
it('reports host occupancy independently of a custom backend', () => {
|
|
79
|
+
setBitmapEncodeBackend(createBackend());
|
|
80
|
+
expect(hasBitmapEncodeHostBackend()).toBe(false);
|
|
81
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
82
|
+
expect(hasBitmapEncodeHostBackend()).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('hasBitmapEncodeOperation', () => {
|
|
87
|
+
it('does not count the sentinel as support', () => {
|
|
88
|
+
expect(hasBitmapEncodeOperation('encodeBitmap')).toBe(false);
|
|
89
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
90
|
+
expect(hasBitmapEncodeOperation('encodeBitmap')).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('installBitmapEncodeHostBackend', () => {
|
|
95
|
+
it('keeps the first host and records a conflicting later identity', () => {
|
|
96
|
+
const first = createBackend();
|
|
97
|
+
installBitmapEncodeHostBackend(first);
|
|
98
|
+
installBitmapEncodeHostBackend(first);
|
|
99
|
+
expect(explainBitmapEncodeBackend().conflict).toBe(false);
|
|
100
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
101
|
+
expect(getBitmapEncodeBackend()).toBe(first);
|
|
102
|
+
expect(explainBitmapEncodeBackend().conflict).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('observeBitmapEncodeHostResult', () => {
|
|
107
|
+
it.each([
|
|
108
|
+
[true, 'available'],
|
|
109
|
+
[false, 'runtime-api-unavailable'],
|
|
110
|
+
] as const)('records host viability for result %s', (succeeded, viability) => {
|
|
111
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
112
|
+
observeBitmapEncodeHostResult('encodeBitmap', succeeded);
|
|
113
|
+
expect(explainBitmapEncodeBackend()).toMatchObject({ operation: 'encodeBitmap', viability });
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('resetBitmapEncodeBackendForTest', () => {
|
|
118
|
+
it('clears custom, host, conflict, and observation state', () => {
|
|
119
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
120
|
+
installBitmapEncodeHostBackend(createBackend());
|
|
121
|
+
setBitmapEncodeBackend(createBackend());
|
|
122
|
+
observeBitmapEncodeHostResult('encodeBitmap', false);
|
|
123
|
+
resetBitmapEncodeBackendForTest();
|
|
124
|
+
expect(getBitmapEncodeBackend()).toBeNull();
|
|
125
|
+
expect(explainBitmapEncodeBackend()).toEqual({
|
|
126
|
+
conflict: false,
|
|
127
|
+
layer: 'host-not-enabled',
|
|
128
|
+
operation: null,
|
|
129
|
+
viability: 'unobserved',
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('setBitmapEncodeBackend', () => {
|
|
135
|
+
it('replaces and clears only the custom layer', () => {
|
|
136
|
+
const host = createBackend();
|
|
137
|
+
const first = createBackend();
|
|
138
|
+
const second = createBackend();
|
|
139
|
+
installBitmapEncodeHostBackend(host);
|
|
140
|
+
setBitmapEncodeBackend(first);
|
|
141
|
+
setBitmapEncodeBackend(second);
|
|
142
|
+
expect(getBitmapEncodeBackend()).toBe(second);
|
|
143
|
+
setBitmapEncodeBackend(null);
|
|
144
|
+
expect(getBitmapEncodeBackend()).toBe(host);
|
|
145
|
+
});
|
|
146
|
+
});
|
package/src/bitmapFrom.test.ts
CHANGED
|
@@ -1,28 +1,39 @@
|
|
|
1
1
|
import { createImageResource } from '@flighthq/image/contract';
|
|
2
|
+
import type { BitmapReadbackBackend } from '@flighthq/types/contract';
|
|
2
3
|
import { vi } from 'vitest';
|
|
3
4
|
|
|
4
5
|
import { createBitmap } from './bitmap';
|
|
5
|
-
import {
|
|
6
|
+
import { captureBitmapFromImageResource, createBitmapFromCanvas, createBitmapFromImageSource } from './bitmapFrom';
|
|
7
|
+
import { resetBitmapReadbackBackendForTest, setBitmapReadbackBackend } from './bitmapReadbackBackend';
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
resetBitmapReadbackBackendForTest();
|
|
11
|
+
});
|
|
6
12
|
|
|
7
13
|
describe('captureBitmapFromImageResource', () => {
|
|
8
|
-
it('
|
|
14
|
+
it('passes the resource source and dimensions through the selected readback backend', () => {
|
|
9
15
|
const canvas = document.createElement('canvas');
|
|
10
16
|
canvas.width = 3;
|
|
11
17
|
canvas.height = 2;
|
|
12
|
-
const bitmap =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
const bitmap = createBitmap(3, 2);
|
|
19
|
+
const readBitmap = vi.fn(() => ({ bitmap, reason: 'ok' as const }));
|
|
20
|
+
setBitmapReadbackBackend({ readBitmap });
|
|
21
|
+
|
|
22
|
+
expect(captureBitmapFromImageResource(createImageResource(canvas))).toBe(bitmap);
|
|
23
|
+
expect(readBitmap).toHaveBeenCalledOnce();
|
|
24
|
+
expect(readBitmap).toHaveBeenCalledWith(canvas, 3, 2, 'bitmap');
|
|
16
25
|
});
|
|
17
26
|
|
|
18
|
-
it('returns
|
|
27
|
+
it('returns null for an expected backend refusal', () => {
|
|
28
|
+
const backend: BitmapReadbackBackend = {
|
|
29
|
+
readBitmap: vi.fn(() => ({ bitmap: null, reason: 'tainted-source' as const })),
|
|
30
|
+
};
|
|
31
|
+
setBitmapReadbackBackend(backend);
|
|
19
32
|
const canvas = document.createElement('canvas');
|
|
20
33
|
canvas.width = 4;
|
|
21
34
|
canvas.height = 4;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
expect(data.width).toBe(4);
|
|
25
|
-
expect(data.height).toBe(4);
|
|
35
|
+
|
|
36
|
+
expect(captureBitmapFromImageResource(createImageResource(canvas))).toBeNull();
|
|
26
37
|
});
|
|
27
38
|
});
|
|
28
39
|
|
|
@@ -67,41 +78,17 @@ describe('createBitmapFromCanvas', () => {
|
|
|
67
78
|
});
|
|
68
79
|
|
|
69
80
|
describe('createBitmapFromImageSource', () => {
|
|
70
|
-
it('
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
expect(bitmap!.width).toBe(8);
|
|
77
|
-
expect(bitmap!.height).toBe(4);
|
|
78
|
-
expect(bitmap!.data.length).toBe(8 * 4 * 4);
|
|
79
|
-
});
|
|
81
|
+
it('returns the exact Bitmap from the selected backend outcome', () => {
|
|
82
|
+
const expected = createBitmap(8, 4);
|
|
83
|
+
const backend: BitmapReadbackBackend = {
|
|
84
|
+
readBitmap: vi.fn(() => ({ bitmap: expected, reason: 'ok' as const })),
|
|
85
|
+
};
|
|
86
|
+
setBitmapReadbackBackend(backend);
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
// A cross-origin draw taints the scratch canvas and the platform refuses its pixels with a
|
|
83
|
-
// SecurityError from getImageData. jsdom does not model tainting, so the throw is staged on
|
|
84
|
-
// getImageData directly — which is the exact call and the exact exception the sentinel exists to
|
|
85
|
-
// stop escaping to a caller who only asked for a bitmap.
|
|
86
|
-
const canvas = document.createElement('canvas');
|
|
87
|
-
canvas.width = 8;
|
|
88
|
-
canvas.height = 4;
|
|
89
|
-
const spy = vi.spyOn(CanvasRenderingContext2D.prototype, 'getImageData').mockImplementation(() => {
|
|
90
|
-
throw new DOMException('Tainted canvases may not be exported.', 'SecurityError');
|
|
91
|
-
});
|
|
92
|
-
try {
|
|
93
|
-
expect(() => createBitmapFromImageSource(canvas, 8, 4)).not.toThrow();
|
|
94
|
-
expect(createBitmapFromImageSource(canvas, 8, 4)).toBeNull();
|
|
95
|
-
} finally {
|
|
96
|
-
spy.mockRestore();
|
|
97
|
-
}
|
|
88
|
+
expect(createBitmapFromImageSource(document.createElement('canvas'), 8, 4)).toBe(expected);
|
|
98
89
|
});
|
|
99
90
|
|
|
100
|
-
it('returns null
|
|
101
|
-
|
|
102
|
-
canvas.width = 8;
|
|
103
|
-
canvas.height = 4;
|
|
104
|
-
expect(createBitmapFromImageSource(canvas, 0, 4)).toBeNull();
|
|
105
|
-
expect(createBitmapFromImageSource(canvas, 8, -1)).toBeNull();
|
|
91
|
+
it('returns null when no backend is installed', () => {
|
|
92
|
+
expect(createBitmapFromImageSource(document.createElement('canvas'), 8, 4)).toBeNull();
|
|
106
93
|
});
|
|
107
94
|
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { BitmapReadbackBackend } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getBitmapReadbackBackend,
|
|
5
|
+
hasBitmapReadbackHostBackend,
|
|
6
|
+
installBitmapReadbackHostBackend,
|
|
7
|
+
resetBitmapReadbackBackendForTest,
|
|
8
|
+
setBitmapReadbackBackend,
|
|
9
|
+
} from './bitmapReadbackBackend';
|
|
10
|
+
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
resetBitmapReadbackBackendForTest();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe('getBitmapReadbackBackend', () => {
|
|
16
|
+
it('prefers a custom backend over the installed host backend', () => {
|
|
17
|
+
const host = createBackend();
|
|
18
|
+
const custom = createBackend();
|
|
19
|
+
installBitmapReadbackHostBackend(host);
|
|
20
|
+
setBitmapReadbackBackend(custom);
|
|
21
|
+
|
|
22
|
+
expect(getBitmapReadbackBackend()).toBe(custom);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('hasBitmapReadbackHostBackend', () => {
|
|
27
|
+
it('reports only host installation', () => {
|
|
28
|
+
setBitmapReadbackBackend(createBackend());
|
|
29
|
+
expect(hasBitmapReadbackHostBackend()).toBe(false);
|
|
30
|
+
installBitmapReadbackHostBackend(createBackend());
|
|
31
|
+
expect(hasBitmapReadbackHostBackend()).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('installBitmapReadbackHostBackend', () => {
|
|
36
|
+
it('keeps the first installed host backend', () => {
|
|
37
|
+
const first = createBackend();
|
|
38
|
+
installBitmapReadbackHostBackend(first);
|
|
39
|
+
installBitmapReadbackHostBackend(createBackend());
|
|
40
|
+
|
|
41
|
+
expect(getBitmapReadbackBackend()).toBe(first);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('resetBitmapReadbackBackendForTest', () => {
|
|
46
|
+
it('clears custom and host backends', () => {
|
|
47
|
+
installBitmapReadbackHostBackend(createBackend());
|
|
48
|
+
setBitmapReadbackBackend(createBackend());
|
|
49
|
+
|
|
50
|
+
resetBitmapReadbackBackendForTest();
|
|
51
|
+
|
|
52
|
+
expect(getBitmapReadbackBackend()).toBeNull();
|
|
53
|
+
expect(hasBitmapReadbackHostBackend()).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('setBitmapReadbackBackend', () => {
|
|
58
|
+
it('sets and clears the custom backend', () => {
|
|
59
|
+
const backend = createBackend();
|
|
60
|
+
setBitmapReadbackBackend(backend);
|
|
61
|
+
expect(getBitmapReadbackBackend()).toBe(backend);
|
|
62
|
+
setBitmapReadbackBackend(null);
|
|
63
|
+
expect(getBitmapReadbackBackend()).toBeNull();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
function createBackend(): BitmapReadbackBackend {
|
|
68
|
+
return { readBitmap: () => ({ bitmap: null, reason: 'ok' }) };
|
|
69
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HostImageSource } from '@flighthq/types/contract';
|
|
2
|
+
import { vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { resetBitmapReadbackBackendForTest, setBitmapReadbackBackend } from './bitmapReadbackBackend';
|
|
5
|
+
import { resolveBitmapReadback } from './bitmapReadbackResolver';
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
resetBitmapReadbackBackendForTest();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
describe('resolveBitmapReadback', () => {
|
|
12
|
+
it('rejects empty dimensions before backend selection', () => {
|
|
13
|
+
expect(resolveBitmapReadback(source, 0, 1, 'bitmap')).toEqual({ bitmap: null, reason: 'empty-size' });
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('reports an absent backend', () => {
|
|
17
|
+
expect(resolveBitmapReadback(source, 1, 1, 'probe')).toEqual({
|
|
18
|
+
bitmap: null,
|
|
19
|
+
reason: 'backend-not-installed',
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('forwards source, dimensions, and mode to the selected backend', () => {
|
|
24
|
+
const readBitmap = vi.fn(() => ({ bitmap: null, reason: 'ok' as const }));
|
|
25
|
+
setBitmapReadbackBackend({ readBitmap });
|
|
26
|
+
|
|
27
|
+
expect(resolveBitmapReadback(source, 3, 5, 'probe')).toEqual({ bitmap: null, reason: 'ok' });
|
|
28
|
+
expect(readBitmap).toHaveBeenCalledWith(source, 3, 5, 'probe');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const source = {} as HostImageSource;
|