Files
apify--crawlee/test/core/playwright_utils.test.ts
2026-07-13 13:23:39 +08:00

410 lines
14 KiB
TypeScript

import type { Server } from 'node:http';
import path from 'node:path';
import { KeyValueStore, launchPlaywright, playwrightUtils, Request } from '@crawlee/playwright';
import type { Browser, Page } from 'playwright';
import { chromium } from 'playwright';
import log from '@apify/log';
import { runExampleComServer } from '../shared/_helper';
import { MemoryStorageEmulator } from '../shared/MemoryStorageEmulator';
let serverAddress = 'http://localhost:';
let port: number;
let server: Server;
const launchContext = { launchOptions: { headless: true } };
beforeAll(async () => {
[server, port] = await runExampleComServer();
serverAddress += port;
});
afterAll(() => {
server.close();
});
describe('playwrightUtils', () => {
let ll: number;
const localStorageEmulator = new MemoryStorageEmulator();
beforeAll(async () => {
ll = log.getLevel();
log.setLevel(log.LEVELS.ERROR);
});
beforeEach(async () => {
await localStorageEmulator.init();
});
afterAll(async () => {
log.setLevel(ll);
await localStorageEmulator.destroy();
});
test('injectFile()', async () => {
const browser2 = await launchPlaywright(launchContext);
const survive = async (browser: Browser) => {
// Survive navigations
const page = await browser.newPage();
// @ts-expect-error
let result = await page.evaluate(() => window.injectedVariable === 42);
expect(result).toBe(false);
await playwrightUtils.injectFile(page, path.join(__dirname, '..', 'shared', 'data', 'inject_file.txt'), {
surviveNavigations: true,
});
// @ts-expect-error
result = await page.evaluate(() => window.injectedVariable);
expect(result).toBe(42);
await page.goto('about:chrome');
// @ts-expect-error
result = await page.evaluate(() => window.injectedVariable);
expect(result).toBe(42);
await page.goto(serverAddress);
// @ts-expect-error
result = await page.evaluate(() => window.injectedVariable);
expect(result).toBe(42);
};
const remove = async (browser: Browser) => {
// Remove with navigations
const page = await browser.newPage();
// @ts-expect-error
let result = await page.evaluate(() => window.injectedVariable === 42);
expect(result).toBe(false);
await page.goto('about:chrome');
// @ts-expect-error
result = await page.evaluate(() => window.injectedVariable === 42);
expect(result).toBe(false);
await playwrightUtils.injectFile(page, path.join(__dirname, '..', 'shared', 'data', 'inject_file.txt'));
// @ts-expect-error
result = await page.evaluate(() => window.injectedVariable);
expect(result).toBe(42);
await page.goto(serverAddress);
// @ts-expect-error
result = await page.evaluate(() => window.injectedVariable === 42);
expect(result).toBe(false);
};
try {
await Promise.all([survive(browser2), remove(browser2)]);
} finally {
await browser2.close();
}
});
test('injectJQuery()', async () => {
const browser = await launchPlaywright(launchContext);
try {
const page = await browser.newPage();
await page.goto('about:blank');
// NOTE: Chrome already defines window.$ as alias to document.querySelector(),
// (https://developers.google.com/web/tools/chrome-devtools/console/command-line-reference#queryselector)
const result1 = await page.evaluate(() => {
return {
// @ts-expect-error
isDefined: window.jQuery !== undefined,
};
});
expect(result1).toEqual({
isDefined: false,
});
await playwrightUtils.injectJQuery(page);
const result2 = await page.evaluate(() => {
/* global $ */
return {
// @ts-expect-error
isDefined: window.jQuery === window.$,
// @ts-expect-error
text: $('h1').text(),
};
});
expect(result2).toEqual({
isDefined: true,
text: '',
});
await page.reload();
const result3 = await page.evaluate(() => {
return {
// @ts-expect-error
isDefined: window.jQuery === window.$,
// @ts-expect-error
text: $('h1').text(),
};
});
expect(result3).toEqual({
isDefined: true,
text: '',
});
} finally {
await browser.close();
}
});
test('parseWithCheerio() works', async () => {
const browser = await launchPlaywright(launchContext);
try {
const page = await browser.newPage();
await page.goto(serverAddress);
const $ = await playwrightUtils.parseWithCheerio(page);
const title = $('h1').text().trim();
expect(title).toBe('Example Domain');
} finally {
await browser.close();
}
});
test('parseWithCheerio() iframe expansion works', async () => {
const browser = await launchPlaywright(launchContext);
try {
const page = await browser.newPage();
await page.goto(new URL('/special/outside-iframe', serverAddress).toString());
const $ = await playwrightUtils.parseWithCheerio(page);
const headings = $('h1')
.map((_, el) => $(el).text())
.get();
const titles = $('title')
.map((_, el) => $(el).text())
.get();
expect(titles).toEqual(['Outside iframe title']);
expect(headings).toEqual(['Outside iframe', 'In iframe']);
} finally {
await browser.close();
}
});
test('parseWithCheerio() iframe expansion works with Trusted Types CSP', async () => {
const browser = await launchPlaywright(launchContext);
try {
const page = await browser.newPage();
await page.goto(new URL('/special/outside-iframe-csp', serverAddress).toString());
const $ = await playwrightUtils.parseWithCheerio(page);
const headings = $('h1')
.map((_, el) => $(el).text())
.get();
expect(headings).toEqual(['Outside iframe', 'In iframe']);
} finally {
await browser.close();
}
});
describe('blockRequests()', () => {
let browser: Browser = null as any;
beforeAll(async () => {
browser = await launchPlaywright(launchContext);
});
afterAll(async () => {
await browser.close();
});
test('works with default values', async () => {
const loadedUrls: string[] = [];
const page = await browser.newPage();
await playwrightUtils.blockRequests(page);
page.on('response', (response) => {
if (response.url() !== `${serverAddress}/special/resources`) {
loadedUrls.push(response.url());
}
});
await page.goto(`${serverAddress}/special/resources`, { waitUntil: 'networkidle' });
expect(loadedUrls).toEqual([`${serverAddress}/script.js`]);
});
test('works with overridden values', async () => {
const loadedUrls: string[] = [];
const page = await browser.newPage();
await playwrightUtils.blockRequests(page, {
urlPatterns: ['.css'],
});
page.on('response', (response) => loadedUrls.push(response.url()));
await page.goto(`${serverAddress}/special/resources`, { waitUntil: 'networkidle' });
expect(loadedUrls).toEqual(
expect.arrayContaining([
`${serverAddress}/image.png`,
`${serverAddress}/script.js`,
`${serverAddress}/image.gif`,
]),
);
});
});
test('gotoExtended() works', async () => {
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
const request = new Request({
url: `${serverAddress}/special/getDebug`,
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
payload: '{ "foo": "bar" }',
});
const response = await playwrightUtils.gotoExtended(page, request);
const { method, headers, bodyLength } = JSON.parse(await response!.text());
expect(method).toBe('POST');
expect(bodyLength).toBe(16);
expect(headers['content-type']).toBe('application/json; charset=utf-8');
} finally {
await browser.close();
}
}, 60_000);
describe('shadow root expansion', () => {
let browser: Browser;
beforeAll(async () => {
browser = await launchPlaywright(launchContext);
});
afterAll(async () => {
await browser.close();
});
test('no expansion with ignoreShadowRoots: true', async () => {
const page = await browser.newPage();
await page.goto(`${serverAddress}/special/shadow-root`);
const result = await playwrightUtils.parseWithCheerio(page, true);
const text = result('body').text().trim();
expect([...text.matchAll(/\[GOOD\]/g)]).toHaveLength(0);
expect([...text.matchAll(/\[BAD\]/g)]).toHaveLength(0);
});
test('expansion works', async () => {
const page = await browser.newPage();
await page.goto(`${serverAddress}/special/shadow-root`);
const result = await playwrightUtils.parseWithCheerio(page);
const text = result('body').text().trim();
expect([...text.matchAll(/\[GOOD\]/g)]).toHaveLength(2);
expect([...text.matchAll(/\[BAD\]/g)]).toHaveLength(0);
});
});
describe('infiniteScroll()', () => {
function isAtBottom() {
return window.innerHeight + window.pageYOffset >= document.body.offsetHeight;
}
let browser: Browser;
beforeAll(async () => {
browser = await chromium.launch({ headless: true });
});
afterAll(async () => {
await browser.close();
});
let page: Page;
beforeEach(async () => {
page = await browser.newPage();
let count = 0;
const content = Array(1000)
.fill(null)
.map(() => {
return `<div style="border: 1px solid black">Div number: ${count++}</div>`;
});
const contentHTML = `<html><body>${content}</body></html>`;
await page.setContent(contentHTML);
});
afterEach(async () => {
await page.close();
});
test('works', async () => {
const before = await page.evaluate(isAtBottom);
expect(before).toBe(false);
await playwrightUtils.infiniteScroll(page, { waitForSecs: 0 });
const after = await page.evaluate(isAtBottom);
expect(after).toBe(true);
});
test('maxScrollHeight works', async () => {
const before = await page.evaluate(isAtBottom);
expect(before).toBe(false);
await playwrightUtils.infiniteScroll(page, {
waitForSecs: Infinity,
maxScrollHeight: 1000,
stopScrollCallback: async () => true,
});
const after = await page.evaluate(isAtBottom);
// It scrolls to the bottom in the first scroll so this is correct.
// The test passes because the Infinite waitForSecs is broken by the height requirement.
// If it didn't, the test would time out.
expect(after).toBe(true);
});
test('stopScrollCallback works', async () => {
const before = await page.evaluate(isAtBottom);
expect(before).toBe(false);
await playwrightUtils.infiniteScroll(page, {
waitForSecs: Infinity,
stopScrollCallback: async () => true,
});
const after = await page.evaluate(isAtBottom);
expect(after).toBe(true);
});
});
test('saveSnapshot() works', async () => {
const openKVSSpy = vitest.spyOn(KeyValueStore, 'open');
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage();
const contentHTML =
'<html><head></head><body><div style="border: 1px solid black">Div number: 1</div></body></html>';
await page.setContent(contentHTML);
const screenshot = await page.screenshot({ fullPage: true, type: 'jpeg', quality: 60 });
// Test saving both image and html
const object = { setValue: vitest.fn() };
openKVSSpy.mockResolvedValue(object as any);
await playwrightUtils.saveSnapshot(page, {
key: 'TEST',
keyValueStoreName: 'TEST-STORE',
screenshotQuality: 60,
});
expect(object.setValue).toBeCalledWith('TEST.jpg', screenshot, { contentType: 'image/jpeg' });
expect(object.setValue).toBeCalledWith('TEST.html', contentHTML, { contentType: 'text/html' });
object.setValue.mockReset();
// Test saving only image
await playwrightUtils.saveSnapshot(page, { saveHtml: false });
// Default quality is 50
const screenshot2 = await page.screenshot({ fullPage: true, type: 'jpeg', quality: 50 });
expect(object.setValue).toBeCalledWith('SNAPSHOT.jpg', screenshot2, { contentType: 'image/jpeg' });
} finally {
await browser.close();
}
});
});