craftdriver 1.7.0 → 1.9.0
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 -12
- package/README.md +16 -6
- package/dist/cli/agentSession.d.ts +62 -0
- package/dist/cli/agentSession.d.ts.map +1 -0
- package/dist/cli/agentSession.js +100 -0
- package/dist/cli/agentSession.js.map +1 -0
- package/dist/cli/artifactPaths.d.ts +42 -0
- package/dist/cli/artifactPaths.d.ts.map +1 -0
- package/dist/cli/artifactPaths.js +144 -0
- package/dist/cli/artifactPaths.js.map +1 -0
- package/dist/cli/bounds.d.ts +88 -0
- package/dist/cli/bounds.d.ts.map +1 -0
- package/dist/cli/bounds.js +128 -0
- package/dist/cli/bounds.js.map +1 -0
- package/dist/cli/client.d.ts +3 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +24 -8
- package/dist/cli/client.js.map +1 -1
- package/dist/cli/daemon.d.ts +3 -0
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +152 -39
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/defaults.d.ts +23 -3
- package/dist/cli/defaults.d.ts.map +1 -1
- package/dist/cli/defaults.js +50 -5
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/dispatcher.d.ts +54 -11
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +851 -54
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +305 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +14 -14
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +328 -180
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/journal.d.ts +153 -0
- package/dist/cli/journal.d.ts.map +1 -0
- package/dist/cli/journal.js +252 -0
- package/dist/cli/journal.js.map +1 -0
- package/dist/cli/lineReader.d.ts +33 -0
- package/dist/cli/lineReader.d.ts.map +1 -0
- package/dist/cli/lineReader.js +66 -0
- package/dist/cli/lineReader.js.map +1 -0
- package/dist/cli/locatorCandidates.d.ts +44 -0
- package/dist/cli/locatorCandidates.d.ts.map +1 -0
- package/dist/cli/locatorCandidates.js +190 -0
- package/dist/cli/locatorCandidates.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +39 -1
- package/dist/cli/mcp/artifacts.d.ts.map +1 -1
- package/dist/cli/mcp/artifacts.js +104 -9
- package/dist/cli/mcp/artifacts.js.map +1 -1
- package/dist/cli/mcp/bounds.d.ts +65 -0
- package/dist/cli/mcp/bounds.d.ts.map +1 -0
- package/dist/cli/mcp/bounds.js +124 -0
- package/dist/cli/mcp/bounds.js.map +1 -0
- package/dist/cli/mcp/params.d.ts +38 -0
- package/dist/cli/mcp/params.d.ts.map +1 -0
- package/dist/cli/mcp/params.js +174 -0
- package/dist/cli/mcp/params.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +94 -16
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +304 -150
- package/dist/cli/mcp/server.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +62 -17
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +373 -158
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/cli/pageSemantics.d.ts +6 -0
- package/dist/cli/pageSemantics.d.ts.map +1 -0
- package/dist/cli/pageSemantics.js +6 -0
- package/dist/cli/pageSemantics.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +6 -18
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +689 -45
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/cli/protocol.d.ts +8 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/selector.d.ts +3 -3
- package/dist/cli/selector.d.ts.map +1 -1
- package/dist/cli/selector.js +6 -9
- package/dist/cli/selector.js.map +1 -1
- package/dist/cli/sessionRegistry.d.ts +42 -0
- package/dist/cli/sessionRegistry.d.ts.map +1 -0
- package/dist/cli/sessionRegistry.js +99 -0
- package/dist/cli/sessionRegistry.js.map +1 -0
- package/dist/cli/snapshot.d.ts +100 -19
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +295 -108
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/cli/stateStore.d.ts +50 -0
- package/dist/cli/stateStore.d.ts.map +1 -0
- package/dist/cli/stateStore.js +122 -0
- package/dist/cli/stateStore.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/a11y.d.ts +6 -2
- package/dist/lib/a11y.d.ts.map +1 -1
- package/dist/lib/a11y.js +3 -1
- package/dist/lib/a11y.js.map +1 -1
- package/dist/lib/bidi/evaluate.d.ts +11 -0
- package/dist/lib/bidi/evaluate.d.ts.map +1 -0
- package/dist/lib/bidi/evaluate.js +25 -0
- package/dist/lib/bidi/evaluate.js.map +1 -0
- package/dist/lib/bidi/index.d.ts +1 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +2 -2
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +3 -0
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +7 -0
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +14 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +45 -7
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +30 -6
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +190 -45
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +9 -2
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +7 -4
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +186 -65
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +58 -24
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +300 -99
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +68 -30
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +54 -23
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/clearFastPath.d.ts.map +1 -1
- package/dist/lib/clearFastPath.js +2 -0
- package/dist/lib/clearFastPath.js.map +1 -1
- package/dist/lib/clickFastPath.d.ts.map +1 -1
- package/dist/lib/clickFastPath.js +2 -0
- package/dist/lib/clickFastPath.js.map +1 -1
- package/dist/lib/driver.d.ts +26 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +123 -4
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/elementHandle.d.ts +13 -10
- package/dist/lib/elementHandle.d.ts.map +1 -1
- package/dist/lib/elementHandle.js +119 -50
- package/dist/lib/elementHandle.js.map +1 -1
- package/dist/lib/errors.d.ts +12 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +12 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/expect.d.ts +10 -0
- package/dist/lib/expect.d.ts.map +1 -1
- package/dist/lib/expect.js +76 -49
- package/dist/lib/expect.js.map +1 -1
- package/dist/lib/fillFastPath.d.ts.map +1 -1
- package/dist/lib/fillFastPath.js +2 -0
- package/dist/lib/fillFastPath.js.map +1 -1
- package/dist/lib/frame.d.ts.map +1 -1
- package/dist/lib/frame.js +29 -17
- package/dist/lib/frame.js.map +1 -1
- package/dist/lib/initScript.d.ts +9 -0
- package/dist/lib/initScript.d.ts.map +1 -0
- package/dist/lib/initScript.js +14 -0
- package/dist/lib/initScript.js.map +1 -0
- package/dist/lib/locator.d.ts +21 -54
- package/dist/lib/locator.d.ts.map +1 -1
- package/dist/lib/locator.js +165 -208
- package/dist/lib/locator.js.map +1 -1
- package/dist/lib/page.d.ts +22 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +71 -13
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/pageSemantics.d.ts +42 -0
- package/dist/lib/pageSemantics.d.ts.map +1 -0
- package/dist/lib/pageSemantics.js +216 -0
- package/dist/lib/pageSemantics.js.map +1 -0
- package/dist/lib/query.d.ts +68 -0
- package/dist/lib/query.d.ts.map +1 -0
- package/dist/lib/query.js +590 -0
- package/dist/lib/query.js.map +1 -0
- package/dist/lib/secureFile.d.ts +8 -0
- package/dist/lib/secureFile.d.ts.map +1 -0
- package/dist/lib/secureFile.js +67 -0
- package/dist/lib/secureFile.js.map +1 -0
- package/dist/lib/sessionStateValidation.d.ts +35 -0
- package/dist/lib/sessionStateValidation.d.ts.map +1 -0
- package/dist/lib/sessionStateValidation.js +223 -0
- package/dist/lib/sessionStateValidation.js.map +1 -0
- package/dist/lib/shadowRoot.d.ts +21 -0
- package/dist/lib/shadowRoot.d.ts.map +1 -0
- package/dist/lib/shadowRoot.js +29 -0
- package/dist/lib/shadowRoot.js.map +1 -0
- package/dist/lib/shadowRootLocator.d.ts +31 -0
- package/dist/lib/shadowRootLocator.d.ts.map +1 -0
- package/dist/lib/shadowRootLocator.js +55 -0
- package/dist/lib/shadowRootLocator.js.map +1 -0
- package/dist/lib/timing.d.ts +2 -2
- package/dist/lib/timing.js +2 -2
- package/dist/lib/tracing.d.ts +2 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +12 -1
- package/dist/lib/tracing.js.map +1 -1
- package/docs/accessibility.md +9 -1
- package/docs/agents.md +56 -61
- package/docs/api-reference.md +5 -1
- package/docs/browser-api.md +27 -7
- package/docs/browser-context.md +21 -15
- package/docs/cli.md +353 -30
- package/docs/error-codes.md +7 -4
- package/docs/getting-started.md +6 -1
- package/docs/index.md +1 -1
- package/docs/mcp.md +136 -65
- package/docs/public/examples/agent-actions.html +48 -0
- package/docs/public/examples/agent-debug.html +86 -0
- package/docs/public/examples/agent-late-mutation.html +18 -0
- package/docs/public/examples/agent-ref-shift.html +33 -0
- package/docs/public/examples/iframe-child.html +12 -1
- package/docs/public/examples/shadow-dom.html +137 -0
- package/docs/recipes/login-once-reuse-session.md +33 -5
- package/docs/selectors.md +61 -0
- package/docs/session-management.md +80 -12
- package/docs/why-craftdriver.md +1 -1
- package/package.json +5 -3
- package/skills/craftdriver/SKILL.md +59 -75
- package/skills/craftdriver/agents/openai.yaml +4 -0
- package/skills/craftdriver/cheatsheet.md +18 -4
- package/skills/craftdriver/cli.md +194 -68
- package/skills/craftdriver/patterns.md +17 -8
- package/skills/craftdriver/workflow.md +149 -0
package/docs/mcp.md
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
# MCP server
|
|
2
2
|
|
|
3
|
-
`craftdriver` ships
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
without managing a daemon, a socket, or filesystem access.
|
|
3
|
+
`craftdriver` ships an optional [Model Context Protocol](https://modelcontextprotocol.io)
|
|
4
|
+
STDIO adapter so MCP-aware coding agents can drive the same local session and
|
|
5
|
+
dispatcher as the CLI.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
post-action payload (compact a11y snapshot, diffed from the previous
|
|
11
|
-
turn) that text models can act on directly.
|
|
7
|
+
CLI plus the installed CraftDriver skill is the recommended workflow. MCP is
|
|
8
|
+
not required for exploration or test authoring.
|
|
12
9
|
|
|
13
10
|
```bash
|
|
14
11
|
# Start once via your MCP client — examples below
|
|
15
|
-
npx -
|
|
12
|
+
npx --no-install craftdriver mcp
|
|
16
13
|
```
|
|
17
14
|
|
|
18
15
|
The server speaks JSON-RPC 2.0 on stdio. The browser launches lazily
|
|
19
16
|
on the first tool call and shuts down when the client disconnects.
|
|
17
|
+
Each newline-delimited input frame is limited to 1 MiB (1,048,576 UTF-8
|
|
18
|
+
bytes); oversized frames return a parse error and are discarded.
|
|
20
19
|
|
|
21
|
-
##
|
|
20
|
+
## Manual project-pinned setup
|
|
21
|
+
|
|
22
|
+
Run `npx craftdriver init codex --mcp` to print the exact snippet. The installer
|
|
23
|
+
does not read or change MCP configuration.
|
|
22
24
|
|
|
23
25
|
### Claude Code / Claude Desktop
|
|
24
26
|
|
|
25
27
|
```bash
|
|
26
|
-
claude mcp add craftdriver -- npx -
|
|
28
|
+
claude mcp add craftdriver -- npx --no-install craftdriver mcp
|
|
27
29
|
```
|
|
28
30
|
|
|
29
31
|
### Cursor / Windsurf / Zed (`.cursor/mcp.json` and similar)
|
|
@@ -33,7 +35,7 @@ claude mcp add craftdriver -- npx -y craftdriver mcp
|
|
|
33
35
|
"mcpServers": {
|
|
34
36
|
"craftdriver": {
|
|
35
37
|
"command": "npx",
|
|
36
|
-
"args": ["-
|
|
38
|
+
"args": ["--no-install", "craftdriver", "mcp"]
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
41
|
}
|
|
@@ -42,7 +44,7 @@ claude mcp add craftdriver -- npx -y craftdriver mcp
|
|
|
42
44
|
### Gemini CLI
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
|
-
gemini mcp add craftdriver npx -
|
|
47
|
+
gemini mcp add craftdriver npx --no-install craftdriver mcp
|
|
46
48
|
```
|
|
47
49
|
|
|
48
50
|
### Goose
|
|
@@ -53,42 +55,90 @@ goose configure # add craftdriver as a stdio server
|
|
|
53
55
|
|
|
54
56
|
## Tools
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
71
|
-
| `
|
|
72
|
-
| `
|
|
73
|
-
| `
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
|
|
77
|
-
|
|
58
|
+
One line each; the long help lives in the schema description, which clients
|
|
59
|
+
render into the model's context once per session. Every tool dispatches a
|
|
60
|
+
command the CLI also has — there are no MCP-only browser semantics.
|
|
61
|
+
|
|
62
|
+
| Tool | Purpose |
|
|
63
|
+
| ----------------------- | ---------------------------------------------------------------------- |
|
|
64
|
+
| `browser_navigate` | Go to a URL (waits for load). |
|
|
65
|
+
| `browser_click` | Click an element; set `double` for a double-click. |
|
|
66
|
+
| `browser_fill` | Fill an input/textarea (clears first, real key events). |
|
|
67
|
+
| `browser_type` | Type into whatever holds focus (no selector). |
|
|
68
|
+
| `browser_element` | `dblclick`/`focus`/`scroll`/`clear`/`check`/`uncheck`/`select`. |
|
|
69
|
+
| `browser_press` | Press a key (`Enter`, `Tab`, `Control+A`). |
|
|
70
|
+
| `browser_key` | Low-level `press`/`down`/`up` for modifier combinations. |
|
|
71
|
+
| `browser_mouse` | `move`/`click`/`down`/`up`/`wheel`, by element or coordinate. |
|
|
72
|
+
| `browser_hover` | Hover over an element. |
|
|
73
|
+
| `browser_upload` | Set files on a file input (bounded; paths never echoed). |
|
|
74
|
+
| `browser_dialog` | `inspect`/`accept`/`dismiss` a native dialog. |
|
|
75
|
+
| `browser_find` | Locate elements without acting (tag/text/visibility). |
|
|
76
|
+
| `browser_exists` | **0-wait probe.** Returns `{exists, count}` in one roundtrip. |
|
|
77
|
+
| `browser_wait` | Wait for a selector state or a load state. |
|
|
78
|
+
| `browser_read` | Read `text` / `attr` / `value` / `is(visible\|enabled\|checked)`. |
|
|
79
|
+
| `browser_snapshot` | **Sanitized DOM summary with refs.** Use `ref=eN` as a selector. |
|
|
80
|
+
| `browser_locators` | **Turn an element into durable selectors for a test.** Never a ref. |
|
|
81
|
+
| `browser_page` | `list`/`open`/`select`/`close` tabs. |
|
|
82
|
+
| `browser_logs` | Console + network history, with cursors. See below. |
|
|
83
|
+
| `browser_mock` | Serve a fixed response or block matching requests. |
|
|
84
|
+
| `browser_state` | Save/restore cookies and local storage (a login, once). |
|
|
85
|
+
| `browser_trace` | Record a run to an owned directory; `zip` for a Vibium archive. |
|
|
86
|
+
| `browser_screenshot` | Capture PNG to a file under the artifact dir; never inlined. |
|
|
87
|
+
| `browser_status` | Browser up? Which URL is active? |
|
|
88
|
+
| `browser_advanced_eval` | Evaluate JS in the page. Last resort. |
|
|
89
|
+
|
|
90
|
+
Each tool carries MCP `annotations` — `title`, `readOnlyHint`,
|
|
91
|
+
`destructiveHint`, `idempotentHint`, `openWorldHint` — and they are accurate:
|
|
92
|
+
a tool marked read-only never dispatches a command the dispatcher treats as
|
|
93
|
+
page-mutating, which is asserted by test rather than by review.
|
|
94
|
+
|
|
95
|
+
### Debugging with evidence
|
|
96
|
+
|
|
97
|
+
`browser_logs` captures console and network from launch, so an error thrown
|
|
98
|
+
during the first navigation is still answerable afterwards. Every result
|
|
99
|
+
carries a `cursor`; pass it back as `since` for only what is new. `kind=error`
|
|
100
|
+
covers both uncaught exceptions and `console.error`. Network rows are
|
|
101
|
+
summaries — url, method, status, mime type — and never carry bodies, cookies,
|
|
102
|
+
or headers.
|
|
78
103
|
|
|
79
104
|
```jsonc
|
|
80
|
-
{ "name": "
|
|
81
|
-
|
|
105
|
+
{ "name": "browser_logs", "arguments": { "action": "list", "kind": "error" } }
|
|
106
|
+
{ "name": "browser_logs", "arguments": {
|
|
107
|
+
"action": "wait", "contains": "checkout ok", "timeout_ms": 10000
|
|
82
108
|
} }
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Tracing
|
|
112
|
+
|
|
113
|
+
```jsonc
|
|
114
|
+
{ "name": "browser_trace", "arguments": { "action": "start", "name": "agentflow" } }
|
|
83
115
|
// ...browser_navigate / browser_fill / browser_click calls...
|
|
84
|
-
{ "name": "browser_trace", "arguments": {
|
|
85
|
-
"action": "stop", "path": "./traces/agent-flow.zip"
|
|
86
|
-
} }
|
|
116
|
+
{ "name": "browser_trace", "arguments": { "action": "stop", "zip": true } }
|
|
87
117
|
```
|
|
88
118
|
|
|
89
|
-
The zip opens at
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
The response reports where the trace and archive landed; the zip opens at
|
|
120
|
+
[player.vibium.dev](https://player.vibium.dev/). Output goes to an owned
|
|
121
|
+
directory (`CRAFTDRIVER_TRACE_DIR`), and `name` is a bare name rather than a
|
|
122
|
+
path — an earlier version accepted an arbitrary filesystem path straight off
|
|
123
|
+
the wire. If the client disconnects before `stop`, the raw NDJSON is still
|
|
124
|
+
valid, just without a finalized zip.
|
|
125
|
+
|
|
126
|
+
## Argument validation
|
|
127
|
+
|
|
128
|
+
Every tool declares its arguments once; that declaration produces both the
|
|
129
|
+
advertised `inputSchema` and the runtime check, so a tool cannot promise a
|
|
130
|
+
constraint it does not enforce. Invalid arguments are rejected as JSON-RPC
|
|
131
|
+
`-32602` **before** anything reaches the browser:
|
|
132
|
+
|
|
133
|
+
- an unknown field (rather than being silently ignored — a misspelled required
|
|
134
|
+
argument would otherwise look like a successful call that did something else);
|
|
135
|
+
- a wrong type, a non-finite number, an out-of-range number, an invalid enum;
|
|
136
|
+
- an oversized string or array.
|
|
137
|
+
|
|
138
|
+
`-32602` also covers an unknown tool name. Ordinary browser failures — a
|
|
139
|
+
missing element, a timeout — are **not** protocol errors; they come back as
|
|
140
|
+
successful responses with `isError: true`, which is what keeps the two
|
|
141
|
+
distinguishable.
|
|
92
142
|
|
|
93
143
|
## Selector syntax
|
|
94
144
|
|
|
@@ -128,24 +178,36 @@ Use `ref=eN` as the selector for the next call:
|
|
|
128
178
|
{ "name": "browser_click", "arguments": { "selector": "ref=e7" } }
|
|
129
179
|
```
|
|
130
180
|
|
|
131
|
-
**
|
|
181
|
+
**Use refs only for immediate exploration**
|
|
132
182
|
|
|
133
|
-
- **
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
- **A ref names one element for as long as it lives.** A surviving node keeps
|
|
184
|
+
its ref across snapshots, and refs are never reused — so a ref cannot drift
|
|
185
|
+
onto a different element. If it is removed or the page
|
|
186
|
+
navigates, the call fails `STALE_REF`; take a fresh snapshot then.
|
|
187
|
+
- **Never copy refs into test code.** Convert live role/name, label, test ID,
|
|
188
|
+
text, or DOM evidence into a durable selector and validate it.
|
|
136
189
|
- **Token efficient.** `ref=e7` is 5 characters; `role=button[name=Sign in]`
|
|
137
190
|
is 26. Over a 50-step flow that adds up.
|
|
138
|
-
- **Auto-waiting still works.**
|
|
139
|
-
|
|
140
|
-
takes the normal visible+enabled wait path.
|
|
191
|
+
- **Auto-waiting still works.** `ref=eN` resolves directly to the exact element
|
|
192
|
+
in the page identity registry, including inside an open shadow root; every
|
|
193
|
+
action takes the normal visible+enabled wait path.
|
|
141
194
|
|
|
142
195
|
**Invalidation rules**
|
|
143
196
|
|
|
144
|
-
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- A
|
|
197
|
+
- A ref binds to one element. An element that survives a DOM change keeps
|
|
198
|
+
its ref across snapshots; snapshots do **not** renumber it.
|
|
199
|
+
- New elements get fresh numbers. Refs are never reused, including after a
|
|
200
|
+
navigation or reload.
|
|
201
|
+
- A ref whose element was removed, or that was issued before
|
|
202
|
+
the page navigated or reloaded, fails with `STALE_REF` — take a fresh
|
|
203
|
+
snapshot. It never resolves to a different element.
|
|
204
|
+
- `error.detail.reason` distinguishes `detached`, `document-changed`,
|
|
205
|
+
`unknown-ref`, `ambiguous`, and `no-snapshot`.
|
|
206
|
+
- Refs are exploration state and must never appear in committed tests.
|
|
207
|
+
|
|
208
|
+
Snapshots recursively enter open Shadow DOM, mark each boundary with an
|
|
209
|
+
indented `#shadow-root (open)` line, flatten slot assignment without duplicate
|
|
210
|
+
entries, and never inspect closed roots.
|
|
149
211
|
|
|
150
212
|
## Post-action payload
|
|
151
213
|
|
|
@@ -197,10 +259,10 @@ button "Click me" #by-text
|
|
|
197
259
|
|
|
198
260
|
Applies to:
|
|
199
261
|
|
|
200
|
-
- **Screenshots** — always written to a file
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
262
|
+
- **Screenshots** — always written to a server-allocated file under the
|
|
263
|
+
per-session artifact directory. The inline block carries the absolute path
|
|
264
|
+
and byte count — **zero** image tokens. The tool accepts no destination
|
|
265
|
+
path, so it cannot write outside that directory.
|
|
204
266
|
- **A11y snapshot diffs** — spill when the rendered diff exceeds the
|
|
205
267
|
threshold (typically only the full first-call snapshot on big pages).
|
|
206
268
|
- **Tool results** — `browser_read`, `browser_advanced_eval`, etc. spill
|
|
@@ -213,20 +275,29 @@ Configuration:
|
|
|
213
275
|
| ---------------------------------- | ---------------- | ------------------------------------------------- |
|
|
214
276
|
| `CRAFTDRIVER_MCP_ARTIFACTS_DIR` | `os.tmpdir()` | Root directory for the per-session artifact dir. |
|
|
215
277
|
| `CRAFTDRIVER_MCP_SPILL_BYTES` | `2048` (~500 tk) | Inline content blocks larger than this spill. |
|
|
278
|
+
| `CRAFTDRIVER_MCP_MAX_RESPONSE_BYTES` | `32768` | Maximum serialized result from one tool call. |
|
|
216
279
|
|
|
217
280
|
The per-session directory (`<root>/craftdriver-mcp-<pid>-<stamp>/`) is
|
|
218
281
|
not deleted on shutdown — agents may still be reading past artifacts.
|
|
219
282
|
Use `$CRAFTDRIVER_MCP_ARTIFACTS_DIR` to point at a dir with your own
|
|
220
|
-
cleanup policy.
|
|
283
|
+
cleanup policy. Each server process refuses more than 500 artifacts or 256 MiB
|
|
284
|
+
in its session directory; browser-written screenshots are counted by their
|
|
285
|
+
actual file size after capture.
|
|
221
286
|
|
|
222
|
-
|
|
223
|
-
|
|
287
|
+
Small results still round-trip in full through `structuredContent`. When the
|
|
288
|
+
complete result would exceed the response cap, it becomes explicit truncation
|
|
289
|
+
metadata and a bounded preview rather than duplicating the full spilled value.
|
|
224
290
|
|
|
225
291
|
## Errors
|
|
226
292
|
|
|
227
|
-
|
|
228
|
-
as JSON-RPC errors. JSON-RPC errors are reserved for
|
|
229
|
-
failures
|
|
293
|
+
Browser and action failures are returned as `isError: true` content (per MCP
|
|
294
|
+
spec), **not** as JSON-RPC errors. JSON-RPC errors are reserved for
|
|
295
|
+
protocol-level failures: a malformed request (`-32700`/`-32600`), an unknown
|
|
296
|
+
method (`-32601`), and an unknown tool or invalid arguments (`-32602`).
|
|
297
|
+
|
|
298
|
+
The split matters: "the element was not there" is a fact about the page that an
|
|
299
|
+
agent should reason about, while "you sent an argument that does not exist" is
|
|
300
|
+
a mistake about the protocol that it should correct.
|
|
230
301
|
|
|
231
302
|
```jsonc
|
|
232
303
|
{
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
Agent-surface fixture for the common action set and for durable locator
|
|
4
|
+
candidates. Deliberately mixes elements that offer good evidence (role +
|
|
5
|
+
name, label, test id) with ones that offer only generated attributes, so
|
|
6
|
+
tests can prove `locators` tells the truth in both cases.
|
|
7
|
+
-->
|
|
8
|
+
<html><head><title>Agent actions</title></head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Account settings</h1>
|
|
11
|
+
|
|
12
|
+
<label for="nickname">Nickname</label>
|
|
13
|
+
<input id="nickname" name="nickname" type="text" value="preset" />
|
|
14
|
+
|
|
15
|
+
<label for="newsletter">Newsletter</label>
|
|
16
|
+
<input id="newsletter" name="newsletter" type="checkbox" />
|
|
17
|
+
|
|
18
|
+
<label for="plan">Plan</label>
|
|
19
|
+
<select id="plan" name="plan">
|
|
20
|
+
<option value="free">Free</option>
|
|
21
|
+
<option value="pro">Pro</option>
|
|
22
|
+
</select>
|
|
23
|
+
|
|
24
|
+
<button id="save" data-testid="save-button" type="button">Save changes</button>
|
|
25
|
+
<button id="a1b2c3d4e5f6" type="button">Generated id</button>
|
|
26
|
+
|
|
27
|
+
<p id="dbl-count">0</p>
|
|
28
|
+
<button id="counter" type="button">Count me</button>
|
|
29
|
+
|
|
30
|
+
<div style="height: 1500px"></div>
|
|
31
|
+
<button id="far-below" type="button">Far below</button>
|
|
32
|
+
|
|
33
|
+
<p id="log"></p>
|
|
34
|
+
<script>
|
|
35
|
+
const log = document.getElementById('log');
|
|
36
|
+
let dbl = 0;
|
|
37
|
+
document.getElementById('counter').addEventListener('dblclick', () => {
|
|
38
|
+
dbl += 1;
|
|
39
|
+
document.getElementById('dbl-count').textContent = String(dbl);
|
|
40
|
+
});
|
|
41
|
+
document.getElementById('nickname').addEventListener('keydown', () => {
|
|
42
|
+
log.textContent = 'keydown seen';
|
|
43
|
+
});
|
|
44
|
+
document.getElementById('save').addEventListener('click', () => {
|
|
45
|
+
log.textContent = 'saved';
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
</body></html>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>Craftdriver Debug Example</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: system-ui, sans-serif; max-width: 34rem; margin: 3rem auto; padding: 0 1rem; }
|
|
8
|
+
fieldset { border: 1px solid #ccc; border-radius: 6px; padding: 1rem 1.25rem; }
|
|
9
|
+
label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 600; }
|
|
10
|
+
input { width: 100%; padding: 0.5rem; font-size: 1rem; box-sizing: border-box; }
|
|
11
|
+
button { margin-top: 1rem; padding: 0.6rem 1.2rem; font-size: 1rem; cursor: pointer; }
|
|
12
|
+
#status { margin-top: 1rem; padding: 0.75rem; border-radius: 6px; }
|
|
13
|
+
#status.error { background: #fdecea; color: #611a15; }
|
|
14
|
+
#status.ok { background: #edf7ed; color: #1e4620; }
|
|
15
|
+
.note { color: #555; font-size: 0.9rem; }
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>
|
|
19
|
+
<h1>Checkout</h1>
|
|
20
|
+
<p class="note">
|
|
21
|
+
A deliberately opaque failure: the page reports only that something went
|
|
22
|
+
wrong. What actually failed is visible in the console and network journal,
|
|
23
|
+
not in the DOM.
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<form id="checkout-form">
|
|
27
|
+
<fieldset>
|
|
28
|
+
<legend>Order details</legend>
|
|
29
|
+
<label for="email">Email</label>
|
|
30
|
+
<input id="email" name="email" type="email" placeholder="you@example.test" />
|
|
31
|
+
|
|
32
|
+
<label for="card">Card number</label>
|
|
33
|
+
<input id="card" name="card" type="text" placeholder="4242 4242 4242 4242" />
|
|
34
|
+
|
|
35
|
+
<button id="place-order" type="submit">Place order</button>
|
|
36
|
+
</fieldset>
|
|
37
|
+
</form>
|
|
38
|
+
|
|
39
|
+
<div id="status" hidden></div>
|
|
40
|
+
|
|
41
|
+
<script>
|
|
42
|
+
// The endpoint does not exist on the static example server, so this always
|
|
43
|
+
// fails with the same status. Nothing here is timing- or order-dependent:
|
|
44
|
+
// the same click produces the same evidence every run.
|
|
45
|
+
const ENDPOINT = '/api/checkout';
|
|
46
|
+
|
|
47
|
+
const form = document.getElementById('checkout-form');
|
|
48
|
+
const status = document.getElementById('status');
|
|
49
|
+
|
|
50
|
+
function show(text, kind) {
|
|
51
|
+
status.hidden = false;
|
|
52
|
+
status.className = kind;
|
|
53
|
+
status.textContent = text;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
form.addEventListener('submit', async (event) => {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
show('Placing order…', '');
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const response = await fetch(ENDPOINT, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: { 'content-type': 'application/json' },
|
|
64
|
+
body: JSON.stringify({ email: document.getElementById('email').value }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
// The detail goes to the console, not to the page — which is exactly
|
|
69
|
+
// the situation `craftdriver logs` exists for.
|
|
70
|
+
console.error(
|
|
71
|
+
'checkout failed: HTTP ' + response.status + ' from ' + ENDPOINT
|
|
72
|
+
);
|
|
73
|
+
show('Something went wrong. Please try again.', 'error');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const order = await response.json();
|
|
78
|
+
show('Order ' + order.orderId + ' confirmed', 'ok');
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.error('checkout request threw: ' + err.message);
|
|
81
|
+
show('Something went wrong. Please try again.', 'error');
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
</script>
|
|
85
|
+
</body>
|
|
86
|
+
</html>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
Agent-surface fixture: the page mutates itself after load with no agent
|
|
4
|
+
action involved. Used to prove explicit and automatic snapshots share one
|
|
5
|
+
baseline — an explicit snapshot must not leave the next post-action diff
|
|
6
|
+
re-reporting changes the agent has already been shown.
|
|
7
|
+
-->
|
|
8
|
+
<html><head><title>Timer repro</title></head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Dashboard</h1>
|
|
11
|
+
<div id="slot"></div>
|
|
12
|
+
<button id="go">Go</button>
|
|
13
|
+
<script>
|
|
14
|
+
setTimeout(() => {
|
|
15
|
+
document.getElementById('slot').innerHTML = '<button id="late">Late arrival</button>';
|
|
16
|
+
}, 800);
|
|
17
|
+
</script>
|
|
18
|
+
</body></html>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<!--
|
|
3
|
+
Agent-surface fixture: a DOM reorder that shifts every element's document
|
|
4
|
+
position. Used to prove snapshot refs bind to element identity, not to
|
|
5
|
+
document order (a stale ref must fail, never act on a replacement node).
|
|
6
|
+
-->
|
|
7
|
+
<html><head><title>Shift repro</title></head>
|
|
8
|
+
<body>
|
|
9
|
+
<h1>Checkout</h1>
|
|
10
|
+
<div id="banner"></div>
|
|
11
|
+
<form id="f">
|
|
12
|
+
<label for="coupon">Coupon</label>
|
|
13
|
+
<input id="coupon" name="coupon" type="text" />
|
|
14
|
+
<button id="apply" type="button">Apply coupon</button>
|
|
15
|
+
<label for="email">Email</label>
|
|
16
|
+
<input id="email" name="email" type="text" />
|
|
17
|
+
<button id="pay" type="button">Pay now</button>
|
|
18
|
+
<button id="remove-pay" type="button">Remove pay button</button>
|
|
19
|
+
</form>
|
|
20
|
+
<script>
|
|
21
|
+
// Realistic: applying an invalid coupon renders an inline error with a
|
|
22
|
+
// help link ABOVE the rest of the form, shifting everything below it.
|
|
23
|
+
document.getElementById('apply').addEventListener('click', () => {
|
|
24
|
+
document.getElementById('banner').innerHTML =
|
|
25
|
+
'<a href="/help" id="help">Why was my coupon rejected?</a>';
|
|
26
|
+
});
|
|
27
|
+
// Removing the pay button lets a test prove a removed ref fails rather
|
|
28
|
+
// than silently resolving to whatever took its place.
|
|
29
|
+
document.getElementById('remove-pay').addEventListener('click', () => {
|
|
30
|
+
document.getElementById('pay').remove();
|
|
31
|
+
});
|
|
32
|
+
</script>
|
|
33
|
+
</body></html>
|
|
@@ -38,7 +38,18 @@
|
|
|
38
38
|
<p id="child-result"></p>
|
|
39
39
|
<input id="child-input" type="text" placeholder="type here" />
|
|
40
40
|
<p id="child-value"></p>
|
|
41
|
+
<frame-widget id="frame-widget"></frame-widget>
|
|
41
42
|
<script>
|
|
43
|
+
customElements.define('frame-widget', class extends HTMLElement {
|
|
44
|
+
constructor() {
|
|
45
|
+
super();
|
|
46
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
47
|
+
root.innerHTML = '<button id="shadow-frame-button" type="button">Shadow frame action</button>';
|
|
48
|
+
root.querySelector('button').addEventListener('click', () => {
|
|
49
|
+
document.querySelector('#child-result').textContent = 'shadow-clicked';
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
42
53
|
document.getElementById('child-btn').addEventListener('click', function () {
|
|
43
54
|
document.getElementById('child-result').textContent = 'clicked';
|
|
44
55
|
});
|
|
@@ -48,4 +59,4 @@
|
|
|
48
59
|
</script>
|
|
49
60
|
</body>
|
|
50
61
|
|
|
51
|
-
</html>
|
|
62
|
+
</html>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<title>CraftDriver Shadow DOM fixture</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; margin: 24px; }
|
|
8
|
+
user-card, closed-card, no-root { display: block; margin: 12px 0; }
|
|
9
|
+
#status { min-height: 20px; }
|
|
10
|
+
</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<h1>Shadow DOM fixture</h1>
|
|
14
|
+
<div id="status">idle</div>
|
|
15
|
+
<button id="replace-card" type="button">Replace card</button>
|
|
16
|
+
|
|
17
|
+
<!-- Open, nested, and slotted happy path -->
|
|
18
|
+
<user-card id="card">
|
|
19
|
+
<span slot="summary" id="slotted-summary" role="note">Slotted summary</span>
|
|
20
|
+
<img slot="summary" id="slotted-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
|
|
21
|
+
</user-card>
|
|
22
|
+
|
|
23
|
+
<!-- Async-host, closed-root, and missing-root cases -->
|
|
24
|
+
<div id="late-card-container"></div>
|
|
25
|
+
<closed-card id="closed-card"></closed-card>
|
|
26
|
+
<no-root id="no-root"></no-root>
|
|
27
|
+
|
|
28
|
+
<script>
|
|
29
|
+
// Nested open-root form control.
|
|
30
|
+
class AddressForm extends HTMLElement {
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
34
|
+
root.innerHTML = `
|
|
35
|
+
<style>:host { display:block; padding:8px; border:1px solid #aaa }</style>
|
|
36
|
+
<label for="city">City</label>
|
|
37
|
+
<input id="city" data-testid="city" placeholder="Enter city" title="Delivery city" />
|
|
38
|
+
<span id="save-address-label">Save address</span>
|
|
39
|
+
<button id="nested-save" type="button" aria-labelledby="save-address-label">Save</button>
|
|
40
|
+
<img id="shadow-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
|
|
41
|
+
`;
|
|
42
|
+
root.querySelector('#nested-save').addEventListener('click', () => {
|
|
43
|
+
document.querySelector('#status').textContent = 'saved:' + root.querySelector('#city').value;
|
|
44
|
+
});
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
const delayed = document.createElement('button');
|
|
47
|
+
delayed.id = 'delayed-shadow-action';
|
|
48
|
+
delayed.type = 'button';
|
|
49
|
+
delayed.textContent = 'Delayed shadow action';
|
|
50
|
+
delayed.addEventListener('click', () => {
|
|
51
|
+
document.querySelector('#status').textContent = 'delayed';
|
|
52
|
+
});
|
|
53
|
+
root.appendChild(delayed);
|
|
54
|
+
}, 250);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Open-root host with slots, semantic content, and actions.
|
|
59
|
+
class UserCard extends HTMLElement {
|
|
60
|
+
constructor() {
|
|
61
|
+
super();
|
|
62
|
+
const root = this.attachShadow({ mode: 'open' });
|
|
63
|
+
root.innerHTML = `
|
|
64
|
+
<style>:host { display:block; padding:8px; border:1px solid #444 }</style>
|
|
65
|
+
<section role="region" aria-label="Account">
|
|
66
|
+
<slot name="summary"><span>Fallback summary</span></slot>
|
|
67
|
+
<slot><button id="fallback-control" type="button">Fallback action</button></slot>
|
|
68
|
+
<a id="profile-link" href="#profile">Profile link</a>
|
|
69
|
+
<img id="avatar" alt="User avatar" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
|
|
70
|
+
<button class="action" id="edit" type="button">Edit</button>
|
|
71
|
+
<button class="action" id="delete" type="button">Delete</button>
|
|
72
|
+
<address-form id="address"></address-form>
|
|
73
|
+
</section>
|
|
74
|
+
`;
|
|
75
|
+
root.querySelector('#edit').addEventListener('click', () => {
|
|
76
|
+
document.querySelector('#status').textContent = 'edited';
|
|
77
|
+
});
|
|
78
|
+
root.querySelector('#delete').addEventListener('click', () => {
|
|
79
|
+
document.querySelector('#status').textContent = 'deleted';
|
|
80
|
+
});
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
const delayed = document.createElement('button');
|
|
83
|
+
delayed.id = 'delayed-card-action';
|
|
84
|
+
delayed.type = 'button';
|
|
85
|
+
delayed.textContent = 'Delayed card action';
|
|
86
|
+
delayed.addEventListener('click', () => {
|
|
87
|
+
document.querySelector('#status').textContent = 'delayed-card';
|
|
88
|
+
});
|
|
89
|
+
root.appendChild(delayed);
|
|
90
|
+
}, 250);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Closed-root privacy boundary.
|
|
95
|
+
class ClosedCard extends HTMLElement {
|
|
96
|
+
constructor() {
|
|
97
|
+
super();
|
|
98
|
+
const root = this.attachShadow({ mode: 'closed' });
|
|
99
|
+
root.innerHTML = `
|
|
100
|
+
<button id="closed-false-positive" type="button">Edit</button>
|
|
101
|
+
<img id="closed-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==" />
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
customElements.define('address-form', AddressForm);
|
|
107
|
+
customElements.define('user-card', UserCard);
|
|
108
|
+
customElements.define('closed-card', ClosedCard);
|
|
109
|
+
|
|
110
|
+
document.querySelector('#replace-card').addEventListener('click', () => {
|
|
111
|
+
const replacement = document.createElement('user-card');
|
|
112
|
+
replacement.id = 'card';
|
|
113
|
+
const summary = document.createElement('span');
|
|
114
|
+
summary.slot = 'summary';
|
|
115
|
+
summary.id = 'slotted-summary';
|
|
116
|
+
summary.setAttribute('role', 'note');
|
|
117
|
+
summary.textContent = 'Replacement summary';
|
|
118
|
+
replacement.appendChild(summary);
|
|
119
|
+
document.querySelector('#card').replaceWith(replacement);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
const late = document.createElement('user-card');
|
|
124
|
+
late.id = 'late-card';
|
|
125
|
+
document.querySelector('#late-card-container').appendChild(late);
|
|
126
|
+
}, 250);
|
|
127
|
+
|
|
128
|
+
setTimeout(() => {
|
|
129
|
+
const delayed = document.createElement('button');
|
|
130
|
+
delayed.id = 'delayed-flat-action';
|
|
131
|
+
delayed.type = 'button';
|
|
132
|
+
delayed.textContent = 'Delayed flat action';
|
|
133
|
+
document.body.appendChild(delayed);
|
|
134
|
+
}, 250);
|
|
135
|
+
</script>
|
|
136
|
+
</body>
|
|
137
|
+
</html>
|