@superblocksteam/vite-plugin-file-sync 2.0.72-next.12 → 2.0.72-next.14
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/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts +2 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.d.ts.map +1 -1
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js +18 -2
- package/dist/ai-service/agent/prompts/build-base-system-prompt.js.map +1 -1
- package/dist/ai-service/agent/subagents/testing/index.d.ts +3 -0
- package/dist/ai-service/agent/subagents/testing/index.d.ts.map +1 -0
- package/dist/ai-service/agent/subagents/testing/index.js +2 -0
- package/dist/ai-service/agent/subagents/testing/index.js.map +1 -0
- package/dist/ai-service/agent/subagents/testing/prompt-builder.d.ts +10 -0
- package/dist/ai-service/agent/subagents/testing/prompt-builder.d.ts.map +1 -0
- package/dist/ai-service/agent/subagents/testing/prompt-builder.js +162 -0
- package/dist/ai-service/agent/subagents/testing/prompt-builder.js.map +1 -0
- package/dist/ai-service/agent/subagents/testing/types.d.ts +67 -0
- package/dist/ai-service/agent/subagents/testing/types.d.ts.map +1 -0
- package/dist/ai-service/agent/subagents/testing/types.js +2 -0
- package/dist/ai-service/agent/subagents/testing/types.js.map +1 -0
- package/dist/ai-service/agent/subagents/types.d.ts +9 -8
- package/dist/ai-service/agent/subagents/types.d.ts.map +1 -1
- package/dist/ai-service/agent/subagents/types.js +9 -9
- package/dist/ai-service/agent/subagents/types.js.map +1 -1
- package/dist/ai-service/agent/tool-message-utils.d.ts +7 -2
- package/dist/ai-service/agent/tool-message-utils.d.ts.map +1 -1
- package/dist/ai-service/agent/tool-message-utils.js +21 -2
- package/dist/ai-service/agent/tool-message-utils.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-source.d.ts +1 -1
- package/dist/ai-service/agent/tools/apis/api-source.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/apis/api-source.js +37 -9
- package/dist/ai-service/agent/tools/apis/api-source.js.map +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.js +1 -1
- package/dist/ai-service/agent/tools/apis/test-api.js.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts +1 -0
- package/dist/ai-service/agent/tools/build-capture-screenshot.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-capture-screenshot.js +4 -2
- package/dist/ai-service/agent/tools/build-capture-screenshot.js.map +1 -1
- package/dist/ai-service/agent/tools/build-delete-file.d.ts +1 -1
- package/dist/ai-service/agent/tools/build-manage-checklist.d.ts +1 -1
- package/dist/ai-service/agent/tools/{build-read-files.d.ts → build-read-file.d.ts} +10 -6
- package/dist/ai-service/agent/tools/build-read-file.d.ts.map +1 -0
- package/dist/ai-service/agent/tools/build-read-file.js +139 -0
- package/dist/ai-service/agent/tools/build-read-file.js.map +1 -0
- package/dist/ai-service/agent/tools/build-reload-file.d.ts +9 -3
- package/dist/ai-service/agent/tools/build-reload-file.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/build-reload-file.js +20 -9
- package/dist/ai-service/agent/tools/build-reload-file.js.map +1 -1
- package/dist/ai-service/agent/tools/get-console-logs.js +1 -1
- package/dist/ai-service/agent/tools/get-console-logs.js.map +1 -1
- package/dist/ai-service/agent/tools/get-runtime-errors.js +1 -1
- package/dist/ai-service/agent/tools/get-runtime-errors.js.map +1 -1
- package/dist/ai-service/agent/tools/index.d.ts +5 -1
- package/dist/ai-service/agent/tools/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/index.js +5 -1
- package/dist/ai-service/agent/tools/index.js.map +1 -1
- package/dist/ai-service/agent/tools/integrations/execute-request.d.ts +1 -1
- package/dist/ai-service/agent/tools/integrations/internal.d.ts.map +1 -1
- package/dist/ai-service/agent/tools/integrations/internal.js +8 -1
- package/dist/ai-service/agent/tools/integrations/internal.js.map +1 -1
- package/dist/ai-service/agent/tools.d.ts.map +1 -1
- package/dist/ai-service/agent/tools.js +124 -31
- package/dist/ai-service/agent/tools.js.map +1 -1
- package/dist/ai-service/agent/tools2/access-control.d.ts +23 -1
- package/dist/ai-service/agent/tools2/access-control.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/access-control.js +67 -1
- package/dist/ai-service/agent/tools2/access-control.js.map +1 -1
- package/dist/ai-service/agent/tools2/entity-permissions.d.ts +26 -0
- package/dist/ai-service/agent/tools2/entity-permissions.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/entity-permissions.js +15 -0
- package/dist/ai-service/agent/tools2/entity-permissions.js.map +1 -1
- package/dist/ai-service/agent/tools2/example.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/example.js +2 -4
- package/dist/ai-service/agent/tools2/example.js.map +1 -1
- package/dist/ai-service/agent/tools2/index.d.ts +1 -1
- package/dist/ai-service/agent/tools2/index.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/index.js +1 -1
- package/dist/ai-service/agent/tools2/index.js.map +1 -1
- package/dist/ai-service/agent/tools2/registry.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/registry.js +37 -23
- package/dist/ai-service/agent/tools2/registry.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.d.ts +0 -1
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.js +1 -6
- package/dist/ai-service/agent/tools2/tools/ask-multi-choice.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/end-test-run.d.ts +31 -0
- package/dist/ai-service/agent/tools2/tools/end-test-run.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/end-test-run.js +107 -0
- package/dist/ai-service/agent/tools2/tools/end-test-run.js.map +1 -0
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts +2 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js +63 -76
- package/dist/ai-service/agent/tools2/tools/exit-plan-mode.js.map +1 -1
- package/dist/ai-service/agent/tools2/tools/list-attachments.d.ts +15 -0
- package/dist/ai-service/agent/tools2/tools/list-attachments.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/list-attachments.js +47 -0
- package/dist/ai-service/agent/tools2/tools/list-attachments.js.map +1 -0
- package/dist/ai-service/agent/tools2/tools/start-test-run.d.ts +24 -0
- package/dist/ai-service/agent/tools2/tools/start-test-run.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/start-test-run.js +340 -0
- package/dist/ai-service/agent/tools2/tools/start-test-run.js.map +1 -0
- package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts +29 -0
- package/dist/ai-service/agent/tools2/tools/update-test-case-status.d.ts.map +1 -0
- package/dist/ai-service/agent/tools2/tools/update-test-case-status.js +106 -0
- package/dist/ai-service/agent/tools2/tools/update-test-case-status.js.map +1 -0
- package/dist/ai-service/agent/tools2/types.d.ts +6 -24
- package/dist/ai-service/agent/tools2/types.d.ts.map +1 -1
- package/dist/ai-service/agent/tools2/types.js +4 -15
- package/dist/ai-service/agent/tools2/types.js.map +1 -1
- package/dist/ai-service/agent/utils.d.ts +10 -0
- package/dist/ai-service/agent/utils.d.ts.map +1 -1
- package/dist/ai-service/agent/utils.js +160 -2
- package/dist/ai-service/agent/utils.js.map +1 -1
- package/dist/ai-service/attachments/index.d.ts +2 -0
- package/dist/ai-service/attachments/index.d.ts.map +1 -0
- package/dist/ai-service/attachments/index.js +2 -0
- package/dist/ai-service/attachments/index.js.map +1 -0
- package/dist/ai-service/attachments/store.d.ts +65 -0
- package/dist/ai-service/attachments/store.d.ts.map +1 -0
- package/dist/ai-service/attachments/store.js +158 -0
- package/dist/ai-service/attachments/store.js.map +1 -0
- package/dist/ai-service/chat/chat-session-store.d.ts.map +1 -1
- package/dist/ai-service/chat/chat-session-store.js +133 -1
- package/dist/ai-service/chat/chat-session-store.js.map +1 -1
- package/dist/ai-service/context-download.d.ts +24 -0
- package/dist/ai-service/context-download.d.ts.map +1 -0
- package/dist/ai-service/context-download.js +127 -0
- package/dist/ai-service/context-download.js.map +1 -0
- package/dist/ai-service/context-upload.d.ts +17 -0
- package/dist/ai-service/context-upload.d.ts.map +1 -0
- package/dist/ai-service/context-upload.js +100 -0
- package/dist/ai-service/context-upload.js.map +1 -0
- package/dist/ai-service/features.d.ts +4 -0
- package/dist/ai-service/features.d.ts.map +1 -1
- package/dist/ai-service/features.js +4 -0
- package/dist/ai-service/features.js.map +1 -1
- package/dist/ai-service/index.d.ts +27 -3
- package/dist/ai-service/index.d.ts.map +1 -1
- package/dist/ai-service/index.js +259 -20
- package/dist/ai-service/index.js.map +1 -1
- package/dist/ai-service/judge/integration/mcp-client.d.ts +3 -6
- package/dist/ai-service/judge/integration/mcp-client.d.ts.map +1 -1
- package/dist/ai-service/judge/integration/mcp-client.js.map +1 -1
- package/dist/ai-service/judge/tools/playwright-action.d.ts +1 -1
- package/dist/ai-service/judge/tools/submit-feedback.d.ts +1 -1
- package/dist/ai-service/llm/client.d.ts +6 -0
- package/dist/ai-service/llm/client.d.ts.map +1 -1
- package/dist/ai-service/llm/client.js +9 -0
- package/dist/ai-service/llm/client.js.map +1 -1
- package/dist/ai-service/llm/context/constants.d.ts +8 -0
- package/dist/ai-service/llm/context/constants.d.ts.map +1 -1
- package/dist/ai-service/llm/context/constants.js +8 -0
- package/dist/ai-service/llm/context/constants.js.map +1 -1
- package/dist/ai-service/llm/context/context.d.ts +4 -0
- package/dist/ai-service/llm/context/context.d.ts.map +1 -1
- package/dist/ai-service/llm/context/context.js +22 -9
- package/dist/ai-service/llm/context/context.js.map +1 -1
- package/dist/ai-service/llm/context/manager.d.ts +6 -1
- package/dist/ai-service/llm/context/manager.d.ts.map +1 -1
- package/dist/ai-service/llm/context/manager.js +9 -1
- package/dist/ai-service/llm/context/manager.js.map +1 -1
- package/dist/ai-service/llm/context/serialization.d.ts +3 -0
- package/dist/ai-service/llm/context/serialization.d.ts.map +1 -1
- package/dist/ai-service/llm/context/utils/message-utils.d.ts +10 -0
- package/dist/ai-service/llm/context/utils/message-utils.d.ts.map +1 -1
- package/dist/ai-service/llm/context/utils/message-utils.js +92 -0
- package/dist/ai-service/llm/context/utils/message-utils.js.map +1 -1
- package/dist/ai-service/llm/interaction/provider.d.ts +1 -0
- package/dist/ai-service/llm/interaction/provider.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/config.d.ts +2 -0
- package/dist/ai-service/llm/stream/config.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/config.js +1 -0
- package/dist/ai-service/llm/stream/config.js.map +1 -1
- package/dist/ai-service/llm/stream/event-bus.d.ts +5 -0
- package/dist/ai-service/llm/stream/event-bus.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/event-bus.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/llmobs.d.ts +4 -1
- package/dist/ai-service/llm/stream/observers/llmobs.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/llmobs.js +194 -10
- package/dist/ai-service/llm/stream/observers/llmobs.js.map +1 -1
- package/dist/ai-service/llm/stream/observers/logging.d.ts +1 -0
- package/dist/ai-service/llm/stream/observers/logging.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/observers/logging.js +92 -20
- package/dist/ai-service/llm/stream/observers/logging.js.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.d.ts +7 -1
- package/dist/ai-service/llm/stream/orchestrator.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/orchestrator.js +23 -3
- package/dist/ai-service/llm/stream/orchestrator.js.map +1 -1
- package/dist/ai-service/llm/stream/retry-engine.d.ts +1 -0
- package/dist/ai-service/llm/stream/retry-engine.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/retry-engine.js +20 -1
- package/dist/ai-service/llm/stream/retry-engine.js.map +1 -1
- package/dist/ai-service/llm/stream/session.d.ts +14 -2
- package/dist/ai-service/llm/stream/session.d.ts.map +1 -1
- package/dist/ai-service/llm/stream/session.js +15 -28
- package/dist/ai-service/llm/stream/session.js.map +1 -1
- package/dist/ai-service/llmobs/otel-exporter.d.ts +58 -0
- package/dist/ai-service/llmobs/otel-exporter.d.ts.map +1 -0
- package/dist/ai-service/llmobs/otel-exporter.js +182 -0
- package/dist/ai-service/llmobs/otel-exporter.js.map +1 -0
- package/dist/ai-service/llmobs/tracer.d.ts +9 -5
- package/dist/ai-service/llmobs/tracer.d.ts.map +1 -1
- package/dist/ai-service/llmobs/tracer.js +46 -41
- package/dist/ai-service/llmobs/tracer.js.map +1 -1
- package/dist/ai-service/llmobs/types.d.ts +1 -0
- package/dist/ai-service/llmobs/types.d.ts.map +1 -1
- package/dist/ai-service/mcp/adapter/mcp-tool-adapter.d.ts +1 -1
- package/dist/ai-service/mcp/adapter/mcp-tool-adapter.d.ts.map +1 -1
- package/dist/ai-service/mcp/adapter/mcp-tool-adapter.js +5 -2
- package/dist/ai-service/mcp/adapter/mcp-tool-adapter.js.map +1 -1
- package/dist/ai-service/mcp/embedded-playwright-mcp-server.d.ts +33 -1
- package/dist/ai-service/mcp/embedded-playwright-mcp-server.d.ts.map +1 -1
- package/dist/ai-service/mcp/embedded-playwright-mcp-server.js +949 -91
- package/dist/ai-service/mcp/embedded-playwright-mcp-server.js.map +1 -1
- package/dist/ai-service/mcp/playwright-server.d.ts +39 -0
- package/dist/ai-service/mcp/playwright-server.d.ts.map +1 -1
- package/dist/ai-service/mcp/playwright-server.js +56 -0
- package/dist/ai-service/mcp/playwright-server.js.map +1 -1
- package/dist/ai-service/mcp/types.d.ts +4 -0
- package/dist/ai-service/mcp/types.d.ts.map +1 -1
- package/dist/ai-service/prompts/explain-code.d.ts +2 -2
- package/dist/ai-service/prompts/explain-code.d.ts.map +1 -1
- package/dist/ai-service/prompts/explain-code.js +2 -2
- package/dist/ai-service/prompts/explain-code.js.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts +20 -1
- package/dist/ai-service/state-machine/clark-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/clark-fsm.js +16 -0
- package/dist/ai-service/state-machine/clark-fsm.js.map +1 -1
- package/dist/ai-service/state-machine/fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/fsm.js +1 -0
- package/dist/ai-service/state-machine/fsm.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/agent-planning.js +71 -7
- package/dist/ai-service/state-machine/handlers/agent-planning.js.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.d.ts.map +1 -1
- package/dist/ai-service/state-machine/handlers/llm-generating.js +112 -33
- package/dist/ai-service/state-machine/handlers/llm-generating.js.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.d.ts +1 -1
- package/dist/ai-service/state-machine/helpers/peer.d.ts.map +1 -1
- package/dist/ai-service/state-machine/helpers/peer.js +21 -4
- package/dist/ai-service/state-machine/helpers/peer.js.map +1 -1
- package/dist/ai-service/state-machine/mocks.d.ts.map +1 -1
- package/dist/ai-service/state-machine/mocks.js +2 -0
- package/dist/ai-service/state-machine/mocks.js.map +1 -1
- package/dist/ai-service/state-machine/traced-fsm.d.ts +2 -0
- package/dist/ai-service/state-machine/traced-fsm.d.ts.map +1 -1
- package/dist/ai-service/state-machine/traced-fsm.js +18 -0
- package/dist/ai-service/state-machine/traced-fsm.js.map +1 -1
- package/dist/ai-service/util/safe-parse.d.ts +2 -0
- package/dist/ai-service/util/safe-parse.d.ts.map +1 -0
- package/dist/ai-service/util/safe-parse.js +9 -0
- package/dist/ai-service/util/safe-parse.js.map +1 -0
- package/dist/ai-service/util/safe-stringify.d.ts.map +1 -1
- package/dist/ai-service/util/safe-stringify.js +7 -0
- package/dist/ai-service/util/safe-stringify.js.map +1 -1
- package/dist/ai-service/util/scoped-token-utils.d.ts +15 -0
- package/dist/ai-service/util/scoped-token-utils.d.ts.map +1 -0
- package/dist/ai-service/util/scoped-token-utils.js +131 -0
- package/dist/ai-service/util/scoped-token-utils.js.map +1 -0
- package/dist/ai-service/util/stop-condition.d.ts +1 -0
- package/dist/ai-service/util/stop-condition.d.ts.map +1 -1
- package/dist/ai-service/util/stop-condition.js +5 -0
- package/dist/ai-service/util/stop-condition.js.map +1 -1
- package/dist/ai-service/util/strip-content.d.ts +2 -0
- package/dist/ai-service/util/strip-content.d.ts.map +1 -0
- package/dist/ai-service/util/strip-content.js +31 -0
- package/dist/ai-service/util/strip-content.js.map +1 -0
- package/dist/ai-service/util/tool-signature.d.ts +13 -0
- package/dist/ai-service/util/tool-signature.d.ts.map +1 -0
- package/dist/ai-service/util/tool-signature.js +38 -0
- package/dist/ai-service/util/tool-signature.js.map +1 -0
- package/dist/file-sync-vite-plugin.d.ts.map +1 -1
- package/dist/file-sync-vite-plugin.js +4 -1
- package/dist/file-sync-vite-plugin.js.map +1 -1
- package/dist/injected-index.js +2 -2
- package/dist/injected-index.js.map +1 -1
- package/dist/parsing/jsx.d.ts.map +1 -1
- package/dist/parsing/jsx.js +0 -2
- package/dist/parsing/jsx.js.map +1 -1
- package/dist/server-rpc/index.js +1 -1
- package/dist/server-rpc/index.js.map +1 -1
- package/dist/socket-manager.d.ts.map +1 -1
- package/dist/socket-manager.js +61 -4
- package/dist/socket-manager.js.map +1 -1
- package/package.json +9 -9
- package/dist/ai-service/agent/tools/build-read-files.d.ts.map +0 -1
- package/dist/ai-service/agent/tools/build-read-files.js +0 -67
- package/dist/ai-service/agent/tools/build-read-files.js.map +0 -1
|
@@ -2,6 +2,164 @@ import { once } from "node:events";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import http from "node:http";
|
|
4
4
|
import { chromium, firefox, webkit, } from "playwright";
|
|
5
|
+
/**
|
|
6
|
+
* Playwright action logger that captures browser actions and saves them as log artifacts.
|
|
7
|
+
*/
|
|
8
|
+
class PlaywrightActionLogger {
|
|
9
|
+
logContent = "";
|
|
10
|
+
sessionId;
|
|
11
|
+
startTime;
|
|
12
|
+
actionCount = 0;
|
|
13
|
+
saveArtifact;
|
|
14
|
+
runTimestamp;
|
|
15
|
+
logger;
|
|
16
|
+
constructor(options) {
|
|
17
|
+
this.sessionId = options.sessionId;
|
|
18
|
+
this.startTime = Date.now();
|
|
19
|
+
this.saveArtifact = options.saveArtifact;
|
|
20
|
+
this.runTimestamp = options.runTimestamp || new Date().toISOString();
|
|
21
|
+
this.logger = options.logger;
|
|
22
|
+
this.logContent += this.formatSection("PLAYWRIGHT SESSION START") + "\n";
|
|
23
|
+
this.logContent += `Session ID: ${this.sessionId}\n`;
|
|
24
|
+
this.logContent += `Timestamp: ${new Date().toISOString()}\n\n`;
|
|
25
|
+
}
|
|
26
|
+
formatSection(title) {
|
|
27
|
+
const prefix = `------ ${title} `;
|
|
28
|
+
const remaining = Math.max(0, 72 - prefix.length);
|
|
29
|
+
return prefix + "-".repeat(remaining);
|
|
30
|
+
}
|
|
31
|
+
truncateString(s, maxBytes) {
|
|
32
|
+
if (s.length <= maxBytes)
|
|
33
|
+
return s;
|
|
34
|
+
return (s.slice(0, maxBytes) + `\n... [truncated ${s.length - maxBytes} bytes]`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Log the start of a Playwright action.
|
|
38
|
+
*/
|
|
39
|
+
logActionStart(action, params) {
|
|
40
|
+
this.actionCount++;
|
|
41
|
+
const timestamp = new Date().toISOString();
|
|
42
|
+
this.logContent += `ACTION #${this.actionCount}: ${action}\n`;
|
|
43
|
+
this.logContent += ` Timestamp: ${timestamp}\n`;
|
|
44
|
+
// Log relevant parameters (exclude screenshot data)
|
|
45
|
+
const logParams = {};
|
|
46
|
+
for (const [key, value] of Object.entries(params)) {
|
|
47
|
+
if (key === "screenshot" || key === "data")
|
|
48
|
+
continue;
|
|
49
|
+
logParams[key] = value;
|
|
50
|
+
}
|
|
51
|
+
if (Object.keys(logParams).length > 0) {
|
|
52
|
+
this.logContent += ` Params:\n`;
|
|
53
|
+
for (const [key, value] of Object.entries(logParams)) {
|
|
54
|
+
const valueStr = typeof value === "string"
|
|
55
|
+
? this.truncateString(value, 200)
|
|
56
|
+
: JSON.stringify(value);
|
|
57
|
+
this.logContent += ` ${key}: ${valueStr}\n`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Log the result of a Playwright action.
|
|
63
|
+
*/
|
|
64
|
+
logActionResult(result) {
|
|
65
|
+
const success = result.success !== false;
|
|
66
|
+
this.logContent += ` Result: ${success ? "SUCCESS" : "FAILED"}\n`;
|
|
67
|
+
// Log error if present
|
|
68
|
+
if (result.error) {
|
|
69
|
+
this.logContent += ` Error: ${result.error}\n`;
|
|
70
|
+
}
|
|
71
|
+
// Log runtime errors if present
|
|
72
|
+
if (result.runtimeErrors?.length > 0) {
|
|
73
|
+
this.logContent += ` Runtime Errors:\n`;
|
|
74
|
+
for (const err of result.runtimeErrors) {
|
|
75
|
+
this.logContent += ` - ${err.header}\n`;
|
|
76
|
+
if (err.stack) {
|
|
77
|
+
const truncatedStack = this.truncateString(err.stack, 500);
|
|
78
|
+
this.logContent += ` Stack: ${truncatedStack}\n`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Log specific result fields (excluding large data like screenshots)
|
|
83
|
+
const fieldsToLog = ["url", "text", "hasErrors", "errorCount", "count"];
|
|
84
|
+
for (const field of fieldsToLog) {
|
|
85
|
+
if (field in result && result[field] !== undefined) {
|
|
86
|
+
this.logContent += ` ${field}: ${JSON.stringify(result[field])}\n`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
this.logContent += "\n";
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Log console messages from the page.
|
|
93
|
+
*/
|
|
94
|
+
logConsoleLogs(logs) {
|
|
95
|
+
if (logs.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
this.logContent += ` Console Logs (${logs.length}):\n`;
|
|
98
|
+
const maxLogs = 20;
|
|
99
|
+
const logsToShow = logs.slice(-maxLogs);
|
|
100
|
+
for (const log of logsToShow) {
|
|
101
|
+
const text = this.truncateString(log.text, 200);
|
|
102
|
+
this.logContent += ` [${log.type}] ${text}\n`;
|
|
103
|
+
}
|
|
104
|
+
if (logs.length > maxLogs) {
|
|
105
|
+
this.logContent += ` ... and ${logs.length - maxLogs} more logs\n`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Save the log to an artifact file.
|
|
110
|
+
*/
|
|
111
|
+
async save() {
|
|
112
|
+
if (!this.saveArtifact) {
|
|
113
|
+
this.logger.debug("[PlaywrightLogger] No saveArtifact function provided, skipping log save");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const duration = Date.now() - this.startTime;
|
|
117
|
+
this.logContent += this.formatSection("PLAYWRIGHT SESSION END") + "\n";
|
|
118
|
+
this.logContent += `End Timestamp: ${new Date().toISOString()}\n`;
|
|
119
|
+
this.logContent += `Duration: ${duration}ms\n`;
|
|
120
|
+
this.logContent += `Total Actions: ${this.actionCount}\n`;
|
|
121
|
+
try {
|
|
122
|
+
const artifact = {
|
|
123
|
+
type: "file",
|
|
124
|
+
filePath: `playwright-session-${this.sessionId}.log`,
|
|
125
|
+
content: this.logContent,
|
|
126
|
+
};
|
|
127
|
+
const stepId = `playwright-session-${this.sessionId}`;
|
|
128
|
+
await this.saveArtifact(artifact, stepId, this.runTimestamp);
|
|
129
|
+
this.logger.debug(`[PlaywrightLogger] Saved log artifact: ${artifact.filePath}`);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
this.logger.error(`[PlaywrightLogger] Failed to save log: ${String(error)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Save the current log and reset for a new session.
|
|
137
|
+
* Used to save logs after each test run without closing the server.
|
|
138
|
+
*/
|
|
139
|
+
async saveAndReset() {
|
|
140
|
+
// Only save if there are actions logged
|
|
141
|
+
if (this.actionCount === 0) {
|
|
142
|
+
this.logger.debug("[PlaywrightLogger] No actions logged, skipping save and reset");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
await this.save();
|
|
146
|
+
// Reset for next session
|
|
147
|
+
const newSessionId = String(Date.now());
|
|
148
|
+
this.sessionId = newSessionId;
|
|
149
|
+
this.startTime = Date.now();
|
|
150
|
+
this.actionCount = 0;
|
|
151
|
+
this.logContent = "";
|
|
152
|
+
this.logContent += this.formatSection("PLAYWRIGHT SESSION START") + "\n";
|
|
153
|
+
this.logContent += `Session ID: ${this.sessionId}\n`;
|
|
154
|
+
this.logContent += `Timestamp: ${new Date().toISOString()}\n\n`;
|
|
155
|
+
this.logger.debug(`[PlaywrightLogger] Reset for new session: ${newSessionId}`);
|
|
156
|
+
}
|
|
157
|
+
getLogContent() {
|
|
158
|
+
return this.logContent;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/** Default viewport dimensions for consistent screenshots */
|
|
162
|
+
const DEFAULT_VIEWPORT = { width: 1280, height: 800 };
|
|
5
163
|
const PLAYWRIGHT_ACTIONS = [
|
|
6
164
|
"navigate",
|
|
7
165
|
"click",
|
|
@@ -12,36 +170,46 @@ const PLAYWRIGHT_ACTIONS = [
|
|
|
12
170
|
"evaluate",
|
|
13
171
|
"getUrl",
|
|
14
172
|
"reload",
|
|
173
|
+
"getConsoleLogs",
|
|
174
|
+
"scroll",
|
|
175
|
+
"scrollIntoView",
|
|
176
|
+
"checkRuntimeErrors",
|
|
15
177
|
];
|
|
178
|
+
let capturedConsoleLogs = [];
|
|
16
179
|
export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
180
|
+
const logger = options.logger;
|
|
181
|
+
// Create action logger for capturing Playwright actions
|
|
182
|
+
const sessionId = String(Date.now());
|
|
183
|
+
const actionLogger = new PlaywrightActionLogger({
|
|
184
|
+
sessionId,
|
|
185
|
+
saveArtifact: options.saveArtifact,
|
|
186
|
+
runTimestamp: options.runTimestamp,
|
|
187
|
+
logger,
|
|
188
|
+
});
|
|
17
189
|
let browser;
|
|
18
190
|
let context = null;
|
|
19
191
|
let shouldCloseBrowser = true;
|
|
20
192
|
if (options?.connectWsEndpoint) {
|
|
21
|
-
|
|
22
|
-
|
|
193
|
+
browser = await chromium.connect({
|
|
194
|
+
wsEndpoint: options.connectWsEndpoint,
|
|
195
|
+
});
|
|
23
196
|
shouldCloseBrowser = false;
|
|
24
197
|
}
|
|
25
198
|
else {
|
|
26
|
-
console.log("🎬 MCP Server: Launching new browser with storage state");
|
|
27
199
|
browser = await launchBrowser();
|
|
28
200
|
}
|
|
29
|
-
console.log(`🎬 MCP Server: Browser contexts available before selection: ${browser.contexts().length}`);
|
|
30
201
|
// Reuse an existing context when connecting to an existing browser
|
|
31
202
|
if (options?.connectWsEndpoint) {
|
|
32
203
|
const existing = browser.contexts();
|
|
33
204
|
if (existing.length > 0) {
|
|
34
205
|
context = existing[0];
|
|
35
|
-
console.log("🎬 MCP Server: Reusing existing browser context");
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
console.warn("🎬 MCP Server: No existing context found on shared browser; falling back to new context with storage/JWT");
|
|
39
206
|
}
|
|
40
207
|
}
|
|
41
208
|
if (!context) {
|
|
42
|
-
const contextOptions = {
|
|
209
|
+
const contextOptions = {
|
|
210
|
+
viewport: DEFAULT_VIEWPORT,
|
|
211
|
+
};
|
|
43
212
|
if (options?.storageStateData) {
|
|
44
|
-
console.log("🎬 MCP Server: Using provided storageStateData");
|
|
45
213
|
contextOptions.storageState = options.storageStateData;
|
|
46
214
|
// Duplicate auth cookies onto app host if needed (e.g., when original domain is auth0)
|
|
47
215
|
if (options.appUrl && options.storageStateData.cookies?.length) {
|
|
@@ -62,12 +230,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
62
230
|
}
|
|
63
231
|
else if (options?.storageStatePath) {
|
|
64
232
|
if (fs.existsSync(options.storageStatePath)) {
|
|
65
|
-
console.log(`🎬 MCP Server: Loading storage state from ${options.storageStatePath}`);
|
|
66
233
|
contextOptions.storageState = options.storageStatePath;
|
|
67
234
|
}
|
|
68
|
-
else {
|
|
69
|
-
console.warn(`🎬 MCP Server: Storage state not found at ${options.storageStatePath}, starting without it`);
|
|
70
|
-
}
|
|
71
235
|
}
|
|
72
236
|
context = await browser.newContext(contextOptions);
|
|
73
237
|
}
|
|
@@ -75,6 +239,154 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
75
239
|
if (!context) {
|
|
76
240
|
throw new Error("Failed to create or reuse a Playwright context");
|
|
77
241
|
}
|
|
242
|
+
// Inject sb-init and sb-bootstrap-response data for direct iframe access
|
|
243
|
+
// This simulates the parent window sending these messages to the iframe
|
|
244
|
+
if (options?.initData || options?.bootstrapData) {
|
|
245
|
+
const initData = options.initData;
|
|
246
|
+
const bootstrapData = options.bootstrapData;
|
|
247
|
+
/**
|
|
248
|
+
* CRITICAL: Mock Parent Window Setup for Playwright Browser Automation
|
|
249
|
+
*
|
|
250
|
+
* WHY THIS IS NECESSARY:
|
|
251
|
+
* The Superblocks library code (in @superblocksteam/library) expects to run inside an iframe
|
|
252
|
+
* embedded by the Superblocks editor. It uses `window.parent.postMessage()` to communicate
|
|
253
|
+
* with the editor and relies on `window.parent !== window` to detect the embedded state.
|
|
254
|
+
*
|
|
255
|
+
* When running in Playwright for E2E testing, there is no parent iframe - the page runs
|
|
256
|
+
* standalone. Without this mock, the library would:
|
|
257
|
+
* 1. Fail the `isEmbeddedBySuperblocksFirstParty()` check
|
|
258
|
+
* 2. Never receive the `sb-init` message needed to establish socket connections
|
|
259
|
+
* 3. Never receive the `sb-bootstrap-response` with auth tokens and app data
|
|
260
|
+
*
|
|
261
|
+
* HOW IT WORKS:
|
|
262
|
+
* 1. We override `window.parent` with a mock object BEFORE library code loads (via addInitScript)
|
|
263
|
+
* 2. The mock intercepts `postMessage` calls from the library
|
|
264
|
+
* 3. When the library sends "sb-ready", we respond with "sb-init" containing peerId/auth data
|
|
265
|
+
* 4. When the library sends "sb-editor-request-bootstrap", we respond with bootstrap data
|
|
266
|
+
* 5. We also send initial messages proactively in case the library sets up listeners late
|
|
267
|
+
*
|
|
268
|
+
* WHAT COULD BREAK THIS:
|
|
269
|
+
* - If the library changes how it detects parent window embedding (e.g., different checks)
|
|
270
|
+
* - If the message types or payload shapes change in the library
|
|
271
|
+
* - If the library adds additional security checks (e.g., origin verification on parent)
|
|
272
|
+
* - If timing changes require different setTimeout delays
|
|
273
|
+
*
|
|
274
|
+
* TESTING NOTE:
|
|
275
|
+
* Integration tests for this setup should verify:
|
|
276
|
+
* 1. The app successfully initializes and shows content
|
|
277
|
+
* 2. API calls are authenticated (tokens were passed correctly)
|
|
278
|
+
* 3. Real-time features work (socket connection established via peerId)
|
|
279
|
+
*/
|
|
280
|
+
await context.addInitScript((payload) => {
|
|
281
|
+
const { initData: init, bootstrapData: bootstrap } = payload;
|
|
282
|
+
/**
|
|
283
|
+
* Mock parent window object that intercepts postMessage calls from the library.
|
|
284
|
+
* When the app sends messages expecting a parent response, we handle them here.
|
|
285
|
+
*/
|
|
286
|
+
const mockParent = {
|
|
287
|
+
postMessage: (message, _targetOrigin) => {
|
|
288
|
+
// When the app sends sb-ready, we respond with sb-init
|
|
289
|
+
// This triggers the library to establish its socket connection
|
|
290
|
+
if (message?.type === "sb-ready" && init) {
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
window.postMessage({
|
|
293
|
+
type: "sb-init",
|
|
294
|
+
payload: {
|
|
295
|
+
peerId: init.peerId,
|
|
296
|
+
userId: init.userId,
|
|
297
|
+
devServerAuthorization: init.devServerAuthorization,
|
|
298
|
+
appId: init.appId,
|
|
299
|
+
windowOriginUrl: init.windowOriginUrl,
|
|
300
|
+
},
|
|
301
|
+
startTime: Date.now(),
|
|
302
|
+
}, "*");
|
|
303
|
+
}, 10);
|
|
304
|
+
}
|
|
305
|
+
// When the app sends sb-editor-request-bootstrap, we respond with bootstrap data
|
|
306
|
+
// This provides auth tokens and configuration needed by the API manager
|
|
307
|
+
if (message?.type === "sb-editor-request-bootstrap" && bootstrap) {
|
|
308
|
+
setTimeout(() => {
|
|
309
|
+
window.postMessage({
|
|
310
|
+
type: "sb-bootstrap-response",
|
|
311
|
+
payload: bootstrap,
|
|
312
|
+
startTime: Date.now(),
|
|
313
|
+
}, "*");
|
|
314
|
+
}, 10);
|
|
315
|
+
}
|
|
316
|
+
// When the app sends authenticate-api-request, we respond with resolve-promise
|
|
317
|
+
// This allows API calls to proceed in test mode without actual auth
|
|
318
|
+
if (message?.type === "authenticate-api-request") {
|
|
319
|
+
const { callbackId } = message.payload || {};
|
|
320
|
+
if (callbackId) {
|
|
321
|
+
setTimeout(() => {
|
|
322
|
+
window.postMessage({
|
|
323
|
+
type: "resolve-promise",
|
|
324
|
+
callbackId,
|
|
325
|
+
payload: {}, // Empty success result - no auth errors
|
|
326
|
+
}, "*");
|
|
327
|
+
}, 10);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
// These properties make the mock look like a real window object
|
|
332
|
+
// which helps pass any instanceof or property existence checks
|
|
333
|
+
window: window,
|
|
334
|
+
document: document,
|
|
335
|
+
location: window.location,
|
|
336
|
+
};
|
|
337
|
+
// Override window.parent to point to our mock
|
|
338
|
+
// This must happen before the library code runs (hence addInitScript)
|
|
339
|
+
Object.defineProperty(window, "parent", {
|
|
340
|
+
value: mockParent,
|
|
341
|
+
writable: false,
|
|
342
|
+
configurable: true,
|
|
343
|
+
});
|
|
344
|
+
// Send initial messages proactively after a delay
|
|
345
|
+
// This handles the case where the library sets up listeners after checking window.parent
|
|
346
|
+
if (init) {
|
|
347
|
+
setTimeout(() => {
|
|
348
|
+
window.postMessage({
|
|
349
|
+
type: "sb-init",
|
|
350
|
+
payload: {
|
|
351
|
+
peerId: init.peerId,
|
|
352
|
+
userId: init.userId,
|
|
353
|
+
devServerAuthorization: init.devServerAuthorization,
|
|
354
|
+
appId: init.appId,
|
|
355
|
+
windowOriginUrl: init.windowOriginUrl,
|
|
356
|
+
},
|
|
357
|
+
startTime: Date.now(),
|
|
358
|
+
}, "*");
|
|
359
|
+
}, 100);
|
|
360
|
+
}
|
|
361
|
+
if (bootstrap) {
|
|
362
|
+
setTimeout(() => {
|
|
363
|
+
window.postMessage({
|
|
364
|
+
type: "sb-bootstrap-response",
|
|
365
|
+
payload: bootstrap,
|
|
366
|
+
startTime: Date.now(),
|
|
367
|
+
}, "*");
|
|
368
|
+
}, 200);
|
|
369
|
+
// Send sb-global-sync with profiles data for API execution
|
|
370
|
+
// This sets superblocksContext.profiles which is needed for profileId
|
|
371
|
+
if (bootstrap.profiles) {
|
|
372
|
+
setTimeout(() => {
|
|
373
|
+
window.postMessage({
|
|
374
|
+
type: "sb-global-sync",
|
|
375
|
+
payload: {
|
|
376
|
+
global: {
|
|
377
|
+
profiles: bootstrap.profiles,
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
startTime: Date.now(),
|
|
381
|
+
}, "*");
|
|
382
|
+
}, 300);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}, {
|
|
386
|
+
initData,
|
|
387
|
+
bootstrapData,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
78
390
|
// Seed a cookie/localStorage with JWT for app domain if provided
|
|
79
391
|
if (options?.jwt && options?.appUrl) {
|
|
80
392
|
try {
|
|
@@ -145,7 +457,13 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
145
457
|
await context.addCookies(cookies);
|
|
146
458
|
}
|
|
147
459
|
catch (error) {
|
|
148
|
-
|
|
460
|
+
logger.error(`[MCP-Server] ❌ Failed to seed JWT cookie/localStorage: ${String(error)}`, {
|
|
461
|
+
error: {
|
|
462
|
+
kind: "McpServerSeedJwtError",
|
|
463
|
+
message: String(error),
|
|
464
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
465
|
+
},
|
|
466
|
+
});
|
|
149
467
|
}
|
|
150
468
|
}
|
|
151
469
|
// Seed session storage for app origin if provided
|
|
@@ -165,8 +483,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
165
483
|
}
|
|
166
484
|
}, { targetOrigin: origin, entries: items });
|
|
167
485
|
}
|
|
168
|
-
catch
|
|
169
|
-
|
|
486
|
+
catch {
|
|
487
|
+
// ignore session storage seeding errors
|
|
170
488
|
}
|
|
171
489
|
}
|
|
172
490
|
// Seed additional origins storage (local/session) if provided
|
|
@@ -202,8 +520,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
202
520
|
ssEntries: seedSession,
|
|
203
521
|
});
|
|
204
522
|
}
|
|
205
|
-
catch
|
|
206
|
-
|
|
523
|
+
catch {
|
|
524
|
+
// ignore extra origin storage seeding errors
|
|
207
525
|
}
|
|
208
526
|
}
|
|
209
527
|
}
|
|
@@ -212,6 +530,35 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
212
530
|
// Prefer an existing page in the reused context (e.g., the CLI-authenticated one)
|
|
213
531
|
const existingPages = context.pages();
|
|
214
532
|
const activePage = existingPages.length > 0 ? existingPages[0] : page;
|
|
533
|
+
// Set up console log capture for the page
|
|
534
|
+
capturedConsoleLogs = []; // Reset logs for new session
|
|
535
|
+
activePage.on("console", (msg) => {
|
|
536
|
+
capturedConsoleLogs.push({
|
|
537
|
+
type: msg.type(),
|
|
538
|
+
text: msg.text(),
|
|
539
|
+
timestamp: Date.now(),
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
// Auto-navigate to the app URL if provided, so the agent doesn't need to navigate manually
|
|
543
|
+
if (options?.appUrl) {
|
|
544
|
+
try {
|
|
545
|
+
await activePage.goto(options.appUrl, {
|
|
546
|
+
waitUntil: "load",
|
|
547
|
+
timeout: 60000,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
// Don't throw - let the agent handle navigation if auto-nav fails
|
|
552
|
+
// Log the error for debugging purposes
|
|
553
|
+
logger.warn(`[MCP-Server] Auto-navigation to ${options.appUrl} failed: ${String(error)}`, {
|
|
554
|
+
error: {
|
|
555
|
+
kind: "McpServerAutoNavError",
|
|
556
|
+
message: String(error),
|
|
557
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
215
562
|
const server = http.createServer((req, res) => {
|
|
216
563
|
void (async () => {
|
|
217
564
|
try {
|
|
@@ -244,7 +591,7 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
244
591
|
}));
|
|
245
592
|
return;
|
|
246
593
|
}
|
|
247
|
-
const response = await handleJsonRpcRequest(request, activePage);
|
|
594
|
+
const response = await handleJsonRpcRequest(request, activePage, actionLogger);
|
|
248
595
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
249
596
|
res.end(JSON.stringify(response));
|
|
250
597
|
}
|
|
@@ -268,6 +615,8 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
268
615
|
return {
|
|
269
616
|
url,
|
|
270
617
|
close: async () => {
|
|
618
|
+
// Save the action log before closing
|
|
619
|
+
await actionLogger.save();
|
|
271
620
|
await Promise.allSettled([
|
|
272
621
|
new Promise((resolve, reject) => {
|
|
273
622
|
server.close((error) => {
|
|
@@ -286,6 +635,62 @@ export async function startEmbeddedPlaywrightMcpServer(options) {
|
|
|
286
635
|
: Promise.resolve(),
|
|
287
636
|
]);
|
|
288
637
|
},
|
|
638
|
+
flushLog: async () => {
|
|
639
|
+
await actionLogger.saveAndReset();
|
|
640
|
+
},
|
|
641
|
+
captureInteractiveElements: async () => {
|
|
642
|
+
try {
|
|
643
|
+
// Wait for page to be ready
|
|
644
|
+
await page.waitForLoadState("domcontentloaded");
|
|
645
|
+
const elements = await page.evaluate(() => {
|
|
646
|
+
const selector = 'button, a, input, select, textarea, [role="button"], [role="link"], [role="checkbox"], [role="radio"], [role="switch"], [role="tab"], [role="menuitem"], [onclick], [data-testid]';
|
|
647
|
+
const nodeList = document.querySelectorAll(selector);
|
|
648
|
+
return Array.from(nodeList)
|
|
649
|
+
.map((el) => {
|
|
650
|
+
const text = el.textContent?.trim().slice(0, 100);
|
|
651
|
+
const id = el.id || undefined;
|
|
652
|
+
const className = el.className || undefined;
|
|
653
|
+
const testId = el.getAttribute("data-testid") || undefined;
|
|
654
|
+
const placeholder = el.getAttribute("placeholder") || undefined;
|
|
655
|
+
const href = el.getAttribute("href") || undefined;
|
|
656
|
+
const type = el.getAttribute("type") || undefined;
|
|
657
|
+
const role = el.getAttribute("role") || undefined;
|
|
658
|
+
const ariaLabel = el.getAttribute("aria-label") || undefined;
|
|
659
|
+
const name = el.getAttribute("name") || undefined;
|
|
660
|
+
const disabled = el.hasAttribute("disabled") ||
|
|
661
|
+
el.getAttribute("aria-disabled") === "true";
|
|
662
|
+
return {
|
|
663
|
+
tag: el.tagName.toLowerCase(),
|
|
664
|
+
text: text || undefined,
|
|
665
|
+
id,
|
|
666
|
+
className: typeof className === "string" ? className : undefined,
|
|
667
|
+
testId,
|
|
668
|
+
placeholder,
|
|
669
|
+
href,
|
|
670
|
+
type,
|
|
671
|
+
role,
|
|
672
|
+
ariaLabel,
|
|
673
|
+
name,
|
|
674
|
+
disabled: disabled || undefined,
|
|
675
|
+
};
|
|
676
|
+
})
|
|
677
|
+
.filter((el) => {
|
|
678
|
+
// Filter out elements with no useful selector info
|
|
679
|
+
return (el.text ||
|
|
680
|
+
el.id ||
|
|
681
|
+
el.testId ||
|
|
682
|
+
el.placeholder ||
|
|
683
|
+
el.ariaLabel ||
|
|
684
|
+
el.name);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
return elements;
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
logger.warn(`Failed to capture interactive elements: ${String(error)}`);
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
},
|
|
289
694
|
};
|
|
290
695
|
}
|
|
291
696
|
async function launchBrowser() {
|
|
@@ -310,16 +715,10 @@ async function readRequestBody(req) {
|
|
|
310
715
|
}
|
|
311
716
|
return Buffer.concat(chunks).toString("utf-8");
|
|
312
717
|
}
|
|
313
|
-
async function handleJsonRpcRequest(request, page) {
|
|
718
|
+
async function handleJsonRpcRequest(request, page, actionLogger) {
|
|
314
719
|
const { method, id } = request;
|
|
315
|
-
console.log("🎬 MCP Server: Received request", {
|
|
316
|
-
method,
|
|
317
|
-
id,
|
|
318
|
-
hasParams: !!request.params,
|
|
319
|
-
});
|
|
320
720
|
try {
|
|
321
721
|
if (method === "tools/list") {
|
|
322
|
-
console.log("🎬 MCP Server: Listing tools");
|
|
323
722
|
return {
|
|
324
723
|
jsonrpc: "2.0",
|
|
325
724
|
id,
|
|
@@ -327,16 +726,113 @@ async function handleJsonRpcRequest(request, page) {
|
|
|
327
726
|
tools: [
|
|
328
727
|
{
|
|
329
728
|
name: "playwright_action",
|
|
330
|
-
description:
|
|
729
|
+
description: `Execute Playwright browser automation actions. The browser is already navigated to the app.
|
|
730
|
+
|
|
731
|
+
ACTIONS:
|
|
732
|
+
- screenshot: Take a screenshot. Params: { fullPage?: boolean }
|
|
733
|
+
- click: Click an element. Params: { selector: string }
|
|
734
|
+
- fill: Type into an input. Params: { selector: string, value: string }
|
|
735
|
+
- getText: Get text content. Params: { selector: string }
|
|
736
|
+
- waitForSelector: Wait for element. Params: { selector: string }
|
|
737
|
+
- evaluate: Run JavaScript. Params: { script: string }
|
|
738
|
+
- getUrl: Get current URL. No params.
|
|
739
|
+
- reload: Reload the page. No params.
|
|
740
|
+
- navigate: Go to URL. Params: { url: string }
|
|
741
|
+
- getConsoleLogs: Get browser console logs. Params: { clear?: boolean }
|
|
742
|
+
- scroll: Scroll the page. Params: { x?: number, y?: number, deltaX?: number, deltaY?: number }
|
|
743
|
+
- scrollIntoView: Scroll element into view. Params: { selector: string }
|
|
744
|
+
- checkRuntimeErrors: Explicitly check for component crashes. No params.
|
|
745
|
+
|
|
746
|
+
RUNTIME ERROR DETECTION:
|
|
747
|
+
Actions (navigate, click, fill, evaluate) automatically detect component crashes.
|
|
748
|
+
When a component crashes, the action returns:
|
|
749
|
+
- success: false
|
|
750
|
+
- runtimeErrors: Array of { header: string, stack?: string }
|
|
751
|
+
- error: Summary message
|
|
752
|
+
|
|
753
|
+
If you see runtimeErrors in a response, the test should FAIL. Include the error details in your summary.
|
|
754
|
+
|
|
755
|
+
SELECTOR EXAMPLES:
|
|
756
|
+
- By testid: [data-testid="login-form"]
|
|
757
|
+
- By name: input[name="username"], textarea[name="bio"]
|
|
758
|
+
- By placeholder: input[placeholder="Enter name"]
|
|
759
|
+
- By type: input[type="email"], button[type="submit"]
|
|
760
|
+
- By text content: button:has-text("Submit"), a:has-text("Click here")
|
|
761
|
+
- By ID: #submitBtn, #loginForm
|
|
762
|
+
- By class: .btn-primary, .form-input
|
|
763
|
+
- Combining: form#login input[name="password"]
|
|
764
|
+
|
|
765
|
+
Any selector that playwright supports can be used.
|
|
766
|
+
|
|
767
|
+
CSS SELECTOR SYNTAX (IMPORTANT):
|
|
768
|
+
✓ CORRECT: input[name="email"], button[type="submit"], #myId, .myClass
|
|
769
|
+
✗ WRONG: input [name="email"] (no space before bracket!)
|
|
770
|
+
✗ WRONG: input[name=email] (quotes required around value)
|
|
771
|
+
|
|
772
|
+
TIPS:
|
|
773
|
+
1. Use specific selectors: prefer [name="x"] or [placeholder="x"] over generic .class
|
|
774
|
+
2. If click/fill fails, the element may not be visible - try waitForSelector first
|
|
775
|
+
3. For dynamic content, use waitForSelector before interacting
|
|
776
|
+
4. Navigate, click, and fill will automatically take a screenshot of the page after the action is performed
|
|
777
|
+
5. If success is false due to runtimeErrors, FAIL the test and report the error details`,
|
|
331
778
|
inputSchema: {
|
|
332
779
|
type: "object",
|
|
333
780
|
properties: {
|
|
334
781
|
action: {
|
|
335
782
|
type: "string",
|
|
336
783
|
enum: PLAYWRIGHT_ACTIONS,
|
|
784
|
+
description: "The action to perform. Most actions require a 'selector' parameter.",
|
|
785
|
+
},
|
|
786
|
+
description: {
|
|
787
|
+
type: "string",
|
|
788
|
+
description: "Brief description of what this test step is verifying (e.g., 'Verify login form appears', 'Test submit button functionality'). This will be displayed in the test report.",
|
|
789
|
+
},
|
|
790
|
+
selector: {
|
|
791
|
+
type: "string",
|
|
792
|
+
description: 'CSS selector for the target element. NO SPACE before brackets! Example: input[name="email"] NOT input [name="email"]',
|
|
793
|
+
},
|
|
794
|
+
value: {
|
|
795
|
+
type: "string",
|
|
796
|
+
description: "Value to fill (for 'fill' action)",
|
|
797
|
+
},
|
|
798
|
+
url: {
|
|
799
|
+
type: "string",
|
|
800
|
+
description: "URL to navigate to (for 'navigate' action)",
|
|
801
|
+
},
|
|
802
|
+
script: {
|
|
803
|
+
type: "string",
|
|
804
|
+
description: "JavaScript to execute in browser (for 'evaluate' action). No imports allowed.",
|
|
805
|
+
},
|
|
806
|
+
fullPage: {
|
|
807
|
+
type: "boolean",
|
|
808
|
+
description: "Capture full scrollable page (for 'screenshot' action)",
|
|
809
|
+
},
|
|
810
|
+
clear: {
|
|
811
|
+
type: "boolean",
|
|
812
|
+
description: "Clear logs after retrieving (for 'getConsoleLogs' action)",
|
|
813
|
+
},
|
|
814
|
+
x: {
|
|
815
|
+
type: "number",
|
|
816
|
+
description: "Absolute horizontal scroll position in pixels (for 'scroll' action)",
|
|
817
|
+
},
|
|
818
|
+
y: {
|
|
819
|
+
type: "number",
|
|
820
|
+
description: "Absolute vertical scroll position in pixels (for 'scroll' action)",
|
|
821
|
+
},
|
|
822
|
+
deltaX: {
|
|
823
|
+
type: "number",
|
|
824
|
+
description: "Relative horizontal scroll amount in pixels (for 'scroll' action)",
|
|
825
|
+
},
|
|
826
|
+
deltaY: {
|
|
827
|
+
type: "number",
|
|
828
|
+
description: "Relative vertical scroll amount in pixels (for 'scroll' action)",
|
|
829
|
+
},
|
|
830
|
+
testCaseId: {
|
|
831
|
+
type: "string",
|
|
832
|
+
description: "ID of the test case this action belongs to. Required when test cases are predefined. Links this action to a specific test case for status tracking.",
|
|
337
833
|
},
|
|
338
834
|
},
|
|
339
|
-
required: ["action"],
|
|
835
|
+
required: ["action", "description"],
|
|
340
836
|
},
|
|
341
837
|
},
|
|
342
838
|
],
|
|
@@ -344,13 +840,7 @@ async function handleJsonRpcRequest(request, page) {
|
|
|
344
840
|
};
|
|
345
841
|
}
|
|
346
842
|
if (method === "tools/call") {
|
|
347
|
-
console.log("🎬 MCP Server: tools/call method");
|
|
348
843
|
const params = request.params;
|
|
349
|
-
console.log("🎬 MCP Server: Tool call params", {
|
|
350
|
-
toolName: params?.name,
|
|
351
|
-
hasArguments: !!params?.arguments,
|
|
352
|
-
action: params?.arguments?.action,
|
|
353
|
-
});
|
|
354
844
|
if (!params || params.name !== "playwright_action") {
|
|
355
845
|
throw new Error(`Unknown tool: ${params?.name ?? "undefined"}`);
|
|
356
846
|
}
|
|
@@ -358,12 +848,16 @@ async function handleJsonRpcRequest(request, page) {
|
|
|
358
848
|
if (!action || !PLAYWRIGHT_ACTIONS.includes(action)) {
|
|
359
849
|
throw new Error(`Unsupported Playwright action: ${String(action)}`);
|
|
360
850
|
}
|
|
361
|
-
|
|
851
|
+
// Log the action start
|
|
852
|
+
actionLogger.logActionStart(action, params.arguments ?? {});
|
|
362
853
|
const result = await executePlaywrightAction(action, page, params.arguments ?? {});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
854
|
+
// Log the action result
|
|
855
|
+
actionLogger.logActionResult(result);
|
|
856
|
+
// Include testCaseId in the result if provided
|
|
857
|
+
const testCaseId = params.arguments?.testCaseId;
|
|
858
|
+
if (testCaseId) {
|
|
859
|
+
result.testCaseId = testCaseId;
|
|
860
|
+
}
|
|
367
861
|
return {
|
|
368
862
|
jsonrpc: "2.0",
|
|
369
863
|
id,
|
|
@@ -392,96 +886,298 @@ async function handleJsonRpcRequest(request, page) {
|
|
|
392
886
|
};
|
|
393
887
|
}
|
|
394
888
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
889
|
+
/**
|
|
890
|
+
* Checks for runtime errors displayed by ErrorBoundary components.
|
|
891
|
+
* These are detected by looking for elements with data-testid="runtime-error-details".
|
|
892
|
+
*
|
|
893
|
+
* @param page - The Playwright page to check
|
|
894
|
+
* @returns Object with hasErrors flag and array of error details
|
|
895
|
+
*/
|
|
896
|
+
async function checkForRuntimeErrors(page) {
|
|
897
|
+
try {
|
|
898
|
+
const errorElements = await page
|
|
899
|
+
.locator('[data-testid="runtime-error-details"]')
|
|
900
|
+
.all();
|
|
901
|
+
if (errorElements.length === 0) {
|
|
902
|
+
return { hasErrors: false, errors: [] };
|
|
903
|
+
}
|
|
904
|
+
const errors = [];
|
|
905
|
+
for (const element of errorElements) {
|
|
906
|
+
try {
|
|
907
|
+
// Get the error header (h3 contains "Something went wrong." or custom header)
|
|
908
|
+
const header = (await element.locator("h3").textContent()) || "Component error";
|
|
909
|
+
// Try to get the stack trace from the pre > code element inside details
|
|
910
|
+
let stack;
|
|
911
|
+
try {
|
|
912
|
+
const codeElement = element.locator("pre code");
|
|
913
|
+
const codeText = await codeElement.textContent({ timeout: 1000 });
|
|
914
|
+
stack = codeText || undefined;
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
// Stack not available or details not expanded
|
|
918
|
+
}
|
|
919
|
+
errors.push({ header, stack });
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
errors.push({ header: "Unknown component error" });
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return { hasErrors: true, errors };
|
|
926
|
+
}
|
|
927
|
+
catch {
|
|
928
|
+
return { hasErrors: false, errors: [] };
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Captures a screenshot in PNG format.
|
|
933
|
+
*
|
|
934
|
+
* TODO: Re-enable WebP conversion using sharp once the build issue is resolved.
|
|
935
|
+
* WebP provides ~25-34% smaller file sizes compared to JPEG at equivalent
|
|
936
|
+
* visual quality, with better compression than PNG.
|
|
937
|
+
*
|
|
938
|
+
* @param page - The Playwright page to capture
|
|
939
|
+
* @returns Screenshot data as base64 string with format identifier
|
|
940
|
+
*/
|
|
941
|
+
async function captureScreenshot(page) {
|
|
942
|
+
try {
|
|
943
|
+
await page.evaluate(() => document.fonts.ready);
|
|
944
|
+
await page.waitForTimeout(100);
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
// Continue anyway if fonts.ready fails
|
|
948
|
+
}
|
|
949
|
+
const pngBuffer = await page.screenshot({
|
|
950
|
+
fullPage: false,
|
|
951
|
+
scale: "css",
|
|
952
|
+
type: "png",
|
|
400
953
|
});
|
|
954
|
+
// TODO: Re-enable WebP conversion once sharp build issue is resolved
|
|
955
|
+
// const webpBuffer = await sharp(pngBuffer).webp({ quality: 80 }).toBuffer();
|
|
956
|
+
// return { data: webpBuffer.toString("base64"), format: "webp" };
|
|
957
|
+
return {
|
|
958
|
+
data: pngBuffer.toString("base64"),
|
|
959
|
+
format: "png",
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
async function executePlaywrightAction(action, page, params) {
|
|
401
963
|
switch (action) {
|
|
402
964
|
case "navigate": {
|
|
403
965
|
const url = params.url;
|
|
966
|
+
const description = params.description || "Navigate to page";
|
|
404
967
|
if (!url)
|
|
405
968
|
throw new Error("Missing url parameter for navigate action");
|
|
406
|
-
console.log(`🎬 MCP Server: Navigating to ${url}`);
|
|
407
969
|
try {
|
|
408
970
|
await page.goto(url, {
|
|
409
971
|
waitUntil: "load",
|
|
410
972
|
timeout: 60000, // 60 second timeout
|
|
411
973
|
});
|
|
412
|
-
|
|
413
|
-
|
|
974
|
+
// Auto-capture screenshot after navigation
|
|
975
|
+
const screenshot = await captureScreenshot(page);
|
|
976
|
+
// Check for runtime errors (component crashes)
|
|
977
|
+
const runtimeErrors = await checkForRuntimeErrors(page);
|
|
978
|
+
return {
|
|
979
|
+
success: !runtimeErrors.hasErrors,
|
|
980
|
+
action: "navigate",
|
|
981
|
+
description,
|
|
982
|
+
screenshot: screenshot.data,
|
|
983
|
+
format: screenshot.format,
|
|
984
|
+
context: {
|
|
985
|
+
url,
|
|
986
|
+
},
|
|
987
|
+
...(runtimeErrors.hasErrors && {
|
|
988
|
+
runtimeErrors: runtimeErrors.errors,
|
|
989
|
+
error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
|
|
990
|
+
}),
|
|
991
|
+
};
|
|
414
992
|
}
|
|
415
993
|
catch (error) {
|
|
416
994
|
console.error(`🎬 MCP Server: Navigation failed: ${String(error)}`);
|
|
417
|
-
//
|
|
995
|
+
// Capture screenshot even on error
|
|
996
|
+
let errorScreenshot = null;
|
|
997
|
+
try {
|
|
998
|
+
errorScreenshot = await captureScreenshot(page);
|
|
999
|
+
}
|
|
1000
|
+
catch {
|
|
1001
|
+
// Ignore screenshot errors
|
|
1002
|
+
}
|
|
418
1003
|
return {
|
|
419
1004
|
success: false,
|
|
1005
|
+
action: "navigate",
|
|
1006
|
+
description,
|
|
1007
|
+
screenshot: errorScreenshot?.data,
|
|
1008
|
+
format: errorScreenshot?.format ?? "png",
|
|
1009
|
+
context: {
|
|
1010
|
+
url,
|
|
1011
|
+
},
|
|
420
1012
|
error: `Navigation to ${url} failed: ${String(error)}`,
|
|
421
|
-
url,
|
|
422
1013
|
};
|
|
423
1014
|
}
|
|
424
1015
|
}
|
|
425
1016
|
case "click": {
|
|
426
1017
|
const selector = params.selector;
|
|
1018
|
+
const description = params.description || "Click element";
|
|
427
1019
|
if (!selector)
|
|
428
1020
|
throw new Error("Missing selector for click action");
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
1021
|
+
try {
|
|
1022
|
+
await page
|
|
1023
|
+
.locator(selector)
|
|
1024
|
+
.click({ timeout: params.timeout ?? 10000 });
|
|
1025
|
+
// Auto-capture screenshot after action
|
|
1026
|
+
const screenshot = await captureScreenshot(page);
|
|
1027
|
+
// Check for runtime errors (component crashes)
|
|
1028
|
+
const runtimeErrors = await checkForRuntimeErrors(page);
|
|
1029
|
+
return {
|
|
1030
|
+
success: !runtimeErrors.hasErrors,
|
|
1031
|
+
action: "click",
|
|
1032
|
+
description,
|
|
1033
|
+
screenshot: screenshot.data,
|
|
1034
|
+
format: screenshot.format,
|
|
1035
|
+
context: {
|
|
1036
|
+
selector,
|
|
1037
|
+
},
|
|
1038
|
+
...(runtimeErrors.hasErrors && {
|
|
1039
|
+
runtimeErrors: runtimeErrors.errors,
|
|
1040
|
+
error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
|
|
1041
|
+
}),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
// Capture screenshot even on error
|
|
1046
|
+
let errorScreenshot = null;
|
|
1047
|
+
try {
|
|
1048
|
+
errorScreenshot = await captureScreenshot(page);
|
|
1049
|
+
}
|
|
1050
|
+
catch {
|
|
1051
|
+
// Ignore screenshot errors
|
|
1052
|
+
}
|
|
1053
|
+
return {
|
|
1054
|
+
success: false,
|
|
1055
|
+
action: "click",
|
|
1056
|
+
description,
|
|
1057
|
+
screenshot: errorScreenshot?.data,
|
|
1058
|
+
format: errorScreenshot?.format ?? "png",
|
|
1059
|
+
context: {
|
|
1060
|
+
selector,
|
|
1061
|
+
},
|
|
1062
|
+
error: `Click failed on "${selector}": ${String(error)}`,
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
433
1065
|
}
|
|
434
1066
|
case "fill": {
|
|
435
1067
|
const selector = params.selector;
|
|
436
1068
|
const value = params.value ?? "";
|
|
1069
|
+
const description = params.description || "Fill input";
|
|
437
1070
|
if (!selector)
|
|
438
1071
|
throw new Error("Missing selector for fill action");
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
await page
|
|
1074
|
+
.locator(selector)
|
|
1075
|
+
.fill(value, { timeout: params.timeout ?? 10000 });
|
|
1076
|
+
// Auto-capture screenshot after fill
|
|
1077
|
+
const screenshot = await captureScreenshot(page);
|
|
1078
|
+
// Check for runtime errors (component crashes)
|
|
1079
|
+
const runtimeErrors = await checkForRuntimeErrors(page);
|
|
1080
|
+
return {
|
|
1081
|
+
success: !runtimeErrors.hasErrors,
|
|
1082
|
+
action: "fill",
|
|
1083
|
+
description,
|
|
1084
|
+
screenshot: screenshot.data,
|
|
1085
|
+
format: screenshot.format,
|
|
1086
|
+
context: {
|
|
1087
|
+
selector,
|
|
1088
|
+
value,
|
|
1089
|
+
},
|
|
1090
|
+
...(runtimeErrors.hasErrors && {
|
|
1091
|
+
runtimeErrors: runtimeErrors.errors,
|
|
1092
|
+
error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
|
|
1093
|
+
}),
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
// Capture screenshot even on error
|
|
1098
|
+
let errorScreenshot = null;
|
|
1099
|
+
try {
|
|
1100
|
+
errorScreenshot = await captureScreenshot(page);
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
// Ignore screenshot errors
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
success: false,
|
|
1107
|
+
action: "fill",
|
|
1108
|
+
description,
|
|
1109
|
+
screenshot: errorScreenshot?.data,
|
|
1110
|
+
format: errorScreenshot?.format ?? "png",
|
|
1111
|
+
context: {
|
|
1112
|
+
selector,
|
|
1113
|
+
value,
|
|
1114
|
+
},
|
|
1115
|
+
error: `Fill failed on "${selector}": ${String(error)}`,
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
443
1118
|
}
|
|
444
1119
|
case "screenshot": {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
1120
|
+
const description = params.description || "Capture current state";
|
|
1121
|
+
// Capture screenshot in WebP format for optimal compression
|
|
1122
|
+
const screenshot = await captureScreenshot(page);
|
|
448
1123
|
return {
|
|
449
1124
|
success: true,
|
|
450
|
-
|
|
1125
|
+
action: "screenshot",
|
|
1126
|
+
description,
|
|
1127
|
+
screenshot: screenshot.data,
|
|
1128
|
+
format: screenshot.format,
|
|
1129
|
+
context: {
|
|
1130
|
+
fullPage: Boolean(params.fullPage),
|
|
1131
|
+
},
|
|
451
1132
|
};
|
|
452
1133
|
}
|
|
453
1134
|
case "getText": {
|
|
454
1135
|
const selector = params.selector;
|
|
455
1136
|
if (!selector)
|
|
456
1137
|
throw new Error("Missing selector for getText action");
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1138
|
+
try {
|
|
1139
|
+
const text = await page.locator(selector).textContent({
|
|
1140
|
+
timeout: params.timeout ?? 10000,
|
|
1141
|
+
});
|
|
1142
|
+
return { success: true, text };
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
return {
|
|
1146
|
+
success: false,
|
|
1147
|
+
error: `getText failed on "${selector}": ${String(error)}`,
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
463
1150
|
}
|
|
464
1151
|
case "waitForSelector": {
|
|
465
1152
|
const selector = params.selector;
|
|
466
1153
|
if (!selector) {
|
|
467
1154
|
throw new Error("Missing selector for waitForSelector action");
|
|
468
1155
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1156
|
+
try {
|
|
1157
|
+
await page.locator(selector).waitFor({
|
|
1158
|
+
timeout: params.timeout ?? 15000,
|
|
1159
|
+
});
|
|
1160
|
+
return { success: true };
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
return {
|
|
1164
|
+
success: false,
|
|
1165
|
+
error: `waitForSelector failed on "${selector}": ${String(error)}`,
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
475
1168
|
}
|
|
476
1169
|
case "evaluate": {
|
|
477
1170
|
const script = params.script;
|
|
1171
|
+
const description = params.description || "Execute JavaScript";
|
|
478
1172
|
if (typeof script !== "string") {
|
|
479
1173
|
throw new Error("Missing script for evaluate action");
|
|
480
1174
|
}
|
|
481
1175
|
// Check for import/export/require statements that can't be used in browser context
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1176
|
+
// Use regex with word boundaries to avoid false positives on strings like "important"
|
|
1177
|
+
const hasImport = /\bimport\s+/.test(script);
|
|
1178
|
+
const hasExport = /\bexport\s+/.test(script);
|
|
1179
|
+
const hasRequire = /\brequire\s*\(/.test(script);
|
|
1180
|
+
if (hasImport || hasExport || hasRequire) {
|
|
485
1181
|
throw new Error("Cannot use import/export/require statements in browser evaluate context. " +
|
|
486
1182
|
"Please use plain JavaScript that can run in a browser console. " +
|
|
487
1183
|
"For example, use 'document.querySelector' instead of importing libraries.");
|
|
@@ -496,32 +1192,70 @@ async function executePlaywrightAction(action, page, params) {
|
|
|
496
1192
|
return { error: e.toString(), stack: e.stack };
|
|
497
1193
|
}
|
|
498
1194
|
})()`;
|
|
499
|
-
|
|
500
|
-
const frameElement = await page
|
|
501
|
-
.locator('[data-test="sb-iframe"]')
|
|
502
|
-
.elementHandle();
|
|
503
|
-
const frame = await frameElement?.contentFrame();
|
|
504
|
-
if (!frame) {
|
|
505
|
-
throw new Error("Could not find iframe [data-test='sb-iframe'] or iframe not loaded");
|
|
506
|
-
}
|
|
507
|
-
const result = await frame.evaluate(wrappedScript);
|
|
1195
|
+
const result = await page.evaluate(wrappedScript);
|
|
508
1196
|
// Check if the evaluation returned an error
|
|
509
1197
|
if (result && typeof result === "object" && "error" in result) {
|
|
510
1198
|
throw new Error(`Browser evaluation failed: ${result.error}`);
|
|
511
1199
|
}
|
|
512
|
-
|
|
1200
|
+
// Auto-capture screenshot after evaluation
|
|
1201
|
+
const screenshot = await captureScreenshot(page);
|
|
1202
|
+
// Check for runtime errors (component crashes)
|
|
1203
|
+
const runtimeErrors = await checkForRuntimeErrors(page);
|
|
1204
|
+
return {
|
|
1205
|
+
success: !runtimeErrors.hasErrors,
|
|
1206
|
+
action: "evaluate",
|
|
1207
|
+
description,
|
|
1208
|
+
result,
|
|
1209
|
+
screenshot: screenshot.data,
|
|
1210
|
+
format: screenshot.format,
|
|
1211
|
+
context: {
|
|
1212
|
+
script: script.substring(0, 100) + (script.length > 100 ? "..." : ""),
|
|
1213
|
+
},
|
|
1214
|
+
...(runtimeErrors.hasErrors && {
|
|
1215
|
+
runtimeErrors: runtimeErrors.errors,
|
|
1216
|
+
error: `Component crashed with ${runtimeErrors.errors.length} runtime error(s): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
|
|
1217
|
+
}),
|
|
1218
|
+
};
|
|
513
1219
|
}
|
|
514
1220
|
catch (error) {
|
|
1221
|
+
// Capture screenshot even on error
|
|
1222
|
+
let errorScreenshot = null;
|
|
1223
|
+
try {
|
|
1224
|
+
errorScreenshot = await captureScreenshot(page);
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
// Ignore screenshot errors
|
|
1228
|
+
}
|
|
515
1229
|
// If the error looks like a syntax error, provide helpful guidance
|
|
516
1230
|
const errorMsg = String(error);
|
|
517
1231
|
if (errorMsg.includes("SyntaxError") ||
|
|
518
1232
|
errorMsg.includes("import") ||
|
|
519
1233
|
errorMsg.includes("module")) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
1234
|
+
return {
|
|
1235
|
+
success: false,
|
|
1236
|
+
action: "evaluate",
|
|
1237
|
+
description,
|
|
1238
|
+
screenshot: errorScreenshot?.data,
|
|
1239
|
+
format: errorScreenshot?.format ?? "png",
|
|
1240
|
+
context: {
|
|
1241
|
+
script: script.substring(0, 100) + (script.length > 100 ? "..." : ""),
|
|
1242
|
+
},
|
|
1243
|
+
error: "JavaScript evaluation failed. Make sure your code is browser-compatible " +
|
|
1244
|
+
"(no import/export statements). Error: " +
|
|
1245
|
+
errorMsg,
|
|
1246
|
+
};
|
|
523
1247
|
}
|
|
524
|
-
|
|
1248
|
+
return {
|
|
1249
|
+
success: false,
|
|
1250
|
+
action: "evaluate",
|
|
1251
|
+
description,
|
|
1252
|
+
screenshot: errorScreenshot?.data,
|
|
1253
|
+
format: errorScreenshot?.format ?? "png",
|
|
1254
|
+
context: {
|
|
1255
|
+
script: script.substring(0, 100) + (script.length > 100 ? "..." : ""),
|
|
1256
|
+
},
|
|
1257
|
+
error: String(error),
|
|
1258
|
+
};
|
|
525
1259
|
}
|
|
526
1260
|
}
|
|
527
1261
|
case "getUrl": {
|
|
@@ -534,6 +1268,130 @@ async function executePlaywrightAction(action, page, params) {
|
|
|
534
1268
|
});
|
|
535
1269
|
return { success: true };
|
|
536
1270
|
}
|
|
1271
|
+
case "getConsoleLogs": {
|
|
1272
|
+
// Return captured console logs and optionally clear them
|
|
1273
|
+
const logs = [...capturedConsoleLogs];
|
|
1274
|
+
if (params.clear) {
|
|
1275
|
+
capturedConsoleLogs = [];
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
success: true,
|
|
1279
|
+
logs,
|
|
1280
|
+
count: logs.length,
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
case "scroll": {
|
|
1284
|
+
const { x, y, deltaX, deltaY } = params;
|
|
1285
|
+
const description = params.description || "Scroll page";
|
|
1286
|
+
try {
|
|
1287
|
+
await page.evaluate(({ x, y, deltaX, deltaY }) => {
|
|
1288
|
+
if (deltaX !== undefined || deltaY !== undefined) {
|
|
1289
|
+
window.scrollBy(deltaX ?? 0, deltaY ?? 0);
|
|
1290
|
+
}
|
|
1291
|
+
else if (x !== undefined || y !== undefined) {
|
|
1292
|
+
window.scrollTo(x ?? window.scrollX, y ?? window.scrollY);
|
|
1293
|
+
}
|
|
1294
|
+
}, { x, y, deltaX, deltaY });
|
|
1295
|
+
const screenshot = await captureScreenshot(page);
|
|
1296
|
+
return {
|
|
1297
|
+
success: true,
|
|
1298
|
+
action: "scroll",
|
|
1299
|
+
description,
|
|
1300
|
+
screenshot: screenshot.data,
|
|
1301
|
+
format: screenshot.format,
|
|
1302
|
+
context: {
|
|
1303
|
+
x,
|
|
1304
|
+
y,
|
|
1305
|
+
deltaX,
|
|
1306
|
+
deltaY,
|
|
1307
|
+
},
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
catch (error) {
|
|
1311
|
+
let errorScreenshot = null;
|
|
1312
|
+
try {
|
|
1313
|
+
errorScreenshot = await captureScreenshot(page);
|
|
1314
|
+
}
|
|
1315
|
+
catch {
|
|
1316
|
+
// Ignore screenshot errors
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
success: false,
|
|
1320
|
+
action: "scroll",
|
|
1321
|
+
description,
|
|
1322
|
+
screenshot: errorScreenshot?.data,
|
|
1323
|
+
format: errorScreenshot?.format ?? "png",
|
|
1324
|
+
context: {
|
|
1325
|
+
x,
|
|
1326
|
+
y,
|
|
1327
|
+
deltaX,
|
|
1328
|
+
deltaY,
|
|
1329
|
+
},
|
|
1330
|
+
error: `Scroll failed: ${String(error)}`,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
case "scrollIntoView": {
|
|
1335
|
+
const selector = params.selector;
|
|
1336
|
+
const description = params.description || "Scroll element into view";
|
|
1337
|
+
if (!selector) {
|
|
1338
|
+
throw new Error("Missing selector for scrollIntoView action");
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
await page.locator(selector).scrollIntoViewIfNeeded({
|
|
1342
|
+
timeout: params.timeout ?? 10000,
|
|
1343
|
+
});
|
|
1344
|
+
const screenshot = await captureScreenshot(page);
|
|
1345
|
+
return {
|
|
1346
|
+
success: true,
|
|
1347
|
+
action: "scrollIntoView",
|
|
1348
|
+
description,
|
|
1349
|
+
screenshot: screenshot.data,
|
|
1350
|
+
format: screenshot.format,
|
|
1351
|
+
context: {
|
|
1352
|
+
selector,
|
|
1353
|
+
},
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
catch (error) {
|
|
1357
|
+
let errorScreenshot = null;
|
|
1358
|
+
try {
|
|
1359
|
+
errorScreenshot = await captureScreenshot(page);
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
// Ignore screenshot errors
|
|
1363
|
+
}
|
|
1364
|
+
return {
|
|
1365
|
+
success: false,
|
|
1366
|
+
action: "scrollIntoView",
|
|
1367
|
+
description,
|
|
1368
|
+
screenshot: errorScreenshot?.data,
|
|
1369
|
+
format: errorScreenshot?.format ?? "png",
|
|
1370
|
+
context: {
|
|
1371
|
+
selector,
|
|
1372
|
+
},
|
|
1373
|
+
error: `scrollIntoView failed on "${selector}": ${String(error)}`,
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
case "checkRuntimeErrors": {
|
|
1378
|
+
const description = params.description || "Check for component runtime errors";
|
|
1379
|
+
const screenshot = await captureScreenshot(page);
|
|
1380
|
+
const runtimeErrors = await checkForRuntimeErrors(page);
|
|
1381
|
+
return {
|
|
1382
|
+
success: !runtimeErrors.hasErrors,
|
|
1383
|
+
action: "checkRuntimeErrors",
|
|
1384
|
+
description,
|
|
1385
|
+
screenshot: screenshot.data,
|
|
1386
|
+
format: screenshot.format,
|
|
1387
|
+
hasErrors: runtimeErrors.hasErrors,
|
|
1388
|
+
errorCount: runtimeErrors.errors.length,
|
|
1389
|
+
...(runtimeErrors.hasErrors && {
|
|
1390
|
+
runtimeErrors: runtimeErrors.errors,
|
|
1391
|
+
error: `Found ${runtimeErrors.errors.length} component crash(es): ${runtimeErrors.errors.map((e) => e.header).join(", ")}`,
|
|
1392
|
+
}),
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
537
1395
|
default:
|
|
538
1396
|
throw new Error(`Unhandled Playwright action: ${action}`);
|
|
539
1397
|
}
|