@flighthq/host-web 0.4.0-next.1836.147ae1e
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 +3 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +3 -0
- package/dist/contract.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/webCursor.d.ts +3 -0
- package/dist/webCursor.d.ts.map +1 -0
- package/dist/webCursor.js +8 -0
- package/dist/webCursor.js.map +1 -0
- package/dist/webGlyphRasterizer.d.ts +5 -0
- package/dist/webGlyphRasterizer.d.ts.map +1 -0
- package/dist/webGlyphRasterizer.js +122 -0
- package/dist/webGlyphRasterizer.js.map +1 -0
- package/package.json +49 -0
- package/src/evidence.test.ts +154 -0
- package/src/webCursor.test.ts +26 -0
- package/src/webGlyphRasterizer.test.ts +105 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC"}
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webCursor.d.ts","sourceRoot":"","sources":["../src/webCursor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEtE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,GAAG,aAAa,CAM1E"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webCursor.js","sourceRoot":"","sources":["../src/webCursor.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,sBAAsB,CAAC,OAAoB;IACzD,OAAO;QACL,SAAS,CAAC,MAAqB;YAC7B,OAAO,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,IAAI,EAAE,CAAC;QACtC,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GlyphRasterizerBackend } from '@flighthq/types/contract';
|
|
2
|
+
export declare function createWebGlyphRasterizerBackend(): GlyphRasterizerBackend;
|
|
3
|
+
export declare function enableHostWebGlyphRasterizer(): void;
|
|
4
|
+
export declare function resetHostWebGlyphRasterizerForTest(): void;
|
|
5
|
+
//# sourceMappingURL=webGlyphRasterizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webGlyphRasterizer.d.ts","sourceRoot":"","sources":["../src/webGlyphRasterizer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,+BAA+B,IAAI,sBAAsB,CAkBxE;AAKD,wBAAgB,4BAA4B,IAAI,IAAI,CAMnD;AAsFD,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { installGlyphRasterizerHostBackend } from '@flighthq/glyphatlas/contract';
|
|
2
|
+
export function createWebGlyphRasterizerBackend() {
|
|
3
|
+
return {
|
|
4
|
+
measureMetrics(options) {
|
|
5
|
+
const context = _acquireGlyphRasterContext();
|
|
6
|
+
if (context === null)
|
|
7
|
+
return null;
|
|
8
|
+
_applyGlyphRasterFont(context, options);
|
|
9
|
+
const metrics = context.measureText('Hg');
|
|
10
|
+
const ascent = metrics.fontBoundingBoxAscent;
|
|
11
|
+
const descent = metrics.fontBoundingBoxDescent;
|
|
12
|
+
if (!(ascent > 0) || !(descent >= 0))
|
|
13
|
+
return null;
|
|
14
|
+
return { ascent, descent, lineGap: 0 };
|
|
15
|
+
},
|
|
16
|
+
rasterize(codepoint, options) {
|
|
17
|
+
const context = _acquireGlyphRasterContext();
|
|
18
|
+
if (context === null)
|
|
19
|
+
return null;
|
|
20
|
+
return _rasterizeGlyphOnContext(context, codepoint, options);
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
// Probes the runtime environment for a 2D canvas context. The enabler calls this once at
|
|
25
|
+
// enable time to decide viability — the backend itself re-acquires on every call, but the
|
|
26
|
+
// viability signal is locked at enable time so explain reports a stable answer.
|
|
27
|
+
export function enableHostWebGlyphRasterizer() {
|
|
28
|
+
if (_enabled)
|
|
29
|
+
return;
|
|
30
|
+
_enabled = true;
|
|
31
|
+
const viable = _probeGlyphRasterizerViability();
|
|
32
|
+
const backend = viable ? createWebGlyphRasterizerBackend() : _unavailableBackend;
|
|
33
|
+
installGlyphRasterizerHostBackend(backend, viable);
|
|
34
|
+
}
|
|
35
|
+
function _acquireGlyphRasterContext() {
|
|
36
|
+
try {
|
|
37
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
38
|
+
const context = new OffscreenCanvas(1, 1).getContext('2d');
|
|
39
|
+
if (context !== null)
|
|
40
|
+
return context;
|
|
41
|
+
}
|
|
42
|
+
if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
|
|
43
|
+
const context = document.createElement('canvas').getContext('2d');
|
|
44
|
+
if (context !== null)
|
|
45
|
+
return context;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function _applyGlyphRasterFont(context, options) {
|
|
54
|
+
const fontStyle = options.fontStyle ?? 'normal';
|
|
55
|
+
const fontWeight = options.fontWeight ?? 'normal';
|
|
56
|
+
context.font = `${fontStyle} ${fontWeight} ${options.fontSize}px ${options.fontFamily}`;
|
|
57
|
+
}
|
|
58
|
+
function _probeGlyphRasterizerViability() {
|
|
59
|
+
try {
|
|
60
|
+
if (typeof OffscreenCanvas !== 'undefined') {
|
|
61
|
+
const context = new OffscreenCanvas(1, 1).getContext('2d');
|
|
62
|
+
if (context !== null)
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
|
|
66
|
+
const context = document.createElement('canvas').getContext('2d');
|
|
67
|
+
if (context !== null)
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function _rasterizeGlyphOnContext(context, codepoint, options) {
|
|
77
|
+
const text = String.fromCodePoint(codepoint);
|
|
78
|
+
_applyGlyphRasterFont(context, options);
|
|
79
|
+
context.textBaseline = 'alphabetic';
|
|
80
|
+
context.textAlign = 'left';
|
|
81
|
+
const metrics = context.measureText(text);
|
|
82
|
+
const advance = metrics.width;
|
|
83
|
+
const left = metrics.actualBoundingBoxLeft ?? 0;
|
|
84
|
+
const right = metrics.actualBoundingBoxRight ?? advance;
|
|
85
|
+
const ascent = metrics.actualBoundingBoxAscent ?? options.fontSize;
|
|
86
|
+
const descent = metrics.actualBoundingBoxDescent ?? 0;
|
|
87
|
+
const guard = 1;
|
|
88
|
+
const width = Math.max(0, Math.ceil(left + right)) + guard * 2;
|
|
89
|
+
const height = Math.max(0, Math.ceil(ascent + descent)) + guard * 2;
|
|
90
|
+
if (width <= guard * 2 || height <= guard * 2)
|
|
91
|
+
return null;
|
|
92
|
+
const canvas = context.canvas;
|
|
93
|
+
canvas.width = width;
|
|
94
|
+
canvas.height = height;
|
|
95
|
+
_applyGlyphRasterFont(context, options);
|
|
96
|
+
context.textBaseline = 'alphabetic';
|
|
97
|
+
context.textAlign = 'left';
|
|
98
|
+
context.clearRect(0, 0, width, height);
|
|
99
|
+
context.fillStyle = '#ffffff';
|
|
100
|
+
context.fillText(text, guard + left, guard + ascent);
|
|
101
|
+
const image = context.getImageData(0, 0, width, height);
|
|
102
|
+
return {
|
|
103
|
+
advance,
|
|
104
|
+
bearingX: -left,
|
|
105
|
+
bearingY: ascent,
|
|
106
|
+
height,
|
|
107
|
+
pixels: new Uint8ClampedArray(image.data),
|
|
108
|
+
width,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function resetHostWebGlyphRasterizerForTest() {
|
|
112
|
+
_enabled = false;
|
|
113
|
+
}
|
|
114
|
+
let _enabled = false;
|
|
115
|
+
// Installed when the environment lacks OffscreenCanvas/document/2D-context. A real object (not
|
|
116
|
+
// null) so get returns it and explain reports layer:host + viability:runtime-api-unavailable.
|
|
117
|
+
const _unavailableBackend = {
|
|
118
|
+
rasterize() {
|
|
119
|
+
return null;
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=webGlyphRasterizer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webGlyphRasterizer.js","sourceRoot":"","sources":["../src/webGlyphRasterizer.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/host-web",
|
|
3
|
+
"version": "0.4.0-next.1836.147ae1e",
|
|
4
|
+
"author": "Joshua Granick and other contributors",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/flighthq/flight.git",
|
|
9
|
+
"directory": "packages/host-web"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./contract": {
|
|
20
|
+
"types": "./dist/contract.d.ts",
|
|
21
|
+
"default": "./dist/contract.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src/**/*.test.ts",
|
|
27
|
+
"!dist/**/*.test.js",
|
|
28
|
+
"!dist/**/*.test.d.ts",
|
|
29
|
+
"!dist/**/*.test.js.map",
|
|
30
|
+
"!dist/**/*.test.d.ts.map"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsc -b",
|
|
34
|
+
"clean": "tsc -b --clean",
|
|
35
|
+
"test": "vitest run --config vitest.config.ts",
|
|
36
|
+
"test:watch": "vitest --watch --config vitest.config.ts",
|
|
37
|
+
"prepack": "npm run clean && npm run clean:dist && npm run build",
|
|
38
|
+
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@flighthq/glyphatlas": "0.4.0-next.1836.147ae1e",
|
|
42
|
+
"@flighthq/types": "0.4.0-next.1836.147ae1e"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.3.0"
|
|
46
|
+
},
|
|
47
|
+
"description": "Web host backend: installs browser-specific implementations of Flight capability seams (Canvas glyph rasterizer, DOM cursor)",
|
|
48
|
+
"sideEffects": false
|
|
49
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'fs';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import type { OutputChunk, RollupOutput } from 'rollup';
|
|
4
|
+
import { build } from 'vite';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
import { gzipSync } from 'zlib';
|
|
7
|
+
|
|
8
|
+
const ROOT = resolve(__dirname, '../../..');
|
|
9
|
+
const EVIDENCE_DIR = resolve(__dirname, '../evidence');
|
|
10
|
+
const FIXTURES = ['control', 'capability', 'cursor', 'glyph', 'both'] as const;
|
|
11
|
+
|
|
12
|
+
type FixtureName = (typeof FIXTURES)[number];
|
|
13
|
+
|
|
14
|
+
interface FixtureResult {
|
|
15
|
+
modules: string[];
|
|
16
|
+
rawBytes: number;
|
|
17
|
+
gzipBytes: number;
|
|
18
|
+
code: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveWorkspaceAliases(): Record<string, string> {
|
|
22
|
+
const packagesDir = resolve(ROOT, 'packages');
|
|
23
|
+
const alias: Record<string, string> = {};
|
|
24
|
+
const entries = require('fs').readdirSync(packagesDir, { withFileTypes: true });
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (!entry.isDirectory()) continue;
|
|
27
|
+
const pkgJsonPath = resolve(packagesDir, entry.name, 'package.json');
|
|
28
|
+
if (!existsSync(pkgJsonPath)) continue;
|
|
29
|
+
const pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
|
|
30
|
+
if (typeof pkgJson.name === 'string') {
|
|
31
|
+
alias[pkgJson.name] = resolve(packagesDir, entry.name, 'src');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return alias;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function buildFixture(name: FixtureName): Promise<FixtureResult> {
|
|
38
|
+
const entry = resolve(EVIDENCE_DIR, `${name}.ts`);
|
|
39
|
+
const alias = resolveWorkspaceAliases();
|
|
40
|
+
|
|
41
|
+
const result = (await build({
|
|
42
|
+
configFile: false,
|
|
43
|
+
root: EVIDENCE_DIR,
|
|
44
|
+
logLevel: 'silent',
|
|
45
|
+
resolve: { alias },
|
|
46
|
+
build: {
|
|
47
|
+
write: false,
|
|
48
|
+
minify: false,
|
|
49
|
+
target: 'esnext',
|
|
50
|
+
sourcemap: false,
|
|
51
|
+
rollupOptions: {
|
|
52
|
+
input: entry,
|
|
53
|
+
output: { format: 'es' },
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
})) as RollupOutput;
|
|
57
|
+
|
|
58
|
+
const chunks = result.output.filter((o): o is OutputChunk => o.type === 'chunk');
|
|
59
|
+
const mainChunk = chunks.find((c) => c.isEntry) ?? chunks[0];
|
|
60
|
+
const allModules = new Set<string>();
|
|
61
|
+
for (const chunk of chunks) {
|
|
62
|
+
for (const id of Object.keys(chunk.modules)) {
|
|
63
|
+
allModules.add(id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const code = mainChunk.code;
|
|
68
|
+
const rawBytes = Buffer.byteLength(code, 'utf-8');
|
|
69
|
+
const gzipBytes = gzipSync(code).byteLength;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
code,
|
|
73
|
+
gzipBytes,
|
|
74
|
+
modules: [...allModules].map((id) => id.replace(ROOT + '/', '')),
|
|
75
|
+
rawBytes,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sourceModules(modules: string[]): string[] {
|
|
80
|
+
return modules.filter((m) => !m.includes('/evidence/'));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasHostWebModule(modules: string[]): boolean {
|
|
84
|
+
return sourceModules(modules).some((m) => m.includes('packages/host-web/'));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function hasGlyphModule(modules: string[]): boolean {
|
|
88
|
+
return sourceModules(modules).some((m) => m.includes('webGlyphRasterizer'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hasCursorModule(modules: string[]): boolean {
|
|
92
|
+
return sourceModules(modules).some((m) => m.includes('webCursor'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
describe('evidence: tree-shaking isolation', () => {
|
|
96
|
+
const results = new Map<FixtureName, FixtureResult>();
|
|
97
|
+
|
|
98
|
+
async function getFixture(name: FixtureName): Promise<FixtureResult> {
|
|
99
|
+
let result = results.get(name);
|
|
100
|
+
if (result === undefined) {
|
|
101
|
+
result = await buildFixture(name);
|
|
102
|
+
results.set(name, result);
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it('control fixture contains zero host-web modules', async () => {
|
|
108
|
+
const { modules } = await getFixture('control');
|
|
109
|
+
expect(hasHostWebModule(modules)).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('capability fixture contains zero host-web modules', async () => {
|
|
113
|
+
const { modules } = await getFixture('capability');
|
|
114
|
+
expect(hasHostWebModule(modules)).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('cursor fixture excludes glyph rasterizer modules', async () => {
|
|
118
|
+
const { modules } = await getFixture('cursor');
|
|
119
|
+
expect(hasHostWebModule(modules)).toBe(true);
|
|
120
|
+
expect(hasGlyphModule(modules)).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('glyph fixture excludes cursor modules', async () => {
|
|
124
|
+
const { modules } = await getFixture('glyph');
|
|
125
|
+
expect(hasHostWebModule(modules)).toBe(true);
|
|
126
|
+
expect(hasCursorModule(modules)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('both module set is within the union of cursor and glyph', async () => {
|
|
130
|
+
const [cursorResult, glyphResult, bothResult] = await Promise.all([
|
|
131
|
+
getFixture('cursor'),
|
|
132
|
+
getFixture('glyph'),
|
|
133
|
+
getFixture('both'),
|
|
134
|
+
]);
|
|
135
|
+
const union = new Set([...sourceModules(cursorResult.modules), ...sourceModules(glyphResult.modules)]);
|
|
136
|
+
const outsideUnion = sourceModules(bothResult.modules).filter((m) => !union.has(m));
|
|
137
|
+
expect(outsideUnion).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('reports control-normalized sizes', async () => {
|
|
141
|
+
const control = await getFixture('control');
|
|
142
|
+
const names: FixtureName[] = ['capability', 'cursor', 'glyph', 'both'];
|
|
143
|
+
for (const name of names) {
|
|
144
|
+
const fixture = await getFixture(name);
|
|
145
|
+
const rawDelta = fixture.rawBytes - control.rawBytes;
|
|
146
|
+
const gzipDelta = fixture.gzipBytes - control.gzipBytes;
|
|
147
|
+
console.log(
|
|
148
|
+
`${name}: raw=${fixture.rawBytes} (${rawDelta >= 0 ? '+' : ''}${rawDelta}), ` +
|
|
149
|
+
`gzip=${fixture.gzipBytes} (${gzipDelta >= 0 ? '+' : ''}${gzipDelta})`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
console.log(`control: raw=${control.rawBytes}, gzip=${control.gzipBytes}`);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { createWebCursorBackend } from './webCursor';
|
|
4
|
+
|
|
5
|
+
describe('createWebCursorBackend', () => {
|
|
6
|
+
it('sets element.style.cursor to the given cursor value', () => {
|
|
7
|
+
const element = document.createElement('div');
|
|
8
|
+
const backend = createWebCursorBackend(element);
|
|
9
|
+
backend.setCursor('pointer');
|
|
10
|
+
expect(element.style.cursor).toBe('pointer');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('clears element.style.cursor when given null', () => {
|
|
14
|
+
const element = document.createElement('div');
|
|
15
|
+
const backend = createWebCursorBackend(element);
|
|
16
|
+
backend.setCursor('pointer');
|
|
17
|
+
backend.setCursor(null);
|
|
18
|
+
expect(element.style.cursor).toBe('');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('constructs without a DOM side effect', () => {
|
|
22
|
+
const element = document.createElement('div');
|
|
23
|
+
createWebCursorBackend(element);
|
|
24
|
+
expect(element.style.cursor).toBe('');
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
explainGlyphRasterizerBackend,
|
|
3
|
+
getGlyphRasterizerBackend,
|
|
4
|
+
installGlyphRasterizerHostBackend,
|
|
5
|
+
resetGlyphRasterizerBackendForTest,
|
|
6
|
+
} from '@flighthq/glyphatlas/contract';
|
|
7
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
createWebGlyphRasterizerBackend,
|
|
11
|
+
enableHostWebGlyphRasterizer,
|
|
12
|
+
resetHostWebGlyphRasterizerForTest,
|
|
13
|
+
} from './webGlyphRasterizer';
|
|
14
|
+
|
|
15
|
+
describe('createWebGlyphRasterizerBackend', () => {
|
|
16
|
+
afterEach(resetGlyphRasterizerBackendForTest);
|
|
17
|
+
|
|
18
|
+
it('constructs a backend with rasterize and measureMetrics', () => {
|
|
19
|
+
const backend = createWebGlyphRasterizerBackend();
|
|
20
|
+
expect(backend.rasterize).toBeTypeOf('function');
|
|
21
|
+
expect(backend.measureMetrics).toBeTypeOf('function');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('rasterize returns null when no canvas context is available', () => {
|
|
25
|
+
const backend = createWebGlyphRasterizerBackend();
|
|
26
|
+
const saved = HTMLCanvasElement.prototype.getContext;
|
|
27
|
+
HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext;
|
|
28
|
+
try {
|
|
29
|
+
expect(backend.rasterize(65, { fontFamily: 'x', fontSize: 16 })).toBeNull();
|
|
30
|
+
} finally {
|
|
31
|
+
HTMLCanvasElement.prototype.getContext = saved;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('enableHostWebGlyphRasterizer', () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
resetGlyphRasterizerBackendForTest();
|
|
39
|
+
resetHostWebGlyphRasterizerForTest();
|
|
40
|
+
vi.restoreAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('installs a host backend so get returns a non-sentinel', () => {
|
|
44
|
+
enableHostWebGlyphRasterizer();
|
|
45
|
+
const backend = getGlyphRasterizerBackend();
|
|
46
|
+
expect(backend).not.toBe(null);
|
|
47
|
+
expect(explainGlyphRasterizerBackend().layer).toBe('host');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('is idempotent — second call preserves provider identity and allocates nothing', () => {
|
|
51
|
+
enableHostWebGlyphRasterizer();
|
|
52
|
+
const first = getGlyphRasterizerBackend();
|
|
53
|
+
enableHostWebGlyphRasterizer();
|
|
54
|
+
const second = getGlyphRasterizerBackend();
|
|
55
|
+
expect(first).toBe(second);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('reports available when the canvas API is present', () => {
|
|
59
|
+
enableHostWebGlyphRasterizer();
|
|
60
|
+
expect(explainGlyphRasterizerBackend()).toEqual({ layer: 'host', viability: 'available' });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('reports runtime-api-unavailable when no canvas context can be acquired', () => {
|
|
64
|
+
const saved = HTMLCanvasElement.prototype.getContext;
|
|
65
|
+
HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext;
|
|
66
|
+
try {
|
|
67
|
+
enableHostWebGlyphRasterizer();
|
|
68
|
+
} finally {
|
|
69
|
+
HTMLCanvasElement.prototype.getContext = saved;
|
|
70
|
+
}
|
|
71
|
+
expect(explainGlyphRasterizerBackend()).toEqual({ layer: 'host', viability: 'runtime-api-unavailable' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('installs a real backend object even when runtime API is unavailable', () => {
|
|
75
|
+
const saved = HTMLCanvasElement.prototype.getContext;
|
|
76
|
+
HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext;
|
|
77
|
+
try {
|
|
78
|
+
enableHostWebGlyphRasterizer();
|
|
79
|
+
} finally {
|
|
80
|
+
HTMLCanvasElement.prototype.getContext = saved;
|
|
81
|
+
}
|
|
82
|
+
const backend = getGlyphRasterizerBackend();
|
|
83
|
+
expect(backend.rasterize).toBeTypeOf('function');
|
|
84
|
+
expect(backend.rasterize(65, { fontFamily: 'x', fontSize: 16 })).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not conflict with a prior install of the same backend via direct install', () => {
|
|
88
|
+
enableHostWebGlyphRasterizer();
|
|
89
|
+
const backend = getGlyphRasterizerBackend();
|
|
90
|
+
installGlyphRasterizerHostBackend(backend, true);
|
|
91
|
+
expect(explainGlyphRasterizerBackend().viability).not.toBe('provider-conflict');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('resetHostWebGlyphRasterizerForTest', () => {
|
|
96
|
+
afterEach(resetGlyphRasterizerBackendForTest);
|
|
97
|
+
|
|
98
|
+
it('clears the enabler flag so a subsequent enable re-installs', () => {
|
|
99
|
+
enableHostWebGlyphRasterizer();
|
|
100
|
+
resetHostWebGlyphRasterizerForTest();
|
|
101
|
+
resetGlyphRasterizerBackendForTest();
|
|
102
|
+
enableHostWebGlyphRasterizer();
|
|
103
|
+
expect(explainGlyphRasterizerBackend().layer).toBe('host');
|
|
104
|
+
});
|
|
105
|
+
});
|