зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
9ed7cf9491
Коммит
7a9d33e4f7
|
@ -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
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче