diff --git a/.github/workflows/branch-cleaner.yml b/.github/workflows/branch-cleaner.yml index 6aedfb7f..bc40e73f 100644 --- a/.github/workflows/branch-cleaner.yml +++ b/.github/workflows/branch-cleaner.yml @@ -12,4 +12,6 @@ jobs: steps: - uses: mmorenoregalado/action-branches-cleaner@v2.0.3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} + base_branches: main + days_old_threshold: 30 diff --git a/apps/agent/biome.json b/apps/agent/biome.json index 1c02279b..a2fd3481 100644 --- a/apps/agent/biome.json +++ b/apps/agent/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.11/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", "root": false, "extends": "//", "vcs": { diff --git a/apps/agent/entrypoints/background/scheduledJobRuns.ts b/apps/agent/entrypoints/background/scheduledJobRuns.ts index 59f9532f..98102fc5 100644 --- a/apps/agent/entrypoints/background/scheduledJobRuns.ts +++ b/apps/agent/entrypoints/background/scheduledJobRuns.ts @@ -176,6 +176,7 @@ export const scheduledJobRuns = async () => { let runningMissedJobs = false + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO(dani) refactor to reduce complexity const runMissedJobs = async () => { if (runningMissedJobs) return runningMissedJobs = true diff --git a/apps/agent/lib/graphql/getQueryKeyFromDocument.ts b/apps/agent/lib/graphql/getQueryKeyFromDocument.ts index 2e6fcc42..84a52f76 100644 --- a/apps/agent/lib/graphql/getQueryKeyFromDocument.ts +++ b/apps/agent/lib/graphql/getQueryKeyFromDocument.ts @@ -15,6 +15,7 @@ const getOperationName = ( export const getQueryKeyFromDocument = < TResult, + // biome-ignore lint/suspicious/noExplicitAny: TODO(dani) type GraphQL variables properly TVariables extends Record | undefined = undefined, >( doc: TypedDocumentString, diff --git a/apps/agent/lib/graphql/useGraphqlInfiniteQuery.ts b/apps/agent/lib/graphql/useGraphqlInfiniteQuery.ts index 5bebeaac..832457bf 100644 --- a/apps/agent/lib/graphql/useGraphqlInfiniteQuery.ts +++ b/apps/agent/lib/graphql/useGraphqlInfiniteQuery.ts @@ -13,6 +13,7 @@ import { getQueryKeyFromDocument } from './getQueryKeyFromDocument' */ export const useGraphqlInfiniteQuery = < TQueryFnData, + // biome-ignore lint/suspicious/noExplicitAny: TODO(dani) type GraphQL variables properly TVariables extends Record | undefined = undefined, TPageParam extends string | undefined | number = undefined, >( diff --git a/apps/agent/lib/graphql/useGraphqlQuery.ts b/apps/agent/lib/graphql/useGraphqlQuery.ts index 911c9fb4..30ebc68e 100644 --- a/apps/agent/lib/graphql/useGraphqlQuery.ts +++ b/apps/agent/lib/graphql/useGraphqlQuery.ts @@ -8,6 +8,7 @@ import { getQueryKeyFromDocument } from './getQueryKeyFromDocument' */ export const useGraphqlQuery = < TResult, + // biome-ignore lint/suspicious/noExplicitAny: TODO(dani) type GraphQL variables properly TVariables extends Record | undefined = undefined, >( query: TypedDocumentString, diff --git a/apps/agent/lib/schedules/syncSchedulesToBackend.ts b/apps/agent/lib/schedules/syncSchedulesToBackend.ts index 639b3590..68aade35 100644 --- a/apps/agent/lib/schedules/syncSchedulesToBackend.ts +++ b/apps/agent/lib/schedules/syncSchedulesToBackend.ts @@ -75,6 +75,7 @@ function getRemoteUpdatedAt(remote: RemoteScheduledJob): Date { return new Date(normalizeTimestamp(remote.updatedAt)) } +// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: TODO(dani) refactor to reduce complexity export async function syncSchedulesToBackend( localJobs: ScheduledJob[], userId: string, diff --git a/apps/agent/web-ext.config.ts b/apps/agent/web-ext.config.ts index 6dce0737..fa5494ba 100644 --- a/apps/agent/web-ext.config.ts +++ b/apps/agent/web-ext.config.ts @@ -19,6 +19,7 @@ const chromiumArgs = [ if (env.BROWSEROS_CDP_PORT) { // TODO: replace with --browseros-cdp-port once we fix the browseros bug chromiumArgs.push(`--remote-debugging-port=${env.BROWSEROS_CDP_PORT}`) + // chromiumArgs.push(`--browseros-cdp-port =${env.BROWSEROS_CDP_PORT}`) } if (env.BROWSEROS_SERVER_PORT) { chromiumArgs.push(`--browseros-mcp-port=${env.BROWSEROS_SERVER_PORT}`) diff --git a/apps/server/package.json b/apps/server/package.json index 1215dda5..cf2fc4c3 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -10,10 +10,7 @@ "scripts": { "start": "bun --watch --env-file=.env.development src/index.ts", "build": "bun ../../scripts/build/server.ts --mode=prod --target=all", - "test": "bun run test:cleanup && bun --env-file=.env.development test tests/tools tests/common", - "test:all": "bun run test:cleanup && bun --env-file=.env.development test", - "test:cdp": "bun run test:cleanup && bun --env-file=.env.development test tests/tools/cdp-based", - "test:controller": "bun run test:cleanup && bun --env-file=.env.development test tests/tools/controller-based", + "test:tools": "bun run test:cleanup && bun --env-file=.env.development test tests/tools", "test:integration": "bun run test:cleanup && bun --env-file=.env.development test tests/server.integration.test.ts", "test:sdk": "bun run test:cleanup && bun --env-file=.env.development test tests/sdk", "test:cleanup": "./tests/__helpers__/cleanup.sh", diff --git a/apps/server/src/browser/browser.ts b/apps/server/src/browser/browser.ts index 8cf60819..981a929b 100644 --- a/apps/server/src/browser/browser.ts +++ b/apps/server/src/browser/browser.ts @@ -242,10 +242,18 @@ export class Browser { ...(opts?.windowId !== undefined && { windowId: opts.windowId }), }) - const infoResult = await this.cdp.Browser.getTabInfo({ - tabId: (createResult.tab as TabInfo).tabId, - }) - const tabInfo = infoResult.tab as TabInfo + const tabId = (createResult.tab as TabInfo).tabId + let tabInfo: TabInfo | undefined + for (let i = 0; i < 10; i++) { + try { + const infoResult = await this.cdp.Browser.getTabInfo({ tabId }) + tabInfo = infoResult.tab as TabInfo + break + } catch { + await new Promise((r) => setTimeout(r, 100)) + } + } + if (!tabInfo) throw new Error(`Tab ${tabId} not found after creation`) const pageId = this.nextPageId++ this.pages.set(pageId, { diff --git a/apps/server/src/browser/snapshot.ts b/apps/server/src/browser/snapshot.ts index eb407c80..ed4583a7 100644 --- a/apps/server/src/browser/snapshot.ts +++ b/apps/server/src/browser/snapshot.ts @@ -66,6 +66,7 @@ export function buildInteractiveTree(nodes: AXNode[]): string[] { const lines: string[] = [] + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: tree-walking with multiple node types is inherently complex function walk(nodeId: string): void { const node = nodeMap.get(nodeId) if (!node) return @@ -113,6 +114,7 @@ export function buildEnhancedTree(nodes: AXNode[]): string[] { const lines: string[] = [] + // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: tree-walking with multiple node types is inherently complex function walk(nodeId: string, depth: number): void { const node = nodeMap.get(nodeId) if (!node) return diff --git a/apps/server/tests/__helpers__/browser.ts b/apps/server/tests/__helpers__/browser.ts index feb37e37..9205e603 100644 --- a/apps/server/tests/__helpers__/browser.ts +++ b/apps/server/tests/__helpers__/browser.ts @@ -80,6 +80,7 @@ export async function spawnBrowser( '--enable-logging=stderr', ...(headless ? ['--headless=new'] : []), `--user-data-dir=${tempUserDataDir}`, + // TODO: replace with --browseros-cdp-port once we fix the browseros bug `--remote-debugging-port=${config.cdpPort}`, `--browseros-mcp-port=${config.serverPort}`, `--browseros-extension-port=${config.extensionPort}`, diff --git a/apps/server/tests/__helpers__/index.ts b/apps/server/tests/__helpers__/index.ts index 1cc04f04..34f0c192 100644 --- a/apps/server/tests/__helpers__/index.ts +++ b/apps/server/tests/__helpers__/index.ts @@ -1,26 +1,13 @@ -/** - * @license - * Copyright 2025 BrowserOS - * - * Test helpers public API. - */ - -// Setup & lifecycle export { cleanupBrowserOS, ensureBrowserOS, type TestEnvironmentConfig, } from './setup' -// Types export type { McpContentItem, TypedCallToolResult } from './utils' -// Test wrappers -// Port management -// Mocks export { asToolResult, - getMockRequest, - getMockResponse, html, killProcessOnPort, withMcpServer, } from './utils' +export { type WithBrowserContext, withBrowser } from './with-browser' diff --git a/apps/server/tests/__helpers__/utils.ts b/apps/server/tests/__helpers__/utils.ts index 8473e5af..7c684896 100644 --- a/apps/server/tests/__helpers__/utils.ts +++ b/apps/server/tests/__helpers__/utils.ts @@ -1,22 +1,8 @@ -/** - * @license - * Copyright 2025 BrowserOS - * - * Test utilities: wrappers, mocks, and port management. - */ import { execSync } from 'node:child_process' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js' import { Mutex } from 'async-mutex' -import type { Browser } from 'puppeteer' -import puppeteer from 'puppeteer' -import type { HTTPRequest, HTTPResponse } from 'puppeteer-core' -import { CdpClient } from '../../src/browser/cdp/cdp-client' -import { PageRegistry } from '../../src/browser/page-registry' -import { SessionState } from '../../src/browser/session-state' -import { logger as cdpLogger } from '../../src/tools/cdp/context/logger' -import { CdpResponse } from '../../src/tools/cdp/response/cdp-response' import { ensureBrowserOS } from './setup' @@ -63,53 +49,7 @@ export async function killProcessOnPort(port: number): Promise { // ============================================================================= const envMutex = new Mutex() -let cachedBrowser: Browser | undefined -export async function withCdpBrowser( - cb: (response: CdpResponse, context: CdpClient) => Promise, - _options: { debug?: boolean } = {}, -): Promise { - return await envMutex.runExclusive(async () => { - const config = await ensureBrowserOS({ skipExtension: true }) - - if (!cachedBrowser || !cachedBrowser.connected) { - cachedBrowser = await puppeteer.connect({ - browserURL: `http://127.0.0.1:${config.cdpPort}`, - }) - } - - const response = new CdpResponse() - const registry = new PageRegistry() - const context = await CdpClient.from( - cachedBrowser, - cdpLogger, - { - experimentalDevToolsDebugging: false, - }, - registry, - ) - - try { - const page = await context.newPage(true) - const state = new SessionState() - await context.withPage(page, state, () => cb(response, context)) - } finally { - context.dispose() - } - }) -} - -/** - * Test helper that provides an MCP client connected to the BrowserOS server. - * - * Lifecycle: - * - First test: Starts full environment (~15-20s) - * - Subsequent tests: Reuses existing environment (fast) - * - After suite exits: Environment stays running (ready for next run) - * - * Cleanup: - * - Run `bun run test:cleanup` when you need to kill processes - */ export async function withMcpServer( cb: (client: Client) => Promise, ): Promise { @@ -134,66 +74,9 @@ export async function withMcpServer( } // ============================================================================= -// Mock Helpers +// HTML Helper // ============================================================================= -export function getMockRequest( - options: { - method?: string - response?: HTTPResponse - failure?: HTTPRequest['failure'] - resourceType?: string - hasPostData?: boolean - postData?: string - fetchPostData?: Promise - } = {}, -): HTTPRequest { - return { - url() { - return 'http://example.com' - }, - method() { - return options.method ?? 'GET' - }, - fetchPostData() { - return options.fetchPostData ?? Promise.reject() - }, - hasPostData() { - return options.hasPostData ?? false - }, - postData() { - return options.postData - }, - response() { - return options.response ?? null - }, - failure() { - return options.failure?.() ?? null - }, - resourceType() { - return options.resourceType ?? 'document' - }, - headers(): Record { - return { - 'content-size': '10', - } - }, - redirectChain(): HTTPRequest[] { - return [] - }, - } as HTTPRequest -} - -export function getMockResponse( - options: { status?: number } = {}, -): HTTPResponse { - return { - status() { - return options.status ?? 200 - }, - } as HTTPResponse -} - export function html( strings: TemplateStringsArray, ...values: unknown[] diff --git a/apps/server/tests/__helpers__/with-browser.ts b/apps/server/tests/__helpers__/with-browser.ts new file mode 100644 index 00000000..bd173d4a --- /dev/null +++ b/apps/server/tests/__helpers__/with-browser.ts @@ -0,0 +1,82 @@ +import { TEST_PORTS } from '@browseros/shared/constants/ports' +import { Mutex } from 'async-mutex' +import { CdpBackend } from '../../src/browser/backends/cdp' +import type { ControllerBackend } from '../../src/browser/backends/types' +import { Browser } from '../../src/browser/browser' +import type { ToolDefinition } from '../../src/tools/framework' +import { executeTool } from '../../src/tools/framework' +import type { ToolResult } from '../../src/tools/response' +import { type BrowserConfig, spawnBrowser } from './browser' +import { killProcessOnPort } from './utils' + +const cdpPort = Number.parseInt( + process.env.BROWSEROS_CDP_PORT || String(TEST_PORTS.cdp), + 10, +) +const serverPort = Number.parseInt( + process.env.BROWSEROS_SERVER_PORT || String(TEST_PORTS.server), + 10, +) +const extensionPort = Number.parseInt( + process.env.BROWSEROS_EXTENSION_PORT || String(TEST_PORTS.extension), + 10, +) +const binaryPath = + process.env.BROWSEROS_BINARY ?? + '/Applications/BrowserOS.app/Contents/MacOS/BrowserOS' + +const mutex = new Mutex() +let cachedCdp: CdpBackend | null = null +let cachedBrowser: Browser | null = null + +const stubController: ControllerBackend = { + start: async () => {}, + stop: async () => {}, + isConnected: () => false, + send: async () => { + throw new Error('Controller not available in test mode') + }, +} + +async function getOrCreateBrowser(): Promise { + if (cachedBrowser && cachedCdp?.isConnected()) return cachedBrowser + + await killProcessOnPort(cdpPort) + + const config: BrowserConfig = { + cdpPort, + serverPort, + extensionPort, + binaryPath, + } + await spawnBrowser(config) + + cachedCdp = new CdpBackend({ port: cdpPort }) + await cachedCdp.connect() + + cachedBrowser = new Browser(cachedCdp, stubController) + return cachedBrowser +} + +export interface WithBrowserContext { + browser: Browser + execute: (tool: ToolDefinition, args: unknown) => Promise +} + +export async function withBrowser( + cb: (ctx: WithBrowserContext) => Promise, +): Promise { + return await mutex.runExclusive(async () => { + const browser = await getOrCreateBrowser() + + const execute = async ( + tool: ToolDefinition, + args: unknown, + ): Promise => { + const signal = AbortSignal.timeout(30_000) + return executeTool(tool, args, { browser }, signal) + } + + await cb({ browser, execute }) + }) +} diff --git a/apps/server/tests/common/page-collector.test.ts b/apps/server/tests/common/page-collector.test.ts deleted file mode 100644 index a7d6abdc..00000000 --- a/apps/server/tests/common/page-collector.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' -import type { Browser, Frame, Page, Target } from 'puppeteer-core' - -import { PageCollector } from '../../src/browser/cdp/page-collector' - -import { getMockRequest } from '../__helpers__/utils' - -function mockListener() { - const listeners: Record void>> = {} - return { - on(eventName: string, listener: (data: unknown) => void) { - if (listeners[eventName]) { - listeners[eventName].push(listener) - } else { - listeners[eventName] = [listener] - } - }, - emit(eventName: string, data: unknown) { - for (const listener of listeners[eventName] ?? []) { - listener(data) - } - }, - } -} - -function getMockPage(): Page { - const mainFrame = {} as Frame - return { - mainFrame() { - return mainFrame - }, - ...mockListener(), - } as Page -} - -function getMockBrowser(): Browser { - const pages = [getMockPage()] - return { - pages() { - return Promise.resolve(pages) - }, - ...mockListener(), - } as Browser -} - -describe('PageCollector', () => { - it('works', async () => { - const browser = getMockBrowser() - const page = (await browser.pages())[0] - const request = getMockRequest() - const collector = new PageCollector(browser, (page, collect) => { - page.on('request', (req) => { - collect(req) - }) - }) - await collector.init() - page.emit('request', request) - - assert.equal(collector.getData(page)[0], request) - }) - - it('clean up after navigation', async () => { - const browser = getMockBrowser() - const page = (await browser.pages())[0] - const mainFrame = page.mainFrame() - const request = getMockRequest() - const collector = new PageCollector(browser, (page, collect) => { - page.on('request', (req) => { - collect(req) - }) - }) - await collector.init() - page.emit('request', request) - - assert.equal(collector.getData(page)[0], request) - page.emit('framenavigated', mainFrame) - - assert.equal(collector.getData(page).length, 0) - }) - - it('does not clean up after sub frame navigation', async () => { - const browser = getMockBrowser() - const page = (await browser.pages())[0] - const request = getMockRequest() - const collector = new PageCollector(browser, (page, collect) => { - page.on('request', (req) => { - collect(req) - }) - }) - await collector.init() - page.emit('request', request) - page.emit('framenavigated', {} as Frame) - - assert.equal(collector.getData(page).length, 1) - }) - - it('clean up after navigation and be able to add data after', async () => { - const browser = getMockBrowser() - const page = (await browser.pages())[0] - const mainFrame = page.mainFrame() - const request = getMockRequest() - const collector = new PageCollector(browser, (page, collect) => { - page.on('request', (req) => { - collect(req) - }) - }) - await collector.init() - page.emit('request', request) - - assert.equal(collector.getData(page)[0], request) - page.emit('framenavigated', mainFrame) - - assert.equal(collector.getData(page).length, 0) - - page.emit('request', request) - - assert.equal(collector.getData(page).length, 1) - }) - - it('should only subscribe once', async () => { - const browser = getMockBrowser() - const page = (await browser.pages())[0] - const request = getMockRequest() - const collector = new PageCollector(browser, (pageListener, collect) => { - pageListener.on('request', (req) => { - collect(req) - }) - }) - await collector.init() - browser.emit('targetcreated', { - page() { - return Promise.resolve(page) - }, - } as Target) - - // The page inside part is async so we need to await some time - await new Promise((res) => res()) - - assert.equal(collector.getData(page).length, 0) - - page.emit('request', request) - - assert.equal(collector.getData(page).length, 1) - - page.emit('request', request) - - assert.equal(collector.getData(page).length, 2) - }) -}) diff --git a/apps/server/tests/tools/bookmarks.test.ts b/apps/server/tests/tools/bookmarks.test.ts new file mode 100644 index 00000000..d2d1d5e3 --- /dev/null +++ b/apps/server/tests/tools/bookmarks.test.ts @@ -0,0 +1,93 @@ +import { describe, it } from 'bun:test' +import assert from 'node:assert' +import { + create_bookmark, + get_bookmarks, + move_bookmark, + remove_bookmark, + search_bookmarks, + update_bookmark, +} from '../../src/tools/bookmarks' +import { withBrowser } from '../__helpers__/with-browser' + +function textOf(result: { + content: { type: string; text?: string }[] +}): string { + return result.content + .filter((c) => c.type === 'text') + .map((c) => c.text) + .join('\n') +} + +describe('bookmark tools', () => { + it('full CRUD lifecycle', async () => { + await withBrowser(async ({ execute }) => { + // Create + const createResult = await execute(create_bookmark, { + title: 'Test Bookmark', + url: 'https://example.com/test-bookmark', + }) + assert.ok(!createResult.isError, textOf(createResult)) + const createText = textOf(createResult) + assert.ok(createText.includes('Test Bookmark')) + const idMatch = createText.match(/ID:\s*(\S+)/) + assert.ok(idMatch, 'Could not extract bookmark ID') + const bookmarkId = idMatch?.[1] + + // Get + const getResult = await execute(get_bookmarks, {}) + assert.ok(!getResult.isError, textOf(getResult)) + assert.ok(textOf(getResult).includes('Test Bookmark')) + + // Search + const searchResult = await execute(search_bookmarks, { + query: 'Test Bookmark', + }) + assert.ok(!searchResult.isError, textOf(searchResult)) + assert.ok(textOf(searchResult).includes('Test Bookmark')) + + // Update + const updateResult = await execute(update_bookmark, { + id: bookmarkId, + title: 'Updated Bookmark', + }) + assert.ok(!updateResult.isError, textOf(updateResult)) + assert.ok(textOf(updateResult).includes('Updated Bookmark')) + + // Remove + const removeResult = await execute(remove_bookmark, { id: bookmarkId }) + assert.ok(!removeResult.isError, textOf(removeResult)) + assert.ok(textOf(removeResult).includes('Removed')) + }) + }, 60_000) + + it('create folder and move bookmark into it', async () => { + await withBrowser(async ({ execute }) => { + // Create folder + const folderResult = await execute(create_bookmark, { + title: 'Test Folder', + }) + assert.ok(!folderResult.isError, textOf(folderResult)) + assert.ok(textOf(folderResult).includes('folder')) + const folderId = textOf(folderResult).match(/ID:\s*(\S+)/)?.[1] + + // Create bookmark + const bmResult = await execute(create_bookmark, { + title: 'Movable Bookmark', + url: 'https://example.com/movable', + }) + const bmId = textOf(bmResult).match(/ID:\s*(\S+)/)?.[1] + + // Move into folder + const moveResult = await execute(move_bookmark, { + id: bmId, + parentId: folderId, + }) + assert.ok(!moveResult.isError, textOf(moveResult)) + assert.ok(textOf(moveResult).includes('Moved')) + + // Cleanup + await execute(remove_bookmark, { id: folderId }) + }) + }, 60_000) +}) diff --git a/apps/server/tests/tools/cdp/console.test.ts b/apps/server/tests/tools/cdp/console.test.ts deleted file mode 100644 index 7f6a221b..00000000 --- a/apps/server/tests/tools/cdp/console.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Console Tools', () => { - it('tests that list_console_messages returns console data', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'list_console_messages', - arguments: {}, - }) - - assert.ok(result.content, 'Should return content') - assert.ok(!result.isError, 'Should not error') - }) - }, 30000) -}) diff --git a/apps/server/tests/tools/cdp/emulation.test.ts b/apps/server/tests/tools/cdp/emulation.test.ts deleted file mode 100644 index c9a08e3e..00000000 --- a/apps/server/tests/tools/cdp/emulation.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' -import { CdpResponse } from '../../../src/tools/cdp/response/cdp-response' -import { emulate } from '../../../src/tools/cdp/tools/emulation' - -import { withCdpBrowser } from '../../__helpers__/utils' - -describe('emulation', () => { - it('emulate - sets network throttling', async () => { - await withCdpBrowser(async (_response, context) => { - const response = new CdpResponse() - await emulate.handler( - { params: { networkConditions: 'Slow 3G' } }, - response, - context, - ) - assert.strictEqual(context.getNetworkConditions(), 'Slow 3G') - }) - }) - - it('emulate - disables network emulation', async () => { - await withCdpBrowser(async (_response, context) => { - const response = new CdpResponse() - await emulate.handler( - { params: { networkConditions: 'No emulation' } }, - response, - context, - ) - assert.strictEqual(context.getNetworkConditions(), null) - }) - }) - - it('emulate - sets cpu throttling', async () => { - await withCdpBrowser(async (_response, context) => { - const response = new CdpResponse() - await emulate.handler( - { params: { cpuThrottlingRate: 4 } }, - response, - context, - ) - assert.strictEqual(context.getCpuThrottlingRate(), 4) - }) - }) - - it('emulate - keeps per-page state', async () => { - await withCdpBrowser(async (_response, context) => { - const pagesBefore = context.getPages() - assert.ok(pagesBefore[0]) - - const response = new CdpResponse() - const newPg = await context.newPage() - const firstPage = pagesBefore[0] - - await emulate.handler( - { params: { networkConditions: 'Slow 3G' } }, - response, - context, - ) - assert.ok(context.isPageSelected(newPg)) - assert.strictEqual(context.getNetworkConditions(), 'Slow 3G') - - context.selectPage(firstPage) - assert.ok(context.isPageSelected(firstPage)) - assert.strictEqual(context.getNetworkConditions(), null) - }) - }) -}) diff --git a/apps/server/tests/tools/cdp/input.test.ts b/apps/server/tests/tools/cdp/input.test.ts deleted file mode 100644 index 8814322c..00000000 --- a/apps/server/tests/tools/cdp/input.test.ts +++ /dev/null @@ -1,311 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' -import fs from 'node:fs/promises' -import path from 'node:path' -import { CdpResponse } from '../../../src/tools/cdp/response/cdp-response' -import { - click, - drag, - fill, - fillForm, - hover, - uploadFile, -} from '../../../src/tools/cdp/tools/input' - -import { serverHooks } from '../../__fixtures__/server' -import { html, withCdpBrowser } from '../../__helpers__/utils' - -function messageFrom(result: { - structuredContent: Record -}): string { - return String(result.structuredContent.message ?? '') -} - -// biome-ignore lint/suspicious/noExplicitAny: test helper -function findUidByName(context: any, name: string): string { - const snapshot = context.getTextSnapshot?.() - assert.ok(snapshot, 'Expected text snapshot to be available') - for (const node of snapshot.idToNode.values()) { - if (node?.name === name) { - return node.id - } - } - throw new Error(`No node found in snapshot with name "${name}"`) -} - -describe('input', () => { - const server = serverHooks() - - it('click - clicks', async () => { - await withCdpBrowser(async (_response, context) => { - const page = context.getSelectedPage() - await page.setContent( - ` - - `, - ) - - await withCdpBrowser(async (_response, context) => { - const page = context.getSelectedPage() - await page.goto(server.getRoute('/unstable')) - await context.createTextSnapshot() - - const response = new CdpResponse() - const uid = findUidByName(context, 'Click to change to see time') - const handlerResolveTime = await click - .handler({ params: { uid } }, response, context) - .then(() => Date.now()) - - const buttonChangeTime = await page.evaluate(() => { - const button = document.querySelector('button') - return Number(button?.textContent) - }) - - assert(handlerResolveTime > buttonChangeTime, 'Waited for stable DOM') - }) - }) - - it('hover - hovers', async () => { - await withCdpBrowser(async (_response, context) => { - const page = context.getSelectedPage() - await page.setContent( - ``, - ) - - await context.createTextSnapshot() - const uid = findUidByName(context, 'test') - - const response = new CdpResponse() - await evaluateScript.handler( - { - params: { - function: String((element: HTMLElement) => element.id), - args: [{ uid }], - }, - }, - response, - context, - ) - const result = await response.handle(evaluateScript.name, context) - const message = String(result.structuredContent.message ?? '') - assert.strictEqual(getJsonResultFromMessage(message), 'test') - }) - }) - - it('browser_evaluate_script - work with multiple element args', async () => { - await withCdpBrowser(async (_response, context) => { - const page = context.getSelectedPage() - await page.setContent( - html`
a
b
`, - ) - await context.createTextSnapshot() - const uidA = findUidByName(context, 'a') - const uidB = findUidByName(context, 'b') - - const response = new CdpResponse() - await evaluateScript.handler( - { - params: { - function: String((a: HTMLElement, b: HTMLElement) => a.id + b.id), - args: [{ uid: uidA }, { uid: uidB }], - }, - }, - response, - context, - ) - const result = await response.handle(evaluateScript.name, context) - const message = String(result.structuredContent.message ?? '') - assert.strictEqual(getJsonResultFromMessage(message), 'ab') - }) - }) -}) diff --git a/apps/server/tests/tools/cdp/snapshot.test.ts b/apps/server/tests/tools/cdp/snapshot.test.ts deleted file mode 100644 index c87f2b65..00000000 --- a/apps/server/tests/tools/cdp/snapshot.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' -import { CdpResponse } from '../../../src/tools/cdp/response/cdp-response' -import { takeSnapshot, waitFor } from '../../../src/tools/cdp/tools/snapshot' - -import { html, withCdpBrowser } from '../../__helpers__/utils' - -describe('snapshot', () => { - it('take_snapshot - includes a snapshot', async () => { - await withCdpBrowser(async (_response, context) => { - const response = new CdpResponse() - await takeSnapshot.handler({ params: {} }, response, context) - const result = await response.handle(takeSnapshot.name, context) - assert.ok( - result.structuredContent.snapshot, - 'Expected snapshot in structuredContent', - ) - }) - }) - - it('wait_for - should work', async () => { - await withCdpBrowser(async (_response, context) => { - const page = await context.getSelectedPage() - - await page.setContent( - html`
Hello
World
`, - ) - - const response = new CdpResponse() - await waitFor.handler({ params: { text: 'Hello' } }, response, context) - const result = await response.handle(waitFor.name, context) - const message = String(result.structuredContent.message ?? '') - assert.ok(message.includes('Element with text "Hello" found.'), message) - assert.ok( - result.structuredContent.snapshot, - 'Expected snapshot in structuredContent', - ) - }) - }) - - it('wait_for - works when element shows up later', async () => { - await withCdpBrowser(async (_response, context) => { - const page = context.getSelectedPage() - - const response = new CdpResponse() - const handlePromise = waitFor.handler( - { params: { text: 'Hello World' } }, - response, - context, - ) - - await page.setContent( - html`
Hello
World
`, - ) - - await handlePromise - - const result = await response.handle(waitFor.name, context) - const message = String(result.structuredContent.message ?? '') - assert.ok( - message.includes('Element with text "Hello World" found.'), - message, - ) - assert.ok( - result.structuredContent.snapshot, - 'Expected snapshot in structuredContent', - ) - }) - }) - - it('wait_for - works with aria elements', async () => { - await withCdpBrowser(async (_response, context) => { - const page = context.getSelectedPage() - - await page.setContent(html`

Header

Text
`) - - const response = new CdpResponse() - await waitFor.handler({ params: { text: 'Header' } }, response, context) - const result = await response.handle(waitFor.name, context) - const message = String(result.structuredContent.message ?? '') - assert.ok(message.includes('Element with text "Header" found.'), message) - assert.ok( - result.structuredContent.snapshot, - 'Expected snapshot in structuredContent', - ) - }) - }) - - it('wait_for - works with iframe content', async () => { - await withCdpBrowser(async (_response, context) => { - const page = await context.getSelectedPage() - - await page.setContent( - html`

Top level

- `, - ) - - const response = new CdpResponse() - await waitFor.handler( - { params: { text: 'Hello iframe' } }, - response, - context, - ) - const result = await response.handle(waitFor.name, context) - const message = String(result.structuredContent.message ?? '') - assert.ok( - message.includes('Element with text "Hello iframe" found.'), - message, - ) - assert.ok( - result.structuredContent.snapshot, - 'Expected snapshot in structuredContent', - ) - }) - }) -}) diff --git a/apps/server/tests/tools/controller/advanced.test.ts b/apps/server/tests/tools/controller/advanced.test.ts deleted file mode 100644 index cc9fb9ee..00000000 --- a/apps/server/tests/tools/controller/advanced.test.ts +++ /dev/null @@ -1,670 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Advanced Tools', () => { - describe('browser_execute_javascript - Success Cases', () => { - it('tests that executing simple JavaScript succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId, code: '1 + 1' }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('JavaScript executed'), - 'Should confirm execution', - ) - assert.ok(textContent.text.includes('Result:'), 'Should include result') - }) - }, 30000) - - it('tests that executing JavaScript returning string succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId, code: '"Hello World"' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that executing JavaScript returning object succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { - tabId, - code: '({name: "test", value: 42})', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that executing JavaScript returning array succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId, code: '[1, 2, 3, 4, 5]' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that executing DOM manipulation JavaScript succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { - tabId, - code: 'document.title', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that executing JavaScript returning undefined succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId, code: 'undefined' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that executing JavaScript returning null succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId, code: 'null' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that executing multiline JavaScript succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const code = ` - const x = 10; - const y = 20; - x + y; - ` - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId, code }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - }) - - describe('browser_execute_javascript - Error Handling', () => { - it('tests that missing code is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId: 1 }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that missing tabId is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { code: '1 + 1' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that invalid JavaScript syntax is handled', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId, code: 'invalid javascript syntax {{{' }, - }) - - // Should either error or return error in result - assert.ok(result, 'Should return a result') - }) - }, 30000) - - it('tests that invalid tabId is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { tabId: 999999, code: '1 + 1' }, - }) - - // Should error - assert.ok(result.isError || result.content, 'Should handle invalid tab') - }) - }, 30000) - }) - - describe('browser_send_keys - Success Cases', () => { - it('tests that sending Enter key succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key: 'Enter' }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - }) - }, 30000) - - it('tests that sending Escape key succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key: 'Escape' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that sending Tab key succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key: 'Tab' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that sending arrow keys succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const arrowKeys = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'] - - for (const key of arrowKeys) { - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key }, - }) - - assert.ok(!result.isError, `Sending ${key} should succeed`) - } - }) - }, 30000) - - it('tests that sending navigation keys succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const navKeys = ['Home', 'End', 'PageUp', 'PageDown'] - - for (const key of navKeys) { - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key }, - }) - - assert.ok(!result.isError, `Sending ${key} should succeed`) - } - }) - }, 30000) - - it('tests that sending Delete key succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key: 'Delete' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that sending Backspace key succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key: 'Backspace' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - }) - - describe('browser_send_keys - Error Handling', () => { - it('tests that missing key is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId: 1 }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that invalid key is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId: 1, key: 'InvalidKey' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Invalid enum value') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that missing tabId is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { key: 'Enter' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that invalid tabId is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId: 999999, key: 'Enter' }, - }) - - // Should error - assert.ok(result.isError || result.content, 'Should handle invalid tab') - }) - }, 30000) - }) - - describe('browser_check_availability - Success Cases', () => { - it('tests that checking BrowserOS availability succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_check_availability', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('BrowserOS APIs available'), - 'Should indicate availability status', - ) - }) - }, 30000) - }) - - describe('Advanced Tools - Response Structure Validation', () => { - it('tests that advanced tools return valid MCP response structure', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const tools = [ - { - name: 'browser_execute_javascript', - args: { tabId, code: '1 + 1' }, - }, - { name: 'browser_send_keys', args: { tabId, key: 'Escape' } }, - { name: 'browser_check_availability', args: {} }, - ] - - for (const tool of tools) { - const result = await client.callTool({ - name: tool.name, - arguments: tool.args, - }) - - // Validate response structure - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(result.content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - // Validate content items - for (const item of result.content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - } - } - }) - }, 30000) - }) - - describe('Advanced Tools - Workflow Tests', () => { - it('tests workflow: check availability → execute JavaScript', async () => { - await withMcpServer(async (client) => { - // Check availability - const availResult = await client.callTool({ - name: 'browser_check_availability', - arguments: {}, - }) - - assert.ok(!availResult.isError, 'Availability check should succeed') - - // Execute JavaScript - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const jsResult = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { - tabId, - code: 'window.location.href', - }, - }) - - assert.ok(!jsResult.isError, 'JavaScript execution should succeed') - }) - }, 30000) - - it('tests workflow: execute JS → send keys → execute JS again', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Execute initial JS - const js1Result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { - tabId, - code: 'document.title', - }, - }) - - assert.ok(!js1Result.isError, 'First JS execution should succeed') - - // Send key - const keyResult = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key: 'Escape' }, - }) - - assert.ok(!keyResult.isError, 'Send key should succeed') - - // Execute JS again - const js2Result = await client.callTool({ - name: 'browser_execute_javascript', - arguments: { - tabId, - code: 'document.readyState', - }, - }) - - assert.ok(!js2Result.isError, 'Second JS execution should succeed') - }) - }, 30000) - - it('tests workflow: multiple key sends in sequence', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const keys = ['ArrowDown', 'ArrowDown', 'ArrowDown', 'Enter'] - - for (const key of keys) { - const result = await client.callTool({ - name: 'browser_send_keys', - arguments: { tabId, key }, - }) - - assert.ok(!result.isError, `Sending ${key} should succeed`) - } - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/bookmarks.test.ts b/apps/server/tests/tools/controller/bookmarks.test.ts deleted file mode 100644 index c62018d6..00000000 --- a/apps/server/tests/tools/controller/bookmarks.test.ts +++ /dev/null @@ -1,635 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Bookmark Tools', () => { - describe('get_bookmarks - Success Cases', () => { - it('tests that getting all bookmarks succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_bookmarks', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Found'), - 'Should indicate bookmarks found', - ) - assert.ok( - textContent.text.includes('bookmarks'), - 'Should mention bookmarks', - ) - }) - }, 30000) - - it('tests that getting bookmarks from specific folder succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_bookmarks', - arguments: { folderId: '1' }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - }) - }, 30000) - - it('tests that empty bookmarks list is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_bookmarks', - arguments: { folderId: '999999' }, - }) - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - }) - }, 30000) - }) - - describe('create_bookmark - Success Cases', () => { - it('tests that creating bookmark with title and URL succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Test Bookmark', - url: 'https://example.com', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Created bookmark'), - 'Should confirm creation', - ) - assert.ok( - textContent.text.includes('Test Bookmark'), - 'Should include title', - ) - assert.ok(textContent.text.includes('ID:'), 'Should include ID') - }) - }, 30000) - - it('tests that creating bookmark with parentId succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Nested Bookmark', - url: 'https://nested.example.com', - parentId: '1', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Created bookmark'), - 'Should confirm creation', - ) - }) - }, 30000) - - it('tests that creating bookmark with special characters succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Test & Special ', - url: 'https://example.com/path?query=value&foo=bar', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that creating bookmark with unicode title succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: '测试书签 📚 テスト', - url: 'https://unicode.example.com', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that creating bookmark with localhost URL succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Localhost', - url: 'http://localhost:3000', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - }) - - describe('create_bookmark - Error Handling', () => { - it('tests that missing title is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - url: 'https://example.com', - }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that missing URL is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Test', - }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that empty title is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: '', - url: 'https://example.com', - }, - }) - - // Should either succeed or return error - assert.ok(result, 'Should return a result') - }) - }, 30000) - }) - - describe('remove_bookmark - Success Cases', () => { - it('tests that removing bookmark by ID succeeds', async () => { - await withMcpServer(async (client) => { - // First create a bookmark - const createResult = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'To Be Deleted', - url: 'https://delete.example.com', - }, - }) - - const createText = createResult.content.find((c) => c.type === 'text') - const idMatch = createText.text.match(/ID: (\d+)/) - const bookmarkId = idMatch ? idMatch[1] : '1' - - // Remove it - const result = await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Removed bookmark'), - 'Should confirm removal', - ) - }) - }, 30000) - - it('tests that removing multiple bookmarks sequentially succeeds', async () => { - await withMcpServer(async (client) => { - // Create two bookmarks - const create1 = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'First', - url: 'https://first.example.com', - }, - }) - - const create2 = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Second', - url: 'https://second.example.com', - }, - }) - - const id1Match = create1.content - .find((c) => c.type === 'text') - .text.match(/ID: (\d+)/) - const id2Match = create2.content - .find((c) => c.type === 'text') - .text.match(/ID: (\d+)/) - - const id1 = id1Match ? id1Match[1] : '1' - const id2 = id2Match ? id2Match[1] : '2' - - // Remove both - const remove1 = await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId: id1 }, - }) - - const remove2 = await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId: id2 }, - }) - - assert.ok(!remove1.isError, 'First removal should succeed') - assert.ok(!remove2.isError, 'Second removal should succeed') - }) - }, 30000) - }) - - describe('remove_bookmark - Error Handling', () => { - it('tests that missing bookmarkId is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'remove_bookmark', - arguments: {}, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that invalid bookmarkId is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId: '999999999' }, - }) - - // Should either error or succeed gracefully - assert.ok(result, 'Should return a result') - }) - }, 30000) - }) - - describe('update_bookmark - Success Cases', () => { - it('tests that updating bookmark title succeeds', async () => { - await withMcpServer(async (client) => { - // First create a bookmark - const createResult = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Original Title', - url: 'https://update.example.com', - }, - }) - - const createText = createResult.content.find((c) => c.type === 'text') - const idMatch = createText.text.match(/ID: (\d+)/) - const bookmarkId = idMatch ? idMatch[1] : '1' - - // Update the title - const result = await client.callTool({ - name: 'update_bookmark', - arguments: { - bookmarkId, - title: 'Updated Title', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Updated bookmark'), - 'Should confirm update', - ) - assert.ok( - textContent.text.includes('Updated Title'), - 'Should include new title', - ) - - // Cleanup - await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId }, - }) - }) - }, 30000) - - it('tests that updating bookmark URL succeeds', async () => { - await withMcpServer(async (client) => { - // First create a bookmark - const createResult = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'URL Test', - url: 'https://old-url.example.com', - }, - }) - - const createText = createResult.content.find((c) => c.type === 'text') - const idMatch = createText.text.match(/ID: (\d+)/) - const bookmarkId = idMatch ? idMatch[1] : '1' - - // Update the URL - const result = await client.callTool({ - name: 'update_bookmark', - arguments: { - bookmarkId, - url: 'https://new-url.example.com', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('https://new-url.example.com'), - 'Should include new URL', - ) - - // Cleanup - await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId }, - }) - }) - }, 30000) - - it('tests that updating both title and URL succeeds', async () => { - await withMcpServer(async (client) => { - // First create a bookmark - const createResult = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Original', - url: 'https://original.example.com', - }, - }) - - const createText = createResult.content.find((c) => c.type === 'text') - const idMatch = createText.text.match(/ID: (\d+)/) - const bookmarkId = idMatch ? idMatch[1] : '1' - - // Update both - const result = await client.callTool({ - name: 'update_bookmark', - arguments: { - bookmarkId, - title: 'New Title', - url: 'https://new.example.com', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('New Title'), - 'Should include new title', - ) - assert.ok( - textContent.text.includes('https://new.example.com'), - 'Should include new URL', - ) - - // Cleanup - await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId }, - }) - }) - }, 30000) - }) - - describe('update_bookmark - Error Handling', () => { - it('tests that missing bookmarkId is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'update_bookmark', - arguments: { title: 'Test' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that invalid bookmarkId is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'update_bookmark', - arguments: { - bookmarkId: '999999999', - title: 'Test', - }, - }) - - // Should error with invalid ID - assert.ok(result, 'Should return a result') - }) - }, 30000) - }) - - describe('Bookmark Tools - Response Structure Validation', () => { - it('tests that bookmark tools return valid MCP response structure', async () => { - await withMcpServer(async (client) => { - const tools = [ - { name: 'get_bookmarks', args: {} }, - { - name: 'create_bookmark', - args: { title: 'Test', url: 'https://test.com' }, - }, - ] - - for (const tool of tools) { - const result = await client.callTool({ - name: tool.name, - arguments: tool.args, - }) - - // Validate response structure - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(result.content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - // Validate content items - for (const item of result.content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - } - } - }) - }, 30000) - }) - - describe('Bookmark Tools - Workflow Tests', () => { - it('tests complete bookmark workflow: create → get → verify → remove', async () => { - await withMcpServer(async (client) => { - // Create bookmark - const createResult = await client.callTool({ - name: 'create_bookmark', - arguments: { - title: 'Workflow Test', - url: 'https://workflow.example.com', - }, - }) - - assert.ok(!createResult.isError, 'Create should succeed') - - const createText = createResult.content.find((c) => c.type === 'text') - const idMatch = createText.text.match(/ID: (\d+)/) - const bookmarkId = idMatch ? idMatch[1] : '1' - - // Get all bookmarks - const getResult = await client.callTool({ - name: 'get_bookmarks', - arguments: {}, - }) - - assert.ok(!getResult.isError, 'Get should succeed') - - const getText = getResult.content.find((c) => c.type === 'text') - assert.ok( - getText.text.includes('Workflow Test'), - 'Should find created bookmark', - ) - - // Remove bookmark - const removeResult = await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId }, - }) - - assert.ok(!removeResult.isError, 'Remove should succeed') - }) - }, 30000) - - it('tests bookmark batch operations workflow', async () => { - await withMcpServer(async (client) => { - const bookmarks = [ - { title: 'Batch 1', url: 'https://batch1.com' }, - { title: 'Batch 2', url: 'https://batch2.com' }, - { title: 'Batch 3', url: 'https://batch3.com' }, - ] - - const bookmarkIds: string[] = [] - - // Create multiple bookmarks - for (const bookmark of bookmarks) { - const result = await client.callTool({ - name: 'create_bookmark', - arguments: bookmark, - }) - - assert.ok( - !result.isError, - `Creating ${bookmark.title} should succeed`, - ) - - const text = result.content.find((c) => c.type === 'text') - const idMatch = text.text.match(/ID: (\d+)/) - if (idMatch) { - bookmarkIds.push(idMatch[1]) - } - } - - // Get all bookmarks - const getAllResult = await client.callTool({ - name: 'get_bookmarks', - arguments: {}, - }) - - assert.ok(!getAllResult.isError, 'Get all should succeed') - - // Remove all created bookmarks - for (const id of bookmarkIds) { - const removeResult = await client.callTool({ - name: 'remove_bookmark', - arguments: { bookmarkId: id }, - }) - - assert.ok(!removeResult.isError, `Removing ${id} should succeed`) - } - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/content.test.ts b/apps/server/tests/tools/controller/content.test.ts deleted file mode 100644 index 6f29f558..00000000 --- a/apps/server/tests/tools/controller/content.test.ts +++ /dev/null @@ -1,460 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Content Tools', () => { - describe('browser_get_page_content - Success Cases', () => { - it('tests that page content extraction with text type succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page with content - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Title

This is a paragraph of text.

Another paragraph.

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text' }, - }) - - assert.ok(Array.isArray(result.content), 'Content should be array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - - // If getSnapshot API is available, check for pagination info - if (!result.isError && textContent.text.includes('Total pages:')) { - assert.ok( - textContent.text.includes('characters total'), - 'Should include character count', - ) - } - }) - }, 30000) - - it.skip('tests that page content extraction with text-with-links type succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page with links - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Links Page

Example Link

Some text

Test Link', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text-with-links' }, - }) - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - - // If getSnapshot API is available, check for pagination info - if (!result.isError) { - assert.ok( - textContent.text.includes('Total pages:') || - textContent.text.includes('Error:'), - 'Should include pagination info or error', - ) - } - }) - }, 30000) - - it.skip('tests that page content extraction with specific page number succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page with content - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Page Title

Content here

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text', page: '1' }, - }) - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - - // If getSnapshot API is available, check for page info - if (!result.isError) { - assert.ok( - textContent.text.includes('Page 1 of') || - textContent.text.includes('Error:'), - 'Should indicate page 1 or error', - ) - } - }) - }, 30000) - - it.skip('tests that page content extraction with all pages succeeds', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Title

Content

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text', page: 'all' }, - }) - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - - // If getSnapshot API is available, check for total pages - if (!result.isError) { - assert.ok( - textContent.text.includes('Total pages:') || - textContent.text.includes('Error:'), - 'Should show total pages or error', - ) - } - }) - }, 30000) - - it.skip('tests that page content extraction with different context window sizes succeeds', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Title

Content

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Test different context windows - const contextWindows = ['20k', '30k', '50k', '100k'] - - for (const contextWindow of contextWindows) { - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text', contextWindow }, - }) - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - - // If getSnapshot API is available, check for context window info - if (!result.isError) { - assert.ok( - textContent.text.includes(contextWindow) || - textContent.text.includes('Error:'), - `Should mention ${contextWindow} or error`, - ) - } - } - }) - }, 60000) - - it('tests that empty page content extraction is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text' }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - }) - }, 30000) - }) - - describe('browser_get_page_content - Error Handling', () => { - it('tests that content extraction with invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId: 999999999, type: 'text' }, - }) - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content array') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that non-numeric tab ID is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId: 'invalid', type: 'text' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that invalid type enum is rejected', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Content

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'invalid-type' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid') || - textContent.text.includes('enum') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it.skip('tests that invalid page number is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Content

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text', page: '999' }, - }) - - assert.ok(!result.isError, 'Should not throw error') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Error') || - textContent.text.includes('Invalid page'), - 'Should indicate invalid page', - ) - }) - }, 30000) - - it.skip('tests that non-numeric page number is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Content

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text', page: 'invalid' }, - }) - - assert.ok(!result.isError, 'Should not throw error') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Error') || - textContent.text.includes('Invalid page'), - 'Should indicate invalid page', - ) - }) - }, 30000) - }) - - describe('browser_get_page_content - Response Structure Validation', () => { - it('tests that content tool returns valid MCP response structure', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Test

Content

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text' }, - }) - - // Validate response structure - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(result.content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - // Validate content items - for (const item of result.content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - } - }) - }, 30000) - }) - - describe('browser_get_page_content - Workflow Tests', () => { - it('tests complete content extraction workflow: navigate -> extract text -> extract text-with-links', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Article Title

This is a paragraph with a link.

Subtitle

More content here.

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Extract text only - const textResult = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text' }, - }) - - assert.ok(!textResult.isError, 'Text extraction should succeed') - - // Extract text with links - const linksResult = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text-with-links' }, - }) - - assert.ok( - !linksResult.isError, - 'Text with links extraction should succeed', - ) - }) - }, 30000) - - it('tests pagination workflow: extract all pages -> extract specific page', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: - 'data:text/html,

Long Content

'.repeat(100) + - 'Content paragraph.' + - '

'.repeat(100) + - '', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Extract all pages with small context window - const allPagesResult = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text', page: 'all', contextWindow: '20k' }, - }) - - assert.ok( - !allPagesResult.isError, - 'All pages extraction should succeed', - ) - - // Extract specific page - const page1Result = await client.callTool({ - name: 'browser_get_page_content', - arguments: { tabId, type: 'text', page: '1', contextWindow: '20k' }, - }) - - assert.ok(!page1Result.isError, 'Page 1 extraction should succeed') - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/coordinates.test.ts b/apps/server/tests/tools/controller/coordinates.test.ts deleted file mode 100644 index 82b84732..00000000 --- a/apps/server/tests/tools/controller/coordinates.test.ts +++ /dev/null @@ -1,572 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Coordinates Tools', () => { - describe('browser_click_coordinates - Success Cases', () => { - it('tests that clicking at coordinates in active tab succeeds', async () => { - await withMcpServer(async (client) => { - // Get active tab - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Click at coordinates - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: 100, y: 100 }, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Clicked at coordinates'), - 'Should confirm click', - ) - assert.ok( - textContent.text.includes('100') && textContent.text.includes('100'), - 'Should mention coordinates', - ) - }) - }, 30000) - - it('tests that clicking at top-left coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: 10, y: 10 }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that clicking at center coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: 500, y: 400 }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that clicking at zero coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: 0, y: 0 }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that clicking at large coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: 2000, y: 1500 }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that clicking with decimal coordinates is rejected', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: 100.5, y: 200.7 }, - }) - - assert.ok(result.isError, 'Should reject decimal coordinates') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('expected int'), - 'Should indicate integer required', - ) - }) - }, 30000) - }) - - describe('browser_click_coordinates - Error Handling', () => { - it('tests that missing tabId is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { x: 100, y: 100 }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that missing coordinates is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId: 1 }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that non-numeric coordinates is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId: 1, x: 'invalid', y: 100 }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that negative coordinates are handled', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: -10, y: -20 }, - }) - - // Should either succeed or error gracefully - assert.ok(result, 'Should return a result') - }) - }, 30000) - - it('tests that invalid tabId is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId: 999999, x: 100, y: 100 }, - }) - - // Should error - assert.ok(result.isError || result.content, 'Should handle invalid tab') - }) - }, 30000) - }) - - describe('browser_type_at_coordinates - Success Cases', () => { - it('tests that typing at coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId, x: 200, y: 200, text: 'Hello World' }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Clicked at'), - 'Should confirm click', - ) - assert.ok( - textContent.text.includes('typed text'), - 'Should confirm typing', - ) - }) - }, 30000) - - it('tests that typing special characters at coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { - tabId, - x: 150, - y: 150, - text: '!@#$%^&*()_+-=[]{}|;:\'",.<>?/', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that typing empty string at coordinates is rejected', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId, x: 100, y: 100, text: '' }, - }) - - assert.ok(result.isError, 'Should reject empty string') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Too small') || - textContent.text.includes('>=1 characters'), - 'Should indicate minimum length required', - ) - }) - }, 30000) - - it('tests that typing unicode at coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId, x: 100, y: 100, text: '你好世界 🌍 テスト' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that typing long text at coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const longText = 'Lorem ipsum dolor sit amet '.repeat(50) - - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId, x: 100, y: 100, text: longText }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that typing multiline text at coordinates succeeds', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId, x: 100, y: 100, text: 'Line 1\nLine 2\nLine 3' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - }) - - describe('browser_type_at_coordinates - Error Handling', () => { - it('tests that missing text is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId: 1, x: 100, y: 100 }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that missing coordinates is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId: 1, text: 'test' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Required') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that invalid tabId is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId: 999999, x: 100, y: 100, text: 'test' }, - }) - - // Should error - assert.ok(result.isError || result.content, 'Should handle invalid tab') - }) - }, 30000) - }) - - describe('Coordinates Tools - Response Structure Validation', () => { - it('tests that coordinates tools return valid MCP response structure', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const tools = [ - { - name: 'browser_click_coordinates', - args: { tabId, x: 50, y: 50 }, - }, - { - name: 'browser_type_at_coordinates', - args: { tabId, x: 60, y: 60, text: 'test' }, - }, - ] - - for (const tool of tools) { - const result = await client.callTool({ - name: tool.name, - arguments: tool.args, - }) - - // Validate response structure - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(result.content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - // Validate content items - for (const item of result.content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - } - } - }) - }, 30000) - }) - - describe('Coordinates Tools - Workflow Tests', () => { - it('tests coordinate workflow: navigate → click → type', async () => { - await withMcpServer(async (client) => { - // Navigate to URL - await client.callTool({ - name: 'browser_navigate', - arguments: { url: 'https://example.com' }, - }) - - // Get active tab - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Click coordinates - const clickResult = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: 300, y: 300 }, - }) - - assert.ok(!clickResult.isError, 'Click should succeed') - - // Type at coordinates - const typeResult = await client.callTool({ - name: 'browser_type_at_coordinates', - arguments: { tabId, x: 350, y: 350, text: 'Workflow test' }, - }) - - assert.ok(!typeResult.isError, 'Type should succeed') - }) - }, 30000) - - it('tests multiple coordinate clicks in sequence', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabIdMatch = tabText.text.match(/ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const coordinates = [ - { x: 100, y: 100 }, - { x: 200, y: 200 }, - { x: 300, y: 300 }, - { x: 400, y: 400 }, - ] - - for (const coord of coordinates) { - const result = await client.callTool({ - name: 'browser_click_coordinates', - arguments: { tabId, x: coord.x, y: coord.y }, - }) - - assert.ok( - !result.isError, - `Click at (${coord.x}, ${coord.y}) should succeed`, - ) - } - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/history.test.ts b/apps/server/tests/tools/controller/history.test.ts deleted file mode 100644 index c09cec5a..00000000 --- a/apps/server/tests/tools/controller/history.test.ts +++ /dev/null @@ -1,345 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller History Tools', () => { - describe('search_history - Success Cases', () => { - it('tests that history search with query succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: 'example' }, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Found'), - 'Should indicate results found', - ) - assert.ok( - textContent.text.includes('history items'), - 'Should mention history items', - ) - }) - }, 30000) - - it('tests that history search with maxResults limit succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: 'test', maxResults: 10 }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok(textContent.text.includes('Found'), 'Should show results') - }) - }, 30000) - - it('tests that history search with empty query succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: '' }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - }) - }, 30000) - - it('tests that history search with special characters succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: 'test@example.com' }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that history search with large maxResults succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: 'test', maxResults: 1000 }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - }) - - describe('search_history - Error Handling', () => { - it('tests that non-numeric maxResults is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: 'test', maxResults: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that zero maxResults is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: 'test', maxResults: 0 }, - }) - - assert.ok(result.isError, 'Should be an error') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Too small') || - textContent.text.includes('expected number to be >0'), - 'Should reject zero maxResults', - ) - }) - }, 30000) - - it('tests that negative maxResults is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'search_history', - arguments: { query: 'test', maxResults: -1 }, - }) - - // Should either succeed with 0 results or handle gracefully - assert.ok(result, 'Should return a result') - }) - }, 30000) - }) - - describe('get_recent_history - Success Cases', () => { - it('tests that getting recent history with default count succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_recent_history', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Retrieved'), - 'Should indicate items retrieved', - ) - assert.ok( - textContent.text.includes('history items'), - 'Should mention history items', - ) - }) - }, 30000) - - it('tests that getting recent history with specific count succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 10 }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - }) - }, 30000) - - it('tests that getting recent history with large count succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 500 }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - - it('tests that getting recent history with count 1 succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 1 }, - }) - - assert.ok(!result.isError, 'Should succeed') - }) - }, 30000) - }) - - describe('get_recent_history - Error Handling', () => { - it('tests that non-numeric count is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that zero count returns all items', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 0 }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Retrieved'), - 'Should return results (zero not enforced)', - ) - }) - }, 30000) - - it('tests that negative count is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_recent_history', - arguments: { count: -1 }, - }) - - // Should either succeed with 0 results or handle gracefully - assert.ok(result, 'Should return a result') - }) - }, 30000) - }) - - describe('History Tools - Response Structure Validation', () => { - it('tests that history tools return valid MCP response structure', async () => { - await withMcpServer(async (client) => { - const tools = [ - { name: 'search_history', args: { query: 'test' } }, - { name: 'get_recent_history', args: {} }, - ] - - for (const tool of tools) { - const result = await client.callTool({ - name: tool.name, - arguments: tool.args, - }) - - // Validate response structure - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(result.content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - // Validate content items - for (const item of result.content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - } - } - }) - }, 30000) - }) - - describe('History Tools - Workflow Tests', () => { - it('tests complete history workflow: get recent -> search specific', async () => { - await withMcpServer(async (client) => { - // Get recent history - const recentResult = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 5 }, - }) - - assert.ok(!recentResult.isError, 'Get recent should succeed') - - // Search history - const searchResult = await client.callTool({ - name: 'search_history', - arguments: { query: 'browseros', maxResults: 10 }, - }) - - assert.ok(!searchResult.isError, 'Search should succeed') - }) - }, 30000) - - it('tests history comparison workflow: get recent multiple times', async () => { - await withMcpServer(async (client) => { - // Get recent history first time - const result1 = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 20 }, - }) - - assert.ok(!result1.isError, 'First call should succeed') - - // Navigate to add to history - await client.callTool({ - name: 'navigate_page', - arguments: { url: 'https://example.com' }, - }) - - // Get recent history second time - const result2 = await client.callTool({ - name: 'get_recent_history', - arguments: { count: 20 }, - }) - - assert.ok(!result2.isError, 'Second call should succeed') - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/interaction.test.ts b/apps/server/tests/tools/controller/interaction.test.ts deleted file mode 100644 index 2463da2a..00000000 --- a/apps/server/tests/tools/controller/interaction.test.ts +++ /dev/null @@ -1,731 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Interaction Tools', () => { - describe('browser_get_interactive_elements - Success Cases', () => { - it('tests that interactive elements are retrieved with simplified format', async () => { - await withMcpServer(async (client) => { - // Navigate to a page with interactive elements - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,Link', - }, - }) - - assert.ok(!navResult.isError, 'Navigation should succeed') - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get interactive elements - const result = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId, simplified: true }, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('INTERACTIVE ELEMENTS'), - 'Should include header', - ) - assert.ok( - textContent.text.includes('Snapshot ID:'), - 'Should include snapshot ID', - ) - assert.ok(textContent.text.includes('Legend'), 'Should include legend') - }) - }, 30000) - - it('tests that interactive elements are retrieved with full format', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId, simplified: false }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - // Full format includes more context (ctx:) in element descriptions - assert.ok( - textContent.text.includes('ctx:') || - textContent.text.includes('INTERACTIVE ELEMENTS'), - 'Full format should include detailed element info', - ) - }) - }, 30000) - - it('tests that page with no interactive elements is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Just plain text

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('INTERACTIVE ELEMENTS') && - textContent.text.includes('Snapshot ID:'), - 'Should return valid response with snapshot info', - ) - }) - }, 30000) - }) - - describe('browser_get_interactive_elements - Error Handling', () => { - it('tests that invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content array') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that non-numeric tab ID is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - }) - - describe('browser_click_element - Success Cases', () => { - it.skip('tests that element click succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page with a clickable button - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get interactive elements to find the button's nodeId - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - assert.ok(!elementsResult.isError, 'Get elements should succeed') - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - // Extract first nodeId from the response (format: [123]) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - assert.ok(nodeIdMatch, 'Should find a nodeId') - const nodeId = parseInt(nodeIdMatch[1], 10) - - // Click the element - const clickResult = await client.callTool({ - name: 'browser_click_element', - arguments: { tabId, nodeId }, - }) - - assert.ok(!clickResult.isError, 'Should succeed') - - const clickText = clickResult.content.find((c) => c.type === 'text') - assert.ok(clickText, 'Should have text content') - assert.ok( - clickText.text.includes(`Clicked element ${nodeId}`), - 'Should confirm click', - ) - assert.ok( - clickText.text.includes(`tab ${tabId}`), - 'Should include tab ID', - ) - }) - }, 30000) - }) - - describe('browser_click_element - Error Handling', () => { - it('tests that clicking with invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_click_element', - arguments: { tabId: 999999999, nodeId: 1 }, - }) - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content array') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that clicking with invalid node ID is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_click_element', - arguments: { tabId, nodeId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that non-numeric parameters are rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_click_element', - arguments: { tabId: 'invalid', nodeId: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - }) - - describe('browser_type_text - Success Cases', () => { - it.skip('tests that typing text into input succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page with an input field - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get interactive elements to find the input's nodeId - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - // Type text into the input - const typeResult = await client.callTool({ - name: 'browser_type_text', - arguments: { tabId, nodeId, text: 'Hello World' }, - }) - - assert.ok(!typeResult.isError, 'Should succeed') - - const typeText = typeResult.content.find((c) => c.type === 'text') - assert.ok(typeText, 'Should have text content') - assert.ok( - typeText.text.includes(`Typed text into element ${nodeId}`), - 'Should confirm text typed', - ) - }) - }, 30000) - - it.skip('tests that typing empty string succeeds', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - const typeResult = await client.callTool({ - name: 'browser_type_text', - arguments: { tabId, nodeId, text: '' }, - }) - - assert.ok(!typeResult.isError, 'Should succeed') - }) - }, 30000) - - it.skip('tests that typing special characters succeeds', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - const typeResult = await client.callTool({ - name: 'browser_type_text', - arguments: { tabId, nodeId, text: '!@#$%^&*()_+-={}[]|:";\'<>?,./' }, - }) - - assert.ok(!typeResult.isError, 'Should succeed') - }) - }, 30000) - }) - - describe('browser_type_text - Error Handling', () => { - it('tests that typing with invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_type_text', - arguments: { tabId: 999999999, nodeId: 1, text: 'test' }, - }) - - assert.ok(result, 'Should return a result') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that typing with invalid node ID is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_type_text', - arguments: { tabId, nodeId: 999999999, text: 'test' }, - }) - - assert.ok(result, 'Should return a result') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - }) - - describe('browser_clear_input - Success Cases', () => { - it.skip('tests that clearing input field succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page with an input field - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get interactive elements - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - // Clear the input - const clearResult = await client.callTool({ - name: 'browser_clear_input', - arguments: { tabId, nodeId }, - }) - - assert.ok(!clearResult.isError, 'Should succeed') - - const clearText = clearResult.content.find((c) => c.type === 'text') - assert.ok(clearText, 'Should have text content') - assert.ok( - clearText.text.includes(`Cleared element ${nodeId}`), - 'Should confirm clear', - ) - }) - }, 30000) - }) - - describe('browser_clear_input - Error Handling', () => { - it('tests that clearing with invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_clear_input', - arguments: { tabId: 999999999, nodeId: 1 }, - }) - - assert.ok(result, 'Should return a result') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that clearing with invalid node ID is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_clear_input', - arguments: { tabId, nodeId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - }) - - describe('browser_scroll_to_element - Success Cases', () => { - it.skip('tests that scrolling to element succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a long page with a button at the bottom - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get interactive elements - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - // Scroll to the element - const scrollResult = await client.callTool({ - name: 'browser_scroll_to_element', - arguments: { tabId, nodeId }, - }) - - assert.ok(!scrollResult.isError, 'Should succeed') - - const scrollText = scrollResult.content.find((c) => c.type === 'text') - assert.ok(scrollText, 'Should have text content') - assert.ok( - scrollText.text.includes(`Scrolled to element ${nodeId}`), - 'Should confirm scroll', - ) - }) - }, 30000) - }) - - describe('browser_scroll_to_element - Error Handling', () => { - it('tests that scrolling with invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_scroll_to_element', - arguments: { tabId: 999999999, nodeId: 1 }, - }) - - assert.ok(result, 'Should return a result') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that scrolling with invalid node ID is handled', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_scroll_to_element', - arguments: { tabId, nodeId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - }) - - describe('Interaction Tools - Workflow Tests', () => { - it.skip('tests complete interaction workflow: get elements -> click', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get elements - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - assert.ok(!elementsResult.isError, 'Get elements should succeed') - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - // Click element - const clickResult = await client.callTool({ - name: 'browser_click_element', - arguments: { tabId, nodeId }, - }) - - assert.ok(!clickResult.isError, 'Click should succeed') - }) - }, 30000) - - it.skip('tests complete form workflow: get elements -> type -> clear', async () => { - await withMcpServer(async (client) => { - // Navigate to a form - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get elements - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - // Get first input nodeId - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - // Type text - const typeResult = await client.callTool({ - name: 'browser_type_text', - arguments: { tabId, nodeId, text: 'John Doe' }, - }) - - assert.ok(!typeResult.isError, 'Type should succeed') - - // Clear input - const clearResult = await client.callTool({ - name: 'browser_clear_input', - arguments: { tabId, nodeId }, - }) - - assert.ok(!clearResult.isError, 'Clear should succeed') - }) - }, 30000) - - it.skip('tests complete scroll workflow: get elements -> scroll to element -> click', async () => { - await withMcpServer(async (client) => { - // Navigate to a long page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Get elements - const elementsResult = await client.callTool({ - name: 'browser_get_interactive_elements', - arguments: { tabId }, - }) - - const elementsText = elementsResult.content.find( - (c) => c.type === 'text', - ) - const nodeIdMatch = elementsText.text.match(/\[(\d+)\]/) - const nodeId = parseInt(nodeIdMatch[1], 10) - - // Scroll to element - const scrollResult = await client.callTool({ - name: 'browser_scroll_to_element', - arguments: { tabId, nodeId }, - }) - - assert.ok(!scrollResult.isError, 'Scroll should succeed') - - // Click element - const clickResult = await client.callTool({ - name: 'browser_click_element', - arguments: { tabId, nodeId }, - }) - - assert.ok(!clickResult.isError, 'Click should succeed') - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/navigation.test.ts b/apps/server/tests/tools/controller/navigation.test.ts deleted file mode 100644 index 856763a9..00000000 --- a/apps/server/tests/tools/controller/navigation.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { type McpContentItem, withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Navigation Tools', () => { - describe('browser_navigate - Success Cases', () => { - it('tests that navigation to HTTPS URL succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'https://example.com', - }, - }) - const content = result.content as McpContentItem[] - - assert.ok(!result.isError, 'Navigation should succeed') - assert.ok(Array.isArray(content), 'Content should be an array') - assert.ok(content.length > 0, 'Content should not be empty') - - const textContent = content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should include text content') - assert.ok( - textContent.text?.includes('Navigating to'), - 'Should include navigation message', - ) - assert.ok( - textContent.text?.includes('Tab ID:'), - 'Should include tab ID', - ) - }) - }, 30000) - - it('tests that navigation to HTTP URL succeeds', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'http://example.com', - }, - }) - const content = result.content as McpContentItem[] - - assert.ok(!result.isError, 'Should succeed') - assert.ok( - Array.isArray(content) && content.length > 0, - 'Should have content', - ) - }) - }, 30000) - }) - - describe('browser_navigate - Error Handling', () => { - it('tests that invalid URL is handled gracefully', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'not-a-valid-url', - }, - }) - const content = result.content as McpContentItem[] - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(content), 'Should have content array') - - if (result.isError) { - const textContent = content.find((c) => c.type === 'text') - assert.ok( - textContent, - 'Error should include text content explaining the issue', - ) - } - }) - }, 30000) - - it('tests that meaningful response structure is provided on any error', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: '', - }, - }) - const content = result.content as McpContentItem[] - - assert.ok(result, 'Should return result object') - assert.ok( - typeof result.isError === 'boolean', - 'isError should be boolean', - ) - assert.ok(Array.isArray(content), 'content should be an array') - - if (result.isError) { - assert.ok(content.length > 0, 'Error response should have content') - const textContent = content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text explaining error') - assert.ok( - textContent.text && textContent.text.length > 0, - 'Error message should not be empty', - ) - } - }) - }, 30000) - }) - - describe('browser_navigate - Response Structure Validation', () => { - it('tests that valid MCP response structure is always returned', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'https://example.com', - }, - }) - const content = result.content as McpContentItem[] - - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - for (const item of content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - } - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/screenshot.test.ts b/apps/server/tests/tools/controller/screenshot.test.ts deleted file mode 100644 index 02d8b48c..00000000 --- a/apps/server/tests/tools/controller/screenshot.test.ts +++ /dev/null @@ -1,428 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Screenshot Tool', () => { - describe('browser_get_screenshot - Success Cases', () => { - it.skip('tests that screenshot capture with default settings succeeds', async () => { - await withMcpServer(async (client) => { - // First navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Screenshot Test Page

Content for screenshot

', - }, - }) - - assert.ok(!navResult.isError, 'Navigation should succeed') - - // Extract tab ID - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - assert.ok(tabIdMatch, 'Should extract tab ID') - const tabId = parseInt(tabIdMatch[1], 10) - - // Capture screenshot - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { tabId }, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be an array') - assert.ok(result.content.length > 0, 'Content should not be empty') - - // Should have text description - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should include text content') - assert.ok( - textContent.text.includes('Screenshot captured'), - 'Should mention screenshot captured', - ) - assert.ok( - textContent.text.includes(`tab ${tabId}`), - 'Should include tab ID', - ) - - // Should have image data - const imageContent = result.content.find((c) => c.type === 'image') - assert.ok(imageContent, 'Should include image content') - assert.ok(imageContent.data, 'Should have image data') - assert.ok(imageContent.mimeType, 'Should have mime type') - assert.ok( - imageContent.mimeType.startsWith('image/'), - 'Should be an image mime type', - ) - }) - }, 30000) - - it.skip('tests that screenshot capture with small size preset succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Small Screenshot Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Capture with small size - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { - tabId, - size: 'small', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const imageContent = result.content.find((c) => c.type === 'image') - assert.ok(imageContent, 'Should include image content') - assert.ok(imageContent.data, 'Should have image data') - }) - }, 30000) - - it.skip('tests that screenshot capture with medium size preset succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Medium Screenshot Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Capture with medium size - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { - tabId, - size: 'medium', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const imageContent = result.content.find((c) => c.type === 'image') - assert.ok(imageContent, 'Should include image content') - }) - }, 30000) - - it.skip('tests that screenshot capture with large size preset succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Large Screenshot Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Capture with large size - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { - tabId, - size: 'large', - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const imageContent = result.content.find((c) => c.type === 'image') - assert.ok(imageContent, 'Should include image content') - }) - }, 30000) - - it.skip('tests that screenshot capture with custom width and height succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Custom Size Screenshot

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Capture with custom dimensions - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { - tabId, - width: 800, - height: 600, - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const imageContent = result.content.find((c) => c.type === 'image') - assert.ok(imageContent, 'Should include image content') - }) - }, 30000) - - it.skip('tests that screenshot capture with showHighlights enabled succeeds', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Highlights Screenshot Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Capture with highlights - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { - tabId, - showHighlights: true, - }, - }) - - assert.ok(!result.isError, 'Should succeed') - - const imageContent = result.content.find((c) => c.type === 'image') - assert.ok(imageContent, 'Should include image content') - }) - }, 30000) - }) - - describe('browser_get_screenshot - Error Handling', () => { - it('tests that screenshot of invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { tabId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content array') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that screenshot with non-numeric tab ID is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { tabId: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that screenshot with invalid size preset is rejected', async () => { - await withMcpServer(async (client) => { - // Navigate to a page first - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { - tabId, - size: 'invalid-size', - }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid') || - textContent.text.includes('enum') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that screenshot with negative dimensions is rejected', async () => { - await withMcpServer(async (client) => { - // Navigate to a page first - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Try with negative width - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { - tabId, - width: -100, - height: 600, - }, - }) - - // May be rejected by validation or extension - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content') - }) - }, 30000) - }) - - describe('browser_get_screenshot - Response Structure Validation', () => { - it('tests that screenshot tool returns valid MCP response structure', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - const result = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { tabId }, - }) - - // Validate response structure - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(result.content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - // Validate content items - for (const item of result.content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - - if (item.type === 'image') { - assert.ok('data' in item, 'Image content must have data property') - assert.ok('mimeType' in item, 'Image content must have mimeType') - assert.strictEqual( - typeof item.data, - 'string', - 'Image data must be string (base64)', - ) - assert.ok( - item.mimeType.startsWith('image/'), - 'mimeType must be image type', - ) - } - } - }) - }, 30000) - }) - - describe('browser_get_screenshot - Workflow Tests', () => { - it.skip('tests complete screenshot workflow: navigate, multiple screenshots with different sizes', async () => { - await withMcpServer(async (client) => { - // Navigate to a page - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Multi-Screenshot Test

', - }, - }) - - const navText = navResult.content.find((c) => c.type === 'text') - const tabIdMatch = navText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch[1], 10) - - // Take small screenshot - const smallResult = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { tabId, size: 'small' }, - }) - - assert.ok(!smallResult.isError, 'Small screenshot should succeed') - - // Take large screenshot - const largeResult = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { tabId, size: 'large' }, - }) - - assert.ok(!largeResult.isError, 'Large screenshot should succeed') - - // Take custom size screenshot - const customResult = await client.callTool({ - name: 'browser_get_screenshot', - arguments: { tabId, width: 1024, height: 768 }, - }) - - assert.ok(!customResult.isError, 'Custom screenshot should succeed') - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/scrolling.test.ts b/apps/server/tests/tools/controller/scrolling.test.ts deleted file mode 100644 index 5e56652b..00000000 --- a/apps/server/tests/tools/controller/scrolling.test.ts +++ /dev/null @@ -1,270 +0,0 @@ -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { type McpContentItem, withMcpServer } from '../../__helpers__/utils' - -describe('MCP Controller Scrolling Tools', () => { - describe('browser_scroll_down - Success Cases', () => { - it('tests that scrolling down in active tab succeeds', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Long Page

Scroll test

', - }, - }) - const navContent = navResult.content as McpContentItem[] - - assert.ok(!navResult.isError, 'Navigation should succeed') - - const navText = navContent.find((c) => c.type === 'text') - const tabIdMatch = navText?.text?.match(/Tab ID: (\d+)/) - assert.ok(tabIdMatch, 'Should extract tab ID') - const tabId = parseInt(tabIdMatch?.[1], 10) - - const scrollResult = await client.callTool({ - name: 'browser_scroll_down', - arguments: { tabId }, - }) - const scrollContent = scrollResult.content as McpContentItem[] - - assert.ok(!scrollResult.isError, 'Should succeed') - assert.ok(Array.isArray(scrollContent), 'Content should be array') - assert.ok(scrollContent.length > 0, 'Should have content') - - const textContent = scrollContent.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text?.includes('Scrolled down'), - 'Should confirm scroll down', - ) - assert.ok( - textContent.text?.includes(`tab ${tabId}`), - 'Should include tab ID', - ) - }) - }, 30000) - }) - - describe('browser_scroll_up - Success Cases', () => { - it('tests that scrolling up in active tab succeeds', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Long Page

', - }, - }) - const navContent = navResult.content as McpContentItem[] - - assert.ok(!navResult.isError, 'Navigation should succeed') - - const navText = navContent.find((c) => c.type === 'text') - const tabIdMatch = navText?.text?.match(/Tab ID: (\d+)/) - assert.ok(tabIdMatch, 'Should extract tab ID') - const tabId = parseInt(tabIdMatch?.[1], 10) - - await client.callTool({ - name: 'browser_scroll_down', - arguments: { tabId }, - }) - - const scrollResult = await client.callTool({ - name: 'browser_scroll_up', - arguments: { tabId }, - }) - const scrollContent = scrollResult.content as McpContentItem[] - - assert.ok(!scrollResult.isError, 'Should succeed') - assert.ok(Array.isArray(scrollContent), 'Content should be array') - - const textContent = scrollContent.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text?.includes('Scrolled up'), - 'Should confirm scroll up', - ) - }) - }, 30000) - }) - - describe('Scrolling - Error Handling', () => { - it('tests that scrolling down with invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_scroll_down', - arguments: { tabId: 999999999 }, - }) - const content = result.content as McpContentItem[] - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(content), 'Should have content array') - - if (result.isError) { - const textContent = content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that scrolling up with invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_scroll_up', - arguments: { tabId: 999999999 }, - }) - const content = result.content as McpContentItem[] - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(content), 'Should have content array') - - if (result.isError) { - const textContent = content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - - it('tests that scroll_down with non-numeric tab ID is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_scroll_down', - arguments: { tabId: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - - it('tests that scroll_up with non-numeric tab ID is rejected', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_scroll_up', - arguments: { tabId: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - }) - - describe('Scrolling - Response Structure Validation', () => { - it('tests that scrolling tools return valid MCP response structure', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Test

', - }, - }) - const navContent = navResult.content as McpContentItem[] - - const navText = navContent.find((c) => c.type === 'text') - const tabIdMatch = navText?.text?.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch?.[1] ?? '0', 10) - - const tools = [ - { name: 'browser_scroll_down', args: { tabId } }, - { name: 'browser_scroll_up', args: { tabId } }, - ] - - for (const tool of tools) { - const result = await client.callTool({ - name: tool.name, - arguments: tool.args, - }) - const content = result.content as McpContentItem[] - - assert.ok(result, 'Result should exist') - assert.ok('content' in result, 'Should have content field') - assert.ok(Array.isArray(content), 'content must be an array') - - if ('isError' in result) { - assert.strictEqual( - typeof result.isError, - 'boolean', - 'isError must be boolean when present', - ) - } - - for (const item of content) { - assert.ok(item.type, 'Content item must have type') - assert.ok( - item.type === 'text' || item.type === 'image', - 'Content type must be text or image', - ) - - if (item.type === 'text') { - assert.ok('text' in item, 'Text content must have text property') - assert.strictEqual( - typeof item.text, - 'string', - 'Text must be string', - ) - } - } - } - }) - }, 30000) - }) - - describe('Scrolling - Workflow Tests', () => { - it('tests complete scrolling workflow: navigate, scroll down multiple times, scroll up', async () => { - await withMcpServer(async (client) => { - const navResult = await client.callTool({ - name: 'browser_navigate', - arguments: { - url: 'data:text/html,

Top

Bottom

', - }, - }) - const navContent = navResult.content as McpContentItem[] - - assert.ok(!navResult.isError, 'Navigation should succeed') - - const navText = navContent.find((c) => c.type === 'text') - const tabIdMatch = navText?.text?.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabIdMatch?.[1] ?? '0', 10) - - const scroll1 = await client.callTool({ - name: 'browser_scroll_down', - arguments: { tabId }, - }) - - assert.ok(!scroll1.isError, 'First scroll down should succeed') - - const scroll2 = await client.callTool({ - name: 'browser_scroll_down', - arguments: { tabId }, - }) - - assert.ok(!scroll2.isError, 'Second scroll down should succeed') - - const scroll3 = await client.callTool({ - name: 'browser_scroll_up', - arguments: { tabId }, - }) - - assert.ok(!scroll3.isError, 'Scroll up should succeed') - }) - }, 30000) - }) -}) diff --git a/apps/server/tests/tools/controller/tab-management.test.ts b/apps/server/tests/tools/controller/tab-management.test.ts deleted file mode 100644 index c3c18f44..00000000 --- a/apps/server/tests/tools/controller/tab-management.test.ts +++ /dev/null @@ -1,614 +0,0 @@ -// @ts-nocheck -/** - * @license - * Copyright 2025 BrowserOS - */ - -import { describe, it } from 'bun:test' -import assert from 'node:assert' - -import { withMcpServer } from '../../__helpers__/utils' - -// Tests that use overlapping controller tools (browser_get_active_tab, browser_list_tabs, -// browser_open_tab, browser_close_tab, browser_switch_tab) are skipped when CDP is enabled. -// These tools are only available in the full controller registry (CDP-disabled mode). -// Run with CDP disabled to enable these tests. - -describe('MCP Controller Tab Management Tools', () => { - describe.skip('browser_get_active_tab - Success Cases (requires CDP-disabled mode)', () => { - it('tests that active tab information is successfully retrieved', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be an array') - assert.ok(result.content.length > 0, 'Content should not be empty') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should include text content') - assert.ok( - textContent.text.includes('Active Tab:'), - 'Should include active tab title', - ) - assert.ok(textContent.text.includes('URL:'), 'Should include URL') - assert.ok(textContent.text.includes('Tab ID:'), 'Should include tab ID') - assert.ok( - textContent.text.includes('Window ID:'), - 'Should include window ID', - ) - }) - }, 30000) - }) - - describe.skip('browser_list_tabs - Success Cases (requires CDP-disabled mode)', () => { - it('tests that all open tabs are successfully listed', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_list_tabs', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - assert.ok(result.content.length > 0, 'Should have content') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Found') && - textContent.text.includes('open tabs'), - 'Should include tab count', - ) - }) - }, 30000) - - it('tests that structured content includes tabs and count', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_list_tabs', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(result.structuredContent, 'Should have structuredContent') - assert.ok( - Array.isArray(result.structuredContent.tabs), - 'structuredContent.tabs should be an array', - ) - assert.ok( - typeof result.structuredContent.count === 'number', - 'structuredContent.count should be a number', - ) - assert.strictEqual( - result.structuredContent.tabs.length, - result.structuredContent.count, - 'tabs array length should match count', - ) - - if (result.structuredContent.tabs.length > 0) { - const tab = result.structuredContent.tabs[0] - assert.ok('id' in tab, 'Tab should have id') - assert.ok('url' in tab, 'Tab should have url') - assert.ok('title' in tab, 'Tab should have title') - assert.ok('windowId' in tab, 'Tab should have windowId') - assert.ok('active' in tab, 'Tab should have active') - assert.ok('index' in tab, 'Tab should have index') - } - }) - }, 30000) - }) - - describe.skip('browser_open_tab - Success Cases (requires CDP-disabled mode)', () => { - it('tests that a new tab with URL is successfully opened', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_open_tab', - arguments: { - url: 'https://example.com', - active: true, - }, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - assert.ok(result.content.length > 0, 'Should have content') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Opened new tab'), - 'Should confirm tab opened', - ) - assert.ok(textContent.text.includes('URL:'), 'Should include URL') - assert.ok(textContent.text.includes('Tab ID:'), 'Should include tab ID') - }) - }, 30000) - - it('tests that a new tab without URL is successfully opened', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_open_tab', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - assert.ok(result.content.length > 0, 'Should have content') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should have text content') - assert.ok( - textContent.text.includes('Opened new tab'), - 'Should confirm tab opened', - ) - }) - }, 30000) - - it('tests that a new tab in background is successfully opened', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_open_tab', - arguments: { - url: 'data:text/html,

Background Tab

', - active: false, - }, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be array') - }) - }, 30000) - }) - - describe.skip('browser_close_tab - Success and Error Cases (requires CDP-disabled mode)', () => { - it('tests that a tab is successfully closed by ID', async () => { - await withMcpServer(async (client) => { - const openResult = await client.callTool({ - name: 'browser_open_tab', - arguments: { - url: 'data:text/html,

Tab to Close

', - active: false, - }, - }) - - assert.ok(!openResult.isError, 'Open should succeed') - - const openText = openResult.content.find((c) => c.type === 'text') - const tabIdMatch = openText.text.match(/Tab ID: (\d+)/) - assert.ok(tabIdMatch, 'Should extract tab ID') - const tabId = parseInt(tabIdMatch[1], 10) - - const closeResult = await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId }, - }) - - assert.ok(!closeResult.isError, 'Should succeed') - assert.ok(Array.isArray(closeResult.content), 'Content should be array') - - const closeText = closeResult.content.find((c) => c.type === 'text') - assert.ok(closeText, 'Should have text content') - assert.ok( - closeText.text.includes(`Closed tab ${tabId}`), - 'Should confirm tab closed', - ) - }) - }, 30000) - - it('tests that invalid tab ID is handled gracefully', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content array') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent, - 'Error should include text content explaining the issue', - ) - } - }) - }, 30000) - - it('tests that non-numeric tab ID is rejected with validation error', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId: 'invalid' }, - }) - - assert.ok(result.isError, 'Should be an error') - const textContent = result.content.find((c) => c.type === 'text') - assert.ok( - textContent.text.includes('Invalid arguments') || - textContent.text.includes('Expected number') || - textContent.text.includes('Input validation error'), - 'Should reject with validation error', - ) - }) - }, 30000) - }) - - describe.skip('browser_switch_tab - Success and Error Cases (requires CDP-disabled mode)', () => { - it('tests that switching to a tab by ID succeeds', async () => { - await withMcpServer(async (client) => { - const openResult = await client.callTool({ - name: 'browser_open_tab', - arguments: { - url: 'data:text/html,

Target Tab

', - active: false, - }, - }) - - assert.ok(!openResult.isError, 'Open should succeed') - - const openText = openResult.content.find((c) => c.type === 'text') - const tabIdMatch = openText.text.match(/Tab ID: (\d+)/) - assert.ok(tabIdMatch, 'Should extract tab ID') - const tabId = parseInt(tabIdMatch[1], 10) - - const switchResult = await client.callTool({ - name: 'browser_switch_tab', - arguments: { tabId }, - }) - - assert.ok(!switchResult.isError, 'Should succeed') - assert.ok( - Array.isArray(switchResult.content), - 'Content should be array', - ) - - const switchText = switchResult.content.find((c) => c.type === 'text') - assert.ok(switchText, 'Should have text content') - assert.ok( - switchText.text.includes('Switched to tab:'), - 'Should confirm tab switch', - ) - assert.ok(switchText.text.includes('URL:'), 'Should include URL') - }) - }, 30000) - - it('tests that switching to invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'browser_switch_tab', - arguments: { tabId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content array') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - }) - - describe.skip('get_load_status - Success and Error Cases (requires tabId from overlapping tools)', () => { - it('tests that load status of active tab is successfully checked', async () => { - await withMcpServer(async (client) => { - const activeResult = await client.callTool({ - name: 'browser_get_active_tab', - arguments: {}, - }) - - assert.ok(!activeResult.isError, 'Get active tab should succeed') - - const activeText = activeResult.content.find((c) => c.type === 'text') - const tabIdMatch = activeText.text.match(/Tab ID: (\d+)/) - assert.ok(tabIdMatch, 'Should extract tab ID') - const tabId = parseInt(tabIdMatch[1], 10) - - const statusResult = await client.callTool({ - name: 'get_load_status', - arguments: { tabId }, - }) - - assert.ok(!statusResult.isError, 'Should succeed') - assert.ok( - Array.isArray(statusResult.content), - 'Content should be array', - ) - - const statusText = statusResult.content.find((c) => c.type === 'text') - assert.ok(statusText, 'Should have text content') - assert.ok( - statusText.text.includes('load status:'), - 'Should include status header', - ) - assert.ok( - statusText.text.includes('Resources Loading:'), - 'Should include resources loading status', - ) - assert.ok( - statusText.text.includes('DOM Content Loaded:'), - 'Should include DOM loaded status', - ) - assert.ok( - statusText.text.includes('Page Complete:'), - 'Should include page complete status', - ) - }) - }, 30000) - - it('tests that checking load status of invalid tab ID is handled', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'get_load_status', - arguments: { tabId: 999999999 }, - }) - - assert.ok(result, 'Should return a result') - assert.ok(Array.isArray(result.content), 'Should have content array') - - if (result.isError) { - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Error should include text content') - } - }) - }, 30000) - }) - - describe('list_tab_groups - Success Cases', () => { - it('tests that tab groups are successfully listed', async () => { - await withMcpServer(async (client) => { - const result = await client.callTool({ - name: 'list_tab_groups', - arguments: {}, - }) - - assert.ok(!result.isError, 'Should succeed') - assert.ok(Array.isArray(result.content), 'Content should be an array') - - const textContent = result.content.find((c) => c.type === 'text') - assert.ok(textContent, 'Should include text content') - - assert.ok(result.structuredContent, 'Should have structuredContent') - assert.ok( - Array.isArray(result.structuredContent.groups), - 'structuredContent.groups should be an array', - ) - assert.ok( - typeof result.structuredContent.count === 'number', - 'structuredContent.count should be a number', - ) - }) - }, 30000) - }) - - describe.skip('group_tabs - Success Cases (requires tabId from overlapping tools)', () => { - it('tests that tabs can be grouped together', async () => { - await withMcpServer(async (client) => { - const tab1Result = await client.callTool({ - name: 'browser_open_tab', - arguments: { url: 'https://example.com/', active: false }, - }) - assert.ok(!tab1Result.isError, 'Open tab 1 should succeed') - const tab1Text = tab1Result.content.find((c) => c.type === 'text') - const tab1Match = tab1Text.text.match(/Tab ID: (\d+)/) - const tabId1 = parseInt(tab1Match[1], 10) - - const tab2Result = await client.callTool({ - name: 'browser_open_tab', - arguments: { url: 'https://example.org/', active: false }, - }) - assert.ok(!tab2Result.isError, 'Open tab 2 should succeed') - const tab2Text = tab2Result.content.find((c) => c.type === 'text') - const tab2Match = tab2Text.text.match(/Tab ID: (\d+)/) - const tabId2 = parseInt(tab2Match[1], 10) - - const groupResult = await client.callTool({ - name: 'group_tabs', - arguments: { - tabIds: [tabId1, tabId2], - title: 'Test Group', - color: 'blue', - }, - }) - - assert.ok(!groupResult.isError, 'Group should succeed') - const groupText = groupResult.content.find((c) => c.type === 'text') - assert.ok(groupText, 'Should have text content') - assert.ok(groupText.text.includes('Grouped'), 'Should confirm grouping') - assert.ok( - groupText.text.includes('Test Group'), - 'Should include group title', - ) - - assert.ok( - groupResult.structuredContent, - 'Should have structuredContent', - ) - assert.ok( - typeof groupResult.structuredContent.groupId === 'number', - 'Should have groupId', - ) - - await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId: tabId1 }, - }) - await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId: tabId2 }, - }) - }) - }, 30000) - }) - - describe.skip('update_tab_group - Success Cases (requires tabId from overlapping tools)', () => { - it('tests that a tab group can be updated', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_open_tab', - arguments: { url: 'https://example.com/', active: false }, - }) - assert.ok(!tabResult.isError, 'Open tab should succeed') - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabMatch = tabText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabMatch[1], 10) - - const groupResult = await client.callTool({ - name: 'group_tabs', - arguments: { - tabIds: [tabId], - title: 'Original Title', - color: 'grey', - }, - }) - assert.ok(!groupResult.isError, 'Group should succeed') - const groupId = groupResult.structuredContent.groupId - - const updateResult = await client.callTool({ - name: 'update_tab_group', - arguments: { - groupId, - title: 'Updated Title', - color: 'green', - }, - }) - - assert.ok(!updateResult.isError, 'Update should succeed') - const updateText = updateResult.content.find((c) => c.type === 'text') - assert.ok(updateText, 'Should have text content') - assert.ok( - updateText.text.includes('Updated group'), - 'Should confirm update', - ) - assert.ok( - updateText.text.includes('Updated Title'), - 'Should include new title', - ) - assert.ok(updateText.text.includes('green'), 'Should include new color') - - await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId }, - }) - }) - }, 30000) - }) - - describe.skip('ungroup_tabs - Success Cases (requires tabId from overlapping tools)', () => { - it('tests that tabs can be ungrouped', async () => { - await withMcpServer(async (client) => { - const tabResult = await client.callTool({ - name: 'browser_open_tab', - arguments: { url: 'https://example.com/', active: false }, - }) - assert.ok(!tabResult.isError, 'Open tab should succeed') - const tabText = tabResult.content.find((c) => c.type === 'text') - const tabMatch = tabText.text.match(/Tab ID: (\d+)/) - const tabId = parseInt(tabMatch[1], 10) - - const groupResult = await client.callTool({ - name: 'group_tabs', - arguments: { - tabIds: [tabId], - title: 'Temp Group', - }, - }) - assert.ok(!groupResult.isError, 'Group should succeed') - - const ungroupResult = await client.callTool({ - name: 'ungroup_tabs', - arguments: { tabIds: [tabId] }, - }) - - assert.ok(!ungroupResult.isError, 'Ungroup should succeed') - const ungroupText = ungroupResult.content.find((c) => c.type === 'text') - assert.ok(ungroupText, 'Should have text content') - assert.ok( - ungroupText.text.includes('Ungrouped'), - 'Should confirm ungrouping', - ) - - await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId }, - }) - }) - }, 30000) - }) - - describe.skip('Tab Group Workflow (requires tabId from overlapping tools)', () => { - it('tests complete tab group lifecycle: create, list, update, ungroup', async () => { - await withMcpServer(async (client) => { - const tab1Result = await client.callTool({ - name: 'browser_open_tab', - arguments: { url: 'https://example.com/', active: false }, - }) - const tab1Text = tab1Result.content.find((c) => c.type === 'text') - const tabId1 = parseInt(tab1Text.text.match(/Tab ID: (\d+)/)[1], 10) - - const tab2Result = await client.callTool({ - name: 'browser_open_tab', - arguments: { url: 'https://example.org/', active: false }, - }) - const tab2Text = tab2Result.content.find((c) => c.type === 'text') - const tabId2 = parseInt(tab2Text.text.match(/Tab ID: (\d+)/)[1], 10) - - const groupResult = await client.callTool({ - name: 'group_tabs', - arguments: { - tabIds: [tabId1, tabId2], - title: 'Workflow Group', - color: 'purple', - }, - }) - assert.ok(!groupResult.isError, 'Group should succeed') - const groupId = groupResult.structuredContent.groupId - - const listResult = await client.callTool({ - name: 'list_tab_groups', - arguments: {}, - }) - assert.ok(!listResult.isError, 'List should succeed') - const groups = listResult.structuredContent.groups - const ourGroup = groups.find((g) => g.id === groupId) - assert.ok(ourGroup, 'Our group should be in the list') - assert.strictEqual( - ourGroup.title, - 'Workflow Group', - 'Title should match', - ) - assert.strictEqual(ourGroup.color, 'purple', 'Color should match') - - const updateResult = await client.callTool({ - name: 'update_tab_group', - arguments: { - groupId, - title: 'Renamed Group', - color: 'cyan', - }, - }) - assert.ok(!updateResult.isError, 'Update should succeed') - - const ungroupResult = await client.callTool({ - name: 'ungroup_tabs', - arguments: { tabIds: [tabId1, tabId2] }, - }) - assert.ok(!ungroupResult.isError, 'Ungroup should succeed') - - await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId: tabId1 }, - }) - await client.callTool({ - name: 'browser_close_tab', - arguments: { tabId: tabId2 }, - }) - }) - }, 60000) - }) -}) diff --git a/apps/server/tests/tools/history.test.ts b/apps/server/tests/tools/history.test.ts new file mode 100644 index 00000000..81c5a460 --- /dev/null +++ b/apps/server/tests/tools/history.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from 'bun:test' +import assert from 'node:assert' +import { + delete_history_range, + delete_history_url, + get_recent_history, + search_history, +} from '../../src/tools/history' +import { close_page, new_page } from '../../src/tools/navigation' +import { withBrowser } from '../__helpers__/with-browser' + +function textOf(result: { + content: { type: string; text?: string }[] +}): string { + return result.content + .filter((c) => c.type === 'text') + .map((c) => c.text) + .join('\n') +} + +describe('history tools', () => { + it('get_recent_history returns items', async () => { + await withBrowser(async ({ execute }) => { + // Navigate somewhere to ensure history exists + const newResult = await execute(new_page, { url: 'https://example.com' }) + const pageId = Number(textOf(newResult).match(/Page ID:\s*(\d+)/)?.[1]) + + const result = await execute(get_recent_history, { maxResults: 10 }) + assert.ok(!result.isError, textOf(result)) + const text = textOf(result) + assert.ok( + text.includes('history') || text.includes('Retrieved'), + 'Expected history response', + ) + + await execute(close_page, { page: pageId }) + }) + }, 60_000) + + it('search_history searches by query', async () => { + await withBrowser(async ({ execute }) => { + const result = await execute(search_history, { + query: 'example', + maxResults: 10, + }) + assert.ok(!result.isError, textOf(result)) + }) + }, 60_000) + + it('delete_history_url removes a URL', async () => { + await withBrowser(async ({ execute }) => { + const result = await execute(delete_history_url, { + url: 'https://example.com/nonexistent-test-url', + }) + assert.ok(!result.isError, textOf(result)) + assert.ok(textOf(result).includes('Deleted')) + }) + }, 60_000) + + it('delete_history_range deletes a time range', async () => { + await withBrowser(async ({ execute }) => { + const now = Date.now() + const result = await execute(delete_history_range, { + startTime: now - 1000, + endTime: now, + }) + assert.ok(!result.isError, textOf(result)) + assert.ok(textOf(result).includes('Deleted history from')) + }) + }, 60_000) +}) diff --git a/apps/server/tests/tools/input.test.ts b/apps/server/tests/tools/input.test.ts new file mode 100644 index 00000000..17b2a1aa --- /dev/null +++ b/apps/server/tests/tools/input.test.ts @@ -0,0 +1,252 @@ +import { describe, it } from 'bun:test' +import assert from 'node:assert' +import { + check, + click, + fill, + hover, + press_key, + scroll, + select_option, + uncheck, +} from '../../src/tools/input' +import { close_page, new_page } from '../../src/tools/navigation' +import { evaluate_script, take_snapshot } from '../../src/tools/snapshot' +import { withBrowser } from '../__helpers__/with-browser' + +function textOf(result: { + content: { type: string; text?: string }[] +}): string { + return result.content + .filter((c) => c.type === 'text') + .map((c) => c.text) + .join('\n') +} + +function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +function findElementId(snapshotText: string, label: string): number { + const regex = new RegExp(`\\[(\\d+)\\].*?${escapeRegex(label)}`) + const match = snapshotText.match(regex) + if (!match) throw new Error(`Element "${label}" not found in snapshot`) + return Number.parseInt(match[1], 10) +} + +const FORM_PAGE = `data:text/html,${encodeURIComponent(` + +

Test Form

+ + + + + +
+
+
Bottom of page
+ +`)}` + +describe('input tools', () => { + it('fill types text into an input', async () => { + await withBrowser(async ({ execute }) => { + const newResult = await execute(new_page, { url: FORM_PAGE }) + const pageId = Number(textOf(newResult).match(/Page ID:\s*(\d+)/)?.[1]) + + const snap = await execute(take_snapshot, { page: pageId }) + const snapText = textOf(snap) + const inputId = findElementId(snapText, 'Enter name') + + const fillResult = await execute(fill, { + page: pageId, + element: inputId, + text: 'John Doe', + }) + assert.ok(!fillResult.isError, textOf(fillResult)) + + const val = await execute(evaluate_script, { + page: pageId, + expression: 'document.getElementById("name").value', + }) + assert.strictEqual(textOf(val), 'John Doe') + + await execute(close_page, { page: pageId }) + }) + }, 60_000) + + it('click triggers a button', async () => { + await withBrowser(async ({ execute }) => { + const newResult = await execute(new_page, { url: FORM_PAGE }) + const pageId = Number(textOf(newResult).match(/Page ID:\s*(\d+)/)?.[1]) + + // Fill the input first + const snap = await execute(take_snapshot, { page: pageId }) + const snapText = textOf(snap) + const inputId = findElementId(snapText, 'Enter name') + await execute(fill, { page: pageId, element: inputId, text: 'Alice' }) + + // Click submit + const btnId = findElementId(snapText, 'Submit') + const clickResult = await execute(click, { + page: pageId, + element: btnId, + }) + assert.ok(!clickResult.isError, textOf(clickResult)) + + const output = await execute(evaluate_script, { + page: pageId, + expression: 'document.getElementById("output").textContent', + }) + assert.strictEqual(textOf(output), 'clicked:Alice') + + await execute(close_page, { page: pageId }) + }) + }, 60_000) + + it('check and uncheck toggle a checkbox', async () => { + await withBrowser(async ({ execute }) => { + const newResult = await execute(new_page, { url: FORM_PAGE }) + const pageId = Number(textOf(newResult).match(/Page ID:\s*(\d+)/)?.[1]) + + const snap = await execute(take_snapshot, { page: pageId }) + const snapText = textOf(snap) + const checkboxId = findElementId(snapText, 'I agree') + + const checkResult = await execute(check, { + page: pageId, + element: checkboxId, + }) + assert.ok(!checkResult.isError, textOf(checkResult)) + + const checked = await execute(evaluate_script, { + page: pageId, + expression: 'document.getElementById("agree").checked', + }) + assert.strictEqual(textOf(checked), 'true') + + const uncheckResult = await execute(uncheck, { + page: pageId, + element: checkboxId, + }) + assert.ok(!uncheckResult.isError, textOf(uncheckResult)) + + const unchecked = await execute(evaluate_script, { + page: pageId, + expression: 'document.getElementById("agree").checked', + }) + assert.strictEqual(textOf(unchecked), 'false') + + await execute(close_page, { page: pageId }) + }) + }, 60_000) + + it('select_option selects a dropdown value', async () => { + await withBrowser(async ({ execute }) => { + const newResult = await execute(new_page, { url: FORM_PAGE }) + const pageId = Number(textOf(newResult).match(/Page ID:\s*(\d+)/)?.[1]) + + // Use evaluate_script to get the select element's backendNodeId directly + const nodeId = await execute(evaluate_script, { + page: pageId, + expression: + '(() => { const el = document.getElementById("color"); return el ? el.getAttribute("id") : null })()', + }) + assert.strictEqual(textOf(nodeId), 'color') + + // Get the select element ID from the snapshot + const snap = await execute(take_snapshot, { page: pageId }) + const snapText = textOf(snap) + + // Find the combobox/listbox element (the