@miragon/modeler-shell 0.1.0 → 0.2.1
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/API.md +1051 -84
- package/README.md +162 -31
- package/dist/appearance-BBaM_4bD.d.ts +323 -0
- package/dist/appearance-XX3JC3pI.d.cts +323 -0
- package/dist/{chunk-6LQOFACK.js → chunk-A4YGKH4Y.js} +1252 -31
- package/dist/chunk-A4YGKH4Y.js.map +1 -0
- package/dist/{chunk-RJRFR6N3.js → chunk-KYDG5DMX.js} +2 -2
- package/dist/{chunk-5JSZ4VGA.js → chunk-OADW3GPA.js} +240 -6
- package/dist/chunk-OADW3GPA.js.map +1 -0
- package/dist/core.cjs +260 -6
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +81 -6
- package/dist/core.d.ts +81 -6
- package/dist/core.js +43 -3
- package/dist/diagram-js.cjs +34 -2
- package/dist/diagram-js.cjs.map +1 -1
- package/dist/diagram-js.d.cts +10 -3
- package/dist/diagram-js.d.ts +10 -3
- package/dist/diagram-js.js +14 -2
- package/dist/diagram-js.js.map +1 -1
- package/dist/index.cjs +1658 -196
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -9
- package/dist/index.d.ts +18 -9
- package/dist/index.js +44 -4
- package/dist/shell.css +692 -3
- package/dist/testing-vscode-stub.js +2 -2
- package/dist/testing.cjs +2849 -550
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +75 -9
- package/dist/testing.d.ts +75 -9
- package/dist/testing.js +2744 -558
- package/dist/testing.js.map +1 -1
- package/dist/toasts-Bp0REqdq.d.cts +142 -0
- package/dist/toasts-Bp0REqdq.d.ts +142 -0
- package/dist/{types-OuFlqZp0.d.cts → types-CnRrINRs.d.cts} +170 -9
- package/dist/{types-DnZn9CPw.d.ts → types-PuDZczzy.d.ts} +170 -9
- package/dist/vscode-extension.js +2 -2
- package/dist/vscode-webview.cjs +1546 -178
- package/dist/vscode-webview.cjs.map +1 -1
- package/dist/vscode-webview.d.cts +6 -4
- package/dist/vscode-webview.d.ts +6 -4
- package/dist/vscode-webview.js +11 -2
- package/dist/vscode-webview.js.map +1 -1
- package/package.json +2 -2
- package/src/core/feedback/appearance.ts +141 -0
- package/src/core/feedback/problems.ts +23 -0
- package/src/core/feedback/toasts.ts +118 -0
- package/src/core/fit.ts +18 -3
- package/src/core/index.ts +43 -0
- package/src/core/keymap.ts +20 -0
- package/src/core/messages.ts +71 -0
- package/src/core/types.ts +24 -2
- package/src/core/web-chrome/bottom-row.ts +37 -0
- package/src/core/web-chrome/drop.ts +78 -0
- package/src/core/web-chrome/top-row.ts +66 -0
- package/src/diagram-js/service.ts +4 -1
- package/src/diagram-js/viewport.ts +19 -0
- package/src/dom/buttons.css +124 -0
- package/src/dom/feedback/appearance.ts +184 -0
- package/src/dom/feedback/feedback.css +435 -0
- package/src/dom/feedback/problems-popover.ts +287 -0
- package/src/dom/feedback/toasts.ts +431 -0
- package/src/dom/index.ts +13 -0
- package/src/dom/keys.ts +8 -0
- package/src/dom/menu.ts +28 -17
- package/src/dom/mount.ts +290 -20
- package/src/dom/problems.ts +5 -4
- package/src/dom/shell-dom.css +7 -4
- package/src/dom/tooltip.ts +10 -0
- package/src/dom/types.ts +188 -6
- package/src/dom/web/drop-zone.ts +181 -0
- package/src/dom/web/empty-state.ts +144 -0
- package/src/dom/web/legal.ts +75 -0
- package/src/dom/web/menu-groups.ts +69 -0
- package/src/dom/web/primary.ts +124 -0
- package/src/dom/web/web.css +386 -0
- package/src/shell.css +3 -0
- package/src/testing/case-tools.ts +164 -0
- package/src/testing/cases.ts +209 -144
- package/src/testing/checks.ts +51 -15
- package/src/testing/facts.ts +56 -2
- package/src/testing/geometry.ts +206 -26
- package/src/testing/harness.ts +56 -7
- package/src/testing/index.ts +4 -0
- package/src/testing/page-tools.ts +24 -1
- package/src/testing/page.ts +10 -0
- package/src/testing/web-cases.ts +913 -0
- package/src/testing/web-checks.ts +1049 -0
- package/src/testing/web-facts.ts +713 -0
- package/src/vscode/webview/mount.ts +13 -1
- package/dist/chunk-5JSZ4VGA.js.map +0 -1
- package/dist/chunk-6LQOFACK.js.map +0 -1
- package/dist/keymap-CObQE6q6.d.cts +0 -151
- package/dist/keymap-DORzHnK-.d.ts +0 -151
- package/dist/types-B6NfkiUG.d.cts +0 -66
- package/dist/types-B6NfkiUG.d.ts +0 -66
- /package/dist/{chunk-RJRFR6N3.js.map → chunk-KYDG5DMX.js.map} +0 -0
package/src/testing/cases.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
APPEARANCE_STORAGE_KEY,
|
|
2
3
|
INVALID_STATUS_DELAY_MS,
|
|
4
|
+
SHELL,
|
|
3
5
|
TOOLTIP_DELAY_MS,
|
|
6
|
+
legalLineHeight,
|
|
4
7
|
type Appearance,
|
|
5
|
-
type
|
|
6
|
-
type Size,
|
|
8
|
+
type AppearanceChoice,
|
|
7
9
|
} from "../core/index.js";
|
|
8
10
|
import {
|
|
9
11
|
absentIslandProblems,
|
|
@@ -11,7 +13,6 @@ import {
|
|
|
11
13
|
appearanceProblems,
|
|
12
14
|
ariaProblems,
|
|
13
15
|
createProblems,
|
|
14
|
-
harnessHookProblems,
|
|
15
16
|
isToolEntry,
|
|
16
17
|
keyReachProblem,
|
|
17
18
|
leftoverProblems,
|
|
@@ -44,12 +45,10 @@ import {
|
|
|
44
45
|
paletteFocusProbe,
|
|
45
46
|
paletteShortcutProbe,
|
|
46
47
|
readOnlyStateProbe,
|
|
47
|
-
shapesProbe,
|
|
48
48
|
surroundProbe,
|
|
49
49
|
zoomButtonsProbe,
|
|
50
50
|
type AppearanceFacts,
|
|
51
51
|
type AriaFacts,
|
|
52
|
-
type MenuEntryFacts,
|
|
53
52
|
type MenuFacts,
|
|
54
53
|
type PaletteEntryState,
|
|
55
54
|
type ShapeFacts,
|
|
@@ -64,14 +63,7 @@ import {
|
|
|
64
63
|
type ChromeMeasurement,
|
|
65
64
|
type TooltipRow,
|
|
66
65
|
} from "./geometry.js";
|
|
67
|
-
import type {
|
|
68
|
-
ShellCaseOutcome,
|
|
69
|
-
ShellConformanceCase,
|
|
70
|
-
ShellConformanceHarness,
|
|
71
|
-
ShellSkip,
|
|
72
|
-
ShowHook,
|
|
73
|
-
ShowHooks,
|
|
74
|
-
} from "./harness.js";
|
|
66
|
+
import type { ShellConformanceCase, ShellConformanceHarness, ShellSkip, ToastFixture } from "./harness.js";
|
|
75
67
|
import type { PageLike } from "./page.js";
|
|
76
68
|
import {
|
|
77
69
|
afterPressProbe,
|
|
@@ -87,24 +79,43 @@ import {
|
|
|
87
79
|
leftoverProbe,
|
|
88
80
|
removeInjectedProbe,
|
|
89
81
|
settle,
|
|
82
|
+
targetProbe,
|
|
90
83
|
type ChromeTarget,
|
|
91
84
|
type InjectedKind,
|
|
92
|
-
type TargetFacts,
|
|
93
85
|
} from "./page-tools.js";
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
86
|
+
import {
|
|
87
|
+
appearanceChoiceProblems,
|
|
88
|
+
expectedWebMode,
|
|
89
|
+
lightOnlyMenuProblems,
|
|
90
|
+
menuGroupProblems,
|
|
91
|
+
modeSwitchProblems,
|
|
92
|
+
pillPopupAriaProblems,
|
|
93
|
+
webModeProblems,
|
|
94
|
+
} from "./web-checks.js";
|
|
95
|
+
import { createWebCases } from "./web-cases.js";
|
|
96
|
+
import {
|
|
97
|
+
DEFAULT_SIZES,
|
|
98
|
+
HOVER_MARGIN_MS,
|
|
99
|
+
LIGHT,
|
|
100
|
+
PARK,
|
|
101
|
+
SHOWN_ISLANDS,
|
|
102
|
+
assertHooksConsistent,
|
|
103
|
+
closeByEscape,
|
|
104
|
+
fail,
|
|
105
|
+
finish,
|
|
106
|
+
hover,
|
|
107
|
+
menuEntries,
|
|
108
|
+
openHarness,
|
|
109
|
+
openMenuByClick,
|
|
110
|
+
pollUntil,
|
|
111
|
+
shapes,
|
|
112
|
+
sizeLabel,
|
|
113
|
+
unfed,
|
|
114
|
+
webHookSkips,
|
|
115
|
+
} from "./case-tools.js";
|
|
116
|
+
|
|
117
|
+
export { DEFAULT_SIZES } from "./case-tools.js";
|
|
118
|
+
import { modeChangesProbe, watchModeProbe, webModeProbe, type WebModeFacts } from "./web-facts.js";
|
|
108
119
|
|
|
109
120
|
/** How far the read-only case drags a shape (guide §13.5: no move). */
|
|
110
121
|
const LOCKED_DRAG_PX = 40;
|
|
@@ -118,8 +129,6 @@ const READ_ONLY_WAIT_MS = 1000;
|
|
|
118
129
|
/** Room for the palette's 150ms colour transition (`--cd-motion-fast`) and a frame on either side. */
|
|
119
130
|
const COLOR_WAIT_MS = 600;
|
|
120
131
|
|
|
121
|
-
const POLL_MS = 50;
|
|
122
|
-
|
|
123
132
|
/** Requested from the modeler by the appearance case (VS Code Dark, High Contrast, High Contrast Light). */
|
|
124
133
|
const REQUESTED_APPEARANCES: readonly Appearance[] = [
|
|
125
134
|
{ theme: "dark" },
|
|
@@ -127,12 +136,6 @@ const REQUESTED_APPEARANCES: readonly Appearance[] = [
|
|
|
127
136
|
{ theme: "light", kontrast: "hoch" },
|
|
128
137
|
];
|
|
129
138
|
|
|
130
|
-
/** Room for the chrome tooltip's own fade after its 300ms hover delay. */
|
|
131
|
-
const HOVER_MARGIN_MS = 200;
|
|
132
|
-
|
|
133
|
-
/** A spot without chrome: the root's top-left corner lies outside every island (12px edge). */
|
|
134
|
-
const PARK = { x: 1, y: 1 } as const;
|
|
135
|
-
|
|
136
139
|
/** Every kind of text field a modeler renders: inspector and dialog fields, the label editor. */
|
|
137
140
|
const TEXT_FIELDS: ReadonlyArray<readonly [InjectedKind, string]> = [
|
|
138
141
|
["input", "a text field"],
|
|
@@ -153,48 +156,10 @@ const TWO_STEPS_UP = 182;
|
|
|
153
156
|
const ZOOM_LIMIT_MAX = 400;
|
|
154
157
|
const ZOOM_LIMIT_MIN = 20;
|
|
155
158
|
|
|
156
|
-
function sizeLabel(size: Size): string {
|
|
157
|
-
return `${size.width} × ${size.height}`;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
159
|
function appearanceLabel(appearance: Appearance): string {
|
|
161
160
|
return appearance.kontrast ? `${appearance.theme} + ${appearance.kontrast}` : appearance.theme;
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
function fail(caseName: string, host: ShellHost, problems: readonly string[]): never {
|
|
165
|
-
const unique = [...new Set(problems)];
|
|
166
|
-
throw new Error(
|
|
167
|
-
`@miragon/modeler-shell conformance: ${caseName} (${host} host)\n${unique.map((problem) => `- ${problem}`).join("\n")}`,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function finish(
|
|
172
|
-
caseName: string,
|
|
173
|
-
host: ShellHost,
|
|
174
|
-
problems: readonly string[],
|
|
175
|
-
skipped: Iterable<ShellSkip>,
|
|
176
|
-
): ShellCaseOutcome | void {
|
|
177
|
-
if (problems.length > 0) fail(caseName, host, problems);
|
|
178
|
-
const seen = new Set<string>();
|
|
179
|
-
const unique = [...skipped].filter((skip) => !seen.has(skip.reason) && seen.add(skip.reason));
|
|
180
|
-
return unique.length > 0 ? { skipped: unique } : undefined;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** A harness that gives a hook it also declares absent contradicts itself: fail before opening anything. */
|
|
184
|
-
function assertHooksConsistent<P extends PageLike>(
|
|
185
|
-
caseName: string,
|
|
186
|
-
harness: ShellConformanceHarness<P>,
|
|
187
|
-
hooks: readonly ShowHook[],
|
|
188
|
-
): void {
|
|
189
|
-
const problems = harnessHookProblems(harness, hooks);
|
|
190
|
-
if (problems.length > 0) fail(caseName, harness.host, problems);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** A hook that is neither given nor declared absent. */
|
|
194
|
-
function unfed<P extends PageLike>(harness: ShellConformanceHarness<P>, hook: ShowHook): boolean {
|
|
195
|
-
return !harness.show?.[hook] && !harness.absent?.includes(hook);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
163
|
function missingHooks<P extends PageLike>(harness: ShellConformanceHarness<P>, what: string): ShellSkip[] {
|
|
199
164
|
return SHOWN_ISLANDS.filter((island) => unfed(harness, island)).map((island) => ({
|
|
200
165
|
hook: island,
|
|
@@ -202,21 +167,6 @@ function missingHooks<P extends PageLike>(harness: ShellConformanceHarness<P>, w
|
|
|
202
167
|
}));
|
|
203
168
|
}
|
|
204
169
|
|
|
205
|
-
/** Reads until `done` holds or `timeoutMs` passed; the last value either way. */
|
|
206
|
-
async function pollUntil<T>(
|
|
207
|
-
page: PageLike,
|
|
208
|
-
read: () => Promise<T>,
|
|
209
|
-
done: (value: T) => boolean,
|
|
210
|
-
timeoutMs: number,
|
|
211
|
-
): Promise<T> {
|
|
212
|
-
let value = await read();
|
|
213
|
-
for (let waited = 0; !done(value) && waited < timeoutMs; waited += POLL_MS) {
|
|
214
|
-
await page.waitForTimeout(POLL_MS);
|
|
215
|
-
value = await read();
|
|
216
|
-
}
|
|
217
|
-
return value;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
170
|
/** The visible islands of an ARIA snapshot that a harness may declare absent. */
|
|
221
171
|
function visibleIslands(facts: AriaFacts): Set<string> {
|
|
222
172
|
const islands = new Set<string>();
|
|
@@ -224,30 +174,6 @@ function visibleIslands(facts: AriaFacts): Set<string> {
|
|
|
224
174
|
return islands;
|
|
225
175
|
}
|
|
226
176
|
|
|
227
|
-
/** Hovers `target` long enough for its chrome tooltip; the hovered control, `null` when there is none. */
|
|
228
|
-
async function hover(page: PageLike, root: string, target: ChromeTarget): Promise<TargetFacts | null> {
|
|
229
|
-
const anchor = await findTarget(page, root, target);
|
|
230
|
-
if (!anchor) return null;
|
|
231
|
-
await page.mouse.move(PARK.x, PARK.y);
|
|
232
|
-
await page.mouse.move(anchor.client.x, anchor.client.y);
|
|
233
|
-
await page.waitForTimeout(TOOLTIP_DELAY_MS + HOVER_MARGIN_MS);
|
|
234
|
-
await settle(page);
|
|
235
|
-
return anchor;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async function openMenuByClick(page: PageLike, root: string): Promise<boolean> {
|
|
239
|
-
const trigger = await findTarget(page, root, "menu");
|
|
240
|
-
if (!trigger) return false;
|
|
241
|
-
await page.mouse.click(trigger.client.x, trigger.client.y);
|
|
242
|
-
await settle(page);
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
async function closeByEscape(page: PageLike): Promise<void> {
|
|
247
|
-
await page.keyboard.press("Escape");
|
|
248
|
-
await settle(page);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
177
|
/** Dropdown and tooltips of one measured state (guide §5.4, §5.6). */
|
|
252
178
|
async function popupGeometry(
|
|
253
179
|
page: PageLike,
|
|
@@ -281,8 +207,11 @@ async function popupGeometry(
|
|
|
281
207
|
if (anchor.label && !tooltip.text.startsWith(anchor.label)) {
|
|
282
208
|
problems.push(`tooltip: hovering ${target} ("${anchor.label}") shows "${tooltip.text}"`);
|
|
283
209
|
}
|
|
210
|
+
// The web app's raised zoom row stands above the legal line (`data-zoom-row="raised"`).
|
|
211
|
+
const legalHeight = legalLineHeight(measurement.islands.legal?.height);
|
|
212
|
+
const bottomRow = measurement.open.zoomRaised ? SHELL.edge + legalHeight + SHELL.gap : SHELL.edge;
|
|
284
213
|
problems.push(
|
|
285
|
-
...tooltipProblems(tooltip.box, anchor.box, row, measurement.container).map(
|
|
214
|
+
...tooltipProblems(tooltip.box, anchor.box, row, measurement.container, undefined, bottomRow).map(
|
|
286
215
|
(problem) => `${problem} (on ${target})`,
|
|
287
216
|
),
|
|
288
217
|
);
|
|
@@ -291,15 +220,36 @@ async function popupGeometry(
|
|
|
291
220
|
return problems;
|
|
292
221
|
}
|
|
293
222
|
|
|
223
|
+
/** Shown by the geometry case after "as opened"; its action keeps it on screen through every state. */
|
|
224
|
+
const GEOMETRY_TOAST: ToastFixture = {
|
|
225
|
+
status: "info",
|
|
226
|
+
text: "Conformance: the toast stack's geometry",
|
|
227
|
+
actionLabel: "Undo",
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** The pill's list in the web app (and an embedded one that opens it), measured after a click (guide §5.10). */
|
|
231
|
+
async function withProblemsListOpen(
|
|
232
|
+
page: PageLike,
|
|
233
|
+
root: string,
|
|
234
|
+
check: () => Promise<unknown>,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
const pill = await findTarget(page, root, "pill");
|
|
237
|
+
if (!pill) return;
|
|
238
|
+
await page.mouse.click(pill.client.x, pill.client.y);
|
|
239
|
+
await settle(page);
|
|
240
|
+
await check();
|
|
241
|
+
await closeByEscape(page);
|
|
242
|
+
}
|
|
243
|
+
|
|
294
244
|
const geometryCase: ShellConformanceCase = {
|
|
295
245
|
name: "geometry",
|
|
296
246
|
async run<P extends PageLike>(page: P, harness: ShellConformanceHarness<P>) {
|
|
297
|
-
assertHooksConsistent("geometry", harness, SHOWN_ISLANDS);
|
|
247
|
+
assertHooksConsistent("geometry", harness, [...SHOWN_ISLANDS, "toast", "emptyState"]);
|
|
298
248
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
299
249
|
const problems: string[] = [];
|
|
300
250
|
for (const size of harness.sizes ?? DEFAULT_SIZES) {
|
|
301
251
|
const at = sizeLabel(size);
|
|
302
|
-
await
|
|
252
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
303
253
|
await settle(page);
|
|
304
254
|
const check = async (state: string): Promise<ChromeMeasurement> => {
|
|
305
255
|
const measurement = await measureChrome(page, root);
|
|
@@ -313,15 +263,36 @@ const geometryCase: ShellConformanceCase = {
|
|
|
313
263
|
};
|
|
314
264
|
const opened = await check("as opened");
|
|
315
265
|
problems.push(...(await popupGeometry(page, root, opened)).map((problem) => `[${at}] ${problem}`));
|
|
266
|
+
const toast = harness.show?.toast;
|
|
267
|
+
if (toast) {
|
|
268
|
+
await toast(page, GEOMETRY_TOAST);
|
|
269
|
+
await settle(page);
|
|
270
|
+
await check("with a toast");
|
|
271
|
+
}
|
|
316
272
|
for (const island of SHOWN_ISLANDS) {
|
|
317
273
|
const show = harness.show?.[island];
|
|
318
274
|
if (!show) continue;
|
|
319
275
|
await show(page);
|
|
320
276
|
await settle(page);
|
|
321
277
|
await check(`after show.${island}`);
|
|
278
|
+
if (island === "problems" && harness.host !== "ide") {
|
|
279
|
+
await withProblemsListOpen(page, root, () => check("with the problems list open"));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const emptyState = harness.show?.emptyState;
|
|
283
|
+
if (emptyState) {
|
|
284
|
+
await emptyState(page);
|
|
285
|
+
await settle(page);
|
|
286
|
+
await check("after show.emptyState");
|
|
322
287
|
}
|
|
323
288
|
}
|
|
324
|
-
return finish("geometry", harness.host, problems,
|
|
289
|
+
return finish("geometry", harness.host, problems, [
|
|
290
|
+
...missingHooks(harness, "the geometry"),
|
|
291
|
+
...webHookSkips(harness, {
|
|
292
|
+
toast: "the geometry of the toast stack",
|
|
293
|
+
emptyState: "the geometry of the start screen",
|
|
294
|
+
}),
|
|
295
|
+
]);
|
|
325
296
|
},
|
|
326
297
|
};
|
|
327
298
|
|
|
@@ -433,8 +404,8 @@ async function paletteShortcutWalk(page: PageLike, root: string): Promise<string
|
|
|
433
404
|
return paletteShortcutProblems(seen);
|
|
434
405
|
}
|
|
435
406
|
|
|
436
|
-
/** Tab from the menu trigger: the palette's one stop, then the canvas (guide §5.14). */
|
|
437
|
-
async function tabSequenceFromMenu(page: PageLike, root: string): Promise<string[]> {
|
|
407
|
+
/** Tab from the menu trigger: the primary action (web app), the palette's one stop, then the canvas (guide §5.14). */
|
|
408
|
+
async function tabSequenceFromMenu(page: PageLike, root: string, primary: boolean): Promise<string[]> {
|
|
438
409
|
if (!(await focusTarget(page, root, "menu"))) return [];
|
|
439
410
|
const stops: TabStop[] = [];
|
|
440
411
|
for (let step = 0; step < 3; step++) {
|
|
@@ -442,7 +413,7 @@ async function tabSequenceFromMenu(page: PageLike, root: string): Promise<string
|
|
|
442
413
|
const stop = await inPage(page, activeStopProbe, { root });
|
|
443
414
|
stops.push({ description: stop.description, kind: stop.kind as TabStop["kind"] });
|
|
444
415
|
}
|
|
445
|
-
return tabSequenceProblems(stops);
|
|
416
|
+
return tabSequenceProblems(stops, primary);
|
|
446
417
|
}
|
|
447
418
|
|
|
448
419
|
/** `aria-disabled` at 400 % and 20 % (guide §5.8), reached by clicking the buttons. */
|
|
@@ -494,15 +465,18 @@ const ariaCase: ShellConformanceCase = {
|
|
|
494
465
|
assertHooksConsistent("aria", harness, SHOWN_ISLANDS);
|
|
495
466
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
496
467
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0]!;
|
|
497
|
-
await
|
|
468
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
498
469
|
await settle(page);
|
|
499
470
|
const facts = await inPage(page, ariaProbe, { root });
|
|
500
471
|
if (!facts) fail("aria", harness.host, [`no element matches the shell root selector "${root}"`]);
|
|
501
472
|
const problems = ariaProblems(facts, harness.host);
|
|
473
|
+
problems.push(...menuGroupProblems(await menuEntries(page, root), harness.host, harness.themes));
|
|
502
474
|
problems.push(...(await menuKeyboardProblems(page, root)));
|
|
503
475
|
problems.push(...(await paletteKeyboardProblems(page, root)));
|
|
504
476
|
problems.push(...(await paletteShortcutWalk(page, root)));
|
|
505
|
-
problems.push(
|
|
477
|
+
problems.push(
|
|
478
|
+
...(await tabSequenceFromMenu(page, root, harness.host === "web" && Boolean(facts.primary?.visible))),
|
|
479
|
+
);
|
|
506
480
|
problems.push(...(await zoomLimitProblems(page, root)));
|
|
507
481
|
|
|
508
482
|
const shown = new Set<string>();
|
|
@@ -519,6 +493,7 @@ const ariaCase: ShellConformanceCase = {
|
|
|
519
493
|
if (withIslands) {
|
|
520
494
|
final = withIslands;
|
|
521
495
|
problems.push(...optionalIslandProblems(withIslands, shown));
|
|
496
|
+
if (shown.has("problems")) problems.push(...pillPopupAriaProblems(withIslands, harness.host));
|
|
522
497
|
const known = new Set(problems);
|
|
523
498
|
problems.push(
|
|
524
499
|
...ariaProblems(withIslands, harness.host)
|
|
@@ -538,7 +513,7 @@ const keysCase: ShellConformanceCase = {
|
|
|
538
513
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
539
514
|
const host = harness.host;
|
|
540
515
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0]!;
|
|
541
|
-
await
|
|
516
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
542
517
|
await settle(page);
|
|
543
518
|
const problems: string[] = [];
|
|
544
519
|
|
|
@@ -733,15 +708,94 @@ async function paletteColorWalk(page: PageLike, root: string): Promise<string[]>
|
|
|
733
708
|
return [...new Set(problems)];
|
|
734
709
|
}
|
|
735
710
|
|
|
711
|
+
/** The web app's mode, its meta tags and the stored choice. */
|
|
712
|
+
function webMode(page: PageLike): Promise<WebModeFacts> {
|
|
713
|
+
return inPage(page, webModeProbe, { key: APPEARANCE_STORAGE_KEY });
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const APPEARANCE_LABELS: Readonly<Record<AppearanceChoice, string>> = {
|
|
717
|
+
system: "System",
|
|
718
|
+
light: "Light",
|
|
719
|
+
dark: "Dark",
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Guide §13.3 for a dark-capable web app: Dark, Light, then System through the Appearance group,
|
|
724
|
+
* each written to `<html>`, `color-scheme`, both `theme-color`s and `localStorage` in one frame and
|
|
725
|
+
* never half-dark; the theme colours are the ones the page showed under System.
|
|
726
|
+
*/
|
|
727
|
+
async function appearanceSwitchProblems<P extends PageLike>(
|
|
728
|
+
page: P,
|
|
729
|
+
harness: ShellConformanceHarness<P>,
|
|
730
|
+
root: string,
|
|
731
|
+
requested: Appearance,
|
|
732
|
+
): Promise<string[]> {
|
|
733
|
+
const problems: string[] = [];
|
|
734
|
+
const system = await webMode(page);
|
|
735
|
+
const media = (scheme: string) =>
|
|
736
|
+
system.themeColors.find((meta) => meta.media?.replace(/\s+/g, "") === `(prefers-color-scheme:${scheme})`)
|
|
737
|
+
?.content;
|
|
738
|
+
const light = media("light");
|
|
739
|
+
const dark = media("dark");
|
|
740
|
+
const colors = light && dark ? { light, dark } : undefined;
|
|
741
|
+
for (const choice of ["dark", "light", "system"] as const) {
|
|
742
|
+
const label = APPEARANCE_LABELS[choice];
|
|
743
|
+
if (!(await openMenuByClick(page, root))) {
|
|
744
|
+
problems.push(`menu: no visible menu trigger to choose ${label}`);
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
const option = await inPage(page, targetProbe, {
|
|
748
|
+
root,
|
|
749
|
+
selector: `[data-menu-id="appearance"] [data-menu-value="${choice}"]`,
|
|
750
|
+
});
|
|
751
|
+
if (!option) {
|
|
752
|
+
problems.push(`menu: no Appearance option "${choice}" to choose (guide §13.3)`);
|
|
753
|
+
await closeByEscape(page);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
await inPage(page, watchModeProbe, null);
|
|
757
|
+
await page.mouse.click(option.client.x, option.client.y);
|
|
758
|
+
await settle(page);
|
|
759
|
+
problems.push(...modeSwitchProblems(await inPage(page, modeChangesProbe, null), label));
|
|
760
|
+
problems.push(
|
|
761
|
+
...appearanceChoiceProblems((await inPage(page, menuEntriesProbe, { root })) ?? [], choice),
|
|
762
|
+
);
|
|
763
|
+
await closeByEscape(page);
|
|
764
|
+
problems.push(...webModeProblems(await webMode(page), harness.themes, choice, colors));
|
|
765
|
+
const facts = await appearanceFacts(page, root);
|
|
766
|
+
if (facts) {
|
|
767
|
+
const expected = expectedWebMode(requested, harness.themes, choice);
|
|
768
|
+
problems.push(
|
|
769
|
+
...appearanceProblems(facts, "web", requested, harness.themes, expected).problems.map(
|
|
770
|
+
(problem) => `[chose ${label}] ${problem}`,
|
|
771
|
+
),
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return problems;
|
|
776
|
+
}
|
|
777
|
+
|
|
736
778
|
const appearanceCase: ShellConformanceCase = {
|
|
737
779
|
name: "appearance",
|
|
738
780
|
async run<P extends PageLike>(page: P, harness: ShellConformanceHarness<P>) {
|
|
739
781
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
740
782
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0]!;
|
|
741
783
|
const problems: string[] = [];
|
|
742
|
-
|
|
784
|
+
const skipped: ShellSkip[] = [];
|
|
785
|
+
const web = harness.host === "web";
|
|
786
|
+
// Without emulateMedia the web app's system preference stays the browser's light default.
|
|
787
|
+
const emulated = !web || page.emulateMedia !== undefined;
|
|
788
|
+
if (!emulated) {
|
|
789
|
+
skipped.push({
|
|
790
|
+
hook: "open",
|
|
791
|
+
reason:
|
|
792
|
+
"page.emulateMedia is undefined: the web app's mode under a dark system and under high contrast was not verified",
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
let switched = false;
|
|
796
|
+
for (const requested of emulated ? REQUESTED_APPEARANCES : [LIGHT]) {
|
|
743
797
|
const at = `[requested ${appearanceLabel(requested)}]`;
|
|
744
|
-
await
|
|
798
|
+
await openHarness(page, harness, { size, requested });
|
|
745
799
|
if (harness.host === "embedded") {
|
|
746
800
|
// The foreign page itself in that mode: a themed page around the modeler (guide §13.3).
|
|
747
801
|
await inPage(page, hostPageModeProbe, {
|
|
@@ -755,21 +809,35 @@ const appearanceCase: ShellConformanceCase = {
|
|
|
755
809
|
problems.push(`${at} no element matches the shell root selector "${root}"`);
|
|
756
810
|
continue;
|
|
757
811
|
}
|
|
758
|
-
const
|
|
812
|
+
const expected = web ? expectedWebMode(requested, harness.themes, "system") : undefined;
|
|
813
|
+
const verdict = appearanceProblems(facts, harness.host, requested, harness.themes, expected);
|
|
759
814
|
problems.push(...verdict.problems.map((problem) => `${at} ${problem}`));
|
|
760
815
|
const surround = await inPage(page, surroundProbe, { root });
|
|
761
816
|
if (surround) problems.push(...surroundProblems(surround).map((problem) => `${at} ${problem}`));
|
|
762
817
|
problems.push(...(await paletteColorWalk(page, root)).map((problem) => `${at} ${problem}`));
|
|
818
|
+
if (web) {
|
|
819
|
+
problems.push(
|
|
820
|
+
...webModeProblems(await webMode(page), harness.themes, "system", undefined).map(
|
|
821
|
+
(problem) => `${at} ${problem}`,
|
|
822
|
+
),
|
|
823
|
+
...lightOnlyMenuProblems(await menuEntries(page, root), harness.themes).map(
|
|
824
|
+
(problem) => `${at} ${problem}`,
|
|
825
|
+
),
|
|
826
|
+
);
|
|
827
|
+
if (harness.themes.includes("dark") && !switched && requested.theme === "dark") {
|
|
828
|
+
switched = true;
|
|
829
|
+
problems.push(
|
|
830
|
+
...(await appearanceSwitchProblems(page, harness, root, requested)).map(
|
|
831
|
+
(problem) => `${at} ${problem}`,
|
|
832
|
+
),
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
763
836
|
}
|
|
764
|
-
return finish("appearance", harness.host, problems,
|
|
837
|
+
return finish("appearance", harness.host, problems, skipped);
|
|
765
838
|
},
|
|
766
839
|
};
|
|
767
840
|
|
|
768
|
-
/** The visible shapes on the canvas. */
|
|
769
|
-
function shapes(page: PageLike, root: string): Promise<ShapeFacts[]> {
|
|
770
|
-
return inPage(page, shapesProbe, { root });
|
|
771
|
-
}
|
|
772
|
-
|
|
773
841
|
/** Moves the palette's roving focus by keyboard to `action`; `false` when it cannot get there. */
|
|
774
842
|
async function focusPaletteEntry(page: PageLike, root: string, action: string): Promise<boolean> {
|
|
775
843
|
const before = await inPage(page, paletteFocusProbe, { root });
|
|
@@ -812,7 +880,7 @@ const createCase: ShellConformanceCase = {
|
|
|
812
880
|
async run<P extends PageLike>(page: P, harness: ShellConformanceHarness<P>) {
|
|
813
881
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
814
882
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0]!;
|
|
815
|
-
await
|
|
883
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
816
884
|
await settle(page);
|
|
817
885
|
const entry = firstCreatingEntry(await inPage(page, paletteEntriesProbe, { root }));
|
|
818
886
|
// A notation whose palette only switches tools has nothing to place.
|
|
@@ -831,14 +899,6 @@ const createCase: ShellConformanceCase = {
|
|
|
831
899
|
},
|
|
832
900
|
};
|
|
833
901
|
|
|
834
|
-
/** The open menu's entries, opened by a click and closed again by Escape. */
|
|
835
|
-
async function menuEntries(page: PageLike, root: string): Promise<MenuEntryFacts[]> {
|
|
836
|
-
if (!(await openMenuByClick(page, root))) return [];
|
|
837
|
-
const entries = (await inPage(page, menuEntriesProbe, { root })) ?? [];
|
|
838
|
-
await closeByEscape(page);
|
|
839
|
-
return entries;
|
|
840
|
-
}
|
|
841
|
-
|
|
842
902
|
/** A shape whose centre a pointer reaches (no chrome above it), found again by its id. */
|
|
843
903
|
async function reachableShape(page: PageLike, root: string, id?: string): Promise<ShapeFacts | undefined> {
|
|
844
904
|
const all = await shapes(page, root);
|
|
@@ -907,7 +967,7 @@ const readOnlyCase: ShellConformanceCase = {
|
|
|
907
967
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0]!;
|
|
908
968
|
const problems: string[] = [];
|
|
909
969
|
const skipped: ShellSkip[] = [];
|
|
910
|
-
await
|
|
970
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
911
971
|
await settle(page);
|
|
912
972
|
const menuBefore = await menuEntries(page, root);
|
|
913
973
|
|
|
@@ -971,7 +1031,7 @@ const cleanupCase: ShellConformanceCase = {
|
|
|
971
1031
|
}
|
|
972
1032
|
const root = harness.root ?? DEFAULT_ROOT;
|
|
973
1033
|
const size = (harness.sizes ?? DEFAULT_SIZES)[0] ?? DEFAULT_SIZES[0]!;
|
|
974
|
-
await
|
|
1034
|
+
await openHarness(page, harness, { size, requested: LIGHT });
|
|
975
1035
|
await settle(page);
|
|
976
1036
|
const armed = await inPage(page, armCleanupProbe, { root });
|
|
977
1037
|
if (!armed) fail("cleanup", harness.host, [`no element matches the shell root selector "${root}"`]);
|
|
@@ -1007,6 +1067,11 @@ const cleanupCase: ShellConformanceCase = {
|
|
|
1007
1067
|
},
|
|
1008
1068
|
};
|
|
1009
1069
|
|
|
1010
|
-
|
|
1070
|
+
/** The 0.1 cases, extended for the web column. */
|
|
1071
|
+
export function createBaseCases(): ShellConformanceCase[] {
|
|
1011
1072
|
return [geometryCase, ariaCase, keysCase, createCase, readOnlyCase, appearanceCase, cleanupCase];
|
|
1012
1073
|
}
|
|
1074
|
+
|
|
1075
|
+
export function createCases(): ShellConformanceCase[] {
|
|
1076
|
+
return [...createBaseCases(), ...createWebCases()];
|
|
1077
|
+
}
|