Skip to content

Commit 9ba59c1

Browse files
author
DavidQ
committed
Add first-class tool Playwright depth guidance and deprecate tools/shared usage in templates - PR_26126_065-tool-template-playwright-e2e-depth
1 parent 632be91 commit 9ba59c1

7 files changed

Lines changed: 234 additions & 13 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# PR_26126_065 Tool Template Playwright E2E Depth
2+
3+
Scope:
4+
- Updated tools/templates/first-class-tool-starter only.
5+
- Added required review reports under docs/dev/reports.
6+
- Preserved runtime behavior outside tools/templates.
7+
8+
Template updates:
9+
- Updated index.html header markup to follow the Palette Manager V2 pattern: body.tools-platform-tool-page, collapsible header/details, #shared-theme-header, and a tool-local header host.
10+
- Added template-local Playwright config at tools/templates/first-class-tool-starter/playwright.config.mjs.
11+
- Added starter Playwright behavior coverage at tools/templates/first-class-tool-starter/tests/playwright/FirstClassToolStarter.spec.mjs.
12+
- Updated README Playwright guidance to require meaningful behavior validation instead of page-load-only checks.
13+
- Updated README and contracts to explicitly document tools/shared/ as deprecated for first-class tools: no imports, no script references, no CSS references, no runtime dependency.
14+
- Kept existing external CSS and external JavaScript module structure; no inline handlers were added.
15+
16+
Template Playwright coverage demonstrates:
17+
- tool launch
18+
- Palette Manager-style header shell presence
19+
- accordion expand/collapse behavior
20+
- primary action button state transition when required input becomes valid
21+
- missing input/failure state
22+
- status/log clear behavior
23+
24+
Validation:
25+
- Playwright impacted: No for active runtime.
26+
- No active runtime/workspace/toolState behavior changed; this PR changes starter template files only.
27+
- Ran template-local Playwright validation: npx playwright test --config tools/templates/first-class-tool-starter/playwright.config.mjs --reporter=list -> 5 passed.
28+
- Syntax checked all first-class-tool-starter JavaScript and Playwright files with node --check.
29+
- Verified template HTML has no inline script blocks, style blocks, or inline event handlers.
30+
- Verified docs clearly state tools/shared/ deprecation.
31+
- Verified no tracked roadmap, sample, start_of_day, games, src, tools/shared, package, or package-lock files changed.
32+
- Did not run npm run test:workspace-v2 because active workspace/tool runtime was not impacted.
33+
34+
Manual test notes:
35+
- Run npx playwright test --config tools/templates/first-class-tool-starter/playwright.config.mjs --reporter=list and confirm all starter behavior tests pass.
36+
- Open tools/templates/first-class-tool-starter/index.html through an HTTP server and confirm header, accordions, Run state, status logging, and Clear still behave as shown by the starter tests.

tools/templates/first-class-tool-starter/README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Copy this folder to `tools/<tool-id>/`, then rename the class prefixes, visible
99
```text
1010
tools/<tool-id>/
1111
index.html
12+
playwright.config.mjs
1213
README.md
1314
docs/
1415
CONTROL_SERVICE_CONTRACTS.md
@@ -26,17 +27,22 @@ tools/<tool-id>/
2627
StatusLogControl.js
2728
services/
2829
ToolStateSerializer.js
30+
tests/
31+
playwright/
32+
FirstClassToolStarter.spec.mjs
2933
```
3034

3135
## Required Files
3236

3337
- `index.html`: semantic tool shell with no inline `<script>`, no inline `<style>`, and no inline event handlers.
38+
- `playwright.config.mjs`: template-local Playwright config for validating the copied starter before registry integration.
3439
- `styles/*.css`: all tool styles.
3540
- `js/bootstrap.js`: small startup file that creates class instances and wires dependencies.
3641
- `js/ToolStarterApp.js`: app/root coordinator only.
3742
- `js/controls/*.js`: one class per UI control or section.
3843
- `js/services/*.js`: focused non-UI helper classes when needed.
3944
- `docs/CONTROL_SERVICE_CONTRACTS.md`: required control, service, app/root, logger, and batch processor contracts.
45+
- `tests/playwright/*.spec.mjs`: starter behavior coverage to copy and rename with the new tool.
4046
- `README.md`: tool-specific usage, contracts, and validation notes.
4147

4248
## Required Contracts
@@ -55,13 +61,18 @@ The contracts define:
5561

5662
- One class per file.
5763
- One control or section per class.
64+
- Header markup follows the Palette Manager V2 pattern: `body.tools-platform-tool-page`, collapsible header/details, `#shared-theme-header`, and a tool-local header host.
5865
- App/root class coordinates only and must not own DOM logic or business logic.
5966
- Controls own their DOM and their events.
6067
- Controls communicate through injected callbacks or the app coordinator.
6168
- Services contain non-DOM logic and return results/errors for the app, controls, or logger to display.
6269
- Logger is the single writer for status/log output.
6370
- Reusable UI behavior must live in reusable classes such as `AccordionSection`.
64-
- Do not depend on `tools/shared`.
71+
- `tools/shared/` is deprecated for first-class tools:
72+
- no imports
73+
- no script references
74+
- no CSS references
75+
- no runtime dependency
6576
- Do not use inline event handlers such as `onclick`, `onchange`, or `oninput`.
6677
- Do not add hidden defaults or silent fallback data.
6778

@@ -80,18 +91,28 @@ The contracts define:
8091

8192
Every new first-class tool must include Playwright coverage that launches the tool and validates meaningful behavior.
8293

94+
The starter includes `tests/playwright/FirstClassToolStarter.spec.mjs` as a copyable baseline. Rename it with the new tool and keep the behavior depth.
95+
8396
Minimum coverage:
8497

8598
- tool page loads without runtime errors
99+
- Palette Manager-style header shell is present
86100
- primary user action can be triggered or correctly blocked
87-
- required controls render and respond
88101
- accordion sections expand and collapse
102+
- primary action button changes state when required input becomes valid
103+
- status/log clear behavior works
89104
- at least one failure state is visible when invalid input is applicable
90105

91106
The required validation command for impacted tool runtime or UI work is:
92107

93108
`npm run test:workspace-v2`
94109

110+
When validating only the copied starter before registry integration, run the copied tool's Playwright spec directly and then add it to the relevant workspace/tool test lane.
111+
112+
Template-local command:
113+
114+
`npx playwright test --config tools/<tool-id>/playwright.config.mjs`
115+
95116
## Review Artifacts
96117

97118
Every PR that creates or changes a first-class tool must produce:

tools/templates/first-class-tool-starter/docs/CONTROL_SERVICE_CONTRACTS.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
These contracts define the required structure for first-class tool code created from this starter.
44

5+
## Deprecated Shared Tool Surface
6+
7+
`tools/shared/` is deprecated for first-class tools.
8+
9+
First-class tools must have:
10+
11+
- no imports from `tools/shared/`
12+
- no script references to `tools/shared/`
13+
- no CSS references to `tools/shared/`
14+
- no runtime dependency on `tools/shared/`
15+
516
## Control Contract
617

718
Controls own one UI control or one UI section.
@@ -146,5 +157,4 @@ Before a first-class tool PR is ready:
146157
- Batch work logs per item and summarizes counts.
147158
- No inline event handlers exist in HTML.
148159
- No inline `<script>` or `<style>` blocks exist in HTML.
149-
- No `tools/shared` dependency exists.
150-
160+
- No `tools/shared/` dependency exists.

tools/templates/first-class-tool-starter/index.html

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@
77
<link rel="stylesheet" href="./styles/toolStarter.css">
88
<script type="module" src="./js/bootstrap.js"></script>
99
</head>
10-
<body>
11-
<header id="shared-theme-header" class="tool-starter__header">
12-
<div>
13-
<h1>First-Class Tool Starter</h1>
14-
<p>Replace this shell with the new tool name and purpose.</p>
10+
<body class="tools-platform-tool-page hub-page-tools" data-tool-id="first-class-tool-starter">
11+
<details class="is-collapsible" open>
12+
<summary class="is-collapsible__summary" data-tool-starter-summary>Hide Header and Details</summary>
13+
<div class="is-collapsible__content">
14+
<div id="shared-theme-header"></div>
15+
<div class="tool-starter__header" data-tool-starter-header>
16+
<div>
17+
<h1>First-Class Tool Starter</h1>
18+
<p>Replace this shell with the new tool name and purpose.</p>
19+
</div>
20+
<p class="tool-starter__header-meta">Self-contained starter</p>
21+
</div>
1522
</div>
16-
<p class="tool-starter__header-meta">Self-contained starter</p>
17-
</header>
23+
</details>
1824

1925
<nav class="tool-starter__menu" aria-label="Tool actions">
2026
<button id="runToolButton" type="button">Run</button>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export default {
2+
testDir: "./tests/playwright",
3+
timeout: 30000,
4+
outputDir: "./tests/results",
5+
use: {
6+
headless: true,
7+
trace: "retain-on-failure"
8+
}
9+
};
10+

tools/templates/first-class-tool-starter/styles/toolStarter.css

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,36 @@ textarea:focus-visible {
4949
outline-offset: 2px;
5050
}
5151

52+
.is-collapsible {
53+
border-bottom: 1px solid var(--tool-starter-line);
54+
background: #151a24;
55+
}
56+
57+
.is-collapsible__summary {
58+
min-height: 36px;
59+
display: flex;
60+
align-items: center;
61+
cursor: pointer;
62+
color: var(--tool-starter-muted);
63+
font-weight: 700;
64+
padding: 8px 18px;
65+
}
66+
67+
.is-collapsible__content {
68+
display: grid;
69+
gap: 0;
70+
}
71+
72+
#shared-theme-header {
73+
min-height: 0;
74+
}
75+
5276
.tool-starter__header {
5377
min-height: 76px;
5478
display: flex;
5579
align-items: center;
5680
justify-content: space-between;
5781
gap: 16px;
58-
border-bottom: 1px solid var(--tool-starter-line);
5982
background: #151a24;
6083
padding: 14px 18px;
6184
}
@@ -273,4 +296,3 @@ textarea:focus-visible {
273296
text-align: left;
274297
}
275298
}
276-
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { expect, test } from "@playwright/test";
2+
import { createServer } from "node:http";
3+
import { extname, join, normalize, relative, resolve } from "node:path";
4+
import { readFile, stat } from "node:fs/promises";
5+
import { fileURLToPath } from "node:url";
6+
7+
const STARTER_ROOT = resolve(fileURLToPath(new URL("../..", import.meta.url)));
8+
const CONTENT_TYPES = new Map([
9+
[".css", "text/css; charset=utf-8"],
10+
[".html", "text/html; charset=utf-8"],
11+
[".js", "text/javascript; charset=utf-8"]
12+
]);
13+
14+
let server;
15+
let starterUrl;
16+
17+
async function startStaticServer() {
18+
server = createServer(async (request, response) => {
19+
try {
20+
const requestPath = request.url === "/" ? "/index.html" : new URL(request.url, "http://127.0.0.1").pathname;
21+
const filePath = normalize(join(STARTER_ROOT, decodeURIComponent(requestPath)));
22+
const relativePath = relative(STARTER_ROOT, filePath);
23+
if (relativePath.startsWith("..")) {
24+
response.writeHead(403);
25+
response.end("Forbidden");
26+
return;
27+
}
28+
29+
const fileStat = await stat(filePath);
30+
if (!fileStat.isFile()) {
31+
response.writeHead(404);
32+
response.end("Not found");
33+
return;
34+
}
35+
36+
const contentType = CONTENT_TYPES.get(extname(filePath)) || "application/octet-stream";
37+
response.writeHead(200, { "content-type": contentType });
38+
response.end(await readFile(filePath));
39+
} catch {
40+
response.writeHead(404);
41+
response.end("Not found");
42+
}
43+
});
44+
45+
await new Promise((resolveServer) => {
46+
server.listen(0, "127.0.0.1", resolveServer);
47+
});
48+
const address = server.address();
49+
starterUrl = `http://127.0.0.1:${address.port}/index.html`;
50+
}
51+
52+
test.describe("First-Class Tool Starter", () => {
53+
test.beforeAll(async () => {
54+
await startStaticServer();
55+
});
56+
57+
test.afterAll(async () => {
58+
await new Promise((resolveServer) => server.close(resolveServer));
59+
});
60+
61+
test.beforeEach(async ({ page }) => {
62+
await page.goto(starterUrl);
63+
});
64+
65+
test("launches with Palette Manager-style header shell", async ({ page }) => {
66+
await expect(page.locator("body.tools-platform-tool-page[data-tool-id='first-class-tool-starter']")).toBeVisible();
67+
await expect(page.locator(".is-collapsible")).toBeVisible();
68+
await expect(page.locator("#shared-theme-header")).toBeAttached();
69+
await expect(page.locator("[data-tool-starter-header]")).toContainText("First-Class Tool Starter");
70+
await expect(page.locator("[data-tool-starter-summary]")).toContainText("Hide Header and Details");
71+
});
72+
73+
test("accordion sections collapse and expand", async ({ page }) => {
74+
const sourceHeader = page.getByRole("button", { name: "Input Source" });
75+
const sourceContent = page.locator("#sourceInputContent");
76+
77+
await expect(sourceContent).toBeVisible();
78+
await sourceHeader.click();
79+
await expect(sourceContent).toBeHidden();
80+
await expect(sourceHeader).toHaveAttribute("aria-expanded", "false");
81+
await sourceHeader.click();
82+
await expect(sourceContent).toBeVisible();
83+
await expect(sourceHeader).toHaveAttribute("aria-expanded", "true");
84+
});
85+
86+
test("primary action state changes when required input is valid", async ({ page }) => {
87+
const runButton = page.locator("#runToolButton");
88+
const exportButton = page.locator("#exportToolStateButton");
89+
const sourceInput = page.locator("#sourceInput");
90+
91+
await expect(runButton).toBeDisabled();
92+
await expect(exportButton).toBeDisabled();
93+
await sourceInput.fill("starter value");
94+
await expect(runButton).toBeEnabled();
95+
await expect(exportButton).toBeEnabled();
96+
});
97+
98+
test("missing input failure state is visible", async ({ page }) => {
99+
const sourceInput = page.locator("#sourceInput");
100+
const runButton = page.locator("#runToolButton");
101+
102+
await sourceInput.fill("starter value");
103+
await sourceInput.fill("");
104+
await expect(runButton).toBeDisabled();
105+
await expect(page.locator("#sourceValidationMessage")).toContainText("Input is required");
106+
});
107+
108+
test("status log clear behavior works after primary action", async ({ page }) => {
109+
await page.locator("#sourceInput").fill("starter value");
110+
await page.locator("#runToolButton").click();
111+
await expect(page.locator("#statusLog")).toHaveValue(/Processed source value/);
112+
113+
await page.locator("#clearStatusButton").click();
114+
await expect(page.locator("#statusLog")).toHaveValue("");
115+
});
116+
});

0 commit comments

Comments
 (0)