playwright-e2e-mcp 0.9.0 → 0.11.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/README.md +57 -18
- package/dist/apps/index.d.ts +29 -0
- package/dist/apps/index.js +52 -0
- package/dist/apps/index.js.map +1 -0
- package/dist/apps/test-report-html.d.ts +15 -0
- package/dist/apps/test-report-html.js +430 -0
- package/dist/apps/test-report-html.js.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +12 -2
- package/dist/oauth.js +44 -6
- package/dist/oauth.js.map +1 -1
- package/dist/server.d.ts +14 -2
- package/dist/server.js +107 -8
- package/dist/server.js.map +1 -1
- package/dist/tools/audit-tests.js +24 -6
- package/dist/tools/audit-tests.js.map +1 -1
- package/dist/tools/compare-visual-state.js +7 -1
- package/dist/tools/compare-visual-state.js.map +1 -1
- package/dist/tools/generate-e2e-test.js +27 -12
- package/dist/tools/generate-e2e-test.js.map +1 -1
- package/dist/tools/inspect-page.js +5 -12
- package/dist/tools/inspect-page.js.map +1 -1
- package/dist/tools/quarantine.js +7 -1
- package/dist/tools/quarantine.js.map +1 -1
- package/dist/tools/shared.d.ts +10 -0
- package/dist/tools/shared.js +15 -0
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/structured.d.ts +2 -0
- package/dist/tools/structured.js +4 -2
- package/dist/tools/structured.js.map +1 -1
- package/dist/tools/suggest-fix.js +6 -1
- package/dist/tools/suggest-fix.js.map +1 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/utils/client-metadata.d.ts +46 -0
- package/dist/utils/client-metadata.js +174 -0
- package/dist/utils/client-metadata.js.map +1 -0
- package/dist/utils/compact-schema.d.ts +20 -0
- package/dist/utils/compact-schema.js +55 -0
- package/dist/utils/compact-schema.js.map +1 -0
- package/dist/utils/confirm.d.ts +68 -0
- package/dist/utils/confirm.js +120 -0
- package/dist/utils/confirm.js.map +1 -0
- package/dist/utils/locator-map.d.ts +25 -0
- package/dist/utils/locator-map.js +113 -0
- package/dist/utils/locator-map.js.map +1 -0
- package/examples/github-actions/playwright-e2e.yml +1 -1
- package/package.json +1 -1
- package/skill/playwright-e2e-mcp/SKILL.md +12 -5
- package/skill/playwright-e2e-mcp/references/setup.md +2 -2
- package/skill/playwright-e2e-mcp/references/tools.md +1 -1
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The interactive test report view (MCP Apps, SEP-1865).
|
|
3
|
+
*
|
|
4
|
+
* One self-contained HTML page with no external requests: the host loads it
|
|
5
|
+
* in a sandboxed iframe and talks to it with JSON-RPC over postMessage. The
|
|
6
|
+
* page speaks the small part of the MCP Apps protocol it needs directly
|
|
7
|
+
* (ui/initialize, tool-input, tool-result, tools/call, ui/message,
|
|
8
|
+
* size-changed), so it ships no framework and no bundle.
|
|
9
|
+
*
|
|
10
|
+
* Everything from the tool result is written with textContent, never as
|
|
11
|
+
* HTML: test titles and error messages come from the project under test.
|
|
12
|
+
*
|
|
13
|
+
* Kept as a TypeScript string so `tsc` ships it in dist/ with the server.
|
|
14
|
+
*/
|
|
15
|
+
export const TEST_REPORT_HTML = String.raw `<!doctype html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="utf-8">
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
20
|
+
<title>Playwright test report</title>
|
|
21
|
+
<style>
|
|
22
|
+
:root {
|
|
23
|
+
--bg: var(--color-background-primary, #ffffff);
|
|
24
|
+
--bg2: var(--color-background-secondary, #f5f6f8);
|
|
25
|
+
--fg: var(--color-text-primary, #1d2125);
|
|
26
|
+
--fg2: var(--color-text-secondary, #5b636b);
|
|
27
|
+
--line: var(--color-border-primary, #dfe2e6);
|
|
28
|
+
--ok: var(--color-text-success, #1f7a3d);
|
|
29
|
+
--bad: var(--color-text-danger, #b42318);
|
|
30
|
+
--warn: var(--color-text-warning, #9a6700);
|
|
31
|
+
--okbg: var(--color-background-success, #e7f6ec);
|
|
32
|
+
--badbg: var(--color-background-danger, #fdecea);
|
|
33
|
+
--warnbg: var(--color-background-warning, #fff4d6);
|
|
34
|
+
--radius: var(--border-radius-md, 8px);
|
|
35
|
+
--sans: var(--font-sans, system-ui, -apple-system, "Segoe UI", sans-serif);
|
|
36
|
+
--mono: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
37
|
+
}
|
|
38
|
+
:root[data-theme="dark"] {
|
|
39
|
+
--bg: var(--color-background-primary, #17191c);
|
|
40
|
+
--bg2: var(--color-background-secondary, #212429);
|
|
41
|
+
--fg: var(--color-text-primary, #e8eaed);
|
|
42
|
+
--fg2: var(--color-text-secondary, #a2a9b1);
|
|
43
|
+
--line: var(--color-border-primary, #33373d);
|
|
44
|
+
--ok: var(--color-text-success, #6fd08c);
|
|
45
|
+
--bad: var(--color-text-danger, #ff8a80);
|
|
46
|
+
--warn: var(--color-text-warning, #f2c94c);
|
|
47
|
+
--okbg: var(--color-background-success, #17301f);
|
|
48
|
+
--badbg: var(--color-background-danger, #3a1d1b);
|
|
49
|
+
--warnbg: var(--color-background-warning, #3a3117);
|
|
50
|
+
}
|
|
51
|
+
* { box-sizing: border-box; }
|
|
52
|
+
html, body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.45 var(--sans); }
|
|
53
|
+
main { padding: 14px 16px 16px; }
|
|
54
|
+
h1 { font-size: 15px; margin: 0 0 10px; display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
55
|
+
.badge { font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 999px; }
|
|
56
|
+
.badge.ok { background: var(--okbg); color: var(--ok); }
|
|
57
|
+
.badge.bad { background: var(--badbg); color: var(--bad); }
|
|
58
|
+
.badge.warn { background: var(--warnbg); color: var(--warn); }
|
|
59
|
+
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(84px, 1fr)); gap: 8px; margin-bottom: 12px; }
|
|
60
|
+
.stat { background: var(--bg2); border-radius: var(--radius); padding: 8px 10px; }
|
|
61
|
+
.stat b { display: block; white-space: nowrap; font-size: 18px; font-variant-numeric: tabular-nums; }
|
|
62
|
+
.stat span { color: var(--fg2); font-size: 12px; }
|
|
63
|
+
.stat.failed b { color: var(--bad); }
|
|
64
|
+
.stat.passed b { color: var(--ok); }
|
|
65
|
+
.stat.flaky b { color: var(--warn); }
|
|
66
|
+
.bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--bg2); margin-bottom: 12px; }
|
|
67
|
+
.bar i { display: block; }
|
|
68
|
+
.toolbar { display: flex; gap: 8px; margin-bottom: 8px; }
|
|
69
|
+
input[type=search] { flex: 1; min-width: 0; padding: 6px 10px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg); color: var(--fg); font: inherit; }
|
|
70
|
+
ul { list-style: none; margin: 0; padding: 0; }
|
|
71
|
+
li.failure { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 8px; overflow: hidden; }
|
|
72
|
+
li.failure > button.head { all: unset; cursor: pointer; display: block; width: 100%; padding: 8px 10px; box-sizing: border-box; }
|
|
73
|
+
li.failure > button.head:focus-visible { outline: 2px solid var(--color-ring-primary, #3b82f6); outline-offset: -2px; }
|
|
74
|
+
.title { font-weight: 600; word-break: break-word; }
|
|
75
|
+
.where { color: var(--fg2); font: 12px var(--mono); word-break: break-all; }
|
|
76
|
+
.kind { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--bad); margin-left: 6px; }
|
|
77
|
+
.body { padding: 0 10px 10px; display: none; }
|
|
78
|
+
li.open .body { display: block; }
|
|
79
|
+
pre { margin: 6px 0; padding: 8px; background: var(--bg2); border-radius: 6px; font: 12px/1.4 var(--mono); white-space: pre-wrap; word-break: break-word; max-height: 260px; overflow: auto; }
|
|
80
|
+
.cmp { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
81
|
+
.cmp div > span { font-size: 12px; color: var(--fg2); }
|
|
82
|
+
@media (max-width: 480px) { .cmp { grid-template-columns: 1fr; } }
|
|
83
|
+
.actions { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
|
|
84
|
+
button.act { font: inherit; font-size: 13px; padding: 5px 10px; border-radius: var(--radius); border: 1px solid var(--line); background: var(--bg); color: var(--fg); cursor: pointer; }
|
|
85
|
+
button.act.primary { background: var(--fg); color: var(--bg); border-color: var(--fg); }
|
|
86
|
+
button.act:disabled { opacity: .55; cursor: default; }
|
|
87
|
+
.muted { color: var(--fg2); }
|
|
88
|
+
.note { font-size: 12px; color: var(--fg2); margin-top: 10px; }
|
|
89
|
+
.empty { padding: 18px; text-align: center; color: var(--fg2); background: var(--bg2); border-radius: var(--radius); }
|
|
90
|
+
.spin { display: inline-block; width: 10px; height: 10px; border: 2px solid var(--fg2); border-right-color: transparent; border-radius: 50%; animation: s 0.8s linear infinite; }
|
|
91
|
+
@keyframes s { to { transform: rotate(360deg); } }
|
|
92
|
+
@media (prefers-reduced-motion: reduce) { .spin { animation: none; } }
|
|
93
|
+
</style>
|
|
94
|
+
</head>
|
|
95
|
+
<body>
|
|
96
|
+
<main id="root"><p class="muted"><span class="spin"></span> Waiting for the test results…</p></main>
|
|
97
|
+
<script>
|
|
98
|
+
(function () {
|
|
99
|
+
"use strict";
|
|
100
|
+
var root = document.getElementById("root");
|
|
101
|
+
var nextId = 1;
|
|
102
|
+
var pending = {};
|
|
103
|
+
var lastInput = {};
|
|
104
|
+
var hostCaps = {};
|
|
105
|
+
var openai = window.openai;
|
|
106
|
+
|
|
107
|
+
// ---- JSON-RPC over postMessage (MCP Apps) ----
|
|
108
|
+
function post(msg) { window.parent.postMessage(msg, "*"); }
|
|
109
|
+
function request(method, params) {
|
|
110
|
+
var id = nextId++;
|
|
111
|
+
return new Promise(function (resolve, reject) {
|
|
112
|
+
pending[id] = { resolve: resolve, reject: reject };
|
|
113
|
+
post({ jsonrpc: "2.0", id: id, method: method, params: params || {} });
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function notify(method, params) { post({ jsonrpc: "2.0", method: method, params: params || {} }); }
|
|
117
|
+
|
|
118
|
+
window.addEventListener("message", function (event) {
|
|
119
|
+
if (event.source !== window.parent) return;
|
|
120
|
+
var msg = event.data;
|
|
121
|
+
if (!msg || msg.jsonrpc !== "2.0") return;
|
|
122
|
+
if (msg.id !== undefined && !msg.method) {
|
|
123
|
+
var p = pending[msg.id];
|
|
124
|
+
if (!p) return;
|
|
125
|
+
delete pending[msg.id];
|
|
126
|
+
if (msg.error) p.reject(new Error(msg.error.message || "request failed"));
|
|
127
|
+
else p.resolve(msg.result);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (msg.method === "ui/notifications/tool-input") { lastInput = (msg.params && msg.params.arguments) || {}; return; }
|
|
131
|
+
if (msg.method === "ui/notifications/tool-result") { render(msg.params || {}); return; }
|
|
132
|
+
if (msg.method === "ui/notifications/tool-cancelled") { message("The tool call was cancelled."); return; }
|
|
133
|
+
if (msg.method === "ui/notifications/host-context-changed") { applyContext(msg.params || {}); return; }
|
|
134
|
+
if (msg.method === "ui/resource-teardown") { post({ jsonrpc: "2.0", id: msg.id, result: {} }); return; }
|
|
135
|
+
if (msg.method === "ping" && msg.id !== undefined) { post({ jsonrpc: "2.0", id: msg.id, result: {} }); }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
function applyContext(ctx) {
|
|
139
|
+
if (ctx.theme === "dark" || ctx.theme === "light") document.documentElement.setAttribute("data-theme", ctx.theme);
|
|
140
|
+
var vars = ctx.styles && ctx.styles.variables;
|
|
141
|
+
if (vars) Object.keys(vars).forEach(function (k) { if (vars[k]) document.documentElement.style.setProperty(k, vars[k]); });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function callTool(name, args) {
|
|
145
|
+
if (openai && typeof openai.callTool === "function") return openai.callTool(name, args);
|
|
146
|
+
return request("tools/call", { name: name, arguments: args || {} });
|
|
147
|
+
}
|
|
148
|
+
function canCallTools() { return !!(openai && openai.callTool) || !!hostCaps.serverTools; }
|
|
149
|
+
function sendMessage(text) {
|
|
150
|
+
if (openai && typeof openai.sendFollowUpMessage === "function") return openai.sendFollowUpMessage({ prompt: text });
|
|
151
|
+
return request("ui/message", { role: "user", content: [{ type: "text", text: text }] });
|
|
152
|
+
}
|
|
153
|
+
function canMessage() { return !!(openai && openai.sendFollowUpMessage) || !!hostCaps.message; }
|
|
154
|
+
|
|
155
|
+
// ---- DOM helpers (textContent only) ----
|
|
156
|
+
function el(tag, cls, text) {
|
|
157
|
+
var node = document.createElement(tag);
|
|
158
|
+
if (cls) node.className = cls;
|
|
159
|
+
if (text !== undefined && text !== null) node.textContent = String(text);
|
|
160
|
+
return node;
|
|
161
|
+
}
|
|
162
|
+
function clear() { while (root.firstChild) root.removeChild(root.firstChild); }
|
|
163
|
+
function message(text) { clear(); root.appendChild(el("p", "muted", text)); }
|
|
164
|
+
function where(f) { return f.file + (f.line ? ":" + f.line : ""); }
|
|
165
|
+
function duration(ms) {
|
|
166
|
+
if (typeof ms !== "number") return "";
|
|
167
|
+
if (ms < 1000) return Math.round(ms) + " ms";
|
|
168
|
+
var s = ms / 1000;
|
|
169
|
+
return s < 60 ? s.toFixed(1) + " s" : Math.floor(s / 60) + "m " + Math.round(s % 60) + "s";
|
|
170
|
+
}
|
|
171
|
+
function button(label, cls, onClick) {
|
|
172
|
+
var b = el("button", "act" + (cls ? " " + cls : ""), label);
|
|
173
|
+
b.type = "button";
|
|
174
|
+
b.addEventListener("click", function () {
|
|
175
|
+
if (b.disabled) return;
|
|
176
|
+
b.disabled = true;
|
|
177
|
+
var before = b.textContent;
|
|
178
|
+
b.textContent = "…";
|
|
179
|
+
Promise.resolve().then(onClick).catch(function (err) {
|
|
180
|
+
b.title = String(err && err.message || err);
|
|
181
|
+
}).then(function () { b.disabled = false; b.textContent = before; });
|
|
182
|
+
});
|
|
183
|
+
return b;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---- views ----
|
|
187
|
+
function render(result) {
|
|
188
|
+
var data = result.structuredContent;
|
|
189
|
+
if (!data && result.content) {
|
|
190
|
+
// Hosts that drop structuredContent: show the Markdown text.
|
|
191
|
+
clear();
|
|
192
|
+
var text = result.content.filter(function (c) { return c.type === "text"; }).map(function (c) { return c.text; }).join("\n\n");
|
|
193
|
+
root.appendChild(el("pre", null, text || "No result."));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
data = data || {};
|
|
197
|
+
if (data.error) return renderError(data.error);
|
|
198
|
+
if (data.run) return renderRun(data);
|
|
199
|
+
if (data.failure) return renderSingleFailure(data);
|
|
200
|
+
if (data.status === "running" && data.runId) return renderRunning(data);
|
|
201
|
+
message(data.ok ? "Done." : "The tool reported a problem; see the text reply.");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function renderError(error) {
|
|
205
|
+
clear();
|
|
206
|
+
var h = el("h1", null, "Could not run the tests");
|
|
207
|
+
h.appendChild(el("span", "badge bad", error.kind || "ERROR"));
|
|
208
|
+
root.appendChild(h);
|
|
209
|
+
root.appendChild(el("pre", null, error.message || ""));
|
|
210
|
+
if (error.hint) root.appendChild(el("p", "muted", error.hint));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderRunning(data) {
|
|
214
|
+
clear();
|
|
215
|
+
var h = el("h1", null, "Tests are running");
|
|
216
|
+
h.appendChild(el("span", "badge warn", data.runId));
|
|
217
|
+
root.appendChild(h);
|
|
218
|
+
var p = el("p", "muted");
|
|
219
|
+
p.appendChild(el("span", "spin"));
|
|
220
|
+
p.appendChild(document.createTextNode(" This report updates when the run finishes."));
|
|
221
|
+
root.appendChild(p);
|
|
222
|
+
if (!canCallTools()) return;
|
|
223
|
+
var poll = function () {
|
|
224
|
+
callTool("get-run-status", { runId: data.runId, waitSeconds: 20 }).then(function (res) {
|
|
225
|
+
var sc = (res && res.structuredContent) || {};
|
|
226
|
+
if (sc.status === "running" && !sc.run) setTimeout(poll, 500);
|
|
227
|
+
else render(res);
|
|
228
|
+
}).catch(function () { setTimeout(poll, 5000); });
|
|
229
|
+
};
|
|
230
|
+
setTimeout(poll, 500);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function renderRun(data) {
|
|
234
|
+
var run = data.run;
|
|
235
|
+
clear();
|
|
236
|
+
var total = run.passed + run.failed + run.flaky + run.skipped;
|
|
237
|
+
var h = el("h1", null, "Playwright test report");
|
|
238
|
+
var verdict = run.failed > 0 ? ["bad", run.failed + " failed"] : run.flaky > 0 ? ["warn", "flaky"] : total === 0 ? ["warn", "no tests ran"] : ["ok", "all passed"];
|
|
239
|
+
h.appendChild(el("span", "badge " + verdict[0], verdict[1]));
|
|
240
|
+
if (run.partial) h.appendChild(el("span", "badge warn", "stopped early"));
|
|
241
|
+
if (data.status === "cancelled") h.appendChild(el("span", "badge warn", "cancelled"));
|
|
242
|
+
root.appendChild(h);
|
|
243
|
+
|
|
244
|
+
var stats = el("div", "stats");
|
|
245
|
+
[["passed", run.passed], ["failed", run.failed], ["flaky", run.flaky], ["skipped", run.skipped]].forEach(function (s) {
|
|
246
|
+
var box = el("div", "stat " + s[0]);
|
|
247
|
+
box.appendChild(el("b", null, s[1]));
|
|
248
|
+
box.appendChild(el("span", null, s[0]));
|
|
249
|
+
stats.appendChild(box);
|
|
250
|
+
});
|
|
251
|
+
var time = el("div", "stat");
|
|
252
|
+
time.appendChild(el("b", null, duration(run.durationMs)));
|
|
253
|
+
time.appendChild(el("span", null, "duration"));
|
|
254
|
+
stats.appendChild(time);
|
|
255
|
+
root.appendChild(stats);
|
|
256
|
+
|
|
257
|
+
if (total > 0) {
|
|
258
|
+
var bar = el("div", "bar");
|
|
259
|
+
bar.setAttribute("role", "img");
|
|
260
|
+
bar.setAttribute("aria-label", run.passed + " passed, " + run.failed + " failed, " + run.flaky + " flaky, " + run.skipped + " skipped");
|
|
261
|
+
[["passed", "var(--ok)"], ["failed", "var(--bad)"], ["flaky", "var(--warn)"], ["skipped", "var(--line)"]].forEach(function (s) {
|
|
262
|
+
if (!run[s[0]]) return;
|
|
263
|
+
var seg = el("i");
|
|
264
|
+
seg.style.width = (100 * run[s[0]] / total) + "%";
|
|
265
|
+
seg.style.background = s[1];
|
|
266
|
+
bar.appendChild(seg);
|
|
267
|
+
});
|
|
268
|
+
root.appendChild(bar);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!run.failures.length) {
|
|
272
|
+
root.appendChild(el("div", "empty", run.failed > 0 ? "Failures were not listed. Ask for get-failure." : "No failures."));
|
|
273
|
+
} else {
|
|
274
|
+
var toolbar = el("div", "toolbar");
|
|
275
|
+
var search = el("input");
|
|
276
|
+
search.type = "search";
|
|
277
|
+
search.placeholder = "Filter failures by title, file or error";
|
|
278
|
+
search.setAttribute("aria-label", "Filter failures");
|
|
279
|
+
toolbar.appendChild(search);
|
|
280
|
+
if (run.failures.length > 1 && canMessage()) {
|
|
281
|
+
toolbar.appendChild(button("Fix all", "primary", function () {
|
|
282
|
+
return sendMessage("Please fix the " + run.failures.length + " failing Playwright tests from the last run. Start with get-failure for each one, then suggest-fix.");
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
root.appendChild(toolbar);
|
|
286
|
+
var list = el("ul");
|
|
287
|
+
run.failures.forEach(function (f, i) { list.appendChild(failureItem(f, i + 1, run.failures.length === 1)); });
|
|
288
|
+
root.appendChild(list);
|
|
289
|
+
search.addEventListener("input", function () {
|
|
290
|
+
var q = search.value.trim().toLowerCase();
|
|
291
|
+
Array.prototype.forEach.call(list.children, function (li) {
|
|
292
|
+
li.style.display = !q || li.getAttribute("data-text").indexOf(q) >= 0 ? "" : "none";
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
if (run.failuresTruncated) root.appendChild(el("p", "note", "Only the first failures are listed; the text reply has the rest."));
|
|
296
|
+
}
|
|
297
|
+
if (run.quarantined) root.appendChild(el("p", "note", run.quarantined + " failing test(s) are quarantined and do not fail the run."));
|
|
298
|
+
var rerun = rerunArgs();
|
|
299
|
+
if (rerun && canCallTools()) {
|
|
300
|
+
var actions = el("div", "actions");
|
|
301
|
+
actions.appendChild(button("Run again", null, function () {
|
|
302
|
+
message("Running the tests again…");
|
|
303
|
+
return callTool("run-test", rerun).then(render);
|
|
304
|
+
}));
|
|
305
|
+
root.appendChild(actions);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function rerunArgs() {
|
|
310
|
+
var keep = {};
|
|
311
|
+
["testFiles", "grep", "browser", "config", "projectRoot", "retries", "workers", "lastFailed", "timeoutMs", "testTimeoutMs"].forEach(function (k) {
|
|
312
|
+
if (lastInput[k] !== undefined) keep[k] = lastInput[k];
|
|
313
|
+
});
|
|
314
|
+
return keep;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function failureItem(f, index, open) {
|
|
318
|
+
var li = el("li", "failure" + (open ? " open" : ""));
|
|
319
|
+
li.setAttribute("data-text", (f.title + " " + f.file + " " + f.message).toLowerCase());
|
|
320
|
+
var head = el("button", "head");
|
|
321
|
+
head.type = "button";
|
|
322
|
+
head.setAttribute("aria-expanded", open ? "true" : "false");
|
|
323
|
+
var t = el("div", "title", f.title);
|
|
324
|
+
t.appendChild(el("span", "kind", f.kind));
|
|
325
|
+
head.appendChild(t);
|
|
326
|
+
head.appendChild(el("div", "where", where(f) + (f.project ? " · " + f.project : "")));
|
|
327
|
+
li.appendChild(head);
|
|
328
|
+
var body = el("div", "body");
|
|
329
|
+
body.appendChild(el("pre", null, f.message));
|
|
330
|
+
var details = el("div");
|
|
331
|
+
body.appendChild(details);
|
|
332
|
+
var actions = el("div", "actions");
|
|
333
|
+
if (canCallTools()) {
|
|
334
|
+
actions.appendChild(button("Show details", null, function () {
|
|
335
|
+
return callTool("get-failure", { index: index }).then(function (res) {
|
|
336
|
+
var sc = (res && res.structuredContent) || {};
|
|
337
|
+
while (details.firstChild) details.removeChild(details.firstChild);
|
|
338
|
+
fillDetails(details, sc.failure, res);
|
|
339
|
+
});
|
|
340
|
+
}));
|
|
341
|
+
actions.appendChild(button("Re-run this test", null, function () {
|
|
342
|
+
message("Running " + where(f) + "…");
|
|
343
|
+
return callTool("run-test", { testFiles: [where(f)] }).then(render);
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
if (canMessage()) {
|
|
347
|
+
actions.appendChild(button("Ask to fix", "primary", function () {
|
|
348
|
+
return sendMessage("Fix the failing Playwright test \"" + f.title + "\" at " + where(f) + ". Call get-failure with index " + index + " first, then suggest-fix.");
|
|
349
|
+
}));
|
|
350
|
+
}
|
|
351
|
+
if (actions.firstChild) body.appendChild(actions);
|
|
352
|
+
li.appendChild(body);
|
|
353
|
+
head.addEventListener("click", function () {
|
|
354
|
+
var isOpen = li.classList.toggle("open");
|
|
355
|
+
head.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
356
|
+
});
|
|
357
|
+
return li;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function fillDetails(target, failure, res) {
|
|
361
|
+
if (failure && (failure.expected || failure.actual)) {
|
|
362
|
+
var cmp = el("div", "cmp");
|
|
363
|
+
var a = el("div"); a.appendChild(el("span", null, "Expected")); a.appendChild(el("pre", null, failure.expected || ""));
|
|
364
|
+
var b = el("div"); b.appendChild(el("span", null, "Received")); b.appendChild(el("pre", null, failure.actual || ""));
|
|
365
|
+
cmp.appendChild(a); cmp.appendChild(b);
|
|
366
|
+
target.appendChild(cmp);
|
|
367
|
+
}
|
|
368
|
+
var text = ((res && res.content) || []).filter(function (c) { return c.type === "text"; }).map(function (c) { return c.text; }).join("\n\n");
|
|
369
|
+
if (text) target.appendChild(el("pre", null, text));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function renderSingleFailure(data) {
|
|
373
|
+
clear();
|
|
374
|
+
var f = data.failure;
|
|
375
|
+
var h = el("h1", null, "Failure " + (data.index || 1) + (data.total ? " of " + data.total : ""));
|
|
376
|
+
h.appendChild(el("span", "badge bad", f.kind));
|
|
377
|
+
root.appendChild(h);
|
|
378
|
+
root.appendChild(el("div", "title", f.title));
|
|
379
|
+
root.appendChild(el("div", "where", where(f)));
|
|
380
|
+
var box = el("div");
|
|
381
|
+
box.appendChild(el("pre", null, f.message));
|
|
382
|
+
fillDetails(box, { expected: f.expected, actual: f.actual }, null);
|
|
383
|
+
root.appendChild(box);
|
|
384
|
+
var actions = el("div", "actions");
|
|
385
|
+
if (canCallTools() && data.total && data.total > 1) {
|
|
386
|
+
var i = data.index || 1;
|
|
387
|
+
if (i > 1) actions.appendChild(button("Previous", null, function () { return callTool("get-failure", { index: i - 1 }).then(render); }));
|
|
388
|
+
if (i < data.total) actions.appendChild(button("Next", null, function () { return callTool("get-failure", { index: i + 1 }).then(render); }));
|
|
389
|
+
}
|
|
390
|
+
if (canMessage()) {
|
|
391
|
+
actions.appendChild(button("Ask to fix", "primary", function () {
|
|
392
|
+
return sendMessage("Fix the failing Playwright test \"" + f.title + "\" at " + where(f) + " with suggest-fix.");
|
|
393
|
+
}));
|
|
394
|
+
}
|
|
395
|
+
if (actions.firstChild) root.appendChild(actions);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---- size reporting ----
|
|
399
|
+
function reportSize() {
|
|
400
|
+
var h = Math.ceil(document.documentElement.getBoundingClientRect().height);
|
|
401
|
+
notify("ui/notifications/size-changed", { width: Math.ceil(window.innerWidth), height: h });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---- start ----
|
|
405
|
+
if (openai) {
|
|
406
|
+
// ChatGPT's window.openai bridge: the result is already there.
|
|
407
|
+
if (openai.toolInput) lastInput = openai.toolInput;
|
|
408
|
+
if (openai.toolOutput) render({ structuredContent: openai.toolOutput });
|
|
409
|
+
window.addEventListener("openai:set_globals", function () {
|
|
410
|
+
if (window.openai.toolOutput) render({ structuredContent: window.openai.toolOutput });
|
|
411
|
+
});
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
request("ui/initialize", {
|
|
415
|
+
appInfo: { name: "playwright-e2e-mcp-report", version: "1.0.0" },
|
|
416
|
+
appCapabilities: {},
|
|
417
|
+
protocolVersion: "2026-01-26"
|
|
418
|
+
}).then(function (init) {
|
|
419
|
+
hostCaps = (init && init.hostCapabilities) || {};
|
|
420
|
+
applyContext((init && init.hostContext) || {});
|
|
421
|
+
notify("ui/notifications/initialized", {});
|
|
422
|
+
if (typeof ResizeObserver === "function") new ResizeObserver(reportSize).observe(document.body);
|
|
423
|
+
reportSize();
|
|
424
|
+
}).catch(function () { /* not inside an MCP Apps host */ });
|
|
425
|
+
})();
|
|
426
|
+
</script>
|
|
427
|
+
</body>
|
|
428
|
+
</html>
|
|
429
|
+
`;
|
|
430
|
+
//# sourceMappingURL=test-report-html.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-report-html.js","sourceRoot":"","sources":["../../src/apps/test-report-html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8ZzC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -93,6 +93,8 @@ async function main() {
|
|
|
93
93
|
' PW_MCP_PYTHON Python for pytest-playwright projects (default: project venv, python3)',
|
|
94
94
|
' PW_MCP_LOGIN_ENV extra variables save-login may type ({{env:NAME}}), comma separated',
|
|
95
95
|
' PW_MCP_PROTOCOL legacy = do not offer the 2026-07-28 MCP revision',
|
|
96
|
+
' PW_MCP_CONFIRM_WRITES auto | always | off: ask before changing files (default auto)',
|
|
97
|
+
' PW_MCP_APPS=0 no interactive test report in MCP Apps clients',
|
|
96
98
|
' PW_MCP_SKILLS 0 = do not serve the bundled skill over the Skills extension',
|
|
97
99
|
' PW_MCP_NO_UPDATE_CHECK 1 = doctor does not ask npm whether a newer version is out',
|
|
98
100
|
' LOG_LEVEL / MCP_LOG_LEVEL debug | info | warn | error | silent (default: info)',
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/oauth.d.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Clients such as claude.ai custom connectors cannot send a fixed bearer
|
|
5
5
|
* token; they discover an authorization server (RFC 9728 / RFC 8414),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* identify themselves and run the authorization-code flow with PKCE. A
|
|
7
|
+
* client identifies itself with a Client ID Metadata Document (an https
|
|
8
|
+
* `client_id` this server fetches; the MCP default since 2025-11-25), or,
|
|
9
|
+
* for older clients, by dynamic registration (RFC 7591). This module is that authorization server, kept stateless so it fits a
|
|
8
10
|
* serverless function: client ids, codes and tokens are HMAC-signed payloads
|
|
9
11
|
* keyed from PW_MCP_HTTP_TOKEN, so nothing is stored and rotating the token
|
|
10
12
|
* signs every client out.
|
|
@@ -14,6 +16,7 @@
|
|
|
14
16
|
* already grants and nothing more.
|
|
15
17
|
*/
|
|
16
18
|
import type { AttemptLimiter } from './utils/attempt-limiter.js';
|
|
19
|
+
import type { ClientMetadataFetcher } from './utils/client-metadata.js';
|
|
17
20
|
export interface OAuthOptions {
|
|
18
21
|
/** The bridge's bearer token: the access key users type, and the signing secret. */
|
|
19
22
|
token: string;
|
|
@@ -28,6 +31,13 @@ export interface OAuthOptions {
|
|
|
28
31
|
* `.example.com` for its subdomains). Empty allows any https host.
|
|
29
32
|
*/
|
|
30
33
|
redirectHosts?: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Accept Client ID Metadata Documents (https client ids). Default true;
|
|
36
|
+
* PW_MCP_OAUTH_CIMD=0 turns it off.
|
|
37
|
+
*/
|
|
38
|
+
clientMetadataDocuments?: boolean;
|
|
39
|
+
/** Fetches and checks a metadata document; tests pass a fake. */
|
|
40
|
+
fetchClientMetadata?: ClientMetadataFetcher;
|
|
31
41
|
}
|
|
32
42
|
export interface OAuthServer {
|
|
33
43
|
/** Answer an OAuth or discovery request, or return undefined for anything else. */
|
package/dist/oauth.js
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Clients such as claude.ai custom connectors cannot send a fixed bearer
|
|
5
5
|
* token; they discover an authorization server (RFC 9728 / RFC 8414),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* identify themselves and run the authorization-code flow with PKCE. A
|
|
7
|
+
* client identifies itself with a Client ID Metadata Document (an https
|
|
8
|
+
* `client_id` this server fetches; the MCP default since 2025-11-25), or,
|
|
9
|
+
* for older clients, by dynamic registration (RFC 7591). This module is that authorization server, kept stateless so it fits a
|
|
8
10
|
* serverless function: client ids, codes and tokens are HMAC-signed payloads
|
|
9
11
|
* keyed from PW_MCP_HTTP_TOKEN, so nothing is stored and rotating the token
|
|
10
12
|
* signs every client out.
|
|
@@ -15,6 +17,7 @@
|
|
|
15
17
|
*/
|
|
16
18
|
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
|
17
19
|
import { clientAddress } from './utils/attempt-limiter.js';
|
|
20
|
+
import { ClientMetadataError, createClientMetadataFetcher, isClientMetadataUrl } from './utils/client-metadata.js';
|
|
18
21
|
/** Lifetimes, in seconds. */
|
|
19
22
|
const CODE_TTL = 5 * 60;
|
|
20
23
|
const ACCESS_TTL = 60 * 60;
|
|
@@ -117,6 +120,8 @@ export function createOAuthServer(options) {
|
|
|
117
120
|
const key = createHmac('sha256', options.token).update('playwright-e2e-mcp oauth v1').digest();
|
|
118
121
|
const redirectHosts = options.redirectHosts ?? [];
|
|
119
122
|
const redirectAllowed = (uri) => isAllowedRedirectUri(uri) && isAllowedRedirectHost(uri, redirectHosts);
|
|
123
|
+
const cimd = options.clientMetadataDocuments ?? process.env.PW_MCP_OAUTH_CIMD !== '0';
|
|
124
|
+
const fetchClientMetadata = options.fetchClientMetadata ?? createClientMetadataFetcher({ redirectAllowed, now });
|
|
120
125
|
function sign(payload) {
|
|
121
126
|
const body = base64url(JSON.stringify(payload));
|
|
122
127
|
const mac = base64url(createHmac('sha256', key).update(body).digest());
|
|
@@ -148,12 +153,34 @@ export function createOAuthServer(options) {
|
|
|
148
153
|
function clientTag(clientId) {
|
|
149
154
|
return base64url(sha256(clientId)).slice(0, 22);
|
|
150
155
|
}
|
|
151
|
-
|
|
156
|
+
/** A client registered with /oauth/register (its id is a signed payload). */
|
|
157
|
+
function registeredClient(clientId) {
|
|
152
158
|
const payload = verify(clientId, 'client');
|
|
153
159
|
if (!payload || !Array.isArray(payload.r))
|
|
154
160
|
return undefined;
|
|
155
161
|
return { redirectUris: payload.r.filter(redirectAllowed), name: typeof payload.n === 'string' ? payload.n : '' };
|
|
156
162
|
}
|
|
163
|
+
/** True for a client id this server accepts without fetching anything. */
|
|
164
|
+
function knownClientId(clientId) {
|
|
165
|
+
return Boolean(registeredClient(clientId)) || (cimd && isClientMetadataUrl(clientId));
|
|
166
|
+
}
|
|
167
|
+
/** Resolve a client id: a signed registration, or a metadata document URL. */
|
|
168
|
+
async function resolveClient(clientId) {
|
|
169
|
+
if (!clientId)
|
|
170
|
+
return undefined;
|
|
171
|
+
const registered = registeredClient(clientId);
|
|
172
|
+
if (registered)
|
|
173
|
+
return registered;
|
|
174
|
+
if (!cimd || !isClientMetadataUrl(clientId))
|
|
175
|
+
return undefined;
|
|
176
|
+
try {
|
|
177
|
+
const metadata = await fetchClientMetadata(clientId);
|
|
178
|
+
return { redirectUris: metadata.redirectUris.filter(redirectAllowed), name: metadata.name, documentHost: new URL(clientId).host };
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
return { error: err instanceof ClientMetadataError ? err.message : 'the client metadata document could not be read' };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
157
184
|
function issueTokens(tag) {
|
|
158
185
|
const issued = now();
|
|
159
186
|
return json(200, {
|
|
@@ -186,6 +213,8 @@ export function createOAuthServer(options) {
|
|
|
186
213
|
code_challenge_methods_supported: ['S256'],
|
|
187
214
|
token_endpoint_auth_methods_supported: ['none'],
|
|
188
215
|
scopes_supported: ['mcp'],
|
|
216
|
+
// Clients may use an https URL as client_id (draft-ietf-oauth-client-id-metadata-document).
|
|
217
|
+
client_id_metadata_document_supported: cimd,
|
|
189
218
|
});
|
|
190
219
|
}
|
|
191
220
|
async function register(request) {
|
|
@@ -253,7 +282,10 @@ export function createOAuthServer(options) {
|
|
|
253
282
|
const isPost = request.method === 'POST';
|
|
254
283
|
const params = isPost ? await readParams(request) : new URL(request.url).searchParams;
|
|
255
284
|
const clientId = params.get('client_id');
|
|
256
|
-
const registered =
|
|
285
|
+
const registered = await resolveClient(clientId);
|
|
286
|
+
if (registered && 'error' in registered) {
|
|
287
|
+
return page(400, 'Sign-in failed', `<h1>Unknown client</h1><p>This app's description could not be used: ${escapeHtml(registered.error)}.</p>`);
|
|
288
|
+
}
|
|
257
289
|
if (!clientId || !registered) {
|
|
258
290
|
return page(400, 'Sign-in failed', '<h1>Unknown client</h1><p>This app is not registered with the server. Try connecting again.</p>');
|
|
259
291
|
}
|
|
@@ -292,9 +324,13 @@ export function createOAuthServer(options) {
|
|
|
292
324
|
.join('');
|
|
293
325
|
const appName = registered.name ? `<strong>${escapeHtml(registered.name)}</strong>` : 'An app';
|
|
294
326
|
const host = new URL(redirectUri).host;
|
|
327
|
+
// For a metadata document, the domain that published it is who the app is.
|
|
328
|
+
const verifiedBy = registered.documentHost && registered.documentHost !== host
|
|
329
|
+
? ` (described by <code>${escapeHtml(registered.documentHost)}</code>)`
|
|
330
|
+
: '';
|
|
295
331
|
const error = isPost ? '<p class="error">That access key is not right. Try again.</p>' : '';
|
|
296
332
|
return page(isPost ? 401 : 200, 'Sign in to playwright-e2e-mcp', `<h1>Sign in to playwright-e2e-mcp</h1>
|
|
297
|
-
<p>${appName} at <code>${escapeHtml(host)}</code
|
|
333
|
+
<p>${appName} at <code>${escapeHtml(host)}</code>${verifiedBy} wants to use this server's Playwright tools. Enter the server's access key to allow it.</p>
|
|
298
334
|
${error}<form method="post" action="/oauth/authorize">${hidden}
|
|
299
335
|
<label for="access_key">Access key</label>
|
|
300
336
|
<input id="access_key" name="access_key" type="password" autocomplete="current-password" required autofocus>
|
|
@@ -304,7 +340,9 @@ ${error}<form method="post" action="/oauth/authorize">${hidden}
|
|
|
304
340
|
async function tokenEndpoint(request) {
|
|
305
341
|
const params = await readParams(request);
|
|
306
342
|
const clientId = params.get('client_id');
|
|
307
|
-
|
|
343
|
+
// Codes and refresh tokens are bound to the client id, so a metadata
|
|
344
|
+
// document URL needs no second fetch here.
|
|
345
|
+
if (clientId && !knownClientId(clientId))
|
|
308
346
|
return oauthError(401, 'invalid_client', 'Unknown client.');
|
|
309
347
|
switch (params.get('grant_type')) {
|
|
310
348
|
case 'authorization_code': {
|