@dailephd/my-dev-kit-lab 0.4.7 → 0.4.9

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 CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to my-dev-kit-lab are documented here.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.4.9] - 2026-09-20
8
+
9
+ Semantic native-select option selection for declarative browser tutorials.
10
+
11
+ - Added the value-only `select-option` declarative tutorial action, anchored to one existing `TutorialLocatorV1` and identifying a native HTML `<select>` option by its stable HTML `value`.
12
+ - Executed the action through Playwright `Locator.selectOption({ value })` using the existing tutorial timeout model, with no retry.
13
+ - Verified the returned selection exactly: the action passes only when the browser reports one selected value equal to the requested value; empty, mismatched, and multiple returned values fail.
14
+ - Added no keyboard fallback: `ArrowDown`/`Enter` emulation, click fallback, `page.evaluate`, and event dispatch are all absent, and `press` remains real keyboard input.
15
+ - Applied closed validation: `locator` and a non-empty trimmed `value` are required, timeouts use the existing contract, and unknown fields — including `label`, `index`, and `values` — are rejected rather than ignored.
16
+ - Moved the synthetic cursor to the select element before execution without fabricating a click ripple or animating the native popup menu.
17
+ - Extended the generic fixture (`examples/tutorial-browser/`) with a visible native `Operation` select whose ordinary `change` handler updates visible application state, and extended the canonical scenario to ten steps.
18
+ - Proved the action against real Chromium, including test-owned live DOM inspection of the selected value and the resulting visible state.
19
+ - Proved the action from the exact packed npm candidate installed into a clean consumer through the new `TUTORIAL_SELECT_OPTION` gate, preserving installed-package, packaged-example, and target immutability.
20
+ - Validated on Windows, macOS, and Linux across Node 24 and Node latest through the existing CI matrix.
21
+ - Confirmed bounded read-only downstream `my-frontend-observer` compatibility: representative native selects are expressible through the generic value-only contract, with no Observer values added to lab production code and no Observer files changed.
22
+ - Kept `TutorialScenarioV1`, `TutorialTargetContractV1`, `TutorialRunResultV1`, and `TutorialManifestV1` schemas at version `1.0.0`.
23
+
24
+ ## [0.4.8] - 2026-09-19
25
+
26
+ Locator-anchored pointer gestures for declarative browser tutorials.
27
+
28
+ - Added `pointer-click` and `pointer-drag` declarative tutorial actions anchored to a single `TutorialLocatorV1` interaction surface.
29
+ - Added normalized fraction coordinates (`coordinateSpace: "fraction"`) with inclusive `[0, 1]` bounds and zero-length pointer-drag rejection.
30
+ - 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.
31
+ - Preserved existing element-to-element `drag` (`source` and `target` locators via Playwright `dragTo`) and all existing scenario action/assertion behaviors without change.
32
+ - 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.
33
+ - 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.
34
+ - 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.
35
+ - 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).
36
+ - Kept `TutorialScenarioV1`, `TutorialTargetContractV1`, `TutorialRunResultV1`, and `TutorialManifestV1` schemas at version `1.0.0`.
37
+
5
38
  ## [0.4.7] - 2026-09-18
6
39
 
7
40
  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,9 @@ 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 published release is v0.4.7 (declarative browser tutorial and video automation). It adds bounded browser scenarios, persistent tutorial sessions, synchronized runtime artifacts, and a packaged generic example while preserving product-specific demo ownership outside this repository. 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).
7
+ Whole-ecosystem workflows are centralized in [my-dev-kit/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md). Its [command-surface compatibility map](https://github.com/dailephd/my-dev-kit/blob/main/docs/ECOSYSTEM_DEVELOPMENT_WORKFLOWS.md#915-command-surface-compatibility-map) documents which Lab outputs can safely seed my-dev-kit investigation, Observer reference work, or Orchestrator evidence, and which look-alike artifacts are not direct inputs.
8
+
9
+ The latest release is v0.4.9 (semantic native-select option selection for declarative browser tutorials, adding the value-only `select-option` action backed by Playwright `Locator.selectOption({ value })`). The previous release, v0.4.8, added locator-anchored pointer gestures (`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
10
 
9
11
  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
12
 
@@ -15,7 +17,7 @@ v0.4.6 adds a supported `my-dev-kit-lab` installed CLI router (`--help`, `--vers
15
17
  - **Validate CLI/package security:** inspect dependencies, package contents, path and subprocess boundaries, malformed inputs, optional static scanners, and bounded fuzz targets.
16
18
  - **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
19
  - **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.
20
+ - **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 and `press` remains real keyboard input. `select-option` performs semantic native HTML `<select>` option selection by stable HTML value, executed through Playwright `Locator.selectOption({ value })` rather than keyboard navigation. 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
21
  - **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
22
  - **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
23
  - **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 +210,9 @@ See [docs/METRICS.md](docs/METRICS.md) for full metric definitions.
208
210
  - 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
211
  - 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
212
  - 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 adds 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.
213
+ - 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.
214
+ - 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.
215
+ - v0.4.9 `select-option` identifies an option by HTML value only: label selection, index selection, multi-select arrays, and automatic selector fallback are not supported, and there is no keyboard (`ArrowDown`/`Enter`) fallback. Native popup/menu traversal is not animated. Chromium remains a separately installed local runtime prerequisite; neither package installation nor tutorial execution downloads it automatically.
212
216
 
213
217
  ---
214
218
 
@@ -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,40 @@ 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 "select-option":
270
+ // Closed to `value` alone: `label`, `index` and `values` are unknown
271
+ // fields here, so a scenario cannot reach a selection mode the runtime
272
+ // does not implement by guessing at Playwright's wider API.
273
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "value", "timeoutMs"]);
274
+ validateLocator(errors, `${location}.locator`, value.locator);
275
+ if (!isNonEmptyString(value.value)) {
276
+ errors.add(`${location}.value`, `expected a non-empty option value string after trimming, received ${describeValue(value.value)}.`);
277
+ }
278
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
279
+ break;
280
+ case "pointer-click":
281
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "position", "coordinateSpace", "timeoutMs"]);
282
+ validateLocator(errors, `${location}.locator`, value.locator);
283
+ validateFractionPoint(errors, `${location}.position`, value.position);
284
+ validatePointerCoordinateSpace(errors, `${location}.coordinateSpace`, value.coordinateSpace);
285
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
286
+ break;
287
+ case "pointer-drag": {
288
+ rejectUnknownKeys(errors, location, value, ["type", "locator", "from", "to", "coordinateSpace", "timeoutMs"]);
289
+ validateLocator(errors, `${location}.locator`, value.locator);
290
+ const fromValid = validateFractionPoint(errors, `${location}.from`, value.from);
291
+ const toValid = validateFractionPoint(errors, `${location}.to`, value.to);
292
+ validatePointerCoordinateSpace(errors, `${location}.coordinateSpace`, value.coordinateSpace);
293
+ validateOptionalTimeout(errors, `${location}.timeoutMs`, value.timeoutMs);
294
+ if (fromValid && toValid) {
295
+ const from = value.from;
296
+ const to = value.to;
297
+ if (from.x === to.x && from.y === to.y) {
298
+ errors.add(`${location}.to`, "pointer-drag endpoints must be distinct; zero-length drags are not allowed.");
299
+ }
300
+ }
301
+ break;
302
+ }
269
303
  case "wait-for":
270
304
  rejectUnknownKeys(errors, location, value, ["type", "locator", "state", "timeoutMs"]);
271
305
  validateLocator(errors, `${location}.locator`, value.locator);
@@ -276,6 +310,27 @@ export function validateAction(errors, location, value) {
276
310
  break;
277
311
  }
278
312
  }
313
+ function validateFractionPoint(errors, location, value) {
314
+ if (!isPlainObject(value)) {
315
+ errors.add(location, `expected a fraction point object, received ${describeValue(value)}.`);
316
+ return false;
317
+ }
318
+ const startLength = errors.length;
319
+ rejectUnknownKeys(errors, location, value, ["x", "y"]);
320
+ validateFractionCoordinate(errors, `${location}.x`, value.x);
321
+ validateFractionCoordinate(errors, `${location}.y`, value.y);
322
+ return errors.length === startLength;
323
+ }
324
+ function validateFractionCoordinate(errors, location, value) {
325
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) {
326
+ errors.add(location, `expected a finite number within inclusive bounds [0, 1], received ${describeValue(value)}.`);
327
+ }
328
+ }
329
+ function validatePointerCoordinateSpace(errors, location, value) {
330
+ if (!TUTORIAL_POINTER_COORDINATE_SPACES.includes(value)) {
331
+ errors.add(location, `expected "fraction", received ${describeValue(value)}.`);
332
+ }
333
+ }
279
334
  /**
280
335
  * A scenario may only navigate inside the target application's own origin, so a
281
336
  * 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,43 @@ 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
+ }
102
+ case "select-option": {
103
+ const locator = resolveTutorialLocator(page, action.locator);
104
+ const selected = await locator.selectOption({ value: action.value }, { timeout: timeoutFor(action.timeoutMs) });
105
+ // Playwright resolves to the values it actually selected. A mismatched or
106
+ // empty result means the intended option was not chosen, so it is a
107
+ // failure rather than a silently-accepted no-op; more than one value
108
+ // would mean a multi-select, which this contract does not express.
109
+ if (selected.length !== 1 || selected[0] !== action.value) {
110
+ throw new Error(`expected exactly one selected value ${JSON.stringify(action.value)}, but the select reported ${JSON.stringify(selected)}`);
111
+ }
112
+ return;
113
+ }
75
114
  case "wait-for":
76
115
  await resolveTutorialLocator(page, action.locator).waitFor({
77
116
  ...(action.state !== undefined ? { state: action.state } : {}),
@@ -80,6 +119,34 @@ async function performAction(page, action, context) {
80
119
  return;
81
120
  }
82
121
  }
122
+ async function runWithMouseDown(mouse, operation) {
123
+ await mouse.down();
124
+ let primaryError;
125
+ let primaryFailed = false;
126
+ try {
127
+ await operation();
128
+ }
129
+ catch (error) {
130
+ primaryFailed = true;
131
+ primaryError = error;
132
+ }
133
+ try {
134
+ await mouse.up();
135
+ }
136
+ catch (cleanupError) {
137
+ if (primaryFailed) {
138
+ const primaryMessage = messageOf(primaryError);
139
+ const cleanupMessage = messageOf(cleanupError);
140
+ throw new Error(`${primaryMessage} (mouse.up cleanup also failed: ${cleanupMessage})`, {
141
+ cause: primaryError
142
+ });
143
+ }
144
+ throw cleanupError;
145
+ }
146
+ if (primaryFailed) {
147
+ throw primaryError;
148
+ }
149
+ }
83
150
  export function timeoutFor(timeoutMs) {
84
151
  return timeoutMs ?? DEFAULT_TUTORIAL_ACTION_TIMEOUT_MS;
85
152
  }
@@ -100,6 +167,12 @@ function describeActionError(action, error) {
100
167
  return `goto ${JSON.stringify(action.path)} failed: ${message}`;
101
168
  case "drag":
102
169
  return `drag from ${describeTutorialLocator(action.source)} to ${describeTutorialLocator(action.target)} failed: ${message}`;
170
+ case "pointer-click":
171
+ return `pointer-click on ${describeTutorialLocator(action.locator)} at fraction ${describeFractionPoint(action.position)} failed: ${message}`;
172
+ case "pointer-drag":
173
+ return `pointer-drag on ${describeTutorialLocator(action.locator)} from fraction ${describeFractionPoint(action.from)} to ${describeFractionPoint(action.to)} failed: ${message}`;
174
+ case "select-option":
175
+ return `select-option on ${describeTutorialLocator(action.locator)} for value ${JSON.stringify(action.value)} failed: ${message}`;
103
176
  case "click":
104
177
  case "fill":
105
178
  case "press":
@@ -108,3 +181,9 @@ function describeActionError(action, error) {
108
181
  return `${action.type} on ${describeTutorialLocator(action.locator)} failed: ${message}`;
109
182
  }
110
183
  }
184
+ function describeFractionPoint(point) {
185
+ return `(${String(point.x)}, ${String(point.y)})`;
186
+ }
187
+ function messageOf(error) {
188
+ return error instanceof Error ? error.message : String(error);
189
+ }
@@ -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
- await runVisual(warnings, stepId, "move cursor", () => moveTutorialCursor(options.page, centerOfBox(box)));
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 box;
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,11 @@ export function cursorLocatorForAction(action) {
220
239
  case "fill":
221
240
  case "press":
222
241
  case "hover":
242
+ // The cursor rests on the select itself; the native popup is OS-drawn and
243
+ // is deliberately not animated through.
244
+ case "select-option":
245
+ case "pointer-click":
246
+ case "pointer-drag":
223
247
  return action.locator;
224
248
  case "drag":
225
249
  return action.source;
@@ -234,8 +258,8 @@ async function applyPostActionVisuals(options, step, cursorTarget, warnings, sle
234
258
  if (!action) {
235
259
  return;
236
260
  }
237
- if (action.type === "click" && cursorTarget) {
238
- await runVisual(warnings, step.id, "click feedback", () => showTutorialClickFeedback(options.page, centerOfBox(cursorTarget)));
261
+ if ((action.type === "click" || action.type === "pointer-click") && cursorTarget) {
262
+ await runVisual(warnings, step.id, "click feedback", () => showTutorialClickFeedback(options.page, cursorTarget.point));
239
263
  // Held only long enough for the ripple to be visible in the recording.
240
264
  await sleep(CLICK_FEEDBACK_DURATION_MS);
241
265
  return;
@@ -247,6 +271,11 @@ async function applyPostActionVisuals(options, step, cursorTarget, warnings, sle
247
271
  await runVisual(warnings, step.id, "move cursor to drag target", () => moveTutorialCursor(options.page, centerOfBox(box)));
248
272
  await sleep(CURSOR_MOVE_DURATION_MS);
249
273
  }
274
+ return;
275
+ }
276
+ if (action.type === "pointer-drag" && cursorTarget?.pointerDragEnd) {
277
+ await runVisual(warnings, step.id, "move cursor to pointer-drag endpoint", () => moveTutorialCursor(options.page, cursorTarget.pointerDragEnd));
278
+ await sleep(CURSOR_MOVE_DURATION_MS);
250
279
  }
251
280
  }
252
281
  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,10 @@ export const TUTORIAL_ACTION_TYPES = [
30
31
  "press",
31
32
  "hover",
32
33
  "drag",
33
- "wait-for"
34
+ "select-option",
35
+ "wait-for",
36
+ "pointer-click",
37
+ "pointer-drag"
34
38
  ];
35
39
  export const TUTORIAL_GOTO_WAIT_UNTIL = ["load", "domcontentloaded", "networkidle"];
36
40
  export const TUTORIAL_WAIT_FOR_STATES = ["visible", "hidden", "attached", "detached"];
@@ -1,47 +1,114 @@
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
- .hidden { display: none; }
18
- #status-banner { padding: 10px; border-radius: 6px; background: #e8f5e9; border: 1px solid #66bb6a; }
19
- </style>
20
- </head>
21
- <body>
22
- <h1 id="page-title">Tutorial Fixture</h1>
23
- <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>
24
- <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>
25
- <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>
26
- <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>
27
- <div class="panel"><div id="status-banner" data-testid="status-banner" class="hidden">Delayed banner is now visible</div></div>
28
- <script>
29
- (() => {
30
- let activations = 0;
31
- const primary = document.getElementById("primary-action");
32
- primary.addEventListener("click", () => { activations += 1; document.getElementById("activation-count").textContent = String(activations); primary.setAttribute("data-state", "activated"); });
33
- const nameField = document.getElementById("name-field");
34
- const greeting = document.getElementById("greeting");
35
- nameField.addEventListener("input", () => { greeting.textContent = nameField.value ? `Hello, ${nameField.value}` : ""; });
36
- nameField.addEventListener("keydown", event => { if (event.key === "Enter") greeting.textContent = `Submitted: ${nameField.value}`; });
37
- document.getElementById("hover-target").addEventListener("mouseenter", () => { document.getElementById("hover-readout").textContent = "hovered"; });
38
- const dragCard = document.getElementById("drag-card");
39
- const dropZone = document.getElementById("drop-zone");
40
- dragCard.addEventListener("dragstart", event => event.dataTransfer.setData("text/plain", "drag-card"));
41
- dropZone.addEventListener("dragover", event => event.preventDefault());
42
- dropZone.addEventListener("drop", event => { event.preventDefault(); dropZone.textContent = "Dropped"; dropZone.setAttribute("data-dropped", "true"); });
43
- window.setTimeout(() => document.getElementById("status-banner").classList.remove("hidden"), 300);
44
- })();
45
- </script>
46
- </body>
47
- </html>
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
+ select { font: inherit; padding: 8px; border: 1px solid #b8bec8; border-radius: 6px; }
16
+ .dropzone { min-height: 72px; border: 2px dashed #9aa3af; border-radius: 8px; padding: 12px; }
17
+ .card { background: #dde7ff; border: 1px solid #5f7fd6; border-radius: 6px; padding: 10px; width: 140px; }
18
+ #pointer-surface { display: block; width: 400px; height: 240px; background: #eef4ff; box-shadow: inset 0 0 0 2px #7890c2; touch-action: none; user-select: none; }
19
+ .hidden { display: none; }
20
+ #status-banner { padding: 10px; border-radius: 6px; background: #e8f5e9; border: 1px solid #66bb6a; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <h1 id="page-title">Tutorial Fixture</h1>
25
+ <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>
26
+ <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>
27
+ <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>
28
+ <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>
29
+ <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>
30
+ <div class="panel"><label for="operation-select">Operation</label> <select id="operation-select" data-testid="operation-select" aria-label="Operation"><option value="inspect">Inspect</option><option value="move">Move</option><option value="resize">Resize</option><option value="remove">Remove</option><option value="preserve">Preserve</option></select> <span id="selected-operation" data-testid="selected-operation" data-selected-value="inspect">inspect</span></div>
31
+ <div class="panel"><div id="status-banner" data-testid="status-banner" class="hidden">Delayed banner is now visible</div></div>
32
+ <script>
33
+ (() => {
34
+ let activations = 0;
35
+ const primary = document.getElementById("primary-action");
36
+ primary.addEventListener("click", () => { activations += 1; document.getElementById("activation-count").textContent = String(activations); primary.setAttribute("data-state", "activated"); });
37
+ const nameField = document.getElementById("name-field");
38
+ const greeting = document.getElementById("greeting");
39
+ nameField.addEventListener("input", () => { greeting.textContent = nameField.value ? `Hello, ${nameField.value}` : ""; });
40
+ nameField.addEventListener("keydown", event => { if (event.key === "Enter") greeting.textContent = `Submitted: ${nameField.value}`; });
41
+ document.getElementById("hover-target").addEventListener("mouseenter", () => { document.getElementById("hover-readout").textContent = "hovered"; });
42
+ const operationSelect = document.getElementById("operation-select");
43
+ const selectedOperation = document.getElementById("selected-operation");
44
+ operationSelect.addEventListener("change", () => { selectedOperation.textContent = operationSelect.value; selectedOperation.setAttribute("data-selected-value", operationSelect.value); });
45
+ const dragCard = document.getElementById("drag-card");
46
+ const dropZone = document.getElementById("drop-zone");
47
+ dragCard.addEventListener("dragstart", event => event.dataTransfer.setData("text/plain", "drag-card"));
48
+ dropZone.addEventListener("dragover", event => event.preventDefault());
49
+ dropZone.addEventListener("drop", event => { event.preventDefault(); dropZone.textContent = "Dropped"; dropZone.setAttribute("data-dropped", "true"); });
50
+ const pointerSurface = document.getElementById("pointer-surface");
51
+ const clickEvidence = document.getElementById("pointer-click-evidence");
52
+ const dragEvidence = document.getElementById("pointer-drag-evidence");
53
+ const POINTER_CLICK_THRESHOLD = 0.01;
54
+ let activePointerId = null;
55
+ let pointerStart = null;
56
+ let pointerLatest = null;
57
+ let activeMoveCount = 0;
58
+ let pointerClickCount = 0;
59
+ let pointerDragCount = 0;
60
+ const fractionPoint = event => {
61
+ const box = pointerSurface.getBoundingClientRect();
62
+ return {
63
+ x: Math.max(0, Math.min(1, (event.clientX - box.left) / box.width)),
64
+ y: Math.max(0, Math.min(1, (event.clientY - box.top) / box.height))
65
+ };
66
+ };
67
+ const zoneFor = point => point.x < 0.5
68
+ ? (point.y < 0.5 ? "upper-left" : "lower-left")
69
+ : (point.y < 0.5 ? "upper-right" : "lower-right");
70
+ pointerSurface.addEventListener("pointerdown", event => {
71
+ activePointerId = event.pointerId;
72
+ pointerStart = fractionPoint(event);
73
+ pointerLatest = pointerStart;
74
+ activeMoveCount = 0;
75
+ pointerSurface.setPointerCapture(event.pointerId);
76
+ });
77
+ pointerSurface.addEventListener("pointermove", event => {
78
+ if (event.pointerId !== activePointerId || !pointerStart) return;
79
+ activeMoveCount += 1;
80
+ pointerLatest = fractionPoint(event);
81
+ });
82
+ pointerSurface.addEventListener("pointerup", event => {
83
+ if (event.pointerId !== activePointerId || !pointerStart) return;
84
+ const pointerEnd = fractionPoint(event);
85
+ const distance = Math.hypot(pointerEnd.x - pointerStart.x, pointerEnd.y - pointerStart.y);
86
+ if (distance <= POINTER_CLICK_THRESHOLD) {
87
+ pointerClickCount += 1;
88
+ pointerSurface.setAttribute("data-pointer-click-count", String(pointerClickCount));
89
+ pointerSurface.setAttribute("data-last-pointer-click-zone", zoneFor(pointerEnd));
90
+ clickEvidence.setAttribute("cx", String(pointerEnd.x * 400));
91
+ clickEvidence.setAttribute("cy", String(pointerEnd.y * 240));
92
+ clickEvidence.setAttribute("visibility", "visible");
93
+ } else {
94
+ pointerDragCount += 1;
95
+ pointerSurface.setAttribute("data-pointer-drag-count", String(pointerDragCount));
96
+ pointerSurface.setAttribute("data-pointer-drag-start-zone", zoneFor(pointerStart));
97
+ pointerSurface.setAttribute("data-pointer-drag-end-zone", zoneFor(pointerEnd));
98
+ pointerSurface.setAttribute("data-pointer-drag-intermediate", String(activeMoveCount >= 2));
99
+ dragEvidence.setAttribute("x1", String(pointerStart.x * 400));
100
+ dragEvidence.setAttribute("y1", String(pointerStart.y * 240));
101
+ dragEvidence.setAttribute("x2", String(pointerEnd.x * 400));
102
+ dragEvidence.setAttribute("y2", String(pointerEnd.y * 240));
103
+ dragEvidence.setAttribute("visibility", "visible");
104
+ }
105
+ pointerLatest = pointerEnd;
106
+ activePointerId = null;
107
+ pointerStart = null;
108
+ void pointerLatest;
109
+ });
110
+ window.setTimeout(() => document.getElementById("status-banner").classList.remove("hidden"), 300);
111
+ })();
112
+ </script>
113
+ </body>
114
+ </html>
@@ -1,17 +1,20 @@
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": "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" } } ] }
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": "select-operation", "narration": "Select the preserve operation from the native selector.", "action": { "type": "select-option", "locator": { "kind": "role", "role": "combobox", "name": "Operation" }, "value": "preserve" }, "highlight": { "kind": "role", "role": "combobox", "name": "Operation" }, "screenshot": { "id": "operation-selected" }, "assertions": [ { "type": "text-equals", "locator": { "kind": "test-id", "testId": "selected-operation" }, "expected": "preserve" }, { "type": "attribute-equals", "locator": { "kind": "test-id", "testId": "selected-operation" }, "name": "data-selected-value", "expected": "preserve" } ] },
18
+ { "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" } } ] }
19
+ ]
20
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {