@dailephd/my-dev-kit-lab 0.4.6 → 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 +25 -2
- package/README.md +8 -1
- package/dist/src/browser/index.js +2 -0
- package/dist/src/browser/playwrightRuntime.js +74 -0
- package/dist/src/browser/types.js +10 -0
- package/dist/src/cli/help.js +76 -0
- package/dist/src/cli/runLabCli.js +29 -1
- package/dist/src/commands/runTutorialRunCommand.js +170 -0
- package/dist/src/commands/runTutorialValidateCommand.js +138 -0
- package/dist/src/core/processTree.js +52 -0
- package/dist/src/core/resolveCommand.js +34 -0
- package/dist/src/core/runMeasuredCommand.js +39 -31
- package/dist/src/runtime/index.js +1 -0
- package/dist/src/runtime/managedProcess.js +380 -0
- package/dist/src/screenshot/captureReportScreenshot.js +30 -27
- package/dist/src/tutorial/index.js +17 -0
- package/dist/src/tutorial/loadTutorialContracts.js +60 -0
- package/dist/src/tutorial/markdownTutorialWriter.js +53 -0
- package/dist/src/tutorial/runTutorial.js +465 -0
- package/dist/src/tutorial/scenarioValidation.js +427 -0
- package/dist/src/tutorial/subtitleWriter.js +88 -0
- package/dist/src/tutorial/targetContractValidation.js +212 -0
- package/dist/src/tutorial/tutorialActions.js +175 -0
- package/dist/src/tutorial/tutorialArtifacts.js +176 -0
- package/dist/src/tutorial/tutorialAssertions.js +249 -0
- package/dist/src/tutorial/tutorialCursor.js +149 -0
- package/dist/src/tutorial/tutorialLocators.js +37 -0
- package/dist/src/tutorial/tutorialManifest.js +74 -0
- package/dist/src/tutorial/tutorialOverlay.js +180 -0
- package/dist/src/tutorial/tutorialPaths.js +86 -0
- package/dist/src/tutorial/tutorialPointerGeometry.js +16 -0
- package/dist/src/tutorial/tutorialSession.js +424 -0
- package/dist/src/tutorial/types.js +51 -0
- package/dist/src/tutorial/validationPrimitives.js +141 -0
- package/examples/tutorial-browser/index.html +109 -0
- package/examples/tutorial-browser/prepare.mjs +14 -0
- package/examples/tutorial-browser/scenario.json +19 -0
- package/examples/tutorial-browser/server.mjs +14 -0
- package/package.json +8 -4
|
@@ -0,0 +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
|
+
#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>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const sourceRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const targetRoot = process.argv[2];
|
|
7
|
+
if (!targetRoot) {
|
|
8
|
+
console.error("prepare requires a target root argument");
|
|
9
|
+
process.exit(2);
|
|
10
|
+
}
|
|
11
|
+
fs.mkdirSync(path.join(targetRoot, "app"), { recursive: true });
|
|
12
|
+
fs.mkdirSync(path.join(targetRoot, "out"), { recursive: true });
|
|
13
|
+
for (const file of ["index.html", "server.mjs"]) fs.copyFileSync(path.join(sourceRoot, file), path.join(targetRoot, "app", file));
|
|
14
|
+
fs.writeFileSync(path.join(targetRoot, "out", "report.json"), JSON.stringify({ prepared: true, counts: { widgets: 3 } }, null, 2), "utf8");
|
|
@@ -0,0 +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": "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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const port = Number(process.env.TUTORIAL_FIXTURE_PORT || 0);
|
|
8
|
+
const server = http.createServer((req, res) => {
|
|
9
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
10
|
+
if (url.pathname === "/api/status") { res.writeHead(200, { "content-type": "application/json" }); res.end(JSON.stringify({ ok: true, widgets: 3 })); return; }
|
|
11
|
+
const filePath = url.pathname === "/" ? path.join(root, "index.html") : path.join(root, url.pathname);
|
|
12
|
+
fs.readFile(filePath, (error, contents) => { if (error) { res.writeHead(404); res.end("not found"); return; } res.writeHead(200, { "content-type": filePath.endsWith(".html") ? "text/html" : "application/octet-stream" }); res.end(contents); });
|
|
13
|
+
});
|
|
14
|
+
server.listen(port, "127.0.0.1", () => console.log("tutorial fixture listening on " + server.address().port));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dailephd/my-dev-kit-lab",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"dist/src/",
|
|
14
14
|
"benchmarks/",
|
|
15
15
|
"docs/METRICS.md",
|
|
16
|
-
"examples/token-savings-cases.json"
|
|
16
|
+
"examples/token-savings-cases.json",
|
|
17
|
+
"examples/tutorial-browser/"
|
|
17
18
|
],
|
|
18
19
|
"repository": {
|
|
19
20
|
"type": "git",
|
|
@@ -79,17 +80,20 @@
|
|
|
79
80
|
"security:codeql": "tsx scripts/security/runCodeql.ts",
|
|
80
81
|
"security:semgrep": "tsx scripts/security/runSemgrep.ts",
|
|
81
82
|
"test:security": "vitest run tests/security/",
|
|
83
|
+
"test:tutorial-browser": "vitest run tests/integration/tutorialRealBrowser.spec.ts",
|
|
82
84
|
"test:fuzz:smoke": "tsx scripts/security/runFuzzSmoke.ts",
|
|
83
85
|
"security:validate": "tsx scripts/security/validate.ts",
|
|
84
86
|
"audit": "tsx scripts/audits/runAudit.ts",
|
|
85
87
|
"report:context-integrity-smoke": "tsx scripts/report-context-integrity-smoke.ts"
|
|
86
88
|
},
|
|
89
|
+
"dependencies": {
|
|
90
|
+
"playwright": "1.60.0"
|
|
91
|
+
},
|
|
87
92
|
"devDependencies": {
|
|
88
93
|
"@types/node": "^24.0.0",
|
|
89
|
-
"playwright": "^1.54.1",
|
|
90
94
|
"tsx": "^4.20.3",
|
|
91
95
|
"typescript": "^5.8.3",
|
|
92
|
-
"vitest": "^
|
|
96
|
+
"vitest": "^4.1.11"
|
|
93
97
|
},
|
|
94
98
|
"overrides": {
|
|
95
99
|
"vite": "7.3.6"
|