diff --git a/.prototools b/.prototools index cd01d14..1510a39 100644 --- a/.prototools +++ b/.prototools @@ -1,6 +1,6 @@ dagger = "^0" moon = "^1" -node = "^22" +node = "^24" yarn = "^4" [plugins] diff --git a/apps/ext-e2e/.swcrc b/apps/ext-e2e/.swcrc index 8c8a322..a5cb49c 100644 --- a/apps/ext-e2e/.swcrc +++ b/apps/ext-e2e/.swcrc @@ -1,4 +1,5 @@ { + "$schema": "https://swc.rs/schema.json", "jsc": { "parser": { "syntax": "typescript", @@ -10,11 +11,12 @@ "legacyDecorator": true, "decoratorMetadata": true }, - "target": "es2022", + "target": "es2023", "keepClassNames": true }, "module": { "type": "es6" - } + }, + "sourceMaps": "inline" } diff --git a/apps/ext-e2e/package.json b/apps/ext-e2e/package.json index 403bf0b..4d9d3fb 100644 --- a/apps/ext-e2e/package.json +++ b/apps/ext-e2e/package.json @@ -1,3 +1,7 @@ { - "name": "@mcp-browser-kit/ext-e2e" + "name": "@mcp-browser-kit/ext-e2e", + "scripts": { + "test": "TS_NODE_TRANSPILE_ONLY=true TS_NODE_COMPILER_OPTIONS='{\"module\":\"CommonJS\"}' NODE_OPTIONS='--require ts-node/register --enable-source-maps' playwright test", + "test:debug": "TS_NODE_TRANSPILE_ONLY=true TS_NODE_COMPILER_OPTIONS='{\"module\":\"CommonJS\"}' NODE_OPTIONS='--require ts-node/register --enable-source-maps' PWDEBUG=1 playwright test --headed" + } } diff --git a/apps/ext-e2e/playwright.config.ts b/apps/ext-e2e/playwright.config.ts index 314b16c..f2ba43b 100644 --- a/apps/ext-e2e/playwright.config.ts +++ b/apps/ext-e2e/playwright.config.ts @@ -1,8 +1,15 @@ +import path from "node:path"; import { defineConfig, devices } from "@playwright/test"; import type { ExtContextOptions } from "./src/fixtures/ext-context"; process.env.NODE_OPTIONS = "--require @swc-node/register"; +// Set browser path for Playwright extension debugging +process.env.PLAYWRIGHT_BROWSERS_PATH ??= path.resolve( + __dirname, + "../../.tmp/playwright/browsers", +); + export default defineConfig({ testDir: "./src/tests", outputDir: "target/playwright/test-results", @@ -31,11 +38,12 @@ export default defineConfig({ webServer: [ { command: "moon run ext-e2e-test-app:react-router-start-csr", + cwd: path.resolve(__dirname, "../.."), url: "http://localhost:3000", timeout: 30000, reuseExistingServer: !process.env.CI, env: { - // biome-ignore lint/style/useNamingConvention: The issue is that process.env.NODE_OPTIONS = "--require @swc-node/register" is inherited by the webServer child process, causing Yarn to fail. + // biome-ignore lint/style/useNamingConvention: process.env.NODE_OPTIONS = "--require @swc-node/register" is inherited by the webServer child process, causing Yarn to fail. NODE_OPTIONS: "", }, }, diff --git a/apps/ext-e2e/src/fixtures/ext-test.ts b/apps/ext-e2e/src/fixtures/ext-test.ts index 8b099b7..ff4e755 100644 --- a/apps/ext-e2e/src/fixtures/ext-test.ts +++ b/apps/ext-e2e/src/fixtures/ext-test.ts @@ -1,5 +1,6 @@ import { McpClientPageObject } from "../pages/mcp-client-page-object"; import { PlaywrightPage } from "../pages/playwright-page"; +import { TestAppPage } from "../pages/test-app-page"; import { type ExtContextFixtures, type ExtContextOptions, @@ -12,6 +13,7 @@ export type ExtTestFixtures = ExtContextFixtures & ExtContextOptions & { playwrightPage: PlaywrightPage; mcpClientPage: McpClientPageObject; + testAppPage: TestAppPage; }; export const test = extContextTest.extend< @@ -29,6 +31,11 @@ export const test = extContextTest.extend< await use(mcpClientPage); await mcpClientPage.disconnect(); }, + testAppPage: async ({ context }, use) => { + const page = await context.newPage(); + const testAppPage = new TestAppPage(page); + await use(testAppPage); + }, }); export { expect } from "@playwright/test"; diff --git a/apps/ext-e2e/src/pages/index.ts b/apps/ext-e2e/src/pages/index.ts index 9819d76..fc3c6e5 100644 --- a/apps/ext-e2e/src/pages/index.ts +++ b/apps/ext-e2e/src/pages/index.ts @@ -1,3 +1,4 @@ export { BasePage } from "./base-page"; export { McpClientPageObject } from "./mcp-client-page-object"; export { PlaywrightPage } from "./playwright-page"; +export { TestAppPage } from "./test-app-page"; diff --git a/apps/ext-e2e/src/pages/test-app-page.ts b/apps/ext-e2e/src/pages/test-app-page.ts new file mode 100644 index 0000000..e507e98 --- /dev/null +++ b/apps/ext-e2e/src/pages/test-app-page.ts @@ -0,0 +1,94 @@ +import type { Locator, Page } from "@playwright/test"; +import { BasePage } from "./base-page"; + +const TEST_APP_BASE_URL = process.env.TEST_APP_URL ?? "http://localhost:3000"; + +export class TestAppPage extends BasePage { + readonly homeUrl = TEST_APP_BASE_URL; + readonly clickTestUrl = `${TEST_APP_BASE_URL}/click-test`; + readonly formTestUrl = `${TEST_APP_BASE_URL}/form-test`; + readonly textTestUrl = `${TEST_APP_BASE_URL}/text-test`; + readonly javascriptTestUrl = `${TEST_APP_BASE_URL}/javascript-test`; + + readonly pageTitle: Locator; + + constructor(page: Page) { + super(page); + this.pageTitle = this.getByTestId("page-title"); + } + + async navigateToHome() { + await this.goto(this.homeUrl); + await this.waitForPageLoad(); + } + + async navigateToClickTest() { + await this.goto(this.clickTestUrl); + await this.waitForPageLoad(); + } + + async navigateToFormTest() { + await this.goto(this.formTestUrl); + await this.waitForPageLoad(); + } + + async navigateToTextTest() { + await this.goto(this.textTestUrl); + await this.waitForPageLoad(); + } + + async navigateToJavaScriptTest() { + await this.goto(this.javascriptTestUrl); + await this.waitForPageLoad(); + } + + getClickTestLocators() { + return { + clickCount: this.getByTestId("click-count"), + lastClicked: this.getByTestId("last-clicked"), + primaryButton: this.getByTestId("primary-button"), + secondaryButton: this.getByTestId("secondary-button"), + dangerButton: this.getByTestId("danger-button"), + topLeftButton: this.getByTestId("top-left-button"), + centerButton: this.getByTestId("center-button"), + nestedButton: this.getByTestId("nested-button"), + }; + } + + getFormTestLocators() { + return { + searchInput: this.getByTestId("search-input"), + searchButton: this.getByTestId("search-button"), + searchResult: this.getByTestId("search-result"), + usernameInput: this.getByTestId("username-input"), + emailInput: this.getByTestId("email-input"), + passwordInput: this.getByTestId("password-input"), + messageTextarea: this.getByTestId("message-textarea"), + submitButton: this.getByTestId("submit-button"), + submittedData: this.getByTestId("submitted-data"), + formState: this.getByTestId("form-state"), + }; + } + + getTextTestLocators() { + return { + heading1: this.getByTestId("heading-1"), + paragraph1: this.getByTestId("paragraph-1"), + selectableText: this.getByTestId("selectable-text"), + dataTable: this.getByTestId("data-table"), + navigation: this.getByTestId("navigation"), + ariaButtonClose: this.getByTestId("aria-button-close"), + }; + } + + getJavaScriptTestLocators() { + return { + pageInfo: this.getByTestId("page-info"), + renderCount: this.getByTestId("render-count"), + testContainer: this.getByTestId("test-container"), + dynamicContent: this.getByTestId("dynamic-content"), + styleTarget: this.getByTestId("style-target"), + dataElement: this.getByTestId("data-element"), + }; + } +} diff --git a/apps/ext-e2e/src/test-utils/assert-defined.ts b/apps/ext-e2e/src/test-utils/assert-defined.ts new file mode 100644 index 0000000..1350b53 --- /dev/null +++ b/apps/ext-e2e/src/test-utils/assert-defined.ts @@ -0,0 +1,7 @@ +import { expect } from "@playwright/test"; + +export function expectToBeDefined( + arg: T, +): asserts arg is Exclude { + expect(arg).toBeDefined(); +} diff --git a/apps/ext-e2e/src/tests/click-tools.spec.ts b/apps/ext-e2e/src/tests/click-tools.spec.ts new file mode 100644 index 0000000..6722ccb --- /dev/null +++ b/apps/ext-e2e/src/tests/click-tools.spec.ts @@ -0,0 +1,138 @@ +import { expect, test } from "../fixtures/ext-test"; +import { expectToBeDefined } from "../test-utils/assert-defined"; + +test.describe("Click Tools", () => { + test.beforeEach(async ({ mcpClientPage }) => { + test.setTimeout(30000); + await mcpClientPage.startServer(); + await mcpClientPage.connect(); + await mcpClientPage.waitForBrowsers(); + }); + + test.describe("clickOnElement", () => { + test("clicks on button by readable path", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToClickTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const browsers = contextResult.structuredContent?.browsers ?? []; + const tabKey = browsers[0]?.browserWindows[0]?.tabs.find((t) => + t.url.includes("click-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const elements = elementsResult.structuredContent?.elements ?? []; + const primaryButtonPath = elements.find((el) => + el[2]?.includes("Primary Button"), + )?.[0]; + expectToBeDefined(primaryButtonPath); + + await mcpClientPage.callTool("clickOnElement", { + tabKey, + readablePath: primaryButtonPath, + }); + + const locators = testAppPage.getClickTestLocators(); + await expect(locators.clickCount).toContainText("Click Count: 1"); + await expect(locators.lastClicked).toContainText("primary-button"); + }); + + test("clicks on nested button element", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToClickTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("click-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const nestedButtonPath = ( + elementsResult.structuredContent?.elements ?? [] + ).find((el) => el[2]?.includes("Nested Button"))?.[0]; + expectToBeDefined(nestedButtonPath); + + await mcpClientPage.callTool("clickOnElement", { + tabKey, + readablePath: nestedButtonPath, + }); + + const locators = testAppPage.getClickTestLocators(); + await expect(locators.lastClicked).toContainText("nested-button"); + }); + }); + + test.describe("clickOnCoordinates", () => { + test("clicks on button at specific coordinates", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToClickTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("click-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const locators = testAppPage.getClickTestLocators(); + const buttonBox = await locators.secondaryButton.boundingBox(); + expectToBeDefined(buttonBox); + + const centerX = buttonBox.x + buttonBox.width / 2; + const centerY = buttonBox.y + buttonBox.height / 2; + + await mcpClientPage.callTool("clickOnCoordinates", { + tabKey, + x: centerX, + y: centerY, + }); + + await expect(locators.lastClicked).toContainText("secondary-button"); + }); + + test("clicks on positioned button using coordinates", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToClickTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("click-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const locators = testAppPage.getClickTestLocators(); + const centerButtonBox = await locators.centerButton.boundingBox(); + expectToBeDefined(centerButtonBox); + + await mcpClientPage.callTool("clickOnCoordinates", { + tabKey, + x: centerButtonBox.x + centerButtonBox.width / 2, + y: centerButtonBox.y + centerButtonBox.height / 2, + }); + + await expect(locators.lastClicked).toContainText("center-button"); + }); + }); +}); diff --git a/apps/ext-e2e/src/tests/form-tools.spec.ts b/apps/ext-e2e/src/tests/form-tools.spec.ts new file mode 100644 index 0000000..b34d769 --- /dev/null +++ b/apps/ext-e2e/src/tests/form-tools.spec.ts @@ -0,0 +1,255 @@ +import { expect, test } from "../fixtures/ext-test"; +import { expectToBeDefined } from "../test-utils/assert-defined"; + +test.describe + .skip("Form Tools", () => { + test.beforeEach(async ({ mcpClientPage }) => { + test.setTimeout(30000); + await mcpClientPage.startServer(); + await mcpClientPage.connect(); + await mcpClientPage.waitForBrowsers(); + }); + + test.describe("fillTextToElement", () => { + test("fills text into input by readable path", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToFormTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("form-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const usernameInputPath = ( + elementsResult.structuredContent?.elements ?? [] + ).find( + (el) => + el[2]?.includes("Enter username") || el[2]?.includes("Username"), + )?.[0]; + expectToBeDefined(usernameInputPath); + + await mcpClientPage.callTool("fillTextToElement", { + tabKey, + readablePath: usernameInputPath, + value: "testuser123", + }); + + const locators = testAppPage.getFormTestLocators(); + await expect(locators.usernameInput).toHaveValue("testuser123"); + }); + + test("fills text into textarea by readable path", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToFormTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("form-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const textareaPath = ( + elementsResult.structuredContent?.elements ?? [] + ).find( + (el) => + el[2]?.includes("Enter your message") || el[2]?.includes("Message"), + )?.[0]; + expectToBeDefined(textareaPath); + + const testMessage = "This is a test message\nwith multiple lines"; + await mcpClientPage.callTool("fillTextToElement", { + tabKey, + readablePath: textareaPath, + value: testMessage, + }); + + const locators = testAppPage.getFormTestLocators(); + await expect(locators.messageTextarea).toHaveValue(testMessage); + }); + }); + + test.describe("fillTextToCoordinates", () => { + test("fills text into input at coordinates", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToFormTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("form-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const locators = testAppPage.getFormTestLocators(); + const emailBox = await locators.emailInput.boundingBox(); + expectToBeDefined(emailBox); + + await mcpClientPage.callTool("fillTextToCoordinates", { + tabKey, + x: emailBox.x + emailBox.width / 2, + y: emailBox.y + emailBox.height / 2, + value: "test@example.com", + }); + + await expect(locators.emailInput).toHaveValue("test@example.com"); + }); + }); + + test.describe("hitEnterOnElement", () => { + test("submits search form with enter key", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToFormTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("form-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const searchInputPath = ( + elementsResult.structuredContent?.elements ?? [] + ).find((el) => el[2]?.includes("Search"))?.[0]; + expectToBeDefined(searchInputPath); + + await mcpClientPage.callTool("fillTextToElement", { + tabKey, + readablePath: searchInputPath, + value: "test search query", + }); + + await mcpClientPage.callTool("hitEnterOnElement", { + tabKey, + readablePath: searchInputPath, + }); + + const locators = testAppPage.getFormTestLocators(); + await expect(locators.searchResult).toContainText("test search query"); + }); + }); + + test.describe("hitEnterOnCoordinates", () => { + test("submits form with enter key at coordinates", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToFormTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("form-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const locators = testAppPage.getFormTestLocators(); + const searchBox = await locators.searchInput.boundingBox(); + expectToBeDefined(searchBox); + + await mcpClientPage.callTool("fillTextToCoordinates", { + tabKey, + x: searchBox.x + searchBox.width / 2, + y: searchBox.y + searchBox.height / 2, + value: "coordinate search", + }); + + await mcpClientPage.callTool("hitEnterOnCoordinates", { + tabKey, + x: searchBox.x + searchBox.width / 2, + y: searchBox.y + searchBox.height / 2, + }); + + await expect(locators.searchResult).toContainText("coordinate search"); + }); + }); + + test.describe("Form submission workflow", () => { + test("fills and submits complete registration form", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToFormTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("form-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const elements = elementsResult.structuredContent?.elements ?? []; + + const usernameInputPath = elements.find((el) => + el[2]?.includes("Enter username"), + )?.[0]; + const emailInputPath = elements.find((el) => + el[2]?.includes("Enter email"), + )?.[0]; + const submitButtonPath = elements.find((el) => + el[2]?.includes("Submit Form"), + )?.[0]; + + if (usernameInputPath) { + await mcpClientPage.callTool("fillTextToElement", { + tabKey, + readablePath: usernameInputPath, + value: "formuser", + }); + } + + if (emailInputPath) { + await mcpClientPage.callTool("fillTextToElement", { + tabKey, + readablePath: emailInputPath, + value: "form@example.com", + }); + } + + if (submitButtonPath) { + await mcpClientPage.callTool("clickOnElement", { + tabKey, + readablePath: submitButtonPath, + }); + } + + const locators = testAppPage.getFormTestLocators(); + await expect(locators.submittedData).toBeVisible(); + await expect(locators.submittedData).toContainText("formuser"); + }); + }); + }); diff --git a/apps/ext-e2e/src/tests/javascript-tools.spec.ts b/apps/ext-e2e/src/tests/javascript-tools.spec.ts new file mode 100644 index 0000000..62259cb --- /dev/null +++ b/apps/ext-e2e/src/tests/javascript-tools.spec.ts @@ -0,0 +1,310 @@ +import { expect, test } from "../fixtures/ext-test"; +import { expectToBeDefined } from "../test-utils/assert-defined"; + +test.describe + .skip("JavaScript Tools", () => { + test.beforeEach(async ({ mcpClientPage }) => { + test.setTimeout(30000); + await mcpClientPage.startServer(); + await mcpClientPage.connect(); + await mcpClientPage.waitForBrowsers(); + }); + + test.describe("invokeJsFn", () => { + test("returns document title", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return document.title;", + }); + + expect(result.structuredContent).toContain("JavaScript Test"); + }); + + test("returns number value", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return 42;", + }); + + expect(result.structuredContent).toBe(42); + }); + + test("returns boolean value", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return true;", + }); + + expect(result.structuredContent).toBe(true); + }); + + test("returns array value", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return [1, 2, 3];", + }); + + expect(result.structuredContent).toEqual([ + 1, + 2, + 3, + ]); + }); + + test("returns object value", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: 'return { name: "test", value: 123 };', + }); + + expect(result.structuredContent).toEqual({ + name: "test", + value: 123, + }); + }); + + test("returns null value", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return null;", + }); + + expect(result.structuredContent).toBeNull(); + }); + + test("calls window.incrementCounter function", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result1 = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return window.incrementCounter();", + }); + expect(result1.structuredContent).toBe(1); + + const result2 = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return window.incrementCounter();", + }); + expect(result2.structuredContent).toBe(2); + }); + + test("calls window.addMessage function", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: 'return window.addMessage("Hello from test");', + }); + + expect(result.structuredContent).toEqual([ + "Hello from test", + ]); + }); + + test("calls window.computeSum function", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return window.computeSum(10, 25);", + }); + + expect(result.structuredContent).toBe(35); + }); + + test("reads and modifies DOM element", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: ` + const el = document.getElementById('dynamic-content'); + el.textContent = 'Modified by test'; + return el.textContent; + `, + }); + + expect(result.structuredContent).toBe("Modified by test"); + + const locators = testAppPage.getJavaScriptTestLocators(); + await expect(locators.dynamicContent).toContainText("Modified by test"); + }); + + test("reads data attributes", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: ` + const el = document.getElementById('data-element'); + return { + value: el.dataset.value, + count: el.dataset.count, + active: el.dataset.active + }; + `, + }); + + expect(result.structuredContent).toEqual({ + value: "initial", + count: "0", + active: "false", + }); + }); + + test("modifies element styles", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: ` + const el = document.getElementById('style-target'); + el.style.backgroundColor = 'rgb(255, 0, 0)'; + return window.getComputedStyle(el).backgroundColor; + `, + }); + + const locators = testAppPage.getJavaScriptTestLocators(); + await expect(locators.styleTarget).toHaveCSS( + "background-color", + "rgb(255, 0, 0)", + ); + }); + + test("gets window testData object", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const result = await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: "return window.getTestData();", + }); + + expect(result.structuredContent).toEqual({ + counter: 0, + messages: [], + lastAction: null, + }); + }); + }); + }); diff --git a/apps/ext-e2e/src/tests/tab-tools.spec.ts b/apps/ext-e2e/src/tests/tab-tools.spec.ts new file mode 100644 index 0000000..d1c7e36 --- /dev/null +++ b/apps/ext-e2e/src/tests/tab-tools.spec.ts @@ -0,0 +1,300 @@ +import { expect, test } from "../fixtures/ext-test"; +import { expectToBeDefined } from "../test-utils/assert-defined"; + +test.describe + .skip("Tab Tools", () => { + test.beforeEach(async ({ mcpClientPage }) => { + test.setTimeout(30000); + await mcpClientPage.startServer(); + await mcpClientPage.connect(); + await mcpClientPage.waitForBrowsers(); + }); + + test.describe("getContext", () => { + test("returns browser context with tabs", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const context = contextResult.structuredContent; + + expect(context).toBeDefined(); + expect(context?.browsers).toBeDefined(); + expect(context?.browsers.length).toBeGreaterThan(0); + }); + + test("context contains browser windows", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const browsers = contextResult.structuredContent?.browsers ?? []; + + expect(browsers[0]?.browserWindows).toBeDefined(); + expect(browsers[0]?.browserWindows.length).toBeGreaterThan(0); + }); + + test("context contains tab information", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const browsers = contextResult.structuredContent?.browsers ?? []; + const tabs = browsers[0]?.browserWindows[0]?.tabs ?? []; + + const homeTab = tabs.find((t) => t.url.includes("localhost")); + expect(homeTab).toBeDefined(); + expect(homeTab?.tabKey).toBeDefined(); + expect(homeTab?.title).toBeDefined(); + expect(homeTab?.url).toBeDefined(); + }); + + test("context includes available tools", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const browsers = contextResult.structuredContent?.browsers ?? []; + + expect(browsers[0]?.availableTools).toBeDefined(); + expect(Array.isArray(browsers[0]?.availableTools)).toBe(true); + }); + }); + + test.describe("openTab", () => { + test("opens new tab with specified URL", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const browsers = contextResult.structuredContent?.browsers ?? []; + const windowKey = browsers[0]?.browserWindows[0]?.windowKey; + expectToBeDefined(windowKey); + + const openResult = await mcpClientPage.callTool("openTab", { + windowKey, + url: "http://localhost:5173/click-test", + }); + + expect(openResult.structuredContent?.tabKey).toBeDefined(); + expect(openResult.structuredContent?.windowKey).toBe(windowKey); + + const newContextResult = await mcpClientPage.callTool("getContext", {}); + const newTabs = + newContextResult.structuredContent?.browsers[0]?.browserWindows[0] + ?.tabs ?? []; + const clickTestTab = newTabs.find((t) => t.url.includes("click-test")); + expect(clickTestTab).toBeDefined(); + }); + + test("returns correct tabKey for new tab", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const windowKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0] + ?.windowKey; + expectToBeDefined(windowKey); + + const openResult = await mcpClientPage.callTool("openTab", { + windowKey, + url: "http://localhost:5173/form-test", + }); + + const newTabKey = openResult.structuredContent?.tabKey; + expectToBeDefined(newTabKey); + + const textResult = await mcpClientPage.callTool("getReadableText", { + tabKey: newTabKey, + }); + expect(textResult.structuredContent).toContain("Form Test"); + }); + }); + + test.describe("closeTab", () => { + test("closes specified tab", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const windowKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0] + ?.windowKey; + expectToBeDefined(windowKey); + + const openResult = await mcpClientPage.callTool("openTab", { + windowKey, + url: "http://localhost:5173/text-test", + }); + const newTabKey = openResult.structuredContent?.tabKey; + expectToBeDefined(newTabKey); + + const beforeClose = await mcpClientPage.callTool("getContext", {}); + const tabsBeforeClose = + beforeClose.structuredContent?.browsers[0]?.browserWindows[0]?.tabs ?? + []; + const textTestTabBefore = tabsBeforeClose.find((t) => + t.url.includes("text-test"), + ); + expect(textTestTabBefore).toBeDefined(); + + await mcpClientPage.callTool("closeTab", { + tabKey: newTabKey, + }); + + const afterClose = await mcpClientPage.callTool("getContext", {}); + const tabsAfterClose = + afterClose.structuredContent?.browsers[0]?.browserWindows[0]?.tabs ?? + []; + const textTestTabAfter = tabsAfterClose.find((t) => + t.url.includes("text-test"), + ); + expect(textTestTabAfter).toBeUndefined(); + }); + }); + + test.describe("captureTab", () => { + test("captures screenshot of tab", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToClickTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("click-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const captureResult = await mcpClientPage.callTool("captureTab", { + tabKey, + }); + const screenshot = captureResult.structuredContent; + + expect(screenshot).toBeDefined(); + expect(screenshot?.width).toBeGreaterThan(0); + expect(screenshot?.height).toBeGreaterThan(0); + expect(screenshot?.data).toBeDefined(); + expect(typeof screenshot?.data).toBe("string"); + }); + + test("screenshot dimensions are reasonable", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("localhost"), + )?.tabKey; + expectToBeDefined(tabKey); + + const captureResult = await mcpClientPage.callTool("captureTab", { + tabKey, + }); + const screenshot = captureResult.structuredContent; + + expect(screenshot?.width).toBeGreaterThanOrEqual(100); + expect(screenshot?.height).toBeGreaterThanOrEqual(100); + expect(screenshot?.width).toBeLessThanOrEqual(5000); + expect(screenshot?.height).toBeLessThanOrEqual(5000); + }); + + test("screenshot data is base64 encoded", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToJavaScriptTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("javascript-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const captureResult = await mcpClientPage.callTool("captureTab", { + tabKey, + }); + const screenshot = captureResult.structuredContent; + + expectToBeDefined(screenshot?.data); + const base64Regex = /^[A-Za-z0-9+/]+=*$/; + expect(base64Regex.test(screenshot.data)).toBe(true); + }); + }); + + test.describe("Tab workflow", () => { + test("opens multiple tabs and switches between them", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToHome(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const windowKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0] + ?.windowKey; + expectToBeDefined(windowKey); + + const tab1Result = await mcpClientPage.callTool("openTab", { + windowKey, + url: "http://localhost:5173/click-test", + }); + const tab1Key = tab1Result.structuredContent?.tabKey; + + const tab2Result = await mcpClientPage.callTool("openTab", { + windowKey, + url: "http://localhost:5173/form-test", + }); + const tab2Key = tab2Result.structuredContent?.tabKey; + + expectToBeDefined(tab1Key); + expectToBeDefined(tab2Key); + + const text1 = await mcpClientPage.callTool("getReadableText", { + tabKey: tab1Key, + }); + expect(text1.structuredContent).toContain("Click Test"); + + const text2 = await mcpClientPage.callTool("getReadableText", { + tabKey: tab2Key, + }); + expect(text2.structuredContent).toContain("Form Test"); + + await mcpClientPage.callTool("closeTab", { + tabKey: tab1Key, + }); + await mcpClientPage.callTool("closeTab", { + tabKey: tab2Key, + }); + + const finalContext = await mcpClientPage.callTool("getContext", {}); + const finalTabs = + finalContext.structuredContent?.browsers[0]?.browserWindows[0] + ?.tabs ?? []; + const clickTestTab = finalTabs.find((t) => + t.url.includes("click-test"), + ); + const formTestTab = finalTabs.find((t) => t.url.includes("form-test")); + + expect(clickTestTab).toBeUndefined(); + expect(formTestTab).toBeUndefined(); + }); + }); + }); diff --git a/apps/ext-e2e/src/tests/text-tools.spec.ts b/apps/ext-e2e/src/tests/text-tools.spec.ts new file mode 100644 index 0000000..0794ccb --- /dev/null +++ b/apps/ext-e2e/src/tests/text-tools.spec.ts @@ -0,0 +1,280 @@ +import { expect, test } from "../fixtures/ext-test"; +import { expectToBeDefined } from "../test-utils/assert-defined"; + +test.describe + .skip("Text Tools", () => { + test.beforeEach(async ({ mcpClientPage }) => { + test.setTimeout(30000); + await mcpClientPage.startServer(); + await mcpClientPage.connect(); + await mcpClientPage.waitForBrowsers(); + }); + + test.describe("getReadableText", () => { + test("extracts all visible text from page", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const textResult = await mcpClientPage.callTool("getReadableText", { + tabKey, + }); + const pageText = textResult.structuredContent ?? ""; + + expect(pageText).toContain("Text Test Screen"); + expect(pageText).toContain("Heading Level 1"); + expect(pageText).toContain("Heading Level 2"); + expect(pageText).toContain("first paragraph"); + expect(pageText).toContain("bold text"); + expect(pageText).toContain("italic text"); + }); + + test("extracts text from lists", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const textResult = await mcpClientPage.callTool("getReadableText", { + tabKey, + }); + const pageText = textResult.structuredContent ?? ""; + + expect(pageText).toContain("First unordered item"); + expect(pageText).toContain("Second unordered item"); + expect(pageText).toContain("First ordered item"); + }); + + test("extracts text from table", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const textResult = await mcpClientPage.callTool("getReadableText", { + tabKey, + }); + const pageText = textResult.structuredContent ?? ""; + + expect(pageText).toContain("John Doe"); + expect(pageText).toContain("john@example.com"); + expect(pageText).toContain("Admin"); + }); + }); + + test.describe("getReadableElements", () => { + test("returns interactive elements with paths", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const elements = elementsResult.structuredContent?.elements ?? []; + + expect(elements.length).toBeGreaterThan(0); + + const hasPath = elements.every((el) => typeof el[0] === "string"); + const hasRole = elements.every( + (el) => typeof el[1] === "string" || el[1] === null, + ); + const hasText = elements.every( + (el) => typeof el[2] === "string" || el[2] === null, + ); + + expect(hasPath).toBe(true); + expect(hasRole).toBe(true); + expect(hasText).toBe(true); + }); + + test("finds buttons with correct text", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const elements = elementsResult.structuredContent?.elements ?? []; + + const actionButton = elements.find((el) => + el[2]?.includes("Action Button"), + ); + const submitButton = elements.find((el) => + el[2]?.includes("Submit Button"), + ); + const resetButton = elements.find((el) => + el[2]?.includes("Reset Button"), + ); + + expect(actionButton).toBeDefined(); + expect(submitButton).toBeDefined(); + expect(resetButton).toBeDefined(); + }); + + test("finds elements with ARIA labels", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const elements = elementsResult.structuredContent?.elements ?? []; + + const closeButton = elements.find((el) => + el[2]?.includes("Close dialog"), + ); + const addButton = elements.find((el) => + el[2]?.includes("Add new item"), + ); + const deleteButton = elements.find((el) => + el[2]?.includes("Delete item"), + ); + + expect(closeButton).toBeDefined(); + expect(addButton).toBeDefined(); + expect(deleteButton).toBeDefined(); + }); + + test("finds navigation links", async ({ testAppPage, mcpClientPage }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const elementsResult = await mcpClientPage.callTool( + "getReadableElements", + { + tabKey, + }, + ); + const elements = elementsResult.structuredContent?.elements ?? []; + + const homeLink = elements.find((el) => el[2] === "Home"); + const aboutLink = elements.find((el) => el[2] === "About"); + const contactLink = elements.find((el) => el[2] === "Contact"); + + expect(homeLink).toBeDefined(); + expect(aboutLink).toBeDefined(); + expect(contactLink).toBeDefined(); + }); + }); + + test.describe("getSelection", () => { + test("returns empty selection when nothing selected", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + const selectionResult = await mcpClientPage.callTool("getSelection", { + tabKey, + }); + const selection = selectionResult.structuredContent; + + expect(selection?.text).toBe(""); + }); + + test("returns selected text after programmatic selection", async ({ + testAppPage, + mcpClientPage, + }) => { + await testAppPage.navigateToTextTest(); + + const contextResult = await mcpClientPage.callTool("getContext", {}); + const tabKey = + contextResult.structuredContent?.browsers[0]?.browserWindows[0]?.tabs.find( + (t) => t.url.includes("text-test"), + )?.tabKey; + expectToBeDefined(tabKey); + + await mcpClientPage.callTool("invokeJsFn", { + tabKey, + fnBodyCode: ` + const element = document.querySelector('[data-testid="heading-1"]'); + const range = document.createRange(); + range.selectNodeContents(element); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + return selection.toString(); + `, + }); + + const selectionResult = await mcpClientPage.callTool("getSelection", { + tabKey, + }); + const selection = selectionResult.structuredContent; + + expect(selection?.text).toContain("Heading Level 1"); + }); + }); + }); diff --git a/apps/ext-e2e/tsconfig.json b/apps/ext-e2e/tsconfig.json index bf165e4..233b1dd 100644 --- a/apps/ext-e2e/tsconfig.json +++ b/apps/ext-e2e/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "../../tsconfig.options.json", "compilerOptions": { + "sourceMap": true, "emitDeclarationOnly": true, "lib": [ "DOM", diff --git a/etc/scripts/package.json b/etc/scripts/package.json index 9fe0750..c0faf43 100644 --- a/etc/scripts/package.json +++ b/etc/scripts/package.json @@ -3,6 +3,7 @@ "packageManager": "yarn@4.2.2", "type": "module", "devDependencies": { + "@playwright/test": "^1.58.0", "@types/fs-extra": "^11.0.4", "fs-extra": "^11.3.2", "tsx": "^4.20.6", diff --git a/package.json b/package.json index 9e54447..a20110e 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "@biomejs/biome": "2.3.7", "@medv/finder": "^4.0.2", "@mozilla/readability": "^0.6.0", - "@playwright/test": "^1.57.0", + "@playwright/test": "^1.58.0", "@readme/data-urls": "^3.0.0", "@swc-node/register": "^1.11.1", "@swc/core": "^1.15.8", @@ -41,8 +41,10 @@ "sanitize-html": "^2.17.0", "serve": "^14.2.5", "superjson": "^2.2.6", + "ts-node": "^10.9.2", "tsconfig-moon": "^1.4.1", "tsup": "^8.5.1", + "tsx": "^4.21.0", "type-fest": "^5.2.0", "typescript": "^5.9.3", "web-ext": "^9.2.0", diff --git a/packages/core-server/src/core/server-tool-calls.ts b/packages/core-server/src/core/server-tool-calls.ts index d2acd9f..92d243f 100644 --- a/packages/core-server/src/core/server-tool-calls.ts +++ b/packages/core-server/src/core/server-tool-calls.ts @@ -373,7 +373,9 @@ export class ToolCallUseCases implements ServerToolCallsInputPort { getReadableElements = async ( tabKey: string, - ): Promise => { + ): Promise<{ + elements: ReadableElementRecord[]; + }> => { this.logger.info(`Getting readable elements from tab: ${tabKey}`); try { @@ -386,7 +388,9 @@ export class ToolCallUseCases implements ServerToolCallsInputPort { }); this.logger.info(`Retrieved ${elementRecords.length} readable elements`); - return elementRecords; + return { + elements: elementRecords, + }; } catch (error) { this.logger.error("Failed to get readable elements", error); throw error; diff --git a/packages/core-server/src/input-ports/server-tool-calls.ts b/packages/core-server/src/input-ports/server-tool-calls.ts index 7e3705b..b53653f 100644 --- a/packages/core-server/src/input-ports/server-tool-calls.ts +++ b/packages/core-server/src/input-ports/server-tool-calls.ts @@ -44,7 +44,9 @@ export type ServerToolCallsInputPort = { value: string, ) => Promise; getContext: () => Promise; - getReadableElements: (tabKey: string) => Promise; + getReadableElements: (tabKey: string) => Promise<{ + elements: ReadableElementRecord[]; + }>; getReadableText: (tabKey: string) => Promise; getSelection: (tabKey: string) => Promise; hitEnterOnCoordinates: ( diff --git a/packages/server-driving-mcp-server/src/services/element-tools.ts b/packages/server-driving-mcp-server/src/services/element-tools.ts index 0d60076..4b97b6a 100644 --- a/packages/server-driving-mcp-server/src/services/element-tools.ts +++ b/packages/server-driving-mcp-server/src/services/element-tools.ts @@ -118,7 +118,7 @@ export class ElementTools { ); } - const elements = overElements.value; + const { elements } = overElements.value; this.logger.verbose("Retrieved readable elements", { tabKey, elementCount: elements.length, diff --git a/yarn.lock b/yarn.lock index 1160dee..db2edf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -464,6 +464,15 @@ __metadata: languageName: node linkType: hard +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10c0/05c5368c13b662ee4c122c7bfbe5dc0b613416672a829f3e78bc49a357a197e0218d6e74e7c66cfcd04e15a179acab080bd3c69658c9fbefd0e1ccd950a07fc6 + languageName: node + linkType: hard + "@csstools/color-helpers@npm:^5.1.0": version: 5.1.0 resolution: "@csstools/color-helpers@npm:5.1.0" @@ -1002,20 +1011,30 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.1.0": +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e languageName: node linkType: hard -"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": +"@jridgewell/sourcemap-codec@npm:^1.4.10, @jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": version: 1.5.5 resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10c0/fa425b606d7c7ee5bfa6a31a7b050dd5814b4082f318e0e4190f991902181b4330f43f4805db1dd4f2433fd0ed9cc7a7b9c2683f1deeab1df1b0a98b1e24055b + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" @@ -1110,6 +1129,7 @@ __metadata: version: 0.0.0-use.local resolution: "@mcp-browser-kit/scripts@workspace:etc/scripts" dependencies: + "@playwright/test": "npm:^1.58.0" "@types/fs-extra": "npm:^11.0.4" fs-extra: "npm:^11.3.2" luxon: "npm:^3.7.2" @@ -1399,14 +1419,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:^1.57.0": - version: 1.57.0 - resolution: "@playwright/test@npm:1.57.0" +"@playwright/test@npm:^1.58.0": + version: 1.58.2 + resolution: "@playwright/test@npm:1.58.2" dependencies: - playwright: "npm:1.57.0" + playwright: "npm:1.58.2" bin: playwright: cli.js - checksum: 10c0/35ba4b28be72bf0a53e33dbb11c6cff848fb9a37f49e893ce63a90675b5291ec29a1ba82c8a3b043abaead129400f0589623e9ace2e6a1c8eaa409721ecc3774 + checksum: 10c0/2164c03ad97c3653ff02e8818a71f3b2bbc344ac07924c9d8e31cd57505d6d37596015a41f51396b3ed8de6840f59143eaa9c21bf65515963da20740119811da languageName: node linkType: hard @@ -2073,6 +2093,34 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.12 + resolution: "@tsconfig/node10@npm:1.0.12" + checksum: 10c0/7bbbd7408cfaced86387a9b1b71cebc91c6fd701a120369735734da8eab1a4773fc079abd9f40c9e0b049e12586c8ac0e13f0da596bfd455b9b4c3faa813ebc5 + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10c0/dddca2b553e2bee1308a056705103fc8304e42bb2d2cbd797b84403a223b25c78f2c683ec3e24a095e82cd435387c877239bffcb15a590ba817cd3f6b9a99fd9 + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10c0/67c1316d065fdaa32525bc9449ff82c197c4c19092b9663b23213c8cbbf8d88b6ed6a17898e0cbc2711950fbfaf40388938c1c748a2ee89f7234fc9e7fe2bf44 + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10c0/05f8f2734e266fb1839eb1d57290df1664fe2aa3b0fdd685a9035806daa635f7519bf6d5d9b33f6e69dd545b8c46bd6e2b5c79acb2b1f146e885f7f11a42a5bb + languageName: node + linkType: hard + "@tybys/wasm-util@npm:^0.10.1": version: 0.10.1 resolution: "@tybys/wasm-util@npm:0.10.1" @@ -2323,7 +2371,16 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.15.0": +"acorn-walk@npm:^8.1.1": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10c0/76537ac5fb2c37a64560feaf3342023dadc086c46da57da363e64c6148dc21b57d49ace26f949e225063acb6fb441eabffd89f7a3066de5ad37ab3e328927c62 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.15.0, acorn@npm:^8.4.1": version: 8.15.0 resolution: "acorn@npm:8.15.0" bin: @@ -2535,6 +2592,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10c0/070ff801a9d236a6caa647507bdcc7034530604844d64408149a26b9e87c2f97650055c0f049abd1efc024b334635c01f29e0b632b371ac3f26130f4cf65997a + languageName: node + linkType: hard + "argparse@npm:^2.0.1": version: 2.0.1 resolution: "argparse@npm:2.0.1" @@ -3352,6 +3416,13 @@ __metadata: languageName: node linkType: hard +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10c0/157cbc59b2430ae9a90034a5f3a1b398b6738bf510f713edc4d4e45e169bc514d3d99dd34d8d01ca7ae7830b5b8b537e46ae8f3c8f932371b0875c0151d7ec91 + languageName: node + linkType: hard + "cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.5, cross-spawn@npm:^7.0.6": version: 7.0.6 resolution: "cross-spawn@npm:7.0.6" @@ -3579,6 +3650,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10c0/81b91f9d39c4eaca068eb0c1eb0e4afbdc5bb2941d197f513dd596b820b956fef43485876226d65d497bebc15666aa2aa82c679e84f65d5f2bfbf14ee46e32c1 + languageName: node + linkType: hard + "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -5740,7 +5818,7 @@ __metadata: languageName: node linkType: hard -"make-error@npm:^1.3.2": +"make-error@npm:^1.1.1, make-error@npm:^1.3.2": version: 1.3.6 resolution: "make-error@npm:1.3.6" checksum: 10c0/171e458d86854c6b3fc46610cfacf0b45149ba043782558c6875d9f42f222124384ad0b468c92e996d815a8a2003817a710c0a160e49c1c394626f76fa45396f @@ -6724,27 +6802,27 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.57.0": - version: 1.57.0 - resolution: "playwright-core@npm:1.57.0" +"playwright-core@npm:1.58.2": + version: 1.58.2 + resolution: "playwright-core@npm:1.58.2" bin: playwright-core: cli.js - checksum: 10c0/798e35d83bf48419a8c73de20bb94d68be5dde68de23f95d80a0ebe401e3b83e29e3e84aea7894d67fa6c79d2d3d40cc5bcde3e166f657ce50987aaa2421b6a9 + checksum: 10c0/5aa15b2b764e6ffe738293a09081a6f7023847a0dbf4cd05fe10eed2e25450d321baf7482f938f2d2eb330291e197fa23e57b29a5b552b89927ceb791266225b languageName: node linkType: hard -"playwright@npm:1.57.0": - version: 1.57.0 - resolution: "playwright@npm:1.57.0" +"playwright@npm:1.58.2": + version: 1.58.2 + resolution: "playwright@npm:1.58.2" dependencies: fsevents: "npm:2.3.2" - playwright-core: "npm:1.57.0" + playwright-core: "npm:1.58.2" dependenciesMeta: fsevents: optional: true bin: playwright: cli.js - checksum: 10c0/ab03c99a67b835bdea9059f516ad3b6e42c21025f9adaa161a4ef6bc7ca716dcba476d287140bb240d06126eb23f889a8933b8f5f1f1a56b80659d92d1358899 + checksum: 10c0/d060d9b7cc124bd8b5dffebaab5e84f6b34654a553758fe7b19cc598dfbee93f6ecfbdc1832b40a6380ae04eade86ef3285ba03aa0b136799e83402246dc0727 languageName: node linkType: hard @@ -7249,7 +7327,7 @@ __metadata: "@biomejs/biome": "npm:2.3.7" "@medv/finder": "npm:^4.0.2" "@mozilla/readability": "npm:^0.6.0" - "@playwright/test": "npm:^1.57.0" + "@playwright/test": "npm:^1.58.0" "@readme/data-urls": "npm:^3.0.0" "@swc-node/register": "npm:^1.11.1" "@swc/core": "npm:^1.15.8" @@ -7287,8 +7365,10 @@ __metadata: sanitize-html: "npm:^2.17.0" serve: "npm:^14.2.5" superjson: "npm:^2.2.6" + ts-node: "npm:^10.9.2" tsconfig-moon: "npm:^1.4.1" tsup: "npm:^8.5.1" + tsx: "npm:^4.21.0" type-fest: "npm:^5.2.0" typescript: "npm:^5.9.3" web-ext: "npm:^9.2.0" @@ -8120,6 +8200,44 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 10c0/5f29938489f96982a25ba650b64218e83a3357d76f7bede80195c65ab44ad279c8357264639b7abdd5d7e75fc269a83daa0e9c62fd8637a3def67254ecc9ddc2 + languageName: node + linkType: hard + "ts-toolbelt@npm:^9.6.0": version: 9.6.0 resolution: "ts-toolbelt@npm:9.6.0" @@ -8197,7 +8315,7 @@ __metadata: languageName: node linkType: hard -"tsx@npm:^4.20.6": +"tsx@npm:^4.20.6, tsx@npm:^4.21.0": version: 4.21.0 resolution: "tsx@npm:4.21.0" dependencies: @@ -8450,6 +8568,13 @@ __metadata: languageName: node linkType: hard +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 10c0/bdc36fb8095d3b41df197f5fb6f11e3a26adf4059df3213e3baa93810d8f0cc76f9a74aaefc18b73e91fe7e19154ed6f134eda6fded2e0f1c8d2272ed2d2d391 + languageName: node + linkType: hard + "valibot@npm:^1.2.0": version: 1.2.0 resolution: "valibot@npm:1.2.0" @@ -8952,6 +9077,13 @@ __metadata: languageName: node linkType: hard +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 10c0/0732468dd7622ed8a274f640f191f3eaf1f39d5349a1b72836df484998d7d9807fbea094e2f5486d6b0cd2414aad5775972df0e68f8604db89a239f0f4bf7443 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0"