Bug 1851438 - [remote] Sync vendored puppeteer to v21.2.0 r=webdriver-reviewers,whimboo

Depends on D188103

Differential Revision: https://phabricator.services.mozilla.com/D188104
This commit is contained in:
Julian Descottes 2023-09-15 13:36:11 +00:00
Родитель 9ed7cf9491
Коммит 7a9d33e4f7
239 изменённых файлов: 13727 добавлений и 8136 удалений

Просмотреть файл

@ -44,5 +44,8 @@ yarn-error.log*
# ESLint ignores.
assets/
third_party/
sandbox/
ng-schematics/src/**/files/
# ng-schematics
packages/ng-schematics/sandbox/**
packages/ng-schematics/multi/**
packages/ng-schematics/src/**/files/

Просмотреть файл

@ -1,3 +1,6 @@
const rulesDirPlugin = require('eslint-plugin-rulesdir');
rulesDirPlugin.RULES_DIR = 'tools/eslint/lib';
module.exports = {
root: true,
env: {
@ -136,10 +139,12 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/stylistic',
],
plugins: ['eslint-plugin-tsdoc', 'local'],
plugins: ['eslint-plugin-tsdoc', 'rulesdir'],
rules: {
// Keeps comments formatted.
'local/prettier-comments': 'error',
'rulesdir/prettier-comments': 'error',
// Enforces clean up of used resources.
'rulesdir/use-using': 'error',
// Brackets keep code readable.
curly: ['error', 'all'],
// Brackets keep code readable and `return` intentions clear.
@ -213,7 +218,22 @@ module.exports = {
{ignoreVoid: true, ignoreIIFE: true},
],
'@typescript-eslint/prefer-ts-expect-error': 'error',
// This is more performant; see https://v8.dev/blog/fast-async.
'@typescript-eslint/return-await': ['error', 'always'],
},
overrides: [
{
files: [
'packages/puppeteer-core/src/**/*.test.ts',
'tools/mochaRunner/src/test.ts',
],
rules: {
// With the Node.js test runner, `describe` and `it` are technically
// promises, but we don't need to await them.
'@typescript-eslint/no-floating-promises': 'off',
},
},
],
},
],
};

Просмотреть файл

@ -1,7 +1,7 @@
{
"packages/puppeteer": "20.9.0",
"packages/puppeteer-core": "20.9.0",
"packages/puppeteer": "21.2.0",
"packages/puppeteer-core": "21.2.0",
"packages/testserver": "0.6.0",
"packages/ng-schematics": "0.3.0",
"packages/browsers": "1.4.6"
"packages/ng-schematics": "0.5.0",
"packages/browsers": "1.7.0"
}

Просмотреть файл

@ -59,6 +59,8 @@ include `$HOME/.cache` into the project's deployment.
For a version of Puppeteer without the browser installation, see
[`puppeteer-core`](#puppeteer-core).
If used with TypeScript, the minimum supported TypeScript version is `4.7.4`.
#### Configuration
Puppeteer uses several defaults that can be customized through configuration

Просмотреть файл

@ -5,6 +5,6 @@ origin:
description: Headless Chrome Node API
license: Apache-2.0
name: puppeteer
release: puppeteer-v20.9.0
release: puppeteer-v21.2.0
url: https://github.com/puppeteer/puppeteer.git
schema: 1

4529
remote/test/puppeteer/package-lock.json сгенерированный

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -10,7 +10,7 @@
"build:docs": "wireit",
"check:pinned-deps": "tsx tools/ensure-pinned-deps",
"check": "npm run check --workspaces --if-present && run-p check:*",
"clean": "rimraf -g \"./**/.wireit\" && npm run clean --workspaces --if-present",
"clean": "npm run clean --workspaces --if-present",
"debug": "mocha --inspect-brk",
"docs": "run-s build:docs generate:markdown",
"format:eslint": "eslint --ext js --ext ts --fix .",
@ -22,6 +22,7 @@
"lint:prettier": "prettier --check .",
"lint": "run-s lint:prettier lint:eslint",
"postinstall": "npm run postinstall --workspaces --if-present",
"prepare": "npm run prepare --workspaces --if-present",
"test-install": "npm run test --workspace @puppeteer-test/installation",
"test-types": "tsd -t packages/puppeteer",
"test:chrome:headful": "wireit",
@ -105,46 +106,47 @@
},
"devDependencies": {
"@actions/core": "1.10.0",
"@microsoft/api-documenter": "7.22.27",
"@microsoft/api-extractor": "7.36.2",
"@microsoft/api-extractor-model": "7.27.4",
"@microsoft/api-documenter": "7.22.33",
"@microsoft/api-extractor": "7.36.4",
"@microsoft/api-extractor-model": "7.27.6",
"@pptr/testserver": "file:packages/testserver",
"@prettier/sync": "0.2.1",
"@rollup/plugin-commonjs": "25.0.2",
"@rollup/plugin-node-resolve": "15.1.0",
"@prettier/sync": "0.3.0",
"@rollup/plugin-commonjs": "25.0.4",
"@rollup/plugin-node-resolve": "15.2.1",
"@rollup/plugin-terser": "0.4.3",
"@types/debug": "4.1.8",
"@types/diff": "5.0.3",
"@types/mime": "3.0.1",
"@types/mocha": "10.0.1",
"@types/node": "20.2.5",
"@types/node": "20.5.9",
"@types/pixelmatch": "5.2.4",
"@types/pngjs": "6.0.1",
"@types/progress": "2.0.5",
"@types/semver": "7.5.0",
"@types/sinon": "10.0.15",
"@types/semver": "7.5.1",
"@types/sinon": "10.0.16",
"@types/tar-fs": "2.0.1",
"@types/unbzip2-stream": "1.4.0",
"@types/unbzip2-stream": "1.4.1",
"@types/ws": "8.5.5",
"@typescript-eslint/eslint-plugin": "6.0.0",
"@typescript-eslint/parser": "6.0.0",
"c8": "8.0.0",
"@typescript-eslint/eslint-plugin": "6.5.0",
"@typescript-eslint/parser": "6.5.0",
"c8": "8.0.1",
"commonmark": "0.30.0",
"cross-env": "7.0.3",
"diff": "5.1.0",
"esbuild": "0.18.12",
"eslint": "8.44.0",
"eslint-config-prettier": "8.8.0",
"esbuild": "0.19.2",
"eslint": "8.48.0",
"eslint-config-prettier": "9.0.0",
"eslint-formatter-codeframe": "7.32.1",
"eslint-plugin-import": "2.27.5",
"eslint-plugin-local": "1.0.0",
"eslint-plugin-import": "2.28.1",
"eslint-plugin-rulesdir": "0.2.2",
"eslint-plugin-mocha": "10.1.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-tsdoc": "0.2.17",
"eslint-plugin-unused-imports": "3.0.0",
"esprima": "4.0.1",
"expect": "29.6.1",
"glob": "10.3.3",
"gts": "4.0.1",
"expect": "29.6.4",
"glob": "10.3.4",
"gts": "5.0.1",
"jpeg-js": "0.4.4",
"license-checker": "25.0.1",
"mime": "3.0.0",
@ -154,24 +156,25 @@
"npm-run-all": "4.1.5",
"pixelmatch": "5.3.0",
"pngjs": "7.0.0",
"prettier": "3.0.0",
"prettier": "3.0.3",
"puppeteer": "file:packages/puppeteer",
"rimraf": "5.0.1",
"rollup": "3.26.2",
"rollup": "3.28.1",
"rollup-plugin-polyfill-node": "0.12.0",
"semver": "7.5.4",
"sinon": "15.2.0",
"source-map-support": "0.5.21",
"spdx-satisfies": "5.0.1",
"text-diff": "1.0.1",
"tsd": "0.28.1",
"tsx": "3.12.7",
"typescript": "5.1.6",
"wireit": "0.10.0",
"zod": "3.21.4"
"tsd": "0.29.0",
"tsx": "3.12.8",
"typescript": "5.2.2",
"wireit": "0.13.0",
"zod": "3.22.2"
},
"workspaces": [
"packages/*",
"test",
"test/installation"
"test/installation",
"tools/eslint"
]
}

Просмотреть файл

@ -1,5 +1,39 @@
# Changelog
## [1.7.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.6.0...browsers-v1.7.0) (2023-08-18)
### Features
* support chrome-headless-shell ([#10739](https://github.com/puppeteer/puppeteer/issues/10739)) ([416843b](https://github.com/puppeteer/puppeteer/commit/416843ba68aaab7ae14bbc74c2ac705e877e91a7))
## [1.6.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.5.1...browsers-v1.6.0) (2023-08-10)
### Features
* allow installing chrome/chromedriver by milestone and version prefix ([#10720](https://github.com/puppeteer/puppeteer/issues/10720)) ([bec2357](https://github.com/puppeteer/puppeteer/commit/bec2357aeedda42cfaf3096c6293c2f49ceb825e))
## [1.5.1](https://github.com/puppeteer/puppeteer/compare/browsers-v1.5.0...browsers-v1.5.1) (2023-08-08)
### Bug Fixes
* add buildId to archive path ([#10699](https://github.com/puppeteer/puppeteer/issues/10699)) ([21461b0](https://github.com/puppeteer/puppeteer/commit/21461b02c65062f5ed240e8ea357e9b7f2d26b32))
## [1.5.0](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.6...browsers-v1.5.0) (2023-08-02)
### Features
* add executablePath to InstalledBrowser ([#10594](https://github.com/puppeteer/puppeteer/issues/10594)) ([87522e7](https://github.com/puppeteer/puppeteer/commit/87522e778a6487111931458755e701f1c4b717d9))
### Bug Fixes
* clear pending TLS socket handle ([#10667](https://github.com/puppeteer/puppeteer/issues/10667)) ([87bd791](https://github.com/puppeteer/puppeteer/commit/87bd791ddc10c247bf154bbac2aa912327a4cf20))
* remove typescript from peer dependencies ([#10593](https://github.com/puppeteer/puppeteer/issues/10593)) ([c60572a](https://github.com/puppeteer/puppeteer/commit/c60572a1ca36ea5946d287bd629ac31798d84cb0))
## [1.4.6](https://github.com/puppeteer/puppeteer/compare/browsers-v1.4.5...browsers-v1.4.6) (2023-07-20)

Просмотреть файл

@ -1,12 +1,11 @@
{
"name": "@puppeteer/browsers",
"version": "1.4.6",
"version": "1.7.0",
"description": "Download and launch browsers",
"scripts": {
"build:docs": "wireit",
"build": "wireit",
"build:test": "wireit",
"clean": "tsc --build --clean && rm -rf lib",
"clean": "git clean -Xdf -e '!node_modules' .",
"test": "wireit"
},
"bin": "lib/cjs/main-cli.js",
@ -101,7 +100,7 @@
"debug": "4.3.4",
"extract-zip": "2.0.1",
"progress": "2.0.3",
"proxy-agent": "6.3.0",
"proxy-agent": "6.3.1",
"tar-fs": "3.0.4",
"unbzip2-stream": "1.4.3",
"yargs": "17.7.1"
@ -109,13 +108,5 @@
"devDependencies": {
"@types/node": "^16.11.7",
"@types/yargs": "17.0.22"
},
"peerDependencies": {
"typescript": ">= 4.7.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}

Просмотреть файл

@ -132,6 +132,38 @@ export class CLI {
'$0 install chrome@latest',
'Install the latest available build for the Chrome browser.'
);
yargs.example(
'$0 install chrome@canary',
'Install the latest available build for the Chrome Canary browser.'
);
yargs.example(
'$0 install chrome@115',
'Install the latest available build for Chrome 115.'
);
yargs.example(
'$0 install chromedriver@canary',
'Install the latest available build for ChromeDriver Canary.'
);
yargs.example(
'$0 install chromedriver@115',
'Install the latest available build for ChromeDriver 115.'
);
yargs.example(
'$0 install chromedriver@115.0.5790',
'Install the latest available patch (115.0.5790.X) build for ChromeDriver.'
);
yargs.example(
'$0 install chrome-headless-shell',
'Install the latest available chrome-headless-shell build.'
);
yargs.example(
'$0 install chrome-headless-shell@beta',
'Install the latest available chrome-headless-shell build corresponding to the Beta channel.'
);
yargs.example(
'$0 install chrome-headless-shell@118',
'Install the latest available chrome-headless-shell 118 build.'
);
yargs.example(
'$0 install chromium@1083080',
'Install the revision 1083080 of the Chromium browser.'
@ -201,15 +233,15 @@ export class CLI {
default: false,
});
yargs.example(
'$0 launch chrome@1083080',
'Launch the Chrome browser identified by the revision 1083080.'
'$0 launch chrome@115.0.5790.170',
'Launch Chrome 115.0.5790.170'
);
yargs.example(
'$0 launch firefox@112.0a1',
'Launch the Firefox browser identified by the milestone 112.0a1.'
);
yargs.example(
'$0 launch chrome@1083080 --detached',
'$0 launch chrome@115.0.5790.170 --detached',
'Launch the browser but detach the sub-processes.'
);
yargs.example(

Просмотреть файл

@ -18,19 +18,53 @@ import fs from 'fs';
import path from 'path';
import {Browser, BrowserPlatform} from './browser-data/browser-data.js';
import {computeExecutablePath} from './launch.js';
/**
* @public
*/
export interface InstalledBrowser {
export class InstalledBrowser {
browser: Browser;
buildId: string;
platform: BrowserPlatform;
#cache: Cache;
/**
* @internal
*/
constructor(
cache: Cache,
browser: Browser,
buildId: string,
platform: BrowserPlatform
) {
this.#cache = cache;
this.browser = browser;
this.buildId = buildId;
this.platform = platform;
}
/**
* Path to the root of the installation folder. Use
* {@link computeExecutablePath} to get the path to the executable binary.
*/
path: string;
browser: Browser;
buildId: string;
platform: BrowserPlatform;
get path(): string {
return this.#cache.installationDir(
this.browser,
this.platform,
this.buildId
);
}
get executablePath(): string {
return computeExecutablePath({
cacheDir: this.#cache.rootDir,
platform: this.platform,
browser: this.browser,
buildId: this.buildId,
});
}
}
/**
@ -54,6 +88,13 @@ export class Cache {
this.#rootDir = rootDir;
}
/**
* @internal
*/
get rootDir(): string {
return this.#rootDir;
}
browserRoot(browser: Browser): string {
return path.join(this.#rootDir, browser);
}
@ -106,14 +147,14 @@ export class Cache {
if (!result) {
return null;
}
return {
path: path.join(this.browserRoot(browser), file),
return new InstalledBrowser(
this,
browser,
platform: result.platform,
buildId: result.buildId,
};
result.buildId,
result.platform as BrowserPlatform
);
})
.filter((item): item is InstalledBrowser => {
.filter((item: InstalledBrowser | null): item is InstalledBrowser => {
return item !== null;
});
});

Просмотреть файл

@ -14,6 +14,7 @@
* limitations under the License.
*/
import * as chromeHeadlessShell from './chrome-headless-shell.js';
import * as chrome from './chrome.js';
import * as chromedriver from './chromedriver.js';
import * as chromium from './chromium.js';
@ -30,6 +31,7 @@ export {ProfileOptions};
export const downloadUrls = {
[Browser.CHROMEDRIVER]: chromedriver.resolveDownloadUrl,
[Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadUrl,
[Browser.CHROME]: chrome.resolveDownloadUrl,
[Browser.CHROMIUM]: chromium.resolveDownloadUrl,
[Browser.FIREFOX]: firefox.resolveDownloadUrl,
@ -37,6 +39,7 @@ export const downloadUrls = {
export const downloadPaths = {
[Browser.CHROMEDRIVER]: chromedriver.resolveDownloadPath,
[Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.resolveDownloadPath,
[Browser.CHROME]: chrome.resolveDownloadPath,
[Browser.CHROMIUM]: chromium.resolveDownloadPath,
[Browser.FIREFOX]: firefox.resolveDownloadPath,
@ -44,6 +47,7 @@ export const downloadPaths = {
export const executablePathByBrowser = {
[Browser.CHROMEDRIVER]: chromedriver.relativeExecutablePath,
[Browser.CHROMEHEADLESSSHELL]: chromeHeadlessShell.relativeExecutablePath,
[Browser.CHROME]: chrome.relativeExecutablePath,
[Browser.CHROMIUM]: chromium.relativeExecutablePath,
[Browser.FIREFOX]: firefox.relativeExecutablePath,
@ -72,36 +76,28 @@ export async function resolveBuildId(
`${tag} is not supported for ${browser}. Use 'latest' instead.`
);
}
case Browser.CHROME:
case Browser.CHROME: {
switch (tag as BrowserTag) {
case BrowserTag.LATEST:
return await chrome.resolveBuildId(
platform,
ChromeReleaseChannel.CANARY
);
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
case BrowserTag.BETA:
return await chrome.resolveBuildId(
platform,
ChromeReleaseChannel.BETA
);
return await chrome.resolveBuildId(ChromeReleaseChannel.BETA);
case BrowserTag.CANARY:
return await chrome.resolveBuildId(
platform,
ChromeReleaseChannel.CANARY
);
return await chrome.resolveBuildId(ChromeReleaseChannel.CANARY);
case BrowserTag.DEV:
return await chrome.resolveBuildId(
platform,
ChromeReleaseChannel.DEV
);
return await chrome.resolveBuildId(ChromeReleaseChannel.DEV);
case BrowserTag.STABLE:
return await chrome.resolveBuildId(
platform,
ChromeReleaseChannel.STABLE
);
return await chrome.resolveBuildId(ChromeReleaseChannel.STABLE);
default:
const result = await chrome.resolveBuildId(tag);
if (result) {
return result;
}
}
case Browser.CHROMEDRIVER:
switch (tag as BrowserTag) {
return tag;
}
case Browser.CHROMEDRIVER: {
switch (tag) {
case BrowserTag.LATEST:
case BrowserTag.CANARY:
return await chromedriver.resolveBuildId(ChromeReleaseChannel.CANARY);
@ -111,7 +107,41 @@ export async function resolveBuildId(
return await chromedriver.resolveBuildId(ChromeReleaseChannel.DEV);
case BrowserTag.STABLE:
return await chromedriver.resolveBuildId(ChromeReleaseChannel.STABLE);
default:
const result = await chromedriver.resolveBuildId(tag);
if (result) {
return result;
}
}
return tag;
}
case Browser.CHROMEHEADLESSSHELL: {
switch (tag) {
case BrowserTag.LATEST:
case BrowserTag.CANARY:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.CANARY
);
case BrowserTag.BETA:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.BETA
);
case BrowserTag.DEV:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.DEV
);
case BrowserTag.STABLE:
return await chromeHeadlessShell.resolveBuildId(
ChromeReleaseChannel.STABLE
);
default:
const result = await chromeHeadlessShell.resolveBuildId(tag);
if (result) {
return result;
}
}
return tag;
}
case Browser.CHROMIUM:
switch (tag as BrowserTag) {
case BrowserTag.LATEST:
@ -155,6 +185,7 @@ export function resolveSystemExecutablePath(
): string {
switch (browser) {
case Browser.CHROMEDRIVER:
case Browser.CHROMEHEADLESSSHELL:
case Browser.FIREFOX:
case Browser.CHROMIUM:
throw new Error(

Просмотреть файл

@ -0,0 +1,79 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'path';
import {BrowserPlatform} from './types.js';
function folder(platform: BrowserPlatform): string {
switch (platform) {
case BrowserPlatform.LINUX:
return 'linux64';
case BrowserPlatform.MAC_ARM:
return 'mac-arm64';
case BrowserPlatform.MAC:
return 'mac-x64';
case BrowserPlatform.WIN32:
return 'win32';
case BrowserPlatform.WIN64:
return 'win64';
}
}
export function resolveDownloadUrl(
platform: BrowserPlatform,
buildId: string,
baseUrl = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing'
): string {
return `${baseUrl}/${resolveDownloadPath(platform, buildId).join('/')}`;
}
export function resolveDownloadPath(
platform: BrowserPlatform,
buildId: string
): string[] {
return [
buildId,
folder(platform),
`chrome-headless-shell-${folder(platform)}.zip`,
];
}
export function relativeExecutablePath(
platform: BrowserPlatform,
_buildId: string
): string {
switch (platform) {
case BrowserPlatform.MAC:
case BrowserPlatform.MAC_ARM:
return path.join(
'chrome-headless-shell-' + folder(platform),
'chrome-headless-shell'
);
case BrowserPlatform.LINUX:
return path.join(
'chrome-headless-shell-linux64',
'chrome-headless-shell'
);
case BrowserPlatform.WIN32:
case BrowserPlatform.WIN64:
return path.join(
'chrome-headless-shell-' + folder(platform),
'chrome-headless-shell.exe'
);
}
}
export {resolveBuildId} from './chrome.js';

Просмотреть файл

@ -97,11 +97,66 @@ export async function getLastKnownGoodReleaseForChannel(
).channels[channel];
}
export async function getLastKnownGoodReleaseForMilestone(
milestone: string
): Promise<{version: string; revision: string} | undefined> {
const data = (await getJSON(
new URL(
'https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone.json'
)
)) as {
milestones: Record<string, {version: string; revision: string}>;
};
return data.milestones[milestone] as
| {version: string; revision: string}
| undefined;
}
export async function getLastKnownGoodReleaseForBuild(
/**
* @example `112.0.23`,
*/
buildPrefix: string
): Promise<{version: string; revision: string} | undefined> {
const data = (await getJSON(
new URL(
'https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build.json'
)
)) as {
builds: Record<string, {version: string; revision: string}>;
};
return data.builds[buildPrefix] as
| {version: string; revision: string}
| undefined;
}
export async function resolveBuildId(
_platform: BrowserPlatform,
channel: ChromeReleaseChannel
): Promise<string> {
return (await getLastKnownGoodReleaseForChannel(channel)).version;
): Promise<string>;
export async function resolveBuildId(
channel: string
): Promise<string | undefined>;
export async function resolveBuildId(
channel: ChromeReleaseChannel | string
): Promise<string | undefined> {
if (
Object.values(ChromeReleaseChannel).includes(
channel as ChromeReleaseChannel
)
) {
return (
await getLastKnownGoodReleaseForChannel(channel as ChromeReleaseChannel)
).version;
}
if (channel.match(/^\d+$/)) {
// Potentially a milestone.
return (await getLastKnownGoodReleaseForMilestone(channel))?.version;
}
if (channel.match(/^\d+\.\d+\.\d+$/)) {
// Potentially a build prefix without the patch version.
return (await getLastKnownGoodReleaseForBuild(channel))?.version;
}
return;
}
export function resolveSystemExecutablePath(

Просмотреть файл

@ -15,8 +15,7 @@
*/
import path from 'path';
import {getLastKnownGoodReleaseForChannel} from './chrome.js';
import {BrowserPlatform, ChromeReleaseChannel} from './types.js';
import {BrowserPlatform} from './types.js';
function folder(platform: BrowserPlatform): string {
switch (platform) {
@ -63,8 +62,5 @@ export function relativeExecutablePath(
return path.join('chromedriver-' + folder(platform), 'chromedriver.exe');
}
}
export async function resolveBuildId(
channel: ChromeReleaseChannel
): Promise<string> {
return (await getLastKnownGoodReleaseForChannel(channel)).version;
}
export {resolveBuildId} from './chrome.js';

Просмотреть файл

@ -156,6 +156,9 @@ function defaultProfilePreferences(
// Do not warn when multiple tabs will be opened
'browser.tabs.warnOnOpen': false,
// Do not automatically offer translations, as tests do not expect this.
'browser.translations.automaticallyPopup': false,
// Disable the UI tour.
'browser.uitour.enabled': false,
// Turn off search suggestions in the location bar so as not to trigger

Просмотреть файл

@ -24,6 +24,7 @@ import * as firefox from './firefox.js';
*/
export enum Browser {
CHROME = 'chrome',
CHROMEHEADLESSSHELL = 'chrome-headless-shell',
CHROMIUM = 'chromium',
FIREFOX = 'firefox',
CHROMEDRIVER = 'chromedriver',

Просмотреть файл

@ -27,6 +27,8 @@ export function headHttpRequest(url: URL): Promise<boolean> {
url,
'HEAD',
response => {
// consume response data free node process
response.resume();
resolve(response.statusCode === 200);
},
false

Просмотреть файл

@ -100,9 +100,18 @@ export interface InstallOptions {
/**
* @public
*/
export function install(
options: InstallOptions & {unpack?: true}
): Promise<InstalledBrowser>;
/**
* @public
*/
export function install(
options: InstallOptions & {unpack: false}
): Promise<string>;
export async function install(
options: InstallOptions
): Promise<InstalledBrowser> {
): Promise<InstalledBrowser | string> {
options.platform ??= detectBrowserPlatform();
options.unpack ??= true;
if (!options.platform) {
@ -118,46 +127,36 @@ export async function install(
);
const fileName = url.toString().split('/').pop();
assert(fileName, `A malformed download URL was found: ${url}.`);
const structure = new Cache(options.cacheDir);
const browserRoot = structure.browserRoot(options.browser);
const archivePath = path.join(browserRoot, fileName);
const cache = new Cache(options.cacheDir);
const browserRoot = cache.browserRoot(options.browser);
const archivePath = path.join(browserRoot, `${options.buildId}-${fileName}`);
if (!existsSync(browserRoot)) {
await mkdir(browserRoot, {recursive: true});
}
if (!options.unpack) {
if (existsSync(archivePath)) {
return {
path: archivePath,
browser: options.browser,
platform: options.platform,
buildId: options.buildId,
};
return archivePath;
}
debugInstall(`Downloading binary from ${url}`);
debugTime('download');
await downloadFile(url, archivePath, options.downloadProgressCallback);
debugTimeEnd('download');
return {
path: archivePath,
browser: options.browser,
platform: options.platform,
buildId: options.buildId,
};
return archivePath;
}
const outputPath = structure.installationDir(
const outputPath = cache.installationDir(
options.browser,
options.platform,
options.buildId
);
if (existsSync(outputPath)) {
return {
path: outputPath,
browser: options.browser,
platform: options.platform,
buildId: options.buildId,
};
return new InstalledBrowser(
cache,
options.browser,
options.buildId,
options.platform
);
}
try {
debugInstall(`Downloading binary from ${url}`);
@ -180,12 +179,12 @@ export async function install(
await unlink(archivePath);
}
}
return {
path: outputPath,
browser: options.browser,
platform: options.platform,
buildId: options.buildId,
};
return new InstalledBrowser(
cache,
options.browser,
options.buildId,
options.platform
);
}
/**

Просмотреть файл

@ -305,7 +305,7 @@ export class Process {
if (!this.#exited) {
this.kill();
}
return this.#browserProcessExiting;
return await this.#browserProcessExiting;
}
hasClosed(): Promise<void> {

Просмотреть файл

@ -1,7 +1,8 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "../lib/cjs"
}
}

Просмотреть файл

@ -0,0 +1,82 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import assert from 'assert';
import path from 'path';
import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js';
import {
resolveDownloadUrl,
relativeExecutablePath,
resolveBuildId,
} from '../../../lib/cjs/browser-data/chrome-headless-shell.js';
describe('chrome-headless-shell', () => {
it('should resolve download URLs', () => {
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.LINUX, '118.0.5950.0'),
'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/linux64/chrome-headless-shell-linux64.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC, '118.0.5950.0'),
'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/mac-x64/chrome-headless-shell-mac-x64.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.MAC_ARM, '118.0.5950.0'),
'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/mac-arm64/chrome-headless-shell-mac-arm64.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN32, '118.0.5950.0'),
'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/win32/chrome-headless-shell-win32.zip'
);
assert.strictEqual(
resolveDownloadUrl(BrowserPlatform.WIN64, '118.0.5950.0'),
'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/118.0.5950.0/win64/chrome-headless-shell-win64.zip'
);
});
// TODO: once no new releases happen for the milestone, we can use the exact match.
it('should resolve milestones', async () => {
assert((await resolveBuildId('118'))?.startsWith('118.0'));
});
it('should resolve build prefix', async () => {
assert.strictEqual(await resolveBuildId('118.0.5950'), '118.0.5950.0');
});
it('should resolve executable paths', () => {
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.LINUX, '12372323'),
path.join('chrome-headless-shell-linux64', 'chrome-headless-shell')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.MAC, '12372323'),
path.join('chrome-headless-shell-mac-x64/', 'chrome-headless-shell')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.MAC_ARM, '12372323'),
path.join('chrome-headless-shell-mac-arm64', 'chrome-headless-shell')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.WIN32, '12372323'),
path.join('chrome-headless-shell-win32', 'chrome-headless-shell.exe')
);
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.WIN64, '12372323'),
path.join('chrome-headless-shell-win64', 'chrome-headless-shell.exe')
);
});
});

Просмотреть файл

@ -0,0 +1,91 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import assert from 'assert';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {CLI} from '../../../lib/cjs/CLI.js';
import {
createMockedReadlineInterface,
setupTestServer,
getServerUrl,
} from '../utils.js';
import {testChromeHeadlessShellBuildId} from '../versions.js';
describe('chrome-headless-shell CLI', function () {
this.timeout(90000);
setupTestServer();
let tmpDir = '/tmp/puppeteer-browsers-test';
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
});
afterEach(async () => {
await new CLI(tmpDir, createMockedReadlineInterface('yes')).run([
'npx',
'@puppeteer/browsers',
'clear',
`--path=${tmpDir}`,
`--base-url=${getServerUrl()}`,
]);
});
it('should download chrome-headless-shell binaries', async () => {
await new CLI(tmpDir).run([
'npx',
'@puppeteer/browsers',
'install',
`chrome-headless-shell@${testChromeHeadlessShellBuildId}`,
`--path=${tmpDir}`,
'--platform=linux',
`--base-url=${getServerUrl()}`,
]);
assert.ok(
fs.existsSync(
path.join(
tmpDir,
'chrome-headless-shell',
`linux-${testChromeHeadlessShellBuildId}`,
'chrome-headless-shell-linux64',
'chrome-headless-shell'
)
)
);
await new CLI(tmpDir, createMockedReadlineInterface('no')).run([
'npx',
'@puppeteer/browsers',
'clear',
`--path=${tmpDir}`,
]);
assert.ok(
fs.existsSync(
path.join(
tmpDir,
'chrome-headless-shell',
`linux-${testChromeHeadlessShellBuildId}`,
'chrome-headless-shell-linux64',
'chrome-headless-shell'
)
)
);
});
});

Просмотреть файл

@ -0,0 +1,103 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import assert from 'assert';
import fs from 'fs';
import os from 'os';
import path from 'path';
import {
install,
canDownload,
Browser,
BrowserPlatform,
Cache,
} from '../../../lib/cjs/main.js';
import {getServerUrl, setupTestServer} from '../utils.js';
import {testChromeDriverBuildId} from '../versions.js';
/**
* Tests in this spec use real download URLs and unpack live browser archives
* so it requires the network access.
*/
describe('ChromeDriver install', () => {
setupTestServer();
let tmpDir = '/tmp/puppeteer-browsers-test';
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'puppeteer-browsers-test'));
});
afterEach(() => {
new Cache(tmpDir).clear();
});
it('should check if a buildId can be downloaded', async () => {
assert.ok(
await canDownload({
cacheDir: tmpDir,
browser: Browser.CHROMEDRIVER,
platform: BrowserPlatform.LINUX,
buildId: testChromeDriverBuildId,
baseUrl: getServerUrl(),
})
);
});
it('should report if a buildId is not downloadable', async () => {
assert.strictEqual(
await canDownload({
cacheDir: tmpDir,
browser: Browser.CHROMEDRIVER,
platform: BrowserPlatform.LINUX,
buildId: 'unknown',
baseUrl: getServerUrl(),
}),
false
);
});
it('should download and unpack the binary', async function () {
this.timeout(60000);
const expectedOutputPath = path.join(
tmpDir,
'chromedriver',
`${BrowserPlatform.LINUX}-${testChromeDriverBuildId}`
);
assert.strictEqual(fs.existsSync(expectedOutputPath), false);
let browser = await install({
cacheDir: tmpDir,
browser: Browser.CHROMEDRIVER,
platform: BrowserPlatform.LINUX,
buildId: testChromeDriverBuildId,
baseUrl: getServerUrl(),
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
// Second iteration should be no-op.
browser = await install({
cacheDir: tmpDir,
browser: Browser.CHROMEDRIVER,
platform: BrowserPlatform.LINUX,
buildId: testChromeDriverBuildId,
baseUrl: getServerUrl(),
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
assert.ok(fs.existsSync(browser.executablePath));
});
});

Просмотреть файл

@ -25,6 +25,7 @@ import {
resolveDownloadUrl,
relativeExecutablePath,
resolveSystemExecutablePath,
resolveBuildId,
} from '../../../lib/cjs/browser-data/chrome.js';
describe('Chrome', () => {
@ -117,4 +118,12 @@ describe('Chrome', () => {
);
}, new Error(`Unable to detect browser executable path for 'canary' on linux.`));
});
it('should resolve milestones', async () => {
assert.strictEqual(await resolveBuildId('115'), '115.0.5790.170');
});
it('should resolve build prefix', async () => {
assert.strictEqual(await resolveBuildId('115.0.5790'), '115.0.5790.170');
});
});

Просмотреть файл

@ -104,6 +104,10 @@ describe('Chrome install', () => {
const cache = new Cache(tmpDir);
const installed = cache.getInstalledBrowsers();
assert.deepStrictEqual(browser, installed[0]);
assert.deepStrictEqual(
browser!.executablePath,
installed[0]?.executablePath
);
});
it('throws on invalid URL', async function () {

Просмотреть файл

@ -21,6 +21,7 @@ import {BrowserPlatform} from '../../../lib/cjs/browser-data/browser-data.js';
import {
resolveDownloadUrl,
relativeExecutablePath,
resolveBuildId,
} from '../../../lib/cjs/browser-data/chromedriver.js';
describe('ChromeDriver', () => {
@ -47,6 +48,14 @@ describe('ChromeDriver', () => {
);
});
it('should resolve milestones', async () => {
assert.strictEqual(await resolveBuildId('115'), '115.0.5790.170');
});
it('should resolve build prefix', async () => {
assert.strictEqual(await resolveBuildId('115.0.5790'), '115.0.5790.170');
});
it('should resolve executable paths', () => {
assert.strictEqual(
relativeExecutablePath(BrowserPlatform.LINUX, '12372323'),

Просмотреть файл

@ -98,5 +98,6 @@ describe('ChromeDriver install', () => {
});
assert.strictEqual(browser.path, expectedOutputPath);
assert.ok(fs.existsSync(expectedOutputPath));
assert.ok(fs.existsSync(browser.executablePath));
});
});

Просмотреть файл

@ -19,7 +19,10 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import sinon from 'sinon';
import {CLI} from '../../../lib/cjs/CLI.js';
import * as httpUtil from '../../../lib/cjs/httpUtil.js';
import {
createMockedReadlineInterface,
getServerUrl,
@ -46,6 +49,8 @@ describe('Firefox CLI', function () {
`--path=${tmpDir}`,
`--base-url=${getServerUrl()}`,
]);
sinon.restore();
});
it('should download Firefox binaries', async () => {
@ -66,6 +71,9 @@ describe('Firefox CLI', function () {
});
it('should download latest Firefox binaries', async () => {
sinon
.stub(httpUtil, 'getJSON')
.returns(Promise.resolve({FIREFOX_NIGHTLY: testFirefoxBuildId}));
await new CLI(tmpDir).run([
'npx',
'@puppeteer/browsers',

Просмотреть файл

@ -1,7 +1,8 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "../build"
},
"references": [{"path": "../../tsconfig.json"}]

Просмотреть файл

@ -16,7 +16,6 @@
export const testChromeBuildId = '113.0.5672.0';
export const testChromiumBuildId = '1083080';
// TODO: We can add a Cron job to auto-update on change.
// Firefox keeps only `latest` version of Nightly builds.
export const testFirefoxBuildId = '117.0a1';
export const testFirefoxBuildId = '119.0a1';
export const testChromeDriverBuildId = '115.0.5763.0';
export const testChromeHeadlessShellBuildId = '118.0.5950.0';

Просмотреть файл

@ -19,29 +19,33 @@
* mirrors the structure of the download server.
*/
import {BrowserPlatform, install} from '@puppeteer/browsers';
import path from 'path';
import fs from 'fs';
import {existsSync, mkdirSync, copyFileSync, rmSync} from 'fs';
import {normalize, join, dirname} from 'path';
import {BrowserPlatform, install} from '@puppeteer/browsers';
import * as versions from '../test/build/versions.js';
import {downloadPaths} from '../lib/esm/browser-data/browser-data.js';
import * as versions from '../test/build/versions.js';
function getBrowser(str) {
const regex = /test(.+)BuildId/;
const match = str.match(regex);
if (match && match[1]) {
return match[1].toLowerCase();
const lowercased = match[1].toLowerCase();
if (lowercased === 'chromeheadlessshell') {
return 'chrome-headless-shell';
}
return lowercased;
} else {
return null;
}
}
const cacheDir = path.normalize(path.join('.', 'test', 'cache'));
const cacheDir = normalize(join('.', 'test', 'cache'));
for (const version of Object.keys(versions)) {
const browser = getBrowser(version);
if (!browser) {
continue;
}
@ -49,32 +53,32 @@ for (const version of Object.keys(versions)) {
const buildId = versions[version];
for (const platform of Object.values(BrowserPlatform)) {
const targetPath = path.join(
const targetPath = join(
cacheDir,
'server',
...downloadPaths[browser](platform, buildId)
);
if (fs.existsSync(targetPath)) {
if (existsSync(targetPath)) {
continue;
}
const result = await install({
const archivePath = await install({
browser,
buildId,
platform,
cacheDir: path.join(cacheDir, 'tmp'),
cacheDir: join(cacheDir, 'tmp'),
unpack: false,
});
fs.mkdirSync(path.dirname(targetPath), {
mkdirSync(dirname(targetPath), {
recursive: true,
});
fs.copyFileSync(result.path, targetPath);
copyFileSync(archivePath, targetPath);
}
}
fs.rmSync(path.join(cacheDir, 'tmp'), {
rmSync(join(cacheDir, 'tmp'), {
recursive: true,
force: true,
maxRetries: 10,

Просмотреть файл

@ -0,0 +1,43 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'node:fs/promises';
const filePath = './test/src/versions.ts';
const getVersion = async () => {
// https://stackoverflow.com/a/1732454/96656
const response = await fetch(
'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central/'
);
const html = await response.text();
const re = /firefox-(.*)\.en-US\.langpack\.xpi">/;
const match = re.exec(html)[1];
return match;
};
const patch = (input, version) => {
const output = input.replace(/testFirefoxBuildId = '([^']+)';/, match => {
return `testFirefoxBuildId = '${version}';`;
});
return output;
};
const version = await getVersion();
const contents = await fs.readFile(filePath, 'utf8');
const patched = patch(contents, version);
fs.writeFile(filePath, patched);

Просмотреть файл

@ -1,3 +1,4 @@
# Sandbox
sandbox/
sandbox/
multi/

Просмотреть файл

@ -1,5 +1,40 @@
# Changelog
## [0.5.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.4.0...ng-schematics-v0.5.0) (2023-08-22)
### Features
* **ng-schematics:** reduce the user options and better defaults ([35dc2d8](https://github.com/puppeteer/puppeteer/commit/35dc2d884052b27a3f9c70b8646f95743be7b84d))
* **ng-schematics:** release version 0.5.0 ([#10768](https://github.com/puppeteer/puppeteer/issues/10768)) ([42fdd0a](https://github.com/puppeteer/puppeteer/commit/42fdd0a733acb2a9af3878bfa8927252f68ed465))
### Bug Fixes
* **ng-schematics:** builder is responsible for resolving commands ([683e181](https://github.com/puppeteer/puppeteer/commit/683e18189c0aedad7deb9007055a1a38801bbf08))
* **ng-schematics:** don't install for library projects ([1376b77](https://github.com/puppeteer/puppeteer/commit/1376b77a7ab2260c2fd236c3cf31abbd544193e8))
## [0.4.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.4.0) (2023-08-08)
### Features
* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.3.0) (2023-08-03)
### Features
* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.3.0...ng-schematics-v0.3.0) (2023-08-02)
### Features
* support for multi projects repos ([#10665](https://github.com/puppeteer/puppeteer/issues/10665)) ([6bca1db](https://github.com/puppeteer/puppeteer/commit/6bca1db956c44358716d52f0b9f3c012ba0b482d))
## [0.3.0](https://github.com/puppeteer/puppeteer/compare/ng-schematics-v0.2.0...ng-schematics-v0.3.0) (2023-06-29)

Просмотреть файл

@ -14,7 +14,7 @@ ng add @puppeteer/ng-schematics
Or you can use the same command followed by the [options](#options) below.
Currently, this schematic supports the following test frameworks:
Currently, this schematic supports the following test runners:
- [**Jasmine**](https://jasmine.github.io/)
- [**Jest**](https://jestjs.io/)
@ -31,12 +31,9 @@ ng e2e
When adding schematics to your project you can to provide following options:
| Option | Description | Value | Required |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | -------- |
| `--isDefaultTester` | When true, replaces default `ng e2e` command. | `boolean` | `true` |
| `--exportConfig` | When true, creates an empty [Puppeteer configuration](https://pptr.dev/guides/configuration) file. (`.puppeteerrc.cjs`) | `boolean` | `true` |
| `--testingFramework` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` |
| `--port` | The port to spawn server for E2E. If default is used `ng serve` and `ng e2e` will not run side-by-side. | `number` | `4200` |
| Option | Description | Value | Required |
| -------------- | ------------------------------------------------------ | ------------------------------------------ | -------- |
| `--testRunner` | The testing framework to install along side Puppeteer. | `"jasmine"`, `"jest"`, `"mocha"`, `"node"` | `true` |
## Creating a single test file
@ -59,7 +56,7 @@ Update either `e2e` or `puppeteer` (depending on the initial setup) to:
"options": {
"commands": [...],
"devServerTarget": "sandbox:serve",
"testingFramework": "<TestingFramework>",
"testRunner": "<TestRunner>",
"port": 8080
},
...
@ -98,6 +95,12 @@ To run the creating of single test schematic:
npm run sandbox:test
```
To create a multi project workspace use the following command
```bash
npm run sandbox -- --init --multi
```
### Unit Testing
The schematics utilize `@angular-devkit/schematics/testing` for verifying correct file creation and `package.json` updates. To execute the test suit:
@ -105,3 +108,49 @@ The schematics utilize `@angular-devkit/schematics/testing` for verifying correc
```bash
npm run test
```
## Migrating from Protractor
### Browser
Puppeteer has its own [`browser`](https://pptr.dev/api/puppeteer.browser) that exposes different API compared to the one exposed by Protractor.
```ts
import puppeteer from 'puppeteer';
(async () => {
const browser = await puppeteer.launch();
it('should work', () => {
const page = await browser.newPage();
// Query elements
const element = await page.$('my-component');
// Do actions
await element.click();
});
await browser.close();
})();
```
### Query Selectors
Puppeteer supports multiple types of selectors, namely, the CSS, ARIA, text, XPath and pierce selectors.
The following table shows Puppeteer's equivalents to [Protractor By](https://www.protractortest.org/#/api?view=ProtractorBy).
> For improved reliability and reduced flakiness try our
> **Experimental** [Locators API](https://pptr.dev/guides/locators)
| By | Protractor code | Puppeteer querySelector |
| ----------------- | --------------------------------------------- | ------------------------------------------------------------ |
| CSS (Single) | `$(by.css('<CSS>'))` | `page.$('<CSS>')` |
| CSS (Multiple) | `$$(by.css('<CSS>'))` | `page.$$('<CSS>')` |
| Id | `$(by.id('<ID>'))` | `page.$('#<ID>')` |
| CssContainingText | `$(by.cssContainingText('<CSS>', '<TEXT>'))` | `page.$('<CSS> ::-p-text(<TEXT>)')` ` |
| DeepCss | `$(by.deepCss('<CSS>'))` | `page.$(':scope >>> <CSS>')` |
| XPath | `$(by.xpath('<XPATH>'))` | `page.$('::-p-xpath(<XPATH>)')` |
| JS | `$(by.js('document.querySelector("<CSS>")'))` | `page.evaluateHandle(() => document.querySelector('<CSS>'))` |
> For advanced use cases such as Protractor's `by.addLocator` you can check Puppeteer's [Custom selectors](https://pptr.dev/guides/query-selectors#custom-selectors).

Просмотреть файл

@ -1,12 +1,12 @@
{
"name": "angular",
"version": "0.3.0",
"version": "0.5.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "angular",
"version": "0.3.0",
"version": "0.5.0",
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "^14.2.6",

Просмотреть файл

@ -1,15 +1,15 @@
{
"name": "@puppeteer/ng-schematics",
"version": "0.3.0",
"version": "0.5.0",
"description": "Puppeteer Angular schematics",
"scripts": {
"build": "wireit",
"clean": "tsc -b --clean && rm -rf lib && rm -rf test/build",
"clean": "git clean -Xdf -e '!node_modules' .",
"dev:test": "npm run test --watch",
"dev": "npm run build --watch",
"test": "wireit",
"sandbox:test": "node tools/sandbox.js --test",
"sandbox": "node tools/sandbox.js",
"sandbox:test": "node tools/sandbox.js --test"
"test": "wireit"
},
"wireit": {
"build": {
@ -48,14 +48,14 @@
"node": ">=16.3.0"
},
"dependencies": {
"@angular-devkit/architect": "^0.1601.4",
"@angular-devkit/core": "^16.1.4",
"@angular-devkit/schematics": "^16.1.4"
"@angular-devkit/architect": "^0.1602.0",
"@angular-devkit/core": "^16.2.0",
"@angular-devkit/schematics": "^16.2.0"
},
"devDependencies": {
"@types/node": "^16.11.7",
"@schematics/angular": "^16.1.4",
"@angular/cli": "^16.1.4",
"@schematics/angular": "^16.2.0",
"@angular/cli": "^16.2.0",
"rxjs": "7.8.1"
},
"files": [

Просмотреть файл

@ -9,6 +9,8 @@ import {
} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
import {TestRunner} from '../../schematics/utils/types.js';
import {PuppeteerBuilderOptions} from './types.js';
const terminalStyles = {
@ -20,44 +22,78 @@ const terminalStyles = {
clear: '\u001b[0m',
};
function getError(executable: string, args: string[]) {
return (
`Error running '${executable}' with arguments '${args.join(' ')}'.` +
`\n` +
'Please look at the output above to determine the issue!'
);
export function getCommandForRunner(runner: TestRunner): [string, ...string[]] {
switch (runner) {
case TestRunner.Jasmine:
return [`jasmine`, '--config=./e2e/jasmine.json'];
case TestRunner.Jest:
return [`jest`, '-c', 'e2e/jest.config.js'];
case TestRunner.Mocha:
return [`mocha`, '--config=./e2e/.mocharc.js'];
case TestRunner.Node:
return ['node', '--test', '--test-reporter', 'spec', 'e2e/build/'];
}
throw new Error(`Unknown test runner ${runner}!`);
}
function getExecutable(command: string[]) {
const executable = command.shift()!;
const error = getError(executable, command);
if (executable === 'node') {
return {
executable: executable,
args: command,
error,
};
}
const debugError = `Error running '${executable}' with arguments '${command.join(
' '
)}'.`;
return {
executable: `./node_modules/.bin/${executable}`,
executable,
args: command,
error,
debugError,
error: 'Please look at the output above to determine the issue!',
};
}
function updateExecutablePath(command: string, root?: string) {
if (command === TestRunner.Node) {
return command;
}
let path = 'node_modules/.bin/';
if (root && root !== '') {
const nested = root
.split('/')
.map(() => {
return '../';
})
.join('');
path = `${nested}${path}${command}`;
} else {
path = `./${path}${command}`;
}
return path;
}
async function executeCommand(context: BuilderContext, command: string[]) {
await new Promise((resolve, reject) => {
let project: JsonObject;
if (context.target) {
project = await context.getProjectMetadata(context.target.project);
command[0] = updateExecutablePath(command[0]!, String(project['root']));
}
await new Promise(async (resolve, reject) => {
context.logger.debug(`Trying to execute command - ${command.join(' ')}.`);
const {executable, args, error} = getExecutable(command);
const {executable, args, debugError, error} = getExecutable(command);
let path = context.workspaceRoot;
if (context.target) {
path = `${path}/${project['root']}`;
}
const child = spawn(executable, args, {
cwd: context.workspaceRoot,
cwd: path,
stdio: 'inherit',
});
child.on('error', message => {
context.logger.debug(debugError);
console.log(message);
reject(error);
});
@ -124,12 +160,14 @@ async function executeE2ETest(
): Promise<BuilderOutput> {
let server: BuilderRun | null = null;
try {
message('\n Building tests 🛠️ ... \n', context);
await executeCommand(context, [`tsc`, '-p', 'e2e/tsconfig.json']);
server = await startServer(options, context);
message('\n Running tests 🧪 ... \n', context);
for (const command of options.commands) {
await executeCommand(context, command);
}
const testRunnerCommand = getCommandForRunner(options.testRunner);
await executeCommand(context, testRunnerCommand);
message('\n 🚀 Test ran successfully! 🚀 ', context, 'success');
return {success: true};

Просмотреть файл

@ -16,10 +16,10 @@
import {JsonObject} from '@angular-devkit/core';
type Command = [string, ...string[]];
import {TestRunner} from '../../schematics/utils/types.js';
export interface PuppeteerBuilderOptions extends JsonObject {
commands: Command[];
testRunner: TestRunner;
devServerTarget: string;
port: number | null;
}

Просмотреть файл

@ -6,10 +6,15 @@
"factory": "./ng-add/index#ngAdd",
"schema": "./ng-add/schema.json"
},
"test": {
"e2e": {
"description": "Create a single test file",
"factory": "./test/index#test",
"schema": "./test/schema.json"
"factory": "./e2e/index#e2e",
"schema": "./e2e/schema.json"
},
"config": {
"description": "Eject Puppeteer config file",
"factory": "./config/index#config",
"schema": "./config/schema.json"
}
}
}

Просмотреть файл

@ -1,4 +1,4 @@
/**
* @type {import("puppeteer").Configuration}
*/
module.exports = {};
export {};

Просмотреть файл

@ -0,0 +1,44 @@
/**
* Copyright 2022 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {chain, Rule, SchematicContext, Tree} from '@angular-devkit/schematics';
import {addFilesSingle} from '../utils/files.js';
import {TestRunner, AngularProject} from '../utils/types.js';
// You don't have to export the function as default. You can also have more than one rule
// factory per file.
export function config(): Rule {
return (tree: Tree, context: SchematicContext) => {
return chain([addPuppeteerConfig()])(tree, context);
};
}
function addPuppeteerConfig(): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer config file.');
return addFilesSingle(tree, context, '', {root: ''} as AngularProject, {
// No-op here to fill types
options: {
testRunner: TestRunner.Jasmine,
port: 4200,
},
applyPath: './files',
relativeToWorkspacePath: `/`,
});
};
}

Просмотреть файл

@ -0,0 +1,8 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "Puppeteer",
"title": "Puppeteer Config Schema",
"type": "object",
"properties": {},
"required": []
}

Просмотреть файл

@ -1,13 +1,17 @@
<% if(testingFramework == 'node') { %>
<% if(testRunner == 'node') { %>
import * as assert from 'assert';
import {describe, it} from 'node:test';
<% } %><% if(testingFramework == 'mocha') { %>
<% } %><% if(testRunner == 'mocha') { %>
import * as assert from 'assert';
<% } %>
import {setupBrowserHooks, getBrowserState} from './utils';
describe('<%= classify(name) %>', function () {
<% if(route) { %>
setupBrowserHooks('<%= route %>');
<% } else { %>
setupBrowserHooks();
<% } %>
it('', async function () {
const {page} = getBrowserState();
});

Просмотреть файл

@ -22,27 +22,52 @@ import {
Tree,
} from '@angular-devkit/schematics';
import {addBaseFiles} from '../utils/files.js';
import {getAngularConfig} from '../utils/json.js';
import {addCommonFiles} from '../utils/files.js';
import {getApplicationProjects} from '../utils/json.js';
import {
TestingFramework,
TestRunner,
SchematicsSpec,
SchematicsOptions,
AngularProject,
PuppeteerSchematicsConfig,
} from '../utils/types.js';
// You don't have to export the function as default. You can also have more than one rule
// factory per file.
export function test(options: SchematicsSpec): Rule {
export function e2e(userArgs: Record<string, string>): Rule {
const options = parseUserTestArgs(userArgs);
return (tree: Tree, context: SchematicContext) => {
return chain([addSpecFile(options)])(tree, context);
return chain([addE2EFile(options)])(tree, context);
};
}
function findTestingOption<Property extends keyof SchematicsOptions>(
function parseUserTestArgs(userArgs: Record<string, string>): SchematicsSpec {
const options: Partial<SchematicsSpec> = {
...userArgs,
};
if ('p' in userArgs) {
options['project'] = userArgs['p'];
}
if ('n' in userArgs) {
options['name'] = userArgs['n'];
}
if ('r' in userArgs) {
options['route'] = userArgs['r'];
}
if (options['route'] && !options['route'].startsWith('/')) {
options['route'] = `/${options['route']}`;
}
return options as SchematicsSpec;
}
function findTestingOption<
Property extends keyof PuppeteerSchematicsConfig['options'],
>(
[name, project]: [string, AngularProject | undefined],
property: Property
): SchematicsOptions[Property] {
): PuppeteerSchematicsConfig['options'][Property] {
if (!project) {
throw new Error(`Project "${name}" not found.`);
}
@ -60,11 +85,11 @@ function findTestingOption<Property extends keyof SchematicsOptions>(
throw new Error(`Can't find property "${property}" for project "${name}".`);
}
function addSpecFile(options: SchematicsSpec): Rule {
function addE2EFile(options: SchematicsSpec): Rule {
return async (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Spec file.');
const {projects} = getAngularConfig(tree);
const projects = getApplicationProjects(tree);
const projectNames = Object.keys(projects) as [string, ...string[]];
const foundProject: [string, AngularProject | undefined] | undefined =
projectNames.length === 1
@ -76,28 +101,30 @@ function addSpecFile(options: SchematicsSpec): Rule {
});
if (!foundProject) {
throw new SchematicsException(
`Project not found! Please use -p to specify in which project to run.`
`Project not found! Please run "ng generate @puppeteer/ng-schematics:test <Test> <Project>"`
);
}
const testingFramework = findTestingOption(
foundProject,
'testingFramework'
);
const testRunner = findTestingOption(foundProject, 'testRunner');
const port = findTestingOption(foundProject, 'port');
context.logger.debug('Creating Spec file.');
return addBaseFiles(tree, context, {
projects: {[foundProject[0]]: foundProject[1]},
options: {
name: options.name,
testingFramework,
// Node test runner does not support glob patterns
// It looks for files `*.test.js`
ext: testingFramework === TestingFramework.Node ? 'test' : 'e2e',
port,
},
});
return addCommonFiles(
tree,
context,
{[foundProject[0]]: foundProject[1]} as Record<string, AngularProject>,
{
options: {
name: options.name,
route: options.route,
testRunner,
// Node test runner does not support glob patterns
// It looks for files `*.test.js`
ext: testRunner === TestRunner.Node ? 'test' : 'e2e',
port,
},
}
);
};
}

Просмотреть файл

@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "Puppeteer",
"title": "Puppeteer Spec Schema",
"title": "Puppeteer E2E Schema",
"type": "object",
"properties": {
"name": {
@ -15,7 +15,19 @@
},
"project": {
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"alias": "p"
},
"route": {
"type": "string",
"$default": {
"$source": "argv",
"index": 1
},
"alias": "r"
}
},
"required": []

Просмотреть файл

@ -1,11 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {<% if(testingFramework == 'jest') { %>
"esModuleInterop": true,<% } %><% if(testingFramework == 'node') { %>
"module": "CommonJS",
"rootDir": "tests/",
"outDir": "build/",<% } %>
"types": ["<%= testingFramework %>"]
},
"include": ["tests/**/*.ts"]
}

Просмотреть файл

@ -0,0 +1,2 @@
# Compiled e2e tests output
build/

Просмотреть файл

@ -1,7 +1,7 @@
<% if(testingFramework == 'node') { %>
<% if(testRunner == 'node') { %>
import * as assert from 'assert';
import {describe, it} from 'node:test';
<% } %><% if(testingFramework == 'mocha') { %>
<% } %><% if(testRunner == 'mocha') { %>
import * as assert from 'assert';
<% } %>
import {setupBrowserHooks, getBrowserState} from './utils';
@ -10,11 +10,11 @@ describe('App test', function () {
setupBrowserHooks();
it('is running', async function () {
const {page} = getBrowserState();
const element = await page.waitForSelector('text/sandbox app is running!');
const element = await page.waitForSelector('text/<%= project %> app is running!');
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>
<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
expect(element).not.toBeNull();
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
assert.ok(element);
<% } %>
});

Просмотреть файл

@ -1,4 +1,4 @@
<% if(testingFramework == 'node') { %>
<% if(testRunner == 'node') { %>
import {before, beforeEach, after, afterEach} from 'node:test';
<% } %>
import * as puppeteer from 'puppeteer';
@ -7,33 +7,35 @@ const baseUrl = '<%= baseUrl %>';
let browser: puppeteer.Browser;
let page: puppeteer.Page;
export function setupBrowserHooks(): void {
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>
export function setupBrowserHooks(path = '/'): void {
<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
beforeAll(async () => {
browser = await puppeteer.launch({
headless: 'new'
});
});
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
before(async () => {
browser = await puppeteer.launch();
browser = await puppeteer.launch({
headless: 'new'
});
});
<% } %>
beforeEach(async () => {
page = await browser.newPage();
await page.goto(baseUrl);
await page.goto(`${baseUrl}${path}`);
});
afterEach(async () => {
await page.close();
});
<% if(testingFramework == 'jasmine' || testingFramework == 'jest') { %>
<% if(testRunner == 'jasmine' || testRunner == 'jest') { %>
afterAll(async () => {
await browser.close();
});
<% } %><% if(testingFramework == 'mocha' || testingFramework == 'node') { %>
<% } %><% if(testRunner == 'mocha' || testRunner == 'node') { %>
after(async () => {
await browser.close();
});

Просмотреть файл

@ -0,0 +1,10 @@
{
"extends": "<%= tsConfigPath %>",
"compilerOptions": {
"module": "CommonJS",
"rootDir": "tests/",
"outDir": "build/",
"types": ["<%= testRunner %>"]
},
"include": ["tests/**/*.ts"]
}

Просмотреть файл

@ -1,4 +0,0 @@
require('@babel/register')({
extensions: ['.js', '.ts'],
presets: ['@babel/preset-env', '@babel/preset-typescript'],
});

Просмотреть файл

@ -0,0 +1,10 @@
{
"spec_dir": "e2e",
"spec_files": ["**/*[eE]2[eE].js"],
"helpers": ["helpers/**/*.?(m)js"],
"env": {
"failSpecWithNoExpectations": true,
"stopSpecOnExpectationFailure": false,
"random": true
}
}

Просмотреть файл

@ -1,9 +0,0 @@
{
"spec_dir": "e2e",
"spec_files": ["**/*[eE]2[eE].ts"],
"helpers": ["helpers/babel.js", "helpers/**/*.{js|ts}"],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true
}
}

Просмотреть файл

@ -3,9 +3,8 @@
* https://jestjs.io/docs/configuration
*/
/** @type {import('ts-jest').JestConfigWithTsJest} */
/** @type {import('jest').Config} */
module.exports = {
testMatch: ['<rootDir>/tests/**/?(*.)+(e2e).[tj]s?(x)'],
preset: 'ts-jest',
testMatch: ['<rootDir>/build/**/?(*.)+(e2e).js?(x)'],
testEnvironment: 'node',
};

Просмотреть файл

@ -1,4 +1,3 @@
module.exports = {
file: ['e2e/babel.js'],
spec: './e2e/tests/**/*.e2e.ts',
spec: './e2e/build/**/*.e2e.js',
};

Просмотреть файл

@ -1,4 +0,0 @@
require('@babel/register')({
extensions: ['.js', '.ts'],
presets: ['@babel/preset-env', '@babel/preset-typescript'],
});

Просмотреть файл

@ -1,3 +0,0 @@
# Compiled e2e tests output Node auto resolves files in folders named 'test'
build/

Просмотреть файл

@ -20,11 +20,12 @@ import {of} from 'rxjs';
import {concatMap, map, scan} from 'rxjs/operators';
import {
addBaseFiles,
addCommonFiles as addCommonFilesHelper,
addFrameworkFiles,
getNgCommandName,
hasE2ETester,
} from '../utils/files.js';
import {getAngularConfig} from '../utils/json.js';
import {getApplicationProjects} from '../utils/json.js';
import {
addPackageJsonDependencies,
addPackageJsonScripts,
@ -34,7 +35,9 @@ import {
type NodePackage,
updateAngularJsonScripts,
} from '../utils/packages.js';
import {TestingFramework, type SchematicsOptions} from '../utils/types.js';
import {TestRunner, type SchematicsOptions} from '../utils/types.js';
const DEFAULT_PORT = 4200;
// You don't have to export the function as default. You can also have more than one rule
// factory per file.
@ -42,9 +45,9 @@ export function ngAdd(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
return chain([
addDependencies(options),
addPuppeteerFiles(options),
addCommonFiles(options),
addOtherFiles(options),
updateScripts(options),
updateScripts(),
updateAngularConfig(options),
])(tree, context);
};
@ -74,15 +77,15 @@ function addDependencies(options: SchematicsOptions): Rule {
};
}
function updateScripts(options: SchematicsOptions): Rule {
function updateScripts(): Rule {
return (tree: Tree, context: SchematicContext): Tree => {
context.logger.debug('Updating "package.json" scripts');
const angularJson = getAngularConfig(tree);
const projects = Object.keys(angularJson['projects']);
const projects = getApplicationProjects(tree);
const projectsKeys = Object.keys(projects);
if (projects.length === 1) {
const name = getNgCommandName(options);
const prefix = options.isDefaultTester ? '' : `run ${projects[0]}:`;
if (projectsKeys.length === 1) {
const name = getNgCommandName(projects);
const prefix = hasE2ETester(projects) ? `run ${projectsKeys[0]}:` : '';
return addPackageJsonScripts(tree, [
{
name,
@ -94,17 +97,16 @@ function updateScripts(options: SchematicsOptions): Rule {
};
}
function addPuppeteerFiles(options: SchematicsOptions): Rule {
function addCommonFiles(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer base files.');
const {projects} = getAngularConfig(tree);
const projects = getApplicationProjects(tree);
return addBaseFiles(tree, context, {
projects,
return addCommonFilesHelper(tree, context, projects, {
options: {
...options,
ext:
options.testingFramework === TestingFramework.Node ? 'test' : 'e2e',
port: DEFAULT_PORT,
ext: options.testRunner === TestRunner.Node ? 'test' : 'e2e',
},
});
};
@ -113,11 +115,13 @@ function addPuppeteerFiles(options: SchematicsOptions): Rule {
function addOtherFiles(options: SchematicsOptions): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.debug('Adding Puppeteer additional files.');
const {projects} = getAngularConfig(tree);
const projects = getApplicationProjects(tree);
return addFrameworkFiles(tree, context, {
projects,
options,
return addFrameworkFiles(tree, context, projects, {
options: {
...options,
port: DEFAULT_PORT,
},
});
};
}

Просмотреть файл

@ -4,25 +4,13 @@
"title": "Puppeteer Install Schema",
"type": "object",
"properties": {
"isDefaultTester": {
"type": "boolean",
"default": true,
"alias": "d",
"x-prompt": "Use Puppeteer as default `ng e2e` command?"
},
"exportConfig": {
"type": "boolean",
"default": false,
"alias": "c",
"x-prompt": "Export default Puppeteer config file?"
},
"testingFramework": {
"testRunner": {
"type": "string",
"enum": ["jasmine", "jest", "mocha", "node"],
"default": "jasmine",
"alias": "t",
"x-prompt": {
"message": "With what Testing Library do you wish to integrate?",
"message": "Which test runners do you wish to use?",
"type": "list",
"items": [
{
@ -43,12 +31,6 @@
}
]
}
},
"port": {
"type": ["number"],
"default": 4200,
"alias": "p",
"x-prompt": "On which port to spawn test server on?"
}
},
"required": []

Просмотреть файл

@ -23,79 +23,80 @@ import {
apply,
applyTemplates,
chain,
filter,
mergeWith,
move,
url,
} from '@angular-devkit/schematics';
import {SchematicsOptions, TestingFramework} from './types.js';
import {AngularProject, TestRunner} from './types.js';
export interface FilesOptions {
projects: Record<string, any>;
options: {
testingFramework: TestingFramework;
testRunner: TestRunner;
port: number;
name?: string;
exportConfig?: boolean;
ext?: string;
route?: string;
};
applyPath: string;
relativeToWorkspacePath: string;
movePath?: string;
filterPredicate?: (path: string) => boolean;
}
const PUPPETEER_CONFIG_TEMPLATE = '.puppeteerrc.cjs.template';
export function addFiles(
export function addFilesToProjects(
tree: Tree,
context: SchematicContext,
{
projects,
options,
applyPath,
movePath,
relativeToWorkspacePath,
filterPredicate,
}: FilesOptions
projects: Record<string, AngularProject>,
options: FilesOptions
): any {
return chain(
Object.keys(projects).map(name => {
const project = projects[name];
const projectPath = resolve(getSystemPath(normalize(project.root)));
const workspacePath = resolve(getSystemPath(normalize('')));
const relativeToWorkspace = relative(
`${projectPath}${relativeToWorkspacePath}`,
workspacePath
);
const baseUrl = getProjectBaseUrl(project, options.port);
return mergeWith(
apply(url(applyPath), [
filter(
filterPredicate ??
(() => {
return true;
})
),
move(movePath ? `${project.root}${movePath}` : project.root),
applyTemplates({
...options,
...strings,
root: project.root ? `${project.root}/` : project.root,
baseUrl,
project: name,
relativeToWorkspace,
}),
])
return addFilesSingle(
tree,
context,
name,
projects[name] as AngularProject,
options
);
})
)(tree, context);
}
export function addFilesSingle(
_tree: Tree,
_context: SchematicContext,
name: string,
project: AngularProject,
{options, applyPath, movePath, relativeToWorkspacePath}: FilesOptions
): any {
const projectPath = resolve(getSystemPath(normalize(project.root)));
const workspacePath = resolve(getSystemPath(normalize('')));
const relativeToWorkspace = relative(
`${projectPath}${relativeToWorkspacePath}`,
workspacePath
);
const baseUrl = getProjectBaseUrl(project, options.port);
const tsConfigPath = getTsConfigPath(project);
return mergeWith(
apply(url(applyPath), [
move(movePath ? `${project.root}${movePath}` : project.root),
applyTemplates({
...options,
...strings,
root: project.root ? `${project.root}/` : project.root,
baseUrl,
tsConfigPath,
project: name,
relativeToWorkspace,
}),
])
);
}
function getProjectBaseUrl(project: any, port: number): string {
let options = {protocol: 'http', port, host: 'localhost'};
@ -109,59 +110,56 @@ function getProjectBaseUrl(project: any, port: number): string {
return `${options.protocol}://${options.host}:${options.port}`;
}
export function addBaseFiles(
function getTsConfigPath(project: AngularProject): string {
if (!project.root) {
return '../tsconfig.json';
}
return `../tsconfig.app.json`;
}
export function addCommonFiles(
tree: Tree,
context: SchematicContext,
projects: Record<string, AngularProject>,
filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
): any {
const options: FilesOptions = {
...filesOptions,
applyPath: './files/base',
applyPath: './files/common',
relativeToWorkspacePath: `/`,
filterPredicate: path => {
return path.includes(PUPPETEER_CONFIG_TEMPLATE) &&
!filesOptions.options.exportConfig
? false
: true;
},
};
return addFiles(tree, context, options);
return addFilesToProjects(tree, context, projects, options);
}
export function addFrameworkFiles(
tree: Tree,
context: SchematicContext,
projects: Record<string, AngularProject>,
filesOptions: Omit<FilesOptions, 'applyPath' | 'relativeToWorkspacePath'>
): any {
const testingFramework = filesOptions.options.testingFramework;
const testRunner = filesOptions.options.testRunner;
const options: FilesOptions = {
...filesOptions,
applyPath: `./files/${testingFramework}`,
applyPath: `./files/${testRunner}`,
relativeToWorkspacePath: `/`,
};
return addFiles(tree, context, options);
return addFilesToProjects(tree, context, projects, options);
}
export function getScriptFromOptions(options: SchematicsOptions): string[][] {
switch (options.testingFramework) {
case TestingFramework.Jasmine:
return [[`jasmine`, '--config=./e2e/support/jasmine.json']];
case TestingFramework.Jest:
return [[`jest`, '-c', 'e2e/jest.config.js']];
case TestingFramework.Mocha:
return [[`mocha`, '--config=./e2e/.mocharc.js']];
case TestingFramework.Node:
return [
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', '--test-reporter', 'spec', 'e2e/build/'],
];
}
export function hasE2ETester(
projects: Record<string, AngularProject>
): boolean {
return Object.values(projects).some((project: AngularProject) => {
return Boolean(project.architect?.e2e);
});
}
export function getNgCommandName(options: SchematicsOptions): string {
if (options.isDefaultTester) {
export function getNgCommandName(
projects: Record<string, AngularProject>
): string {
if (!hasE2ETester(projects)) {
return 'e2e';
}
return 'puppeteer';

Просмотреть файл

@ -16,7 +16,7 @@
import {SchematicsException, Tree} from '@angular-devkit/schematics';
import {AngularJson} from './types.js';
import type {AngularJson, AngularProject} from './types.js';
export function getJsonFileAsObject(
tree: Tree,
@ -38,3 +38,18 @@ export function getObjectAsJson(object: Record<string, any>): string {
export function getAngularConfig(tree: Tree): AngularJson {
return getJsonFileAsObject(tree, './angular.json') as AngularJson;
}
export function getApplicationProjects(
tree: Tree
): Record<string, AngularProject> {
const {projects} = getAngularConfig(tree);
const applications: Record<string, AngularProject> = {};
for (const key in projects) {
const project = projects[key]!;
if (project.projectType === 'application') {
applications[key] = project;
}
}
return applications;
}

Просмотреть файл

@ -18,13 +18,14 @@ import {get} from 'https';
import {Tree} from '@angular-devkit/schematics';
import {getNgCommandName, getScriptFromOptions} from './files.js';
import {getNgCommandName} from './files.js';
import {
getAngularConfig,
getApplicationProjects,
getJsonFileAsObject,
getObjectAsJson,
} from './json.js';
import {SchematicsOptions, TestingFramework} from './types.js';
import {SchematicsOptions, TestRunner} from './types.js';
export interface NodePackage {
name: string;
version: string;
@ -115,24 +116,18 @@ export function getDependenciesFromOptions(
options: SchematicsOptions
): string[] {
const dependencies = ['puppeteer'];
const babelPackages = [
'@babel/core',
'@babel/register',
'@babel/preset-env',
'@babel/preset-typescript',
];
switch (options.testingFramework) {
case TestingFramework.Jasmine:
dependencies.push('jasmine', ...babelPackages);
switch (options.testRunner) {
case TestRunner.Jasmine:
dependencies.push('jasmine');
break;
case TestingFramework.Jest:
dependencies.push('jest', '@types/jest', 'ts-jest');
case TestRunner.Jest:
dependencies.push('jest', '@types/jest');
break;
case TestingFramework.Mocha:
dependencies.push('mocha', '@types/mocha', ...babelPackages);
case TestRunner.Mocha:
dependencies.push('mocha', '@types/mocha');
break;
case TestingFramework.Node:
case TestRunner.Node:
dependencies.push('@types/node');
break;
}
@ -168,21 +163,18 @@ export function updateAngularJsonScripts(
overwrite = true
): Tree {
const angularJson = getAngularConfig(tree);
const commands = getScriptFromOptions(options);
const name = getNgCommandName(options);
const port = options.port !== 4200 ? Number(options.port) : undefined;
const projects = getApplicationProjects(tree);
const name = getNgCommandName(projects);
Object.keys(angularJson['projects']).forEach(project => {
Object.keys(projects).forEach(project => {
const e2eScript = [
{
name,
value: {
builder: '@puppeteer/ng-schematics:puppeteer',
options: {
commands,
devServerTarget: `${project}:serve`,
testingFramework: options.testingFramework,
port,
testRunner: options.testRunner,
},
configurations: {
production: {

Просмотреть файл

@ -14,7 +14,7 @@
* limitations under the License.
*/
export enum TestingFramework {
export enum TestRunner {
Jasmine = 'jasmine',
Jest = 'jest',
Mocha = 'mocha',
@ -22,17 +22,18 @@ export enum TestingFramework {
}
export interface SchematicsOptions {
isDefaultTester: boolean;
exportConfig: boolean;
testingFramework: TestingFramework;
port: number;
testRunner: TestRunner;
}
export interface PuppeteerSchematicsConfig {
builder: string;
options: SchematicsOptions;
options: {
port: number;
testRunner: TestRunner;
};
}
export interface AngularProject {
projectType: 'application' | 'library';
root: string;
architect: {
e2e?: PuppeteerSchematicsConfig;
@ -46,4 +47,5 @@ export interface AngularJson {
export interface SchematicsSpec {
name: string;
project?: string;
route?: string;
}

Просмотреть файл

@ -0,0 +1,28 @@
import expect from 'expect';
import {
buildTestingTree,
getMultiApplicationFile,
setupHttpHooks,
} from './utils.js';
describe('@puppeteer/ng-schematics: config', () => {
setupHttpHooks();
describe('Single Project', () => {
it('should create default file', async () => {
const tree = await buildTestingTree('config', 'single');
expect(tree.files).toContain('/.puppeteerrc.mjs');
});
});
describe('Multi projects', () => {
it('should create default file', async () => {
const tree = await buildTestingTree('config', 'multi');
expect(tree.files).toContain('/.puppeteerrc.mjs');
expect(tree.files).not.toContain(
getMultiApplicationFile('.puppeteerrc.mjs')
);
});
});
});

Просмотреть файл

@ -0,0 +1,109 @@
import expect from 'expect';
import {
buildTestingTree,
getMultiApplicationFile,
setupHttpHooks,
} from './utils.js';
describe('@puppeteer/ng-schematics: e2e', () => {
setupHttpHooks();
describe('Single Project', () => {
it('should create default file', async () => {
const tree = await buildTestingTree('e2e', 'single', {
name: 'myTest',
});
expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
expect(tree.files).not.toContain('/e2e/tests/my-test.test.ts');
});
it('should create Node file', async () => {
const tree = await buildTestingTree('e2e', 'single', {
name: 'myTest',
testRunner: 'node',
});
expect(tree.files).not.toContain('/e2e/tests/my-test.e2e.ts');
expect(tree.files).toContain('/e2e/tests/my-test.test.ts');
});
it('should create file with route', async () => {
const route = 'home';
const tree = await buildTestingTree('e2e', 'single', {
name: 'myTest',
route,
});
expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
expect(tree.readContent('/e2e/tests/my-test.e2e.ts')).toContain(
`setupBrowserHooks('/${route}');`
);
});
it('should create with route with starting slash', async () => {
const route = '/home';
const tree = await buildTestingTree('e2e', 'single', {
name: 'myTest',
route,
});
expect(tree.files).toContain('/e2e/tests/my-test.e2e.ts');
expect(tree.readContent('/e2e/tests/my-test.e2e.ts')).toContain(
`setupBrowserHooks('${route}');`
);
});
});
describe('Multi projects', () => {
it('should create default file', async () => {
const tree = await buildTestingTree('e2e', 'multi', {
name: 'myTest',
});
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
);
expect(tree.files).not.toContain(
getMultiApplicationFile('e2e/tests/my-test.test.ts')
);
});
it('should create Node file', async () => {
const tree = await buildTestingTree('e2e', 'multi', {
name: 'myTest',
testRunner: 'node',
});
expect(tree.files).not.toContain(
getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
);
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tests/my-test.test.ts')
);
});
it('should create file with route', async () => {
const route = 'home';
const tree = await buildTestingTree('e2e', 'multi', {
name: 'myTest',
route,
});
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
);
expect(
tree.readContent(getMultiApplicationFile('e2e/tests/my-test.e2e.ts'))
).toContain(`setupBrowserHooks('/${route}');`);
});
it('should create with route with starting slash', async () => {
const route = '/home';
const tree = await buildTestingTree('e2e', 'multi', {
name: 'myTest',
route,
});
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tests/my-test.e2e.ts')
);
expect(
tree.readContent(getMultiApplicationFile('e2e/tests/my-test.e2e.ts'))
).toContain(`setupBrowserHooks('${route}');`);
});
});
});

Просмотреть файл

@ -1,140 +1,232 @@
import expect from 'expect';
import {
MULTI_LIBRARY_OPTIONS,
buildTestingTree,
getAngularJsonScripts,
getMultiApplicationFile,
getMultiLibraryFile,
getPackageJson,
getProjectFile,
runSchematic,
setupHttpHooks,
} from './utils.js';
describe('@puppeteer/ng-schematics: ng-add', () => {
setupHttpHooks();
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree('ng-add');
const {devDependencies, scripts} = getPackageJson(tree);
const {builder, configurations} = getAngularJsonScripts(tree);
describe('Single Project', () => {
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree('ng-add');
const {devDependencies, scripts} = getPackageJson(tree);
const {builder, configurations} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/tsconfig.json'));
expect(tree.files).toContain(getProjectFile('e2e/tests/app.e2e.ts'));
expect(tree.files).toContain(getProjectFile('e2e/tests/utils.ts'));
expect(devDependencies).toContain('puppeteer');
expect(scripts['e2e']).toBe('ng e2e');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
expect(configurations).toEqual({
production: {
devServerTarget: 'sandbox:serve:production',
},
expect(tree.files).toContain('/e2e/tsconfig.json');
expect(tree.files).toContain('/e2e/tests/app.e2e.ts');
expect(tree.files).toContain('/e2e/tests/utils.ts');
expect(devDependencies).toContain('puppeteer');
expect(scripts['e2e']).toBe('ng e2e');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
expect(configurations).toEqual({
production: {
devServerTarget: 'sandbox:serve:production',
},
});
});
it('should update create proper "ng" command for non default tester', async () => {
let tree = await buildTestingTree('ng-add', 'single');
// Re-run schematic to have e2e populated
tree = await runSchematic(tree, 'ng-add');
const {scripts} = getPackageJson(tree);
const {builder} = getAngularJsonScripts(tree, false);
expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
});
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', 'single');
expect(files).not.toContain('/.puppeteerrc.cjs');
});
it('should create Jasmine files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'single', {
testRunner: 'jasmine',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain('/e2e/jasmine.json');
expect(devDependencies).toContain('jasmine');
expect(options['testRunner']).toBe('jasmine');
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'single', {
testRunner: 'jest',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain('/e2e/jest.config.js');
expect(devDependencies).toContain('jest');
expect(devDependencies).toContain('@types/jest');
expect(options['testRunner']).toBe('jest');
});
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'single', {
testRunner: 'mocha',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain('/e2e/.mocharc.js');
expect(devDependencies).toContain('mocha');
expect(devDependencies).toContain('@types/mocha');
expect(options['testRunner']).toBe('mocha');
});
it('should create Node files', async () => {
const tree = await buildTestingTree('ng-add', 'single', {
testRunner: 'node',
});
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain('/e2e/.gitignore');
expect(tree.files).not.toContain('/e2e/tests/app.e2e.ts');
expect(tree.files).toContain('/e2e/tests/app.test.ts');
expect(options['testRunner']).toBe('node');
});
it('should not create port value', async () => {
const tree = await buildTestingTree('ng-add');
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBeUndefined();
});
});
it('should update create proper "ng" command for non default tester', async () => {
const tree = await buildTestingTree('ng-add', {
isDefaultTester: false,
});
const {scripts} = getPackageJson(tree);
const {builder} = getAngularJsonScripts(tree, false);
describe('Multi projects Application', () => {
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi');
const {devDependencies, scripts} = getPackageJson(tree);
const {builder, configurations} = getAngularJsonScripts(tree);
expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tsconfig.json')
);
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tests/app.e2e.ts')
);
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tests/utils.ts')
);
expect(devDependencies).toContain('puppeteer');
expect(scripts['e2e']).toBe('ng e2e');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
expect(configurations).toEqual({
production: {
devServerTarget: 'sandbox:serve:production',
},
});
});
it('should update create proper "ng" command for non default tester', async () => {
let tree = await buildTestingTree('ng-add', 'multi');
// Re-run schematic to have e2e populated
tree = await runSchematic(tree, 'ng-add');
const {scripts} = getPackageJson(tree);
const {builder} = getAngularJsonScripts(tree, false);
expect(scripts['puppeteer']).toBe('ng run sandbox:puppeteer');
expect(builder).toBe('@puppeteer/ng-schematics:puppeteer');
});
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', 'multi');
expect(files).not.toContain(getMultiApplicationFile('.puppeteerrc.cjs'));
expect(files).not.toContain('/.puppeteerrc.cjs');
});
it('should create Jasmine files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testRunner: 'jasmine',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getMultiApplicationFile('e2e/jasmine.json'));
expect(devDependencies).toContain('jasmine');
expect(options['testRunner']).toBe('jasmine');
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testRunner: 'jest',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(
getMultiApplicationFile('e2e/jest.config.js')
);
expect(devDependencies).toContain('jest');
expect(devDependencies).toContain('@types/jest');
expect(options['testRunner']).toBe('jest');
});
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testRunner: 'mocha',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getMultiApplicationFile('e2e/.mocharc.js'));
expect(devDependencies).toContain('mocha');
expect(devDependencies).toContain('@types/mocha');
expect(options['testRunner']).toBe('mocha');
});
it('should create Node files', async () => {
const tree = await buildTestingTree('ng-add', 'multi', {
testRunner: 'node',
});
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getMultiApplicationFile('e2e/.gitignore'));
expect(tree.files).not.toContain(
getMultiApplicationFile('e2e/tests/app.e2e.ts')
);
expect(tree.files).toContain(
getMultiApplicationFile('e2e/tests/app.test.ts')
);
expect(options['testRunner']).toBe('node');
});
it('should not create port value', async () => {
const tree = await buildTestingTree('ng-add');
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBeUndefined();
});
});
it('should create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', {
exportConfig: true,
describe('Multi projects Library', () => {
it('should create base files and update to "package.json"', async () => {
const tree = await buildTestingTree('ng-add', 'multi');
const config = getAngularJsonScripts(
tree,
true,
MULTI_LIBRARY_OPTIONS.name
);
expect(tree.files).not.toContain(
getMultiLibraryFile('e2e/tsconfig.json')
);
expect(tree.files).not.toContain(
getMultiLibraryFile('e2e/tests/app.e2e.ts')
);
expect(tree.files).not.toContain(
getMultiLibraryFile('e2e/tests/utils.ts')
);
expect(config).toBeUndefined();
});
expect(files).toContain(getProjectFile('.puppeteerrc.cjs'));
});
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', 'multi');
it('should not create Puppeteer config', async () => {
const {files} = await buildTestingTree('ng-add', {
exportConfig: false,
expect(files).not.toContain(getMultiLibraryFile('.puppeteerrc.cjs'));
expect(files).not.toContain('/.puppeteerrc.cjs');
});
expect(files).not.toContain(getProjectFile('.puppeteerrc.cjs'));
});
it('should create Jasmine files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', {
testingFramework: 'jasmine',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/support/jasmine.json'));
expect(tree.files).toContain(getProjectFile('e2e/helpers/babel.js'));
expect(devDependencies).toContain('jasmine');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(options['commands']).toEqual([
[`jasmine`, '--config=./e2e/support/jasmine.json'],
]);
});
it('should create Jest files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', {
testingFramework: 'jest',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/jest.config.js'));
expect(devDependencies).toContain('jest');
expect(devDependencies).toContain('@types/jest');
expect(devDependencies).toContain('ts-jest');
expect(options['commands']).toEqual([[`jest`, '-c', 'e2e/jest.config.js']]);
});
it('should create Mocha files and update "package.json"', async () => {
const tree = await buildTestingTree('ng-add', {
testingFramework: 'mocha',
});
const {devDependencies} = getPackageJson(tree);
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/.mocharc.js'));
expect(tree.files).toContain(getProjectFile('e2e/babel.js'));
expect(devDependencies).toContain('mocha');
expect(devDependencies).toContain('@types/mocha');
expect(devDependencies).toContain('@babel/core');
expect(devDependencies).toContain('@babel/register');
expect(devDependencies).toContain('@babel/preset-typescript');
expect(options['commands']).toEqual([
[`mocha`, '--config=./e2e/.mocharc.js'],
]);
});
it('should create Node files', async () => {
const tree = await buildTestingTree('ng-add', {
testingFramework: 'node',
});
const {options} = getAngularJsonScripts(tree);
expect(tree.files).toContain(getProjectFile('e2e/.gitignore'));
expect(tree.files).not.toContain(getProjectFile('e2e/tests/app.e2e.ts'));
expect(tree.files).toContain(getProjectFile('e2e/tests/app.test.ts'));
expect(options['commands']).toEqual([
[`tsc`, '-p', 'e2e/tsconfig.json'],
['node', '--test', '--test-reporter', 'spec', 'e2e/build/'],
]);
});
it('should not create port option', async () => {
const tree = await buildTestingTree('ng-add');
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBeUndefined();
});
it('should create port option when specified', async () => {
const port = 8080;
const tree = await buildTestingTree('ng-add', {
port,
});
const {options} = getAngularJsonScripts(tree);
expect(options['port']).toBe(port);
});
});

Просмотреть файл

@ -1,28 +0,0 @@
import expect from 'expect';
import {buildTestingTree, getProjectFile, setupHttpHooks} from './utils.js';
describe('@puppeteer/ng-schematics: test', () => {
setupHttpHooks();
it('should create default file', async () => {
const tree = await buildTestingTree('test', {
name: 'myTest',
});
expect(tree.files).toContain(getProjectFile('e2e/tests/my-test.e2e.ts'));
expect(tree.files).not.toContain(
getProjectFile('e2e/tests/my-test.test.ts')
);
});
it('should create Node file', async () => {
const tree = await buildTestingTree('test', {
name: 'myTest',
testingFramework: 'node',
});
expect(tree.files).not.toContain(
getProjectFile('e2e/tests/my-test.e2e.ts')
);
expect(tree.files).toContain(getProjectFile('e2e/tests/my-test.test.ts'));
});
});

Просмотреть файл

@ -14,8 +14,19 @@ const WORKSPACE_OPTIONS = {
version: '14.0.0',
};
const APPLICATION_OPTIONS = {
const SINGLE_APPLICATION_OPTIONS = {
name: 'sandbox',
directory: '.',
createApplication: true,
version: '14.0.0',
};
const MULTI_APPLICATION_OPTIONS = {
name: SINGLE_APPLICATION_OPTIONS.name,
};
export const MULTI_LIBRARY_OPTIONS = {
name: 'components',
};
export function setupHttpHooks(): void {
@ -34,13 +45,10 @@ export function setupHttpHooks(): void {
});
}
export function getProjectFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${APPLICATION_OPTIONS.name}/${file}`;
}
export function getAngularJsonScripts(
tree: UnitTestTree,
isDefault = true
isDefault = true,
name = SINGLE_APPLICATION_OPTIONS.name
): {
builder: string;
configurations: Record<string, any>;
@ -48,9 +56,7 @@ export function getAngularJsonScripts(
} {
const angularJson = tree.readJson('angular.json') as any;
const e2eScript = isDefault ? 'e2e' : 'puppeteer';
return angularJson['projects']?.[APPLICATION_OPTIONS.name]?.['architect'][
e2eScript
];
return angularJson['projects']?.[name]?.['architect'][e2eScript];
}
export function getPackageJson(tree: UnitTestTree): {
@ -66,8 +72,16 @@ export function getPackageJson(tree: UnitTestTree): {
};
}
export function getMultiApplicationFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${MULTI_APPLICATION_OPTIONS.name}/${file}`;
}
export function getMultiLibraryFile(file: string): string {
return `/${WORKSPACE_OPTIONS.newProjectRoot}/${MULTI_LIBRARY_OPTIONS.name}/${file}`;
}
export async function buildTestingTree(
command: 'ng-add' | 'test',
command: 'ng-add' | 'e2e' | 'config',
type: 'single' | 'multi' = 'single',
userOptions?: Record<string, any>
): Promise<UnitTestTree> {
const runner = new SchematicTestRunner(
@ -75,26 +89,40 @@ export async function buildTestingTree(
join(__dirname, '../../lib/schematics/collection.json')
);
const options = {
isDefaultTester: true,
exportConfig: false,
testingFramework: 'jasmine',
testRunner: 'jasmine',
...userOptions,
};
let workingTree: UnitTestTree;
// Build workspace
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'workspace',
WORKSPACE_OPTIONS
);
// Build dummy application
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'application',
APPLICATION_OPTIONS,
workingTree
);
if (type === 'single') {
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'ng-new',
SINGLE_APPLICATION_OPTIONS
);
} else {
// Build workspace
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'workspace',
WORKSPACE_OPTIONS
);
// Build dummy application
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'application',
MULTI_APPLICATION_OPTIONS,
workingTree
);
// Build dummy library
workingTree = await runner.runExternalSchematic(
'@schematics/angular',
'library',
MULTI_LIBRARY_OPTIONS,
workingTree
);
}
if (command !== 'ng-add') {
// We want to create update the proper files with `ng-add`
@ -104,3 +132,15 @@ export async function buildTestingTree(
return await runner.runSchematic(command, options, workingTree);
}
export async function runSchematic(
tree: UnitTestTree,
command: 'ng-add' | 'test',
options?: Record<string, any>
): Promise<UnitTestTree> {
const runner = new SchematicTestRunner(
'schematics',
join(__dirname, '../../lib/schematics/collection.json')
);
return await runner.runSchematic(command, options, tree);
}

Просмотреть файл

@ -14,17 +14,36 @@
* limitations under the License.
*/
const {spawn} = require('child_process');
const {readFile, writeFile} = require('fs/promises');
const {join} = require('path');
const {cwd} = require('process');
import {spawn} from 'child_process';
import {readFile, writeFile} from 'fs/promises';
import {join} from 'path';
import {cwd} from 'process';
const isInit = process.argv.indexOf('--init') !== -1;
const isMulti = process.argv.indexOf('--multi') !== -1;
const isBuild = process.argv.indexOf('--build') !== -1;
const isTest = process.argv.indexOf('--test') !== -1;
const isE2E = process.argv.indexOf('--e2e') !== -1;
const isConfig = process.argv.indexOf('--config') !== -1;
const commands = {
build: ['npm run build'],
createSandbox: ['npx ng new sandbox --defaults'],
createMultiWorkspace: [
'ng new sandbox --create-application=false --directory=multi',
],
createMultiProjects: [
{
command: 'ng generate application core --style=css --routing=true',
options: {
cwd: join(cwd(), '/multi/'),
},
},
{
command: 'ng generate application admin --style=css --routing=false',
options: {
cwd: join(cwd(), '/multi/'),
},
},
],
runSchematics: [
{
command: 'npm run schematics',
@ -33,9 +52,17 @@ const commands = {
},
},
],
runSchematicsTest: [
runSchematicsE2E: [
{
command: 'npm run schematics:test',
command: 'npm run schematics:e2e',
options: {
cwd: join(cwd(), '/sandbox/'),
},
},
],
runSchematicsConfig: [
{
command: 'npm run schematics:config',
options: {
cwd: join(cwd(), '/sandbox/'),
},
@ -51,8 +78,10 @@ const scripts = {
// Runs the Puppeteer Ng-Schematics against the sandbox
schematics:
'npm run delete:file && npm run build:schematics && schematics ../:ng-add --dry-run=false',
'schematics:spec':
'npm run build:schematics && schematics ../:test --dry-run=false',
'schematics:e2e':
'npm run build:schematics && schematics ../:e2e --dry-run=false',
'schematics:config':
'npm run build:schematics && schematics ../:config --dry-run=false',
};
/**
*
@ -79,7 +108,7 @@ async function executeCommand(commands) {
});
createProcess.on('error', message => {
console.error(message);
console.error(`Running ${toExecute} exited with error:`, message);
reject(message);
});
@ -96,9 +125,16 @@ async function executeCommand(commands) {
async function main() {
if (isInit) {
await executeCommand(commands.createSandbox);
if (isMulti) {
await executeCommand(commands.createMultiWorkspace);
await executeCommand(commands.createMultiProjects);
} else {
await executeCommand(commands.createSandbox);
}
const packageJsonFile = join(cwd(), '/sandbox/package.json');
const directory = isMulti ? 'multi' : 'sandbox';
const packageJsonFile = join(cwd(), `/${directory}/package.json`);
const packageJson = JSON.parse(await readFile(packageJsonFile));
packageJson['scripts'] = {
...packageJson['scripts'],
@ -109,9 +145,13 @@ async function main() {
if (isBuild) {
await executeCommand(commands.build);
}
await executeCommand(
isTest ? commands.runSchematicsTest : commands.runSchematics
);
if (isE2E) {
await executeCommand(commands.runSchematicsE2E);
} else if (isConfig) {
await executeCommand(commands.runSchematicsConfig);
} else {
await executeCommand(commands.runSchematics);
}
}
}

Просмотреть файл

@ -2,7 +2,8 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": "tsconfig",
"module": "CommonJS",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"noEmitOnError": true,
"rootDir": "src/",
"outDir": "lib/",

Просмотреть файл

@ -14,6 +14,115 @@ All notable changes to this project will be documented in this file. See [standa
* dependencies
* @puppeteer/browsers bumped from 1.4.4 to 1.4.5
### Dependencies
* The following workspace dependencies were updated
* dependencies
* @puppeteer/browsers bumped from 1.5.1 to 1.6.0
## [21.2.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.1.1...puppeteer-core-v21.2.0) (2023-09-12)
### Features
* expose DevTools as a target ([#10812](https://github.com/puppeteer/puppeteer/issues/10812)) ([a540085](https://github.com/puppeteer/puppeteer/commit/a540085176d92bd160a12ebc54606dbacd064979))
### Bug Fixes
* add --disable-search-engine-choice-screen to default arguments ([#10880](https://github.com/puppeteer/puppeteer/issues/10880)) ([d08ad5f](https://github.com/puppeteer/puppeteer/commit/d08ad5fbbe3be4349dd6132c209895f8436ae9e6))
* apply viewport emulation to prerender targets ([#10804](https://github.com/puppeteer/puppeteer/issues/10804)) ([14f0ab7](https://github.com/puppeteer/puppeteer/commit/14f0ab7397053db5591823c716e142c684f25b44))
* implement `throwIfDetached` ([#10826](https://github.com/puppeteer/puppeteer/issues/10826)) ([538bb73](https://github.com/puppeteer/puppeteer/commit/538bb73ea7e280cacf15fc1d2100251d8e17f906))
* LifecycleWatcher sub frames handling ([#10841](https://github.com/puppeteer/puppeteer/issues/10841)) ([06c1588](https://github.com/puppeteer/puppeteer/commit/06c1588016e1ebef5ed8f079dc34507f6d781e07))
* make network manager multi session ([#10793](https://github.com/puppeteer/puppeteer/issues/10793)) ([085936b](https://github.com/puppeteer/puppeteer/commit/085936bd7e17ed5a8085311f5b212c7b9ca96a0d))
* make page.goBack work with bfcache in tab mode ([#10818](https://github.com/puppeteer/puppeteer/issues/10818)) ([22daf18](https://github.com/puppeteer/puppeteer/commit/22daf1861fc358acf4d84c360049736c22249f92))
* only a single disable features flag is allowed ([#10887](https://github.com/puppeteer/puppeteer/issues/10887)) ([4852e22](https://github.com/puppeteer/puppeteer/commit/4852e222b771ed9b95596657f70e45c1d5b9790d))
* trimCache should remove Firefox too ([#10872](https://github.com/puppeteer/puppeteer/issues/10872)) ([acdd7d3](https://github.com/puppeteer/puppeteer/commit/acdd7d3cd5529bc934edbb8479bdb950cc7d8a6a))
## [21.1.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.1.0...puppeteer-core-v21.1.1) (2023-08-28)
### Bug Fixes
* **locators:** do not retry via catchError ([#10762](https://github.com/puppeteer/puppeteer/issues/10762)) ([8f9388f](https://github.com/puppeteer/puppeteer/commit/8f9388f2ce5220ad9b3c05fb3f3d9a86fac894dc))
## [21.1.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.0.3...puppeteer-core-v21.1.0) (2023-08-18)
### Features
* roll to Chrome 116.0.5845.96 (r1160321) ([#10735](https://github.com/puppeteer/puppeteer/issues/10735)) ([e12b558](https://github.com/puppeteer/puppeteer/commit/e12b558f505aab13f38030a7b748261bdeadc48b))
### Bug Fixes
* locator.fill should work for textareas ([#10737](https://github.com/puppeteer/puppeteer/issues/10737)) ([fc08a7d](https://github.com/puppeteer/puppeteer/commit/fc08a7dd54226878300f3a4b52fb16aeb5cc93e8))
* relative ordering of events and command responses should be ensured ([#10725](https://github.com/puppeteer/puppeteer/issues/10725)) ([81ecb60](https://github.com/puppeteer/puppeteer/commit/81ecb60190f89389abb6d8834158f38ff7317ec8))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* @puppeteer/browsers bumped from 1.6.0 to 1.7.0
## [21.0.2](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.0.1...puppeteer-core-v21.0.2) (2023-08-08)
### Bug Fixes
* destroy puppeteer utility on context destruction ([#10672](https://github.com/puppeteer/puppeteer/issues/10672)) ([8b8770c](https://github.com/puppeteer/puppeteer/commit/8b8770c004ba842496e0ca4845642fe82a211051))
* roll to Chrome 115.0.5790.170 (r1148114) ([#10677](https://github.com/puppeteer/puppeteer/issues/10677)) ([e5af57e](https://github.com/puppeteer/puppeteer/commit/e5af57ebd0187c296bc44426c1b931f57442732e))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* @puppeteer/browsers bumped from 1.5.0 to 1.5.1
## [21.0.1](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v21.0.0...puppeteer-core-v21.0.1) (2023-08-03)
### Bug Fixes
* use handle frame instead of page ([#10676](https://github.com/puppeteer/puppeteer/issues/10676)) ([1b44b91](https://github.com/puppeteer/puppeteer/commit/1b44b911d3633df89bd6106aaf7accb49230934d))
## [21.0.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.9.0...puppeteer-core-v21.0.0) (2023-08-02)
### ⚠ BREAKING CHANGES
* use Target for filters ([#10601](https://github.com/puppeteer/puppeteer/issues/10601))
### Features
* add page.createCDPSession method ([#10515](https://github.com/puppeteer/puppeteer/issues/10515)) ([d0c5b8e](https://github.com/puppeteer/puppeteer/commit/d0c5b8e08905f3802705a1a90d7cc8fa04bc82db))
* implement `Locator.prototype.filter` ([#10631](https://github.com/puppeteer/puppeteer/issues/10631)) ([e73d35d](https://github.com/puppeteer/puppeteer/commit/e73d35def0718468fe854ac2ef5f4a8beafb2fb3))
* implement `Locator.prototype.map` ([#10630](https://github.com/puppeteer/puppeteer/issues/10630)) ([47eecf5](https://github.com/puppeteer/puppeteer/commit/47eecf5bb11daba0114ad04282beb01c85eb9405))
* implement `Locator.prototype.wait` ([#10629](https://github.com/puppeteer/puppeteer/issues/10629)) ([5d34d42](https://github.com/puppeteer/puppeteer/commit/5d34d42d1536cbe7cf2ba1aa8670d909c4e6a6fc))
* implement `Locator.prototype.waitHandle` ([#10650](https://github.com/puppeteer/puppeteer/issues/10650)) ([fdada74](https://github.com/puppeteer/puppeteer/commit/fdada74ba7265b3571ebdf60ae301b64d13a8226))
* implement function locators ([#10632](https://github.com/puppeteer/puppeteer/issues/10632)) ([6ad92f7](https://github.com/puppeteer/puppeteer/commit/6ad92f7f84f477b22674f52f0a145a500c3aa152))
* implement immutable locator operations ([#10638](https://github.com/puppeteer/puppeteer/issues/10638)) ([34be28d](https://github.com/puppeteer/puppeteer/commit/34be28db5d9971cf16d9741b0141357df3cbf74c))
### Bug Fixes
* remove typescript from peer dependencies ([#10593](https://github.com/puppeteer/puppeteer/issues/10593)) ([c60572a](https://github.com/puppeteer/puppeteer/commit/c60572a1ca36ea5946d287bd629ac31798d84cb0))
* roll to Chrome 115.0.5790.102 (r1148114) ([#10608](https://github.com/puppeteer/puppeteer/issues/10608)) ([8649c53](https://github.com/puppeteer/puppeteer/commit/8649c53a706e5a09ae5e16849eb29a793cec5bec))
### Code Refactoring
* use Target for filters ([#10601](https://github.com/puppeteer/puppeteer/issues/10601)) ([44712d1](https://github.com/puppeteer/puppeteer/commit/44712d1e6efcb3fa49c27b1195d17c0c1c92a0ca))
### Dependencies
* The following workspace dependencies were updated
* dependencies
* @puppeteer/browsers bumped from 1.4.6 to 1.5.0
## [20.9.0](https://github.com/puppeteer/puppeteer/compare/puppeteer-core-v20.8.3...puppeteer-core-v20.9.0) (2023-07-20)

Просмотреть файл

@ -1,6 +1,6 @@
{
"name": "puppeteer-core",
"version": "20.9.0",
"version": "21.2.0",
"description": "A high-level API to control headless Chrome over the DevTools Protocol",
"keywords": [
"puppeteer",
@ -35,13 +35,9 @@
},
"scripts": {
"build:docs": "wireit",
"build:tsc": "wireit",
"build:types": "wireit",
"build": "wireit",
"check": "tsx tools/ensure-correct-devtools-protocol-package",
"clean": "tsc -b --clean && rm -rf lib src/generated",
"generate:package-json": "wireit",
"generate:sources": "wireit",
"clean": "git clean -Xdf -e '!node_modules' .",
"prepack": "wireit",
"unit": "wireit"
},
@ -144,23 +140,17 @@
"author": "The Chromium Authors",
"license": "Apache-2.0",
"dependencies": {
"chromium-bidi": "0.4.16",
"@puppeteer/browsers": "1.7.0",
"chromium-bidi": "0.4.26",
"cross-fetch": "4.0.0",
"debug": "4.3.4",
"devtools-protocol": "0.0.1147663",
"ws": "8.13.0",
"@puppeteer/browsers": "1.4.6"
},
"peerDependencies": {
"typescript": ">= 4.7.4"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
"devtools-protocol": "0.0.1159816",
"ws": "8.14.0"
},
"devDependencies": {
"mitt": "3.0.0",
"parsel-js": "1.1.0"
"disposablestack": "1.1.1",
"mitt": "3.0.1",
"parsel-js": "1.1.2",
"rxjs": "7.8.1"
}
}

Просмотреть файл

@ -15,21 +15,38 @@
*/
import commonjs from '@rollup/plugin-commonjs';
import {nodeResolve} from '@rollup/plugin-node-resolve';
import terser from '@rollup/plugin-terser';
import {globSync} from 'glob';
import nodePolyfills from 'rollup-plugin-polyfill-node';
export default ['cjs', 'esm'].flatMap(outputType => {
const configs = [];
// Note we don't use path.join here. We cannot since `glob` does not support
// the backslash path separator.
for (const file of globSync(`lib/${outputType}/third_party/**/*.js`)) {
configs.push({
input: file,
output: {
const configs = [];
// Note we don't use path.join here. We cannot since `glob` does not support
// the backslash path separator.
for (const file of globSync(`lib/esm/third_party/**/*.js`)) {
configs.push({
input: file,
output: [
{
file,
format: outputType,
format: 'esm',
},
plugins: [commonjs(), nodeResolve()],
});
}
return configs;
});
{
file: file.replace('/esm/', '/cjs/'),
format: 'cjs',
},
],
plugins: [
terser(),
nodeResolve(),
// This is used internally within the polyfill. It gets ignored for the
// most part via this plugin.
nodePolyfills({include: ['util']}),
commonjs({
transformMixedEsModules: true,
}),
],
});
}
export default configs;

Просмотреть файл

@ -20,11 +20,14 @@ import {ChildProcess} from 'child_process';
import {Protocol} from 'devtools-protocol';
import {Symbol} from '../../third_party/disposablestack/disposablestack.js';
import {EventEmitter} from '../common/EventEmitter.js';
import type {Target} from '../common/Target.js'; // TODO: move to ./api
import {debugError, waitWithTimeout} from '../common/util.js';
import {Deferred} from '../util/Deferred.js';
import type {BrowserContext} from './BrowserContext.js';
import type {Page} from './Page.js';
import type {Target} from './Target.js';
/**
* BrowserContext options.
@ -51,16 +54,12 @@ export type BrowserCloseCallback = () => Promise<void> | void;
/**
* @public
*/
export type TargetFilterCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
export type TargetFilterCallback = (target: Target) => boolean;
/**
* @internal
*/
export type IsPageTargetCallback = (
target: Protocol.Target.TargetInfo
) => boolean;
export type IsPageTargetCallback = (target: Target) => boolean;
/**
* @internal
@ -219,7 +218,10 @@ export const enum BrowserEmittedEvents {
*
* @public
*/
export class Browser extends EventEmitter {
export class Browser
extends EventEmitter
implements AsyncDisposable, Disposable
{
/**
* @internal
*/
@ -380,12 +382,35 @@ export class Browser extends EventEmitter {
* );
* ```
*/
waitForTarget(
async waitForTarget(
predicate: (x: Target) => boolean | Promise<boolean>,
options?: WaitForTargetOptions
): Promise<Target>;
waitForTarget(): Promise<Target> {
throw new Error('Not implemented');
options: WaitForTargetOptions = {}
): Promise<Target> {
const {timeout = 30000} = options;
const targetDeferred = Deferred.create<Target | PromiseLike<Target>>();
this.on(BrowserEmittedEvents.TargetCreated, check);
this.on(BrowserEmittedEvents.TargetChanged, check);
try {
this.targets().forEach(check);
if (!timeout) {
return await targetDeferred.valueOrThrow();
}
return await waitWithTimeout(
targetDeferred.valueOrThrow(),
'target',
timeout
);
} finally {
this.off(BrowserEmittedEvents.TargetCreated, check);
this.off(BrowserEmittedEvents.TargetChanged, check);
}
async function check(target: Target): Promise<void> {
if ((await predicate(target)) && !targetDeferred.resolved()) {
targetDeferred.resolve(target);
}
}
}
/**
@ -457,6 +482,14 @@ export class Browser extends EventEmitter {
isConnected(): boolean {
throw new Error('Not implemented');
}
[Symbol.dispose](): void {
return void this.close().catch(debugError);
}
[Symbol.asyncDispose](): Promise<void> {
return this.close();
}
}
/**
* @public

Просмотреть файл

@ -15,10 +15,10 @@
*/
import {EventEmitter} from '../common/EventEmitter.js';
import {Target} from '../common/Target.js';
import type {Permission, Browser} from './Browser.js';
import {Page} from './Page.js';
import type {Target} from './Target.js';
/**
* BrowserContexts provide a way to operate multiple independent browser

Просмотреть файл

@ -0,0 +1,120 @@
/**
* Copyright 2017 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Protocol} from 'devtools-protocol';
import {assert} from '../util/assert.js';
/**
* Dialog instances are dispatched by the {@link Page} via the `dialog` event.
*
* @remarks
*
* @example
*
* ```ts
* import puppeteer from 'puppeteer';
*
* (async () => {
* const browser = await puppeteer.launch();
* const page = await browser.newPage();
* page.on('dialog', async dialog => {
* console.log(dialog.message());
* await dialog.dismiss();
* await browser.close();
* });
* page.evaluate(() => alert('1'));
* })();
* ```
*
* @public
*/
export abstract class Dialog {
#type: Protocol.Page.DialogType;
#message: string;
#defaultValue: string;
#handled = false;
/**
* @internal
*/
constructor(
type: Protocol.Page.DialogType,
message: string,
defaultValue = ''
) {
this.#type = type;
this.#message = message;
this.#defaultValue = defaultValue;
}
/**
* The type of the dialog.
*/
type(): Protocol.Page.DialogType {
return this.#type;
}
/**
* The message displayed in the dialog.
*/
message(): string {
return this.#message;
}
/**
* The default value of the prompt, or an empty string if the dialog
* is not a `prompt`.
*/
defaultValue(): string {
return this.#defaultValue;
}
/**
* @internal
*/
protected abstract sendCommand(options: {
accept: boolean;
text?: string;
}): Promise<void>;
/**
* A promise that resolves when the dialog has been accepted.
*
* @param promptText - optional text that will be entered in the dialog
* prompt. Has no effect if the dialog's type is not `prompt`.
*
*/
async accept(promptText?: string): Promise<void> {
assert(!this.#handled, 'Cannot accept dialog which is already handled!');
this.#handled = true;
await this.sendCommand({
accept: true,
text: promptText,
});
}
/**
* A promise which will resolve once the dialog has been dismissed
*/
async dismiss(): Promise<void> {
assert(!this.#handled, 'Cannot dismiss dialog which is already handled!');
this.#handled = true;
await this.sendCommand({
accept: false,
});
}
}

Просмотреть файл

@ -17,8 +17,6 @@
import {Protocol} from 'devtools-protocol';
import {Frame} from '../api/Frame.js';
import {CDPSession} from '../common/Connection.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {WaitForSelectorOptions} from '../common/IsolatedWorld.js';
import {LazyArg} from '../common/LazyArg.js';
@ -35,21 +33,26 @@ import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {
KeyboardTypeOptions,
KeyPressOptions,
MouseClickOptions,
KeyboardTypeOptions,
} from './Input.js';
import {JSHandle} from './JSHandle.js';
import {ScreenshotOptions} from './Page.js';
/**
* @public
*/
export type Quad = [Point, Point, Point, Point];
/**
* @public
*/
export interface BoxModel {
content: Point[];
padding: Point[];
border: Point[];
margin: Point[];
content: Quad;
padding: Quad;
border: Quad;
margin: Quad;
width: number;
height: number;
}
@ -133,14 +136,65 @@ export interface Point {
*
* @public
*/
export class ElementHandle<
export abstract class ElementHandle<
ElementType extends Node = Element,
> extends JSHandle<ElementType> {
/**
* A given method will have it's `this` replaced with an isolated version of
* `this` when decorated with this decorator.
*
* All changes of isolated `this` are reflected on the actual `this`.
*
* @internal
*/
protected handle;
static bindIsolatedHandle<This extends ElementHandle<Node>>(
target: (this: This, ...args: any[]) => Promise<any>,
_: unknown
): typeof target {
return async function (...args) {
// If the handle is already isolated, then we don't need to adopt it
// again.
if (this.realm === this.frame.isolatedRealm()) {
return await target.call(this, ...args);
}
using adoptedThis = await this.frame.isolatedRealm().adoptHandle(this);
const result = await target.call(adoptedThis, ...args);
// If the function returns `adoptedThis`, then we return `this`.
if (result === adoptedThis) {
return this;
}
// If the function returns a handle, transfer it into the current realm.
if (result instanceof JSHandle) {
return await this.realm.transferHandle(result);
}
// If the function returns an array of handlers, transfer them into the
// current realm.
if (Array.isArray(result)) {
await Promise.all(
result.map(async (item, index, result) => {
if (item instanceof JSHandle) {
result[index] = await this.realm.transferHandle(item);
}
})
);
}
if (result instanceof Map) {
await Promise.all(
[...result.entries()].map(async ([key, value]) => {
if (value instanceof JSHandle) {
result.set(key, await this.realm.transferHandle(value));
}
})
);
}
return result;
};
}
/**
* @internal
*/
protected readonly handle;
/**
* @internal
@ -174,17 +228,22 @@ export class ElementHandle<
* @internal
*/
override async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
/**
* @internal
*/
@ElementHandle.bindIsolatedHandle
override async getProperty<K extends keyof ElementType>(
propertyName: HandleOr<K>
): Promise<HandleFor<ElementType[K]>> {
return this.handle.getProperty(propertyName);
return await this.handle.getProperty(propertyName);
}
/**
* @internal
*/
@ElementHandle.bindIsolatedHandle
override async getProperties(): Promise<Map<string, JSHandle>> {
return this.handle.getProperties();
return await this.handle.getProperties();
}
/**
@ -200,13 +259,13 @@ export class ElementHandle<
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
return this.handle.evaluate(pageFunction, ...args);
return await this.handle.evaluate(pageFunction, ...args);
}
/**
* @internal
*/
override evaluateHandle<
override async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFuncWith<ElementType, Params> = EvaluateFuncWith<
ElementType,
@ -216,14 +275,15 @@ export class ElementHandle<
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.handle.evaluateHandle(pageFunction, ...args);
return await this.handle.evaluateHandle(pageFunction, ...args);
}
/**
* @internal
*/
@ElementHandle.bindIsolatedHandle
override async jsonValue(): Promise<ElementType> {
return this.handle.jsonValue();
return await this.handle.jsonValue();
}
/**
@ -236,31 +296,28 @@ export class ElementHandle<
/**
* @internal
*/
override async dispose(): Promise<void> {
return await this.handle.dispose();
override remoteObject(): Protocol.Runtime.RemoteObject {
return this.handle.remoteObject();
}
/**
* @internal
*/
override dispose(): Promise<void> {
return this.handle.dispose();
}
/**
* @internal
*/
override asElement(): ElementHandle<ElementType> {
return this;
}
/**
* @internal
* Frame corresponding to the current handle.
*/
override executionContext(): ExecutionContext {
throw new Error('Not implemented');
}
/**
* @internal
*/
override get client(): CDPSession {
throw new Error('Not implemented');
}
get frame(): Frame {
throw new Error('Not implemented');
}
abstract get frame(): Frame;
/**
* Queries the current element for an element matching the given selector.
@ -269,6 +326,7 @@ export class ElementHandle<
* @returns A {@link ElementHandle | element handle} to the first element
* matching the given selector. Otherwise, `null`.
*/
@ElementHandle.bindIsolatedHandle
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> {
@ -287,14 +345,15 @@ export class ElementHandle<
* @returns An array of {@link ElementHandle | element handles} that point to
* elements matching the given selector.
*/
@ElementHandle.bindIsolatedHandle
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
return AsyncIterableUtil.collect(
return await (AsyncIterableUtil.collect(
QueryHandler.queryAll(this, updatedSelector)
) as Promise<Array<ElementHandle<NodeFor<Selector>>>>;
) as Promise<Array<ElementHandle<NodeFor<Selector>>>>);
}
/**
@ -336,15 +395,13 @@ export class ElementHandle<
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
const elementHandle = await this.$(selector);
using elementHandle = await this.$(selector);
if (!elementHandle) {
throw new Error(
`Error: failed to find element matching selector "${selector}"`
);
}
const result = await elementHandle.evaluate(pageFunction, ...args);
await elementHandle.dispose();
return result;
return await elementHandle.evaluate(pageFunction, ...args);
}
/**
@ -394,7 +451,7 @@ export class ElementHandle<
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
const results = await this.$$(selector);
const elements = await this.evaluateHandle(
using elements = await this.evaluateHandle(
(_, ...elements) => {
return elements;
},
@ -406,7 +463,6 @@ export class ElementHandle<
return results.dispose();
}),
]);
await elements.dispose();
return result;
}
@ -422,11 +478,12 @@ export class ElementHandle<
* If there are no such elements, the method will resolve to an empty array.
* @param expression - Expression to {@link https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate | evaluate}
*/
@ElementHandle.bindIsolatedHandle
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
if (expression.startsWith('//')) {
expression = `.${expression}`;
}
return this.$$(`xpath/${expression}`);
return await this.$$(`xpath/${expression}`);
}
/**
@ -466,6 +523,7 @@ export class ElementHandle<
* @returns An element matching the given selector.
* @throws Throws if an element matching the given selector doesn't appear.
*/
@ElementHandle.bindIsolatedHandle
async waitForSelector<Selector extends string>(
selector: Selector,
options: WaitForSelectorOptions = {}
@ -480,37 +538,33 @@ export class ElementHandle<
}
async #checkVisibility(visibility: boolean): Promise<boolean> {
const element = await this.frame.isolatedRealm().adoptHandle(this);
try {
return await this.frame.isolatedRealm().evaluate(
async (PuppeteerUtil, element, visibility) => {
return Boolean(PuppeteerUtil.checkVisibility(element, visibility));
},
LazyArg.create(context => {
return context.puppeteerUtil;
}),
element,
visibility
);
} finally {
await element.dispose();
}
return await this.evaluate(
async (element, PuppeteerUtil, visibility) => {
return Boolean(PuppeteerUtil.checkVisibility(element, visibility));
},
LazyArg.create(context => {
return context.puppeteerUtil;
}),
visibility
);
}
/**
* Checks if an element is visible using the same mechanism as
* {@link ElementHandle.waitForSelector}.
*/
@ElementHandle.bindIsolatedHandle
async isVisible(): Promise<boolean> {
return this.#checkVisibility(true);
return await this.#checkVisibility(true);
}
/**
* Checks if an element is hidden using the same mechanism as
* {@link ElementHandle.waitForSelector}.
*/
@ElementHandle.bindIsolatedHandle
async isHidden(): Promise<boolean> {
return this.#checkVisibility(false);
return await this.#checkVisibility(false);
}
/**
@ -575,6 +629,7 @@ export class ElementHandle<
* default value can be changed by using the {@link Page.setDefaultTimeout}
* method.
*/
@ElementHandle.bindIsolatedHandle
async waitForXPath(
xpath: string,
options: {
@ -586,7 +641,7 @@ export class ElementHandle<
if (xpath.startsWith('//')) {
xpath = `.${xpath}`;
}
return this.waitForSelector(`xpath/${xpath}`, options);
return await this.waitForSelector(`xpath/${xpath}`, options);
}
/**
@ -599,15 +654,15 @@ export class ElementHandle<
* '.class-name-of-anchor'
* );
* // DO NOT DISPOSE `element`, this will be always be the same handle.
* const anchor: ElementHandle<HTMLAnchorElement> = await element.toElement(
* 'a'
* );
* const anchor: ElementHandle<HTMLAnchorElement> =
* await element.toElement('a');
* ```
*
* @param tagName - The tag name of the desired element type.
* @throws An error if the handle does not match. **The handle will not be
* automatically disposed.**
*/
@ElementHandle.bindIsolatedHandle
async toElement<
K extends keyof HTMLElementTagNameMap | keyof SVGElementTagNameMap,
>(tagName: K): Promise<HandleFor<ElementFor<K>>> {
@ -621,19 +676,31 @@ export class ElementHandle<
}
/**
* Resolves to the content frame for element handles referencing
* iframe nodes, or null otherwise
* Resolves the frame associated with the element, if any. Always exists for
* HTMLIFrameElements.
*/
async contentFrame(): Promise<Frame | null> {
throw new Error('Not implemented');
}
abstract contentFrame(this: ElementHandle<HTMLIFrameElement>): Promise<Frame>;
abstract contentFrame(): Promise<Frame | null>;
/**
* Returns the middle point within an element unless a specific offset is provided.
*/
async clickablePoint(offset?: Offset): Promise<Point>;
async clickablePoint(): Promise<Point> {
throw new Error('Not implemented');
@ElementHandle.bindIsolatedHandle
async clickablePoint(offset?: Offset): Promise<Point> {
const box = await this.#clickableBox();
if (!box) {
throw new Error('Node is either not clickable or not an Element');
}
if (offset !== undefined) {
return {
x: box.x + offset.x,
y: box.y + offset.y,
};
}
return {
x: box.x + box.width / 2,
y: box.y + box.height / 2,
};
}
/**
@ -641,8 +708,11 @@ export class ElementHandle<
* uses {@link Page} to hover over the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
@ElementHandle.bindIsolatedHandle
async hover(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().mouse.move(x, y);
}
/**
@ -650,12 +720,14 @@ export class ElementHandle<
* uses {@link Page | Page.mouse} to click in the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
@ElementHandle.bindIsolatedHandle
async click(
this: ElementHandle<Element>,
options?: ClickOptions
): Promise<void>;
async click(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
options: Readonly<ClickOptions> = {}
): Promise<void> {
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint(options.offset);
await this.frame.page().mouse.click(x, y, options);
}
/**
@ -730,6 +802,7 @@ export class ElementHandle<
* `multiple` attribute, all values are considered, otherwise only the first
* one is taken into account.
*/
@ElementHandle.bindIsolatedHandle
async select(...values: string[]): Promise<string[]> {
for (const value of values) {
assert(
@ -742,7 +815,7 @@ export class ElementHandle<
);
}
return this.evaluate((element, vals): string[] => {
return await this.evaluate((element, vals): string[] => {
const values = new Set(vals);
if (!(element instanceof HTMLSelectElement)) {
throw new Error('Element is not a <select> element.');
@ -798,25 +871,38 @@ export class ElementHandle<
* {@link Touchscreen.tap} to tap in the center of the element.
* If the element is detached from DOM, the method throws an error.
*/
@ElementHandle.bindIsolatedHandle
async tap(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().touchscreen.touchStart(x, y);
await this.frame.page().touchscreen.touchEnd();
}
@ElementHandle.bindIsolatedHandle
async touchStart(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().touchscreen.touchStart(x, y);
}
@ElementHandle.bindIsolatedHandle
async touchMove(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
await this.scrollIntoViewIfNeeded();
const {x, y} = await this.clickablePoint();
await this.frame.page().touchscreen.touchMove(x, y);
}
@ElementHandle.bindIsolatedHandle
async touchEnd(this: ElementHandle<Element>): Promise<void> {
throw new Error('Not implemented');
await this.scrollIntoViewIfNeeded();
await this.frame.page().touchscreen.touchEnd();
}
/**
* Calls {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus | focus} on the element.
*/
@ElementHandle.bindIsolatedHandle
async focus(): Promise<void> {
await this.evaluate(element => {
if (!(element instanceof HTMLElement)) {
@ -851,12 +937,13 @@ export class ElementHandle<
*
* @param options - Delay in milliseconds. Defaults to 0.
*/
@ElementHandle.bindIsolatedHandle
async type(
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void>;
async type(): Promise<void> {
throw new Error('Not implemented');
): Promise<void> {
await this.focus();
await this.frame.page().keyboard.type(text, options);
}
/**
@ -873,20 +960,121 @@ export class ElementHandle<
* @param key - Name of key to press, such as `ArrowLeft`.
* See {@link KeyInput} for a list of all key names.
*/
@ElementHandle.bindIsolatedHandle
async press(
key: KeyInput,
options?: Readonly<KeyPressOptions>
): Promise<void>;
async press(): Promise<void> {
throw new Error('Not implemented');
): Promise<void> {
await this.focus();
await this.frame.page().keyboard.press(key, options);
}
async #clickableBox(): Promise<BoundingBox | null> {
const boxes = await this.evaluate(element => {
if (!(element instanceof Element)) {
return null;
}
return [...element.getClientRects()].map(rect => {
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
});
});
if (!boxes?.length) {
return null;
}
await this.#intersectBoundingBoxesWithFrame(boxes);
let frame = this.frame;
let parentFrame: Frame | null | undefined;
while ((parentFrame = frame?.parentFrame())) {
using handle = await frame.frameElement();
if (!handle) {
throw new Error('Unsupported frame type');
}
const parentBox = await handle.evaluate(element => {
// Element is not visible.
if (element.getClientRects().length === 0) {
return null;
}
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return {
left:
rect.left +
parseInt(style.paddingLeft, 10) +
parseInt(style.borderLeftWidth, 10),
top:
rect.top +
parseInt(style.paddingTop, 10) +
parseInt(style.borderTopWidth, 10),
};
});
if (!parentBox) {
return null;
}
for (const box of boxes) {
box.x += parentBox.left;
box.y += parentBox.top;
}
await handle.#intersectBoundingBoxesWithFrame(boxes);
frame = parentFrame;
}
const box = boxes.find(box => {
return box.width >= 1 && box.height >= 1;
});
if (!box) {
return null;
}
return {
x: box.x,
y: box.y,
height: box.height,
width: box.width,
};
}
async #intersectBoundingBoxesWithFrame(boxes: BoundingBox[]) {
const {documentWidth, documentHeight} = await this.frame
.isolatedRealm()
.evaluate(() => {
return {
documentWidth: document.documentElement.clientWidth,
documentHeight: document.documentElement.clientHeight,
};
});
for (const box of boxes) {
intersectBoundingBox(box, documentWidth, documentHeight);
}
}
/**
* This method returns the bounding box of the element (relative to the main frame),
* or `null` if the element is not visible.
*/
@ElementHandle.bindIsolatedHandle
async boundingBox(): Promise<BoundingBox | null> {
throw new Error('Not implemented');
const box = await this.evaluate(element => {
if (!(element instanceof Element)) {
return null;
}
// Element is not visible.
if (element.getClientRects().length === 0) {
return null;
}
const rect = element.getBoundingClientRect();
return {x: rect.x, y: rect.y, width: rect.width, height: rect.height};
});
if (!box) {
return null;
}
const offset = await this.#getTopLeftCornerOfFrame();
if (!offset) {
return null;
}
return {
x: box.x + offset.x,
y: box.y + offset.y,
height: box.height,
width: box.width,
};
}
/**
@ -897,8 +1085,136 @@ export class ElementHandle<
* Boxes are represented as an array of points;
* Each Point is an object `{x, y}`. Box points are sorted clock-wise.
*/
@ElementHandle.bindIsolatedHandle
async boxModel(): Promise<BoxModel | null> {
throw new Error('Not implemented');
const model = await this.evaluate(element => {
if (!(element instanceof Element)) {
return null;
}
// Element is not visible.
if (element.getClientRects().length === 0) {
return null;
}
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
const offsets = {
padding: {
left: parseInt(style.paddingLeft, 10),
top: parseInt(style.paddingTop, 10),
right: parseInt(style.paddingRight, 10),
bottom: parseInt(style.paddingBottom, 10),
},
margin: {
left: -parseInt(style.marginLeft, 10),
top: -parseInt(style.marginTop, 10),
right: -parseInt(style.marginRight, 10),
bottom: -parseInt(style.marginBottom, 10),
},
border: {
left: parseInt(style.borderLeft, 10),
top: parseInt(style.borderTop, 10),
right: parseInt(style.borderRight, 10),
bottom: parseInt(style.borderBottom, 10),
},
};
const border: Quad = [
{x: rect.left, y: rect.top},
{x: rect.left + rect.width, y: rect.top},
{x: rect.left + rect.width, y: rect.top + rect.bottom},
{x: rect.left, y: rect.top + rect.bottom},
];
const padding = transformQuadWithOffsets(border, offsets.border);
const content = transformQuadWithOffsets(padding, offsets.padding);
const margin = transformQuadWithOffsets(border, offsets.margin);
return {
content,
padding,
border,
margin,
width: rect.width,
height: rect.height,
};
function transformQuadWithOffsets(
quad: Quad,
offsets: {top: number; left: number; right: number; bottom: number}
): Quad {
return [
{
x: quad[0].x + offsets.left,
y: quad[0].y + offsets.top,
},
{
x: quad[1].x - offsets.right,
y: quad[1].y + offsets.top,
},
{
x: quad[2].x - offsets.right,
y: quad[2].y - offsets.bottom,
},
{
x: quad[3].x + offsets.left,
y: quad[3].y - offsets.bottom,
},
];
}
});
if (!model) {
return null;
}
const offset = await this.#getTopLeftCornerOfFrame();
if (!offset) {
return null;
}
for (const attribute of [
'content',
'padding',
'border',
'margin',
] as const) {
for (const point of model[attribute]) {
point.x += offset.x;
point.y += offset.y;
}
}
return model;
}
async #getTopLeftCornerOfFrame() {
const point = {x: 0, y: 0};
let frame = this.frame;
let parentFrame: Frame | null | undefined;
while ((parentFrame = frame?.parentFrame())) {
using handle = await frame.frameElement();
if (!handle) {
throw new Error('Unsupported frame type');
}
const parentBox = await handle.evaluate(element => {
// Element is not visible.
if (element.getClientRects().length === 0) {
return null;
}
const rect = element.getBoundingClientRect();
const style = window.getComputedStyle(element);
return {
left:
rect.left +
parseInt(style.paddingLeft, 10) +
parseInt(style.borderLeftWidth, 10),
top:
rect.top +
parseInt(style.paddingTop, 10) +
parseInt(style.borderTopWidth, 10),
};
});
if (!parentBox) {
return null;
}
point.x += parentBox.left;
point.y += parentBox.top;
frame = parentFrame;
}
return point;
}
/**
@ -918,17 +1234,15 @@ export class ElementHandle<
* @internal
*/
protected async assertConnectedElement(): Promise<void> {
const error = await this.evaluate(
async (element): Promise<string | undefined> => {
if (!element.isConnected) {
return 'Node is detached from document';
}
if (element.nodeType !== Node.ELEMENT_NODE) {
return 'Node is not of type HTMLElement';
}
return;
const error = await this.evaluate(async element => {
if (!element.isConnected) {
return 'Node is detached from document';
}
);
if (element.nodeType !== Node.ELEMENT_NODE) {
return 'Node is not of type HTMLElement';
}
return;
});
if (error) {
throw new Error(error);
@ -959,22 +1273,19 @@ export class ElementHandle<
* @param options - Threshold for the intersection between 0 (no intersection) and 1
* (full intersection). Defaults to 1.
*/
@ElementHandle.bindIsolatedHandle
async isIntersectingViewport(
this: ElementHandle<Element>,
options?: {
options: {
threshold?: number;
}
} = {}
): Promise<boolean> {
await this.assertConnectedElement();
const {threshold = 0} = options ?? {};
const svgHandle = await this.#asSVGElementHandle(this);
const intersectionTarget: ElementHandle<Element> = svgHandle
? await this.#getOwnerSVGElement(svgHandle)
: this;
try {
return await intersectionTarget.evaluate(async (element, threshold) => {
// eslint-disable-next-line rulesdir/use-using -- Returns `this`.
const handle = await this.#asSVGElementHandle();
using target = handle && (await handle.#getOwnerSVGElement());
return await ((target ?? this) as ElementHandle<Element>).evaluate(
async (element, threshold) => {
const visibleRatio = await new Promise<number>(resolve => {
const observer = new IntersectionObserver(entries => {
resolve(entries[0]!.intersectionRatio);
@ -983,18 +1294,16 @@ export class ElementHandle<
observer.observe(element);
});
return threshold === 1 ? visibleRatio === 1 : visibleRatio > threshold;
}, threshold);
} finally {
if (intersectionTarget !== this) {
await intersectionTarget.dispose();
}
}
},
options.threshold ?? 0
);
}
/**
* Scrolls the element into view using either the automation protocol client
* or by calling element.scrollIntoView.
*/
@ElementHandle.bindIsolatedHandle
async scrollIntoView(this: ElementHandle<Element>): Promise<void> {
await this.assertConnectedElement();
await this.evaluate(async (element): Promise<void> => {
@ -1011,24 +1320,24 @@ export class ElementHandle<
* etc.).
*/
async #asSVGElementHandle(
handle: ElementHandle<Element>
this: ElementHandle<Element>
): Promise<ElementHandle<SVGElement> | null> {
if (
await handle.evaluate(element => {
await this.evaluate(element => {
return element instanceof SVGElement;
})
) {
return handle as ElementHandle<SVGElement>;
return this as ElementHandle<SVGElement>;
} else {
return null;
}
}
async #getOwnerSVGElement(
handle: ElementHandle<SVGElement>
this: ElementHandle<SVGElement>
): Promise<ElementHandle<SVGSVGElement>> {
// SVGSVGElement.ownerSVGElement === null.
return await handle.evaluateHandle(element => {
return await this.evaluateHandle(element => {
if (element instanceof SVGSVGElement) {
return element;
}
@ -1036,13 +1345,6 @@ export class ElementHandle<
});
}
/**
* @internal
*/
assertElementHasWorld(): asserts this {
assert(this.executionContext()._world);
}
/**
* If the element is a form input, you can use {@link ElementHandle.autofill}
* to test if the form is compatible with the browser's autofill
@ -1068,12 +1370,12 @@ export class ElementHandle<
* });
* ```
*/
autofill(data: AutofillData): Promise<void>;
autofill(): Promise<void> {
throw new Error('Not implemented');
}
abstract autofill(data: AutofillData): Promise<void>;
}
/**
* @public
*/
export interface AutofillData {
creditCard: {
// See https://chromedevtools.github.io/devtools-protocol/tot/Autofill/#type-CreditCard.
@ -1084,3 +1386,22 @@ export interface AutofillData {
cvc: string;
};
}
function intersectBoundingBox(
box: BoundingBox,
width: number,
height: number
): void {
box.width = Math.max(
box.x >= 0
? Math.min(width - box.x, box.width)
: Math.min(width, box.width + box.x),
0
);
box.height = Math.max(
box.y >= 0
? Math.min(height - box.y, box.height)
: Math.min(height, box.height + box.y),
0
);
}

Просмотреть файл

@ -0,0 +1,27 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {CDPSession} from '../common/Connection.js';
import {Realm} from './Realm.js';
/**
* @internal
*/
export interface Environment {
get client(): CDPSession;
mainRealm(): Realm;
}

Просмотреть файл

@ -19,8 +19,9 @@ import {HTTPResponse} from '../api/HTTPResponse.js';
import {Page, WaitTimeoutOptions} from '../api/Page.js';
import {CDPSession} from '../common/Connection.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {getQueryHandlerAndSelector} from '../common/GetQueryHandler.js';
import {transposeIterableHandle} from '../common/HandleIterator.js';
import {
IsolatedWorldChart,
WaitForSelectorOptions,
@ -28,66 +29,23 @@ import {
import {LazyArg} from '../common/LazyArg.js';
import {PuppeteerLifeCycleEvent} from '../common/LifecycleWatcher.js';
import {
Awaitable,
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
InnerLazyParams,
NodeFor,
} from '../common/types.js';
import {importFSPromises} from '../common/util.js';
import {TaskManager} from '../common/WaitTask.js';
import {
getPageContent,
importFSPromises,
withSourcePuppeteerURLIfNone,
} from '../common/util.js';
import {assert} from '../util/assert.js';
import {throwIfDisposed} from '../util/decorators.js';
import {KeyboardTypeOptions} from './Input.js';
import {JSHandle} from './JSHandle.js';
import {Locator} from './Locator.js';
/**
* @internal
*/
export interface Realm {
taskManager: TaskManager;
waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params>
>,
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
signal?: AbortSignal;
},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
click(selector: string, options: Readonly<ClickOptions>): Promise<void>;
focus(selector: string): Promise<void>;
hover(selector: string): Promise<void>;
select(selector: string, ...values: string[]): Promise<string[]>;
tap(selector: string): Promise<void>;
type(
selector: string,
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void>;
}
import {FunctionLocator, Locator, NodeLocator} from './locators/locators.js';
import {Realm} from './Realm.js';
/**
* @public
@ -169,6 +127,13 @@ export interface FrameAddStyleTagOptions {
content?: string;
}
/**
* @internal
*/
export const throwIfDetached = throwIfDisposed<Frame>(frame => {
return `Attempted to use detached Frame '${frame._id}'.`;
});
/**
* Represents a DOM frame.
*
@ -222,7 +187,7 @@ export interface FrameAddStyleTagOptions {
*
* @public
*/
export class Frame {
export abstract class Frame extends EventEmitter {
/**
* @internal
*/
@ -250,14 +215,14 @@ export class Frame {
/**
* @internal
*/
constructor() {}
constructor() {
super();
}
/**
* The page associated with the frame.
*/
page(): Page {
throw new Error('Not implemented');
}
abstract page(): Page;
/**
* Is `true` if the frame is an out-of-process (OOP) frame. Otherwise,
@ -304,7 +269,7 @@ export class Frame {
* Server Error". The status code for such responses can be retrieved by
* calling {@link HTTPResponse.status}.
*/
async goto(
abstract goto(
url: string,
options?: {
referer?: string;
@ -313,9 +278,6 @@ export class Frame {
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<HTTPResponse | null>;
async goto(): Promise<HTTPResponse | null> {
throw new Error('Not implemented');
}
/**
* Waits for the frame to navigate. It is useful for when you run code which
@ -340,40 +302,72 @@ export class Frame {
* finished.
* @returns a promise that resolves when the frame navigates to a new URL.
*/
async waitForNavigation(options?: {
abstract waitForNavigation(options?: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}): Promise<HTTPResponse | null>;
async waitForNavigation(): Promise<HTTPResponse | null> {
throw new Error('Not implemented');
/**
* @internal
*/
abstract get client(): CDPSession;
/**
* @internal
*/
abstract mainRealm(): Realm;
/**
* @internal
*/
abstract isolatedRealm(): Realm;
#_document: Promise<ElementHandle<Document>> | undefined;
/**
* @internal
*/
#document(): Promise<ElementHandle<Document>> {
if (!this.#_document) {
this.#_document = this.isolatedRealm()
.evaluateHandle(() => {
return document;
})
.then(handle => {
return this.mainRealm().transferHandle(handle);
});
}
return this.#_document;
}
/**
* Used to clear the document handle that has been destroyed.
*
* @internal
*/
clearDocumentHandle(): void {
this.#_document = undefined;
}
/**
* @internal
*/
_client(): CDPSession {
throw new Error('Not implemented');
}
/**
* @internal
*/
executionContext(): Promise<ExecutionContext> {
throw new Error('Not implemented');
}
/**
* @internal
*/
mainRealm(): Realm {
throw new Error('Not implemented');
}
/**
* @internal
*/
isolatedRealm(): Realm {
throw new Error('Not implemented');
@throwIfDetached
async frameElement(): Promise<HandleFor<HTMLIFrameElement> | null> {
const parentFrame = this.parentFrame();
if (!parentFrame) {
return null;
}
using list = await parentFrame.isolatedRealm().evaluateHandle(() => {
return document.querySelectorAll('iframe');
});
for await (using iframe of transposeIterableHandle(list)) {
const frame = await iframe.contentFrame();
if (frame._id === this._id) {
return iframe.move();
}
}
return null;
}
/**
@ -382,18 +376,19 @@ export class Frame {
*
* @see {@link Page.evaluateHandle} for details.
*/
@throwIfDetached
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
throw new Error('Not implemented');
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.mainRealm().evaluateHandle(pageFunction, ...args);
}
/**
@ -402,22 +397,23 @@ export class Frame {
*
* @see {@link Page.evaluate} for details.
*/
@throwIfDetached
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(): Promise<Awaited<ReturnType<Func>>> {
throw new Error('Not implemented');
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.mainRealm().evaluate(pageFunction, ...args);
}
/**
* Creates a locator for the provided `selector`. See {@link Locator} for
* Creates a locator for the provided selector. See {@link Locator} for
* details and supported actions.
*
* @remarks
@ -426,10 +422,31 @@ export class Frame {
*/
locator<Selector extends string>(
selector: Selector
): Locator<NodeFor<Selector>> {
return Locator.create(this, selector);
}
): Locator<NodeFor<Selector>>;
/**
* Creates a locator for the provided function. See {@link Locator} for
* details and supported actions.
*
* @remarks
* Locators API is experimental and we will not follow semver for breaking
* change in the Locators API.
*/
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
/**
* @internal
*/
@throwIfDetached
locator<Selector extends string, Ret>(
selectorOrFunc: Selector | (() => Awaitable<Ret>)
): Locator<NodeFor<Selector>> | Locator<Ret> {
if (typeof selectorOrFunc === 'string') {
return NodeLocator.create(this, selectorOrFunc);
} else {
return FunctionLocator.create(this, selectorOrFunc);
}
}
/**
* Queries the frame for an element matching the given selector.
*
@ -437,13 +454,13 @@ export class Frame {
* @returns A {@link ElementHandle | element handle} to the first element
* matching the given selector. Otherwise, `null`.
*/
@throwIfDetached
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null>;
async $<Selector extends string>(): Promise<ElementHandle<
NodeFor<Selector>
> | null> {
throw new Error('Not implemented');
): Promise<ElementHandle<NodeFor<Selector>> | null> {
// eslint-disable-next-line rulesdir/use-using -- This is cached.
const document = await this.#document();
return await document.$(selector);
}
/**
@ -453,13 +470,13 @@ export class Frame {
* @returns An array of {@link ElementHandle | element handles} that point to
* elements matching the given selector.
*/
@throwIfDetached
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>>;
async $$<Selector extends string>(): Promise<
Array<ElementHandle<NodeFor<Selector>>>
> {
throw new Error('Not implemented');
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
// eslint-disable-next-line rulesdir/use-using -- This is cached.
const document = await this.#document();
return await document.$$(selector);
}
/**
@ -482,6 +499,7 @@ export class Frame {
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
@throwIfDetached
async $eval<
Selector extends string,
Params extends unknown[],
@ -491,18 +509,13 @@ export class Frame {
>,
>(
selector: Selector,
pageFunction: Func | string,
pageFunction: string | Func,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async $eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<NodeFor<Selector>, Params> = EvaluateFuncWith<
NodeFor<Selector>,
Params
>,
>(): Promise<Awaited<ReturnType<Func>>> {
throw new Error('Not implemented');
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
// eslint-disable-next-line rulesdir/use-using -- This is cached.
const document = await this.#document();
return await document.$eval(selector, pageFunction, ...args);
}
/**
@ -525,6 +538,7 @@ export class Frame {
* @param args - Additional arguments to pass to `pageFunction`.
* @returns A promise to the result of the function.
*/
@throwIfDetached
async $$eval<
Selector extends string,
Params extends unknown[],
@ -534,18 +548,13 @@ export class Frame {
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
>(
selector: Selector,
pageFunction: Func | string,
pageFunction: string | Func,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async $$eval<
Selector extends string,
Params extends unknown[],
Func extends EvaluateFuncWith<
Array<NodeFor<Selector>>,
Params
> = EvaluateFuncWith<Array<NodeFor<Selector>>, Params>,
>(): Promise<Awaited<ReturnType<Func>>> {
throw new Error('Not implemented');
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
// eslint-disable-next-line rulesdir/use-using -- This is cached.
const document = await this.#document();
return await document.$$eval(selector, pageFunction, ...args);
}
/**
@ -558,9 +567,11 @@ export class Frame {
* automatically.
* @param expression - the XPath expression to evaluate.
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>>;
async $x(): Promise<Array<ElementHandle<Node>>> {
throw new Error('Not implemented');
@throwIfDetached
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
// eslint-disable-next-line rulesdir/use-using -- This is cached.
const document = await this.#document();
return await document.$x(expression);
}
/**
@ -598,6 +609,7 @@ export class Frame {
* @returns An element matching the given selector.
* @throws Throws if an element matching the given selector doesn't appear.
*/
@throwIfDetached
async waitForSelector<Selector extends string>(
selector: Selector,
options: WaitForSelectorOptions = {}
@ -633,6 +645,7 @@ export class Frame {
* @param options - options to configure the visibility of the element and how
* long to wait before timing out.
*/
@throwIfDetached
async waitForXPath(
xpath: string,
options: WaitForSelectorOptions = {}
@ -640,7 +653,7 @@ export class Frame {
if (xpath.startsWith('//')) {
xpath = `.${xpath}`;
}
return this.waitForSelector(`xpath/${xpath}`, options);
return await this.waitForSelector(`xpath/${xpath}`, options);
}
/**
@ -676,7 +689,8 @@ export class Frame {
* @param args - arguments to pass to the `pageFunction`.
* @returns the promise which resolve when the `pageFunction` returns a truthy value.
*/
waitForFunction<
@throwIfDetached
async waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
@ -684,17 +698,18 @@ export class Frame {
options: FrameWaitForFunctionOptions = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
return this.mainRealm().waitForFunction(
return await (this.mainRealm().waitForFunction(
pageFunction,
options,
...args
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>;
) as Promise<HandleFor<Awaited<ReturnType<Func>>>>);
}
/**
* The full HTML contents of the frame, including the DOCTYPE.
*/
@throwIfDetached
async content(): Promise<string> {
throw new Error('Not implemented');
return await this.evaluate(getPageContent);
}
/**
@ -704,16 +719,13 @@ export class Frame {
* @param options - Options to configure how long before timing out and at
* what point to consider the content setting successful.
*/
async setContent(
abstract setContent(
html: string,
options?: {
timeout?: number;
waitUntil?: PuppeteerLifeCycleEvent | PuppeteerLifeCycleEvent[];
}
): Promise<void>;
async setContent(): Promise<void> {
throw new Error('Not implemented');
}
/**
* The frame's `name` attribute as specified in the tag.
@ -732,29 +744,37 @@ export class Frame {
/**
* The frame's URL.
*/
url(): string {
throw new Error('Not implemented');
}
abstract url(): string;
/**
* The parent frame, if any. Detached and main frames return `null`.
*/
parentFrame(): Frame | null {
throw new Error('Not implemented');
}
abstract parentFrame(): Frame | null;
/**
* An array of child frames.
*/
childFrames(): Frame[] {
throw new Error('Not implemented');
}
abstract childFrames(): Frame[];
/**
* @returns `true` if the frame has detached. `false` otherwise.
*/
abstract get detached(): boolean;
/**
* Is`true` if the frame has been detached. Otherwise, `false`.
*
* @deprecated Use the `detached` getter.
*/
isDetached(): boolean {
throw new Error('Not implemented');
return this.detached;
}
/**
* @internal
*/
get disposed(): boolean {
return this.detached;
}
/**
@ -764,6 +784,7 @@ export class Frame {
* @returns An {@link ElementHandle | element handle} to the injected
* `<script>` element.
*/
@throwIfDetached
async addScriptTag(
options: FrameAddScriptTagOptions
): Promise<ElementHandle<HTMLScriptElement>> {
@ -783,7 +804,7 @@ export class Frame {
type = type ?? 'text/javascript';
return this.mainRealm().transferHandle(
return await this.mainRealm().transferHandle(
await this.isolatedRealm().evaluateHandle(
async ({Deferred}, {url, id, type, content}) => {
const deferred = Deferred.create<void>();
@ -827,18 +848,29 @@ export class Frame {
}
/**
* Adds a `<link rel="stylesheet">` tag into the page with the desired URL or
* a `<style type="text/css">` tag with the content.
* Adds a `HTMLStyleElement` into the frame with the desired URL
*
* @returns An {@link ElementHandle | element handle} to the loaded `<link>`
* or `<style>` element.
* @returns An {@link ElementHandle | element handle} to the loaded `<style>`
* element.
*/
async addStyleTag(
options: Omit<FrameAddStyleTagOptions, 'url'>
): Promise<ElementHandle<HTMLStyleElement>>;
/**
* Adds a `HTMLLinkElement` into the frame with the desired URL
*
* @returns An {@link ElementHandle | element handle} to the loaded `<link>`
* element.
*/
async addStyleTag(
options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLLinkElement>>;
/**
* @internal
*/
@throwIfDetached
async addStyleTag(
options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
@ -858,7 +890,7 @@ export class Frame {
options.content = content;
}
return this.mainRealm().transferHandle(
return await this.mainRealm().transferHandle(
await this.isolatedRealm().evaluateHandle(
async ({Deferred}, {url, content}) => {
const deferred = Deferred.create<void>();
@ -920,8 +952,15 @@ export class Frame {
*
* @param selector - The selector to query for.
*/
click(selector: string, options: Readonly<ClickOptions> = {}): Promise<void> {
return this.isolatedRealm().click(selector, options);
@throwIfDetached
async click(
selector: string,
options: Readonly<ClickOptions> = {}
): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.click(options);
await handle.dispose();
}
/**
@ -930,8 +969,11 @@ export class Frame {
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
@throwIfDetached
async focus(selector: string): Promise<void> {
return this.isolatedRealm().focus(selector);
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.focus();
}
/**
@ -941,8 +983,11 @@ export class Frame {
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
hover(selector: string): Promise<void> {
return this.isolatedRealm().hover(selector);
@throwIfDetached
async hover(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.hover();
}
/**
@ -963,8 +1008,11 @@ export class Frame {
* @returns the list of values that were successfully selected.
* @throws Throws if there's no `<select>` matching `selector`.
*/
select(selector: string, ...values: string[]): Promise<string[]> {
return this.isolatedRealm().select(selector, ...values);
@throwIfDetached
async select(selector: string, ...values: string[]): Promise<string[]> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
return await handle.select(...values);
}
/**
@ -973,8 +1021,11 @@ export class Frame {
* @param selector - The selector to query for.
* @throws Throws if there's no element matching `selector`.
*/
tap(selector: string): Promise<void> {
return this.isolatedRealm().tap(selector);
@throwIfDetached
async tap(selector: string): Promise<void> {
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.tap();
}
/**
@ -998,12 +1049,15 @@ export class Frame {
* @param options - takes one option, `delay`, which sets the time to wait
* between key presses in milliseconds. Defaults to `0`.
*/
type(
@throwIfDetached
async type(
selector: string,
text: string,
options?: Readonly<KeyboardTypeOptions>
): Promise<void> {
return this.isolatedRealm().type(selector, text, options);
using handle = await this.$(selector);
assert(handle, `No element found for selector: ${selector}`);
await handle.type(text, options);
}
/**
@ -1026,8 +1080,8 @@ export class Frame {
*
* @param milliseconds - the number of milliseconds to wait.
*/
waitForTimeout(milliseconds: number): Promise<void> {
return new Promise(resolve => {
async waitForTimeout(milliseconds: number): Promise<void> {
return await new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
@ -1035,8 +1089,11 @@ export class Frame {
/**
* The frame's title.
*/
@throwIfDetached
async title(): Promise<string> {
throw new Error('Not implemented');
return await this.isolatedRealm().evaluate(() => {
return document.title;
});
}
/**
@ -1065,7 +1122,22 @@ export class Frame {
waitForDevicePrompt(
options?: WaitTimeoutOptions
): Promise<DeviceRequestPrompt>;
/**
* @internal
*/
waitForDevicePrompt(): Promise<DeviceRequestPrompt> {
throw new Error('Not implemented');
}
/**
* @internal
*/
exposeFunction<Args extends unknown[], Ret>(
name: string,
fn: (...args: Args) => Awaitable<Ret>
): Promise<void>;
exposeFunction(): Promise<void> {
throw new Error('Not implemented');
}
}

Просмотреть файл

@ -552,6 +552,13 @@ export class Touchscreen {
* Dispatches a `touchMove` event.
* @param x - Horizontal position of the move.
* @param y - Vertical position of the move.
*
* @remarks
*
* Not every `touchMove` call results in a `touchmove` event being emitted,
* depending on the browser's optimizations. For example, Chrome
* {@link https://developer.chrome.com/blog/a-more-compatible-smoother-touch/#chromes-new-model-the-throttled-async-touchmove-model | throttles}
* touch move events.
*/
async touchMove(x: number, y: number): Promise<void>;
async touchMove(): Promise<void> {

Просмотреть файл

@ -16,11 +16,18 @@
import Protocol from 'devtools-protocol';
import {CDPSession} from '../common/Connection.js';
import {ExecutionContext} from '../common/ExecutionContext.js';
import {EvaluateFuncWith, HandleFor, HandleOr} from '../common/types.js';
import {Symbol} from '../../third_party/disposablestack/disposablestack.js';
import {
EvaluateFuncWith,
HandleFor,
HandleOr,
Moveable,
} from '../common/types.js';
import {debugError, withSourcePuppeteerURLIfNone} from '../common/util.js';
import {moveable} from '../util/decorators.js';
import {ElementHandle} from './ElementHandle.js';
import {Realm} from './Realm.js';
/**
* Represents a reference to a JavaScript object. Instances can be created using
@ -43,7 +50,12 @@ import {ElementHandle} from './ElementHandle.js';
*
* @public
*/
export class JSHandle<T = unknown> {
@moveable
export abstract class JSHandle<T = unknown>
implements Disposable, AsyncDisposable, Moveable
{
declare move: () => this;
/**
* Used for nominally typing {@link JSHandle}.
*/
@ -54,6 +66,11 @@ export class JSHandle<T = unknown> {
*/
constructor() {}
/**
* @internal
*/
abstract get realm(): Realm;
/**
* @internal
*/
@ -61,20 +78,6 @@ export class JSHandle<T = unknown> {
throw new Error('Not implemented');
}
/**
* @internal
*/
executionContext(): ExecutionContext {
throw new Error('Not implemented');
}
/**
* @internal
*/
get client(): CDPSession {
throw new Error('Not implemented');
}
/**
* Evaluates the given function with the current handle as its first argument.
*/
@ -84,9 +87,12 @@ export class JSHandle<T = unknown> {
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async evaluate(): Promise<unknown> {
throw new Error('Not implemented');
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.realm.evaluate(pageFunction, this, ...args);
}
/**
@ -99,23 +105,31 @@ export class JSHandle<T = unknown> {
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async evaluateHandle(): Promise<HandleFor<unknown>> {
throw new Error('Not implemented');
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.realm.evaluateHandle(pageFunction, this, ...args);
}
/**
* Fetches a single property from the referenced object.
*/
async getProperty<K extends keyof T>(
getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
async getProperty(propertyName: string): Promise<JSHandle<unknown>>;
getProperty(propertyName: string): Promise<JSHandle<unknown>>;
/**
* @internal
*/
async getProperty<K extends keyof T>(
propertyName: HandleOr<K>
): Promise<HandleFor<T[K]>>;
async getProperty<K extends keyof T>(): Promise<HandleFor<T[K]>> {
throw new Error('Not implemented');
): Promise<HandleFor<T[K]>> {
return await this.evaluateHandle((object, propertyName) => {
return object[propertyName as K];
}, propertyName);
}
/**
@ -137,7 +151,29 @@ export class JSHandle<T = unknown> {
* ```
*/
async getProperties(): Promise<Map<string, JSHandle>> {
throw new Error('Not implemented');
const propertyNames = await this.evaluate(object => {
const enumerableProperties = [];
const descriptors = Object.getOwnPropertyDescriptors(object);
for (const propertyName in descriptors) {
if (descriptors[propertyName]?.enumerable) {
enumerableProperties.push(propertyName);
}
}
return enumerableProperties;
});
const map = new Map<string, JSHandle>();
const results = await Promise.all(
propertyNames.map(key => {
return this.getProperty(key);
})
);
for (const [key, value] of Object.entries(propertyNames)) {
using handle = results[key as any];
if (handle) {
map.set(value, handle.move());
}
}
return map;
}
/**
@ -148,24 +184,18 @@ export class JSHandle<T = unknown> {
* @remarks
* If the object has a `toJSON` function, it **will not** be called.
*/
async jsonValue(): Promise<T> {
throw new Error('Not implemented');
}
abstract jsonValue(): Promise<T>;
/**
* Either `null` or the handle itself if the handle is an
* instance of {@link ElementHandle}.
*/
asElement(): ElementHandle<Node> | null {
throw new Error('Not implemented');
}
abstract asElement(): ElementHandle<Node> | null;
/**
* Releases the object referenced by the handle for garbage collection.
*/
async dispose(): Promise<void> {
throw new Error('Not implemented');
}
abstract dispose(): Promise<void>;
/**
* Returns a string representation of the JSHandle.
@ -173,23 +203,25 @@ export class JSHandle<T = unknown> {
* @remarks
* Useful during debugging.
*/
toString(): string {
throw new Error('Not implemented');
}
abstract toString(): string;
/**
* @internal
*/
get id(): string | undefined {
throw new Error('Not implemented');
}
abstract get id(): string | undefined;
/**
* Provides access to the
* {@link https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject | Protocol.Runtime.RemoteObject}
* backing this handle.
*/
remoteObject(): Protocol.Runtime.RemoteObject {
throw new Error('Not implemented');
abstract remoteObject(): Protocol.Runtime.RemoteObject;
[Symbol.dispose](): void {
return void this.dispose().catch(debugError);
}
[Symbol.asyncDispose](): Promise<void> {
return this.dispose();
}
}

Просмотреть файл

@ -1,938 +0,0 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TimeoutError} from '../common/Errors.js';
import {EventEmitter} from '../common/EventEmitter.js';
import {Awaitable, HandleFor, NodeFor} from '../common/types.js';
import {debugError} from '../common/util.js';
import {isErrorLike} from '../util/ErrorLike.js';
import {BoundingBox, ClickOptions, ElementHandle} from './ElementHandle.js';
import type {Frame} from './Frame.js';
import type {Page} from './Page.js';
interface LocatorContext<T> {
conditions?: Set<ActionCondition<T>>;
}
const LOCATOR_CONTEXTS = new WeakMap<Locator<unknown>, LocatorContext<never>>();
/**
* @public
*/
export type VisibilityOption = 'hidden' | 'visible' | null;
/**
* @public
*/
export interface LocatorOptions {
/**
* Whether to wait for the element to be `visible` or `hidden`. `null` to
* disable visibility checks.
*/
visibility: VisibilityOption;
/**
* Total timeout for the entire locator operation.
*
* Pass `0` to disable timeout.
*
* @defaultValue `Page.getDefaultTimeout()`
*/
timeout: number;
/**
* Whether to scroll the element into viewport if not in the viewprot already.
* @defaultValue `true`
*/
ensureElementIsInTheViewport: boolean;
/**
* Whether to wait for input elements to become enabled before the action.
* Applicable to `click` and `fill` actions.
* @defaultValue `true`
*/
waitForEnabled: boolean;
/**
* Whether to wait for the element's bounding box to be same between two
* animation frames.
* @defaultValue `true`
*/
waitForStableBoundingBox: boolean;
}
/**
* Timeout for individual operations inside the locator. On errors the
* operation is retried as long as {@link Locator.setTimeout} is not
* exceeded. This timeout should be generally much lower as locating an
* element means multiple asynchronious operations.
*/
const CONDITION_TIMEOUT = 1_000;
const WAIT_FOR_FUNCTION_DELAY = 100;
/**
* @internal
*/
export type ActionCondition<T> = (
element: HandleFor<T>,
signal: AbortSignal
) => Promise<void>;
/**
* @public
*/
export type Predicate<From, To extends From = From> =
| ((value: From) => value is To)
| ((value: From) => Awaitable<boolean>);
/**
* @public
*/
export interface ActionOptions {
signal?: AbortSignal;
}
/**
* @public
*/
export type LocatorClickOptions = ClickOptions & ActionOptions;
/**
* @public
*/
export interface LocatorScrollOptions extends ActionOptions {
scrollTop?: number;
scrollLeft?: number;
}
/**
* All the events that a locator instance may emit.
*
* @public
*/
export enum LocatorEmittedEvents {
/**
* Emitted every time before the locator performs an action on the located element(s).
*/
Action = 'action',
}
/**
* @public
*/
export interface LocatorEventObject {
[LocatorEmittedEvents.Action]: never;
}
type UnionLocatorOf<T> = T extends Array<Locator<infer S>> ? S : never;
/**
* Locators describe a strategy of locating elements and performing an action on
* them. If the action fails because the element is not ready for the action,
* the whole operation is retried. Various preconditions for a successful action
* are checked automatically.
*
* @public
*/
export abstract class Locator<T> extends EventEmitter {
/**
* Used for nominally typing {@link Locator}.
*/
declare _?: T;
/**
* @internal
*/
static create<Selector extends string>(
pageOrFrame: Page | Frame,
selector: Selector
): Locator<NodeFor<Selector>> {
return new NodeLocator<NodeFor<Selector>>(pageOrFrame, selector).setTimeout(
'getDefaultTimeout' in pageOrFrame
? pageOrFrame.getDefaultTimeout()
: pageOrFrame.page().getDefaultTimeout()
);
}
/**
* Creates a race between multiple locators but ensures that only a single one
* acts.
*/
static race<Locators extends Array<Locator<unknown>>>(
locators: Locators
): Locator<UnionLocatorOf<Locators>> {
return new RaceLocator(
locators as Array<Locator<UnionLocatorOf<Locators>>>
);
}
/**
* Creates an expectation that is evaluated against located values.
*
* If the expectations do not match, then the locator will retry.
*
* @internal
*/
expect<S extends T>(predicate: Predicate<T, S>): Locator<S> {
return new ExpectedLocator(this, predicate);
}
override on<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.on(eventName, handler);
}
override once<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.once(eventName, handler);
}
override off<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.off(eventName, handler);
}
abstract setVisibility(visibility: VisibilityOption): this;
abstract setTimeout(timeout: number): this;
abstract setEnsureElementIsInTheViewport(value: boolean): this;
abstract setWaitForEnabled(value: boolean): this;
abstract setWaitForStableBoundingBox(value: boolean): this;
abstract click<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorClickOptions>
): Promise<void>;
/**
* Fills out the input identified by the locator using the provided value. The
* type of the input is determined at runtime and the appropriate fill-out
* method is chosen based on the type. contenteditable, selector, inputs are
* supported.
*/
abstract fill<ElementType extends Element>(
this: Locator<ElementType>,
value: string,
options?: Readonly<ActionOptions>
): Promise<void>;
abstract hover<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<ActionOptions>
): Promise<void>;
abstract scroll<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorScrollOptions>
): Promise<void>;
}
/**
* @internal
*/
export class NodeLocator<T extends Node> extends Locator<T> {
#pageOrFrame: Page | Frame;
#selector: string;
#visibility: VisibilityOption = 'visible';
#timeout = 30_000;
#ensureElementIsInTheViewport = true;
#waitForEnabled = true;
#waitForStableBoundingBox = true;
constructor(pageOrFrame: Page | Frame, selector: string) {
super();
this.#pageOrFrame = pageOrFrame;
this.#selector = selector;
}
setVisibility(visibility: VisibilityOption): this {
this.#visibility = visibility;
return this;
}
setTimeout(timeout: number): this {
this.#timeout = timeout;
return this;
}
setEnsureElementIsInTheViewport(value: boolean): this {
this.#ensureElementIsInTheViewport = value;
return this;
}
setWaitForEnabled(value: boolean): this {
this.#waitForEnabled = value;
return this;
}
setWaitForStableBoundingBox(value: boolean): this {
this.#waitForStableBoundingBox = value;
return this;
}
/**
* Retries the `fn` until a truthy result is returned.
*/
async #waitForFunction(
fn: (signal: AbortSignal) => unknown,
signal?: AbortSignal,
timeout = CONDITION_TIMEOUT
): Promise<void> {
let isActive = true;
let controller: AbortController;
// If the loop times out, we abort only the last iteration's controller.
const timeoutId = timeout
? setTimeout(() => {
isActive = false;
controller?.abort();
}, timeout)
: 0;
// If the user's signal aborts, we abort the last iteration and the loop.
signal?.addEventListener(
'abort',
() => {
controller?.abort();
isActive = false;
clearTimeout(timeoutId);
},
{once: true}
);
while (isActive) {
controller = new AbortController();
try {
const result = await fn(controller.signal);
if (result) {
clearTimeout(timeoutId);
return;
}
} catch (err) {
if (isErrorLike(err)) {
debugError(err);
// Retry on all timeouts.
if (err instanceof TimeoutError) {
continue;
}
// Abort error are ignored as they only affect one iteration.
if (err.name === 'AbortError') {
continue;
}
}
throw err;
} finally {
// We abort any operations that might have been started by `fn`, because
// the iteration is now over.
controller.abort();
}
await new Promise(resolve => {
return setTimeout(resolve, WAIT_FOR_FUNCTION_DELAY);
});
}
signal?.throwIfAborted();
throw new TimeoutError(
`waitForFunction timed out. The timeout is ${timeout}ms.`
);
}
/**
* Checks if the element is in the viewport and auto-scrolls it if it is not.
*/
#ensureElementIsInTheViewportIfNeeded = async <ElementType extends Element>(
element: HandleFor<ElementType>,
signal?: AbortSignal
): Promise<void> => {
if (!this.#ensureElementIsInTheViewport) {
return;
}
// Side-effect: this also checks if it is connected.
const isIntersectingViewport = await element.isIntersectingViewport({
threshold: 0,
});
signal?.throwIfAborted();
if (!isIntersectingViewport) {
await element.scrollIntoView();
signal?.throwIfAborted();
await this.#waitForFunction(async () => {
return await element.isIntersectingViewport({
threshold: 0,
});
}, signal);
signal?.throwIfAborted();
}
};
/**
* Waits for the element to become visible or hidden. visibility === 'visible'
* means that the element has a computed style, the visibility property other
* than 'hidden' or 'collapse' and non-empty bounding box. visibility ===
* 'hidden' means the opposite of that.
*/
#waitForVisibilityIfNeeded = async <ElementType extends Element>(
element: HandleFor<ElementType>,
signal?: AbortSignal
): Promise<void> => {
if (this.#visibility === null) {
return;
}
if (this.#visibility === 'hidden') {
await this.#waitForFunction(async () => {
return element.isHidden();
}, signal);
}
await this.#waitForFunction(async () => {
return element.isVisible();
}, signal);
};
/**
* If the element has a "disabled" property, wait for the element to be
* enabled.
*/
#waitForEnabledIfNeeded = async <ElementType extends Element>(
element: HandleFor<ElementType>,
signal?: AbortSignal
): Promise<void> => {
if (!this.#waitForEnabled) {
return;
}
await this.#pageOrFrame.waitForFunction(
el => {
if ('disabled' in el && typeof el.disabled === 'boolean') {
return !el.disabled;
}
return true;
},
{
timeout: CONDITION_TIMEOUT,
signal,
},
element
);
};
/**
* Compares the bounding box of the element for two consecutive animation
* frames and waits till they are the same.
*/
#waitForStableBoundingBoxIfNeeded = async <ElementType extends Element>(
element: HandleFor<ElementType>,
signal?: AbortSignal
): Promise<void> => {
if (!this.#waitForStableBoundingBox) {
return;
}
function getClientRect() {
return element.evaluate(el => {
return new Promise<[BoundingBox, BoundingBox]>(resolve => {
window.requestAnimationFrame(() => {
const rect1 = el.getBoundingClientRect();
window.requestAnimationFrame(() => {
const rect2 = el.getBoundingClientRect();
resolve([
{
x: rect1.x,
y: rect1.y,
width: rect1.width,
height: rect1.height,
},
{
x: rect2.x,
y: rect2.y,
width: rect2.width,
height: rect2.height,
},
]);
});
});
});
});
}
await this.#waitForFunction(async () => {
const [rect1, rect2] = await getClientRect();
return (
rect1.x === rect2.x &&
rect1.y === rect2.y &&
rect1.width === rect2.width &&
rect1.height === rect2.height
);
}, signal);
};
#run(
action: (el: HandleFor<T>) => Promise<void>,
signal?: AbortSignal,
conditions: Array<ActionCondition<T>> = []
) {
const globalConditions = [
...(LOCATOR_CONTEXTS.get(this)?.conditions?.values() ?? []),
] as Array<ActionCondition<T>>;
const allConditions = conditions.concat(globalConditions);
return this.#waitForFunction(
async signal => {
// 1. Select the element without visibility checks.
const element = (await this.#pageOrFrame.waitForSelector(
this.#selector,
{
visible: false,
timeout: this.#timeout,
signal,
}
)) as HandleFor<T> | null;
// Retry if no element is found.
if (!element) {
return false;
}
try {
signal?.throwIfAborted();
// 2. Perform action specific checks.
await Promise.all(
allConditions.map(check => {
return check(element, signal);
})
);
signal?.throwIfAborted();
// 3. Perform the action
this.emit(LocatorEmittedEvents.Action);
await action(element);
return true;
} finally {
void element.dispose().catch(debugError);
}
},
signal,
this.#timeout
);
}
async click<ElementType extends Element>(
this: NodeLocator<ElementType>,
options?: Readonly<LocatorClickOptions>
): Promise<void> {
return await this.#run(
async element => {
await element.click(options);
},
options?.signal,
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForVisibilityIfNeeded,
this.#waitForEnabledIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
]
);
}
/**
* Fills out the input identified by the locator using the provided value. The
* type of the input is determined at runtime and the appropriate fill-out
* method is chosen based on the type. contenteditable, selector, inputs are
* supported.
*/
fill<ElementType extends Element>(
this: NodeLocator<ElementType>,
value: string,
options?: Readonly<ActionOptions>
): Promise<void> {
return this.#run(
async element => {
const input = element as unknown as ElementHandle<HTMLElement>;
const inputType = await input.evaluate(el => {
if (el instanceof HTMLSelectElement) {
return 'select';
}
if (el instanceof HTMLInputElement) {
if (
new Set([
'textarea',
'text',
'url',
'tel',
'search',
'password',
'number',
'email',
]).has(el.type)
) {
return 'typeable-input';
} else {
return 'other-input';
}
}
if (el.isContentEditable) {
return 'contenteditable';
}
return 'unknown';
});
switch (inputType) {
case 'select':
await input.select(value);
break;
case 'contenteditable':
case 'typeable-input':
const textToType = await (
input as ElementHandle<HTMLInputElement>
).evaluate((input, newValue) => {
const currentValue = input.isContentEditable
? input.innerText
: input.value;
// Clear the input if the current value does not match the filled
// out value.
if (
newValue.length <= currentValue.length ||
!newValue.startsWith(input.value)
) {
if (input.isContentEditable) {
input.innerText = '';
} else {
input.value = '';
}
return newValue;
}
const originalValue = input.isContentEditable
? input.innerText
: input.value;
// If the value is partially filled out, only type the rest. Move
// cursor to the end of the common prefix.
if (input.isContentEditable) {
input.innerText = '';
input.innerText = originalValue;
} else {
input.value = '';
input.value = originalValue;
}
return newValue.substring(originalValue.length);
}, value);
await input.type(textToType);
break;
case 'other-input':
await input.focus();
await input.evaluate((input, value) => {
(input as HTMLInputElement).value = value;
input.dispatchEvent(new Event('input', {bubbles: true}));
input.dispatchEvent(new Event('change', {bubbles: true}));
}, value);
break;
case 'unknown':
throw new Error(`Element cannot be filled out.`);
}
},
options?.signal,
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForVisibilityIfNeeded,
this.#waitForEnabledIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
]
);
}
hover<ElementType extends Element>(
this: NodeLocator<ElementType>,
options?: Readonly<ActionOptions>
): Promise<void> {
return this.#run(
async element => {
await element.hover();
},
options?.signal,
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForVisibilityIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
]
);
}
scroll<ElementType extends Element>(
this: NodeLocator<ElementType>,
options?: Readonly<LocatorScrollOptions>
): Promise<void> {
return this.#run(
async element => {
await element.evaluate(
(el, scrollTop, scrollLeft) => {
if (scrollTop !== undefined) {
el.scrollTop = scrollTop;
}
if (scrollLeft !== undefined) {
el.scrollLeft = scrollLeft;
}
},
options?.scrollTop,
options?.scrollLeft
);
},
options?.signal,
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForVisibilityIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
]
);
}
}
class ExpectedLocator<From, To extends From> extends Locator<To> {
#base: Locator<From>;
#predicate: Predicate<From, To>;
constructor(base: Locator<From>, predicate: Predicate<From, To>) {
super();
this.#base = base;
this.#predicate = predicate;
}
override setVisibility(visibility: VisibilityOption): this {
this.#base.setVisibility(visibility);
return this;
}
override setTimeout(timeout: number): this {
this.#base.setTimeout(timeout);
return this;
}
override setEnsureElementIsInTheViewport(value: boolean): this {
this.#base.setEnsureElementIsInTheViewport(value);
return this;
}
override setWaitForEnabled(value: boolean): this {
this.#base.setWaitForEnabled(value);
return this;
}
override setWaitForStableBoundingBox(value: boolean): this {
this.#base.setWaitForStableBoundingBox(value);
return this;
}
#condition: ActionCondition<From> = async (handle, signal) => {
// TODO(jrandolf): We should remove this once JSHandle has waitForFunction.
await (handle as ElementHandle<Node>).frame.waitForFunction(
this.#predicate,
{signal},
handle
);
};
#insertFilterCondition<
FromElement extends Node,
ToElement extends FromElement,
>(this: ExpectedLocator<FromElement, ToElement>): void {
const context = (LOCATOR_CONTEXTS.get(this.#base) ??
{}) as LocatorContext<FromElement>;
context.conditions ??= new Set();
context.conditions.add(this.#condition);
LOCATOR_CONTEXTS.set(this.#base, context);
}
override click<FromElement extends Element, ToElement extends FromElement>(
this: ExpectedLocator<FromElement, ToElement>,
options?: Readonly<LocatorClickOptions>
): Promise<void> {
this.#insertFilterCondition();
return this.#base.click(options);
}
override fill<FromElement extends Element, ToElement extends FromElement>(
this: ExpectedLocator<FromElement, ToElement>,
value: string,
options?: Readonly<ActionOptions>
): Promise<void> {
this.#insertFilterCondition();
return this.#base.fill(value, options);
}
override hover<FromElement extends Element, ToElement extends FromElement>(
this: ExpectedLocator<FromElement, ToElement>,
options?: Readonly<ActionOptions>
): Promise<void> {
this.#insertFilterCondition();
return this.#base.hover(options);
}
override scroll<FromElement extends Element, ToElement extends FromElement>(
this: ExpectedLocator<FromElement, ToElement>,
options?: Readonly<LocatorScrollOptions>
): Promise<void> {
this.#insertFilterCondition();
return this.#base.scroll(options);
}
}
/**
* @internal
*/
class RaceLocator<T> extends Locator<T> {
#locators: Array<Locator<T>>;
constructor(locators: Array<Locator<T>>) {
super();
this.#locators = locators;
}
override setVisibility(visibility: VisibilityOption): this {
for (const locator of this.#locators) {
locator.setVisibility(visibility);
}
return this;
}
override setTimeout(timeout: number): this {
for (const locator of this.#locators) {
locator.setTimeout(timeout);
}
return this;
}
override setEnsureElementIsInTheViewport(value: boolean): this {
for (const locator of this.#locators) {
locator.setEnsureElementIsInTheViewport(value);
}
return this;
}
override setWaitForEnabled(value: boolean): this {
for (const locator of this.#locators) {
locator.setWaitForEnabled(value);
}
return this;
}
override setWaitForStableBoundingBox(value: boolean): this {
for (const locator of this.#locators) {
locator.setWaitForStableBoundingBox(value);
}
return this;
}
async #run(
action: (locator: Locator<T>, signal: AbortSignal) => Promise<void>,
signal?: AbortSignal
) {
const abortControllers = new WeakMap<Locator<T>, AbortController>();
// Abort all locators if the user-provided signal aborts.
signal?.addEventListener('abort', () => {
for (const locator of this.#locators) {
abortControllers.get(locator)?.abort();
}
});
const handleLocatorAction = (locator: Locator<T>): (() => void) => {
return () => {
// When one locator is ready to act, we will abort other locators.
for (const other of this.#locators) {
if (other !== locator) {
abortControllers.get(other)?.abort();
}
}
this.emit(LocatorEmittedEvents.Action);
};
};
const createAbortController = (locator: Locator<T>): AbortController => {
const abortController = new AbortController();
abortControllers.set(locator, abortController);
return abortController;
};
const results = await Promise.allSettled(
this.#locators.map(locator => {
return action(
locator.on(LocatorEmittedEvents.Action, handleLocatorAction(locator)),
createAbortController(locator).signal
);
})
);
signal?.throwIfAborted();
const rejected = results.filter(
(result): result is PromiseRejectedResult => {
return result.status === 'rejected';
}
);
// If some locators are fulfilled, do not throw.
if (rejected.length !== results.length) {
return;
}
for (const result of rejected) {
const reason = result.reason;
// AbortError is be an expected result of a race.
if (isErrorLike(reason) && reason.name === 'AbortError') {
continue;
}
throw reason;
}
}
async click<ElementType extends Element>(
this: RaceLocator<ElementType>,
options?: Readonly<LocatorClickOptions>
): Promise<void> {
return await this.#run(
(locator, signal) => {
return locator.click({...options, signal});
},
options?.signal
);
}
async fill<ElementType extends Element>(
this: RaceLocator<ElementType>,
value: string,
options?: Readonly<ActionOptions>
): Promise<void> {
return await this.#run(
(locator, signal) => {
return locator.fill(value, {...options, signal});
},
options?.signal
);
}
async hover<ElementType extends Element>(
this: RaceLocator<ElementType>,
options?: Readonly<ActionOptions>
): Promise<void> {
return await this.#run(
(locator, signal) => {
return locator.hover({...options, signal});
},
options?.signal
);
}
async scroll<ElementType extends Element>(
this: RaceLocator<ElementType>,
options?: Readonly<LocatorScrollOptions>
): Promise<void> {
return await this.#run(
(locator, signal) => {
return locator.scroll({...options, signal});
},
options?.signal
);
}
}

Просмотреть файл

@ -18,14 +18,30 @@ import type {Readable} from 'stream';
import {Protocol} from 'devtools-protocol';
import {
filterAsync,
first,
firstValueFrom,
from,
fromEvent,
map,
merge,
Observable,
raceWith,
delay,
filter,
of,
switchMap,
startWith,
} from '../../third_party/rxjs/rxjs.js';
import type {HTTPRequest} from '../api/HTTPRequest.js';
import type {HTTPResponse} from '../api/HTTPResponse.js';
import type {Accessibility} from '../common/Accessibility.js';
import type {CDPSession} from '../common/Connection.js';
import type {ConsoleMessage} from '../common/ConsoleMessage.js';
import type {Coverage} from '../common/Coverage.js';
import {Device} from '../common/Device.js';
import {DeviceRequestPrompt} from '../common/DeviceRequestPrompt.js';
import type {Dialog} from '../common/Dialog.js';
import {TargetCloseError} from '../common/Errors.js';
import {EventEmitter, Handler} from '../common/EventEmitter.js';
import type {FileChooser} from '../common/FileChooser.js';
@ -43,19 +59,20 @@ import {
PDFOptions,
} from '../common/PDFOptions.js';
import type {Viewport} from '../common/PuppeteerViewport.js';
import type {Target} from '../common/Target.js';
import type {Tracing} from '../common/Tracing.js';
import type {
Awaitable,
EvaluateFunc,
EvaluateFuncWith,
HandleFor,
NodeFor,
} from '../common/types.js';
import {
debugError,
importFSPromises,
isNumber,
isString,
waitForEvent,
timeout,
withSourcePuppeteerURLIfNone,
} from '../common/util.js';
import type {WebWorker} from '../common/WebWorker.js';
@ -64,6 +81,7 @@ import {Deferred} from '../util/Deferred.js';
import type {Browser} from './Browser.js';
import type {BrowserContext} from './BrowserContext.js';
import type {Dialog} from './Dialog.js';
import type {ClickOptions, ElementHandle} from './ElementHandle.js';
import type {
Frame,
@ -71,9 +89,15 @@ import type {
FrameAddStyleTagOptions,
FrameWaitForFunctionOptions,
} from './Frame.js';
import {Keyboard, Mouse, Touchscreen, KeyboardTypeOptions} from './Input.js';
import {Keyboard, KeyboardTypeOptions, Mouse, Touchscreen} from './Input.js';
import type {JSHandle} from './JSHandle.js';
import {Locator} from './Locator.js';
import {
AwaitedLocator,
FunctionLocator,
Locator,
NodeLocator,
} from './locators/locators.js';
import type {Target} from './Target.js';
/**
* @public
@ -458,7 +482,10 @@ export interface NewDocumentScriptEvaluation {
*
* @public
*/
export class Page extends EventEmitter {
export abstract class Page
extends EventEmitter
implements AsyncDisposable, Disposable
{
#handlerMap = new WeakMap<Handler<any>, Handler<any>>();
/**
@ -622,6 +649,13 @@ export class Page extends EventEmitter {
throw new Error('Not implemented');
}
/**
* Creates a Chrome Devtools Protocol session attached to the page.
*/
createCDPSession(): Promise<CDPSession> {
throw new Error('Not implemented');
}
/**
* {@inheritDoc Keyboard}
*/
@ -824,7 +858,7 @@ export class Page extends EventEmitter {
}
/**
* Creates a locator for the provided `selector`. See {@link Locator} for
* Creates a locator for the provided selector. See {@link Locator} for
* details and supported actions.
*
* @remarks
@ -833,8 +867,25 @@ export class Page extends EventEmitter {
*/
locator<Selector extends string>(
selector: Selector
): Locator<NodeFor<Selector>> {
return Locator.create(this, selector);
): Locator<NodeFor<Selector>>;
/**
* Creates a locator for the provided function. See {@link Locator} for
* details and supported actions.
*
* @remarks
* Locators API is experimental and we will not follow semver for breaking
* change in the Locators API.
*/
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;
locator<Selector extends string, Ret>(
selectorOrFunc: Selector | (() => Awaitable<Ret>)
): Locator<NodeFor<Selector>> | Locator<Ret> {
if (typeof selectorOrFunc === 'string') {
return NodeLocator.create(this, selectorOrFunc);
} else {
return FunctionLocator.create(this, selectorOrFunc);
}
}
/**
@ -842,7 +893,9 @@ export class Page extends EventEmitter {
*
* @internal
*/
locatorRace(locators: Array<Locator<Node>>): Locator<Node> {
locatorRace<Locators extends readonly unknown[] | []>(
locators: Locators
): Locator<AwaitedLocator<Locators[number]>> {
return Locator.race(locators);
}
@ -857,7 +910,7 @@ export class Page extends EventEmitter {
async $<Selector extends string>(
selector: Selector
): Promise<ElementHandle<NodeFor<Selector>> | null> {
return this.mainFrame().$(selector);
return await this.mainFrame().$(selector);
}
/**
@ -870,7 +923,7 @@ export class Page extends EventEmitter {
async $$<Selector extends string>(
selector: Selector
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
return this.mainFrame().$$(selector);
return await this.mainFrame().$$(selector);
}
/**
@ -936,12 +989,12 @@ export class Page extends EventEmitter {
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
async evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
throw new Error('Not implemented');
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluateHandle.name,
pageFunction
);
return await this.mainFrame().evaluateHandle(pageFunction, ...args);
}
/**
@ -1049,7 +1102,7 @@ export class Page extends EventEmitter {
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$eval.name, pageFunction);
return this.mainFrame().$eval(selector, pageFunction, ...args);
return await this.mainFrame().$eval(selector, pageFunction, ...args);
}
/**
@ -1127,7 +1180,7 @@ export class Page extends EventEmitter {
...args: Params
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(this.$$eval.name, pageFunction);
return this.mainFrame().$$eval(selector, pageFunction, ...args);
return await this.mainFrame().$$eval(selector, pageFunction, ...args);
}
/**
@ -1141,7 +1194,7 @@ export class Page extends EventEmitter {
* @param expression - Expression to evaluate
*/
async $x(expression: string): Promise<Array<ElementHandle<Node>>> {
return this.mainFrame().$x(expression);
return await this.mainFrame().$x(expression);
}
/**
@ -1186,7 +1239,7 @@ export class Page extends EventEmitter {
async addScriptTag(
options: FrameAddScriptTagOptions
): Promise<ElementHandle<HTMLScriptElement>> {
return this.mainFrame().addScriptTag(options);
return await this.mainFrame().addScriptTag(options);
}
/**
@ -1208,7 +1261,7 @@ export class Page extends EventEmitter {
async addStyleTag(
options: FrameAddStyleTagOptions
): Promise<ElementHandle<HTMLStyleElement | HTMLLinkElement>> {
return this.mainFrame().addStyleTag(options);
return await this.mainFrame().addStyleTag(options);
}
/**
@ -1281,13 +1334,10 @@ export class Page extends EventEmitter {
* @param pptrFunction - Callback function which will be called in Puppeteer's
* context.
*/
async exposeFunction(
abstract exposeFunction(
name: string,
pptrFunction: Function | {default: Function}
): Promise<void>;
async exposeFunction(): Promise<void> {
throw new Error('Not implemented');
}
/**
* The method removes a previously added function via ${@link Page.exposeFunction}
@ -1394,14 +1444,14 @@ export class Page extends EventEmitter {
* {@link Frame.url | page.mainFrame().url()}.
*/
url(): string {
throw new Error('Not implemented');
return this.mainFrame().url();
}
/**
* The full HTML contents of the page, including the DOCTYPE.
*/
async content(): Promise<string> {
throw new Error('Not implemented');
return await this.mainFrame().content();
}
/**
@ -1430,9 +1480,8 @@ export class Page extends EventEmitter {
* - `networkidle2` : consider setting content to be finished when there are
* no more than 2 network connections for at least `500` ms.
*/
async setContent(html: string, options?: WaitForOptions): Promise<void>;
async setContent(): Promise<void> {
throw new Error('Not implemented');
async setContent(html: string, options?: WaitForOptions): Promise<void> {
await this.mainFrame().setContent(html, options);
}
/**
@ -1495,9 +1544,8 @@ export class Page extends EventEmitter {
async goto(
url: string,
options?: WaitForOptions & {referer?: string; referrerPolicy?: string}
): Promise<HTTPResponse | null>;
async goto(): Promise<HTTPResponse | null> {
throw new Error('Not implemented');
): Promise<HTTPResponse | null> {
return await this.mainFrame().goto(url, options);
}
/**
@ -1652,69 +1700,39 @@ export class Page extends EventEmitter {
inFlightRequestsCount: () => number;
},
idleTime: number,
timeout: number,
ms: number,
closedDeferred: Deferred<TargetCloseError>
): Promise<void> {
const idleDeferred = Deferred.create<void>();
const abortDeferred = Deferred.create<Error>();
let idleTimer: NodeJS.Timeout | undefined;
const cleanup = () => {
clearTimeout(idleTimer);
abortDeferred.reject(new Error('abort'));
};
const evaluate = () => {
clearTimeout(idleTimer);
if (networkManager.inFlightRequestsCount() === 0) {
idleTimer = setTimeout(() => {
return idleDeferred.resolve();
}, idleTime);
}
};
const listenToEvent = (event: symbol) => {
return waitForEvent(
networkManager,
event,
() => {
evaluate();
return false;
},
timeout,
abortDeferred
);
};
const eventPromises = [
listenToEvent(NetworkManagerEmittedEvents.Request),
listenToEvent(NetworkManagerEmittedEvents.Response),
listenToEvent(NetworkManagerEmittedEvents.RequestFailed),
];
evaluate();
// We don't want to reject the closed deferred when
// the race if finished so we pass the Promise instead
const closedPromise = closedDeferred.valueOrThrow();
await Deferred.race([idleDeferred, ...eventPromises, closedPromise]).then(
r => {
cleanup();
return r;
},
error => {
cleanup();
throw error;
}
await firstValueFrom(
merge(
fromEvent(
networkManager,
NetworkManagerEmittedEvents.Request as unknown as string
),
fromEvent(
networkManager,
NetworkManagerEmittedEvents.Response as unknown as string
),
fromEvent(
networkManager,
NetworkManagerEmittedEvents.RequestFailed as unknown as string
)
).pipe(
startWith(null),
filter(() => {
return networkManager.inFlightRequestsCount() === 0;
}),
switchMap(v => {
return of(v).pipe(delay(idleTime));
}),
raceWith(timeout(ms), from(closedDeferred.valueOrThrow()))
)
);
}
/**
* @param urlOrPredicate - A URL or predicate to wait for.
* @param options - Optional waiting parameters
* @returns Promise which resolves to the matched frame.
* Waits for a frame matching the given conditions to appear.
*
* @example
*
* ```ts
@ -1722,20 +1740,37 @@ export class Page extends EventEmitter {
* return frame.name() === 'Test';
* });
* ```
*
* @remarks
* Optional Parameter have:
*
* - `timeout`: Maximum wait time in milliseconds, defaults to `30` seconds,
* pass `0` to disable the timeout. The default value can be changed by using
* the {@link Page.setDefaultTimeout} method.
*/
async waitForFrame(
urlOrPredicate: string | ((frame: Frame) => boolean | Promise<boolean>),
options?: {timeout?: number}
): Promise<Frame>;
async waitForFrame(): Promise<Frame> {
throw new Error('Not implemented');
urlOrPredicate: string | ((frame: Frame) => Awaitable<boolean>),
options: WaitTimeoutOptions = {}
): Promise<Frame> {
const {timeout: ms = this.getDefaultTimeout()} = options;
if (isString(urlOrPredicate)) {
urlOrPredicate = (frame: Frame) => {
return urlOrPredicate === frame.url();
};
}
return await firstValueFrom(
merge(
fromEvent(this, PageEmittedEvents.FrameAttached) as Observable<Frame>,
fromEvent(this, PageEmittedEvents.FrameNavigated) as Observable<Frame>,
from(this.frames())
).pipe(
filterAsync(urlOrPredicate),
first(),
raceWith(
timeout(ms),
fromEvent(this, PageEmittedEvents.Close).pipe(
map(() => {
throw new TargetCloseError('Page closed.');
})
)
)
)
);
}
/**
@ -2169,12 +2204,12 @@ export class Page extends EventEmitter {
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(): Promise<Awaited<ReturnType<Func>>> {
throw new Error('Not implemented');
): Promise<Awaited<ReturnType<Func>>> {
pageFunction = withSourcePuppeteerURLIfNone(
this.evaluate.name,
pageFunction
);
return await this.mainFrame().evaluate(pageFunction, ...args);
}
/**
@ -2407,7 +2442,7 @@ export class Page extends EventEmitter {
* Shortcut for {@link Frame.title | page.mainFrame().title()}.
*/
async title(): Promise<string> {
throw new Error('Not implemented');
return await this.mainFrame().title();
}
async close(options?: {runBeforeUnload?: boolean}): Promise<void>;
@ -2808,6 +2843,14 @@ export class Page extends EventEmitter {
waitForDevicePrompt(): Promise<DeviceRequestPrompt> {
throw new Error('Not implemented');
}
[Symbol.dispose](): void {
return void this.close().catch(debugError);
}
[Symbol.asyncDispose](): Promise<void> {
return this.close();
}
}
/**

Просмотреть файл

@ -0,0 +1,106 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {TimeoutSettings} from '../common/TimeoutSettings.js';
import {EvaluateFunc, HandleFor, InnerLazyParams} from '../common/types.js';
import {TaskManager, WaitTask} from '../common/WaitTask.js';
import {ElementHandle} from './ElementHandle.js';
import {Environment} from './Environment.js';
import {JSHandle} from './JSHandle.js';
/**
* @internal
*/
export abstract class Realm implements Disposable {
protected readonly timeoutSettings: TimeoutSettings;
readonly taskManager = new TaskManager();
constructor(timeoutSettings: TimeoutSettings) {
this.timeoutSettings = timeoutSettings;
}
abstract get environment(): Environment;
abstract adoptHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
abstract transferHandle<T extends JSHandle<Node>>(handle: T): Promise<T>;
abstract evaluateHandle<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>>;
abstract evaluate<
Params extends unknown[],
Func extends EvaluateFunc<Params> = EvaluateFunc<Params>,
>(
pageFunction: Func | string,
...args: Params
): Promise<Awaited<ReturnType<Func>>>;
async waitForFunction<
Params extends unknown[],
Func extends EvaluateFunc<InnerLazyParams<Params>> = EvaluateFunc<
InnerLazyParams<Params>
>,
>(
pageFunction: Func | string,
options: {
polling?: 'raf' | 'mutation' | number;
timeout?: number;
root?: ElementHandle<Node>;
signal?: AbortSignal;
} = {},
...args: Params
): Promise<HandleFor<Awaited<ReturnType<Func>>>> {
const {
polling = 'raf',
timeout = this.timeoutSettings.timeout(),
root,
signal,
} = options;
if (typeof polling === 'number' && polling < 0) {
throw new Error('Cannot poll with non-positive interval');
}
const waitTask = new WaitTask(
this,
{
polling,
root,
timeout,
signal,
},
pageFunction as unknown as
| ((...args: unknown[]) => Promise<Awaited<ReturnType<Func>>>)
| string,
...args
);
return await waitTask.result;
}
get disposed(): boolean {
return this.#disposed;
}
#disposed = false;
[Symbol.dispose](): void {
this.#disposed = true;
this.taskManager.terminateAll(
new Error('waitForFunction failed: frame got detached.')
);
}
}

Просмотреть файл

@ -0,0 +1,110 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import type {Browser} from '../api/Browser.js';
import type {BrowserContext} from '../api/BrowserContext.js';
import {Page} from '../api/Page.js';
import {CDPSession} from '../common/Connection.js';
import {WebWorker} from '../common/WebWorker.js';
/**
* @public
*/
export enum TargetType {
PAGE = 'page',
BACKGROUND_PAGE = 'background_page',
SERVICE_WORKER = 'service_worker',
SHARED_WORKER = 'shared_worker',
BROWSER = 'browser',
WEBVIEW = 'webview',
OTHER = 'other',
/**
* @internal
*/
TAB = 'tab',
}
/**
* Target represents a
* {@link https://chromedevtools.github.io/devtools-protocol/tot/Target/ | CDP target}.
* In CDP a target is something that can be debugged such a frame, a page or a
* worker.
* @public
*/
export class Target {
/**
* @internal
*/
protected constructor() {}
/**
* If the target is not of type `"service_worker"` or `"shared_worker"`, returns `null`.
*/
async worker(): Promise<WebWorker | null> {
return null;
}
/**
* If the target is not of type `"page"`, `"webview"` or `"background_page"`,
* returns `null`.
*/
async page(): Promise<Page | null> {
return null;
}
url(): string {
throw new Error('not implemented');
}
/**
* Creates a Chrome Devtools Protocol session attached to the target.
*/
createCDPSession(): Promise<CDPSession> {
throw new Error('not implemented');
}
/**
* Identifies what kind of target this is.
*
* @remarks
*
* See {@link https://developer.chrome.com/extensions/background_pages | docs} for more info about background pages.
*/
type(): TargetType {
throw new Error('not implemented');
}
/**
* Get the browser the target belongs to.
*/
browser(): Browser {
throw new Error('not implemented');
}
/**
* Get the browser context the target belongs to.
*/
browserContext(): BrowserContext {
throw new Error('not implemented');
}
/**
* Get the target that opened this target. Top-level targets return `null`.
*/
opener(): Target | undefined {
throw new Error('not implemented');
}
}

Просмотреть файл

@ -16,11 +16,15 @@
export * from './Browser.js';
export * from './BrowserContext.js';
export * from './Page.js';
export * from './JSHandle.js';
export * from './Dialog.js';
export * from './ElementHandle.js';
export * from './Input.js';
export * from './Environment.js';
export * from './Frame.js';
export * from './HTTPResponse.js';
export * from './HTTPRequest.js';
export * from './Locator.js';
export * from './HTTPResponse.js';
export * from './Input.js';
export * from './JSHandle.js';
export * from './locators/locators.js';
export * from './Page.js';
export * from './Realm.js';
export * from './Target.js';

Просмотреть файл

@ -0,0 +1,97 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Observable} from '../../../third_party/rxjs/rxjs.js';
import {HandleFor} from '../../common/common.js';
import {Locator, VisibilityOption} from './locators.js';
/**
* @internal
*/
export abstract class DelegatedLocator<T, U> extends Locator<U> {
#delegate: Locator<T>;
constructor(delegate: Locator<T>) {
super();
this.#delegate = delegate;
this.copyOptions(this.#delegate);
}
protected get delegate(): Locator<T> {
return this.#delegate;
}
override setTimeout(timeout: number): DelegatedLocator<T, U> {
const locator = super.setTimeout(timeout) as DelegatedLocator<T, U>;
locator.#delegate = this.#delegate.setTimeout(timeout);
return locator;
}
override setVisibility<ValueType extends Node, NodeType extends Node>(
this: DelegatedLocator<ValueType, NodeType>,
visibility: VisibilityOption
): DelegatedLocator<ValueType, NodeType> {
const locator = super.setVisibility<NodeType>(
visibility
) as DelegatedLocator<ValueType, NodeType>;
locator.#delegate = locator.#delegate.setVisibility<ValueType>(visibility);
return locator;
}
override setWaitForEnabled<ValueType extends Node, NodeType extends Node>(
this: DelegatedLocator<ValueType, NodeType>,
value: boolean
): DelegatedLocator<ValueType, NodeType> {
const locator = super.setWaitForEnabled<NodeType>(
value
) as DelegatedLocator<ValueType, NodeType>;
locator.#delegate = this.#delegate.setWaitForEnabled(value);
return locator;
}
override setEnsureElementIsInTheViewport<
ValueType extends Element,
ElementType extends Element,
>(
this: DelegatedLocator<ValueType, ElementType>,
value: boolean
): DelegatedLocator<ValueType, ElementType> {
const locator = super.setEnsureElementIsInTheViewport<ElementType>(
value
) as DelegatedLocator<ValueType, ElementType>;
locator.#delegate = this.#delegate.setEnsureElementIsInTheViewport(value);
return locator;
}
override setWaitForStableBoundingBox<
ValueType extends Element,
ElementType extends Element,
>(
this: DelegatedLocator<ValueType, ElementType>,
value: boolean
): DelegatedLocator<ValueType, ElementType> {
const locator = super.setWaitForStableBoundingBox<ElementType>(
value
) as DelegatedLocator<ValueType, ElementType>;
locator.#delegate = this.#delegate.setWaitForStableBoundingBox(value);
return locator;
}
abstract override _clone(): DelegatedLocator<T, U>;
abstract override _wait(): Observable<HandleFor<U>>;
}

Просмотреть файл

@ -0,0 +1,83 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Observable,
filter,
from,
map,
mergeMap,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/common.js';
import {DelegatedLocator} from './DelegatedLocator.js';
import {ActionOptions, Locator} from './locators.js';
/**
* @public
*/
export type Predicate<From, To extends From = From> =
| ((value: From) => value is To)
| ((value: From) => Awaitable<boolean>);
/**
* @internal
*/
export type HandlePredicate<From, To extends From = From> =
| ((value: HandleFor<From>, signal?: AbortSignal) => value is HandleFor<To>)
| ((value: HandleFor<From>, signal?: AbortSignal) => Awaitable<boolean>);
/**
* @internal
*/
export class FilteredLocator<From, To extends From> extends DelegatedLocator<
From,
To
> {
#predicate: HandlePredicate<From, To>;
constructor(base: Locator<From>, predicate: HandlePredicate<From, To>) {
super(base);
this.#predicate = predicate;
}
override _clone(): FilteredLocator<From, To> {
return new FilteredLocator(
this.delegate.clone(),
this.#predicate
).copyOptions(this);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
return this.delegate._wait(options).pipe(
mergeMap(handle => {
return from(
Promise.resolve(this.#predicate(handle, options?.signal))
).pipe(
filter(value => {
return value;
}),
map(() => {
// SAFETY: It passed the predicate, so this is correct.
return handle as HandleFor<To>;
})
);
}),
throwIfEmpty()
);
}
}

Просмотреть файл

@ -0,0 +1,69 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
Observable,
defer,
from,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/types.js';
import {Frame} from '../Frame.js';
import {Page} from '../Page.js';
import {ActionOptions, Locator} from './locators.js';
/**
* @internal
*/
export class FunctionLocator<T> extends Locator<T> {
static create<Ret>(
pageOrFrame: Page | Frame,
func: () => Awaitable<Ret>
): Locator<Ret> {
return new FunctionLocator<Ret>(pageOrFrame, func).setTimeout(
'getDefaultTimeout' in pageOrFrame
? pageOrFrame.getDefaultTimeout()
: pageOrFrame.page().getDefaultTimeout()
);
}
#pageOrFrame: Page | Frame;
#func: () => Awaitable<T>;
private constructor(pageOrFrame: Page | Frame, func: () => Awaitable<T>) {
super();
this.#pageOrFrame = pageOrFrame;
this.#func = func;
}
override _clone(): FunctionLocator<T> {
return new FunctionLocator(this.#pageOrFrame, this.#func);
}
_wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
const signal = options?.signal;
return defer(() => {
return from(
this.#pageOrFrame.waitForFunction(this.#func, {
timeout: this.timeout,
signal,
})
);
}).pipe(throwIfEmpty());
}
}

Просмотреть файл

@ -0,0 +1,773 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EMPTY,
Observable,
OperatorFunction,
catchError,
defaultIfEmpty,
defer,
filter,
first,
firstValueFrom,
from,
fromEvent,
identity,
ignoreElements,
map,
merge,
mergeMap,
noop,
pipe,
raceWith,
retry,
tap,
} from '../../../third_party/rxjs/rxjs.js';
import {EventEmitter} from '../../common/EventEmitter.js';
import {HandleFor} from '../../common/types.js';
import {debugError, timeout} from '../../common/util.js';
import {BoundingBox, ClickOptions, ElementHandle} from '../ElementHandle.js';
import {
Action,
AwaitedLocator,
FilteredLocator,
HandleMapper,
MappedLocator,
Mapper,
Predicate,
RaceLocator,
} from './locators.js';
/**
* For observables coming from promises, a delay is needed, otherwise RxJS will
* never yield in a permanent failure for a promise.
*
* We also don't want RxJS to do promise operations to often, so we bump the
* delay up to 100ms.
*
* @internal
*/
export const RETRY_DELAY = 100;
/**
* @public
*/
export type VisibilityOption = 'hidden' | 'visible' | null;
/**
* @public
*/
export interface LocatorOptions {
/**
* Whether to wait for the element to be `visible` or `hidden`. `null` to
* disable visibility checks.
*/
visibility: VisibilityOption;
/**
* Total timeout for the entire locator operation.
*
* Pass `0` to disable timeout.
*
* @defaultValue `Page.getDefaultTimeout()`
*/
timeout: number;
/**
* Whether to scroll the element into viewport if not in the viewprot already.
* @defaultValue `true`
*/
ensureElementIsInTheViewport: boolean;
/**
* Whether to wait for input elements to become enabled before the action.
* Applicable to `click` and `fill` actions.
* @defaultValue `true`
*/
waitForEnabled: boolean;
/**
* Whether to wait for the element's bounding box to be same between two
* animation frames.
* @defaultValue `true`
*/
waitForStableBoundingBox: boolean;
}
/**
* @public
*/
export interface ActionOptions {
signal?: AbortSignal;
}
/**
* @public
*/
export type LocatorClickOptions = ClickOptions & ActionOptions;
/**
* @public
*/
export interface LocatorScrollOptions extends ActionOptions {
scrollTop?: number;
scrollLeft?: number;
}
/**
* All the events that a locator instance may emit.
*
* @public
*/
export enum LocatorEmittedEvents {
/**
* Emitted every time before the locator performs an action on the located element(s).
*/
Action = 'action',
}
/**
* @public
*/
export interface LocatorEventObject {
[LocatorEmittedEvents.Action]: never;
}
/**
* Locators describe a strategy of locating objects and performing an action on
* them. If the action fails because the object is not ready for the action, the
* whole operation is retried. Various preconditions for a successful action are
* checked automatically.
*
* @public
*/
export abstract class Locator<T> extends EventEmitter {
/**
* Creates a race between multiple locators but ensures that only a single one
* acts.
*
* @public
*/
static race<Locators extends readonly unknown[] | []>(
locators: Locators
): Locator<AwaitedLocator<Locators[number]>> {
return RaceLocator.create(locators);
}
/**
* Used for nominally typing {@link Locator}.
*/
declare _?: T;
/**
* @internal
*/
protected visibility: VisibilityOption = null;
/**
* @internal
*/
protected _timeout = 30_000;
#ensureElementIsInTheViewport = true;
#waitForEnabled = true;
#waitForStableBoundingBox = true;
/**
* @internal
*/
protected operators = {
conditions: (
conditions: Array<Action<T, never>>,
signal?: AbortSignal
): OperatorFunction<HandleFor<T>, HandleFor<T>> => {
return mergeMap((handle: HandleFor<T>) => {
return merge(
...conditions.map(condition => {
return condition(handle, signal);
})
).pipe(defaultIfEmpty(handle));
});
},
retryAndRaceWithSignalAndTimer: <T>(
signal?: AbortSignal
): OperatorFunction<T, T> => {
const candidates = [];
if (signal) {
candidates.push(
fromEvent(signal, 'abort').pipe(
map(() => {
throw signal.reason;
})
)
);
}
candidates.push(timeout(this._timeout));
return pipe(
retry({delay: RETRY_DELAY}),
raceWith<T, never[]>(...candidates)
);
},
};
// Determines when the locator will timeout for actions.
get timeout(): number {
return this._timeout;
}
override on<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.on(eventName, handler);
}
override once<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.once(eventName, handler);
}
override off<K extends keyof LocatorEventObject>(
eventName: K,
handler: (event: LocatorEventObject[K]) => void
): this {
return super.off(eventName, handler);
}
setTimeout(timeout: number): Locator<T> {
const locator = this._clone();
locator._timeout = timeout;
return locator;
}
setVisibility<NodeType extends Node>(
this: Locator<NodeType>,
visibility: VisibilityOption
): Locator<NodeType> {
const locator = this._clone();
locator.visibility = visibility;
return locator;
}
setWaitForEnabled<NodeType extends Node>(
this: Locator<NodeType>,
value: boolean
): Locator<NodeType> {
const locator = this._clone();
locator.#waitForEnabled = value;
return locator;
}
setEnsureElementIsInTheViewport<ElementType extends Element>(
this: Locator<ElementType>,
value: boolean
): Locator<ElementType> {
const locator = this._clone();
locator.#ensureElementIsInTheViewport = value;
return locator;
}
setWaitForStableBoundingBox<ElementType extends Element>(
this: Locator<ElementType>,
value: boolean
): Locator<ElementType> {
const locator = this._clone();
locator.#waitForStableBoundingBox = value;
return locator;
}
/**
* @internal
*/
copyOptions<T>(locator: Locator<T>): this {
this._timeout = locator._timeout;
this.visibility = locator.visibility;
this.#waitForEnabled = locator.#waitForEnabled;
this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
this.#waitForStableBoundingBox = locator.#waitForStableBoundingBox;
return this;
}
/**
* If the element has a "disabled" property, wait for the element to be
* enabled.
*/
#waitForEnabledIfNeeded = <ElementType extends Node>(
handle: HandleFor<ElementType>,
signal?: AbortSignal
): Observable<never> => {
if (!this.#waitForEnabled) {
return EMPTY;
}
return from(
handle.frame.waitForFunction(
element => {
if (!(element instanceof HTMLElement)) {
return true;
}
const isNativeFormControl = [
'BUTTON',
'INPUT',
'SELECT',
'TEXTAREA',
'OPTION',
'OPTGROUP',
].includes(element.nodeName);
return !isNativeFormControl || !element.hasAttribute('disabled');
},
{
timeout: this._timeout,
signal,
},
handle
)
).pipe(ignoreElements());
};
/**
* Compares the bounding box of the element for two consecutive animation
* frames and waits till they are the same.
*/
#waitForStableBoundingBoxIfNeeded = <ElementType extends Element>(
handle: HandleFor<ElementType>
): Observable<never> => {
if (!this.#waitForStableBoundingBox) {
return EMPTY;
}
return defer(() => {
// Note we don't use waitForFunction because that relies on RAF.
return from(
handle.evaluate(element => {
return new Promise<[BoundingBox, BoundingBox]>(resolve => {
window.requestAnimationFrame(() => {
const rect1 = element.getBoundingClientRect();
window.requestAnimationFrame(() => {
const rect2 = element.getBoundingClientRect();
resolve([
{
x: rect1.x,
y: rect1.y,
width: rect1.width,
height: rect1.height,
},
{
x: rect2.x,
y: rect2.y,
width: rect2.width,
height: rect2.height,
},
]);
});
});
});
})
);
}).pipe(
first(([rect1, rect2]) => {
return (
rect1.x === rect2.x &&
rect1.y === rect2.y &&
rect1.width === rect2.width &&
rect1.height === rect2.height
);
}),
retry({delay: RETRY_DELAY}),
ignoreElements()
);
};
/**
* Checks if the element is in the viewport and auto-scrolls it if it is not.
*/
#ensureElementIsInTheViewportIfNeeded = <ElementType extends Element>(
handle: HandleFor<ElementType>
): Observable<never> => {
if (!this.#ensureElementIsInTheViewport) {
return EMPTY;
}
return from(handle.isIntersectingViewport({threshold: 0})).pipe(
filter(isIntersectingViewport => {
return !isIntersectingViewport;
}),
mergeMap(() => {
return from(handle.scrollIntoView());
}),
mergeMap(() => {
return defer(() => {
return from(handle.isIntersectingViewport({threshold: 0}));
}).pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
})
);
};
#click<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorClickOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
this.#waitForEnabledIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(handle.click(options)).pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
#fill<ElementType extends Element>(
this: Locator<ElementType>,
value: string,
options?: Readonly<ActionOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
this.#waitForEnabledIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(
(handle as unknown as ElementHandle<HTMLElement>).evaluate(el => {
if (el instanceof HTMLSelectElement) {
return 'select';
}
if (el instanceof HTMLTextAreaElement) {
return 'typeable-input';
}
if (el instanceof HTMLInputElement) {
if (
new Set([
'textarea',
'text',
'url',
'tel',
'search',
'password',
'number',
'email',
]).has(el.type)
) {
return 'typeable-input';
} else {
return 'other-input';
}
}
if (el.isContentEditable) {
return 'contenteditable';
}
return 'unknown';
})
)
.pipe(
mergeMap(inputType => {
switch (inputType) {
case 'select':
return from(handle.select(value).then(noop));
case 'contenteditable':
case 'typeable-input':
return from(
(
handle as unknown as ElementHandle<HTMLInputElement>
).evaluate((input, newValue) => {
const currentValue = input.isContentEditable
? input.innerText
: input.value;
// Clear the input if the current value does not match the filled
// out value.
if (
newValue.length <= currentValue.length ||
!newValue.startsWith(input.value)
) {
if (input.isContentEditable) {
input.innerText = '';
} else {
input.value = '';
}
return newValue;
}
const originalValue = input.isContentEditable
? input.innerText
: input.value;
// If the value is partially filled out, only type the rest. Move
// cursor to the end of the common prefix.
if (input.isContentEditable) {
input.innerText = '';
input.innerText = originalValue;
} else {
input.value = '';
input.value = originalValue;
}
return newValue.substring(originalValue.length);
}, value)
).pipe(
mergeMap(textToType => {
return from(handle.type(textToType));
})
);
case 'other-input':
return from(handle.focus()).pipe(
mergeMap(() => {
return from(
handle.evaluate((input, value) => {
(input as HTMLInputElement).value = value;
input.dispatchEvent(
new Event('input', {bubbles: true})
);
input.dispatchEvent(
new Event('change', {bubbles: true})
);
}, value)
);
})
);
case 'unknown':
throw new Error(`Element cannot be filled out.`);
}
})
)
.pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
#hover<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<ActionOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(handle.hover()).pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
#scroll<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorScrollOptions>
): Observable<void> {
const signal = options?.signal;
return this._wait(options).pipe(
this.operators.conditions(
[
this.#ensureElementIsInTheViewportIfNeeded,
this.#waitForStableBoundingBoxIfNeeded,
],
signal
),
tap(() => {
return this.emit(LocatorEmittedEvents.Action);
}),
mergeMap(handle => {
return from(
handle.evaluate(
(el, scrollTop, scrollLeft) => {
if (scrollTop !== undefined) {
el.scrollTop = scrollTop;
}
if (scrollLeft !== undefined) {
el.scrollLeft = scrollLeft;
}
},
options?.scrollTop,
options?.scrollLeft
)
).pipe(
catchError(err => {
void handle.dispose().catch(debugError);
throw err;
})
);
}),
this.operators.retryAndRaceWithSignalAndTimer(signal)
);
}
/**
* @internal
*/
abstract _clone(): Locator<T>;
/**
* @internal
*/
abstract _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>>;
/**
* Clones the locator.
*/
clone(): Locator<T> {
return this._clone();
}
/**
* Waits for the locator to get a handle from the page.
*
* @public
*/
async waitHandle(options?: Readonly<ActionOptions>): Promise<HandleFor<T>> {
return await firstValueFrom(
this._wait(options).pipe(
this.operators.retryAndRaceWithSignalAndTimer(options?.signal)
)
);
}
/**
* Waits for the locator to get the serialized value from the page.
*
* Note this requires the value to be JSON-serializable.
*
* @public
*/
async wait(options?: Readonly<ActionOptions>): Promise<T> {
using handle = await this.waitHandle(options);
return await handle.jsonValue();
}
/**
* Maps the locator using the provided mapper.
*
* @public
*/
map<To>(mapper: Mapper<T, To>): Locator<To> {
return new MappedLocator(this._clone(), handle => {
// SAFETY: TypeScript cannot deduce the type.
return (handle as any).evaluateHandle(mapper);
});
}
/**
* Creates an expectation that is evaluated against located values.
*
* If the expectations do not match, then the locator will retry.
*
* @public
*/
filter<S extends T>(predicate: Predicate<T, S>): Locator<S> {
return new FilteredLocator(this._clone(), async (handle, signal) => {
await (handle as ElementHandle<Node>).frame.waitForFunction(
predicate,
{signal, timeout: this._timeout},
handle
);
return true;
});
}
/**
* Creates an expectation that is evaluated against located handles.
*
* If the expectations do not match, then the locator will retry.
*
* @internal
*/
filterHandle<S extends T>(
predicate: Predicate<HandleFor<T>, HandleFor<S>>
): Locator<S> {
return new FilteredLocator(this._clone(), predicate);
}
/**
* Maps the locator using the provided mapper.
*
* @internal
*/
mapHandle<To>(mapper: HandleMapper<T, To>): Locator<To> {
return new MappedLocator(this._clone(), mapper);
}
click<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorClickOptions>
): Promise<void> {
return firstValueFrom(this.#click(options));
}
/**
* Fills out the input identified by the locator using the provided value. The
* type of the input is determined at runtime and the appropriate fill-out
* method is chosen based on the type. contenteditable, selector, inputs are
* supported.
*/
fill<ElementType extends Element>(
this: Locator<ElementType>,
value: string,
options?: Readonly<ActionOptions>
): Promise<void> {
return firstValueFrom(this.#fill(value, options));
}
hover<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<ActionOptions>
): Promise<void> {
return firstValueFrom(this.#hover(options));
}
scroll<ElementType extends Element>(
this: Locator<ElementType>,
options?: Readonly<LocatorScrollOptions>
): Promise<void> {
return firstValueFrom(this.#scroll(options));
}
}

Просмотреть файл

@ -0,0 +1,59 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Observable, from, mergeMap} from '../../../third_party/rxjs/rxjs.js';
import {Awaitable, HandleFor} from '../../common/common.js';
import {ActionOptions, DelegatedLocator, Locator} from './locators.js';
/**
* @public
*/
export type Mapper<From, To> = (value: From) => Awaitable<To>;
/**
* @internal
*/
export type HandleMapper<From, To> = (
value: HandleFor<From>,
signal?: AbortSignal
) => Awaitable<HandleFor<To>>;
/**
* @internal
*/
export class MappedLocator<From, To> extends DelegatedLocator<From, To> {
#mapper: HandleMapper<From, To>;
constructor(base: Locator<From>, mapper: HandleMapper<From, To>) {
super(base);
this.#mapper = mapper;
}
override _clone(): MappedLocator<From, To> {
return new MappedLocator(this.delegate.clone(), this.#mapper).copyOptions(
this
);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
return this.delegate._wait(options).pipe(
mergeMap(handle => {
return from(Promise.resolve(this.#mapper(handle, options?.signal)));
})
);
}
}

Просмотреть файл

@ -0,0 +1,117 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
EMPTY,
Observable,
defer,
filter,
first,
from,
identity,
ignoreElements,
retry,
throwIfEmpty,
} from '../../../third_party/rxjs/rxjs.js';
import {HandleFor, NodeFor} from '../../common/types.js';
import {Frame} from '../Frame.js';
import {Page} from '../Page.js';
import {ActionOptions, Locator, RETRY_DELAY} from './locators.js';
/**
* @internal
*/
export type Action<T, U> = (
element: HandleFor<T>,
signal?: AbortSignal
) => Observable<U>;
/**
* @internal
*/
export class NodeLocator<T extends Node> extends Locator<T> {
static create<Selector extends string>(
pageOrFrame: Page | Frame,
selector: Selector
): Locator<NodeFor<Selector>> {
return new NodeLocator<NodeFor<Selector>>(pageOrFrame, selector).setTimeout(
'getDefaultTimeout' in pageOrFrame
? pageOrFrame.getDefaultTimeout()
: pageOrFrame.page().getDefaultTimeout()
);
}
#pageOrFrame: Page | Frame;
#selector: string;
private constructor(pageOrFrame: Page | Frame, selector: string) {
super();
this.#pageOrFrame = pageOrFrame;
this.#selector = selector;
}
/**
* Waits for the element to become visible or hidden. visibility === 'visible'
* means that the element has a computed style, the visibility property other
* than 'hidden' or 'collapse' and non-empty bounding box. visibility ===
* 'hidden' means the opposite of that.
*/
#waitForVisibilityIfNeeded = (handle: HandleFor<T>): Observable<never> => {
if (!this.visibility) {
return EMPTY;
}
return (() => {
switch (this.visibility) {
case 'hidden':
return defer(() => {
return from(handle.isHidden());
});
case 'visible':
return defer(() => {
return from(handle.isVisible());
});
}
})().pipe(first(identity), retry({delay: RETRY_DELAY}), ignoreElements());
};
override _clone(): NodeLocator<T> {
return new NodeLocator<T>(this.#pageOrFrame, this.#selector).copyOptions(
this
);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
const signal = options?.signal;
return defer(() => {
return from(
this.#pageOrFrame.waitForSelector(this.#selector, {
visible: false,
timeout: this._timeout,
signal,
}) as Promise<HandleFor<T> | null>
);
}).pipe(
filter((value): value is NonNullable<typeof value> => {
return value !== null;
}),
throwIfEmpty(),
this.operators.conditions([this.#waitForVisibilityIfNeeded], signal)
);
}
}

Просмотреть файл

@ -0,0 +1,71 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Observable, race} from '../../../third_party/rxjs/rxjs.js';
import {HandleFor} from '../../puppeteer-core.js';
import {ActionOptions, Locator} from './locators.js';
/**
* @public
*/
export type AwaitedLocator<T> = T extends Locator<infer S> ? S : never;
function checkLocatorArray<T extends readonly unknown[] | []>(
locators: T
): ReadonlyArray<Locator<AwaitedLocator<T[number]>>> {
for (const locator of locators) {
if (!(locator instanceof Locator)) {
throw new Error('Unknown locator for race candidate');
}
}
return locators as ReadonlyArray<Locator<AwaitedLocator<T[number]>>>;
}
/**
* @internal
*/
export class RaceLocator<T> extends Locator<T> {
static create<T extends readonly unknown[]>(
locators: T
): Locator<AwaitedLocator<T[number]>> {
const array = checkLocatorArray(locators);
return new RaceLocator(array);
}
#locators: ReadonlyArray<Locator<T>>;
constructor(locators: ReadonlyArray<Locator<T>>) {
super();
this.#locators = locators;
}
override _clone(): RaceLocator<T> {
return new RaceLocator<T>(
this.#locators.map(locator => {
return locator.clone();
})
).copyOptions(this);
}
override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
return race(
...this.#locators.map(locator => {
return locator._wait(options);
})
);
}
}

Просмотреть файл

@ -0,0 +1,27 @@
/**
* Copyright 2023 Google Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Order of exports matters
* Don't sort
*/
export * from './Locator.js';
export * from './DelegatedLocator.js';
export * from './FilteredLocator.js';
export * from './FunctionLocator.js';
export * from './MappedLocator.js';
export * from './NodeLocator.js';
export * from './RaceLocator.js';

Просмотреть файл

@ -141,6 +141,13 @@ export class Accessibility {
this.#client = client;
}
/**
* @internal
*/
updateClient(client: CDPSession): void {
this.#client = client;
}
/**
* Captures the current state of the accessibility tree.
* The returned object represents the root accessible node of the page.

Просмотреть файл

@ -21,6 +21,7 @@ import {assert} from '../util/assert.js';
import {AsyncIterableUtil} from '../util/AsyncIterableUtil.js';
import {CDPSession} from './Connection.js';
import {IsolatedWorld} from './IsolatedWorld.js';
import {QueryHandler, QuerySelector} from './QueryHandler.js';
import {AwaitableIterable} from './types.js';
@ -98,21 +99,24 @@ export class ARIAQueryHandler extends QueryHandler {
selector,
{ariaQuerySelector}
) => {
return ariaQuerySelector(node, selector);
return await ariaQuerySelector(node, selector);
};
static override async *queryAll(
element: ElementHandle<Node>,
selector: string
): AwaitableIterable<ElementHandle<Node>> {
const context = element.executionContext();
const {name, role} = parseARIASelector(selector);
const results = await queryAXTree(context._client, element, name, role);
const world = context._world!;
const results = await queryAXTree(
element.realm.environment.client,
element,
name,
role
);
yield* AsyncIterableUtil.map(results, node => {
return world.adoptBackendNode(node.backendDOMNodeId) as Promise<
ElementHandle<Node>
>;
return (element.realm as IsolatedWorld).adoptBackendNode(
node.backendDOMNodeId
) as Promise<ElementHandle<Node>>;
});
}

Просмотреть файл

@ -32,11 +32,11 @@ export class Binding {
args: unknown[],
isTrivial: boolean
): Promise<void> {
const garbage = [];
const stack = new DisposableStack();
try {
if (!isTrivial) {
// Getting non-trivial arguments.
const handles = await context.evaluateHandle(
using handles = await context.evaluateHandle(
(name, seq) => {
// @ts-expect-error Code is evaluated in a different context.
return globalThis[name].args.get(seq);
@ -44,25 +44,21 @@ export class Binding {
this.#name,
id
);
try {
const properties = await handles.getProperties();
for (const [index, handle] of properties) {
// This is not straight-forward since some arguments can stringify, but
// aren't plain objects so add subtypes when the use-case arises.
if (index in args) {
switch (handle.remoteObject().subtype) {
case 'node':
args[+index] = handle;
break;
default:
garbage.push(handle.dispose());
}
} else {
garbage.push(handle.dispose());
const properties = await handles.getProperties();
for (const [index, handle] of properties) {
// This is not straight-forward since some arguments can stringify, but
// aren't plain objects so add subtypes when the use-case arises.
if (index in args) {
switch (handle.remoteObject().subtype) {
case 'node':
args[+index] = handle;
break;
default:
stack.use(handle);
}
} else {
stack.use(handle);
}
} finally {
await handles.dispose();
}
}
@ -80,7 +76,7 @@ export class Binding {
for (const arg of args) {
if (arg instanceof JSHandle) {
garbage.push(arg.dispose());
stack.use(arg);
}
}
} catch (error) {
@ -116,8 +112,6 @@ export class Binding {
)
.catch(debugError);
}
} finally {
await Promise.all(garbage);
}
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше