better test. spawn the extension with playwright
This commit is contained in:
+4
-1
@@ -18,5 +18,8 @@
|
|||||||
},
|
},
|
||||||
"repository": "https://github.com/remorses/",
|
"repository": "https://github.com/remorses/",
|
||||||
"author": "remorses <beats.by.morse@gmail.com>",
|
"author": "remorses <beats.by.morse@gmail.com>",
|
||||||
"license": ""
|
"license": "",
|
||||||
|
"dependencies": {
|
||||||
|
"mcp-extension": "workspace:*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"mcp": "vite-node src/mcp.ts",
|
"mcp": "vite-node src/mcp.ts",
|
||||||
"test": "vitest run",
|
"test": "vitest run -u",
|
||||||
"test:watch": "vitest"
|
"test:watch": "vitest"
|
||||||
},
|
},
|
||||||
"bin": "./bin.js",
|
"bin": "./bin.js",
|
||||||
|
|||||||
+186
-1
@@ -2,6 +2,12 @@ import { createMCPClient } from './mcp-client.js'
|
|||||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||||
import { exec } from 'node:child_process'
|
import { exec } from 'node:child_process'
|
||||||
import { promisify } from 'node:util'
|
import { promisify } from 'node:util'
|
||||||
|
import { chromium } from 'playwright-core'
|
||||||
|
import path from 'node:path'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
|
|
||||||
|
import { spawn } from 'node:child_process'
|
||||||
|
|
||||||
const execAsync = promisify(exec)
|
const execAsync = promisify(exec)
|
||||||
|
|
||||||
@@ -29,15 +35,94 @@ async function killProcessOnPort(port: number): Promise<void> {
|
|||||||
describe('MCP Server Tests', () => {
|
describe('MCP Server Tests', () => {
|
||||||
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
||||||
let cleanup: (() => Promise<void>) | null = null
|
let cleanup: (() => Promise<void>) | null = null
|
||||||
|
let browserContext: Awaited<ReturnType<typeof chromium.launchPersistentContext>> | null = null
|
||||||
|
let userDataDir: string
|
||||||
|
let relayServerProcess: any
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await killProcessOnPort(19988)
|
await killProcessOnPort(19988)
|
||||||
|
|
||||||
|
// Build extension
|
||||||
|
console.log('Building extension...')
|
||||||
|
await execAsync('pnpm build', { cwd: '../extension' })
|
||||||
|
console.log('Extension built')
|
||||||
|
|
||||||
|
// Start Relay Server manually
|
||||||
|
relayServerProcess = spawn('pnpm', ['tsx', 'src/start-relay-server.ts'], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
stdio: 'inherit'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for port 19988 to be ready
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
let retries = 0
|
||||||
|
const interval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync('lsof -ti:19988')
|
||||||
|
if (stdout.trim()) {
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
retries++
|
||||||
|
if (retries > 30) {
|
||||||
|
clearInterval(interval)
|
||||||
|
reject(new Error('Relay server failed to start'))
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
const result = await createMCPClient()
|
const result = await createMCPClient()
|
||||||
client = result.client
|
client = result.client
|
||||||
cleanup = result.cleanup
|
cleanup = result.cleanup
|
||||||
})
|
|
||||||
|
userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pw-test-'))
|
||||||
|
const extensionPath = path.resolve('../extension/dist')
|
||||||
|
|
||||||
|
browserContext = await chromium.launchPersistentContext(userDataDir, {
|
||||||
|
headless: false,
|
||||||
|
args: [
|
||||||
|
`--disable-extensions-except=${extensionPath}`,
|
||||||
|
`--load-extension=${extensionPath}`,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Wait for service worker and connect
|
||||||
|
let serviceWorker = browserContext.serviceWorkers()[0]
|
||||||
|
if (!serviceWorker) {
|
||||||
|
serviceWorker = await browserContext.waitForEvent('serviceworker')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a page to attach to
|
||||||
|
const page = await browserContext.newPage()
|
||||||
|
await page.goto('about:blank')
|
||||||
|
|
||||||
|
// Connect the tab
|
||||||
|
await serviceWorker.evaluate(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
await globalThis.toggleExtensionForActiveTab()
|
||||||
|
})
|
||||||
|
|
||||||
|
}, 120000) // 2 minutes timeout
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
if (browserContext) {
|
||||||
|
await browserContext.close()
|
||||||
|
}
|
||||||
|
if (relayServerProcess) {
|
||||||
|
relayServerProcess.kill()
|
||||||
|
}
|
||||||
|
await killProcessOnPort(19988)
|
||||||
|
|
||||||
|
if (userDataDir) {
|
||||||
|
try {
|
||||||
|
fs.rmSync(userDataDir, { recursive: true, force: true })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to cleanup user data dir:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (cleanup) {
|
if (cleanup) {
|
||||||
await cleanup()
|
await cleanup()
|
||||||
cleanup = null
|
cleanup = null
|
||||||
@@ -174,6 +259,106 @@ describe('MCP Server Tests', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should handle new pages and toggling', async () => {
|
||||||
|
if (!browserContext) throw new Error('Browser not initialized')
|
||||||
|
|
||||||
|
// Find the correct service worker by URL
|
||||||
|
const extensionPath = path.resolve('../extension/dist')
|
||||||
|
let serviceWorker = browserContext.serviceWorkers().find(sw => sw.url().startsWith('chrome-extension://'))
|
||||||
|
|
||||||
|
if (!serviceWorker) {
|
||||||
|
serviceWorker = await browserContext.waitForEvent('serviceworker', {
|
||||||
|
predicate: (sw) => sw.url().startsWith('chrome-extension://')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Create a new page
|
||||||
|
const page = await browserContext.newPage()
|
||||||
|
const testUrl = 'https://example.com/'
|
||||||
|
await page.goto(testUrl)
|
||||||
|
await page.bringToFront()
|
||||||
|
|
||||||
|
// 2. Enable extension on this new tab
|
||||||
|
// Since it's a new page, extension is not connected yet
|
||||||
|
const result = await serviceWorker.evaluate(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
return await globalThis.toggleExtensionForActiveTab()
|
||||||
|
})
|
||||||
|
expect(result.isConnected).toBe(true)
|
||||||
|
|
||||||
|
// 3. Verify we can connect via direct CDP and see the page
|
||||||
|
const randomId = Math.random().toString(36).substring(7)
|
||||||
|
const cdpUrl = `ws://localhost:19988/cdp/${randomId}`
|
||||||
|
let directBrowser = await chromium.connectOverCDP(cdpUrl)
|
||||||
|
let contexts = directBrowser.contexts()
|
||||||
|
let pages = contexts[0].pages()
|
||||||
|
|
||||||
|
// Find our page
|
||||||
|
let foundPage = pages.find(p => p.url() === testUrl)
|
||||||
|
expect(foundPage).toBeDefined()
|
||||||
|
expect(foundPage?.url()).toBe(testUrl)
|
||||||
|
|
||||||
|
await directBrowser.close()
|
||||||
|
|
||||||
|
// 4. Disable extension on this tab
|
||||||
|
const resultDisabled = await serviceWorker.evaluate(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
return await globalThis.toggleExtensionForActiveTab()
|
||||||
|
})
|
||||||
|
expect(resultDisabled.isConnected).toBe(false)
|
||||||
|
|
||||||
|
// 5. Try to connect/use the page.
|
||||||
|
// connecting to relay will succeed, but listing pages should NOT show our page
|
||||||
|
// OR if we try to send command to the previous targetId (via session), it should fail
|
||||||
|
|
||||||
|
// Connect to relay again
|
||||||
|
directBrowser = await chromium.connectOverCDP(cdpUrl)
|
||||||
|
contexts = directBrowser.contexts()
|
||||||
|
pages = contexts[0].pages() // this calls Target.getTargets internally via contexts() or pages()?
|
||||||
|
// Actually contexts() just returns what it knows.
|
||||||
|
// If relay sent detachedFromTarget, the page should be gone from the context.
|
||||||
|
|
||||||
|
foundPage = pages.find(p => p.url() === testUrl)
|
||||||
|
expect(foundPage).toBeUndefined()
|
||||||
|
|
||||||
|
// Also try to send a command to the old target ID if possible?
|
||||||
|
// Hard to do with high-level Playwright API on the browser object.
|
||||||
|
// We can try to use a raw CDPSession on the browser target if we knew how to route it.
|
||||||
|
// But checking it's gone is good enough for "connection disabled".
|
||||||
|
|
||||||
|
await directBrowser.close()
|
||||||
|
|
||||||
|
// 6. Re-enable extension
|
||||||
|
const resultEnabled = await serviceWorker.evaluate(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
return await globalThis.toggleExtensionForActiveTab()
|
||||||
|
})
|
||||||
|
expect(resultEnabled.isConnected).toBe(true)
|
||||||
|
|
||||||
|
// 7. Verify page is back
|
||||||
|
// Note: targetId might have changed! Fetch it again just in case, or rely on relay to update routing.
|
||||||
|
// But relay routing is based on `sessionId`. New session ID generated on attach.
|
||||||
|
// The URL path param `targetId` is just a label.
|
||||||
|
|
||||||
|
directBrowser = await chromium.connectOverCDP(cdpUrl)
|
||||||
|
// Wait a bit for targets to populate
|
||||||
|
await new Promise(r => setTimeout(r, 500))
|
||||||
|
|
||||||
|
contexts = directBrowser.contexts()
|
||||||
|
// pages() might need a moment if target attached event comes in
|
||||||
|
if (contexts[0].pages().length === 0) {
|
||||||
|
await new Promise(r => setTimeout(r, 1000))
|
||||||
|
}
|
||||||
|
pages = contexts[0].pages()
|
||||||
|
|
||||||
|
foundPage = pages.find(p => p.url() === testUrl)
|
||||||
|
expect(foundPage).toBeDefined()
|
||||||
|
expect(foundPage?.url()).toBe(testUrl)
|
||||||
|
|
||||||
|
await directBrowser.close()
|
||||||
|
await page.close()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
function tryJsonParse(str: string) {
|
function tryJsonParse(str: string) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Generated
+4
@@ -7,6 +7,10 @@ settings:
|
|||||||
importers:
|
importers:
|
||||||
|
|
||||||
.:
|
.:
|
||||||
|
dependencies:
|
||||||
|
mcp-extension:
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:extension
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@changesets/cli':
|
'@changesets/cli':
|
||||||
specifier: ^2.29.7
|
specifier: ^2.29.7
|
||||||
|
|||||||
Reference in New Issue
Block a user