|
| 1 | +/** |
| 2 | + * Every preloadable asset type, with and without a space in the filename. |
| 3 | + * |
| 4 | + * A space is entirely ordinary in a shipped asset name, and it broke `fontface` |
| 5 | + * outright: that parser embeds the path in a CSS `url()` token, and an |
| 6 | + * UNQUOTED `url()` may not contain whitespace, so the descriptor never parsed |
| 7 | + * and no request was made: |
| 8 | + * |
| 9 | + * SyntaxError: The source provided ('url(data/fnt/Super Bouncer.ttf)') |
| 10 | + * could not be parsed as a value list. |
| 11 | + * |
| 12 | + * Every other transport hands the raw string to the browser, which |
| 13 | + * percent-encodes it and issues a real request. Measured, against a file that |
| 14 | + * does not exist so the outcome is purely about the transport: |
| 15 | + * |
| 16 | + * fetch() (binary/json/tmx/shader/obj/mtl) -> 404, request made |
| 17 | + * Image.src (image) -> error event |
| 18 | + * script.src (js) -> error event |
| 19 | + * video.src (video) -> error event |
| 20 | + * FontFace, unquoted -> SyntaxError, NO request |
| 21 | + * |
| 22 | + * So `fontface` was the only one affected. These tests pin the whole matrix so |
| 23 | + * a future parser that builds a URL by string concatenation cannot regress it |
| 24 | + * silently, and so the paired plain-name case proves the fixture itself is |
| 25 | + * sound rather than the test passing for the wrong reason. |
| 26 | + */ |
| 27 | +import { beforeAll, describe, expect, it } from "vitest"; |
| 28 | +import { boot, loader } from "../src/index.js"; |
| 29 | +import { preloadFontFace } from "../src/loader/parsers/fontface.js"; |
| 30 | + |
| 31 | +/** load one asset descriptor, resolving to "ok" or the failure reason */ |
| 32 | +const load = (asset) => { |
| 33 | + return new Promise((resolve) => { |
| 34 | + try { |
| 35 | + loader.load( |
| 36 | + asset, |
| 37 | + () => { |
| 38 | + return resolve("ok"); |
| 39 | + }, |
| 40 | + (err) => { |
| 41 | + return resolve(`error: ${err?.message ?? err}`); |
| 42 | + }, |
| 43 | + ); |
| 44 | + } catch (e) { |
| 45 | + resolve(`threw: ${e.message}`); |
| 46 | + } |
| 47 | + }); |
| 48 | +}; |
| 49 | + |
| 50 | +describe("asset paths containing a space", () => { |
| 51 | + beforeAll(() => { |
| 52 | + boot(); |
| 53 | + }); |
| 54 | + |
| 55 | + // Each entry is the SAME asset twice: once plainly named, once with a |
| 56 | + // space. The plain case is the control — if it fails, the fixture or the |
| 57 | + // parser is broken and the spaced case proves nothing. |
| 58 | + const matrix = [ |
| 59 | + ["image", "data/img/rect.png", "data/img/rect with space.png"], |
| 60 | + ["json", "data/misc/plain.json", "data/misc/name with space.json"], |
| 61 | + ["binary", "data/misc/plain.bin", "data/misc/name with space.bin"], |
| 62 | + ["js", "data/misc/plain.js", "data/misc/name with space.js"], |
| 63 | + ]; |
| 64 | + |
| 65 | + // Not in the round trip, and deliberately so: |
| 66 | + // |
| 67 | + // shader, tmx, tsx, obj, mtl, gltf, aseprite — all fetched through the |
| 68 | + // same `fetchData` path as "json" and "binary" above, so the transport |
| 69 | + // under test is already covered. `shader` additionally refuses to load |
| 70 | + // without an initialized Application and a GPU renderer, which would |
| 71 | + // put a WebGL context in this spec for nothing. |
| 72 | + // audio — goes through Howler, a third-party loader with its own URL |
| 73 | + // handling; worth its own test if it ever proves to matter. |
| 74 | + // video — `video.src`, measured to issue a real request for a spaced |
| 75 | + // name exactly as Image.src does. |
| 76 | + |
| 77 | + for (const [type, plainSrc, spacedSrc] of matrix) { |
| 78 | + it(`loads a "${type}" asset with a plain name`, async () => { |
| 79 | + const r = await load({ name: `plain-${type}`, type, src: plainSrc }); |
| 80 | + expect(r).toBe("ok"); |
| 81 | + }); |
| 82 | + |
| 83 | + it(`loads a "${type}" asset whose name contains a space`, async () => { |
| 84 | + const r = await load({ name: `spaced-${type}`, type, src: spacedSrc }); |
| 85 | + expect(r).toBe("ok"); |
| 86 | + }); |
| 87 | + } |
| 88 | + |
| 89 | + // `fontface` cannot round-trip here without shipping a real font binary, |
| 90 | + // and it does not need to: the bug was a PARSE failure that happened before |
| 91 | + // any request, so distinguishing SyntaxError from a network outcome is the |
| 92 | + // exact discrimination that matters. |
| 93 | + describe("fontface", () => { |
| 94 | + const outcomeFor = (src) => { |
| 95 | + return new Promise((resolve) => { |
| 96 | + preloadFontFace( |
| 97 | + { name: `probe-${src}`, src }, |
| 98 | + () => { |
| 99 | + return resolve("loaded"); |
| 100 | + }, |
| 101 | + (error) => { |
| 102 | + return resolve(error?.name ?? "unknown"); |
| 103 | + }, |
| 104 | + ); |
| 105 | + }); |
| 106 | + }; |
| 107 | + |
| 108 | + it("builds a parseable descriptor for a plain name", async () => { |
| 109 | + expect(await outcomeFor("data/fnt/Plain.ttf")).not.toBe("SyntaxError"); |
| 110 | + }); |
| 111 | + |
| 112 | + it("builds a parseable descriptor for a name with a space", async () => { |
| 113 | + expect(await outcomeFor("data/fnt/Super Bouncer.ttf")).not.toBe( |
| 114 | + "SyntaxError", |
| 115 | + ); |
| 116 | + }); |
| 117 | + }); |
| 118 | +}); |
0 commit comments