@ccheever/exact-renderer 0.1.0
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/package.json +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
package/package.json
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ccheever/exact-renderer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Renderer internals and primitives for Exact",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.ts",
|
|
11
|
+
"types": "./src/index.ts"
|
|
12
|
+
},
|
|
13
|
+
"./classname-resolve": {
|
|
14
|
+
"import": "./src/classname-resolve.ts",
|
|
15
|
+
"types": "./src/classname-resolve.ts"
|
|
16
|
+
},
|
|
17
|
+
"./classname-contract": {
|
|
18
|
+
"import": "./src/classname-contract.ts",
|
|
19
|
+
"types": "./src/classname-contract.ts"
|
|
20
|
+
},
|
|
21
|
+
"./adapter-contract": {
|
|
22
|
+
"import": "./src/adapter-contract.ts",
|
|
23
|
+
"types": "./src/adapter-contract.ts"
|
|
24
|
+
},
|
|
25
|
+
"./dom-shim": {
|
|
26
|
+
"import": "./src/dom-shim.ts",
|
|
27
|
+
"types": "./src/dom-shim.ts"
|
|
28
|
+
},
|
|
29
|
+
"./dom-mirror": {
|
|
30
|
+
"import": "./src/dom-mirror.ts",
|
|
31
|
+
"types": "./src/dom-mirror.ts"
|
|
32
|
+
},
|
|
33
|
+
"./web-host": {
|
|
34
|
+
"import": "./src/web-host.ts",
|
|
35
|
+
"types": "./src/web-host.ts"
|
|
36
|
+
},
|
|
37
|
+
"./host-ops": {
|
|
38
|
+
"import": "./src/host-ops.ts",
|
|
39
|
+
"types": "./src/host-ops.ts"
|
|
40
|
+
},
|
|
41
|
+
"./inspector": {
|
|
42
|
+
"import": "./src/inspector.ts",
|
|
43
|
+
"types": "./src/inspector.ts"
|
|
44
|
+
},
|
|
45
|
+
"./native-primitives": {
|
|
46
|
+
"import": "./src/native-primitives.ts",
|
|
47
|
+
"types": "./src/native-primitives.ts"
|
|
48
|
+
},
|
|
49
|
+
"./primitives": {
|
|
50
|
+
"native": "./src/native-primitives.ts",
|
|
51
|
+
"import": "./src/primitives.ts",
|
|
52
|
+
"types": "./src/primitives.ts"
|
|
53
|
+
},
|
|
54
|
+
"./runtime": {
|
|
55
|
+
"import": "./src/runtime.ts",
|
|
56
|
+
"types": "./src/runtime.ts"
|
|
57
|
+
},
|
|
58
|
+
"./style": {
|
|
59
|
+
"import": "./src/style/index.ts",
|
|
60
|
+
"types": "./src/style/index.ts"
|
|
61
|
+
},
|
|
62
|
+
"./text/paragraph-lowering": {
|
|
63
|
+
"import": "./src/text/paragraph-lowering.ts",
|
|
64
|
+
"types": "./src/text/paragraph-lowering.ts"
|
|
65
|
+
},
|
|
66
|
+
"./solid": {
|
|
67
|
+
"import": "./src/solid.ts",
|
|
68
|
+
"types": "./src/solid.ts"
|
|
69
|
+
},
|
|
70
|
+
"./svelte": {
|
|
71
|
+
"import": "./src/svelte.ts",
|
|
72
|
+
"types": "./src/svelte.ts"
|
|
73
|
+
},
|
|
74
|
+
"./types": {
|
|
75
|
+
"import": "./src/types.ts",
|
|
76
|
+
"types": "./src/types.ts"
|
|
77
|
+
},
|
|
78
|
+
"./vue": {
|
|
79
|
+
"import": "./src/vue.ts",
|
|
80
|
+
"types": "./src/vue.ts"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"dependencies": {
|
|
84
|
+
"@exact/core": "npm:@ccheever/exact-core@0.1.0",
|
|
85
|
+
"@exact/text": "npm:@ccheever/exact-text@0.1.0",
|
|
86
|
+
"@exact/runtime-js": "npm:@ccheever/exact-runtime-js@0.1.0",
|
|
87
|
+
"react-reconciler": "^0.33.0",
|
|
88
|
+
"thumbhash": "^0.1.1"
|
|
89
|
+
},
|
|
90
|
+
"peerDependencies": {
|
|
91
|
+
"@vue/runtime-core": "^3.5.0",
|
|
92
|
+
"react": "^19.0.0",
|
|
93
|
+
"solid-js": "^1.9.0",
|
|
94
|
+
"svelte": "^5.0.0"
|
|
95
|
+
},
|
|
96
|
+
"peerDependenciesMeta": {
|
|
97
|
+
"@vue/runtime-core": {
|
|
98
|
+
"optional": true
|
|
99
|
+
},
|
|
100
|
+
"solid-js": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"svelte": {
|
|
104
|
+
"optional": true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"devDependencies": {
|
|
108
|
+
"@types/react": "^19.2.8",
|
|
109
|
+
"@types/react-dom": "^19.2.3",
|
|
110
|
+
"@types/react-reconciler": "^0.32.3",
|
|
111
|
+
"@vue/runtime-core": "^3.5.0",
|
|
112
|
+
"react": "^19.2.3",
|
|
113
|
+
"react-dom": "^19.2.3",
|
|
114
|
+
"solid-js": "^1.9.0",
|
|
115
|
+
"svelte": "^5.0.0",
|
|
116
|
+
"happy-dom": "^20.8.4"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import React, { act } from 'react';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { View } from '../components.js';
|
|
5
|
+
import { getRootNode } from '../inspector.js';
|
|
6
|
+
import { _resetNodeIdCounter, NodeKind, type ElementNode } from '../nodes/index.js';
|
|
7
|
+
import {
|
|
8
|
+
render as renderReact,
|
|
9
|
+
reset as resetReact,
|
|
10
|
+
} from '../reconciler.js';
|
|
11
|
+
import {
|
|
12
|
+
createApp as createVueApp,
|
|
13
|
+
h,
|
|
14
|
+
nextTick,
|
|
15
|
+
ref,
|
|
16
|
+
reset as resetVue,
|
|
17
|
+
render as renderVue,
|
|
18
|
+
} from '../vue.js';
|
|
19
|
+
import {
|
|
20
|
+
createElement as createSolidElement,
|
|
21
|
+
render as renderSolid,
|
|
22
|
+
reset as resetSolid,
|
|
23
|
+
setProp as setSolidProp,
|
|
24
|
+
} from '../solid.js';
|
|
25
|
+
import { _clearHandlers } from '../host-config.js';
|
|
26
|
+
import { _resetHostOpsState } from '../host-ops.js';
|
|
27
|
+
import { _resetInspectorState } from '../inspector.js';
|
|
28
|
+
import { _resetWindowRuntimeForTests } from '../../../../js/src/window-management.js';
|
|
29
|
+
|
|
30
|
+
interface ExactRuntimeMock {
|
|
31
|
+
dispatch: ReturnType<typeof vi.fn>;
|
|
32
|
+
screenWidth: number;
|
|
33
|
+
screenHeight: number;
|
|
34
|
+
screenScale?: number;
|
|
35
|
+
safeAreaInsets?: {
|
|
36
|
+
top?: number;
|
|
37
|
+
right?: number;
|
|
38
|
+
bottom?: number;
|
|
39
|
+
left?: number;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ExactGlobals {
|
|
44
|
+
exact?: ExactRuntimeMock;
|
|
45
|
+
__exactSafeAreaInsetsChanged?: (
|
|
46
|
+
top?: number,
|
|
47
|
+
right?: number,
|
|
48
|
+
bottom?: number,
|
|
49
|
+
left?: number,
|
|
50
|
+
) => void;
|
|
51
|
+
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getFirstElement(rootId: number = 0): ElementNode {
|
|
55
|
+
const root = getRootNode(rootId);
|
|
56
|
+
expect(root).not.toBeNull();
|
|
57
|
+
expect(root?.children[0]?.kind).toBe(NodeKind.Element);
|
|
58
|
+
|
|
59
|
+
const child = root?.children[0];
|
|
60
|
+
if (!child || child.kind !== NodeKind.Element) {
|
|
61
|
+
throw new Error(`Expected root ${rootId} to have an element child.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return child;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getFirstTextValue(rootId: number = 0): string | undefined {
|
|
68
|
+
const element = getFirstElement(rootId);
|
|
69
|
+
const child = element.children[0];
|
|
70
|
+
|
|
71
|
+
if (!child || child.kind !== NodeKind.Text) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return child.text;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
beforeEach(async () => {
|
|
79
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
80
|
+
globals.IS_REACT_ACT_ENVIRONMENT = true;
|
|
81
|
+
globals.exact = {
|
|
82
|
+
dispatch: vi.fn(),
|
|
83
|
+
screenWidth: 393,
|
|
84
|
+
screenHeight: 852,
|
|
85
|
+
screenScale: 2,
|
|
86
|
+
safeAreaInsets: {
|
|
87
|
+
top: 0,
|
|
88
|
+
right: 0,
|
|
89
|
+
bottom: 0,
|
|
90
|
+
left: 0,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
_resetNodeIdCounter();
|
|
95
|
+
_clearHandlers();
|
|
96
|
+
_resetHostOpsState();
|
|
97
|
+
_resetInspectorState();
|
|
98
|
+
_resetWindowRuntimeForTests();
|
|
99
|
+
await act(async () => {
|
|
100
|
+
resetReact();
|
|
101
|
+
});
|
|
102
|
+
resetSolid();
|
|
103
|
+
resetVue();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
afterEach(async () => {
|
|
107
|
+
await act(async () => {
|
|
108
|
+
resetReact();
|
|
109
|
+
});
|
|
110
|
+
resetSolid();
|
|
111
|
+
resetVue();
|
|
112
|
+
_clearHandlers();
|
|
113
|
+
_resetNodeIdCounter();
|
|
114
|
+
_resetHostOpsState();
|
|
115
|
+
_resetInspectorState();
|
|
116
|
+
_resetWindowRuntimeForTests();
|
|
117
|
+
|
|
118
|
+
const globals = globalThis as typeof globalThis & ExactGlobals;
|
|
119
|
+
delete globals.exact;
|
|
120
|
+
delete globals.IS_REACT_ACT_ENVIRONMENT;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('adapter window-state integration', () => {
|
|
124
|
+
it('React re-resolves safe-area styles when the host insets change', async () => {
|
|
125
|
+
await act(async () => {
|
|
126
|
+
renderReact(<View safeArea="container" />);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
expect(getFirstElement().safeAreaState?.appliedInsets.top).toBe(0);
|
|
130
|
+
|
|
131
|
+
await act(async () => {
|
|
132
|
+
globalThis.__exactSafeAreaInsetsChanged?.(12, 0, 18, 0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const child = getFirstElement();
|
|
136
|
+
expect(child.safeAreaState?.appliedInsets.top).toBe(12);
|
|
137
|
+
expect(child.safeAreaState?.appliedInsets.bottom).toBe(18);
|
|
138
|
+
expect(child.style.paddingTop).toEqual({ type: 'points', value: 12 });
|
|
139
|
+
expect(child.style.paddingBottom).toEqual({ type: 'points', value: 18 });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('Solid re-resolves safe-area styles when the host insets change', () => {
|
|
143
|
+
renderSolid(() => {
|
|
144
|
+
const node = createSolidElement('View');
|
|
145
|
+
setSolidProp(node, 'safeArea', 'container');
|
|
146
|
+
return node;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(getFirstElement().safeAreaState?.appliedInsets.top).toBe(0);
|
|
150
|
+
|
|
151
|
+
globalThis.__exactSafeAreaInsetsChanged?.(14, 0, 22, 0);
|
|
152
|
+
|
|
153
|
+
const child = getFirstElement();
|
|
154
|
+
expect(child.safeAreaState?.appliedInsets.top).toBe(14);
|
|
155
|
+
expect(child.safeAreaState?.appliedInsets.bottom).toBe(22);
|
|
156
|
+
expect(child.style.paddingTop).toEqual({ type: 'points', value: 14 });
|
|
157
|
+
expect(child.style.paddingBottom).toEqual({ type: 'points', value: 22 });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('Vue re-resolves safe-area styles when the host insets change', () => {
|
|
161
|
+
renderVue(h('View', { safeArea: 'container' }));
|
|
162
|
+
|
|
163
|
+
expect(getFirstElement().safeAreaState?.appliedInsets.top).toBe(0);
|
|
164
|
+
|
|
165
|
+
globalThis.__exactSafeAreaInsetsChanged?.(10, 0, 24, 0);
|
|
166
|
+
|
|
167
|
+
const child = getFirstElement();
|
|
168
|
+
expect(child.safeAreaState?.appliedInsets.top).toBe(10);
|
|
169
|
+
expect(child.safeAreaState?.appliedInsets.bottom).toBe(24);
|
|
170
|
+
expect(child.style.paddingTop).toEqual({ type: 'points', value: 10 });
|
|
171
|
+
expect(child.style.paddingBottom).toEqual({ type: 'points', value: 24 });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('Vue createApp keeps component reactivity without a manual raw effect wrapper', async () => {
|
|
175
|
+
const count = ref(0);
|
|
176
|
+
const app = createVueApp({
|
|
177
|
+
setup() {
|
|
178
|
+
return () => h('Text', {}, String(count.value));
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
app.mount();
|
|
183
|
+
expect(getFirstTextValue()).toBe('0');
|
|
184
|
+
|
|
185
|
+
count.value = 7;
|
|
186
|
+
await nextTick();
|
|
187
|
+
|
|
188
|
+
expect(getFirstTextValue()).toBe('7');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/// <reference path="../../../../js/src/types/bun-test.d.ts" />
|
|
2
|
+
// LLP 0201 W0 / LLP 0202 H0: shared semantic DOM attr lowering.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, it } from 'bun:test';
|
|
5
|
+
|
|
6
|
+
import { filterDOMProps, lowerHostAttrs } from '../attrs.js';
|
|
7
|
+
|
|
8
|
+
describe('lowerHostAttrs (Contract web W0)', () => {
|
|
9
|
+
it('lowers authored heading intent to native heading tags without redundant ARIA', () => {
|
|
10
|
+
const lowered = lowerHostAttrs({
|
|
11
|
+
tag: 'Text',
|
|
12
|
+
props: {
|
|
13
|
+
accessibilityRole: 'heading',
|
|
14
|
+
accessibilityHeadingLevel: 2,
|
|
15
|
+
testID: 'title',
|
|
16
|
+
},
|
|
17
|
+
}, undefined, { defaultTag: 'span' });
|
|
18
|
+
|
|
19
|
+
expect(lowered.tag).toBe('h2');
|
|
20
|
+
expect(lowered.attrs).toMatchObject({ 'data-testid': 'title' });
|
|
21
|
+
expect(lowered.attrs.role).toBeUndefined();
|
|
22
|
+
expect(lowered.attrs['aria-level']).toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('lowers landmarks and list semantics to real DOM tags', () => {
|
|
26
|
+
const nav = lowerHostAttrs({
|
|
27
|
+
tag: 'View',
|
|
28
|
+
props: { accessibilityRole: 'navigation', accessibilityLabel: 'Primary navigation' },
|
|
29
|
+
}, undefined, { defaultTag: 'div' });
|
|
30
|
+
const main = lowerHostAttrs({ tag: 'View', props: { accessibilityRole: 'main' } }, undefined, {
|
|
31
|
+
defaultTag: 'div',
|
|
32
|
+
});
|
|
33
|
+
const list = lowerHostAttrs({ tag: 'List', props: { accessibilityRole: 'list' } }, undefined, {
|
|
34
|
+
defaultTag: 'div',
|
|
35
|
+
});
|
|
36
|
+
const item = lowerHostAttrs({ tag: 'View', props: { accessibilityRole: 'listitem' } }, undefined, {
|
|
37
|
+
defaultTag: 'div',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(nav.tag).toBe('nav');
|
|
41
|
+
expect(nav.attrs).toMatchObject({ 'aria-label': 'Primary navigation' });
|
|
42
|
+
expect(nav.attrs.role).toBeUndefined();
|
|
43
|
+
expect(main.tag).toBe('main');
|
|
44
|
+
expect(list.tag).toBe('ul');
|
|
45
|
+
expect(item.tag).toBe('li');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('lowers editorial semantics to native DOM tags', () => {
|
|
49
|
+
const quote = lowerHostAttrs({
|
|
50
|
+
tag: 'View',
|
|
51
|
+
props: { __exactSemanticTag: 'blockquote', testID: 'quote' },
|
|
52
|
+
}, { tag: 'blockquote' }, { defaultTag: 'div' });
|
|
53
|
+
const pre = lowerHostAttrs({
|
|
54
|
+
tag: 'View',
|
|
55
|
+
props: { __exactSemanticTag: 'pre', testID: 'pre' },
|
|
56
|
+
}, { tag: 'pre' }, { defaultTag: 'div' });
|
|
57
|
+
const code = lowerHostAttrs({
|
|
58
|
+
tag: 'Text',
|
|
59
|
+
props: { __exactSemanticTag: 'code', testID: 'code' },
|
|
60
|
+
}, { tag: 'code' }, { defaultTag: 'span' });
|
|
61
|
+
const rule = lowerHostAttrs({
|
|
62
|
+
tag: 'View',
|
|
63
|
+
props: { __exactSemanticTag: 'hr', testID: 'rule' },
|
|
64
|
+
}, { tag: 'hr' }, { defaultTag: 'div' });
|
|
65
|
+
|
|
66
|
+
expect(quote.tag).toBe('blockquote');
|
|
67
|
+
expect(pre.tag).toBe('pre');
|
|
68
|
+
expect(code.tag).toBe('code');
|
|
69
|
+
expect(rule.tag).toBe('hr');
|
|
70
|
+
expect(quote.attrs).toMatchObject({ 'data-testid': 'quote' });
|
|
71
|
+
expect(code.attrs.role).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('lowers Contract link semantics to real anchors', () => {
|
|
75
|
+
const link = lowerHostAttrs({
|
|
76
|
+
tag: 'Pressable',
|
|
77
|
+
props: {
|
|
78
|
+
__exactSemanticTag: 'a',
|
|
79
|
+
accessibilityRole: 'link',
|
|
80
|
+
accessibilityLabel: 'Facet Contract Lab',
|
|
81
|
+
href: '/facet-contract-lab',
|
|
82
|
+
testID: 'route-facet-contract-link',
|
|
83
|
+
},
|
|
84
|
+
}, { tag: 'a' }, { defaultTag: 'button' });
|
|
85
|
+
|
|
86
|
+
expect(link.tag).toBe('a');
|
|
87
|
+
expect(link.attrs).toMatchObject({
|
|
88
|
+
'aria-label': 'Facet Contract Lab',
|
|
89
|
+
'data-testid': 'route-facet-contract-link',
|
|
90
|
+
href: '/facet-contract-lab',
|
|
91
|
+
});
|
|
92
|
+
expect(link.attrs.role).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('preserves label associations, inert semantics, and DOM prop filtering', () => {
|
|
96
|
+
const label = lowerHostAttrs({
|
|
97
|
+
tag: 'Text',
|
|
98
|
+
props: { accessibilityFor: 'email', nativeID: 'email-label' },
|
|
99
|
+
}, { tag: 'label' }, { defaultTag: 'span' });
|
|
100
|
+
const input = lowerHostAttrs({
|
|
101
|
+
tag: 'TextInput',
|
|
102
|
+
props: { nativeID: 'email', accessibilityLabelledBy: 'email-label' },
|
|
103
|
+
}, undefined, { defaultTag: 'input' });
|
|
104
|
+
const inert = lowerHostAttrs({
|
|
105
|
+
tag: 'View',
|
|
106
|
+
props: { inert: true, accessibilityRole: 'region', className: 'panel' },
|
|
107
|
+
}, undefined, { defaultTag: 'section' });
|
|
108
|
+
const filtered = filterDOMProps({
|
|
109
|
+
accessibilityRole: 'navigation',
|
|
110
|
+
accessibilityLabel: 'Primary navigation',
|
|
111
|
+
onPress: () => undefined,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(label.tag).toBe('label');
|
|
115
|
+
expect(label.attrs).toMatchObject({ id: 'email-label', htmlFor: 'email' });
|
|
116
|
+
expect(label.labelAssociation).toEqual({ for: 'email' });
|
|
117
|
+
expect(input.tag).toBe('input');
|
|
118
|
+
expect(input.attrs).toMatchObject({ id: 'email', 'aria-labelledby': 'email-label' });
|
|
119
|
+
expect(input.labelAssociation).toEqual({ labelledBy: 'email-label' });
|
|
120
|
+
expect(inert.attrs).toMatchObject({
|
|
121
|
+
inert: true,
|
|
122
|
+
'aria-hidden': true,
|
|
123
|
+
className: 'panel',
|
|
124
|
+
});
|
|
125
|
+
expect(filtered).toMatchObject({
|
|
126
|
+
role: 'navigation',
|
|
127
|
+
'aria-label': 'Primary navigation',
|
|
128
|
+
});
|
|
129
|
+
expect(filtered.onPress).toBeUndefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('blocks unsafe DOM attribute passthroughs while preserving function handlers for React', () => {
|
|
133
|
+
const lowered = lowerHostAttrs({
|
|
134
|
+
tag: 'Pressable',
|
|
135
|
+
props: {
|
|
136
|
+
__exactSemanticTag: 'a',
|
|
137
|
+
href: 'javascript:alert(1)',
|
|
138
|
+
srcdoc: '<script>alert(1)</script>',
|
|
139
|
+
onmouseover: 'alert(1)',
|
|
140
|
+
onClick: () => undefined,
|
|
141
|
+
'data-safe': 'ok',
|
|
142
|
+
'aria-current': 'page',
|
|
143
|
+
rel: 'noreferrer',
|
|
144
|
+
},
|
|
145
|
+
}, { tag: 'a' }, { defaultTag: 'button' });
|
|
146
|
+
|
|
147
|
+
expect(lowered.attrs.href).toBeUndefined();
|
|
148
|
+
expect(lowered.attrs.srcdoc).toBeUndefined();
|
|
149
|
+
expect(lowered.attrs.onmouseover).toBeUndefined();
|
|
150
|
+
expect(typeof lowered.attrs.onClick).toBe('function');
|
|
151
|
+
expect(lowered.attrs).toMatchObject({
|
|
152
|
+
'data-safe': 'ok',
|
|
153
|
+
'aria-current': 'page',
|
|
154
|
+
rel: 'noreferrer',
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
});
|