From 6e76d5e55064a060010b1a77d6c25b0de2204fc0 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Mon, 28 Apr 2025 16:14:16 -0700 Subject: [PATCH] chore: split context.ts into files (#284) --- config.d.ts | 5 + src/config.ts | 86 ++++++++++++++ src/context.ts | 238 +++++++------------------------------ src/index.ts | 68 +---------- src/pageSnapshot.ts | 101 ++++++++++++++++ src/server.ts | 28 +---- src/tab.ts | 92 ++++++++++++++ src/tools/install.ts | 4 +- src/tools/snapshot.ts | 6 +- tests/capabilities.spec.ts | 5 - 10 files changed, 344 insertions(+), 289 deletions(-) create mode 100644 src/config.ts create mode 100644 src/pageSnapshot.ts create mode 100644 src/tab.ts diff --git a/config.d.ts b/config.d.ts index 637aff4..efddda8 100644 --- a/config.d.ts +++ b/config.d.ts @@ -45,6 +45,11 @@ export type Config = { * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. */ cdpEndpoint?: string; + + /** + * Remote endpoint to connect to an existing Playwright server. + */ + remoteEndpoint?: string; }, server?: { diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..8c56f89 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,86 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import net from 'net'; +import os from 'os'; + +import type { Config } from '../config'; +import type { LaunchOptions, BrowserContextOptions } from 'playwright'; + +export type BrowserOptions = { + browserName: 'chromium' | 'firefox' | 'webkit'; + launchOptions: LaunchOptions; + contextOptions: BrowserContextOptions; +}; + +export async function toBrowserOptions(config: Config): Promise { + let browserName: 'chromium' | 'firefox' | 'webkit'; + let channel: string | undefined; + switch (config.browser?.type) { + case 'chrome': + case 'chrome-beta': + case 'chrome-canary': + case 'chrome-dev': + case 'chromium': + case 'msedge': + case 'msedge-beta': + case 'msedge-canary': + case 'msedge-dev': + browserName = 'chromium'; + channel = config.browser.type; + break; + case 'firefox': + browserName = 'firefox'; + break; + case 'webkit': + browserName = 'webkit'; + break; + default: + browserName = 'chromium'; + channel = 'chrome'; + } + + const launchOptions: LaunchOptions = { + headless: !!(config.browser?.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)), + channel, + executablePath: config.browser?.executablePath, + ...{ assistantMode: true }, + }; + + const contextOptions: BrowserContextOptions = { + viewport: null, + }; + + if (browserName === 'chromium') + (launchOptions as any).webSocketPort = await findFreePort(); + + return { + browserName, + launchOptions, + contextOptions, + }; +} + +async function findFreePort() { + return new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, () => { + const { port } = server.address() as net.AddressInfo; + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); +} diff --git a/src/context.ts b/src/context.ts index fe5d3bf..03d806b 100644 --- a/src/context.ts +++ b/src/context.ts @@ -14,26 +14,21 @@ * limitations under the License. */ -import net from 'net'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; import * as playwright from 'playwright'; -import yaml from 'yaml'; import { waitForCompletion } from './tools/utils'; import { ManualPromise } from './manualPromise'; +import { toBrowserOptions } from './config'; +import { Tab } from './tab'; import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types'; import type { ModalState, Tool, ToolActionResult } from './tools/tool'; - -export type ContextOptions = { - browserName?: 'chromium' | 'firefox' | 'webkit'; - userDataDir: string; - launchOptions?: playwright.LaunchOptions & playwright.BrowserContextOptions; - cdpEndpoint?: string; - remoteEndpoint?: string; -}; - -type PageOrFrameLocator = playwright.Page | playwright.FrameLocator; +import type { Config } from '../config'; +import type { BrowserOptions } from './config'; type PendingAction = { dialogShown: ManualPromise; @@ -41,7 +36,7 @@ type PendingAction = { export class Context { readonly tools: Tool[]; - readonly options: ContextOptions; + readonly config: Config; private _browser: playwright.Browser | undefined; private _browserContext: playwright.BrowserContext | undefined; private _createBrowserContextPromise: Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> | undefined; @@ -50,9 +45,9 @@ export class Context { private _modalStates: (ModalState & { tab: Tab })[] = []; private _pendingAction: PendingAction | undefined; - constructor(tools: Tool[], options: ContextOptions) { + constructor(tools: Tool[], config: Config) { this.tools = tools; - this.options = options; + this.config = config; } modalStates(): ModalState[] { @@ -290,205 +285,58 @@ ${code.join('\n')} } private async _innerCreateBrowserContext(): Promise<{ browser?: playwright.Browser, browserContext: playwright.BrowserContext }> { - if (this.options.browserName === 'chromium') - (this.options.launchOptions as any).webSocketPort = await findFreePort(); + const browserOptions = await toBrowserOptions(this.config); - if (this.options.remoteEndpoint) { - const url = new URL(this.options.remoteEndpoint); - if (this.options.browserName) - url.searchParams.set('browser', this.options.browserName); - if (this.options.launchOptions) - url.searchParams.set('launch-options', JSON.stringify(this.options.launchOptions)); - const browser = await playwright[this.options.browserName ?? 'chromium'].connect(String(url)); + if (this.config.browser?.remoteEndpoint) { + const url = new URL(this.config.browser?.remoteEndpoint); + if (browserOptions.browserName) + url.searchParams.set('browser', browserOptions.browserName); + if (browserOptions.launchOptions) + url.searchParams.set('launch-options', JSON.stringify(browserOptions.launchOptions)); + const browser = await playwright[browserOptions.browserName ?? 'chromium'].connect(String(url)); const browserContext = await browser.newContext(); return { browser, browserContext }; } - if (this.options.cdpEndpoint) { - const browser = await playwright.chromium.connectOverCDP(this.options.cdpEndpoint); + if (this.config.browser?.cdpEndpoint) { + const browser = await playwright.chromium.connectOverCDP(this.config.browser?.cdpEndpoint); const browserContext = browser.contexts()[0]; return { browser, browserContext }; } - const browserContext = await this._launchPersistentContext(); + const browserContext = await launchPersistentContext(this.config.browser?.userDataDir, browserOptions); return { browserContext }; } - - private async _launchPersistentContext(): Promise { - try { - const browserType = this.options.browserName ? playwright[this.options.browserName] : playwright.chromium; - return await browserType.launchPersistentContext(this.options.userDataDir, this.options.launchOptions); - } catch (error: any) { - if (error.message.includes('Executable doesn\'t exist')) - throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); - throw error; - } - } } -export class Tab { - readonly context: Context; - readonly page: playwright.Page; - private _console: playwright.ConsoleMessage[] = []; - private _requests: Map = new Map(); - private _snapshot: PageSnapshot | undefined; - private _onPageClose: (tab: Tab) => void; - - constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { - this.context = context; - this.page = page; - this._onPageClose = onPageClose; - page.on('console', event => this._console.push(event)); - page.on('request', request => this._requests.set(request, null)); - page.on('response', response => this._requests.set(response.request(), response)); - page.on('framenavigated', frame => { - if (!frame.parentFrame()) - this._clearCollectedArtifacts(); - }); - page.on('close', () => this._onClose()); - page.on('filechooser', chooser => { - this.context.setModalState({ - type: 'fileChooser', - description: 'File chooser', - fileChooser: chooser, - }, this); - }); - page.on('dialog', dialog => this.context.dialogShown(this, dialog)); - page.setDefaultNavigationTimeout(60000); - page.setDefaultTimeout(5000); - } - - private _clearCollectedArtifacts() { - this._console.length = 0; - this._requests.clear(); - } - - private _onClose() { - this._clearCollectedArtifacts(); - this._onPageClose(this); - } - - async navigate(url: string) { - await this.page.goto(url, { waitUntil: 'domcontentloaded' }); - // Cap load event to 5 seconds, the page is operational at this point. - await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => {}); - } - - hasSnapshot(): boolean { - return !!this._snapshot; - } - - snapshotOrDie(): PageSnapshot { - if (!this._snapshot) - throw new Error('No snapshot available'); - return this._snapshot; - } - - console(): playwright.ConsoleMessage[] { - return this._console; - } - - requests(): Map { - return this._requests; - } - - async captureSnapshot() { - this._snapshot = await PageSnapshot.create(this.page); - } +async function createUserDataDir(browserName: 'chromium' | 'firefox' | 'webkit') { + let cacheDirectory: string; + if (process.platform === 'linux') + cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); + else if (process.platform === 'darwin') + cacheDirectory = path.join(os.homedir(), 'Library', 'Caches'); + else if (process.platform === 'win32') + cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); + else + throw new Error('Unsupported platform: ' + process.platform); + const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`); + await fs.promises.mkdir(result, { recursive: true }); + return result; } -class PageSnapshot { - private _frameLocators: PageOrFrameLocator[] = []; - private _text!: string; +async function launchPersistentContext(userDataDir: string | undefined, browserOptions: BrowserOptions): Promise { + userDataDir = userDataDir ?? await createUserDataDir(browserOptions.browserName); - constructor() { - } - - static async create(page: playwright.Page): Promise { - const snapshot = new PageSnapshot(); - await snapshot._build(page); - return snapshot; - } - - text(): string { - return this._text; - } - - private async _build(page: playwright.Page) { - const yamlDocument = await this._snapshotFrame(page); - this._text = [ - `- Page Snapshot`, - '```yaml', - yamlDocument.toString({ indentSeq: false }).trim(), - '```', - ].join('\n'); - } - - private async _snapshotFrame(frame: playwright.Page | playwright.FrameLocator) { - const frameIndex = this._frameLocators.push(frame) - 1; - const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true }); - const snapshot = yaml.parseDocument(snapshotString); - - const visit = async (node: any): Promise => { - if (yaml.isPair(node)) { - await Promise.all([ - visit(node.key).then(k => node.key = k), - visit(node.value).then(v => node.value = v) - ]); - } else if (yaml.isSeq(node) || yaml.isMap(node)) { - node.items = await Promise.all(node.items.map(visit)); - } else if (yaml.isScalar(node)) { - if (typeof node.value === 'string') { - const value = node.value; - if (frameIndex > 0) - node.value = value.replace('[ref=', `[ref=f${frameIndex}`); - if (value.startsWith('iframe ')) { - const ref = value.match(/\[ref=(.*)\]/)?.[1]; - if (ref) { - try { - const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`)); - return snapshot.createPair(node.value, childSnapshot); - } catch (error) { - return snapshot.createPair(node.value, ''); - } - } - } - } - } - - return node; - }; - await visit(snapshot.contents); - return snapshot; - } - - refLocator(ref: string): playwright.Locator { - let frame = this._frameLocators[0]; - const match = ref.match(/^f(\d+)(.*)/); - if (match) { - const frameIndex = parseInt(match[1], 10); - frame = this._frameLocators[frameIndex]; - ref = match[2]; - } - - if (!frame) - throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`); - - return frame.locator(`aria-ref=${ref}`); + try { + const browserType = browserOptions.browserName ? playwright[browserOptions.browserName] : playwright.chromium; + return await browserType.launchPersistentContext(userDataDir, browserOptions.launchOptions); + } catch (error: any) { + if (error.message.includes('Executable doesn\'t exist')) + throw new Error(`Browser specified in your config is not installed. Either install it (likely) or change the config.`); + throw error; } } export async function generateLocator(locator: playwright.Locator): Promise { return (locator as any)._generateLocatorString(); } - -async function findFreePort() { - return new Promise((resolve, reject) => { - const server = net.createServer(); - server.listen(0, () => { - const { port } = server.address() as net.AddressInfo; - server.close(() => resolve(port)); - }); - server.on('error', reject); - }); -} diff --git a/src/index.ts b/src/index.ts index c6559ed..3dfe1ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,10 +14,6 @@ * limitations under the License. */ -import path from 'path'; -import os from 'os'; -import fs from 'fs'; - import { createServerWithTools } from './server'; import common from './tools/common'; import console from './tools/console'; @@ -35,7 +31,6 @@ import screen from './tools/screen'; import type { Tool } from './tools/tool'; import type { Config } from '../config'; import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import type { LaunchOptions, BrowserContextOptions } from 'playwright'; const snapshotTools: Tool[] = [ ...common(true), @@ -67,67 +62,12 @@ const screenshotTools: Tool[] = [ const packageJSON = require('../package.json'); -export async function createServer(config?: Config): Promise { - let browserName: 'chromium' | 'firefox' | 'webkit'; - let channel: string | undefined; - switch (config?.browser?.type) { - case 'chrome': - case 'chrome-beta': - case 'chrome-canary': - case 'chrome-dev': - case 'chromium': - case 'msedge': - case 'msedge-beta': - case 'msedge-canary': - case 'msedge-dev': - browserName = 'chromium'; - channel = config.browser.type; - break; - case 'firefox': - browserName = 'firefox'; - break; - case 'webkit': - browserName = 'webkit'; - break; - default: - browserName = 'chromium'; - channel = 'chrome'; - } - const userDataDir = config?.browser?.userDataDir ?? await createUserDataDir(browserName); - - const launchOptions: LaunchOptions & BrowserContextOptions = { - headless: !!(config?.browser?.headless ?? (os.platform() === 'linux' && !process.env.DISPLAY)), - channel, - executablePath: config?.browser?.executablePath, - viewport: null, - ...{ assistantMode: true }, - }; - - const allTools = config?.vision ? screenshotTools : snapshotTools; - const tools = allTools.filter(tool => !config?.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability)); +export async function createServer(config: Config = {}): Promise { + const allTools = config.vision ? screenshotTools : snapshotTools; + const tools = allTools.filter(tool => !config.capabilities || tool.capability === 'core' || config.capabilities.includes(tool.capability)); return createServerWithTools({ name: 'Playwright', version: packageJSON.version, tools, - resources: [], - browserName, - userDataDir, - launchOptions, - cdpEndpoint: config?.browser?.cdpEndpoint, - }); -} - -async function createUserDataDir(browserName: 'chromium' | 'firefox' | 'webkit') { - let cacheDirectory: string; - if (process.platform === 'linux') - cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'); - else if (process.platform === 'darwin') - cacheDirectory = path.join(os.homedir(), 'Library', 'Caches'); - else if (process.platform === 'win32') - cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'); - else - throw new Error('Unsupported platform: ' + process.platform); - const result = path.join(cacheDirectory, 'ms-playwright', `mcp-${browserName}-profile`); - await fs.promises.mkdir(result, { recursive: true }); - return result; + }, config); } diff --git a/src/pageSnapshot.ts b/src/pageSnapshot.ts new file mode 100644 index 0000000..f7d42cd --- /dev/null +++ b/src/pageSnapshot.ts @@ -0,0 +1,101 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as playwright from 'playwright'; +import yaml from 'yaml'; + +type PageOrFrameLocator = playwright.Page | playwright.FrameLocator; + +export class PageSnapshot { + private _frameLocators: PageOrFrameLocator[] = []; + private _text!: string; + + constructor() { + } + + static async create(page: playwright.Page): Promise { + const snapshot = new PageSnapshot(); + await snapshot._build(page); + return snapshot; + } + + text(): string { + return this._text; + } + + private async _build(page: playwright.Page) { + const yamlDocument = await this._snapshotFrame(page); + this._text = [ + `- Page Snapshot`, + '```yaml', + yamlDocument.toString({ indentSeq: false }).trim(), + '```', + ].join('\n'); + } + + private async _snapshotFrame(frame: playwright.Page | playwright.FrameLocator) { + const frameIndex = this._frameLocators.push(frame) - 1; + const snapshotString = await frame.locator('body').ariaSnapshot({ ref: true, emitGeneric: true }); + const snapshot = yaml.parseDocument(snapshotString); + + const visit = async (node: any): Promise => { + if (yaml.isPair(node)) { + await Promise.all([ + visit(node.key).then(k => node.key = k), + visit(node.value).then(v => node.value = v) + ]); + } else if (yaml.isSeq(node) || yaml.isMap(node)) { + node.items = await Promise.all(node.items.map(visit)); + } else if (yaml.isScalar(node)) { + if (typeof node.value === 'string') { + const value = node.value; + if (frameIndex > 0) + node.value = value.replace('[ref=', `[ref=f${frameIndex}`); + if (value.startsWith('iframe ')) { + const ref = value.match(/\[ref=(.*)\]/)?.[1]; + if (ref) { + try { + const childSnapshot = await this._snapshotFrame(frame.frameLocator(`aria-ref=${ref}`)); + return snapshot.createPair(node.value, childSnapshot); + } catch (error) { + return snapshot.createPair(node.value, ''); + } + } + } + } + } + + return node; + }; + await visit(snapshot.contents); + return snapshot; + } + + refLocator(ref: string): playwright.Locator { + let frame = this._frameLocators[0]; + const match = ref.match(/^f(\d+)(.*)/); + if (match) { + const frameIndex = parseInt(match[1], 10); + frame = this._frameLocators[frameIndex]; + ref = match[2]; + } + + if (!frame) + throw new Error(`Frame does not exist. Provide ref from the most current snapshot.`); + + return frame.locator(`aria-ref=${ref}`); + } +} diff --git a/src/server.ts b/src/server.ts index 2716528..c37fa67 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,29 +15,26 @@ */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { zodToJsonSchema } from 'zod-to-json-schema'; import { Context } from './context'; import type { Tool } from './tools/tool'; -import type { Resource } from './resources/resource'; -import type { ContextOptions } from './context'; +import type { Config } from '../config'; -type Options = ContextOptions & { +type MCPServerOptions = { name: string; version: string; tools: Tool[]; - resources: Resource[], }; -export function createServerWithTools(options: Options): Server { - const { name, version, tools, resources } = options; - const context = new Context(tools, options); +export function createServerWithTools(serverOptions: MCPServerOptions, config: Config): Server { + const { name, version, tools } = serverOptions; + const context = new Context(tools, config); const server = new Server({ name, version }, { capabilities: { tools: {}, - resources: {}, } }); @@ -51,10 +48,6 @@ export function createServerWithTools(options: Options): Server { }; }); - server.setRequestHandler(ListResourcesRequestSchema, async () => { - return { resources: resources.map(resource => resource.schema) }; - }); - server.setRequestHandler(CallToolRequestSchema, async request => { const tool = tools.find(tool => tool.schema.name === request.params.name); if (!tool) { @@ -87,15 +80,6 @@ export function createServerWithTools(options: Options): Server { } }); - server.setRequestHandler(ReadResourceRequestSchema, async request => { - const resource = resources.find(resource => resource.schema.uri === request.params.uri); - if (!resource) - return { contents: [] }; - - const contents = await resource.read(context, request.params.uri); - return { contents }; - }); - const oldClose = server.close.bind(server); server.close = async () => { diff --git a/src/tab.ts b/src/tab.ts new file mode 100644 index 0000000..1ea9cfe --- /dev/null +++ b/src/tab.ts @@ -0,0 +1,92 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as playwright from 'playwright'; + +import { PageSnapshot } from './pageSnapshot'; + +import type { Context } from './context'; + +export class Tab { + readonly context: Context; + readonly page: playwright.Page; + private _console: playwright.ConsoleMessage[] = []; + private _requests: Map = new Map(); + private _snapshot: PageSnapshot | undefined; + private _onPageClose: (tab: Tab) => void; + + constructor(context: Context, page: playwright.Page, onPageClose: (tab: Tab) => void) { + this.context = context; + this.page = page; + this._onPageClose = onPageClose; + page.on('console', event => this._console.push(event)); + page.on('request', request => this._requests.set(request, null)); + page.on('response', response => this._requests.set(response.request(), response)); + page.on('framenavigated', frame => { + if (!frame.parentFrame()) + this._clearCollectedArtifacts(); + }); + page.on('close', () => this._onClose()); + page.on('filechooser', chooser => { + this.context.setModalState({ + type: 'fileChooser', + description: 'File chooser', + fileChooser: chooser, + }, this); + }); + page.on('dialog', dialog => this.context.dialogShown(this, dialog)); + page.setDefaultNavigationTimeout(60000); + page.setDefaultTimeout(5000); + } + + private _clearCollectedArtifacts() { + this._console.length = 0; + this._requests.clear(); + } + + private _onClose() { + this._clearCollectedArtifacts(); + this._onPageClose(this); + } + + async navigate(url: string) { + await this.page.goto(url, { waitUntil: 'domcontentloaded' }); + // Cap load event to 5 seconds, the page is operational at this point. + await this.page.waitForLoadState('load', { timeout: 5000 }).catch(() => {}); + } + + hasSnapshot(): boolean { + return !!this._snapshot; + } + + snapshotOrDie(): PageSnapshot { + if (!this._snapshot) + throw new Error('No snapshot available'); + return this._snapshot; + } + + console(): playwright.ConsoleMessage[] { + return this._console; + } + + requests(): Map { + return this._requests; + } + + async captureSnapshot() { + this._snapshot = await PageSnapshot.create(this.page); + } +} diff --git a/src/tools/install.ts b/src/tools/install.ts index bf69697..30edfaa 100644 --- a/src/tools/install.ts +++ b/src/tools/install.ts @@ -19,6 +19,7 @@ import path from 'path'; import { z } from 'zod'; import { defineTool } from './tool'; +import { toBrowserOptions } from '../config'; const install = defineTool({ capability: 'install', @@ -29,7 +30,8 @@ const install = defineTool({ }, handle: async context => { - const channel = context.options.launchOptions?.channel ?? context.options.browserName ?? 'chrome'; + const browserOptions = await toBrowserOptions(context.config); + const channel = browserOptions.launchOptions?.channel ?? browserOptions.browserName ?? 'chrome'; const cli = path.join(require.resolve('playwright/package.json'), '..', 'cli.js'); const child = fork(cli, ['install', channel], { stdio: 'pipe', diff --git a/src/tools/snapshot.ts b/src/tools/snapshot.ts index 43b0e9c..44ce4e0 100644 --- a/src/tools/snapshot.ts +++ b/src/tools/snapshot.ts @@ -20,11 +20,10 @@ import os from 'os'; import { z } from 'zod'; import { sanitizeForFilePath } from './utils'; -import { generateLocator } from '../context'; +import { defineTool } from './tool'; import * as javascript from '../javascript'; import type * as playwright from 'playwright'; -import { defineTool } from './tool'; const snapshot = defineTool({ capability: 'core', @@ -268,6 +267,9 @@ const screenshot = defineTool({ } }); +export async function generateLocator(locator: playwright.Locator): Promise { + return (locator as any)._generateLocatorString(); +} export default [ snapshot, diff --git a/tests/capabilities.spec.ts b/tests/capabilities.spec.ts index f4ad688..389f107 100644 --- a/tests/capabilities.spec.ts +++ b/tests/capabilities.spec.ts @@ -74,11 +74,6 @@ test('test vision tool list', async ({ visionClient }) => { ])); }); -test('test resources list', async ({ client }) => { - const { resources } = await client.listResources(); - expect(resources).toEqual([]); -}); - test('test capabilities', async ({ startClient }) => { const client = await startClient({ args: ['--caps="core"'],