@mcp-pane/playwright 0.1.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 +123 -0
- package/dist/fixture.d.ts +79 -0
- package/dist/fixture.d.ts.map +1 -0
- package/dist/fixture.js +200 -0
- package/dist/fixture.js.map +1 -0
- package/dist/host-page.d.ts +17 -0
- package/dist/host-page.d.ts.map +1 -0
- package/dist/host-page.js +160 -0
- package/dist/host-page.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/test.d.ts +33 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +68 -0
- package/dist/test.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# @mcp-pane/playwright
|
|
2
|
+
|
|
3
|
+
**Playwright fixtures for end-to-end testing of MCP Apps in a real browser.**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install --save-dev @mcp-pane/playwright @playwright/test
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`@mcp-pane/playwright` lets you test the **actual UI** of your MCP App — the iframe, the postMessage bridge, the bidirectional flow — in a real browser, the same way Claude Desktop or VS Code would render it.
|
|
10
|
+
|
|
11
|
+
This is the **only** testing tool for MCP Apps that exercises the iframe and the host↔UI postMessage protocol end-to-end. Protocol-level tests with `@mcp-pane/test` are great for tools and resources, but they can't catch UI regressions.
|
|
12
|
+
|
|
13
|
+
## What it gives you
|
|
14
|
+
|
|
15
|
+
- A `mcpApp` fixture that spins up your server, renders the UI in an iframe inside a real Chromium page, and bridges postMessage exactly like a real MCP host does
|
|
16
|
+
- `mcpApp.iframe` — Playwright `FrameLocator` for the UI inside the sandbox
|
|
17
|
+
- `mcpApp.toolCalls` — observable list of tool calls the UI made via postMessage
|
|
18
|
+
- `mcpApp.mockTool()` — override any tool's response, propagated to the UI
|
|
19
|
+
- `mcpApp.getRecording()` — full session capture (tool calls + postMessage) auto-attached to failed test reports
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { test, expect } from '@mcp-pane/playwright';
|
|
25
|
+
|
|
26
|
+
test.use({
|
|
27
|
+
server: {
|
|
28
|
+
command: 'node',
|
|
29
|
+
args: ['./dist/server.js'],
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('dashboard opens and renders KPIs', async ({ mcpApp }) => {
|
|
34
|
+
await mcpApp.open('show_dashboard', { period: 'week' });
|
|
35
|
+
|
|
36
|
+
await expect(mcpApp.iframe.locator('text=Revenue')).toBeVisible();
|
|
37
|
+
await expect(mcpApp.iframe.locator('text=/\\$[\\d,]+/')).toBeVisible();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('bidirectional: clicking in iframe triggers tool/call back to server', async ({ mcpApp }) => {
|
|
41
|
+
await mcpApp.open('show_dashboard', { period: 'week' });
|
|
42
|
+
|
|
43
|
+
// User clicks a button inside the iframe
|
|
44
|
+
await mcpApp.iframe.locator('button:has-text("month")').click();
|
|
45
|
+
|
|
46
|
+
// The UI posts a tool/call back through our host. We observe it.
|
|
47
|
+
await expect.poll(() => mcpApp.toolCalls).toContainEqual(
|
|
48
|
+
expect.objectContaining({
|
|
49
|
+
name: 'show_dashboard',
|
|
50
|
+
args: expect.objectContaining({ period: 'month' }),
|
|
51
|
+
})
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// And the UI re-renders with the new data.
|
|
55
|
+
await expect(mcpApp.iframe.locator('text=monthly')).toBeVisible();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('UI handles extreme values gracefully', async ({ mcpApp }) => {
|
|
59
|
+
// Mock before open so the UI gets our crafted data
|
|
60
|
+
mcpApp.mockTool('show_dashboard', () => ({
|
|
61
|
+
totals: { revenue: 0, deals: 0 }, // edge case
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
await mcpApp.open('show_dashboard', { period: 'week' });
|
|
65
|
+
|
|
66
|
+
await expect(mcpApp.iframe.locator('text=$0')).toBeVisible();
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## How it works under the hood
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
┌─────────────────────────────────────────────────────┐
|
|
74
|
+
│ Playwright (Chromium) │
|
|
75
|
+
│ ┌───────────────────────────────────────────────┐ │
|
|
76
|
+
│ │ Headless host page │ │
|
|
77
|
+
│ │ ┌─────────────────────────────────────────┐ │ │
|
|
78
|
+
│ │ │ <iframe sandbox="allow-scripts"> │ │ │
|
|
79
|
+
│ │ │ YOUR MCP APP UI │ │ │
|
|
80
|
+
│ │ │ (real, sandboxed, no same-origin) │ │ │
|
|
81
|
+
│ │ └─────────────────────────────────────────┘ │ │
|
|
82
|
+
│ │ ↕ postMessage │ │
|
|
83
|
+
│ │ window.__mcpPane (host bridge) │ │
|
|
84
|
+
│ └────────────────┬──────────────────────────────┘ │
|
|
85
|
+
└───────────────────┼─────────────────────────────────┘
|
|
86
|
+
↕ Playwright evaluate
|
|
87
|
+
↕ poll for new tool/calls
|
|
88
|
+
┌───────────────────┴─────────────────────────────────┐
|
|
89
|
+
│ McpAppFixture (Node.js test process) │
|
|
90
|
+
│ ↕ McpHarness │
|
|
91
|
+
│ ↕ stdio JSON-RPC │
|
|
92
|
+
│ YOUR MCP SERVER (child process) │
|
|
93
|
+
└─────────────────────────────────────────────────────┘
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The fixture spawns your MCP server through the same `McpHarness` from `@mcp-pane/test`, opens a Playwright page with a minimal "host shell" HTML, and polls `window.__mcpPane.getToolCalls()` to detect new tool calls from the UI. Each call is automatically proxied through the harness (or a mock you set), and the result is pushed back as a `tool/result` postMessage.
|
|
97
|
+
|
|
98
|
+
This is **exactly** the lifecycle a real MCP host implements. If your tests pass here, your UI works in Claude Desktop and VS Code.
|
|
99
|
+
|
|
100
|
+
## Auto-attached recordings on failure
|
|
101
|
+
|
|
102
|
+
When a test fails, the fixture automatically attaches a JSON recording of the session to the Playwright report:
|
|
103
|
+
|
|
104
|
+
- Every `tools/call` with args, result, duration
|
|
105
|
+
- Every `postMessage` between host and iframe, with direction
|
|
106
|
+
- Timing relative to test start
|
|
107
|
+
|
|
108
|
+
Open the HTML report, click the failed test, find `mcp-recording.json` in the attachments. This usually tells you immediately what went wrong.
|
|
109
|
+
|
|
110
|
+
## Browser support
|
|
111
|
+
|
|
112
|
+
Anything Playwright supports — Chromium, Firefox, WebKit. Defaults to Chromium since that's what Claude Desktop and VS Code use under the hood (Electron / Chromium-based).
|
|
113
|
+
|
|
114
|
+
## Companion packages
|
|
115
|
+
|
|
116
|
+
- **[@mcp-pane/test](https://npmjs.com/@mcp-pane/test)** — Vitest/Jest utilities for protocol-level tests (same `McpHarness` under the hood)
|
|
117
|
+
- **[mcp-pane](https://github.com/YOUR/mcp-pane)** — scaffolder for new MCP Apps
|
|
118
|
+
|
|
119
|
+
Works with any MCP server — official SDK, `mcp-use`, anything that speaks the standard MCP protocol with the Apps extension.
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpAppFixture — то, что тест получает через Playwright fixture.
|
|
3
|
+
*
|
|
4
|
+
* Объединяет McpHarness (наш test-пакет) и Playwright Page.
|
|
5
|
+
* Жизненный цикл:
|
|
6
|
+
* 1. Playwright создаёт fixture: поднимает MCP-сервер через Harness,
|
|
7
|
+
* открывает страницу с host-page HTML.
|
|
8
|
+
* 2. Тест вызывает `mcpApp.open('show_dashboard', {...})`:
|
|
9
|
+
* - harness.callTool('show_dashboard', args) — данные
|
|
10
|
+
* - harness.readUiForTool('show_dashboard') — HTML
|
|
11
|
+
* - page.evaluate(__mcpPane.openApp(...)) — рендерим в iframe
|
|
12
|
+
* 3. Тест взаимодействует с iframe через `mcpApp.iframe` (Playwright FrameLocator).
|
|
13
|
+
* 4. После теста — fixture closes harness и context.
|
|
14
|
+
*
|
|
15
|
+
* Bidirectional flow: UI внутри iframe шлёт postMessage `tool/call`,
|
|
16
|
+
* host-page их собирает. Fixture регулярно опрашивает через
|
|
17
|
+
* `getToolCalls()` и автоматически проксирует на реальный сервер через
|
|
18
|
+
* harness (или применяет mock'и).
|
|
19
|
+
*/
|
|
20
|
+
import type { Page, FrameLocator } from '@playwright/test';
|
|
21
|
+
import { type HarnessOptions } from '@mcp-pane/test';
|
|
22
|
+
import type { ToolMock } from '@mcp-pane/test';
|
|
23
|
+
export type McpAppFixtureOptions = HarnessOptions & {
|
|
24
|
+
/** Интервал опроса postMessage от UI в миллисекундах (по умолчанию 50ms). */
|
|
25
|
+
pollInterval?: number;
|
|
26
|
+
};
|
|
27
|
+
type ToolCallFromUi = {
|
|
28
|
+
name: string;
|
|
29
|
+
args: Record<string, unknown>;
|
|
30
|
+
callId?: string;
|
|
31
|
+
};
|
|
32
|
+
export declare class McpAppFixture {
|
|
33
|
+
readonly page: Page;
|
|
34
|
+
private harness;
|
|
35
|
+
private polling;
|
|
36
|
+
private pollTimer?;
|
|
37
|
+
private processedSeq;
|
|
38
|
+
private toolCallHistory;
|
|
39
|
+
private pollInterval;
|
|
40
|
+
private constructor();
|
|
41
|
+
/**
|
|
42
|
+
* Фабрика. Подключает MCP-сервер, открывает host-page, начинает polling.
|
|
43
|
+
* Должна вызываться внутри Playwright fixture'а.
|
|
44
|
+
*/
|
|
45
|
+
static create(page: Page, opts: McpAppFixtureOptions): Promise<McpAppFixture>;
|
|
46
|
+
/**
|
|
47
|
+
* Вызвать tool сервера и отрендерить его UI в iframe.
|
|
48
|
+
*
|
|
49
|
+
* После вызова `mcpApp.iframe` указывает на свежемонтированный iframe.
|
|
50
|
+
*/
|
|
51
|
+
open(toolName: string, args?: Record<string, unknown>): Promise<void>;
|
|
52
|
+
/** Playwright FrameLocator для iframe с загруженным UI. */
|
|
53
|
+
get iframe(): FrameLocator;
|
|
54
|
+
/**
|
|
55
|
+
* Все tool/call сообщения, которые UI прислал из iframe с момента
|
|
56
|
+
* открытия (или последнего clearHistory). Каждый вызов в этом
|
|
57
|
+
* списке означает, что UI в iframe попросил host'а вызвать tool.
|
|
58
|
+
*/
|
|
59
|
+
get toolCalls(): readonly ToolCallFromUi[];
|
|
60
|
+
/** Очистить историю tool/call'ов от UI. */
|
|
61
|
+
clearToolCalls(): void;
|
|
62
|
+
/** Делегируем в harness. */
|
|
63
|
+
mockTool(name: string, mock: ToolMock): void;
|
|
64
|
+
unmockTool(name: string): void;
|
|
65
|
+
clearMocks(): void;
|
|
66
|
+
/**
|
|
67
|
+
* Возвращает полную запись сессии, включая postMessage'ы между
|
|
68
|
+
* host'ом и iframe (для time-travel debugging).
|
|
69
|
+
*/
|
|
70
|
+
getRecording(outcome?: 'pass' | 'fail' | 'unknown'): Promise<import("@mcp-pane/test").SessionRecording>;
|
|
71
|
+
close(): Promise<void>;
|
|
72
|
+
private startPolling;
|
|
73
|
+
private stopPolling;
|
|
74
|
+
private scheduleNextPoll;
|
|
75
|
+
private poll;
|
|
76
|
+
private proxyToolCall;
|
|
77
|
+
}
|
|
78
|
+
export {};
|
|
79
|
+
//# sourceMappingURL=fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../src/fixture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAI/C,MAAM,MAAM,oBAAoB,GAAG,cAAc,GAAG;IAClD,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,qBAAa,aAAa;aASN,IAAI,EAAE,IAAI;IAR5B,OAAO,CAAC,OAAO,CAAa;IAC5B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAC,CAAgC;IAClD,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO;IASP;;;OAGG;WACU,MAAM,CACjB,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,aAAa,CAAC;IAmBzB;;;;OAIG;IACG,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA2B/E,2DAA2D;IAC3D,IAAI,MAAM,IAAI,YAAY,CAEzB;IAMD;;;;OAIG;IACH,IAAI,SAAS,IAAI,SAAS,cAAc,EAAE,CAEzC;IAED,2CAA2C;IAC3C,cAAc,IAAI,IAAI;IAQtB,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI;IAI5C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAI9B,UAAU,IAAI,IAAI;IAQlB;;;OAGG;IACG,YAAY,CAAC,OAAO,GAAE,MAAM,GAAG,MAAM,GAAG,SAAqB;IAsB7D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,WAAW;IAQnB,OAAO,CAAC,gBAAgB;YAKV,IAAI;YA0BJ,aAAa;CAyB5B"}
|
package/dist/fixture.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* McpAppFixture — то, что тест получает через Playwright fixture.
|
|
3
|
+
*
|
|
4
|
+
* Объединяет McpHarness (наш test-пакет) и Playwright Page.
|
|
5
|
+
* Жизненный цикл:
|
|
6
|
+
* 1. Playwright создаёт fixture: поднимает MCP-сервер через Harness,
|
|
7
|
+
* открывает страницу с host-page HTML.
|
|
8
|
+
* 2. Тест вызывает `mcpApp.open('show_dashboard', {...})`:
|
|
9
|
+
* - harness.callTool('show_dashboard', args) — данные
|
|
10
|
+
* - harness.readUiForTool('show_dashboard') — HTML
|
|
11
|
+
* - page.evaluate(__mcpPane.openApp(...)) — рендерим в iframe
|
|
12
|
+
* 3. Тест взаимодействует с iframe через `mcpApp.iframe` (Playwright FrameLocator).
|
|
13
|
+
* 4. После теста — fixture closes harness и context.
|
|
14
|
+
*
|
|
15
|
+
* Bidirectional flow: UI внутри iframe шлёт postMessage `tool/call`,
|
|
16
|
+
* host-page их собирает. Fixture регулярно опрашивает через
|
|
17
|
+
* `getToolCalls()` и автоматически проксирует на реальный сервер через
|
|
18
|
+
* harness (или применяет mock'и).
|
|
19
|
+
*/
|
|
20
|
+
import { McpHarness } from '@mcp-pane/test';
|
|
21
|
+
import { buildHostPageHtml } from './host-page.js';
|
|
22
|
+
export class McpAppFixture {
|
|
23
|
+
page;
|
|
24
|
+
harness;
|
|
25
|
+
polling = false;
|
|
26
|
+
pollTimer;
|
|
27
|
+
processedSeq = 0;
|
|
28
|
+
toolCallHistory = [];
|
|
29
|
+
pollInterval;
|
|
30
|
+
constructor(page, harness, opts) {
|
|
31
|
+
this.page = page;
|
|
32
|
+
this.harness = harness;
|
|
33
|
+
this.pollInterval = opts.pollInterval ?? 50;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Фабрика. Подключает MCP-сервер, открывает host-page, начинает polling.
|
|
37
|
+
* Должна вызываться внутри Playwright fixture'а.
|
|
38
|
+
*/
|
|
39
|
+
static async create(page, opts) {
|
|
40
|
+
const harness = await McpHarness.create(opts);
|
|
41
|
+
// Грузим host-page как data URL — не нужен веб-сервер.
|
|
42
|
+
const html = buildHostPageHtml();
|
|
43
|
+
await page.setContent(html, { waitUntil: 'load' });
|
|
44
|
+
// Ждём, пока __mcpPane появится на window (наша guarantee).
|
|
45
|
+
await page.waitForFunction(() => window.__mcpPane?.ready === true);
|
|
46
|
+
const fixture = new McpAppFixture(page, harness, opts);
|
|
47
|
+
fixture.startPolling();
|
|
48
|
+
return fixture;
|
|
49
|
+
}
|
|
50
|
+
// ============================================================
|
|
51
|
+
// Открытие приложения
|
|
52
|
+
// ============================================================
|
|
53
|
+
/**
|
|
54
|
+
* Вызвать tool сервера и отрендерить его UI в iframe.
|
|
55
|
+
*
|
|
56
|
+
* После вызова `mcpApp.iframe` указывает на свежемонтированный iframe.
|
|
57
|
+
*/
|
|
58
|
+
async open(toolName, args = {}) {
|
|
59
|
+
// 1. Узнаём URI UI-ресурса
|
|
60
|
+
const meta = await this.harness.getToolUiMeta(toolName);
|
|
61
|
+
if (!meta?.resourceUri) {
|
|
62
|
+
throw new Error(`Tool "${toolName}" has no UI metadata — not an MCP App tool`);
|
|
63
|
+
}
|
|
64
|
+
// 2. Вызываем tool и читаем HTML параллельно
|
|
65
|
+
const [data, html] = await Promise.all([
|
|
66
|
+
this.harness.callTool(toolName, args),
|
|
67
|
+
this.harness.readUiResource(meta.resourceUri),
|
|
68
|
+
]);
|
|
69
|
+
// 3. Просим host-page смонтировать iframe
|
|
70
|
+
await this.page.evaluate(({ uri, html, data }) => window.__mcpPane.openApp(uri, html, data), { uri: meta.resourceUri, html, data });
|
|
71
|
+
// 4. Сбрасываем processedSeq — это новый iframe, история начинается с 0
|
|
72
|
+
this.processedSeq = 0;
|
|
73
|
+
}
|
|
74
|
+
// ============================================================
|
|
75
|
+
// Доступ к UI
|
|
76
|
+
// ============================================================
|
|
77
|
+
/** Playwright FrameLocator для iframe с загруженным UI. */
|
|
78
|
+
get iframe() {
|
|
79
|
+
return this.page.frameLocator('iframe[data-mcp-uri]');
|
|
80
|
+
}
|
|
81
|
+
// ============================================================
|
|
82
|
+
// Tool calls от UI
|
|
83
|
+
// ============================================================
|
|
84
|
+
/**
|
|
85
|
+
* Все tool/call сообщения, которые UI прислал из iframe с момента
|
|
86
|
+
* открытия (или последнего clearHistory). Каждый вызов в этом
|
|
87
|
+
* списке означает, что UI в iframe попросил host'а вызвать tool.
|
|
88
|
+
*/
|
|
89
|
+
get toolCalls() {
|
|
90
|
+
return this.toolCallHistory;
|
|
91
|
+
}
|
|
92
|
+
/** Очистить историю tool/call'ов от UI. */
|
|
93
|
+
clearToolCalls() {
|
|
94
|
+
this.toolCallHistory = [];
|
|
95
|
+
}
|
|
96
|
+
// ============================================================
|
|
97
|
+
// Mocking
|
|
98
|
+
// ============================================================
|
|
99
|
+
/** Делегируем в harness. */
|
|
100
|
+
mockTool(name, mock) {
|
|
101
|
+
this.harness.mockTool(name, mock);
|
|
102
|
+
}
|
|
103
|
+
unmockTool(name) {
|
|
104
|
+
this.harness.unmockTool(name);
|
|
105
|
+
}
|
|
106
|
+
clearMocks() {
|
|
107
|
+
this.harness.clearMocks();
|
|
108
|
+
}
|
|
109
|
+
// ============================================================
|
|
110
|
+
// Recording
|
|
111
|
+
// ============================================================
|
|
112
|
+
/**
|
|
113
|
+
* Возвращает полную запись сессии, включая postMessage'ы между
|
|
114
|
+
* host'ом и iframe (для time-travel debugging).
|
|
115
|
+
*/
|
|
116
|
+
async getRecording(outcome = 'unknown') {
|
|
117
|
+
// Подтягиваем последние postMessages из браузерного контекста.
|
|
118
|
+
const [received, sent] = await Promise.all([
|
|
119
|
+
this.page.evaluate(() => window.__mcpPane.getReceivedMessages()),
|
|
120
|
+
this.page.evaluate(() => window.__mcpPane.getSentMessages()),
|
|
121
|
+
]);
|
|
122
|
+
// Аккумулируем postMessages в harness recording.
|
|
123
|
+
for (const m of received) {
|
|
124
|
+
this.harness.recordPostMessage({ direction: 'ui-to-host', data: m.data });
|
|
125
|
+
}
|
|
126
|
+
for (const m of sent) {
|
|
127
|
+
this.harness.recordPostMessage({ direction: 'host-to-ui', data: m.data });
|
|
128
|
+
}
|
|
129
|
+
return this.harness.getRecording(outcome);
|
|
130
|
+
}
|
|
131
|
+
// ============================================================
|
|
132
|
+
// Lifecycle
|
|
133
|
+
// ============================================================
|
|
134
|
+
async close() {
|
|
135
|
+
this.stopPolling();
|
|
136
|
+
await this.harness.close();
|
|
137
|
+
}
|
|
138
|
+
// ============================================================
|
|
139
|
+
// Internal: polling postMessage от UI
|
|
140
|
+
// ============================================================
|
|
141
|
+
startPolling() {
|
|
142
|
+
this.polling = true;
|
|
143
|
+
this.scheduleNextPoll();
|
|
144
|
+
}
|
|
145
|
+
stopPolling() {
|
|
146
|
+
this.polling = false;
|
|
147
|
+
if (this.pollTimer) {
|
|
148
|
+
clearTimeout(this.pollTimer);
|
|
149
|
+
this.pollTimer = undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
scheduleNextPoll() {
|
|
153
|
+
if (!this.polling)
|
|
154
|
+
return;
|
|
155
|
+
this.pollTimer = setTimeout(() => this.poll(), this.pollInterval);
|
|
156
|
+
}
|
|
157
|
+
async poll() {
|
|
158
|
+
if (!this.polling)
|
|
159
|
+
return;
|
|
160
|
+
try {
|
|
161
|
+
const calls = await this.page.evaluate(() => window.__mcpPane.getToolCalls());
|
|
162
|
+
// Берём только новые (после processedSeq).
|
|
163
|
+
const newCalls = calls.slice(this.processedSeq);
|
|
164
|
+
this.processedSeq = calls.length;
|
|
165
|
+
// Проксируем каждый tool call через harness (он сам решит — реальный или mock).
|
|
166
|
+
for (const call of newCalls) {
|
|
167
|
+
this.toolCallHistory.push(call);
|
|
168
|
+
this.proxyToolCall(call).catch((err) => {
|
|
169
|
+
// Логируем, но не падаем — тест сам решит, как реагировать.
|
|
170
|
+
console.error('[mcp-pane/playwright] tool/call failed:', err);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
// Page might be navigating or closed — skip this tick.
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
this.scheduleNextPoll();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async proxyToolCall(call) {
|
|
182
|
+
try {
|
|
183
|
+
const result = await this.harness.callTool(call.name, call.args);
|
|
184
|
+
await this.page.evaluate(({ result, callId }) => window.__mcpPane.pushToUi({
|
|
185
|
+
type: 'tool/result',
|
|
186
|
+
callId,
|
|
187
|
+
payload: result,
|
|
188
|
+
}), { result, callId: call.callId });
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
192
|
+
await this.page.evaluate(({ message, callId }) => window.__mcpPane.pushToUi({
|
|
193
|
+
type: 'tool/result',
|
|
194
|
+
callId,
|
|
195
|
+
error: message,
|
|
196
|
+
}), { message, callId: call.callId });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=fixture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.js","sourceRoot":"","sources":["../src/fixture.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless test host — HTML, который рендерится в Playwright-странице
|
|
3
|
+
* и предоставляет окружение для MCP App.
|
|
4
|
+
*
|
|
5
|
+
* Эта функция возвращает HTML-строку (с инлайн JS), которая:
|
|
6
|
+
* 1. Создаёт iframe с переданным UI HTML
|
|
7
|
+
* 2. Слушает все postMessage от iframe
|
|
8
|
+
* 3. Экспортирует window.__mcpPane API для управления из теста:
|
|
9
|
+
* __mcpPane.openApp(uri, html, initialData)
|
|
10
|
+
* __mcpPane.pushToUi(payload)
|
|
11
|
+
* __mcpPane.getReceivedMessages()
|
|
12
|
+
*
|
|
13
|
+
* Playwright-fixture использует evaluate() / exposeFunction() поверх этого
|
|
14
|
+
* API, чтобы дать тесту удобный TypeScript-интерфейс.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildHostPageHtml(): string;
|
|
17
|
+
//# sourceMappingURL=host-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-page.d.ts","sourceRoot":"","sources":["../src/host-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,wBAAgB,iBAAiB,IAAI,MAAM,CA+I1C"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless test host — HTML, который рендерится в Playwright-странице
|
|
3
|
+
* и предоставляет окружение для MCP App.
|
|
4
|
+
*
|
|
5
|
+
* Эта функция возвращает HTML-строку (с инлайн JS), которая:
|
|
6
|
+
* 1. Создаёт iframe с переданным UI HTML
|
|
7
|
+
* 2. Слушает все postMessage от iframe
|
|
8
|
+
* 3. Экспортирует window.__mcpPane API для управления из теста:
|
|
9
|
+
* __mcpPane.openApp(uri, html, initialData)
|
|
10
|
+
* __mcpPane.pushToUi(payload)
|
|
11
|
+
* __mcpPane.getReceivedMessages()
|
|
12
|
+
*
|
|
13
|
+
* Playwright-fixture использует evaluate() / exposeFunction() поверх этого
|
|
14
|
+
* API, чтобы дать тесту удобный TypeScript-интерфейс.
|
|
15
|
+
*/
|
|
16
|
+
export function buildHostPageHtml() {
|
|
17
|
+
return `<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="UTF-8">
|
|
21
|
+
<title>mcp-pane test host</title>
|
|
22
|
+
<style>
|
|
23
|
+
body { margin: 0; font-family: -apple-system, sans-serif; }
|
|
24
|
+
#host-shell {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
height: 100vh;
|
|
28
|
+
}
|
|
29
|
+
#host-status {
|
|
30
|
+
padding: 4px 12px;
|
|
31
|
+
background: #f3f4f6;
|
|
32
|
+
border-bottom: 1px solid #e5e7eb;
|
|
33
|
+
font-size: 11px;
|
|
34
|
+
font-family: ui-monospace, monospace;
|
|
35
|
+
color: #666;
|
|
36
|
+
}
|
|
37
|
+
#host-frame-container {
|
|
38
|
+
flex: 1;
|
|
39
|
+
min-height: 0;
|
|
40
|
+
display: flex;
|
|
41
|
+
}
|
|
42
|
+
iframe {
|
|
43
|
+
flex: 1;
|
|
44
|
+
border: none;
|
|
45
|
+
width: 100%;
|
|
46
|
+
background: #fff;
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
</head>
|
|
50
|
+
<body>
|
|
51
|
+
<div id="host-shell">
|
|
52
|
+
<div id="host-status">mcp-pane test host • no app loaded</div>
|
|
53
|
+
<div id="host-frame-container"></div>
|
|
54
|
+
</div>
|
|
55
|
+
<script>
|
|
56
|
+
(function () {
|
|
57
|
+
// ============================================================
|
|
58
|
+
// Internal state
|
|
59
|
+
// ============================================================
|
|
60
|
+
var iframe = null;
|
|
61
|
+
var currentUri = null;
|
|
62
|
+
var currentData = null;
|
|
63
|
+
var receivedFromUi = [];
|
|
64
|
+
var sentToUi = [];
|
|
65
|
+
|
|
66
|
+
// ============================================================
|
|
67
|
+
// Message handler — слушаем все сообщения от iframe
|
|
68
|
+
// ============================================================
|
|
69
|
+
window.addEventListener('message', function (event) {
|
|
70
|
+
// Принимаем только от нашего iframe.
|
|
71
|
+
if (!iframe || event.source !== iframe.contentWindow) return;
|
|
72
|
+
|
|
73
|
+
receivedFromUi.push({
|
|
74
|
+
at: Date.now(),
|
|
75
|
+
data: event.data,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ui/ready — пушим initial data, если есть.
|
|
79
|
+
if (event.data && event.data.type === 'ui/ready' && currentData !== null) {
|
|
80
|
+
pushToUi({ type: 'tool/result', payload: currentData });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// tool/call — Playwright фикстура подхватит это через
|
|
84
|
+
// __mcpPane.getReceivedMessages() и решит, как обработать
|
|
85
|
+
// (через реальный сервер, или через mock).
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
function pushToUi(payload) {
|
|
89
|
+
if (!iframe || !iframe.contentWindow) return false;
|
|
90
|
+
sentToUi.push({ at: Date.now(), data: payload });
|
|
91
|
+
iframe.contentWindow.postMessage(payload, '*');
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============================================================
|
|
96
|
+
// Public API на window
|
|
97
|
+
// ============================================================
|
|
98
|
+
window.__mcpPane = {
|
|
99
|
+
// Открыть UI ресурс. html — srcdoc для iframe, initialData —
|
|
100
|
+
// данные tool/result для broadcast после ui/ready.
|
|
101
|
+
openApp: function (uri, html, initialData) {
|
|
102
|
+
currentUri = uri;
|
|
103
|
+
currentData = initialData;
|
|
104
|
+
|
|
105
|
+
var container = document.getElementById('host-frame-container');
|
|
106
|
+
container.innerHTML = '';
|
|
107
|
+
|
|
108
|
+
iframe = document.createElement('iframe');
|
|
109
|
+
iframe.srcdoc = html;
|
|
110
|
+
iframe.sandbox = 'allow-scripts';
|
|
111
|
+
iframe.setAttribute('data-mcp-uri', uri);
|
|
112
|
+
container.appendChild(iframe);
|
|
113
|
+
|
|
114
|
+
document.getElementById('host-status').textContent =
|
|
115
|
+
'mcp-pane test host • loaded: ' + uri;
|
|
116
|
+
|
|
117
|
+
return new Promise(function (resolve) {
|
|
118
|
+
iframe.addEventListener('load', function () { resolve(); }, { once: true });
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
// Push новой data в UI (например, после tool call с новыми параметрами).
|
|
123
|
+
pushToUi: function (payload) {
|
|
124
|
+
return pushToUi(payload);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Полный список того, что UI прислал нам.
|
|
128
|
+
getReceivedMessages: function () {
|
|
129
|
+
return receivedFromUi.slice();
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Что мы отправили UI.
|
|
133
|
+
getSentMessages: function () {
|
|
134
|
+
return sentToUi.slice();
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Только tool/call'ы от UI — самая частая нужда в тестах.
|
|
138
|
+
getToolCalls: function () {
|
|
139
|
+
return receivedFromUi
|
|
140
|
+
.filter(function (m) { return m.data && m.data.type === 'tool/call'; })
|
|
141
|
+
.map(function (m) {
|
|
142
|
+
return { name: m.data.name, args: m.data.args || {}, callId: m.data.callId };
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Очистить историю — полезно при beforeEach.
|
|
147
|
+
clearHistory: function () {
|
|
148
|
+
receivedFromUi = [];
|
|
149
|
+
sentToUi = [];
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// Признак того, что хост готов. Используется ожиданием.
|
|
153
|
+
ready: true,
|
|
154
|
+
};
|
|
155
|
+
})();
|
|
156
|
+
</script>
|
|
157
|
+
</body>
|
|
158
|
+
</html>`;
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=host-page.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"host-page.js","sourceRoot":"","sources":["../src/host-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,UAAU,iBAAiB;IAC/B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QA6ID,CAAC;AACT,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mcp-pane/playwright — Playwright fixtures for testing MCP Apps.
|
|
3
|
+
*
|
|
4
|
+
* Quick start:
|
|
5
|
+
*
|
|
6
|
+
* import { test, expect } from '@mcp-pane/playwright';
|
|
7
|
+
*
|
|
8
|
+
* test.use({
|
|
9
|
+
* server: { command: 'node', args: ['./dist/server.js'] }
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* test('dashboard works', async ({ mcpApp }) => {
|
|
13
|
+
* await mcpApp.open('show_dashboard', { period: 'week' });
|
|
14
|
+
* await expect(mcpApp.iframe.locator('text=Revenue')).toBeVisible();
|
|
15
|
+
*
|
|
16
|
+
* await mcpApp.iframe.locator('button:has-text("month")').click();
|
|
17
|
+
* await expect.poll(() => mcpApp.toolCalls).toContainEqual({
|
|
18
|
+
* name: 'show_dashboard',
|
|
19
|
+
* args: { period: 'month' },
|
|
20
|
+
* });
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
export { test, expect, McpAppFixture, type McpFixtures } from './test.js';
|
|
24
|
+
export type { McpAppFixtureOptions } from './fixture.js';
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAC1E,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mcp-pane/playwright — Playwright fixtures for testing MCP Apps.
|
|
3
|
+
*
|
|
4
|
+
* Quick start:
|
|
5
|
+
*
|
|
6
|
+
* import { test, expect } from '@mcp-pane/playwright';
|
|
7
|
+
*
|
|
8
|
+
* test.use({
|
|
9
|
+
* server: { command: 'node', args: ['./dist/server.js'] }
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* test('dashboard works', async ({ mcpApp }) => {
|
|
13
|
+
* await mcpApp.open('show_dashboard', { period: 'week' });
|
|
14
|
+
* await expect(mcpApp.iframe.locator('text=Revenue')).toBeVisible();
|
|
15
|
+
*
|
|
16
|
+
* await mcpApp.iframe.locator('button:has-text("month")').click();
|
|
17
|
+
* await expect.poll(() => mcpApp.toolCalls).toContainEqual({
|
|
18
|
+
* name: 'show_dashboard',
|
|
19
|
+
* args: { period: 'month' },
|
|
20
|
+
* });
|
|
21
|
+
* });
|
|
22
|
+
*/
|
|
23
|
+
export { test, expect, McpAppFixture } from './test.js';
|
|
24
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,aAAa,EAAoB,MAAM,WAAW,CAAC"}
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright fixtures for MCP Apps.
|
|
3
|
+
*
|
|
4
|
+
* Использование:
|
|
5
|
+
*
|
|
6
|
+
* import { test, expect } from '@mcp-pane/playwright';
|
|
7
|
+
*
|
|
8
|
+
* test.use({ server: { command: 'node', args: ['./dist/server.js'] } });
|
|
9
|
+
*
|
|
10
|
+
* test('dashboard renders', async ({ mcpApp }) => {
|
|
11
|
+
* await mcpApp.open('show_dashboard', { period: 'week' });
|
|
12
|
+
* await expect(mcpApp.iframe.locator('text=Revenue')).toBeVisible();
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* Fixture `server` пользователь задаёт через test.use(). Fixture `mcpApp`
|
|
16
|
+
* автоматически поднимает harness, рендерит host-page, начинает polling.
|
|
17
|
+
*/
|
|
18
|
+
import { McpAppFixture } from './fixture.js';
|
|
19
|
+
import type { ServerSpec } from '@mcp-pane/test';
|
|
20
|
+
export type McpFixtures = {
|
|
21
|
+
/** Описание сервера, который надо запустить. Задаётся через test.use(). */
|
|
22
|
+
server: ServerSpec;
|
|
23
|
+
/** McpAppFixture — главный объект для теста. */
|
|
24
|
+
mcpApp: McpAppFixture;
|
|
25
|
+
};
|
|
26
|
+
export declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & McpFixtures, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
|
|
27
|
+
/**
|
|
28
|
+
* Расширенный expect с MCP-specific матчерами для FrameLocator.
|
|
29
|
+
* Можно использовать как обычный expect.
|
|
30
|
+
*/
|
|
31
|
+
export declare const expect: import("@playwright/test").Expect<{}>;
|
|
32
|
+
export { McpAppFixture };
|
|
33
|
+
//# sourceMappingURL=test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEjD,MAAM,MAAM,WAAW,GAAG;IACxB,2EAA2E;IAC3E,MAAM,EAAE,UAAU,CAAC;IACnB,gDAAgD;IAChD,MAAM,EAAE,aAAa,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,IAAI,2PA4Cf,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,MAAM,uCAAa,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,CAAC"}
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright fixtures for MCP Apps.
|
|
3
|
+
*
|
|
4
|
+
* Использование:
|
|
5
|
+
*
|
|
6
|
+
* import { test, expect } from '@mcp-pane/playwright';
|
|
7
|
+
*
|
|
8
|
+
* test.use({ server: { command: 'node', args: ['./dist/server.js'] } });
|
|
9
|
+
*
|
|
10
|
+
* test('dashboard renders', async ({ mcpApp }) => {
|
|
11
|
+
* await mcpApp.open('show_dashboard', { period: 'week' });
|
|
12
|
+
* await expect(mcpApp.iframe.locator('text=Revenue')).toBeVisible();
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* Fixture `server` пользователь задаёт через test.use(). Fixture `mcpApp`
|
|
16
|
+
* автоматически поднимает harness, рендерит host-page, начинает polling.
|
|
17
|
+
*/
|
|
18
|
+
import { test as base, expect as baseExpect } from '@playwright/test';
|
|
19
|
+
import { McpAppFixture } from './fixture.js';
|
|
20
|
+
export const test = base.extend({
|
|
21
|
+
// pollInterval можно настраивать через test.use({ server: ... }),
|
|
22
|
+
// но базовое значение — undefined и попадёт в default.
|
|
23
|
+
server: [
|
|
24
|
+
// По умолчанию падаем с понятным сообщением, если пользователь
|
|
25
|
+
// забыл задать `test.use({ server: ... })`.
|
|
26
|
+
{
|
|
27
|
+
command: '',
|
|
28
|
+
args: [],
|
|
29
|
+
},
|
|
30
|
+
{ option: true },
|
|
31
|
+
],
|
|
32
|
+
mcpApp: async ({ page, server }, use, testInfo) => {
|
|
33
|
+
if (!server.command) {
|
|
34
|
+
throw new Error('@mcp-pane/playwright: please configure `test.use({ server: { command, args } })` ' +
|
|
35
|
+
'before using mcpApp fixture');
|
|
36
|
+
}
|
|
37
|
+
const fixture = await McpAppFixture.create(page, {
|
|
38
|
+
...server,
|
|
39
|
+
testName: testInfo.title,
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
await use(fixture);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
// Если тест упал — автоматически прикрепляем recording к отчёту.
|
|
46
|
+
if (testInfo.status !== testInfo.expectedStatus) {
|
|
47
|
+
try {
|
|
48
|
+
const recording = await fixture.getRecording('fail');
|
|
49
|
+
await testInfo.attach('mcp-recording.json', {
|
|
50
|
+
body: JSON.stringify(recording, null, 2),
|
|
51
|
+
contentType: 'application/json',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Recording attach — best-effort, не должен ронять teardown.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
await fixture.close();
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Расширенный expect с MCP-specific матчерами для FrameLocator.
|
|
64
|
+
* Можно использовать как обычный expect.
|
|
65
|
+
*/
|
|
66
|
+
export const expect = baseExpect;
|
|
67
|
+
export { McpAppFixture };
|
|
68
|
+
//# sourceMappingURL=test.js.map
|
package/dist/test.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../src/test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,IAAI,IAAI,IAAI,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAU7C,MAAM,CAAC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAc;IAC3C,kEAAkE;IAClE,uDAAuD;IACvD,MAAM,EAAE;QACN,+DAA+D;QAC/D,4CAA4C;QAC5C;YACE,OAAO,EAAE,EAAE;YACX,IAAI,EAAE,EAAE;SACT;QACD,EAAE,MAAM,EAAE,IAAI,EAAE;KACjB;IAED,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE;QAChD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CACb,mFAAmF;gBACnF,6BAA6B,CAC9B,CAAC;QACJ,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,aAAa,CAAC,MAAM,CAAC,IAAI,EAAE;YAC/C,GAAG,MAAM;YACT,QAAQ,EAAE,QAAQ,CAAC,KAAK;SACzB,CAAC,CAAC;QAEH,IAAI,CAAC;YACH,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC;gBAAS,CAAC;YACT,iEAAiE;YACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,cAAc,EAAE,CAAC;gBAChD,IAAI,CAAC;oBACH,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;oBACrD,MAAM,QAAQ,CAAC,MAAM,CAAC,oBAAoB,EAAE;wBAC1C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;wBACxC,WAAW,EAAE,kBAAkB;qBAChC,CAAC,CAAC;gBACL,CAAC;gBAAC,MAAM,CAAC;oBACP,6DAA6D;gBAC/D,CAAC;YACH,CAAC;YACD,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC;CACF,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,MAAM,GAAG,UAAU,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mcp-pane/playwright",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Playwright fixtures for testing MCP Apps — interactive UIs rendered in iframe by MCP hosts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"test": "vitest run",
|
|
21
|
+
"clean": "rm -rf dist"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"mcp-apps",
|
|
26
|
+
"playwright",
|
|
27
|
+
"testing",
|
|
28
|
+
"ui-testing"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"@playwright/test": ">=1.40.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
39
|
+
"@mcp-pane/test": "workspace:*"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@playwright/test": "^1.48.0",
|
|
43
|
+
"vitest": "^2.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|