269 lines
8.4 KiB
TypeScript
269 lines
8.4 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
const connect = vi.fn();
|
|
const connectOverCDP = vi.fn();
|
|
const launch = vi.fn();
|
|
|
|
const routeContinue = vi.fn();
|
|
const routeAbort = vi.fn();
|
|
const route = {
|
|
request: vi.fn(),
|
|
continue: routeContinue,
|
|
abort: routeAbort,
|
|
};
|
|
|
|
const request = {
|
|
resourceType: vi.fn(),
|
|
url: vi.fn(),
|
|
};
|
|
|
|
let page: any;
|
|
let context: any;
|
|
let browser: any;
|
|
|
|
const createBrowserMocks = () => {
|
|
page = {
|
|
context: vi.fn(),
|
|
goto: vi.fn(),
|
|
on: vi.fn(),
|
|
route: vi.fn(),
|
|
setExtraHTTPHeaders: vi.fn(),
|
|
unroute: vi.fn(),
|
|
};
|
|
|
|
context = {
|
|
addCookies: vi.fn(),
|
|
close: vi.fn(() => Promise.resolve()),
|
|
cookies: vi.fn(),
|
|
newPage: vi.fn(() => Promise.resolve(page)),
|
|
};
|
|
|
|
browser = {
|
|
close: vi.fn(),
|
|
newContext: vi.fn(() => Promise.resolve(context)),
|
|
};
|
|
};
|
|
|
|
const proxyMock = {
|
|
proxyObj: { url_regex: '.*' },
|
|
proxyUrlHandler: new URL('http://proxy.local'),
|
|
multiProxy: undefined as any,
|
|
getCurrentProxy: vi.fn(),
|
|
markProxyFailed: vi.fn(),
|
|
getDispatcherForProxy: vi.fn(),
|
|
};
|
|
|
|
vi.mock('playwright', () => ({
|
|
chromium: {
|
|
connect,
|
|
connectOverCDP,
|
|
launch,
|
|
},
|
|
}));
|
|
|
|
vi.mock('@/utils/proxy', () => ({
|
|
default: proxyMock,
|
|
}));
|
|
|
|
vi.mock('@/utils/logger', () => ({
|
|
default: {
|
|
warn: vi.fn(),
|
|
debug: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
const loadPlaywright = async () => {
|
|
vi.resetModules();
|
|
const mod = await import('@/utils/playwright');
|
|
return mod.getPlaywrightPage;
|
|
};
|
|
|
|
const resetMocks = () => {
|
|
createBrowserMocks();
|
|
connect.mockReset();
|
|
connectOverCDP.mockReset();
|
|
launch.mockReset();
|
|
routeContinue.mockReset();
|
|
routeAbort.mockReset();
|
|
route.request.mockReset();
|
|
request.resourceType.mockReset();
|
|
request.url.mockReset();
|
|
proxyMock.multiProxy = undefined;
|
|
proxyMock.getCurrentProxy.mockReset();
|
|
proxyMock.markProxyFailed.mockReset();
|
|
delete process.env.PLAYWRIGHT_WS_ENDPOINT;
|
|
delete process.env.PUPPETEER_WS_ENDPOINT;
|
|
};
|
|
|
|
createBrowserMocks();
|
|
|
|
describe('getPlaywrightPage (mocked)', () => {
|
|
it('connects via ws endpoint and runs onBeforeLoad', async () => {
|
|
resetMocks();
|
|
connect.mockResolvedValue(browser);
|
|
connectOverCDP.mockResolvedValue(browser);
|
|
launch.mockResolvedValue(browser);
|
|
page.goto.mockResolvedValue(undefined);
|
|
browser.close.mockResolvedValue(undefined);
|
|
process.env.PLAYWRIGHT_WS_ENDPOINT = 'ws://localhost:3000/?token=abc';
|
|
proxyMock.getCurrentProxy.mockReturnValue(null);
|
|
|
|
const getPlaywrightPage = await loadPlaywright();
|
|
const onBeforeLoad = vi.fn();
|
|
const close = browser.close;
|
|
const result = await getPlaywrightPage('https://example.com', {
|
|
noGoto: true,
|
|
onBeforeLoad,
|
|
});
|
|
|
|
const endpoint = connect.mock.calls[0][0] as string;
|
|
expect(connectOverCDP).not.toHaveBeenCalled();
|
|
expect(endpoint).toContain('launch-options=');
|
|
expect(endpoint).not.toContain('launch=');
|
|
const launchOptions = JSON.parse(new URL(endpoint).searchParams.get('launch-options') || '{}');
|
|
expect(launchOptions.args).not.toContainEqual(expect.stringContaining('--user-agent='));
|
|
expect(onBeforeLoad).toHaveBeenCalled();
|
|
|
|
await result.destroy();
|
|
expect(close).toHaveBeenCalled();
|
|
});
|
|
|
|
it('does not override the browser user agent', async () => {
|
|
resetMocks();
|
|
launch.mockResolvedValue(browser);
|
|
page.goto.mockResolvedValue(undefined);
|
|
proxyMock.getCurrentProxy.mockReturnValue(null);
|
|
|
|
const getPlaywrightPage = await loadPlaywright();
|
|
await getPlaywrightPage('https://example.com');
|
|
|
|
expect(launch).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
args: expect.not.arrayContaining([expect.stringContaining('--user-agent=')]),
|
|
})
|
|
);
|
|
expect(browser.newContext).toHaveBeenCalledWith(
|
|
expect.not.objectContaining({
|
|
userAgent: expect.any(String),
|
|
})
|
|
);
|
|
});
|
|
|
|
it('supports extending the browser auto close timeout', async () => {
|
|
vi.useFakeTimers();
|
|
try {
|
|
resetMocks();
|
|
launch.mockResolvedValue(browser);
|
|
browser.close.mockResolvedValue(undefined);
|
|
page.goto.mockResolvedValue(undefined);
|
|
proxyMock.getCurrentProxy.mockReturnValue(null);
|
|
|
|
const getPlaywrightPage = await loadPlaywright();
|
|
const close = browser.close;
|
|
await getPlaywrightPage('https://example.com', {
|
|
closeTimeout: 90000,
|
|
noGoto: true,
|
|
});
|
|
|
|
await vi.advanceTimersByTimeAsync(89999);
|
|
expect(close).not.toHaveBeenCalled();
|
|
|
|
await vi.advanceTimersByTimeAsync(1);
|
|
expect(close).toHaveBeenCalled();
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
it('handles requestfinished events after the remote page closes', async () => {
|
|
resetMocks();
|
|
launch.mockResolvedValue(browser);
|
|
page.goto.mockResolvedValue(undefined);
|
|
proxyMock.getCurrentProxy.mockReturnValue(null);
|
|
|
|
const getPlaywrightPage = await loadPlaywright();
|
|
const handler = vi.fn();
|
|
const rawOn = page.on;
|
|
await getPlaywrightPage('https://example.com', {
|
|
onBeforeLoad: (page) => {
|
|
page.on('requestfinished', handler);
|
|
},
|
|
});
|
|
|
|
const finishedHandler = rawOn.mock.calls.find(([event]) => event === 'requestfinished')?.[1];
|
|
await finishedHandler?.({
|
|
response: vi.fn().mockRejectedValue(new Error('closed')),
|
|
url: () => 'https://example.com/api',
|
|
});
|
|
|
|
expect(handler).toHaveBeenCalledTimes(1);
|
|
const finishedRequest = handler.mock.calls[0][0];
|
|
expect(finishedRequest.response()).toBeNull();
|
|
expect(finishedRequest.url()).toBe('https://example.com/api');
|
|
});
|
|
|
|
it('marks proxy failed when navigation throws with multi-proxy', async () => {
|
|
resetMocks();
|
|
launch.mockResolvedValue(browser);
|
|
page.goto.mockRejectedValueOnce(new Error('fail'));
|
|
|
|
const currentProxy = {
|
|
uri: 'http://user:pass@proxy.local:8080',
|
|
urlHandler: new URL('http://user:pass@proxy.local:8080'),
|
|
};
|
|
proxyMock.multiProxy = {};
|
|
proxyMock.getCurrentProxy.mockReturnValue(currentProxy);
|
|
|
|
const getPlaywrightPage = await loadPlaywright();
|
|
await expect(getPlaywrightPage('https://example.com')).rejects.toThrow('fail');
|
|
|
|
expect(proxyMock.markProxyFailed).toHaveBeenCalledWith(currentProxy.uri);
|
|
});
|
|
|
|
it('maps legacy networkidle waits to playwright networkidle', async () => {
|
|
resetMocks();
|
|
launch.mockResolvedValue(browser);
|
|
page.goto.mockResolvedValue(undefined);
|
|
proxyMock.getCurrentProxy.mockReturnValue(null);
|
|
|
|
const getPlaywrightPage = await loadPlaywright();
|
|
const goto = page.goto;
|
|
await getPlaywrightPage('https://example.com', {
|
|
gotoConfig: {
|
|
waitUntil: 'networkidle2',
|
|
},
|
|
});
|
|
|
|
expect(goto).toHaveBeenCalledWith('https://example.com', {
|
|
waitUntil: 'networkidle',
|
|
});
|
|
});
|
|
|
|
it('keeps legacy request interception helpers', async () => {
|
|
resetMocks();
|
|
launch.mockResolvedValue(browser);
|
|
page.goto.mockResolvedValue(undefined);
|
|
proxyMock.getCurrentProxy.mockReturnValue(null);
|
|
request.resourceType.mockReturnValue('image');
|
|
request.url.mockReturnValue('https://example.com/logo.png');
|
|
routeAbort.mockReturnValueOnce(new Promise((resolve) => setTimeout(resolve, 0)));
|
|
route.request.mockReturnValue(request);
|
|
|
|
const getPlaywrightPage = await loadPlaywright();
|
|
await getPlaywrightPage('https://example.com', {
|
|
onBeforeLoad: async (page) => {
|
|
await page.setRequestInterception(true);
|
|
page.on('request', (request) => {
|
|
request.resourceType() === 'document' ? request.continue() : request.abort();
|
|
});
|
|
},
|
|
});
|
|
|
|
const routeHandler = page.route.mock.calls[0][1];
|
|
await routeHandler(route);
|
|
|
|
expect(routeAbort).toHaveBeenCalled();
|
|
expect(routeContinue).not.toHaveBeenCalled();
|
|
});
|
|
});
|