add sse test to make sure responses are not buffered
This commit is contained in:
+116
-4
@@ -12,13 +12,15 @@ import { getCDPSessionForPage } from './cdp-session.js'
|
|||||||
import { Debugger } from './debugger.js'
|
import { Debugger } from './debugger.js'
|
||||||
import { Editor } from './editor.js'
|
import { Editor } from './editor.js'
|
||||||
import type { CDPCommand } from './cdp-types.js'
|
import type { CDPCommand } from './cdp-types.js'
|
||||||
import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, type TestContext } from './test-utils.js'
|
import { setupTestContext, cleanupTestContext, getExtensionServiceWorker, createSseServer, type TestContext, withTimeout } from './test-utils.js'
|
||||||
|
import { screenshotWithAccessibilityLabels } from './aria-snapshot.js'
|
||||||
|
|
||||||
declare const window: any
|
declare const window: any
|
||||||
declare const document: any
|
declare const document: any
|
||||||
|
|
||||||
const TEST_PORT = 19987
|
const TEST_PORT = 19987
|
||||||
|
|
||||||
|
|
||||||
function js(strings: TemplateStringsArray, ...values: any[]): string {
|
function js(strings: TemplateStringsArray, ...values: any[]): string {
|
||||||
return strings.reduce(
|
return strings.reduce(
|
||||||
(result, str, i) => result + str + (values[i] || ''),
|
(result, str, i) => result + str + (values[i] || ''),
|
||||||
@@ -26,6 +28,7 @@ function js(strings: TemplateStringsArray, ...values: any[]): string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var toggleExtensionForActiveTab: () => Promise<{ isConnected: boolean; state: ExtensionState }>;
|
var toggleExtensionForActiveTab: () => Promise<{ isConnected: boolean; state: ExtensionState }>;
|
||||||
var getExtensionState: () => ExtensionState;
|
var getExtensionState: () => ExtensionState;
|
||||||
@@ -2347,7 +2350,7 @@ describe('MCP Server Tests', () => {
|
|||||||
// Verify the image is valid JPEG by checking base64
|
// Verify the image is valid JPEG by checking base64
|
||||||
const buffer = Buffer.from(imageContent.data, 'base64')
|
const buffer = Buffer.from(imageContent.data, 'base64')
|
||||||
const dimensions = imageSize(buffer)
|
const dimensions = imageSize(buffer)
|
||||||
|
|
||||||
// Get actual viewport size from page
|
// Get actual viewport size from page
|
||||||
const viewport = await page.evaluate(() => ({
|
const viewport = await page.evaluate(() => ({
|
||||||
innerWidth: window.innerWidth,
|
innerWidth: window.innerWidth,
|
||||||
@@ -2357,7 +2360,7 @@ describe('MCP Server Tests', () => {
|
|||||||
}))
|
}))
|
||||||
console.log('Screenshot dimensions:', dimensions.width, 'x', dimensions.height)
|
console.log('Screenshot dimensions:', dimensions.width, 'x', dimensions.height)
|
||||||
console.log('Window viewport:', viewport)
|
console.log('Window viewport:', viewport)
|
||||||
|
|
||||||
expect(dimensions.type).toBe('jpg')
|
expect(dimensions.type).toBe('jpg')
|
||||||
expect(dimensions.width).toBeGreaterThan(0)
|
expect(dimensions.width).toBeGreaterThan(0)
|
||||||
expect(dimensions.height).toBeGreaterThan(0)
|
expect(dimensions.height).toBeGreaterThan(0)
|
||||||
@@ -2365,6 +2368,7 @@ describe('MCP Server Tests', () => {
|
|||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|
||||||
|
|
||||||
it('should get clean HTML with getCleanHTML', async () => {
|
it('should get clean HTML with getCleanHTML', async () => {
|
||||||
const browserContext = getBrowserContext()
|
const browserContext = getBrowserContext()
|
||||||
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
const serviceWorker = await getExtensionServiceWorker(browserContext)
|
||||||
@@ -2573,7 +2577,7 @@ describe('MCP Server Tests', () => {
|
|||||||
const listJson = await listRes.json() as Array<{ url?: string }>
|
const listJson = await listRes.json() as Array<{ url?: string }>
|
||||||
expect(Array.isArray(listJson)).toBe(true)
|
expect(Array.isArray(listJson)).toBe(true)
|
||||||
expect(listJson.length).toBeGreaterThan(0)
|
expect(listJson.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
// Find the example.com page (there may be other pages like about:blank)
|
// Find the example.com page (there may be other pages like about:blank)
|
||||||
const examplePage = listJson.find((t) => t.url?.includes('example.com'))
|
const examplePage = listJson.find((t) => t.url?.includes('example.com'))
|
||||||
expect(examplePage).toBeDefined()
|
expect(examplePage).toBeDefined()
|
||||||
@@ -3651,6 +3655,114 @@ describe('Service Worker Target Tests', () => {
|
|||||||
await browser.close()
|
await browser.close()
|
||||||
await page.close()
|
await page.close()
|
||||||
}, 60000)
|
}, 60000)
|
||||||
|
|
||||||
|
it('should stream SSE without waiting for response end', async () => {
|
||||||
|
const browserContext = getBrowserContext()
|
||||||
|
const serviceWorker = await withTimeout({
|
||||||
|
promise: getExtensionServiceWorker(browserContext),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'getExtensionServiceWorker timed out',
|
||||||
|
})
|
||||||
|
const sseServer = await withTimeout({
|
||||||
|
promise: createSseServer(),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'createSseServer timed out',
|
||||||
|
})
|
||||||
|
let page: Awaited<ReturnType<typeof browserContext.newPage>> | null = null
|
||||||
|
let browser: Awaited<ReturnType<typeof chromium.connectOverCDP>> | null = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
page = await withTimeout({
|
||||||
|
promise: browserContext.newPage(),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'newPage timed out',
|
||||||
|
})
|
||||||
|
await withTimeout({
|
||||||
|
promise: page.goto(`${sseServer.baseUrl}/`),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'page.goto timed out',
|
||||||
|
})
|
||||||
|
await page.bringToFront()
|
||||||
|
|
||||||
|
await withTimeout({
|
||||||
|
promise: serviceWorker.evaluate(async () => {
|
||||||
|
await globalThis.toggleExtensionForActiveTab()
|
||||||
|
}),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'toggleExtensionForActiveTab timed out',
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 100)
|
||||||
|
})
|
||||||
|
|
||||||
|
browser = await withTimeout({
|
||||||
|
promise: chromium.connectOverCDP(getCdpUrl({ port: TEST_PORT })),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'connectOverCDP timed out',
|
||||||
|
})
|
||||||
|
const cdpPage = browser.contexts()[0].pages().find((p) => {
|
||||||
|
return p.url().startsWith(sseServer.baseUrl)
|
||||||
|
})
|
||||||
|
expect(cdpPage).toBeDefined()
|
||||||
|
|
||||||
|
await cdpPage!.evaluate(() => {
|
||||||
|
return window.startSse()
|
||||||
|
})
|
||||||
|
await withTimeout({
|
||||||
|
promise: cdpPage!.waitForFunction(() => {
|
||||||
|
return window.__sseMessages.length > 0
|
||||||
|
}, { timeout: 5000 }),
|
||||||
|
timeoutMs: 7000,
|
||||||
|
errorMessage: 'SSE message not received in time',
|
||||||
|
})
|
||||||
|
|
||||||
|
const firstMessage = await cdpPage!.evaluate(() => {
|
||||||
|
return window.__sseMessages[0]
|
||||||
|
})
|
||||||
|
expect(firstMessage).toBe('hello')
|
||||||
|
|
||||||
|
const sseState = sseServer.getState()
|
||||||
|
expect(sseState.connected).toBe(true)
|
||||||
|
expect(sseState.finished).toBe(false)
|
||||||
|
expect(sseState.closed).toBe(false)
|
||||||
|
expect(sseState.writeCount).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const readyState = await cdpPage!.evaluate(() => {
|
||||||
|
if (!window.__sseSource) {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return window.__sseSource.readyState
|
||||||
|
})
|
||||||
|
expect(readyState).toBe(1)
|
||||||
|
|
||||||
|
await cdpPage!.evaluate(() => {
|
||||||
|
window.stopSse()
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
setTimeout(resolve, 100)
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
if (browser) {
|
||||||
|
await withTimeout({
|
||||||
|
promise: browser.close(),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'browser.close timed out',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (page) {
|
||||||
|
await withTimeout({
|
||||||
|
promise: page.close(),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'page.close timed out',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await withTimeout({
|
||||||
|
promise: sseServer.close(),
|
||||||
|
timeoutMs: 5000,
|
||||||
|
errorMessage: 'sseServer.close timed out',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 60000)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Auto-enable Tests', () => {
|
describe('Auto-enable Tests', () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { exec } from 'node:child_process'
|
import { exec } from 'node:child_process'
|
||||||
import { promisify } from 'node:util'
|
import { promisify } from 'node:util'
|
||||||
|
import http from 'node:http'
|
||||||
|
import net from 'node:net'
|
||||||
import { chromium, BrowserContext } from 'playwright-core'
|
import { chromium, BrowserContext } from 'playwright-core'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs'
|
||||||
@@ -101,3 +103,167 @@ export async function cleanupTestContext(ctx: TestContext | null, cleanup?: (()
|
|||||||
await cleanup()
|
await cleanup()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SseServerState = {
|
||||||
|
connected: boolean
|
||||||
|
finished: boolean
|
||||||
|
writeCount: number
|
||||||
|
closed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SseServer = {
|
||||||
|
baseUrl: string
|
||||||
|
getState: () => SseServerState
|
||||||
|
close: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSseServer(): Promise<SseServer> {
|
||||||
|
let sseResponse: http.ServerResponse | null = null
|
||||||
|
let sseFinished = false
|
||||||
|
let sseClosed = false
|
||||||
|
let sseWriteCount = 0
|
||||||
|
let sseInterval: NodeJS.Timeout | null = null
|
||||||
|
const openResponses: Set<http.ServerResponse> = new Set()
|
||||||
|
const openSockets: Set<net.Socket> = new Set()
|
||||||
|
|
||||||
|
const server = http.createServer((req, res) => {
|
||||||
|
if (req.url === '/') {
|
||||||
|
res.writeHead(200, { 'Content-Type': 'text/html' })
|
||||||
|
res.end(`<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>SSE Test</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script>
|
||||||
|
window.__sseMessages = [];
|
||||||
|
window.__sseOpen = false;
|
||||||
|
window.__sseError = null;
|
||||||
|
window.startSse = function () {
|
||||||
|
const source = new EventSource('/sse');
|
||||||
|
window.__sseSource = source;
|
||||||
|
source.onopen = function () {
|
||||||
|
window.__sseOpen = true;
|
||||||
|
};
|
||||||
|
source.onmessage = function (event) {
|
||||||
|
window.__sseMessages.push(event.data);
|
||||||
|
};
|
||||||
|
source.onerror = function () {
|
||||||
|
window.__sseError = 'SSE error';
|
||||||
|
};
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
window.stopSse = function () {
|
||||||
|
if (window.__sseSource) {
|
||||||
|
window.__sseSource.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === '/sse') {
|
||||||
|
res.writeHead(200, {
|
||||||
|
'Content-Type': 'text/event-stream',
|
||||||
|
'Cache-Control': 'no-cache, no-transform',
|
||||||
|
'Connection': 'keep-alive',
|
||||||
|
})
|
||||||
|
res.write('retry: 1000\n\n')
|
||||||
|
res.write('data: hello\n\n')
|
||||||
|
sseResponse = res
|
||||||
|
sseWriteCount += 1
|
||||||
|
openResponses.add(res)
|
||||||
|
|
||||||
|
res.on('finish', () => {
|
||||||
|
sseFinished = true
|
||||||
|
})
|
||||||
|
res.on('close', () => {
|
||||||
|
sseClosed = true
|
||||||
|
openResponses.delete(res)
|
||||||
|
if (sseInterval) {
|
||||||
|
clearInterval(sseInterval)
|
||||||
|
sseInterval = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
sseInterval = setInterval(() => {
|
||||||
|
res.write('data: ping\n\n')
|
||||||
|
sseWriteCount += 1
|
||||||
|
}, 200)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404)
|
||||||
|
res.end('Not found')
|
||||||
|
})
|
||||||
|
|
||||||
|
server.on('connection', (socket) => {
|
||||||
|
openSockets.add(socket)
|
||||||
|
socket.on('close', () => {
|
||||||
|
openSockets.delete(socket)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
server.listen(0, '127.0.0.1', () => {
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const address = server.address()
|
||||||
|
if (!address || typeof address === 'string') {
|
||||||
|
throw new Error('Failed to bind SSE server')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
baseUrl: `http://127.0.0.1:${address.port}`,
|
||||||
|
getState: () => ({
|
||||||
|
connected: sseResponse !== null,
|
||||||
|
finished: sseFinished,
|
||||||
|
closed: sseClosed,
|
||||||
|
writeCount: sseWriteCount,
|
||||||
|
}),
|
||||||
|
close: async () => {
|
||||||
|
for (const response of openResponses) {
|
||||||
|
response.destroy()
|
||||||
|
}
|
||||||
|
for (const socket of openSockets) {
|
||||||
|
socket.destroy()
|
||||||
|
}
|
||||||
|
if (sseInterval) {
|
||||||
|
clearInterval(sseInterval)
|
||||||
|
sseInterval = null
|
||||||
|
}
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
server.close((error) => {
|
||||||
|
if (error) {
|
||||||
|
reject(error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function withTimeout<T>({ promise, timeoutMs, errorMessage }: { promise: Promise<T>; timeoutMs: number; errorMessage: string }): Promise<T> {
|
||||||
|
return await new Promise<T>((resolve, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
reject(new Error(errorMessage))
|
||||||
|
}, timeoutMs)
|
||||||
|
|
||||||
|
promise
|
||||||
|
.then((value) => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
resolve(value)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
clearTimeout(timeoutId)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user