410 lines
14 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
});
|