@swiftbrowser/web 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/README.md +600 -0
- package/app/index.html +125 -0
- package/index.html +56 -0
- package/node/dist/node/apps.d.ts +58 -0
- package/node/dist/node/apps.d.ts.map +1 -0
- package/node/dist/node/apps.js +60 -0
- package/node/dist/node/apps.js.map +1 -0
- package/node/dist/node/config.d.ts +42 -0
- package/node/dist/node/config.d.ts.map +1 -0
- package/node/dist/node/config.js +99 -0
- package/node/dist/node/config.js.map +1 -0
- package/node/dist/node/examples.d.ts +39 -0
- package/node/dist/node/examples.d.ts.map +1 -0
- package/node/dist/node/examples.js +116 -0
- package/node/dist/node/examples.js.map +1 -0
- package/node/dist/node/index.d.ts +47 -0
- package/node/dist/node/index.d.ts.map +1 -0
- package/node/dist/node/index.js +52 -0
- package/node/dist/node/index.js.map +1 -0
- package/node/dist/plugins/app-manifests.d.ts +61 -0
- package/node/dist/plugins/app-manifests.d.ts.map +1 -0
- package/node/dist/plugins/app-manifests.js +168 -0
- package/node/dist/plugins/app-manifests.js.map +1 -0
- package/node/dist/plugins/asset-catalogs.d.ts +119 -0
- package/node/dist/plugins/asset-catalogs.d.ts.map +1 -0
- package/node/dist/plugins/asset-catalogs.js +488 -0
- package/node/dist/plugins/asset-catalogs.js.map +1 -0
- package/node/dist/plugins/bundle-resources.d.ts +49 -0
- package/node/dist/plugins/bundle-resources.d.ts.map +1 -0
- package/node/dist/plugins/bundle-resources.js +172 -0
- package/node/dist/plugins/bundle-resources.js.map +1 -0
- package/node/dist/plugins/swift-wasm.d.ts +33 -0
- package/node/dist/plugins/swift-wasm.d.ts.map +1 -0
- package/node/dist/plugins/swift-wasm.js +241 -0
- package/node/dist/plugins/swift-wasm.js.map +1 -0
- package/node/dist/src/assets.d.ts +60 -0
- package/node/dist/src/assets.d.ts.map +1 -0
- package/node/dist/src/assets.js +54 -0
- package/node/dist/src/assets.js.map +1 -0
- package/node/dist/src/devEvents.d.ts +31 -0
- package/node/dist/src/devEvents.d.ts.map +1 -0
- package/node/dist/src/devEvents.js +9 -0
- package/node/dist/src/devEvents.js.map +1 -0
- package/package.json +56 -0
- package/plugins/app-manifests.ts +192 -0
- package/plugins/asset-catalogs.ts +558 -0
- package/plugins/bundle-resources.ts +195 -0
- package/plugins/swift-wasm.ts +259 -0
- package/public/_headers +23 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/icon.svg +14 -0
- package/public/manifest.webmanifest +14 -0
- package/src/animation.ts +521 -0
- package/src/assets.ts +86 -0
- package/src/chartDom.ts +481 -0
- package/src/devEvents.ts +35 -0
- package/src/deviceMode.ts +80 -0
- package/src/ghost.ts +25 -0
- package/src/interaction.ts +1113 -0
- package/src/landing.css +261 -0
- package/src/landing.ts +141 -0
- package/src/layout/chart.ts +477 -0
- package/src/layout/controls.ts +116 -0
- package/src/layout/engine.ts +3581 -0
- package/src/layout/index.ts +91 -0
- package/src/layout/measure.ts +86 -0
- package/src/layout/text.ts +124 -0
- package/src/layout/types.ts +153 -0
- package/src/layout/typography.ts +188 -0
- package/src/layoutDom.ts +549 -0
- package/src/layoutEvents.ts +110 -0
- package/src/locale.ts +64 -0
- package/src/main.ts +554 -0
- package/src/mock.ts +752 -0
- package/src/nodeQueries.ts +60 -0
- package/src/protocol.ts +1421 -0
- package/src/renderer.ts +2882 -0
- package/src/styles.css +2834 -0
- package/src/styles.ts +373 -0
- package/src/swipe.ts +110 -0
- package/src/symbols.ts +595 -0
- package/src/typography.ts +76 -0
- package/src/vite-env.d.ts +26 -0
- package/src/wasm.ts +400 -0
package/src/mock.ts
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock apps used by `?mock=1`: screens expressed as ops, with a tiny in-page
|
|
3
|
+
* "Swift core" that answers events with ops. Lets the renderer be exercised
|
|
4
|
+
* end to end without a Wasm build.
|
|
5
|
+
*
|
|
6
|
+
* - `?mock=1` (or `&app=Counter`): the Phase 1 Counter screen.
|
|
7
|
+
* - `?mock=1&app=Todos`: a Phase 2 navigation stack with a list, toggle,
|
|
8
|
+
* navigation link, text field and bordered button.
|
|
9
|
+
* - `?mock=1&app=Gallery`: Phase 3 text styles (Dynamic Type), `color` and
|
|
10
|
+
* `shape` views, an animated subtree with a transition, and a `sheet`.
|
|
11
|
+
* - `?mock=1&app=Settings`: Phase 4 `tabview` with a `geometry` reader in a
|
|
12
|
+
* navigation stack and a `grouped` list of `picker`s.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ElementId, Op, RendererEvent, TextStyle } from './protocol';
|
|
16
|
+
import type { AppHandle, OpsSink } from './wasm';
|
|
17
|
+
|
|
18
|
+
export type MockAppName = 'Counter' | 'Todos' | 'Gallery' | 'Settings';
|
|
19
|
+
|
|
20
|
+
export interface MockAppHandle extends AppHandle {
|
|
21
|
+
/** Every event the renderer delivered, in order (for tests). */
|
|
22
|
+
readonly received: RendererEvent[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Handle fields every mock shares: no Wasm instance, JSON events, no executor. */
|
|
26
|
+
const MOCK_BASE = {
|
|
27
|
+
exitCode: 0,
|
|
28
|
+
instance: undefined as unknown as WebAssembly.Instance,
|
|
29
|
+
supportsJsonEvents: true,
|
|
30
|
+
supportsJobs: false,
|
|
31
|
+
runJobs: (): number | null => null,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
export const MOCK_IDS = {
|
|
35
|
+
root: 0,
|
|
36
|
+
vstack: 1,
|
|
37
|
+
titleStyle: 2,
|
|
38
|
+
title: 3,
|
|
39
|
+
countStyle: 4,
|
|
40
|
+
count: 5,
|
|
41
|
+
controls: 6,
|
|
42
|
+
minusPad: 7,
|
|
43
|
+
minusButton: 8,
|
|
44
|
+
minusLabel: 9,
|
|
45
|
+
plusPad: 10,
|
|
46
|
+
plusButton: 11,
|
|
47
|
+
plusLabel: 12,
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
/** First render pass of the Counter app, as the Swift core would emit it. */
|
|
51
|
+
export function counterFixture(count = 0): Op[] {
|
|
52
|
+
const ids = MOCK_IDS;
|
|
53
|
+
return [
|
|
54
|
+
{ op: 'create', id: ids.vstack, kind: 'vstack', props: { spacing: 16, alignment: 'center' } },
|
|
55
|
+
{
|
|
56
|
+
op: 'create',
|
|
57
|
+
id: ids.titleStyle,
|
|
58
|
+
kind: 'styled',
|
|
59
|
+
props: { style: { font: { size: 34, weight: 'bold', design: 'default' } } },
|
|
60
|
+
},
|
|
61
|
+
{ op: 'create', id: ids.title, kind: 'text', props: { text: 'Counter' } },
|
|
62
|
+
{
|
|
63
|
+
op: 'create',
|
|
64
|
+
id: ids.countStyle,
|
|
65
|
+
kind: 'styled',
|
|
66
|
+
props: {
|
|
67
|
+
style: {
|
|
68
|
+
font: { size: 48, weight: 'semibold', design: 'monospaced' },
|
|
69
|
+
foreground: { name: 'primary' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{ op: 'create', id: ids.count, kind: 'text', props: { text: String(count) } },
|
|
74
|
+
{ op: 'create', id: ids.controls, kind: 'hstack', props: { spacing: 24, alignment: 'center' } },
|
|
75
|
+
{
|
|
76
|
+
op: 'create',
|
|
77
|
+
id: ids.minusPad,
|
|
78
|
+
kind: 'styled',
|
|
79
|
+
props: { style: { padding: { top: 8, leading: 16, bottom: 8, trailing: 16 } } },
|
|
80
|
+
},
|
|
81
|
+
{ op: 'create', id: ids.minusButton, kind: 'button', props: {} },
|
|
82
|
+
{ op: 'create', id: ids.minusLabel, kind: 'text', props: { text: '−' } },
|
|
83
|
+
{
|
|
84
|
+
op: 'create',
|
|
85
|
+
id: ids.plusPad,
|
|
86
|
+
kind: 'styled',
|
|
87
|
+
props: { style: { padding: { top: 8, leading: 16, bottom: 8, trailing: 16 } } },
|
|
88
|
+
},
|
|
89
|
+
{ op: 'create', id: ids.plusButton, kind: 'button', props: {} },
|
|
90
|
+
{ op: 'create', id: ids.plusLabel, kind: 'text', props: { text: '+' } },
|
|
91
|
+
|
|
92
|
+
{ op: 'insert', parent: ids.root, id: ids.vstack, index: 0 },
|
|
93
|
+
{ op: 'insert', parent: ids.vstack, id: ids.titleStyle, index: 0 },
|
|
94
|
+
{ op: 'insert', parent: ids.titleStyle, id: ids.title, index: 0 },
|
|
95
|
+
{ op: 'insert', parent: ids.vstack, id: ids.countStyle, index: 1 },
|
|
96
|
+
{ op: 'insert', parent: ids.countStyle, id: ids.count, index: 0 },
|
|
97
|
+
{ op: 'insert', parent: ids.vstack, id: ids.controls, index: 2 },
|
|
98
|
+
{ op: 'insert', parent: ids.controls, id: ids.minusPad, index: 0 },
|
|
99
|
+
{ op: 'insert', parent: ids.minusPad, id: ids.minusButton, index: 0 },
|
|
100
|
+
{ op: 'insert', parent: ids.minusButton, id: ids.minusLabel, index: 0 },
|
|
101
|
+
{ op: 'insert', parent: ids.controls, id: ids.plusPad, index: 1 },
|
|
102
|
+
{ op: 'insert', parent: ids.plusPad, id: ids.plusButton, index: 0 },
|
|
103
|
+
{ op: 'insert', parent: ids.plusButton, id: ids.plusLabel, index: 0 },
|
|
104
|
+
{ op: 'commit' },
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Emits the first render pass and answers taps on the two buttons. */
|
|
109
|
+
export function createCounterMock(onOps: OpsSink): MockAppHandle {
|
|
110
|
+
let count = 0;
|
|
111
|
+
const received: RendererEvent[] = [];
|
|
112
|
+
onOps(counterFixture(count));
|
|
113
|
+
const sendEvent = (id: ElementId): void => {
|
|
114
|
+
received.push({ type: 'tap', id });
|
|
115
|
+
if (id === MOCK_IDS.plusButton) count += 1;
|
|
116
|
+
else if (id === MOCK_IDS.minusButton) count -= 1;
|
|
117
|
+
else return;
|
|
118
|
+
onOps([{ op: 'update', id: MOCK_IDS.count, props: { text: String(count) } }, { op: 'commit' }]);
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
...MOCK_BASE,
|
|
122
|
+
received,
|
|
123
|
+
sendEvent,
|
|
124
|
+
sendEventJson(event) {
|
|
125
|
+
if (event.type === 'tap') sendEvent(event.id);
|
|
126
|
+
else received.push(event);
|
|
127
|
+
return true;
|
|
128
|
+
},
|
|
129
|
+
snapshotState: () => JSON.stringify({ 'ContentView#0': count }),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Todos (Phase 2)
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
export const TODOS_IDS = {
|
|
138
|
+
root: 0,
|
|
139
|
+
navstack: 1,
|
|
140
|
+
screen: 2,
|
|
141
|
+
list: 3,
|
|
142
|
+
todaySection: 4,
|
|
143
|
+
toggle: 5,
|
|
144
|
+
toggleLabel: 6,
|
|
145
|
+
navlink: 7,
|
|
146
|
+
navlinkLabel: 8,
|
|
147
|
+
doneRow: 9,
|
|
148
|
+
doneIconStyle: 10,
|
|
149
|
+
doneIcon: 11,
|
|
150
|
+
doneText: 12,
|
|
151
|
+
addSection: 13,
|
|
152
|
+
addRow: 14,
|
|
153
|
+
field: 15,
|
|
154
|
+
addDisabled: 16,
|
|
155
|
+
addButton: 17,
|
|
156
|
+
addLabel: 18,
|
|
157
|
+
// Pushed on navlink tap.
|
|
158
|
+
detailScreen: 20,
|
|
159
|
+
detailStack: 21,
|
|
160
|
+
detailText: 22,
|
|
161
|
+
} as const;
|
|
162
|
+
|
|
163
|
+
/** First render pass of the Todos mock: navstack > navscreen > list. */
|
|
164
|
+
export function todosFixture(): Op[] {
|
|
165
|
+
const ids = TODOS_IDS;
|
|
166
|
+
return [
|
|
167
|
+
{ op: 'create', id: ids.navstack, kind: 'navstack', props: {} },
|
|
168
|
+
{ op: 'insert', parent: ids.root, id: ids.navstack, index: 0 },
|
|
169
|
+
{
|
|
170
|
+
op: 'create',
|
|
171
|
+
id: ids.screen,
|
|
172
|
+
kind: 'navscreen',
|
|
173
|
+
props: { title: 'Todos', displayMode: 'large', depth: 0 },
|
|
174
|
+
},
|
|
175
|
+
{ op: 'insert', parent: ids.navstack, id: ids.screen, index: 0 },
|
|
176
|
+
{ op: 'create', id: ids.list, kind: 'list', props: {} },
|
|
177
|
+
{ op: 'insert', parent: ids.screen, id: ids.list, index: 0 },
|
|
178
|
+
|
|
179
|
+
{ op: 'create', id: ids.todaySection, kind: 'section', props: { header: 'Today', footer: null } },
|
|
180
|
+
{ op: 'insert', parent: ids.list, id: ids.todaySection, index: 0 },
|
|
181
|
+
{ op: 'create', id: ids.toggle, kind: 'toggle', props: { isOn: false } },
|
|
182
|
+
{ op: 'insert', parent: ids.todaySection, id: ids.toggle, index: 0 },
|
|
183
|
+
{ op: 'create', id: ids.toggleLabel, kind: 'text', props: { text: 'Buy milk' } },
|
|
184
|
+
{ op: 'insert', parent: ids.toggle, id: ids.toggleLabel, index: 0 },
|
|
185
|
+
{ op: 'create', id: ids.navlink, kind: 'navlink', props: {} },
|
|
186
|
+
{ op: 'insert', parent: ids.todaySection, id: ids.navlink, index: 1 },
|
|
187
|
+
{ op: 'create', id: ids.navlinkLabel, kind: 'text', props: { text: 'Write the layout engine' } },
|
|
188
|
+
{ op: 'insert', parent: ids.navlink, id: ids.navlinkLabel, index: 0 },
|
|
189
|
+
{ op: 'create', id: ids.doneRow, kind: 'hstack', props: { spacing: 12, alignment: 'center' } },
|
|
190
|
+
{ op: 'insert', parent: ids.todaySection, id: ids.doneRow, index: 2 },
|
|
191
|
+
{
|
|
192
|
+
op: 'create',
|
|
193
|
+
id: ids.doneIconStyle,
|
|
194
|
+
kind: 'styled',
|
|
195
|
+
props: { style: { foreground: { r: 0.204, g: 0.78, b: 0.349, a: 1 } } },
|
|
196
|
+
},
|
|
197
|
+
{ op: 'insert', parent: ids.doneRow, id: ids.doneIconStyle, index: 0 },
|
|
198
|
+
{ op: 'create', id: ids.doneIcon, kind: 'image', props: { systemName: 'checkmark.circle.fill' } },
|
|
199
|
+
{ op: 'insert', parent: ids.doneIconStyle, id: ids.doneIcon, index: 0 },
|
|
200
|
+
{ op: 'create', id: ids.doneText, kind: 'text', props: { text: 'Ship Phase 2' } },
|
|
201
|
+
{ op: 'insert', parent: ids.doneRow, id: ids.doneText, index: 1 },
|
|
202
|
+
|
|
203
|
+
{ op: 'create', id: ids.addSection, kind: 'section', props: { header: null, footer: 'Press Add to save.' } },
|
|
204
|
+
{ op: 'insert', parent: ids.list, id: ids.addSection, index: 1 },
|
|
205
|
+
{ op: 'create', id: ids.addRow, kind: 'hstack', props: { spacing: null, alignment: 'center' } },
|
|
206
|
+
{ op: 'insert', parent: ids.addSection, id: ids.addRow, index: 0 },
|
|
207
|
+
{
|
|
208
|
+
op: 'create',
|
|
209
|
+
id: ids.field,
|
|
210
|
+
kind: 'textfield',
|
|
211
|
+
props: { text: '', placeholder: 'New todo', style: 'roundedBorder', secure: false },
|
|
212
|
+
},
|
|
213
|
+
{ op: 'insert', parent: ids.addRow, id: ids.field, index: 0 },
|
|
214
|
+
{ op: 'create', id: ids.addDisabled, kind: 'styled', props: { style: { disabled: true } } },
|
|
215
|
+
{ op: 'insert', parent: ids.addRow, id: ids.addDisabled, index: 1 },
|
|
216
|
+
{ op: 'create', id: ids.addButton, kind: 'button', props: { style: 'bordered', role: null } },
|
|
217
|
+
{ op: 'insert', parent: ids.addDisabled, id: ids.addButton, index: 0 },
|
|
218
|
+
{ op: 'create', id: ids.addLabel, kind: 'text', props: { text: 'Add' } },
|
|
219
|
+
{ op: 'insert', parent: ids.addButton, id: ids.addLabel, index: 0 },
|
|
220
|
+
{ op: 'commit' },
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Ops that push the detail screen (navlink tap). */
|
|
225
|
+
export function todosDetailFixture(): Op[] {
|
|
226
|
+
const ids = TODOS_IDS;
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
op: 'create',
|
|
230
|
+
id: ids.detailScreen,
|
|
231
|
+
kind: 'navscreen',
|
|
232
|
+
props: { title: 'Detail', displayMode: 'inline', depth: 1 },
|
|
233
|
+
},
|
|
234
|
+
{ op: 'insert', parent: ids.navstack, id: ids.detailScreen, index: 1 },
|
|
235
|
+
{ op: 'create', id: ids.detailStack, kind: 'vstack', props: { spacing: 12, alignment: 'center' } },
|
|
236
|
+
{ op: 'insert', parent: ids.detailScreen, id: ids.detailStack, index: 0 },
|
|
237
|
+
{ op: 'create', id: ids.detailText, kind: 'text', props: { text: 'Write the layout engine' } },
|
|
238
|
+
{ op: 'insert', parent: ids.detailStack, id: ids.detailText, index: 0 },
|
|
239
|
+
{ op: 'commit' },
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Todos mock: toggling flips `isOn`, the navlink pushes a detail screen, a
|
|
245
|
+
* tap on the detail screen's own id (the back button) pops it, typing in the
|
|
246
|
+
* field echoes the text back and enables the Add button.
|
|
247
|
+
*/
|
|
248
|
+
export function createTodosMock(onOps: OpsSink): MockAppHandle {
|
|
249
|
+
const ids = TODOS_IDS;
|
|
250
|
+
const received: RendererEvent[] = [];
|
|
251
|
+
let isOn = false;
|
|
252
|
+
let draft = '';
|
|
253
|
+
let detailShown = false;
|
|
254
|
+
onOps(todosFixture());
|
|
255
|
+
|
|
256
|
+
const sendEvent = (id: ElementId): void => {
|
|
257
|
+
received.push({ type: 'tap', id });
|
|
258
|
+
if (id === ids.navlink && !detailShown) {
|
|
259
|
+
detailShown = true;
|
|
260
|
+
onOps(todosDetailFixture());
|
|
261
|
+
} else if (id === ids.detailScreen && detailShown) {
|
|
262
|
+
detailShown = false;
|
|
263
|
+
onOps([{ op: 'remove', id: ids.detailScreen }, { op: 'commit' }]);
|
|
264
|
+
} else if (id === ids.addButton) {
|
|
265
|
+
draft = '';
|
|
266
|
+
onOps([
|
|
267
|
+
{
|
|
268
|
+
op: 'update',
|
|
269
|
+
id: ids.field,
|
|
270
|
+
props: { text: '', placeholder: 'New todo', style: 'roundedBorder', secure: false },
|
|
271
|
+
},
|
|
272
|
+
{ op: 'update', id: ids.addDisabled, props: { style: { disabled: true } } },
|
|
273
|
+
{ op: 'commit' },
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
...MOCK_BASE,
|
|
280
|
+
received,
|
|
281
|
+
sendEvent,
|
|
282
|
+
sendEventJson(event) {
|
|
283
|
+
switch (event.type) {
|
|
284
|
+
case 'tap':
|
|
285
|
+
sendEvent(event.id);
|
|
286
|
+
break;
|
|
287
|
+
case 'toggle':
|
|
288
|
+
received.push(event);
|
|
289
|
+
if (event.id === ids.toggle) {
|
|
290
|
+
isOn = event.value;
|
|
291
|
+
onOps([{ op: 'update', id: ids.toggle, props: { isOn } }, { op: 'commit' }]);
|
|
292
|
+
}
|
|
293
|
+
break;
|
|
294
|
+
case 'text':
|
|
295
|
+
received.push(event);
|
|
296
|
+
if (event.id === ids.field) {
|
|
297
|
+
const wasEmpty = draft.length === 0;
|
|
298
|
+
draft = event.value;
|
|
299
|
+
const ops: Op[] = [
|
|
300
|
+
{
|
|
301
|
+
op: 'update',
|
|
302
|
+
id: ids.field,
|
|
303
|
+
props: { text: draft, placeholder: 'New todo', style: 'roundedBorder', secure: false },
|
|
304
|
+
},
|
|
305
|
+
];
|
|
306
|
+
if (wasEmpty !== (draft.length === 0)) {
|
|
307
|
+
ops.push({
|
|
308
|
+
op: 'update',
|
|
309
|
+
id: ids.addDisabled,
|
|
310
|
+
props: { style: draft.length === 0 ? { disabled: true } : {} },
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
ops.push({ op: 'commit' });
|
|
314
|
+
onOps(ops);
|
|
315
|
+
}
|
|
316
|
+
break;
|
|
317
|
+
default:
|
|
318
|
+
received.push(event);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
},
|
|
323
|
+
snapshotState: () => JSON.stringify({ 'ContentView#0': draft, 'ContentView#1': isOn }),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
// Gallery (Phase 3)
|
|
329
|
+
// ---------------------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
export const GALLERY_IDS = {
|
|
332
|
+
root: 0,
|
|
333
|
+
vstack: 1,
|
|
334
|
+
/** `styled{font: textStyle}` boxes are 10, 12, …; their texts 11, 13, … (see GALLERY_TEXT_STYLES). */
|
|
335
|
+
firstTextStyle: 10,
|
|
336
|
+
shapes: 40,
|
|
337
|
+
colorFrame: 41,
|
|
338
|
+
color: 42,
|
|
339
|
+
circleFrame: 43,
|
|
340
|
+
circle: 44,
|
|
341
|
+
capsuleFrame: 45,
|
|
342
|
+
capsule: 46,
|
|
343
|
+
/** `styled{animation, animationToken}` around the animated subtree. */
|
|
344
|
+
animated: 50,
|
|
345
|
+
animatedStack: 51,
|
|
346
|
+
boxFrame: 52,
|
|
347
|
+
box: 53,
|
|
348
|
+
expandedTransition: 54,
|
|
349
|
+
expandedText: 55,
|
|
350
|
+
controls: 60,
|
|
351
|
+
toggleButton: 61,
|
|
352
|
+
toggleLabel: 62,
|
|
353
|
+
sheetButton: 63,
|
|
354
|
+
sheetLabel: 64,
|
|
355
|
+
sheet: 70,
|
|
356
|
+
sheetStack: 71,
|
|
357
|
+
sheetTitleStyle: 72,
|
|
358
|
+
sheetTitle: 73,
|
|
359
|
+
doneButton: 74,
|
|
360
|
+
doneLabel: 75,
|
|
361
|
+
} as const;
|
|
362
|
+
|
|
363
|
+
/** Text styles shown in the Gallery, top to bottom; the i-th uses ids 10+2i / 11+2i. */
|
|
364
|
+
export const GALLERY_TEXT_STYLES: readonly TextStyle[] = [
|
|
365
|
+
'largeTitle',
|
|
366
|
+
'title',
|
|
367
|
+
'title2',
|
|
368
|
+
'title3',
|
|
369
|
+
'headline',
|
|
370
|
+
'body',
|
|
371
|
+
'callout',
|
|
372
|
+
'subheadline',
|
|
373
|
+
'footnote',
|
|
374
|
+
'caption',
|
|
375
|
+
'caption2',
|
|
376
|
+
];
|
|
377
|
+
|
|
378
|
+
export function galleryTextStyleIds(style: TextStyle): { styled: number; text: number } {
|
|
379
|
+
const i = GALLERY_TEXT_STYLES.indexOf(style);
|
|
380
|
+
return { styled: GALLERY_IDS.firstTextStyle + 2 * i, text: GALLERY_IDS.firstTextStyle + 2 * i + 1 };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const BLUE = { r: 0, g: 0.478, b: 1, a: 1 };
|
|
384
|
+
const RED = { r: 1, g: 0.231, b: 0.188, a: 1 };
|
|
385
|
+
const GREEN = { r: 0.204, g: 0.78, b: 0.349, a: 1 };
|
|
386
|
+
|
|
387
|
+
/** The animated box: `styled{frame}` > `shape`; expanded doubles the width and swaps the fill. */
|
|
388
|
+
function galleryBoxOps(expanded: boolean): Op[] {
|
|
389
|
+
const ids = GALLERY_IDS;
|
|
390
|
+
return [
|
|
391
|
+
{
|
|
392
|
+
op: 'update',
|
|
393
|
+
id: ids.boxFrame,
|
|
394
|
+
props: { style: { frame: { width: expanded ? 160 : 80, height: 40 } } },
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
op: 'update',
|
|
398
|
+
id: ids.box,
|
|
399
|
+
props: { shape: 'roundedRectangle', cornerRadius: 12, fill: expanded ? GREEN : RED, stroke: null },
|
|
400
|
+
},
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** First render pass of the Gallery mock. */
|
|
405
|
+
export function galleryFixture(): Op[] {
|
|
406
|
+
const ids = GALLERY_IDS;
|
|
407
|
+
const ops: Op[] = [
|
|
408
|
+
{ op: 'create', id: ids.vstack, kind: 'vstack', props: { spacing: 6, alignment: 'center' } },
|
|
409
|
+
{ op: 'insert', parent: ids.root, id: ids.vstack, index: 0 },
|
|
410
|
+
];
|
|
411
|
+
GALLERY_TEXT_STYLES.forEach((textStyle, i) => {
|
|
412
|
+
const { styled, text } = galleryTextStyleIds(textStyle);
|
|
413
|
+
ops.push(
|
|
414
|
+
{ op: 'create', id: styled, kind: 'styled', props: { style: { font: { textStyle } } } },
|
|
415
|
+
{ op: 'insert', parent: ids.vstack, id: styled, index: i },
|
|
416
|
+
{ op: 'create', id: text, kind: 'text', props: { text: textStyle } },
|
|
417
|
+
{ op: 'insert', parent: styled, id: text, index: 0 },
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
const after = GALLERY_TEXT_STYLES.length;
|
|
421
|
+
ops.push(
|
|
422
|
+
// Color and shapes in fixed frames.
|
|
423
|
+
{ op: 'create', id: ids.shapes, kind: 'hstack', props: { spacing: 16, alignment: 'center' } },
|
|
424
|
+
{ op: 'insert', parent: ids.vstack, id: ids.shapes, index: after },
|
|
425
|
+
{ op: 'create', id: ids.colorFrame, kind: 'styled', props: { style: { frame: { width: 60, height: 60 } } } },
|
|
426
|
+
{ op: 'insert', parent: ids.shapes, id: ids.colorFrame, index: 0 },
|
|
427
|
+
{ op: 'create', id: ids.color, kind: 'color', props: { color: BLUE } },
|
|
428
|
+
{ op: 'insert', parent: ids.colorFrame, id: ids.color, index: 0 },
|
|
429
|
+
{ op: 'create', id: ids.circleFrame, kind: 'styled', props: { style: { frame: { width: 60, height: 60 } } } },
|
|
430
|
+
{ op: 'insert', parent: ids.shapes, id: ids.circleFrame, index: 1 },
|
|
431
|
+
{ op: 'create', id: ids.circle, kind: 'shape', props: { shape: 'circle', cornerRadius: 0, fill: BLUE, stroke: null } },
|
|
432
|
+
{ op: 'insert', parent: ids.circleFrame, id: ids.circle, index: 0 },
|
|
433
|
+
{ op: 'create', id: ids.capsuleFrame, kind: 'styled', props: { style: { frame: { width: 100, height: 44 } } } },
|
|
434
|
+
{ op: 'insert', parent: ids.shapes, id: ids.capsuleFrame, index: 2 },
|
|
435
|
+
{
|
|
436
|
+
op: 'create',
|
|
437
|
+
id: ids.capsule,
|
|
438
|
+
kind: 'shape',
|
|
439
|
+
props: { shape: 'capsule', cornerRadius: 0, fill: null, stroke: { color: { name: 'accent' }, lineWidth: 2 } },
|
|
440
|
+
},
|
|
441
|
+
{ op: 'insert', parent: ids.capsuleFrame, id: ids.capsule, index: 0 },
|
|
442
|
+
|
|
443
|
+
// Animated subtree: `.animation(.bouncy, value: expanded)`.
|
|
444
|
+
{
|
|
445
|
+
op: 'create',
|
|
446
|
+
id: ids.animated,
|
|
447
|
+
kind: 'styled',
|
|
448
|
+
props: { style: { animation: { type: 'spring', duration: 0.5, bounce: 0.3 }, animationToken: 0 } },
|
|
449
|
+
},
|
|
450
|
+
{ op: 'insert', parent: ids.vstack, id: ids.animated, index: after + 1 },
|
|
451
|
+
{ op: 'create', id: ids.animatedStack, kind: 'vstack', props: { spacing: 8, alignment: 'center' } },
|
|
452
|
+
{ op: 'insert', parent: ids.animated, id: ids.animatedStack, index: 0 },
|
|
453
|
+
{ op: 'create', id: ids.boxFrame, kind: 'styled', props: { style: { frame: { width: 80, height: 40 } } } },
|
|
454
|
+
{ op: 'insert', parent: ids.animatedStack, id: ids.boxFrame, index: 0 },
|
|
455
|
+
{ op: 'create', id: ids.box, kind: 'shape', props: { shape: 'roundedRectangle', cornerRadius: 12, fill: RED, stroke: null } },
|
|
456
|
+
{ op: 'insert', parent: ids.boxFrame, id: ids.box, index: 0 },
|
|
457
|
+
|
|
458
|
+
// Controls.
|
|
459
|
+
{ op: 'create', id: ids.controls, kind: 'hstack', props: { spacing: 16, alignment: 'center' } },
|
|
460
|
+
{ op: 'insert', parent: ids.vstack, id: ids.controls, index: after + 2 },
|
|
461
|
+
{ op: 'create', id: ids.toggleButton, kind: 'button', props: { style: 'bordered', role: null } },
|
|
462
|
+
{ op: 'insert', parent: ids.controls, id: ids.toggleButton, index: 0 },
|
|
463
|
+
{ op: 'create', id: ids.toggleLabel, kind: 'text', props: { text: 'Toggle' } },
|
|
464
|
+
{ op: 'insert', parent: ids.toggleButton, id: ids.toggleLabel, index: 0 },
|
|
465
|
+
{ op: 'create', id: ids.sheetButton, kind: 'button', props: { style: 'borderedProminent', role: null } },
|
|
466
|
+
{ op: 'insert', parent: ids.controls, id: ids.sheetButton, index: 1 },
|
|
467
|
+
{ op: 'create', id: ids.sheetLabel, kind: 'text', props: { text: 'Show sheet' } },
|
|
468
|
+
{ op: 'insert', parent: ids.sheetButton, id: ids.sheetLabel, index: 0 },
|
|
469
|
+
{ op: 'commit' },
|
|
470
|
+
);
|
|
471
|
+
return ops;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Ops that present the sheet (root-level `sheet` after the app's tree). */
|
|
475
|
+
export function gallerySheetFixture(): Op[] {
|
|
476
|
+
const ids = GALLERY_IDS;
|
|
477
|
+
return [
|
|
478
|
+
{ op: 'create', id: ids.sheet, kind: 'sheet', props: { detents: ['large'] } },
|
|
479
|
+
{ op: 'insert', parent: ids.root, id: ids.sheet, index: 1 },
|
|
480
|
+
{ op: 'create', id: ids.sheetStack, kind: 'vstack', props: { spacing: 16, alignment: 'center' } },
|
|
481
|
+
{ op: 'insert', parent: ids.sheet, id: ids.sheetStack, index: 0 },
|
|
482
|
+
{ op: 'create', id: ids.sheetTitleStyle, kind: 'styled', props: { style: { font: { textStyle: 'title2' } } } },
|
|
483
|
+
{ op: 'insert', parent: ids.sheetStack, id: ids.sheetTitleStyle, index: 0 },
|
|
484
|
+
{ op: 'create', id: ids.sheetTitle, kind: 'text', props: { text: 'Sheet' } },
|
|
485
|
+
{ op: 'insert', parent: ids.sheetTitleStyle, id: ids.sheetTitle, index: 0 },
|
|
486
|
+
{ op: 'create', id: ids.doneButton, kind: 'button', props: { style: 'bordered', role: null } },
|
|
487
|
+
{ op: 'insert', parent: ids.sheetStack, id: ids.doneButton, index: 1 },
|
|
488
|
+
{ op: 'create', id: ids.doneLabel, kind: 'text', props: { text: 'Done' } },
|
|
489
|
+
{ op: 'insert', parent: ids.doneButton, id: ids.doneLabel, index: 0 },
|
|
490
|
+
{ op: 'commit' },
|
|
491
|
+
];
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Gallery mock: "Toggle" bumps the `animationToken` of the animated subtree,
|
|
496
|
+
* swaps the box's fill and width and inserts (or removes) an "Expanded" text
|
|
497
|
+
* with an opacity transition, all inside a `withAnimation(.easeInOut)`
|
|
498
|
+
* commit. "Show sheet" presents the sheet; "Done" or a tap on the sheet's
|
|
499
|
+
* own id (scrim / drag) removes it.
|
|
500
|
+
*/
|
|
501
|
+
export function createGalleryMock(onOps: OpsSink): MockAppHandle {
|
|
502
|
+
const ids = GALLERY_IDS;
|
|
503
|
+
const received: RendererEvent[] = [];
|
|
504
|
+
let expanded = false;
|
|
505
|
+
let token = 0;
|
|
506
|
+
let sheetShown = false;
|
|
507
|
+
onOps(galleryFixture());
|
|
508
|
+
|
|
509
|
+
const sendEvent = (id: ElementId): void => {
|
|
510
|
+
received.push({ type: 'tap', id });
|
|
511
|
+
if (id === ids.toggleButton) {
|
|
512
|
+
expanded = !expanded;
|
|
513
|
+
token += 1;
|
|
514
|
+
const ops: Op[] = [
|
|
515
|
+
{
|
|
516
|
+
op: 'update',
|
|
517
|
+
id: ids.animated,
|
|
518
|
+
props: { style: { animation: { type: 'spring', duration: 0.5, bounce: 0.3 }, animationToken: token } },
|
|
519
|
+
},
|
|
520
|
+
...galleryBoxOps(expanded),
|
|
521
|
+
];
|
|
522
|
+
if (expanded) {
|
|
523
|
+
ops.push(
|
|
524
|
+
{ op: 'create', id: ids.expandedTransition, kind: 'styled', props: { style: { transition: { kind: 'opacity' } } } },
|
|
525
|
+
{ op: 'insert', parent: ids.animatedStack, id: ids.expandedTransition, index: 1 },
|
|
526
|
+
{ op: 'create', id: ids.expandedText, kind: 'text', props: { text: 'Expanded' } },
|
|
527
|
+
{ op: 'insert', parent: ids.expandedTransition, id: ids.expandedText, index: 0 },
|
|
528
|
+
);
|
|
529
|
+
} else {
|
|
530
|
+
ops.push({ op: 'remove', id: ids.expandedTransition });
|
|
531
|
+
}
|
|
532
|
+
ops.push({ op: 'commit', animation: { type: 'easeInOut', duration: 0.35 } });
|
|
533
|
+
onOps(ops);
|
|
534
|
+
} else if (id === ids.sheetButton && !sheetShown) {
|
|
535
|
+
sheetShown = true;
|
|
536
|
+
onOps(gallerySheetFixture());
|
|
537
|
+
} else if ((id === ids.doneButton || id === ids.sheet) && sheetShown) {
|
|
538
|
+
sheetShown = false;
|
|
539
|
+
onOps([{ op: 'remove', id: ids.sheet }, { op: 'commit' }]);
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
...MOCK_BASE,
|
|
545
|
+
received,
|
|
546
|
+
sendEvent,
|
|
547
|
+
sendEventJson(event) {
|
|
548
|
+
if (event.type === 'tap') sendEvent(event.id);
|
|
549
|
+
else received.push(event);
|
|
550
|
+
return true;
|
|
551
|
+
},
|
|
552
|
+
snapshotState: () => JSON.stringify({ 'ContentView#0': expanded, 'ContentView#1': sheetShown }),
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Settings (Phase 4)
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
export const SETTINGS_IDS = {
|
|
561
|
+
root: 0,
|
|
562
|
+
tabview: 1,
|
|
563
|
+
homeTab: 2,
|
|
564
|
+
navstack: 3,
|
|
565
|
+
homeScreen: 4,
|
|
566
|
+
homeList: 5,
|
|
567
|
+
geometrySection: 6,
|
|
568
|
+
geometryFrame: 7,
|
|
569
|
+
geometry: 8,
|
|
570
|
+
geometryText: 9,
|
|
571
|
+
rowsSection: 10,
|
|
572
|
+
/** Rows "Item 1" … "Item N" are 11 … 10 + SETTINGS_ROW_COUNT. */
|
|
573
|
+
firstRow: 11,
|
|
574
|
+
settingsTab: 40,
|
|
575
|
+
settingsList: 41,
|
|
576
|
+
appearanceSection: 42,
|
|
577
|
+
appearancePicker: 43,
|
|
578
|
+
unitsSection: 44,
|
|
579
|
+
unitsPicker: 45,
|
|
580
|
+
toggle: 46,
|
|
581
|
+
toggleLabel: 47,
|
|
582
|
+
} as const;
|
|
583
|
+
|
|
584
|
+
/** Enough rows to make the Home list scroll under the tab bar. */
|
|
585
|
+
export const SETTINGS_ROW_COUNT = 20;
|
|
586
|
+
|
|
587
|
+
export const SETTINGS_OPTIONS = {
|
|
588
|
+
appearance: ['Light', 'Dark', 'Auto'],
|
|
589
|
+
units: ['Metric', 'Imperial'],
|
|
590
|
+
} as const;
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* First render pass of the Settings mock: a `tabview` whose Home tab holds a
|
|
594
|
+
* navigation stack with a geometry reader and a long inset-grouped list, and
|
|
595
|
+
* whose Settings tab holds a `grouped` list with a segmented picker, a menu
|
|
596
|
+
* picker and a toggle.
|
|
597
|
+
*/
|
|
598
|
+
export function settingsFixture(): Op[] {
|
|
599
|
+
const ids = SETTINGS_IDS;
|
|
600
|
+
const ops: Op[] = [
|
|
601
|
+
{ op: 'create', id: ids.tabview, kind: 'tabview', props: { selected: 0 } },
|
|
602
|
+
{ op: 'insert', parent: ids.root, id: ids.tabview, index: 0 },
|
|
603
|
+
|
|
604
|
+
// Home tab: navstack > navscreen "Home" > list.
|
|
605
|
+
{ op: 'create', id: ids.homeTab, kind: 'tab', props: { title: 'Home', systemImage: 'house' } },
|
|
606
|
+
{ op: 'insert', parent: ids.tabview, id: ids.homeTab, index: 0 },
|
|
607
|
+
{ op: 'create', id: ids.navstack, kind: 'navstack', props: {} },
|
|
608
|
+
{ op: 'insert', parent: ids.homeTab, id: ids.navstack, index: 0 },
|
|
609
|
+
{ op: 'create', id: ids.homeScreen, kind: 'navscreen', props: { title: 'Home', displayMode: 'large', depth: 0 } },
|
|
610
|
+
{ op: 'insert', parent: ids.navstack, id: ids.homeScreen, index: 0 },
|
|
611
|
+
{ op: 'create', id: ids.homeList, kind: 'list', props: { style: 'insetGrouped' } },
|
|
612
|
+
{ op: 'insert', parent: ids.homeScreen, id: ids.homeList, index: 0 },
|
|
613
|
+
{ op: 'create', id: ids.geometrySection, kind: 'section', props: { header: 'Geometry', footer: null } },
|
|
614
|
+
{ op: 'insert', parent: ids.homeList, id: ids.geometrySection, index: 0 },
|
|
615
|
+
// A GeometryReader in a list row would collapse to 10pt, like SwiftUI; give it a height.
|
|
616
|
+
{ op: 'create', id: ids.geometryFrame, kind: 'styled', props: { style: { frame: { height: 22 } } } },
|
|
617
|
+
{ op: 'insert', parent: ids.geometrySection, id: ids.geometryFrame, index: 0 },
|
|
618
|
+
{ op: 'create', id: ids.geometry, kind: 'geometry', props: {} },
|
|
619
|
+
{ op: 'insert', parent: ids.geometryFrame, id: ids.geometry, index: 0 },
|
|
620
|
+
{ op: 'create', id: ids.geometryText, kind: 'text', props: { text: 'Waiting for geometry…' } },
|
|
621
|
+
{ op: 'insert', parent: ids.geometry, id: ids.geometryText, index: 0 },
|
|
622
|
+
{ op: 'create', id: ids.rowsSection, kind: 'section', props: { header: 'Rows', footer: null } },
|
|
623
|
+
{ op: 'insert', parent: ids.homeList, id: ids.rowsSection, index: 1 },
|
|
624
|
+
];
|
|
625
|
+
for (let i = 0; i < SETTINGS_ROW_COUNT; i++) {
|
|
626
|
+
ops.push(
|
|
627
|
+
{ op: 'create', id: ids.firstRow + i, kind: 'text', props: { text: `Item ${i + 1}` } },
|
|
628
|
+
{ op: 'insert', parent: ids.rowsSection, id: ids.firstRow + i, index: i },
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
ops.push(
|
|
632
|
+
// Settings tab: a grouped list (Form) of pickers and a toggle.
|
|
633
|
+
{ op: 'create', id: ids.settingsTab, kind: 'tab', props: { title: 'Settings', systemImage: 'gear' } },
|
|
634
|
+
{ op: 'insert', parent: ids.tabview, id: ids.settingsTab, index: 1 },
|
|
635
|
+
{ op: 'create', id: ids.settingsList, kind: 'list', props: { style: 'grouped' } },
|
|
636
|
+
{ op: 'insert', parent: ids.settingsTab, id: ids.settingsList, index: 0 },
|
|
637
|
+
{ op: 'create', id: ids.appearanceSection, kind: 'section', props: { header: 'Appearance', footer: null } },
|
|
638
|
+
{ op: 'insert', parent: ids.settingsList, id: ids.appearanceSection, index: 0 },
|
|
639
|
+
{
|
|
640
|
+
op: 'create',
|
|
641
|
+
id: ids.appearancePicker,
|
|
642
|
+
kind: 'picker',
|
|
643
|
+
props: { style: 'segmented', label: 'Appearance', options: [...SETTINGS_OPTIONS.appearance], selected: 0 },
|
|
644
|
+
},
|
|
645
|
+
{ op: 'insert', parent: ids.appearanceSection, id: ids.appearancePicker, index: 0 },
|
|
646
|
+
{ op: 'create', id: ids.unitsSection, kind: 'section', props: { header: 'Units', footer: 'Pickers send a select event with the index.' } },
|
|
647
|
+
{ op: 'insert', parent: ids.settingsList, id: ids.unitsSection, index: 1 },
|
|
648
|
+
{
|
|
649
|
+
op: 'create',
|
|
650
|
+
id: ids.unitsPicker,
|
|
651
|
+
kind: 'picker',
|
|
652
|
+
props: { style: 'menu', label: 'Units', options: [...SETTINGS_OPTIONS.units], selected: 0 },
|
|
653
|
+
},
|
|
654
|
+
{ op: 'insert', parent: ids.unitsSection, id: ids.unitsPicker, index: 0 },
|
|
655
|
+
{ op: 'create', id: ids.toggle, kind: 'toggle', props: { isOn: true } },
|
|
656
|
+
{ op: 'insert', parent: ids.unitsSection, id: ids.toggle, index: 1 },
|
|
657
|
+
{ op: 'create', id: ids.toggleLabel, kind: 'text', props: { text: 'Notifications' } },
|
|
658
|
+
{ op: 'insert', parent: ids.toggle, id: ids.toggleLabel, index: 0 },
|
|
659
|
+
{ op: 'commit' },
|
|
660
|
+
);
|
|
661
|
+
return ops;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Settings mock: `select` on the tab view updates `selected` (the renderer
|
|
666
|
+
* switches tabs only on that update), `select` on a picker updates its
|
|
667
|
+
* `selected`, `toggle` flips the switch and a `geometry` event rewrites the
|
|
668
|
+
* reader's text with the reported size.
|
|
669
|
+
*/
|
|
670
|
+
export function createSettingsMock(onOps: OpsSink): MockAppHandle {
|
|
671
|
+
const ids = SETTINGS_IDS;
|
|
672
|
+
const received: RendererEvent[] = [];
|
|
673
|
+
let tab = 0;
|
|
674
|
+
let appearance = 0;
|
|
675
|
+
let units = 0;
|
|
676
|
+
let isOn = true;
|
|
677
|
+
onOps(settingsFixture());
|
|
678
|
+
|
|
679
|
+
const sendEvent = (id: ElementId): void => {
|
|
680
|
+
received.push({ type: 'tap', id });
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
...MOCK_BASE,
|
|
685
|
+
received,
|
|
686
|
+
sendEvent,
|
|
687
|
+
sendEventJson(event) {
|
|
688
|
+
switch (event.type) {
|
|
689
|
+
case 'tap':
|
|
690
|
+
sendEvent(event.id);
|
|
691
|
+
break;
|
|
692
|
+
case 'select':
|
|
693
|
+
received.push(event);
|
|
694
|
+
if (event.id === ids.tabview) {
|
|
695
|
+
tab = event.value;
|
|
696
|
+
onOps([{ op: 'update', id: ids.tabview, props: { selected: tab } }, { op: 'commit' }]);
|
|
697
|
+
} else if (event.id === ids.appearancePicker) {
|
|
698
|
+
appearance = event.value;
|
|
699
|
+
onOps([
|
|
700
|
+
{
|
|
701
|
+
op: 'update',
|
|
702
|
+
id: ids.appearancePicker,
|
|
703
|
+
props: { style: 'segmented', label: 'Appearance', options: [...SETTINGS_OPTIONS.appearance], selected: appearance },
|
|
704
|
+
},
|
|
705
|
+
{ op: 'commit' },
|
|
706
|
+
]);
|
|
707
|
+
} else if (event.id === ids.unitsPicker) {
|
|
708
|
+
units = event.value;
|
|
709
|
+
onOps([
|
|
710
|
+
{
|
|
711
|
+
op: 'update',
|
|
712
|
+
id: ids.unitsPicker,
|
|
713
|
+
props: { style: 'menu', label: 'Units', options: [...SETTINGS_OPTIONS.units], selected: units },
|
|
714
|
+
},
|
|
715
|
+
{ op: 'commit' },
|
|
716
|
+
]);
|
|
717
|
+
}
|
|
718
|
+
break;
|
|
719
|
+
case 'toggle':
|
|
720
|
+
received.push(event);
|
|
721
|
+
if (event.id === ids.toggle) {
|
|
722
|
+
isOn = event.value;
|
|
723
|
+
onOps([{ op: 'update', id: ids.toggle, props: { isOn } }, { op: 'commit' }]);
|
|
724
|
+
}
|
|
725
|
+
break;
|
|
726
|
+
case 'geometry':
|
|
727
|
+
received.push(event);
|
|
728
|
+
if (event.id === ids.geometry) {
|
|
729
|
+
onOps([
|
|
730
|
+
{ op: 'update', id: ids.geometryText, props: { text: `${event.width} × ${event.height}` } },
|
|
731
|
+
{ op: 'commit' },
|
|
732
|
+
]);
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
default:
|
|
736
|
+
received.push(event);
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
return true;
|
|
740
|
+
},
|
|
741
|
+
snapshotState: () =>
|
|
742
|
+
JSON.stringify({ 'ContentView#0': tab, 'ContentView#1': appearance, 'ContentView#2': units, 'ContentView#3': isOn }),
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/** Pick the mock for `?app=`; unknown names fall back to Counter. */
|
|
747
|
+
export function createMockApp(onOps: OpsSink, name: string = 'Counter'): MockAppHandle {
|
|
748
|
+
if (name === 'Todos') return createTodosMock(onOps);
|
|
749
|
+
if (name === 'Gallery') return createGalleryMock(onOps);
|
|
750
|
+
if (name === 'Settings') return createSettingsMock(onOps);
|
|
751
|
+
return createCounterMock(onOps);
|
|
752
|
+
}
|