/** * Tests for the PagePool module. * * Uses mock BrowserContext and Page objects to verify pooling, * borrowing, returning, resetting, and disposal behavior. */ import { describe, it } from 'node:test' import assert from 'node:assert' import type { Page, BrowserContext } from 'playwright' import { PagePool, PageEntry, ContextPool } from './page-pool.js' function createMockContext(): BrowserContext & Record { const context: any = { newPageCalls: 0, clearCookiesCalls: 0, closeCalls: 0, createdPages: [], newPage: async () => { context.newPageCalls++ const page = createMockPage(context) context.createdPages.push(page) return page }, clearCookies: async () => { context.clearCookiesCalls++ }, close: async () => { context.closeCalls++ }, } return context as BrowserContext & Record } function createMockPage(context?: any): Page & Record { const page: any = { gotoCalls: [], evaluateCalls: 0, setViewportSizeCalls: [], closeCalls: 0, goto: async (url: string) => { page.gotoCalls.push(url) }, context: () => context, evaluate: async () => { page.evaluateCalls++ }, setViewportSize: async (size: any) => { page.setViewportSizeCalls.push(size) }, close: async () => { page.closeCalls++ }, addInitScript: async () => {}, } return page as Page & Record } describe('PagePool', () => { it('warm creates specified number of pages', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool) await pool.warm(context, 3) assert.strictEqual(context.newPageCalls, 3) const entries: PageEntry[] = (pool as any).pools.get(context) ?? [] assert.strictEqual(entries.length, 3) assert.strictEqual(entries.every((e) => !e.inUse), true) }) it('borrow returns warmed page', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool) await pool.warm(context, 1) const entriesBefore: PageEntry[] = (pool as any).pools.get(context) ?? [] const warmedPage = entriesBefore[0].imhotepPage const borrowed = await pool.borrow(context) assert.strictEqual(borrowed, warmedPage) const entriesAfter: PageEntry[] = (pool as any).pools.get(context) ?? [] assert.strictEqual(entriesAfter[0].inUse, true) }) it('borrow creates new page when none available', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool) const borrowed = await pool.borrow(context) assert.strictEqual(context.newPageCalls, 1) assert.ok(borrowed) const entries: PageEntry[] = (pool as any).pools.get(context) ?? [] assert.strictEqual(entries.length, 1) assert.strictEqual(entries[0].inUse, true) }) it('borrow throws when at maxPagesPerContext and all in use', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool, { maxPagesPerContext: 2 }) await pool.borrow(context) await pool.borrow(context) await assert.rejects( async () => pool.borrow(context), /Max pages per context \(2\) reached and all pages are in use/ ) }) it('return resets page and marks available', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool) const borrowed = await pool.borrow(context) const mockPage = context.createdPages[0] // Reset was called once during borrow assert.strictEqual(mockPage.evaluateCalls, 1) await pool.return(borrowed) // Return called reset again assert.strictEqual(mockPage.evaluateCalls, 2) const entries: PageEntry[] = (pool as any).pools.get(context) ?? [] assert.strictEqual(entries[0].inUse, false) }) it('reset clears cookies and localStorage', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool) await pool.borrow(context) const mockPage = context.createdPages[0] assert.strictEqual(mockPage.gotoCalls.includes('about:blank'), true) assert.strictEqual(context.clearCookiesCalls, 1) assert.strictEqual(mockPage.evaluateCalls, 1) assert.deepStrictEqual(mockPage.setViewportSizeCalls, [{ width: 1280, height: 720 }]) }) it('dispose closes all pages', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool) await pool.warm(context, 2) await pool.dispose() assert.strictEqual(context.createdPages[0].closeCalls, 1) assert.strictEqual(context.createdPages[1].closeCalls, 1) }) it('idempotent dispose', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool) await pool.warm(context, 1) await pool.dispose() await pool.dispose() assert.strictEqual(context.createdPages[0].closeCalls, 1) }) it('borrowed page is inUse', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool, { maxPagesPerContext: 1 }) await pool.warm(context, 1) await pool.borrow(context) await assert.rejects( async () => pool.borrow(context), /Max pages per context \(1\) reached and all pages are in use/ ) }) it('returned page is not inUse', async () => { const context = createMockContext() const pool = new PagePool({} as ContextPool, { maxPagesPerContext: 1 }) await pool.warm(context, 1) const first = await pool.borrow(context) await pool.return(first) const second = await pool.borrow(context) assert.strictEqual(context.newPageCalls, 1) assert.strictEqual(second, first) }) })