@flighthq/font 0.3.0-next.1032.e2b99fc → 0.3.0-next.1162.e90e518

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.
@@ -1 +1 @@
1
- {"version":3,"file":"fontResourceFrom.d.ts","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAKtE,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3G;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAIvF;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAMnG;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAY3G"}
1
+ {"version":3,"file":"fontResourceFrom.d.ts","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAWtE,wBAAsB,yBAAyB,CAAC,GAAG,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAS3G;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAIvF;AAED,wBAAsB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAMnG;AAED,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAY3G"}
@@ -1,5 +1,10 @@
1
1
  import { inferFontFormatFromUrl } from './fontFormat';
2
2
  import { getFontShorthand } from './fontShorthand';
3
+ // These loaders fill a caller-owned FontResource. Each assigns `out.face` only after its load
4
+ // resolves, which is the contract on failure: a rejected load leaves the resource on whatever face it
5
+ // already had rather than blanking it, so a caller that swallows the rejection keeps rendering with a
6
+ // working font instead of losing one. Assigning before the await would silently turn a failed reload
7
+ // into a broken resource.
3
8
  export async function loadFontResourceFromBytes(out, bytes) {
4
9
  const face = new FontFace(out.family, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
5
10
  await face.load();
@@ -1 +1 @@
1
- {"version":3,"file":"fontResourceFrom.js","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAiB,EAAE,KAAiB;IAClF,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,GAAG,CAAC,MAAM,EACT,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB;IAC9D,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAiB,EAAE,GAAW;IAC1E,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB,EAAE,OAAkB;IAClF,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC"}
1
+ {"version":3,"file":"fontResourceFrom.js","sourceRoot":"","sources":["../src/fontResourceFrom.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,8FAA8F;AAC9F,sGAAsG;AACtG,sGAAsG;AACtG,qGAAqG;AACrG,0BAA0B;AAE1B,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAAC,GAAiB,EAAE,KAAiB;IAClF,MAAM,IAAI,GAAG,IAAI,QAAQ,CACvB,GAAG,CAAC,MAAM,EACT,KAAK,CAAC,MAAsB,CAAC,KAAK,CAAC,KAAK,CAAC,UAAU,EAAE,KAAK,CAAC,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,CAC3F,CAAC;IACF,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB;IAC9D,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACtE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,GAAG,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;IAC1C,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,GAAiB,EAAE,GAAW;IAC1E,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,GAAG,CAAC,CAAC;IACrD,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAAC,GAAiB,EAAE,OAAkB;IAClF,MAAM,GAAG,GAAG,OAAO;SAChB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE;QACvB,MAAM,GAAG,GAAG,MAAM,IAAI,sBAAsB,CAAC,GAAG,CAAC,CAAC;QAClD,OAAO,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,aAAa,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,GAAG,CAAC;IACvE,CAAC,CAAC;SACD,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,IAAI,CAAC,IAAI,EAAE,CAAC;IAClB,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACzB,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC;IAChB,OAAO,GAAG,CAAC;AACb,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flighthq/font",
3
- "version": "0.3.0-next.1032.e2b99fc",
3
+ "version": "0.3.0-next.1162.e90e518",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/flighthq/flight.git",
@@ -36,8 +36,8 @@
36
36
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
37
  },
38
38
  "dependencies": {
39
- "@flighthq/entity": "0.3.0-next.1032.e2b99fc",
40
- "@flighthq/types": "0.3.0-next.1032.e2b99fc"
39
+ "@flighthq/entity": "0.3.0-next.1162.e90e518",
40
+ "@flighthq/types": "0.3.0-next.1162.e90e518"
41
41
  },
42
42
  "devDependencies": {
43
43
  "typescript": "^5.3.0"
@@ -6,86 +6,231 @@ import {
6
6
  loadFontResourceFromUrls,
7
7
  } from './fontResourceFrom';
8
8
 
9
- let mockFace: { load: ReturnType<typeof vi.fn> };
9
+ interface FontFaceConstruction {
10
+ family: string;
11
+ source: string | ArrayBuffer;
12
+ instance: MockFontFace;
13
+ }
10
14
 
15
+ let constructions: FontFaceConstruction[];
16
+ let addMock: ReturnType<typeof vi.fn>;
17
+ let loadMock: ReturnType<typeof vi.fn>;
18
+
19
+ class MockFontFace {
20
+ load = vi.fn().mockResolvedValue(undefined);
21
+ constructor(family: string, source: string | ArrayBuffer) {
22
+ constructions.push({ family, source, instance: this });
23
+ }
24
+ }
25
+
26
+ // Mirrors the harness in fontFrom.test.ts, including its restore discipline: the document.fonts
27
+ // descriptor is saved and put back per test rather than left overwritten, so under a shared
28
+ // (isolate:false) worker the patch cannot leak into a sibling file.
29
+ let originalFonts: PropertyDescriptor | undefined;
11
30
  beforeEach(() => {
12
- mockFace = { load: vi.fn().mockResolvedValue(undefined) };
13
- vi.stubGlobal(
14
- 'FontFace',
15
- vi.fn(function () {
16
- return mockFace;
17
- }),
18
- );
31
+ constructions = [];
32
+ addMock = vi.fn();
33
+ loadMock = vi.fn().mockResolvedValue([]);
34
+ vi.stubGlobal('FontFace', MockFontFace);
35
+ originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
19
36
  Object.defineProperty(document, 'fonts', {
20
- value: { add: vi.fn(), load: vi.fn() },
37
+ value: { add: addMock, load: loadMock },
21
38
  configurable: true,
22
- writable: true,
23
39
  });
24
- vi.spyOn(document.fonts, 'add').mockImplementation(() => document.fonts);
25
- vi.spyOn(document.fonts, 'load').mockResolvedValue([mockFace as unknown as FontFace]);
26
40
  });
27
41
 
28
42
  afterEach(() => {
43
+ if (originalFonts) {
44
+ Object.defineProperty(document, 'fonts', originalFonts);
45
+ } else {
46
+ delete (document as unknown as { fonts?: unknown }).fonts;
47
+ }
29
48
  vi.unstubAllGlobals();
30
49
  vi.restoreAllMocks();
31
50
  });
32
51
 
33
52
  describe('loadFontResourceFromBytes', () => {
34
- it('loads the face and attaches it to the resource', async () => {
53
+ it('constructs the face under the resource family, loads it, and registers it', async () => {
35
54
  const resource = createFontResource('TestFont');
36
55
  const result = await loadFontResourceFromBytes(resource, new Uint8Array(8));
37
56
  expect(result).toBe(resource);
38
- expect(resource.face).toBe(mockFace);
39
- expect(mockFace.load).toHaveBeenCalledOnce();
40
- expect(document.fonts.add).toHaveBeenCalledWith(mockFace);
57
+ expect(constructions).toHaveLength(1);
58
+ expect(constructions[0].family).toBe('TestFont');
59
+ expect(constructions[0].instance.load).toHaveBeenCalledOnce();
60
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
61
+ expect(resource.face).toBe(constructions[0].instance);
62
+ });
63
+
64
+ it('slices only the view bytes out of a larger backing buffer', async () => {
65
+ // Same slice arithmetic as loadFontFromBytes. Tested on both because they are separate
66
+ // implementations: without this, a regression in one of the two would go unnoticed.
67
+ const backing = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
68
+ await loadFontResourceFromBytes(createFontResource('TestFont'), backing.subarray(2, 6));
69
+ expect(new Uint8Array(constructions[0].source as ArrayBuffer)).toEqual(new Uint8Array([3, 4, 5, 6]));
70
+ });
71
+
72
+ it('propagates a load failure and does not register the face', async () => {
73
+ const resource = createFontResource('TestFont');
74
+ const failing = new MockFontFace('TestFont', new ArrayBuffer(0));
75
+ failing.load.mockRejectedValue(new Error('bad font'));
76
+ vi.stubGlobal(
77
+ 'FontFace',
78
+ vi.fn(function () {
79
+ return failing;
80
+ }),
81
+ );
82
+ await expect(loadFontResourceFromBytes(resource, new Uint8Array(4))).rejects.toThrow('bad font');
83
+ expect(addMock).not.toHaveBeenCalled();
84
+ expect(resource.face).toBeNull();
85
+ });
86
+
87
+ it('leaves a previously loaded face attached when a reload fails', async () => {
88
+ // The contract this pins: `face` is assigned only after the load resolves, so a failed reload
89
+ // rejects and leaves the resource on the font it already had rather than blanking it. A caller
90
+ // that ignores the rejection keeps rendering with a working font instead of losing one.
91
+ const resource = createFontResource('TestFont');
92
+ await loadFontResourceFromBytes(resource, new Uint8Array(4));
93
+ const loaded = resource.face;
94
+ expect(loaded).not.toBeNull();
95
+
96
+ const failing = new MockFontFace('TestFont', new ArrayBuffer(0));
97
+ failing.load.mockRejectedValue(new Error('boom'));
98
+ vi.stubGlobal(
99
+ 'FontFace',
100
+ vi.fn(function () {
101
+ return failing;
102
+ }),
103
+ );
104
+ await expect(loadFontResourceFromBytes(resource, new Uint8Array(4))).rejects.toThrow('boom');
105
+ expect(resource.face).toBe(loaded);
106
+ expect(addMock).toHaveBeenCalledTimes(1);
41
107
  });
42
108
  });
43
109
 
44
110
  describe('loadFontResourceFromName', () => {
45
- it('resolves a face already registered with the document', async () => {
111
+ it('queries the family via the escaped font shorthand', async () => {
112
+ await loadFontResourceFromName(createFontResource('MyFont'));
113
+ expect(loadMock).toHaveBeenCalledWith("1em 'MyFont'");
114
+ });
115
+
116
+ it('escapes a single quote in the family name so the shorthand stays valid', async () => {
117
+ await loadFontResourceFromName(createFontResource("Josh's Font"));
118
+ expect(loadMock).toHaveBeenCalledWith("1em 'Josh\\'s Font'");
119
+ });
120
+
121
+ it('attaches the first face when the document returns several', async () => {
122
+ const first = { family: 'first' } as unknown as FontFace;
123
+ const second = { family: 'second' } as unknown as FontFace;
124
+ loadMock.mockResolvedValue([first, second]);
46
125
  const resource = createFontResource('TestFont');
47
126
  const result = await loadFontResourceFromName(resource);
48
127
  expect(result).toBe(resource);
49
- expect(resource.face).toBe(mockFace);
128
+ expect(resource.face).toBe(first);
50
129
  });
51
130
 
52
131
  it('leaves face null when no registered faces are found', async () => {
53
- vi.spyOn(document.fonts, 'load').mockResolvedValue([]);
54
132
  const resource = createFontResource('UnknownFont');
55
133
  await loadFontResourceFromName(resource);
56
134
  expect(resource.face).toBeNull();
57
135
  });
136
+
137
+ it('registers nothing — a name lookup finds an existing face rather than adding one', async () => {
138
+ loadMock.mockResolvedValue([{ family: 'x' } as unknown as FontFace]);
139
+ await loadFontResourceFromName(createFontResource('TestFont'));
140
+ expect(addMock).not.toHaveBeenCalled();
141
+ expect(constructions).toHaveLength(0);
142
+ });
143
+
144
+ it('propagates a lookup failure', async () => {
145
+ loadMock.mockRejectedValue(new Error('no such font'));
146
+ await expect(loadFontResourceFromName(createFontResource('MyFont'))).rejects.toThrow('no such font');
147
+ });
58
148
  });
59
149
 
60
150
  describe('loadFontResourceFromUrl', () => {
61
- it('loads the face from a URL and attaches it to the resource', async () => {
62
- const resource = createFontResource('TestFont');
151
+ it('builds a url() source under the resource family and registers the face', async () => {
152
+ const resource = createFontResource('MyFont');
63
153
  const result = await loadFontResourceFromUrl(resource, 'https://example.com/font.woff2');
64
154
  expect(result).toBe(resource);
65
- expect(resource.face).toBe(mockFace);
66
- expect(document.fonts.add).toHaveBeenCalledWith(mockFace);
155
+ expect(constructions[0].family).toBe('MyFont');
156
+ expect(constructions[0].source).toBe('url(https://example.com/font.woff2)');
157
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
158
+ expect(resource.face).toBe(constructions[0].instance);
159
+ });
160
+
161
+ it('adds no format() hint — a single url carries none, unlike the multi-source form', async () => {
162
+ await loadFontResourceFromUrl(createFontResource('MyFont'), 'font.woff2');
163
+ expect(constructions[0].source).toBe('url(font.woff2)');
164
+ });
165
+
166
+ it('propagates a load failure and does not register the face', async () => {
167
+ const resource = createFontResource('MyFont');
168
+ const failing = new MockFontFace('MyFont', '');
169
+ failing.load.mockRejectedValue(new Error('bad url'));
170
+ vi.stubGlobal(
171
+ 'FontFace',
172
+ vi.fn(function () {
173
+ return failing;
174
+ }),
175
+ );
176
+ await expect(loadFontResourceFromUrl(resource, 'font.woff2')).rejects.toThrow('bad url');
177
+ expect(addMock).not.toHaveBeenCalled();
178
+ expect(resource.face).toBeNull();
67
179
  });
68
180
  });
69
181
 
70
182
  describe('loadFontResourceFromUrls', () => {
71
- it('builds a multi-source src string and loads the face', async () => {
72
- const resource = createFontResource('TestFont');
183
+ it('composes a comma-joined src with explicit and inferred format() hints', async () => {
184
+ // The block this replaces was titled "builds a multi-source src string" and asserted nothing
185
+ // about the string — only that a face was attached. The composed src is the whole point of this
186
+ // function, and it was the one thing not checked.
187
+ const resource = createFontResource('MyFont');
73
188
  const result = await loadFontResourceFromUrls(resource, [
74
189
  { url: 'font.woff2', format: 'woff2' },
75
190
  { url: 'font.ttf' },
76
191
  ]);
77
192
  expect(result).toBe(resource);
78
- expect(resource.face).toBe(mockFace);
79
- expect(document.fonts.add).toHaveBeenCalledWith(mockFace);
193
+ expect(constructions[0].family).toBe('MyFont');
194
+ expect(constructions[0].source).toBe("url(font.woff2) format('woff2'), url(font.ttf) format('truetype')");
195
+ expect(addMock).toHaveBeenCalledWith(constructions[0].instance);
80
196
  });
81
197
 
82
- it('infers format from file extension when no format is provided', async () => {
83
- const MockFontFace = vi.fn(function () {
84
- return mockFace;
85
- });
86
- vi.stubGlobal('FontFace', MockFontFace);
87
- const resource = createFontResource('TestFont');
88
- await loadFontResourceFromUrls(resource, [{ url: 'font.otf' }]);
89
- expect(MockFontFace).toHaveBeenCalledWith('TestFont', expect.stringContaining("format('opentype')"));
198
+ it('infers the format from the file extension when none is given', async () => {
199
+ await loadFontResourceFromUrls(createFontResource('MyFont'), [{ url: 'font.otf' }]);
200
+ expect(constructions[0].source).toBe("url(font.otf) format('opentype')");
201
+ });
202
+
203
+ it('omits the format() hint when it cannot be inferred and none is given', async () => {
204
+ await loadFontResourceFromUrls(createFontResource('MyFont'), [{ url: 'font.bin' }]);
205
+ expect(constructions[0].source).toBe('url(font.bin)');
206
+ });
207
+
208
+ it('mixes inferred, explicit, and absent hints across one src list', async () => {
209
+ await loadFontResourceFromUrls(createFontResource('MyFont'), [
210
+ { url: 'a.woff2', format: 'woff2' },
211
+ { url: 'b.ttf' },
212
+ { url: 'c.bin' },
213
+ ]);
214
+ expect(constructions[0].source).toBe("url(a.woff2) format('woff2'), url(b.ttf) format('truetype'), url(c.bin)");
215
+ });
216
+
217
+ it('builds an empty src for an empty source list rather than throwing', async () => {
218
+ await loadFontResourceFromUrls(createFontResource('MyFont'), []);
219
+ expect(constructions[0].source).toBe('');
220
+ });
221
+
222
+ it('propagates a load failure and does not register the face', async () => {
223
+ const resource = createFontResource('MyFont');
224
+ const failing = new MockFontFace('MyFont', '');
225
+ failing.load.mockRejectedValue(new Error('bad url'));
226
+ vi.stubGlobal(
227
+ 'FontFace',
228
+ vi.fn(function () {
229
+ return failing;
230
+ }),
231
+ );
232
+ await expect(loadFontResourceFromUrls(resource, [{ url: 'font.woff2' }])).rejects.toThrow('bad url');
233
+ expect(addMock).not.toHaveBeenCalled();
234
+ expect(resource.face).toBeNull();
90
235
  });
91
236
  });