@dailephd/my-dev-kit-lab 0.4.7 → 0.4.8
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/CHANGELOG.md +14 -0
- package/README.md +4 -3
- package/dist/src/tutorial/index.js +1 -0
- package/dist/src/tutorial/scenarioValidation.js +45 -1
- package/dist/src/tutorial/tutorialActions.js +65 -0
- package/dist/src/tutorial/tutorialPointerGeometry.js +16 -0
- package/dist/src/tutorial/tutorialSession.js +30 -4
- package/dist/src/tutorial/types.js +4 -1
- package/examples/tutorial-browser/index.html +109 -47
- package/examples/tutorial-browser/scenario.json +19 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to my-dev-kit-lab are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.4.8] - 2026-09-19
|
|
6
|
+
|
|
7
|
+
Locator-anchored pointer gestures for declarative browser tutorials.
|
|
8
|
+
|
|
9
|
+
- Added `pointer-click` and `pointer-drag` declarative tutorial actions anchored to a single `TutorialLocatorV1` interaction surface.
|
|
10
|
+
- Added normalized fraction coordinates (`coordinateSpace: "fraction"`) with inclusive `[0, 1]` bounds and zero-length pointer-drag rejection.
|
|
11
|
+
- Used real Playwright mouse input (`mouse.move`, `mouse.down`, `mouse.up`) with a fixed 8-step drag movement (`POINTER_DRAG_MOVE_STEPS = 8`) and robust `mouse.up` error cleanup.
|
|
12
|
+
- Preserved existing element-to-element `drag` (`source` and `target` locators via Playwright `dragTo`) and all existing scenario action/assertion behaviors without change.
|
|
13
|
+
- Preserved closed security boundaries: no arbitrary JavaScript, no `page.evaluate` callbacks, no generic DOM event dispatch, no shell actions, and no unanchored page/screen coordinates.
|
|
14
|
+
- Extended the generic real-browser fixture (`examples/tutorial-browser/`) with an interactive SVG pointer surface, verifying pointer clicks, pointer drags, and intermediate pointermove tracking through genuine Chromium.
|
|
15
|
+
- Extended packed-package verification (`npm run verify:packed-package`) to validate and execute the new pointer actions against a clean installed-tarball consumer while maintaining package, target, and example immutability.
|
|
16
|
+
- Validated downstream `my-frontend-observer` compatibility evidence as a read-only compatibility consumer (rectangle, line, arrow, point, and note gestures confirmed expressible without Observer code modifications or runtime dependencies).
|
|
17
|
+
- Kept `TutorialScenarioV1`, `TutorialTargetContractV1`, `TutorialRunResultV1`, and `TutorialManifestV1` schemas at version `1.0.0`.
|
|
18
|
+
|
|
5
19
|
## [0.4.7] - 2026-09-18
|
|
6
20
|
|
|
7
21
|
The v0.4.7 implementation adds generic declarative browser tutorial automation while preserving product-specific demo ownership outside this repository.
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ my-dev-kit-lab is the experiment, audit, and evidence companion for [my-dev-kit]
|
|
|
4
4
|
|
|
5
5
|
my-dev-kit provides local repository indexing and graph-guided retrieval. my-dev-kit-lab supplies the controlled benchmarks, agent adapters, metrics, security checks, and reports needed to evaluate when that retrieval is useful. Results are evidence for a specific target and configuration; they do not guarantee token savings or security.
|
|
6
6
|
|
|
7
|
-
The latest
|
|
7
|
+
The latest release is v0.4.8 (locator-anchored pointer gestures for declarative browser tutorials, adding `pointer-click` and `pointer-drag` using normalized locator-relative fraction positions while existing `drag` remains element-to-element). The v0.4.5 context-integrity evaluation remains intentionally frozen against the published `@dailephd/my-dev-kit@1.10.4` and `@dailephd/my-dev-kit-orchestrator@1.2.3` contracts; those versions are historical validation baselines, not a statement that they are the ecosystem's current releases. See [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md).
|
|
8
8
|
|
|
9
9
|
v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--version`, `security validate`, `audit`, the `experiment` family, `report render`, `plots generate`, `gallery build`, `demo final`, and the historical direct final-demo invocation form), a writable lab workspace model kept separate from the installed package and the inspected target, and a permanent packed-tarball installation/execution acceptance gate (`npm run verify:packed-package`). See [docs/ROADMAP.md](docs/ROADMAP.md) and [docs/CURRENT_STATE.md](docs/CURRENT_STATE.md) for status detail and [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for the runtime path model. The "Installed CLI" section below documents the shipped command surface; the source-checkout `npm run` workflow in Quickstart remains available for contributors.
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--vers
|
|
|
15
15
|
- **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
|
|
16
16
|
- **Validate Android projects:** run nineteen static checks by default, with Gradle operations, external tools, and network access available only through explicit opt-in flags.
|
|
17
17
|
- **Review evidence:** generate JSON and HTML reports, SVG plots, optional screenshots, visualization demos, and a static gallery.
|
|
18
|
-
- **Generate declarative browser tutorials:** validate and execute bounded local browser scenarios with WebM recording, step screenshots, SRT/VTT subtitles, Markdown, and a tutorial manifest. Tutorial execution requires a compatible locally installed Chromium; see [docs/COMMANDS.md](docs/COMMANDS.md) and [docs/TUTORIAL.md](docs/TUTORIAL.md) for details.
|
|
18
|
+
- **Generate declarative browser tutorials:** validate and execute bounded local browser scenarios with WebM recording, step screenshots, SRT/VTT subtitles, Markdown, and a tutorial manifest. Locator-anchored pointer gestures (`pointer-click` and `pointer-drag`) operate using normalized fraction coordinates inside a single located surface, while existing `drag` remains element-to-element. Tutorial execution requires a compatible locally installed Chromium; see [docs/COMMANDS.md](docs/COMMANDS.md) and [docs/TUTORIAL.md](docs/TUTORIAL.md) for details.
|
|
19
19
|
- **Evaluate stage-context strategies:** compare the two legacy strategies against six additional bounded stage-context strategies — `architecture-context-only`, `architecture-plus-implementation-refresh`, `architecture-plus-implementation-and-test-refresh`, `full-workflow-library`, `bounded-workflow-instruction-packet`, and `combined-bounded-stage-context` — selected through programmatic configuration, not CLI flags. Each strategy's evidence is reported through bounded `report.json`, `report.html`, and `report.txt` output with an explicit `available`/`unavailable`/`not-applicable` metric-availability model and no composite score, grade, ranking, or winning strategy.
|
|
20
20
|
- **Evaluate the producer-readiness bridge (v0.4.4):** optionally extend `combined-bounded-stage-context` with the frozen my-dev-kit-orchestrator supplemental implementation/test-context packet and retrieval-report documents plus an observed readiness result, all supplied programmatically (there is no CLI flag), to measure owner, allocation, truncation-cause, supplemental/raw agreement, readiness-agreement, and criticality-overlay evidence without reimplementing upstream owner-selection, allocation, producer-parity, or readiness policy.
|
|
21
21
|
- **Evaluate context integrity (v0.4.5):** compare condition-aware producer evidence from my-dev-kit v1.10.4 (role condition coverage, allocation/spillover, required-evidence-loss) against my-dev-kit-orchestrator v1.2.3 run-integrity evidence (run-integrity gate, judge integrity, final-report eligibility, artifact lifecycle state), reporting agreement or contradiction between them rather than re-deriving a verdict. Evaluation runs against a frozen, hash-verified regression fixture pair — a byte-exact real historical failed run and a hand-distilled corrected-replay counterpart representing the same validated contracts — and is programmatic/test-driven only. `npm run report:context-integrity-smoke` renders both fixtures' reports for manual inspection; it takes no arguments and is a developer convenience, not a configurable evaluation CLI.
|
|
@@ -208,7 +208,8 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
|
|
|
208
208
|
- The published upstream artifacts the stage-context strategies read do not expose considered-but-unselected reads or unnecessary-read evidence; those metrics report `unavailable` rather than zero
|
|
209
209
|
- The v0.4.4 producer-readiness bridge is released. All bridge inputs are programmatic — there is no CLI flag. The coordinated upstream releases my-dev-kit@1.10.3 and orchestrator@1.2.2 were verified published before lab publication.
|
|
210
210
|
- The v0.4.5 context-integrity evaluation is released. It has no CLI flags, no plots/screenshot/gallery integration, and produces no composite score, grade, ranking, or winner. Its corrected-replay fixture is a hand-distilled representation of the validated my-dev-kit v1.10.4 and my-dev-kit-orchestrator v1.2.3 contracts, not a live capture of a complete ten-stage workflow. The orchestrator agreement evidence does not read a literal upstream `promptMode` field; `stageMayRenderNormalPrompt`, derived from structured blocked-stage evidence, is the bounded substitute used instead.
|
|
211
|
-
- v0.4.7
|
|
211
|
+
- v0.4.7 added declarative browser tutorial validation and execution with bounded actions/assertions, persistent Chromium sessions, WebM, screenshots, SRT/VTT, Markdown, and `tutorial-manifest.json`. Chromium is a separate local runtime prerequisite; the package does not download browser binaries automatically. Gallery consumption remains future scope.
|
|
212
|
+
- v0.4.8 added `pointer-click` and `pointer-drag` for normalized fraction positions inside a single located element. Existing `drag` remains element-to-element between source and target locators.
|
|
212
213
|
|
|
213
214
|
---
|
|
214
215
|
|
|
@@ -5,6 +5,7 @@ export * from "./loadTutorialContracts.js";
|
|
|
5
5
|
export * from "./tutorialPaths.js";
|
|
6
6
|
export * from "./tutorialLocators.js";
|
|
7
7
|
export * from "./tutorialActions.js";
|
|
8
|
+
export * from "./tutorialPointerGeometry.js";
|
|
8
9
|
export * from "./tutorialAssertions.js";
|
|
9
10
|
export * from "./tutorialCursor.js";
|
|
10
11
|
export * from "./tutorialOverlay.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MAX_STEP_PAUSE_MS, MAX_TUTORIAL_STEPS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, MIN_VIEWPORT_HEIGHT, MIN_VIEWPORT_WIDTH, TUTORIAL_ACTION_TYPES, TUTORIAL_ASSERTION_TYPES, TUTORIAL_CALLOUT_PLACEMENTS, TUTORIAL_GOTO_WAIT_UNTIL, TUTORIAL_LOCATOR_KINDS, TUTORIAL_SCHEMA_VERSION, TUTORIAL_WAIT_FOR_STATES } from "./types.js";
|
|
1
|
+
import { MAX_STEP_PAUSE_MS, MAX_TUTORIAL_STEPS, MAX_VIEWPORT_HEIGHT, MAX_VIEWPORT_WIDTH, MIN_VIEWPORT_HEIGHT, MIN_VIEWPORT_WIDTH, TUTORIAL_ACTION_TYPES, TUTORIAL_ASSERTION_TYPES, TUTORIAL_CALLOUT_PLACEMENTS, TUTORIAL_GOTO_WAIT_UNTIL, TUTORIAL_LOCATOR_KINDS, TUTORIAL_POINTER_COORDINATE_SPACES, TUTORIAL_SCHEMA_VERSION, TUTORIAL_WAIT_FOR_STATES } from "./types.js";
|
|
2
2
|
import { createErrorCollector, describeValue, isJsonScalar, isNonEmptyString, isNonNegativeInteger, isPlainObject, rejectUnknownKeys, validateOptionalTimeout, validateSchemaVersion, validateStableId } from "./validationPrimitives.js";
|
|
3
3
|
const SCENARIO_KEYS = ["schemaVersion", "id", "title", "description", "targetId", "browser", "steps"];
|
|
4
4
|
const STEP_KEYS = [
|
|
@@ -266,6 +266,29 @@ export function validateAction(errors, location, value) {
|
|
|
266
266
|
validateLocator(errors, `${location}.target`, value.target);
|
|
267
267
|
validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
|
|
268
268
|
break;
|
|
269
|
+
case "pointer-click":
|
|
270
|
+
rejectUnknownKeys(errors, location, value, ["type", "locator", "position", "coordinateSpace", "timeoutMs"]);
|
|
271
|
+
validateLocator(errors, `${location}.locator`, value.locator);
|
|
272
|
+
validateFractionPoint(errors, `${location}.position`, value.position);
|
|
273
|
+
validatePointerCoordinateSpace(errors, `${location}.coordinateSpace`, value.coordinateSpace);
|
|
274
|
+
validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
|
|
275
|
+
break;
|
|
276
|
+
case "pointer-drag": {
|
|
277
|
+
rejectUnknownKeys(errors, location, value, ["type", "locator", "from", "to", "coordinateSpace", "timeoutMs"]);
|
|
278
|
+
validateLocator(errors, `${location}.locator`, value.locator);
|
|
279
|
+
const fromValid = validateFractionPoint(errors, `${location}.from`, value.from);
|
|
280
|
+
const toValid = validateFractionPoint(errors, `${location}.to`, value.to);
|
|
281
|
+
validatePointerCoordinateSpace(errors, `${location}.coordinateSpace`, value.coordinateSpace);
|
|
282
|
+
validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
|
|
283
|
+
if (fromValid && toValid) {
|
|
284
|
+
const from = value.from;
|
|
285
|
+
const to = value.to;
|
|
286
|
+
if (from.x === to.x && from.y === to.y) {
|
|
287
|
+
errors.add(`${location}.to`, "pointer-drag endpoints must be distinct; zero-length drags are not allowed.");
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
269
292
|
case "wait-for":
|
|
270
293
|
rejectUnknownKeys(errors, location, value, ["type", "locator", "state", "timeoutMs"]);
|
|
271
294
|
validateLocator(errors, `${location}.locator`, value.locator);
|
|
@@ -276,6 +299,27 @@ export function validateAction(errors, location, value) {
|
|
|
276
299
|
break;
|
|
277
300
|
}
|
|
278
301
|
}
|
|
302
|
+
function validateFractionPoint(errors, location, value) {
|
|
303
|
+
if (!isPlainObject(value)) {
|
|
304
|
+
errors.add(location, `expected a fraction point object, received ${describeValue(value)}.`);
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
const startLength = errors.length;
|
|
308
|
+
rejectUnknownKeys(errors, location, value, ["x", "y"]);
|
|
309
|
+
validateFractionCoordinate(errors, `${location}.x`, value.x);
|
|
310
|
+
validateFractionCoordinate(errors, `${location}.y`, value.y);
|
|
311
|
+
return errors.length === startLength;
|
|
312
|
+
}
|
|
313
|
+
function validateFractionCoordinate(errors, location, value) {
|
|
314
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
|
|
315
|
+
errors.add(location, `expected a finite number within inclusive bounds [0, 1], received ${describeValue(value)}.`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function validatePointerCoordinateSpace(errors, location, value) {
|
|
319
|
+
if (!TUTORIAL_POINTER_COORDINATE_SPACES.includes(value)) {
|
|
320
|
+
errors.add(location, `expected "fraction", received ${describeValue(value)}.`);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
279
323
|
/**
|
|
280
324
|
* A scenario may only navigate inside the target application's own origin, so a
|
|
281
325
|
* goto path is a root-relative path, never a URL. Rejecting the URL forms here
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describeTutorialLocator, resolveTutorialLocator } from "./tutorialLocators.js";
|
|
2
|
+
import { resolveTutorialFractionPoint } from "./tutorialPointerGeometry.js";
|
|
2
3
|
import { DEFAULT_TUTORIAL_ACTION_TIMEOUT_MS } from "./types.js";
|
|
4
|
+
export const POINTER_DRAG_MOVE_STEPS = 8;
|
|
3
5
|
/**
|
|
4
6
|
* Resolves a scenario `goto` path against the target application's URL and
|
|
5
7
|
* proves the result never leaves that origin.
|
|
@@ -72,6 +74,31 @@ async function performAction(page, action, context) {
|
|
|
72
74
|
await source.dragTo(target, { timeout: timeoutFor(action.timeoutMs) });
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
77
|
+
case "pointer-click": {
|
|
78
|
+
const locator = resolveTutorialLocator(page, action.locator);
|
|
79
|
+
await locator.waitFor({ state: "visible", timeout: timeoutFor(action.timeoutMs) });
|
|
80
|
+
const box = await locator.boundingBox();
|
|
81
|
+
if (!box) {
|
|
82
|
+
throw new Error("locator has no bounding box");
|
|
83
|
+
}
|
|
84
|
+
const point = resolveTutorialFractionPoint(box, action.position);
|
|
85
|
+
await page.mouse.move(point.x, point.y);
|
|
86
|
+
await runWithMouseDown(page.mouse, async () => { });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
case "pointer-drag": {
|
|
90
|
+
const locator = resolveTutorialLocator(page, action.locator);
|
|
91
|
+
await locator.waitFor({ state: "visible", timeout: timeoutFor(action.timeoutMs) });
|
|
92
|
+
const box = await locator.boundingBox();
|
|
93
|
+
if (!box) {
|
|
94
|
+
throw new Error("locator has no bounding box");
|
|
95
|
+
}
|
|
96
|
+
const start = resolveTutorialFractionPoint(box, action.from);
|
|
97
|
+
const end = resolveTutorialFractionPoint(box, action.to);
|
|
98
|
+
await page.mouse.move(start.x, start.y);
|
|
99
|
+
await runWithMouseDown(page.mouse, () => page.mouse.move(end.x, end.y, { steps: POINTER_DRAG_MOVE_STEPS }));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
75
102
|
case "wait-for":
|
|
76
103
|
await resolveTutorialLocator(page, action.locator).waitFor({
|
|
77
104
|
...(action.state !== undefined ? { state: action.state } : {}),
|
|
@@ -80,6 +107,34 @@ async function performAction(page, action, context) {
|
|
|
80
107
|
return;
|
|
81
108
|
}
|
|
82
109
|
}
|
|
110
|
+
async function runWithMouseDown(mouse, operation) {
|
|
111
|
+
await mouse.down();
|
|
112
|
+
let primaryError;
|
|
113
|
+
let primaryFailed = false;
|
|
114
|
+
try {
|
|
115
|
+
await operation();
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
primaryFailed = true;
|
|
119
|
+
primaryError = error;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await mouse.up();
|
|
123
|
+
}
|
|
124
|
+
catch (cleanupError) {
|
|
125
|
+
if (primaryFailed) {
|
|
126
|
+
const primaryMessage = messageOf(primaryError);
|
|
127
|
+
const cleanupMessage = messageOf(cleanupError);
|
|
128
|
+
throw new Error(`${primaryMessage} (mouse.up cleanup also failed: ${cleanupMessage})`, {
|
|
129
|
+
cause: primaryError
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
throw cleanupError;
|
|
133
|
+
}
|
|
134
|
+
if (primaryFailed) {
|
|
135
|
+
throw primaryError;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
83
138
|
export function timeoutFor(timeoutMs) {
|
|
84
139
|
return timeoutMs ?? DEFAULT_TUTORIAL_ACTION_TIMEOUT_MS;
|
|
85
140
|
}
|
|
@@ -100,6 +155,10 @@ function describeActionError(action, error) {
|
|
|
100
155
|
return `goto ${JSON.stringify(action.path)} failed: ${message}`;
|
|
101
156
|
case "drag":
|
|
102
157
|
return `drag from ${describeTutorialLocator(action.source)} to ${describeTutorialLocator(action.target)} failed: ${message}`;
|
|
158
|
+
case "pointer-click":
|
|
159
|
+
return `pointer-click on ${describeTutorialLocator(action.locator)} at fraction ${describeFractionPoint(action.position)} failed: ${message}`;
|
|
160
|
+
case "pointer-drag":
|
|
161
|
+
return `pointer-drag on ${describeTutorialLocator(action.locator)} from fraction ${describeFractionPoint(action.from)} to ${describeFractionPoint(action.to)} failed: ${message}`;
|
|
103
162
|
case "click":
|
|
104
163
|
case "fill":
|
|
105
164
|
case "press":
|
|
@@ -108,3 +167,9 @@ function describeActionError(action, error) {
|
|
|
108
167
|
return `${action.type} on ${describeTutorialLocator(action.locator)} failed: ${message}`;
|
|
109
168
|
}
|
|
110
169
|
}
|
|
170
|
+
function describeFractionPoint(point) {
|
|
171
|
+
return `(${String(point.x)}, ${String(point.y)})`;
|
|
172
|
+
}
|
|
173
|
+
function messageOf(error) {
|
|
174
|
+
return error instanceof Error ? error.message : String(error);
|
|
175
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Converts a validated locator-relative fraction into page coordinates. */
|
|
2
|
+
export function resolveTutorialFractionPoint(box, point) {
|
|
3
|
+
if (![box.x, box.y, box.width, box.height].every(Number.isFinite)) {
|
|
4
|
+
throw new Error("Tutorial pointer bounding box values must all be finite.");
|
|
5
|
+
}
|
|
6
|
+
if (box.width <= 0 || box.height <= 0) {
|
|
7
|
+
throw new Error("Tutorial pointer bounding box must have positive width and height.");
|
|
8
|
+
}
|
|
9
|
+
if (!Number.isFinite(point.x) || !Number.isFinite(point.y) || point.x < 0 || point.x > 1 || point.y < 0 || point.y > 1) {
|
|
10
|
+
throw new Error("Tutorial pointer fractions must be finite numbers within inclusive bounds [0, 1].");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
x: box.x + box.width * point.x,
|
|
14
|
+
y: box.y + box.height * point.y
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { executeTutorialAction } from "./tutorialActions.js";
|
|
3
3
|
import { executeTutorialAssertion } from "./tutorialAssertions.js";
|
|
4
4
|
import { resolveTutorialLocator, describeTutorialLocator } from "./tutorialLocators.js";
|
|
5
|
+
import { resolveTutorialFractionPoint } from "./tutorialPointerGeometry.js";
|
|
5
6
|
import { buildScreenshotPath, toRunRelativePosixPath, verifyArtifactFile } from "./tutorialArtifacts.js";
|
|
6
7
|
import { CLICK_FEEDBACK_DURATION_MS, CURSOR_MOVE_DURATION_MS, centerOfBox, installTutorialCursor, moveTutorialCursor, removeTutorialVisuals, showTutorialClickFeedback } from "./tutorialCursor.js";
|
|
7
8
|
import { applyTutorialCallout, applyTutorialHighlight, clearTutorialStepOverlays, resolveCalloutPlacement } from "./tutorialOverlay.js";
|
|
@@ -209,9 +210,27 @@ async function moveCursorForAction(options, action, warnings, stepId, sleep) {
|
|
|
209
210
|
if (!box) {
|
|
210
211
|
return undefined;
|
|
211
212
|
}
|
|
212
|
-
|
|
213
|
+
let point;
|
|
214
|
+
let pointerDragEnd;
|
|
215
|
+
try {
|
|
216
|
+
if (action.type === "pointer-click") {
|
|
217
|
+
point = resolveTutorialFractionPoint(box, action.position);
|
|
218
|
+
}
|
|
219
|
+
else if (action.type === "pointer-drag") {
|
|
220
|
+
point = resolveTutorialFractionPoint(box, action.from);
|
|
221
|
+
pointerDragEnd = resolveTutorialFractionPoint(box, action.to);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
point = centerOfBox(box);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
warnings.push(`Step ${JSON.stringify(stepId)}: pointer visual for ${describeTutorialLocator(locator)} could not be planned: ${messageOf(error)}`);
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
await runVisual(warnings, stepId, "move cursor", () => moveTutorialCursor(options.page, point));
|
|
213
232
|
await sleep(CURSOR_MOVE_DURATION_MS);
|
|
214
|
-
return
|
|
233
|
+
return { point, ...(pointerDragEnd ? { pointerDragEnd } : {}) };
|
|
215
234
|
}
|
|
216
235
|
/** The locator whose center the cursor points at before each action type. */
|
|
217
236
|
export function cursorLocatorForAction(action) {
|
|
@@ -220,6 +239,8 @@ export function cursorLocatorForAction(action) {
|
|
|
220
239
|
case "fill":
|
|
221
240
|
case "press":
|
|
222
241
|
case "hover":
|
|
242
|
+
case "pointer-click":
|
|
243
|
+
case "pointer-drag":
|
|
223
244
|
return action.locator;
|
|
224
245
|
case "drag":
|
|
225
246
|
return action.source;
|
|
@@ -234,8 +255,8 @@ async function applyPostActionVisuals(options, step, cursorTarget, warnings, sle
|
|
|
234
255
|
if (!action) {
|
|
235
256
|
return;
|
|
236
257
|
}
|
|
237
|
-
if (action.type === "click" && cursorTarget) {
|
|
238
|
-
await runVisual(warnings, step.id, "click feedback", () => showTutorialClickFeedback(options.page,
|
|
258
|
+
if ((action.type === "click" || action.type === "pointer-click") && cursorTarget) {
|
|
259
|
+
await runVisual(warnings, step.id, "click feedback", () => showTutorialClickFeedback(options.page, cursorTarget.point));
|
|
239
260
|
// Held only long enough for the ripple to be visible in the recording.
|
|
240
261
|
await sleep(CLICK_FEEDBACK_DURATION_MS);
|
|
241
262
|
return;
|
|
@@ -247,6 +268,11 @@ async function applyPostActionVisuals(options, step, cursorTarget, warnings, sle
|
|
|
247
268
|
await runVisual(warnings, step.id, "move cursor to drag target", () => moveTutorialCursor(options.page, centerOfBox(box)));
|
|
248
269
|
await sleep(CURSOR_MOVE_DURATION_MS);
|
|
249
270
|
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (action.type === "pointer-drag" && cursorTarget?.pointerDragEnd) {
|
|
274
|
+
await runVisual(warnings, step.id, "move cursor to pointer-drag endpoint", () => moveTutorialCursor(options.page, cursorTarget.pointerDragEnd));
|
|
275
|
+
await sleep(CURSOR_MOVE_DURATION_MS);
|
|
250
276
|
}
|
|
251
277
|
}
|
|
252
278
|
async function applyStepOverlays(options, step, warnings) {
|
|
@@ -23,6 +23,7 @@ export const TUTORIAL_ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
|
23
23
|
/** The only placeholder a trusted target contract may use. */
|
|
24
24
|
export const TARGET_ROOT_PLACEHOLDER = "{{targetRoot}}";
|
|
25
25
|
export const TUTORIAL_LOCATOR_KINDS = ["role", "text", "css", "test-id"];
|
|
26
|
+
export const TUTORIAL_POINTER_COORDINATE_SPACES = ["fraction"];
|
|
26
27
|
export const TUTORIAL_ACTION_TYPES = [
|
|
27
28
|
"goto",
|
|
28
29
|
"click",
|
|
@@ -30,7 +31,9 @@ export const TUTORIAL_ACTION_TYPES = [
|
|
|
30
31
|
"press",
|
|
31
32
|
"hover",
|
|
32
33
|
"drag",
|
|
33
|
-
"wait-for"
|
|
34
|
+
"wait-for",
|
|
35
|
+
"pointer-click",
|
|
36
|
+
"pointer-drag"
|
|
34
37
|
];
|
|
35
38
|
export const TUTORIAL_GOTO_WAIT_UNTIL = ["load", "domcontentloaded", "networkidle"];
|
|
36
39
|
export const TUTORIAL_WAIT_FOR_STATES = ["visible", "hidden", "attached", "detached"];
|
|
@@ -1,47 +1,109 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<title>Tutorial Fixture</title>
|
|
6
|
-
<style>
|
|
7
|
-
body { font: 16px/1.5 system-ui, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #111; }
|
|
8
|
-
h1 { margin: 0 0 16px; font-size: 28px; }
|
|
9
|
-
.panel { background: #fff; border: 1px solid #d8dbe0; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
10
|
-
.row { display: flex; gap: 16px; align-items: flex-start; }
|
|
11
|
-
.col { flex: 1; }
|
|
12
|
-
button { font: inherit; padding: 8px 16px; border-radius: 6px; border: 1px solid #5a6270; background: #fff; cursor: pointer; }
|
|
13
|
-
button:hover { background: #eef1f5; }
|
|
14
|
-
input { font: inherit; padding: 8px; border: 1px solid #b8bec8; border-radius: 6px; width: 100%; box-sizing: border-box; }
|
|
15
|
-
.dropzone { min-height: 72px; border: 2px dashed #9aa3af; border-radius: 8px; padding: 12px; }
|
|
16
|
-
.card { background: #dde7ff; border: 1px solid #5f7fd6; border-radius: 6px; padding: 10px; width: 140px; }
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<
|
|
24
|
-
<div class="panel"><
|
|
25
|
-
<div class="panel"><
|
|
26
|
-
<div class="panel
|
|
27
|
-
<div class="panel"><
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
nameField
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
dragCard
|
|
41
|
-
dropZone.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Tutorial Fixture</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font: 16px/1.5 system-ui, sans-serif; margin: 0; padding: 24px; background: #f6f7f9; color: #111; }
|
|
8
|
+
h1 { margin: 0 0 16px; font-size: 28px; }
|
|
9
|
+
.panel { background: #fff; border: 1px solid #d8dbe0; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
|
10
|
+
.row { display: flex; gap: 16px; align-items: flex-start; }
|
|
11
|
+
.col { flex: 1; }
|
|
12
|
+
button { font: inherit; padding: 8px 16px; border-radius: 6px; border: 1px solid #5a6270; background: #fff; cursor: pointer; }
|
|
13
|
+
button:hover { background: #eef1f5; }
|
|
14
|
+
input { font: inherit; padding: 8px; border: 1px solid #b8bec8; border-radius: 6px; width: 100%; box-sizing: border-box; }
|
|
15
|
+
.dropzone { min-height: 72px; border: 2px dashed #9aa3af; border-radius: 8px; padding: 12px; }
|
|
16
|
+
.card { background: #dde7ff; border: 1px solid #5f7fd6; border-radius: 6px; padding: 10px; width: 140px; }
|
|
17
|
+
#pointer-surface { display: block; width: 400px; height: 240px; background: #eef4ff; box-shadow: inset 0 0 0 2px #7890c2; touch-action: none; user-select: none; }
|
|
18
|
+
.hidden { display: none; }
|
|
19
|
+
#status-banner { padding: 10px; border-radius: 6px; background: #e8f5e9; border: 1px solid #66bb6a; }
|
|
20
|
+
</style>
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
<h1 id="page-title">Tutorial Fixture</h1>
|
|
24
|
+
<div class="panel row"><div class="col"><div id="drag-card" data-testid="drag-card" class="card" draggable="true">Draggable card</div></div><div class="col"><div id="drop-zone" data-testid="drop-zone" class="dropzone">Drop target</div></div></div>
|
|
25
|
+
<div class="panel"><svg id="pointer-surface" data-testid="pointer-surface" width="400" height="240" viewBox="0 0 400 240" aria-label="Pointer gesture surface" data-pointer-click-count="0" data-last-pointer-click-zone="none" data-pointer-drag-count="0" data-pointer-drag-start-zone="none" data-pointer-drag-end-zone="none" data-pointer-drag-intermediate="false"><line id="pointer-drag-evidence" x1="0" y1="0" x2="0" y2="0" stroke="#3659a2" stroke-width="4" visibility="hidden" pointer-events="none"></line><circle id="pointer-click-evidence" cx="0" cy="0" r="8" fill="#d94f04" visibility="hidden" pointer-events="none"></circle></svg></div>
|
|
26
|
+
<div class="panel"><p id="intro-text">Welcome to the generic tutorial fixture.</p><button id="primary-action" data-testid="primary-action" data-state="idle">Activate</button> <span id="activation-count" data-testid="activation-count">0</span></div>
|
|
27
|
+
<div class="panel"><label for="name-field">Your name</label><input id="name-field" data-testid="name-field" aria-label="Your name" /><p id="greeting" data-testid="greeting"></p></div>
|
|
28
|
+
<div class="panel"><button id="hover-target" data-testid="hover-target">Hover me</button><p id="hover-readout" data-testid="hover-readout">not hovered</p></div>
|
|
29
|
+
<div class="panel"><div id="status-banner" data-testid="status-banner" class="hidden">Delayed banner is now visible</div></div>
|
|
30
|
+
<script>
|
|
31
|
+
(() => {
|
|
32
|
+
let activations = 0;
|
|
33
|
+
const primary = document.getElementById("primary-action");
|
|
34
|
+
primary.addEventListener("click", () => { activations += 1; document.getElementById("activation-count").textContent = String(activations); primary.setAttribute("data-state", "activated"); });
|
|
35
|
+
const nameField = document.getElementById("name-field");
|
|
36
|
+
const greeting = document.getElementById("greeting");
|
|
37
|
+
nameField.addEventListener("input", () => { greeting.textContent = nameField.value ? `Hello, ${nameField.value}` : ""; });
|
|
38
|
+
nameField.addEventListener("keydown", event => { if (event.key === "Enter") greeting.textContent = `Submitted: ${nameField.value}`; });
|
|
39
|
+
document.getElementById("hover-target").addEventListener("mouseenter", () => { document.getElementById("hover-readout").textContent = "hovered"; });
|
|
40
|
+
const dragCard = document.getElementById("drag-card");
|
|
41
|
+
const dropZone = document.getElementById("drop-zone");
|
|
42
|
+
dragCard.addEventListener("dragstart", event => event.dataTransfer.setData("text/plain", "drag-card"));
|
|
43
|
+
dropZone.addEventListener("dragover", event => event.preventDefault());
|
|
44
|
+
dropZone.addEventListener("drop", event => { event.preventDefault(); dropZone.textContent = "Dropped"; dropZone.setAttribute("data-dropped", "true"); });
|
|
45
|
+
const pointerSurface = document.getElementById("pointer-surface");
|
|
46
|
+
const clickEvidence = document.getElementById("pointer-click-evidence");
|
|
47
|
+
const dragEvidence = document.getElementById("pointer-drag-evidence");
|
|
48
|
+
const POINTER_CLICK_THRESHOLD = 0.01;
|
|
49
|
+
let activePointerId = null;
|
|
50
|
+
let pointerStart = null;
|
|
51
|
+
let pointerLatest = null;
|
|
52
|
+
let activeMoveCount = 0;
|
|
53
|
+
let pointerClickCount = 0;
|
|
54
|
+
let pointerDragCount = 0;
|
|
55
|
+
const fractionPoint = event => {
|
|
56
|
+
const box = pointerSurface.getBoundingClientRect();
|
|
57
|
+
return {
|
|
58
|
+
x: Math.max(0, Math.min(1, (event.clientX - box.left) / box.width)),
|
|
59
|
+
y: Math.max(0, Math.min(1, (event.clientY - box.top) / box.height))
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const zoneFor = point => point.x < 0.5
|
|
63
|
+
? (point.y < 0.5 ? "upper-left" : "lower-left")
|
|
64
|
+
: (point.y < 0.5 ? "upper-right" : "lower-right");
|
|
65
|
+
pointerSurface.addEventListener("pointerdown", event => {
|
|
66
|
+
activePointerId = event.pointerId;
|
|
67
|
+
pointerStart = fractionPoint(event);
|
|
68
|
+
pointerLatest = pointerStart;
|
|
69
|
+
activeMoveCount = 0;
|
|
70
|
+
pointerSurface.setPointerCapture(event.pointerId);
|
|
71
|
+
});
|
|
72
|
+
pointerSurface.addEventListener("pointermove", event => {
|
|
73
|
+
if (event.pointerId !== activePointerId || !pointerStart) return;
|
|
74
|
+
activeMoveCount += 1;
|
|
75
|
+
pointerLatest = fractionPoint(event);
|
|
76
|
+
});
|
|
77
|
+
pointerSurface.addEventListener("pointerup", event => {
|
|
78
|
+
if (event.pointerId !== activePointerId || !pointerStart) return;
|
|
79
|
+
const pointerEnd = fractionPoint(event);
|
|
80
|
+
const distance = Math.hypot(pointerEnd.x - pointerStart.x, pointerEnd.y - pointerStart.y);
|
|
81
|
+
if (distance <= POINTER_CLICK_THRESHOLD) {
|
|
82
|
+
pointerClickCount += 1;
|
|
83
|
+
pointerSurface.setAttribute("data-pointer-click-count", String(pointerClickCount));
|
|
84
|
+
pointerSurface.setAttribute("data-last-pointer-click-zone", zoneFor(pointerEnd));
|
|
85
|
+
clickEvidence.setAttribute("cx", String(pointerEnd.x * 400));
|
|
86
|
+
clickEvidence.setAttribute("cy", String(pointerEnd.y * 240));
|
|
87
|
+
clickEvidence.setAttribute("visibility", "visible");
|
|
88
|
+
} else {
|
|
89
|
+
pointerDragCount += 1;
|
|
90
|
+
pointerSurface.setAttribute("data-pointer-drag-count", String(pointerDragCount));
|
|
91
|
+
pointerSurface.setAttribute("data-pointer-drag-start-zone", zoneFor(pointerStart));
|
|
92
|
+
pointerSurface.setAttribute("data-pointer-drag-end-zone", zoneFor(pointerEnd));
|
|
93
|
+
pointerSurface.setAttribute("data-pointer-drag-intermediate", String(activeMoveCount >= 2));
|
|
94
|
+
dragEvidence.setAttribute("x1", String(pointerStart.x * 400));
|
|
95
|
+
dragEvidence.setAttribute("y1", String(pointerStart.y * 240));
|
|
96
|
+
dragEvidence.setAttribute("x2", String(pointerEnd.x * 400));
|
|
97
|
+
dragEvidence.setAttribute("y2", String(pointerEnd.y * 240));
|
|
98
|
+
dragEvidence.setAttribute("visibility", "visible");
|
|
99
|
+
}
|
|
100
|
+
pointerLatest = pointerEnd;
|
|
101
|
+
activePointerId = null;
|
|
102
|
+
pointerStart = null;
|
|
103
|
+
void pointerLatest;
|
|
104
|
+
});
|
|
105
|
+
window.setTimeout(() => document.getElementById("status-banner").classList.remove("hidden"), 300);
|
|
106
|
+
})();
|
|
107
|
+
</script>
|
|
108
|
+
</body>
|
|
109
|
+
</html>
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
{
|
|
2
|
-
"schemaVersion": "1.0.0",
|
|
3
|
-
"id": "lab-browser-fixture",
|
|
4
|
-
"title": "Lab browser fixture tutorial",
|
|
5
|
-
"description": "A generic, lab-owned tutorial that exercises browser actions, locators, assertions and visual declarations against a local static fixture app.",
|
|
6
|
-
"targetId": "lab-browser-fixture",
|
|
7
|
-
"browser": { "viewport": { "width": 1024, "height": 768 } },
|
|
8
|
-
"steps": [
|
|
9
|
-
{ "id": "open-app", "narration": "Open the fixture application in the browser.", "action": { "type": "goto", "path": "/", "waitUntil": "load" }, "highlight": { "kind": "css", "selector": "#page-title" }, "callout": { "text": "This is the fixture application.", "locator": { "kind": "css", "selector": "#page-title" }, "placement": "bottom" }, "screenshot": { "id": "app-open" }, "assertions": [ { "type": "url-path-equals", "expected": "/" }, { "type": "element-visible", "locator": { "kind": "role", "role": "heading", "name": "Tutorial Fixture" } }, { "type": "text-equals", "locator": { "kind": "css", "selector": "#page-title" }, "expected": "Tutorial Fixture" }, { "type": "file-exists", "path": "out/report.json" }, { "type": "json-file-equals", "path": "out/report.json", "pointer": "/counts/widgets", "expected": 3 }, { "type": "http-json-equals", "path": "/api/status", "pointer": "/ok", "expected": true } ] },
|
|
10
|
-
{ "id": "activate", "narration": "Click the primary action button to activate the panel.", "action": { "type": "click", "locator": { "kind": "test-id", "testId": "primary-action" } }, "highlight": { "kind": "test-id", "testId": "primary-action" }, "callout": { "text": "Clicking here activates the panel.", "placement": "auto", "locator": { "kind": "test-id", "testId": "primary-action" } }, "screenshot": { "id": "activated" }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "activation-count" }, "expected": "1" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "primary-action" }, "name": "data-state", "expected": "activated" } ] },
|
|
11
|
-
{ "id": "fill-name", "narration": "Type a name into the form field.", "action": { "type": "fill", "locator": { "kind": "test-id", "testId": "name-field" }, "value": "Ada" }, "assertions": [ { "type": "text-contains", "locator": { "kind": "test-id", "testId": "greeting" }, "expected": "Hello, Ada" } ] },
|
|
12
|
-
{ "id": "submit-name", "narration": "Press Enter to submit the name.", "action": { "type": "press", "locator": { "kind": "test-id", "testId": "name-field" }, "key": "Enter" }, "screenshot": { "id": "submitted" }, "assertions": [ { "type": "text-contains", "locator": { "kind": "test-id", "testId": "greeting" }, "expected": "Submitted: Ada" } ] },
|
|
13
|
-
{ "id": "hover-button", "narration": "Hover the secondary button to reveal its state.", "action": { "type": "hover", "locator": { "kind": "test-id", "testId": "hover-target" } }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "hover-readout" }, "expected": "hovered" } ] },
|
|
14
|
-
{ "id": "drag-card", "narration": "Drag the card onto the drop target.", "action": { "type": "drag", "source": { "kind": "test-id", "testId": "drag-card" }, "target": { "kind": "test-id", "testId": "drop-zone" } }, "highlight": { "kind": "test-id", "testId": "drop-zone" }, "screenshot": { "id": "dropped" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "drop-zone" }, "name": "data-dropped", "expected": "true" } ] },
|
|
15
|
-
{ "id": "
|
|
16
|
-
|
|
17
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": "1.0.0",
|
|
3
|
+
"id": "lab-browser-fixture",
|
|
4
|
+
"title": "Lab browser fixture tutorial",
|
|
5
|
+
"description": "A generic, lab-owned tutorial that exercises browser actions, locators, assertions and visual declarations against a local static fixture app.",
|
|
6
|
+
"targetId": "lab-browser-fixture",
|
|
7
|
+
"browser": { "viewport": { "width": 1024, "height": 768 } },
|
|
8
|
+
"steps": [
|
|
9
|
+
{ "id": "open-app", "narration": "Open the fixture application in the browser.", "action": { "type": "goto", "path": "/", "waitUntil": "load" }, "highlight": { "kind": "css", "selector": "#page-title" }, "callout": { "text": "This is the fixture application.", "locator": { "kind": "css", "selector": "#page-title" }, "placement": "bottom" }, "screenshot": { "id": "app-open" }, "assertions": [ { "type": "url-path-equals", "expected": "/" }, { "type": "element-visible", "locator": { "kind": "role", "role": "heading", "name": "Tutorial Fixture" } }, { "type": "text-equals", "locator": { "kind": "css", "selector": "#page-title" }, "expected": "Tutorial Fixture" }, { "type": "file-exists", "path": "out/report.json" }, { "type": "json-file-equals", "path": "out/report.json", "pointer": "/counts/widgets", "expected": 3 }, { "type": "http-json-equals", "path": "/api/status", "pointer": "/ok", "expected": true } ] },
|
|
10
|
+
{ "id": "activate", "narration": "Click the primary action button to activate the panel.", "action": { "type": "click", "locator": { "kind": "test-id", "testId": "primary-action" } }, "highlight": { "kind": "test-id", "testId": "primary-action" }, "callout": { "text": "Clicking here activates the panel.", "placement": "auto", "locator": { "kind": "test-id", "testId": "primary-action" } }, "screenshot": { "id": "activated" }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "activation-count" }, "expected": "1" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "primary-action" }, "name": "data-state", "expected": "activated" } ] },
|
|
11
|
+
{ "id": "fill-name", "narration": "Type a name into the form field.", "action": { "type": "fill", "locator": { "kind": "test-id", "testId": "name-field" }, "value": "Ada" }, "assertions": [ { "type": "text-contains", "locator": { "kind": "test-id", "testId": "greeting" }, "expected": "Hello, Ada" } ] },
|
|
12
|
+
{ "id": "submit-name", "narration": "Press Enter to submit the name.", "action": { "type": "press", "locator": { "kind": "test-id", "testId": "name-field" }, "key": "Enter" }, "screenshot": { "id": "submitted" }, "assertions": [ { "type": "text-contains", "locator": { "kind": "test-id", "testId": "greeting" }, "expected": "Submitted: Ada" } ] },
|
|
13
|
+
{ "id": "hover-button", "narration": "Hover the secondary button to reveal its state.", "action": { "type": "hover", "locator": { "kind": "test-id", "testId": "hover-target" } }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "hover-readout" }, "expected": "hovered" } ] },
|
|
14
|
+
{ "id": "drag-card", "narration": "Drag the card onto the drop target.", "action": { "type": "drag", "source": { "kind": "test-id", "testId": "drag-card" }, "target": { "kind": "test-id", "testId": "drop-zone" } }, "highlight": { "kind": "test-id", "testId": "drop-zone" }, "screenshot": { "id": "dropped" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "drop-zone" }, "name": "data-dropped", "expected": "true" } ] },
|
|
15
|
+
{ "id": "pointer-click-surface", "narration": "Click a deliberate position inside the pointer gesture surface.", "action": { "type": "pointer-click", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "position": { "x": 0.25, "y": 0.25 }, "coordinateSpace": "fraction" }, "highlight": { "kind": "test-id", "testId": "pointer-surface" }, "screenshot": { "id": "pointer-clicked" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-click-count", "expected": "1" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-last-pointer-click-zone", "expected": "upper-left" } ] },
|
|
16
|
+
{ "id": "pointer-drag-surface", "narration": "Drag across two distinct positions inside the same pointer gesture surface.", "action": { "type": "pointer-drag", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "from": { "x": 0.20, "y": 0.25 }, "to": { "x": 0.80, "y": 0.75 }, "coordinateSpace": "fraction" }, "screenshot": { "id": "pointer-dragged" }, "assertions": [ { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-count", "expected": "1" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-start-zone", "expected": "upper-left" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-end-zone", "expected": "lower-right" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "pointer-surface" }, "name": "data-pointer-drag-intermediate", "expected": "true" } ] },
|
|
17
|
+
{ "id": "wait-banner", "narration": "Wait for the delayed status banner to appear.", "action": { "type": "wait-for", "locator": { "kind": "text", "text": "Delayed banner is now visible" }, "state": "visible", "timeoutMs": 5000 }, "callout": { "text": "The banner appears after a short delay." }, "screenshot": { "id": "banner-visible" }, "assertions": [ { "type": "element-visible", "locator": { "kind": "test-id", "testId": "status-banner" } } ] }
|
|
18
|
+
]
|
|
19
|
+
}
|