@flighthq/host 0.5.1-edge.1370.903f328
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/LICENSE.md +9 -0
- package/README.md +17 -0
- package/dist/contract.d.ts +5 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +5 -0
- package/dist/contract.js.map +1 -0
- package/dist/enableHostGuards.d.ts +3 -0
- package/dist/enableHostGuards.d.ts.map +1 -0
- package/dist/enableHostGuards.js +37 -0
- package/dist/enableHostGuards.js.map +1 -0
- package/dist/host.d.ts +4 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +35 -0
- package/dist/host.js.map +1 -0
- package/dist/hostExplain.d.ts +10 -0
- package/dist/hostExplain.d.ts.map +1 -0
- package/dist/hostExplain.js +167 -0
- package/dist/hostExplain.js.map +1 -0
- package/dist/hostQuery.d.ts +58 -0
- package/dist/hostQuery.d.ts.map +1 -0
- package/dist/hostQuery.js +180 -0
- package/dist/hostQuery.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -0
- package/src/enableHostGuards.test.ts +93 -0
- package/src/host.test.ts +99 -0
- package/src/hostExplain.test.ts +218 -0
- package/src/hostQuery.test.ts +689 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// The public lane: querying a host and diagnosing one. createHost/initializeHost stay on ./contract —
|
|
2
|
+
// an app imports a prebuilt host constant from a @flighthq/host-* package rather than constructing one.
|
|
3
|
+
export { enableHostGuards, explainHost, explainHostAudioDevice, explainHostAudioMixer, explainHostImage, explainHostInputIngress, explainHostTextSegmenter, explainHostTextShaper, explainHostVideo, getHostAudioDevice, getHostAudioMixer, getHostBitmapEncode, getHostBitmapReadback, getHostClipboardFormats, getHostDevice, getHostFileSystem, getHostFontLoading, getHostGeolocation, getHostGlyphRasterizer, getHostHaptics, getHostImage, getHostInputIngress, getHostLifecycle, getHostNet, getHostNotificationPermission, getHostPlatform, getHostPowerKeepAwake, getHostScreenQuery, getHostSensors, getHostSocket, getHostSoftKeyboardInfo, getHostStorage, getHostStoragePersistenceQuery, getHostTextSegmenter, getHostTextShaper, getHostVideo, getHostWgpu, hasHostAudioDevice, hasHostAudioMixer, hasHostBitmapEncode, hasHostBitmapReadback, hasHostClipboardFormats, hasHostDevice, hasHostFileSystem, hasHostFontLoading, hasHostGeolocation, hasHostGlyphRasterizer, hasHostHaptics, hasHostImage, hasHostInputIngress, hasHostLifecycle, hasHostNet, hasHostNotificationPermission, hasHostPlatform, hasHostPowerKeepAwake, hasHostScreenQuery, hasHostSensors, hasHostSocket, hasHostSoftKeyboardInfo, hasHostStorage, hasHostStoragePersistenceQuery, hasHostTextSegmenter, hasHostTextShaper, hasHostVideo, hasHostWgpu, } from './contract';
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sGAAsG;AACtG,wGAAwG;AACxG,OAAO,EACL,gBAAgB,EAChB,WAAW,EACX,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,uBAAuB,EACvB,wBAAwB,EACxB,qBAAqB,EACrB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,6BAA6B,EAC7B,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,uBAAuB,EACvB,cAAc,EACd,8BAA8B,EAC9B,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,qBAAqB,EACrB,uBAAuB,EACvB,aAAa,EACb,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,cAAc,EACd,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,UAAU,EACV,6BAA6B,EAC7B,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,cAAc,EACd,aAAa,EACb,uBAAuB,EACvB,cAAc,EACd,8BAA8B,EAC9B,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACZ,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flighthq/host",
|
|
3
|
+
"version": "0.5.1-edge.1370.903f328",
|
|
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"
|
|
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/entity": "0.5.1-edge.1370.903f328",
|
|
42
|
+
"@flighthq/log": "0.5.1-edge.1370.903f328",
|
|
43
|
+
"@flighthq/types": "0.5.1-edge.1370.903f328"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"typescript": "^5.3.0"
|
|
47
|
+
},
|
|
48
|
+
"description": "Portable host interface: host construction, capability queries, and host diagnostics",
|
|
49
|
+
"sideEffects": false
|
|
50
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addLogSink,
|
|
3
|
+
clearLogOnceKeys,
|
|
4
|
+
createMemoryLogSink,
|
|
5
|
+
getMemoryLogSinkEntries,
|
|
6
|
+
removeLogSink,
|
|
7
|
+
} from '@flighthq/log/contract';
|
|
8
|
+
import type { EntityWithoutRuntime, Host, LogEntry } from '@flighthq/types/contract';
|
|
9
|
+
import { LogLevel } from '@flighthq/types/contract';
|
|
10
|
+
|
|
11
|
+
import { enableHostGuards } from './enableHostGuards';
|
|
12
|
+
import { createHost } from './host';
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
clearLogOnceKeys();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('enableHostGuards', () => {
|
|
19
|
+
it('warns once per commonly needed provider a host is missing', () => {
|
|
20
|
+
const entries = captureLog(() => enableHostGuards(createHost()));
|
|
21
|
+
|
|
22
|
+
expect(entries.length).toBe(GUARDED_SLOTS.length);
|
|
23
|
+
expect(entries.every((entry) => entry.level === LogLevel.Warn)).toBe(true);
|
|
24
|
+
expect(entries.map((entry) => slotOf(entry)).sort()).toEqual(GUARDED_SLOTS.slice().sort());
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('stays silent about a provider the host actually carries', () => {
|
|
28
|
+
const entries = captureLog(() => enableHostGuards(hostWith({ media: { video: PROVIDER } })));
|
|
29
|
+
|
|
30
|
+
expect(entries.map((entry) => slotOf(entry))).not.toContain('video');
|
|
31
|
+
expect(entries.length).toBe(GUARDED_SLOTS.length - 1);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('says nothing at all when every guarded provider is present', () => {
|
|
35
|
+
const entries = captureLog(() =>
|
|
36
|
+
enableHostGuards(
|
|
37
|
+
hostWith({
|
|
38
|
+
graphics: { image: PROVIDER },
|
|
39
|
+
input: { ingress: PROVIDER },
|
|
40
|
+
media: { audioDevice: PROVIDER, audioMixer: PROVIDER, video: PROVIDER },
|
|
41
|
+
text: { segmenter: PROVIDER, shaper: PROVIDER },
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
expect(entries).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('warns once across hosts, because the logOnce key is the slot rather than the host', () => {
|
|
50
|
+
const entries = captureLog(() => {
|
|
51
|
+
enableHostGuards(createHost());
|
|
52
|
+
enableHostGuards(createHost());
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
expect(entries.length).toBe(GUARDED_SLOTS.length);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('carries the remedy and its backends as structured data, not only as prose', () => {
|
|
59
|
+
const entries = captureLog(() => enableHostGuards(createHost()));
|
|
60
|
+
const video = entries.find((entry) => slotOf(entry) === 'video');
|
|
61
|
+
const data = video?.data as Readonly<Record<string, unknown>>;
|
|
62
|
+
|
|
63
|
+
expect(data.group).toBe('media');
|
|
64
|
+
expect(data.provider).toBe('HostVideoProvider');
|
|
65
|
+
expect(data.backends).toEqual(['webHost (@flighthq/host-web)']);
|
|
66
|
+
expect(String(data.message)).toContain('getHostVideo');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const PROVIDER = {};
|
|
71
|
+
|
|
72
|
+
const GUARDED_SLOTS = ['audioDevice', 'audioMixer', 'image', 'ingress', 'segmenter', 'shaper', 'video'];
|
|
73
|
+
|
|
74
|
+
function captureLog(run: () => void): readonly LogEntry[] {
|
|
75
|
+
const sink = createMemoryLogSink(32);
|
|
76
|
+
addLogSink(sink.sink);
|
|
77
|
+
try {
|
|
78
|
+
run();
|
|
79
|
+
return getMemoryLogSinkEntries(sink);
|
|
80
|
+
} finally {
|
|
81
|
+
removeLogSink(sink.sink);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function hostWith(groups: Readonly<Record<string, Readonly<Record<string, unknown>>>>): Host {
|
|
86
|
+
// A capability literal built from plain stand-ins cannot satisfy createHost's generic, so the shape is
|
|
87
|
+
// asserted. Only the presence of a slot value matters to the guard.
|
|
88
|
+
return createHost(groups as Partial<EntityWithoutRuntime<Host>>);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function slotOf(entry: Readonly<LogEntry>): string {
|
|
92
|
+
return String((entry.data as Readonly<Record<string, unknown>>).slot);
|
|
93
|
+
}
|
package/src/host.test.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { EntityRuntimeKey } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import * as contract from './contract';
|
|
4
|
+
import { initializeHost } from './host';
|
|
5
|
+
|
|
6
|
+
describe('createHost', () => {
|
|
7
|
+
it('creates an Entity immediately while leaving its runtime allocation lazy', () => {
|
|
8
|
+
const host = requiredFunction(contract, 'createHost')();
|
|
9
|
+
|
|
10
|
+
expect(EntityRuntimeKey in host).toBe(true);
|
|
11
|
+
expect(host[EntityRuntimeKey]).toBeUndefined();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('materializes the complete stable Host shape with truthful empty groups', () => {
|
|
15
|
+
const host = requiredFunction(contract, 'createHost')();
|
|
16
|
+
|
|
17
|
+
expect(
|
|
18
|
+
Object.keys(host)
|
|
19
|
+
.filter((key) => key !== String(EntityRuntimeKey))
|
|
20
|
+
.sort(),
|
|
21
|
+
).toEqual(HOST_GROUPS);
|
|
22
|
+
for (const group of HOST_GROUPS) expect(host[group]).toEqual({});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('preserves supplied capability-group and provider identities without populating another group', () => {
|
|
26
|
+
const provider = {};
|
|
27
|
+
const accessibility = { provider };
|
|
28
|
+
const host = requiredFunction(contract, 'createHost')({ accessibility });
|
|
29
|
+
|
|
30
|
+
expect(host.accessibility).toBe(accessibility);
|
|
31
|
+
expect(host.accessibility.provider).toBe(provider);
|
|
32
|
+
for (const group of HOST_GROUPS) {
|
|
33
|
+
if (group !== 'accessibility') expect(host[group]).toEqual({});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('is reachable on the contract lane only, because an app imports a built host rather than constructing one', async () => {
|
|
38
|
+
const publicLane = (await import('./index')) as Record<string, unknown>;
|
|
39
|
+
|
|
40
|
+
expect('createHost' in contract).toBe(true);
|
|
41
|
+
expect('createHost' in publicLane).toBe(false);
|
|
42
|
+
expect('initializeHost' in publicLane).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('initializeHost', () => {
|
|
47
|
+
it('is the construction initializer of createHost', () => {
|
|
48
|
+
expect(typeof initializeHost).toBe('function');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('writes every group into an existing construction target without allocating', () => {
|
|
52
|
+
const out = {} as Parameters<typeof initializeHost>[0];
|
|
53
|
+
const media = {};
|
|
54
|
+
initializeHost(out, { media });
|
|
55
|
+
|
|
56
|
+
expect(out.media).toBe(media);
|
|
57
|
+
expect(
|
|
58
|
+
Object.keys(out)
|
|
59
|
+
.filter((key) => key !== String(EntityRuntimeKey))
|
|
60
|
+
.sort(),
|
|
61
|
+
).toEqual(HOST_GROUPS);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const HOST_GROUPS = [
|
|
66
|
+
'accessibility',
|
|
67
|
+
'app',
|
|
68
|
+
'clipboard',
|
|
69
|
+
'connectivity',
|
|
70
|
+
'dialog',
|
|
71
|
+
'graphics',
|
|
72
|
+
'input',
|
|
73
|
+
'ipc',
|
|
74
|
+
'media',
|
|
75
|
+
'menu',
|
|
76
|
+
'midi',
|
|
77
|
+
'net',
|
|
78
|
+
'notification',
|
|
79
|
+
'power',
|
|
80
|
+
'protocol',
|
|
81
|
+
'screen',
|
|
82
|
+
'share',
|
|
83
|
+
'shell',
|
|
84
|
+
'shortcut',
|
|
85
|
+
'storage',
|
|
86
|
+
'system',
|
|
87
|
+
'text',
|
|
88
|
+
'tray',
|
|
89
|
+
'ui',
|
|
90
|
+
'updater',
|
|
91
|
+
'window',
|
|
92
|
+
] as const;
|
|
93
|
+
|
|
94
|
+
function requiredFunction(module: object, name: string): (...args: any[]) => any {
|
|
95
|
+
const value = Reflect.get(module, name);
|
|
96
|
+
expect(value, `${name} export`).toBeTypeOf('function');
|
|
97
|
+
if (typeof value !== 'function') throw new TypeError(`${name} is not exported`);
|
|
98
|
+
return value as (...args: any[]) => any;
|
|
99
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { EntityWithoutRuntime, Host } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import { createHost } from './host';
|
|
4
|
+
import {
|
|
5
|
+
explainHost,
|
|
6
|
+
explainHostAudioDevice,
|
|
7
|
+
explainHostAudioMixer,
|
|
8
|
+
explainHostImage,
|
|
9
|
+
explainHostInputIngress,
|
|
10
|
+
explainHostTextSegmenter,
|
|
11
|
+
explainHostTextShaper,
|
|
12
|
+
explainHostVideo,
|
|
13
|
+
} from './hostExplain';
|
|
14
|
+
import * as hostQuery from './hostQuery';
|
|
15
|
+
|
|
16
|
+
describe('explainHost', () => {
|
|
17
|
+
it('enumerates every capability group the host object carries', () => {
|
|
18
|
+
const explanation = explainHost(createHost());
|
|
19
|
+
|
|
20
|
+
expect(explanation.groups.map((group) => group.group)).toEqual(HOST_GROUPS);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('lists the slot keys a populated group actually holds', () => {
|
|
24
|
+
const host = hostWith({ media: { audioDevice: PROVIDER, video: PROVIDER } });
|
|
25
|
+
const media = explainHost(host).groups.find((group) => group.group === 'media');
|
|
26
|
+
|
|
27
|
+
expect(media?.slots.slice().sort()).toEqual(['audioDevice', 'video']);
|
|
28
|
+
expect(media?.isProvider).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('marks host.window as a provider rather than reporting its operations as slots', () => {
|
|
32
|
+
const host = hostWith({ window: { getWindowSize: () => undefined } });
|
|
33
|
+
const window = explainHost(host).groups.find((group) => group.group === 'window');
|
|
34
|
+
|
|
35
|
+
expect(window?.isProvider).toBe(true);
|
|
36
|
+
expect(window?.slots).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('reports every covered provider as absent on an empty host', () => {
|
|
40
|
+
const providers = explainHost(createHost()).providers;
|
|
41
|
+
|
|
42
|
+
expect(providers.length).toBeGreaterThan(0);
|
|
43
|
+
expect(providers.every((provider) => !provider.isPresent)).toBe(true);
|
|
44
|
+
expect(providers.map((provider) => `${provider.group}.${provider.slot}`)).toContain('media.video');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('flips a covered provider to present when its slot is filled', () => {
|
|
48
|
+
const host = hostWith({ media: { video: PROVIDER } });
|
|
49
|
+
const video = explainHost(host).providers.find((provider) => provider.slot === 'video');
|
|
50
|
+
|
|
51
|
+
expect(video?.isPresent).toBe(true);
|
|
52
|
+
expect(video?.provider).toBe('HostVideoProvider');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('covers exactly the slots hostQuery exposes accessors for, in both directions', () => {
|
|
56
|
+
const censused = explainHost(createHost())
|
|
57
|
+
.providers.map((provider) => `get${provider.provider.slice(0, -'Provider'.length)}`)
|
|
58
|
+
.sort();
|
|
59
|
+
const exported = Object.keys(hostQuery)
|
|
60
|
+
.filter((name) => name.startsWith('getHost'))
|
|
61
|
+
.sort();
|
|
62
|
+
|
|
63
|
+
expect(censused).toEqual(exported);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps every per-provider explainer naming the backends it returns as data', () => {
|
|
67
|
+
for (const explain of EXPLAINERS) {
|
|
68
|
+
const explanation = explain(createHost());
|
|
69
|
+
expect(explanation.backends.length, explanation.provider).toBeGreaterThan(0);
|
|
70
|
+
for (const backend of explanation.backends) {
|
|
71
|
+
expect(explanation.message, explanation.provider).toContain(backend.entryPoint);
|
|
72
|
+
expect(explanation.message, explanation.provider).toContain(backend.packageName);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('pairs every getHost accessor with a hasHost accessor', () => {
|
|
78
|
+
const getters = Object.keys(hostQuery)
|
|
79
|
+
.filter((name) => name.startsWith('getHost'))
|
|
80
|
+
.sort();
|
|
81
|
+
const predicates = Object.keys(hostQuery)
|
|
82
|
+
.filter((name) => name.startsWith('hasHost'))
|
|
83
|
+
.sort();
|
|
84
|
+
|
|
85
|
+
expect(predicates).toEqual(getters.map((name) => `has${name.slice('get'.length)}`));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('explainHostAudioDevice', () => {
|
|
90
|
+
it('names the accessor, the slot, and where to get the provider when it is absent', () => {
|
|
91
|
+
const explanation = explainHostAudioDevice(createHost());
|
|
92
|
+
|
|
93
|
+
expect(explanation.isPresent).toBe(false);
|
|
94
|
+
expect(explanation.group).toBe('media');
|
|
95
|
+
expect(explanation.slot).toBe('audioDevice');
|
|
96
|
+
expect(explanation.provider).toBe('HostAudioDeviceProvider');
|
|
97
|
+
expect(explanation.message).toContain('getHostAudioDevice');
|
|
98
|
+
expect(explanation.backends.map((backend) => backend.packageName)).toEqual(['@flighthq/host-web']);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('reports presence without a remedy when the slot is filled', () => {
|
|
102
|
+
const explanation = explainHostAudioDevice(hostWith({ media: { audioDevice: PROVIDER } }));
|
|
103
|
+
|
|
104
|
+
expect(explanation.isPresent).toBe(true);
|
|
105
|
+
expect(explanation.message).toBe('getHostAudioDevice: host.media.audioDevice is present');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('explainHostAudioMixer', () => {
|
|
110
|
+
it('explains an absent mixer against the media group', () => {
|
|
111
|
+
const explanation = explainHostAudioMixer(createHost());
|
|
112
|
+
|
|
113
|
+
expect(explanation.slot).toBe('audioMixer');
|
|
114
|
+
expect(explanation.isPresent).toBe(false);
|
|
115
|
+
expect(explanation.backends.length).toBeGreaterThan(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('explainHostImage', () => {
|
|
120
|
+
it('explains an absent image provider against the graphics group', () => {
|
|
121
|
+
const explanation = explainHostImage(createHost());
|
|
122
|
+
|
|
123
|
+
expect(explanation.group).toBe('graphics');
|
|
124
|
+
expect(explanation.provider).toBe('HostImageProvider');
|
|
125
|
+
expect(explanation.isPresent).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('explainHostInputIngress', () => {
|
|
130
|
+
it('explains an absent ingress provider against the input group', () => {
|
|
131
|
+
const explanation = explainHostInputIngress(createHost());
|
|
132
|
+
|
|
133
|
+
expect(explanation.group).toBe('input');
|
|
134
|
+
expect(explanation.slot).toBe('ingress');
|
|
135
|
+
expect(explanation.isPresent).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('explainHostTextSegmenter', () => {
|
|
140
|
+
it('points at the package that builds a segmenter rather than at a host backend', () => {
|
|
141
|
+
const explanation = explainHostTextSegmenter(createHost());
|
|
142
|
+
|
|
143
|
+
expect(explanation.backends.map((backend) => backend.packageName)).toEqual(['@flighthq/textsegment']);
|
|
144
|
+
expect(explanation.message).toContain('createWebTextSegmenterBackend');
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe('explainHostTextShaper', () => {
|
|
149
|
+
it('points at the package that builds a shaper rather than at a host backend', () => {
|
|
150
|
+
const explanation = explainHostTextShaper(createHost());
|
|
151
|
+
|
|
152
|
+
expect(explanation.backends.map((backend) => backend.packageName)).toEqual(['@flighthq/textshaper-canvas']);
|
|
153
|
+
expect(explanation.message).toContain('createCanvasTextShaperBackend');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('reports presence when the shaper is composed into the text group', () => {
|
|
157
|
+
const explanation = explainHostTextShaper(hostWith({ text: { shaper: PROVIDER } }));
|
|
158
|
+
|
|
159
|
+
expect(explanation.isPresent).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('explainHostVideo', () => {
|
|
164
|
+
it('explains an absent video provider against the media group', () => {
|
|
165
|
+
const explanation = explainHostVideo(createHost());
|
|
166
|
+
|
|
167
|
+
expect(explanation.group).toBe('media');
|
|
168
|
+
expect(explanation.provider).toBe('HostVideoProvider');
|
|
169
|
+
expect(explanation.isPresent).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const PROVIDER = {};
|
|
174
|
+
|
|
175
|
+
function hostWith(groups: Readonly<Record<string, Readonly<Record<string, unknown>>>>): Host {
|
|
176
|
+
// A capability literal built from plain stand-ins cannot satisfy createHost's generic, so the shape is
|
|
177
|
+
// asserted. Identity, not structure, is what every assertion below reads.
|
|
178
|
+
return createHost(groups as Partial<EntityWithoutRuntime<Host>>);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const EXPLAINERS = [
|
|
182
|
+
explainHostAudioDevice,
|
|
183
|
+
explainHostAudioMixer,
|
|
184
|
+
explainHostImage,
|
|
185
|
+
explainHostInputIngress,
|
|
186
|
+
explainHostTextSegmenter,
|
|
187
|
+
explainHostTextShaper,
|
|
188
|
+
explainHostVideo,
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const HOST_GROUPS = [
|
|
192
|
+
'accessibility',
|
|
193
|
+
'app',
|
|
194
|
+
'clipboard',
|
|
195
|
+
'connectivity',
|
|
196
|
+
'dialog',
|
|
197
|
+
'graphics',
|
|
198
|
+
'input',
|
|
199
|
+
'ipc',
|
|
200
|
+
'media',
|
|
201
|
+
'menu',
|
|
202
|
+
'midi',
|
|
203
|
+
'net',
|
|
204
|
+
'notification',
|
|
205
|
+
'power',
|
|
206
|
+
'protocol',
|
|
207
|
+
'screen',
|
|
208
|
+
'share',
|
|
209
|
+
'shell',
|
|
210
|
+
'shortcut',
|
|
211
|
+
'storage',
|
|
212
|
+
'system',
|
|
213
|
+
'text',
|
|
214
|
+
'tray',
|
|
215
|
+
'ui',
|
|
216
|
+
'updater',
|
|
217
|
+
'window',
|
|
218
|
+
];
|