@flighthq/swf 0.3.0 → 0.3.1-next.320.fe56fc2
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/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/swfDocument.d.ts +5 -5
- package/dist/swfDocument.d.ts.map +1 -1
- package/dist/swfDocument.js +359 -102
- package/dist/swfDocument.js.map +1 -1
- package/dist/swfFilter.d.ts +2 -2
- package/dist/swfFilter.d.ts.map +1 -1
- package/dist/swfFilter.js +24 -2
- package/dist/swfFilter.js.map +1 -1
- package/dist/swfFrameAction.d.ts +2 -2
- package/dist/swfFrameAction.d.ts.map +1 -1
- package/dist/swfFrameAction.js +13 -6
- package/dist/swfFrameAction.js.map +1 -1
- package/dist/swfFrameActionTestHelper.d.ts +2 -0
- package/dist/swfFrameActionTestHelper.d.ts.map +1 -0
- package/dist/swfFrameActionTestHelper.js +99 -0
- package/dist/swfFrameActionTestHelper.js.map +1 -0
- package/dist/swfImageDecoder.d.ts +4 -0
- package/dist/swfImageDecoder.d.ts.map +1 -0
- package/dist/swfImageDecoder.js +21 -0
- package/dist/swfImageDecoder.js.map +1 -0
- package/dist/swfMorphShape.d.ts +2 -1
- package/dist/swfMorphShape.d.ts.map +1 -1
- package/dist/swfMorphShape.js +18 -7
- package/dist/swfMorphShape.js.map +1 -1
- package/dist/swfText.d.ts +2 -2
- package/dist/swfText.d.ts.map +1 -1
- package/dist/swfText.js +15 -2
- package/dist/swfText.js.map +1 -1
- package/package.json +24 -22
- package/src/swfDocument.test.ts +1527 -163
- package/src/swfFilter.test.ts +53 -0
- package/src/swfFrameAction.test.ts +26 -97
- package/src/swfImageDecoder.test.ts +53 -0
- package/src/swfImageResources.test.ts +14 -17
- package/src/swfMorphShape.test.ts +18 -0
package/src/swfFilter.test.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
|
|
1
2
|
import type {
|
|
2
3
|
Adjustment,
|
|
3
4
|
BevelEffect,
|
|
@@ -6,11 +7,13 @@ import type {
|
|
|
6
7
|
ConvolutionEffect,
|
|
7
8
|
DropShadowEffect,
|
|
8
9
|
GradientGlowEffect,
|
|
10
|
+
ImportDiagnostic,
|
|
9
11
|
InnerGlowEffect,
|
|
10
12
|
InnerShadowEffect,
|
|
11
13
|
OuterGlowEffect,
|
|
12
14
|
RenderEffect,
|
|
13
15
|
} from '@flighthq/types/contract';
|
|
16
|
+
import { ImportDiagnosticSeverity } from '@flighthq/types/contract';
|
|
14
17
|
|
|
15
18
|
import { readSwfFilterList, setSwfFilterListGuard } from './swfFilter';
|
|
16
19
|
import { SwfReader } from './swfReader';
|
|
@@ -124,6 +127,35 @@ describe('readSwfFilterList', () => {
|
|
|
124
127
|
expect(glow.ratios).toEqual([0, 255]);
|
|
125
128
|
});
|
|
126
129
|
|
|
130
|
+
it('reports a gradient glow angle and distance it cannot represent, and stays silent without them', () => {
|
|
131
|
+
// GradientGlowEffect has no angle, distance, or inner/on-top member, so a glow authored with any of
|
|
132
|
+
// them loses that placement. The zero case is the load-bearing half: no crumb when nothing is lost.
|
|
133
|
+
const withPlacement = collectImportDiagnostics((sink) => {
|
|
134
|
+
readGlow(fixed(45), fixed(4), sink);
|
|
135
|
+
});
|
|
136
|
+
expect(withPlacement.map((entry) => entry.kind)).toEqual(['swf.filter-field-unrepresentable']);
|
|
137
|
+
expect(withPlacement[0].severity).toBe(ImportDiagnosticSeverity.Skip);
|
|
138
|
+
expect(withPlacement[0].detail).toEqual({
|
|
139
|
+
capability: 'swf.placement.filter-list',
|
|
140
|
+
field: 'placement',
|
|
141
|
+
filterId: 4,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(collectImportDiagnostics((sink) => readGlow(fixed(0), fixed(0), sink))).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('reports a blur pass count it cannot represent, and stays silent at one pass', () => {
|
|
148
|
+
// Found by auditing the CLAIM against the code: the filter-list wire covered gradient glow only,
|
|
149
|
+
// while BlurEffect silently discards an authored pass count. Same kind, distinguished by `field`.
|
|
150
|
+
const multi = collectImportDiagnostics((sink) => {
|
|
151
|
+
readBlur(3, sink);
|
|
152
|
+
});
|
|
153
|
+
expect(multi.map((entry) => entry.kind)).toEqual(['swf.filter-field-unrepresentable']);
|
|
154
|
+
expect(multi[0].detail).toEqual({ capability: 'swf.placement.filter-list', field: 'passes', filterId: 1 });
|
|
155
|
+
|
|
156
|
+
expect(collectImportDiagnostics((sink) => readBlur(1, sink))).toEqual([]);
|
|
157
|
+
});
|
|
158
|
+
|
|
127
159
|
it('reads a convolution kernel with its flags', () => {
|
|
128
160
|
const { effects } = read(
|
|
129
161
|
joinBytes(
|
|
@@ -225,6 +257,27 @@ describe('setSwfFilterListGuard', () => {
|
|
|
225
257
|
});
|
|
226
258
|
});
|
|
227
259
|
|
|
260
|
+
function readBlur(passes: number, diagnostics: ImportDiagnostic[]): void {
|
|
261
|
+
const bytes = joinBytes(new Uint8Array([1, 1]), fixed(4), fixed(4), new Uint8Array([passes]));
|
|
262
|
+
readSwfFilterList(new SwfReader(bytes, 0, bytes.length), [], [], diagnostics);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function readGlow(angle: Uint8Array, distance: Uint8Array, diagnostics: ImportDiagnostic[]): void {
|
|
266
|
+
const bytes = joinBytes(
|
|
267
|
+
new Uint8Array([1, 4, 2]),
|
|
268
|
+
rgba(0xff, 0, 0, 0),
|
|
269
|
+
rgba(0, 0, 0xff, 0xff),
|
|
270
|
+
new Uint8Array([0, 255]),
|
|
271
|
+
fixed(6),
|
|
272
|
+
fixed(6),
|
|
273
|
+
angle,
|
|
274
|
+
distance,
|
|
275
|
+
fixed8(1),
|
|
276
|
+
new Uint8Array([0]),
|
|
277
|
+
);
|
|
278
|
+
readSwfFilterList(new SwfReader(bytes, 0, bytes.length), [], [], diagnostics);
|
|
279
|
+
}
|
|
280
|
+
|
|
228
281
|
function read(bytes: Uint8Array): { adjustments: Adjustment[]; complete: boolean; effects: RenderEffect[] } {
|
|
229
282
|
const adjustments: Adjustment[] = [];
|
|
230
283
|
const effects: RenderEffect[] = [];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
|
|
1
2
|
import {
|
|
2
3
|
createMovieClip,
|
|
3
4
|
getMovieClipCurrentFrame,
|
|
@@ -6,8 +7,10 @@ import {
|
|
|
6
7
|
setMovieClipSource,
|
|
7
8
|
} from '@flighthq/movieclip/contract';
|
|
8
9
|
import type { MovieClip, Node2D } from '@flighthq/types/contract';
|
|
10
|
+
import { ImportDiagnosticSeverity } from '@flighthq/types/contract';
|
|
9
11
|
|
|
10
12
|
import { readSwfAbcFrameScripts, readSwfFrameActions } from './swfFrameAction';
|
|
13
|
+
import { buildFrameScriptAbc } from './swfFrameActionTestHelper';
|
|
11
14
|
import { SwfReader } from './swfReader';
|
|
12
15
|
|
|
13
16
|
describe('readSwfAbcFrameScripts', () => {
|
|
@@ -25,6 +28,29 @@ describe('readSwfAbcFrameScripts', () => {
|
|
|
25
28
|
expect(isMovieClipPlaying(clip)).toBe(false);
|
|
26
29
|
});
|
|
27
30
|
|
|
31
|
+
it('reports a frame script whose body is not a command this importer obeys', () => {
|
|
32
|
+
// The handler calls a name that is not one of the recognized playback commands, so the body parses
|
|
33
|
+
// and is then declined. The file bound a script to frame 1 and the document has none.
|
|
34
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
35
|
+
expect(readSwfAbcFrameScripts(buildFrameScriptAbc(1), sink)).not.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const dropped = diagnostics.filter((entry) => entry.kind === 'swf.abc-frame-script-declined');
|
|
39
|
+
expect(dropped).toHaveLength(1);
|
|
40
|
+
expect(dropped[0].severity).toBe(ImportDiagnosticSeverity.Drop);
|
|
41
|
+
expect(dropped[0].detail).toEqual({ frame: 1, reason: 'commands-declined' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('stays silent about a frame script it does obey, so the drop entry carries information', () => {
|
|
45
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
46
|
+
const scripts = readSwfAbcFrameScripts(buildFrameScriptAbc(), sink);
|
|
47
|
+
// Non-vacuous: a run that bound no frame script would be silent for the wrong reason.
|
|
48
|
+
expect([...scripts!.get('Main')!.keys()]).toEqual([1]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(diagnostics).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
28
54
|
it('reports nothing for bytecode that is not a readable container', () => {
|
|
29
55
|
expect(readSwfAbcFrameScripts(new Uint8Array([1, 2, 3]))).toBeNull();
|
|
30
56
|
});
|
|
@@ -86,103 +112,6 @@ describe('readSwfFrameActions', () => {
|
|
|
86
112
|
|
|
87
113
|
// An ABC file shaped the way a compiler writes one: a class whose constructor calls
|
|
88
114
|
// `addFrameScript(0, this.frame1)`, and a `frame1` method whose whole body is `stop()`.
|
|
89
|
-
function buildFrameScriptAbc(): Uint8Array {
|
|
90
|
-
const bytes: number[] = [];
|
|
91
|
-
const u8 = (v: number): void => void bytes.push(v & 0xff);
|
|
92
|
-
const u16 = (v: number): void => {
|
|
93
|
-
u8(v);
|
|
94
|
-
u8(v >> 8);
|
|
95
|
-
};
|
|
96
|
-
const u30 = (v: number): void => {
|
|
97
|
-
let r = v;
|
|
98
|
-
do {
|
|
99
|
-
const b = r % 0x80;
|
|
100
|
-
r = Math.floor(r / 0x80);
|
|
101
|
-
u8(r > 0 ? b | 0x80 : b);
|
|
102
|
-
} while (r > 0);
|
|
103
|
-
};
|
|
104
|
-
const str = (v: string): void => {
|
|
105
|
-
const e = new TextEncoder().encode(v);
|
|
106
|
-
u30(e.length);
|
|
107
|
-
for (const b of e) u8(b);
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
u16(16);
|
|
111
|
-
u16(46);
|
|
112
|
-
u30(1); // no integers
|
|
113
|
-
u30(1); // no unsigned integers
|
|
114
|
-
u30(1); // no doubles
|
|
115
|
-
u30(6); // strings: Main, addFrameScript, stop, frame1, ''
|
|
116
|
-
str('Main');
|
|
117
|
-
str('addFrameScript');
|
|
118
|
-
str('stop');
|
|
119
|
-
str('frame1');
|
|
120
|
-
str('');
|
|
121
|
-
u30(2); // one namespace: public, named by the empty string
|
|
122
|
-
u8(0x16);
|
|
123
|
-
u30(5);
|
|
124
|
-
u30(1); // no namespace sets
|
|
125
|
-
u30(5); // four qualified names, one per string above
|
|
126
|
-
for (let i = 1; i <= 4; i++) {
|
|
127
|
-
u8(0x07);
|
|
128
|
-
u30(1);
|
|
129
|
-
u30(i);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
u30(2); // two methods: the constructor and frame1
|
|
133
|
-
for (let i = 0; i < 2; i++) {
|
|
134
|
-
u30(0);
|
|
135
|
-
u30(0);
|
|
136
|
-
u30(0);
|
|
137
|
-
u8(0);
|
|
138
|
-
}
|
|
139
|
-
u30(0); // no metadata
|
|
140
|
-
|
|
141
|
-
u30(1); // one class
|
|
142
|
-
u30(1); // instance name -> Main
|
|
143
|
-
u30(1); // super name
|
|
144
|
-
u8(0);
|
|
145
|
-
u30(0); // no interfaces
|
|
146
|
-
u30(0); // instance initializer is method 0
|
|
147
|
-
u30(1); // one trait: the frame1 method
|
|
148
|
-
u30(4);
|
|
149
|
-
u8(0x01); // method trait
|
|
150
|
-
u30(0); // dispatch id
|
|
151
|
-
u30(1); // method index
|
|
152
|
-
u30(0); // class initializer
|
|
153
|
-
u30(0); // no class traits
|
|
154
|
-
|
|
155
|
-
u30(1); // one script
|
|
156
|
-
u30(0);
|
|
157
|
-
u30(0);
|
|
158
|
-
|
|
159
|
-
u30(2); // two method bodies
|
|
160
|
-
// The constructor: addFrameScript(0, this.frame1)
|
|
161
|
-
const constructorCode = [0xd0, 0x30, 0x5d, 0x02, 0x24, 0x00, 0xd0, 0x66, 0x04, 0x4f, 0x02, 0x02, 0x47];
|
|
162
|
-
u30(0);
|
|
163
|
-
u30(3);
|
|
164
|
-
u30(1);
|
|
165
|
-
u30(1);
|
|
166
|
-
u30(2);
|
|
167
|
-
u30(constructorCode.length);
|
|
168
|
-
for (const b of constructorCode) u8(b);
|
|
169
|
-
u30(0);
|
|
170
|
-
u30(0);
|
|
171
|
-
// frame1: stop()
|
|
172
|
-
const handlerCode = [0xd0, 0x30, 0x5d, 0x03, 0x4f, 0x03, 0x00, 0x47];
|
|
173
|
-
u30(1);
|
|
174
|
-
u30(2);
|
|
175
|
-
u30(1);
|
|
176
|
-
u30(1);
|
|
177
|
-
u30(2);
|
|
178
|
-
u30(handlerCode.length);
|
|
179
|
-
for (const b of handlerCode) u8(b);
|
|
180
|
-
u30(0);
|
|
181
|
-
u30(0);
|
|
182
|
-
|
|
183
|
-
return new Uint8Array(bytes);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
115
|
function actions(bytes: readonly number[]): SwfReader {
|
|
187
116
|
const source = new Uint8Array(bytes);
|
|
188
117
|
return new SwfReader(source, 0, source.length);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { registerDeflateDecompressor, unregisterDecompressor } from '@flighthq/compression/contract';
|
|
2
|
+
import {
|
|
3
|
+
clearImageDecoders,
|
|
4
|
+
decodeImage,
|
|
5
|
+
decodeImagePremultiplied,
|
|
6
|
+
hasImageDecoder,
|
|
7
|
+
} from '@flighthq/image-codec/contract';
|
|
8
|
+
import { Compression } from '@flighthq/types/contract';
|
|
9
|
+
|
|
10
|
+
import { registerSwfImageDecoders, SWF_LOSSLESS_ALPHA_MIME_TYPE, SWF_LOSSLESS_MIME_TYPE } from './swfImageDecoder';
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
clearImageDecoders();
|
|
14
|
+
unregisterDecompressor(Compression.Deflate);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
clearImageDecoders();
|
|
19
|
+
unregisterDecompressor(Compression.Deflate);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
describe('registerSwfImageDecoders', () => {
|
|
23
|
+
it('explicitly registers both container-native lossless MIME types', () => {
|
|
24
|
+
expect(hasImageDecoder(SWF_LOSSLESS_MIME_TYPE)).toBe(false);
|
|
25
|
+
expect(hasImageDecoder(SWF_LOSSLESS_ALPHA_MIME_TYPE)).toBe(false);
|
|
26
|
+
|
|
27
|
+
registerSwfImageDecoders();
|
|
28
|
+
|
|
29
|
+
expect(hasImageDecoder(SWF_LOSSLESS_MIME_TYPE)).toBe(true);
|
|
30
|
+
expect(hasImageDecoder(SWF_LOSSLESS_ALPHA_MIME_TYPE)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('retains authored premultiplied pixels when requested and normalizes the default to straight', async () => {
|
|
34
|
+
registerDeflateDecompressor();
|
|
35
|
+
registerSwfImageDecoders();
|
|
36
|
+
const payload = losslessPayload(5, 1, 1, stored([0x80, 0x40, 0x20, 0x10]));
|
|
37
|
+
|
|
38
|
+
const premultiplied = await decodeImagePremultiplied(payload, SWF_LOSSLESS_ALPHA_MIME_TYPE);
|
|
39
|
+
const straight = await decodeImage(payload, SWF_LOSSLESS_ALPHA_MIME_TYPE);
|
|
40
|
+
|
|
41
|
+
expect([...premultiplied!.data]).toEqual([0x40, 0x20, 0x10, 0x80]);
|
|
42
|
+
expect([...straight!.data]).toEqual([0x80, 0x40, 0x20, 0x80]);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function losslessPayload(format: number, width: number, height: number, pixels: readonly number[]): Uint8Array {
|
|
47
|
+
return new Uint8Array([format, width & 0xff, width >> 8, height & 0xff, height >> 8, ...pixels]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function stored(bytes: readonly number[]): number[] {
|
|
51
|
+
const length = bytes.length;
|
|
52
|
+
return [0x78, 0x01, 0x01, length & 0xff, length >> 8, ~length & 0xff, (~length >> 8) & 0xff, ...bytes, 0, 0, 0, 0];
|
|
53
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
+
import { clearImageDecoders, registerImageDecoder } from '@flighthq/image-codec/contract';
|
|
2
3
|
import { getNodeChildren } from '@flighthq/node/contract';
|
|
3
4
|
import { loadScene2DImageResources } from '@flighthq/scene2d-resources/contract';
|
|
4
5
|
import { getTextureSource } from '@flighthq/texture/contract';
|
|
5
|
-
import type {
|
|
6
|
+
import type { Bitmap, Shape, Texture2D, TextureSource } from '@flighthq/types/contract';
|
|
6
7
|
import {
|
|
8
|
+
BitmapTextureSourceKind,
|
|
7
9
|
ImageResourceReferenceKind,
|
|
8
|
-
ImageTextureSourceKind,
|
|
9
10
|
ResourceResolutionState,
|
|
10
11
|
ShapeKind,
|
|
11
12
|
} from '@flighthq/types/contract';
|
|
@@ -14,18 +15,15 @@ import { createScene2DFromSwf } from './swfDocument';
|
|
|
14
15
|
import { ShapeWriter } from './swfShapeTestHelper';
|
|
15
16
|
|
|
16
17
|
beforeEach(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return Promise.resolve();
|
|
23
|
-
});
|
|
18
|
+
registerImageDecoder('image/png', async () => ({
|
|
19
|
+
data: new Uint8ClampedArray([0x11, 0x22, 0x33, 0xff]),
|
|
20
|
+
height: 1,
|
|
21
|
+
width: 1,
|
|
22
|
+
}));
|
|
24
23
|
});
|
|
25
24
|
|
|
26
25
|
afterEach(() => {
|
|
27
|
-
|
|
28
|
-
delete (HTMLImageElement.prototype as Partial<HTMLImageElement>).decode;
|
|
26
|
+
clearImageDecoders();
|
|
29
27
|
});
|
|
30
28
|
|
|
31
29
|
describe('SWF image resources', () => {
|
|
@@ -53,18 +51,17 @@ describe('SWF image resources', () => {
|
|
|
53
51
|
expect(resources.resolved).toEqual([reference]);
|
|
54
52
|
expect(resources.unresolved).toEqual([]);
|
|
55
53
|
expect(reference.state).toBe(ResourceResolutionState.Resolved);
|
|
56
|
-
expect(image?.kind).toBe(
|
|
57
|
-
if (!
|
|
54
|
+
expect(image?.kind).toBe(BitmapTextureSourceKind);
|
|
55
|
+
if (!isBitmapTextureSource(image)) throw new Error('Expected decoded bitmap source');
|
|
58
56
|
expect(image.width).toBe(1);
|
|
59
57
|
expect(image.height).toBe(1);
|
|
60
|
-
expect(image.
|
|
61
|
-
expect(HTMLImageElement.prototype.decode).toHaveBeenCalledOnce();
|
|
58
|
+
expect([...image.data]).toEqual([0x11, 0x22, 0x33, 0xff]);
|
|
62
59
|
expect(getTextureSource(reference.textures![0])).toBe(image);
|
|
63
60
|
});
|
|
64
61
|
});
|
|
65
62
|
|
|
66
|
-
function
|
|
67
|
-
return source?.kind ===
|
|
63
|
+
function isBitmapTextureSource(source: Readonly<TextureSource> | null): source is Bitmap {
|
|
64
|
+
return source?.kind === BitmapTextureSourceKind;
|
|
68
65
|
}
|
|
69
66
|
|
|
70
67
|
function createBitmapFillSwf(imageBytes: Uint8Array): Uint8Array {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { collectImportDiagnostics } from '@flighthq/importdiagnostics/contract';
|
|
1
2
|
import { setMorphShapeProgress } from '@flighthq/shape/contract';
|
|
2
3
|
import { createTexture } from '@flighthq/texture/contract';
|
|
4
|
+
import { ImportDiagnosticSeverity } from '@flighthq/types/contract';
|
|
3
5
|
import type { Texture2D } from '@flighthq/types/contract';
|
|
4
6
|
|
|
5
7
|
import { createSwfMorphShape } from './swfMorphShape';
|
|
@@ -61,6 +63,22 @@ describe('createSwfMorphShape', () => {
|
|
|
61
63
|
expect(shape.data.commands[index + 2]).toBe(texture);
|
|
62
64
|
});
|
|
63
65
|
|
|
66
|
+
// No fire proof: the two edge streams are read in lockstep by readSwfMorphShapePaths, which breaks the
|
|
67
|
+
// moment either runs out, so both paths of a pair are built with identical structure and the mismatches
|
|
68
|
+
// createPathMorph declines on cannot arise from SWF bytes. The wire stays as a guard; the absence of a
|
|
69
|
+
// fire proof is recorded rather than papered over with a fixture that does not reach it.
|
|
70
|
+
it('stays silent when every path pair morphs, so the declined count carries information', () => {
|
|
71
|
+
const body = buildMorph({ endWidth: 400, startWidth: 200 });
|
|
72
|
+
let shape: ReturnType<typeof createSwfMorphShape> = null;
|
|
73
|
+
const diagnostics = collectImportDiagnostics((sink) => {
|
|
74
|
+
shape = createSwfMorphShape(new SwfReader(body, 0, body.length), 1, null, sink);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Non-vacuous: a run that produced no morph at all would be silent for the wrong reason.
|
|
78
|
+
expect(shape).not.toBeNull();
|
|
79
|
+
expect(diagnostics).toEqual([]);
|
|
80
|
+
});
|
|
81
|
+
|
|
64
82
|
it('returns null for a body that does not decode rather than throwing', () => {
|
|
65
83
|
expect(readMorph(new Uint8Array())).toBeNull();
|
|
66
84
|
expect(readMorph(new Uint8Array([1, 2, 3, 4, 5]))).toBeNull();
|