better test. spawn the extension with playwright
This commit is contained in:
+4
-1
@@ -18,5 +18,8 @@
|
||||
},
|
||||
"repository": "https://github.com/remorses/",
|
||||
"author": "remorses <beats.by.morse@gmail.com>",
|
||||
"license": ""
|
||||
"license": "",
|
||||
"dependencies": {
|
||||
"mcp-extension": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"watch": "tsc -w",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"mcp": "vite-node src/mcp.ts",
|
||||
"test": "vitest run",
|
||||
"test": "vitest run -u",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"bin": "./bin.js",
|
||||
|
||||
+186
-1
@@ -2,6 +2,12 @@ import { createMCPClient } from './mcp-client.js'
|
||||
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
|
||||
import { exec } from 'node:child_process'
|
||||
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)
|
||||
|
||||
@@ -29,15 +35,94 @@ async function killProcessOnPort(port: number): Promise<void> {
|
||||
describe('MCP Server Tests', () => {
|
||||
let client: Awaited<ReturnType<typeof createMCPClient>>['client']
|
||||
let cleanup: (() => Promise<void>) | null = null
|
||||
let browserContext: Awaited<ReturnType<typeof chromium.launchPersistentContext>> | null = null
|
||||
let userDataDir: string
|
||||
let relayServerProcess: any
|
||||
|
||||
beforeAll(async () => {
|
||||
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()
|
||||
client = result.client
|
||||
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 () => {
|
||||
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) {
|
||||
await cleanup()
|
||||
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) {
|
||||
try {
|
||||
|
||||
Generated
+4
@@ -7,6 +7,10 @@ settings:
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
mcp-extension:
|
||||
specifier: workspace:*
|
||||
version: link:extension
|
||||
devDependencies:
|
||||
'@changesets/cli':
|
||||
specifier: ^2.29.7
|
||||
|
||||
Reference in New Issue
Block a user