chore: implement pick locator in trace viewer (#20965)
Fixes https://github.com/microsoft/playwright/issues/7853
This commit is contained in:
parent
d96d3c3381
commit
d7a0b3bb4e
98
package-lock.json
generated
98
package-lock.json
generated
|
|
@ -21,6 +21,7 @@
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.19.6",
|
"@babel/plugin-transform-modules-commonjs": "^7.19.6",
|
||||||
"@babel/plugin-transform-typescript": "^7.20.2",
|
"@babel/plugin-transform-typescript": "^7.20.2",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
|
"@types/babel__core": "^7.20.0",
|
||||||
"@types/codemirror": "^5.60.5",
|
"@types/codemirror": "^5.60.5",
|
||||||
"@types/formidable": "^2.0.4",
|
"@types/formidable": "^2.0.4",
|
||||||
"@types/node": "=14.18.34",
|
"@types/node": "=14.18.34",
|
||||||
|
|
@ -1335,6 +1336,47 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/babel__core": {
|
||||||
|
"version": "7.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz",
|
||||||
|
"integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.20.7",
|
||||||
|
"@babel/types": "^7.20.7",
|
||||||
|
"@types/babel__generator": "*",
|
||||||
|
"@types/babel__template": "*",
|
||||||
|
"@types/babel__traverse": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__generator": {
|
||||||
|
"version": "7.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
|
||||||
|
"integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__template": {
|
||||||
|
"version": "7.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
|
||||||
|
"integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/parser": "^7.1.0",
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/babel__traverse": {
|
||||||
|
"version": "7.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
|
||||||
|
"integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/types": "^7.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/codemirror": {
|
"node_modules/@types/codemirror": {
|
||||||
"version": "5.60.5",
|
"version": "5.60.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
||||||
|
|
@ -5939,6 +5981,9 @@
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"vite": "^4.1.1"
|
"vite": "^4.1.1"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
|
|
@ -5952,6 +5997,9 @@
|
||||||
"vite": "^4.1.1",
|
"vite": "^4.1.1",
|
||||||
"vite-plugin-solid": "^2.5.0"
|
"vite-plugin-solid": "^2.5.0"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"solid-js": "^1.6.10"
|
"solid-js": "^1.6.10"
|
||||||
},
|
},
|
||||||
|
|
@ -5968,6 +6016,9 @@
|
||||||
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
"@sveltejs/vite-plugin-svelte": "^2.0.2",
|
||||||
"vite": "^4.1.1"
|
"vite": "^4.1.1"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"svelte": "^3.55.1"
|
"svelte": "^3.55.1"
|
||||||
},
|
},
|
||||||
|
|
@ -6004,6 +6055,9 @@
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
"vite": "^4.1.1"
|
"vite": "^4.1.1"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
|
|
@ -6053,6 +6107,9 @@
|
||||||
"@vitejs/plugin-vue2": "^2.2.0",
|
"@vitejs/plugin-vue2": "^2.2.0",
|
||||||
"vite": "^4.1.1"
|
"vite": "^4.1.1"
|
||||||
},
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vue": "^2.7.14"
|
"vue": "^2.7.14"
|
||||||
},
|
},
|
||||||
|
|
@ -6942,6 +6999,47 @@
|
||||||
"defer-to-connect": "^1.0.1"
|
"defer-to-connect": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/babel__core": {
|
||||||
|
"version": "7.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.0.tgz",
|
||||||
|
"integrity": "sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/parser": "^7.20.7",
|
||||||
|
"@babel/types": "^7.20.7",
|
||||||
|
"@types/babel__generator": "*",
|
||||||
|
"@types/babel__template": "*",
|
||||||
|
"@types/babel__traverse": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/babel__generator": {
|
||||||
|
"version": "7.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz",
|
||||||
|
"integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/babel__template": {
|
||||||
|
"version": "7.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz",
|
||||||
|
"integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/parser": "^7.1.0",
|
||||||
|
"@babel/types": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/babel__traverse": {
|
||||||
|
"version": "7.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz",
|
||||||
|
"integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@babel/types": "^7.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/codemirror": {
|
"@types/codemirror": {
|
||||||
"version": "5.60.5",
|
"version": "5.60.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
"clean": "node utils/build/clean.js",
|
"clean": "node utils/build/clean.js",
|
||||||
"build": "node utils/build/build.js",
|
"build": "node utils/build/build.js",
|
||||||
"watch": "node utils/build/build.js --watch --lint",
|
"watch": "node utils/build/build.js --watch --lint",
|
||||||
"test-types": "node utils/generate_types/ && npx -p typescript@3.7.5 tsc -p utils/generate_types/test/tsconfig.json && tsc -p ./tests/",
|
"test-types": "node utils/generate_types/ && tsc -p utils/generate_types/test/tsconfig.json && tsc -p ./tests/",
|
||||||
"roll": "node utils/roll_browser.js",
|
"roll": "node utils/roll_browser.js",
|
||||||
"check-deps": "node utils/check_deps.js",
|
"check-deps": "node utils/check_deps.js",
|
||||||
"build-android-driver": "./utils/build_android_driver.sh",
|
"build-android-driver": "./utils/build_android_driver.sh",
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.19.6",
|
"@babel/plugin-transform-modules-commonjs": "^7.19.6",
|
||||||
"@babel/plugin-transform-typescript": "^7.20.2",
|
"@babel/plugin-transform-typescript": "^7.20.2",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
|
"@types/babel__core": "^7.20.0",
|
||||||
"@types/codemirror": "^5.60.5",
|
"@types/codemirror": "^5.60.5",
|
||||||
"@types/formidable": "^2.0.4",
|
"@types/formidable": "^2.0.4",
|
||||||
"@types/node": "=14.18.34",
|
"@types/node": "=14.18.34",
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
const module = {};
|
const module = {};
|
||||||
${injectedScriptSource.source}
|
${injectedScriptSource.source}
|
||||||
return new module.exports(
|
return new module.exports(
|
||||||
|
globalThis,
|
||||||
${isUnderTest()},
|
${isUnderTest()},
|
||||||
"${sdkLanguage}",
|
"${sdkLanguage}",
|
||||||
${JSON.stringify(this.frame._page.selectors.testIdAttributeName())},
|
${JSON.stringify(this.frame._page.selectors.testIdAttributeName())},
|
||||||
|
|
|
||||||
10
packages/playwright-core/src/server/injected/.eslintrc.js
Normal file
10
packages/playwright-core/src/server/injected/.eslintrc.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
module.exports = {
|
||||||
|
rules: {
|
||||||
|
"no-restricted-globals": [
|
||||||
|
"error",
|
||||||
|
{ "name": "window" },
|
||||||
|
{ "name": "document" },
|
||||||
|
{ "name": "globalThis" },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -38,8 +38,8 @@ class Locator {
|
||||||
selector += ` >> internal:has=` + JSON.stringify((options.has as any)[selectorSymbol]);
|
selector += ` >> internal:has=` + JSON.stringify((options.has as any)[selectorSymbol]);
|
||||||
if (selector) {
|
if (selector) {
|
||||||
const parsed = injectedScript.parseSelector(selector);
|
const parsed = injectedScript.parseSelector(selector);
|
||||||
this.element = injectedScript.querySelector(parsed, document, false);
|
this.element = injectedScript.querySelector(parsed, injectedScript.document, false);
|
||||||
this.elements = injectedScript.querySelectorAll(parsed, document);
|
this.elements = injectedScript.querySelectorAll(parsed, injectedScript.document);
|
||||||
}
|
}
|
||||||
const selectorBase = selector;
|
const selectorBase = selector;
|
||||||
const self = this as any;
|
const self = this as any;
|
||||||
|
|
@ -73,9 +73,9 @@ class ConsoleAPI {
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
if (window.playwright)
|
if (this._injectedScript.window.playwright)
|
||||||
return;
|
return;
|
||||||
window.playwright = {
|
this._injectedScript.window.playwright = {
|
||||||
$: (selector: string, strict?: boolean) => this._querySelector(selector, !!strict),
|
$: (selector: string, strict?: boolean) => this._querySelector(selector, !!strict),
|
||||||
$$: (selector: string) => this._querySelectorAll(selector),
|
$$: (selector: string) => this._querySelectorAll(selector),
|
||||||
inspect: (selector: string) => this._inspect(selector),
|
inspect: (selector: string) => this._inspect(selector),
|
||||||
|
|
@ -84,30 +84,30 @@ class ConsoleAPI {
|
||||||
resume: () => this._resume(),
|
resume: () => this._resume(),
|
||||||
...new Locator(injectedScript, ''),
|
...new Locator(injectedScript, ''),
|
||||||
};
|
};
|
||||||
delete window.playwright.filter;
|
delete this._injectedScript.window.playwright.filter;
|
||||||
delete window.playwright.first;
|
delete this._injectedScript.window.playwright.first;
|
||||||
delete window.playwright.last;
|
delete this._injectedScript.window.playwright.last;
|
||||||
delete window.playwright.nth;
|
delete this._injectedScript.window.playwright.nth;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _querySelector(selector: string, strict: boolean): (Element | undefined) {
|
private _querySelector(selector: string, strict: boolean): (Element | undefined) {
|
||||||
if (typeof selector !== 'string')
|
if (typeof selector !== 'string')
|
||||||
throw new Error(`Usage: playwright.query('Playwright >> selector').`);
|
throw new Error(`Usage: playwright.query('Playwright >> selector').`);
|
||||||
const parsed = this._injectedScript.parseSelector(selector);
|
const parsed = this._injectedScript.parseSelector(selector);
|
||||||
return this._injectedScript.querySelector(parsed, document, strict);
|
return this._injectedScript.querySelector(parsed, this._injectedScript.document, strict);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _querySelectorAll(selector: string): Element[] {
|
private _querySelectorAll(selector: string): Element[] {
|
||||||
if (typeof selector !== 'string')
|
if (typeof selector !== 'string')
|
||||||
throw new Error(`Usage: playwright.$$('Playwright >> selector').`);
|
throw new Error(`Usage: playwright.$$('Playwright >> selector').`);
|
||||||
const parsed = this._injectedScript.parseSelector(selector);
|
const parsed = this._injectedScript.parseSelector(selector);
|
||||||
return this._injectedScript.querySelectorAll(parsed, document);
|
return this._injectedScript.querySelectorAll(parsed, this._injectedScript.document);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _inspect(selector: string) {
|
private _inspect(selector: string) {
|
||||||
if (typeof selector !== 'string')
|
if (typeof selector !== 'string')
|
||||||
throw new Error(`Usage: playwright.inspect('Playwright >> selector').`);
|
throw new Error(`Usage: playwright.inspect('Playwright >> selector').`);
|
||||||
window.inspect(this._querySelector(selector, false));
|
this._injectedScript.window.inspect(this._querySelector(selector, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _selector(element: Element) {
|
private _selector(element: Element) {
|
||||||
|
|
@ -124,7 +124,7 @@ class ConsoleAPI {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _resume() {
|
private _resume() {
|
||||||
window.__pw_resume().catch(() => {});
|
this._injectedScript.window.__pw_resume().catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ export function isElementVisible(element: Element): boolean {
|
||||||
|
|
||||||
function isVisibleTextNode(node: Text) {
|
function isVisibleTextNode(node: Text) {
|
||||||
// https://stackoverflow.com/questions/1461059/is-there-an-equivalent-to-getboundingclientrect-for-text-nodes
|
// https://stackoverflow.com/questions/1461059/is-there-an-equivalent-to-getboundingclientrect-for-text-nodes
|
||||||
const range = document.createRange();
|
const range = node.ownerDocument.createRange();
|
||||||
range.selectNode(node);
|
range.selectNode(node);
|
||||||
const rect = range.getBoundingClientRect();
|
const rect = range.getBoundingClientRect();
|
||||||
return rect.width > 0 && rect.height > 0;
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export class Highlight {
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
|
const document = injectedScript.document;
|
||||||
this._isUnderTest = injectedScript.isUnderTest;
|
this._isUnderTest = injectedScript.isUnderTest;
|
||||||
this._glassPaneElement = document.createElement('x-pw-glass');
|
this._glassPaneElement = document.createElement('x-pw-glass');
|
||||||
this._glassPaneElement.style.position = 'fixed';
|
this._glassPaneElement.style.position = 'fixed';
|
||||||
|
|
@ -100,7 +101,7 @@ export class Highlight {
|
||||||
}
|
}
|
||||||
|
|
||||||
install() {
|
install() {
|
||||||
document.documentElement.appendChild(this._glassPaneElement);
|
this._injectedScript.document.documentElement.appendChild(this._glassPaneElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
setLanguage(language: Language) {
|
setLanguage(language: Language) {
|
||||||
|
|
@ -110,7 +111,7 @@ export class Highlight {
|
||||||
runHighlightOnRaf(selector: ParsedSelector) {
|
runHighlightOnRaf(selector: ParsedSelector) {
|
||||||
if (this._rafRequest)
|
if (this._rafRequest)
|
||||||
cancelAnimationFrame(this._rafRequest);
|
cancelAnimationFrame(this._rafRequest);
|
||||||
this.updateHighlight(this._injectedScript.querySelectorAll(selector, document.documentElement), stringifySelector(selector), false);
|
this.updateHighlight(this._injectedScript.querySelectorAll(selector, this._injectedScript.document.documentElement), stringifySelector(selector), false);
|
||||||
this._rafRequest = requestAnimationFrame(() => this.runHighlightOnRaf(selector));
|
this._rafRequest = requestAnimationFrame(() => this.runHighlightOnRaf(selector));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,7 +122,7 @@ export class Highlight {
|
||||||
}
|
}
|
||||||
|
|
||||||
isInstalled(): boolean {
|
isInstalled(): boolean {
|
||||||
return this._glassPaneElement.parentElement === document.documentElement && !this._glassPaneElement.nextElementSibling;
|
return this._glassPaneElement.parentElement === this._injectedScript.document.documentElement && !this._glassPaneElement.nextElementSibling;
|
||||||
}
|
}
|
||||||
|
|
||||||
showActionPoint(x: number, y: number) {
|
showActionPoint(x: number, y: number) {
|
||||||
|
|
@ -173,7 +174,7 @@ export class Highlight {
|
||||||
|
|
||||||
let tooltipElement;
|
let tooltipElement;
|
||||||
if (options.tooltipText) {
|
if (options.tooltipText) {
|
||||||
tooltipElement = document.createElement('x-pw-tooltip');
|
tooltipElement = this._injectedScript.document.createElement('x-pw-tooltip');
|
||||||
this._glassPaneShadow.appendChild(tooltipElement);
|
this._glassPaneShadow.appendChild(tooltipElement);
|
||||||
const suffix = elements.length > 1 ? ` [${i + 1} of ${elements.length}]` : '';
|
const suffix = elements.length > 1 ? ` [${i + 1} of ${elements.length}]` : '';
|
||||||
tooltipElement.textContent = options.tooltipText + suffix;
|
tooltipElement.textContent = options.tooltipText + suffix;
|
||||||
|
|
@ -252,7 +253,7 @@ export class Highlight {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createHighlightElement(): HTMLElement {
|
private _createHighlightElement(): HTMLElement {
|
||||||
const highlightElement = document.createElement('x-pw-highlight');
|
const highlightElement = this._injectedScript.document.createElement('x-pw-highlight');
|
||||||
highlightElement.style.position = 'absolute';
|
highlightElement.style.position = 'absolute';
|
||||||
highlightElement.style.top = '0';
|
highlightElement.style.top = '0';
|
||||||
highlightElement.style.left = '0';
|
highlightElement.style.left = '0';
|
||||||
|
|
|
||||||
|
|
@ -79,8 +79,14 @@ export class InjectedScript {
|
||||||
private _sdkLanguage: Language;
|
private _sdkLanguage: Language;
|
||||||
private _testIdAttributeNameForStrictErrorAndConsoleCodegen: string = 'data-testid';
|
private _testIdAttributeNameForStrictErrorAndConsoleCodegen: string = 'data-testid';
|
||||||
private _markedTargetElements = new Set<Element>();
|
private _markedTargetElements = new Set<Element>();
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
readonly window: Window & typeof globalThis;
|
||||||
|
readonly document: Document;
|
||||||
|
|
||||||
constructor(isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) {
|
// eslint-disable-next-line no-restricted-globals
|
||||||
|
constructor(window: Window & typeof globalThis, isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) {
|
||||||
|
this.window = window;
|
||||||
|
this.document = window.document;
|
||||||
this.isUnderTest = isUnderTest;
|
this.isUnderTest = isUnderTest;
|
||||||
this._sdkLanguage = sdkLanguage;
|
this._sdkLanguage = sdkLanguage;
|
||||||
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen;
|
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen;
|
||||||
|
|
@ -124,11 +130,11 @@ export class InjectedScript {
|
||||||
this._setupHitTargetInterceptors();
|
this._setupHitTargetInterceptors();
|
||||||
|
|
||||||
if (isUnderTest)
|
if (isUnderTest)
|
||||||
(window as any).__injectedScript = this;
|
(this.window as any).__injectedScript = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
eval(expression: string): any {
|
eval(expression: string): any {
|
||||||
return globalThis.eval(expression);
|
return this.window.eval(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
testIdAttributeNameForStrictErrorAndConsoleCodegen(): string {
|
testIdAttributeNameForStrictErrorAndConsoleCodegen(): string {
|
||||||
|
|
@ -370,7 +376,7 @@ export class InjectedScript {
|
||||||
}
|
}
|
||||||
|
|
||||||
extend(source: string, params: any): any {
|
extend(source: string, params: any): any {
|
||||||
const constrFunction = globalThis.eval(`
|
const constrFunction = this.window.eval(`
|
||||||
(() => {
|
(() => {
|
||||||
const module = {};
|
const module = {};
|
||||||
${source}
|
${source}
|
||||||
|
|
@ -827,7 +833,7 @@ export class InjectedScript {
|
||||||
const elements: Element[] = root.elementsFromPoint(hitPoint.x, hitPoint.y);
|
const elements: Element[] = root.elementsFromPoint(hitPoint.x, hitPoint.y);
|
||||||
const singleElement = root.elementFromPoint(hitPoint.x, hitPoint.y);
|
const singleElement = root.elementFromPoint(hitPoint.x, hitPoint.y);
|
||||||
if (singleElement && elements[0] && parentElementOrShadowHost(singleElement) === elements[0]) {
|
if (singleElement && elements[0] && parentElementOrShadowHost(singleElement) === elements[0]) {
|
||||||
const style = document.defaultView?.getComputedStyle(singleElement);
|
const style = this.window.getComputedStyle(singleElement);
|
||||||
if (style?.display === 'contents') {
|
if (style?.display === 'contents') {
|
||||||
// Workaround a case where elementsFromPoint misses the inner-most element with display:contents.
|
// Workaround a case where elementsFromPoint misses the inner-most element with display:contents.
|
||||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=1342092
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=1342092
|
||||||
|
|
@ -851,7 +857,7 @@ export class InjectedScript {
|
||||||
if (hitElement === targetElement)
|
if (hitElement === targetElement)
|
||||||
return 'done';
|
return 'done';
|
||||||
|
|
||||||
const hitTargetDescription = this.previewNode(hitParents[0] || document.documentElement);
|
const hitTargetDescription = this.previewNode(hitParents[0] || this.document.documentElement);
|
||||||
// Root is the topmost element in the hitTarget's chain that is not in the
|
// Root is the topmost element in the hitTarget's chain that is not in the
|
||||||
// element's chain. For example, it might be a dialog element that overlays
|
// element's chain. For example, it might be a dialog element that overlays
|
||||||
// the target.
|
// the target.
|
||||||
|
|
@ -939,7 +945,7 @@ export class InjectedScript {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Determine the event point. Note that Firefox does not always have window.TouchEvent.
|
// Determine the event point. Note that Firefox does not always have window.TouchEvent.
|
||||||
const point = (!!window.TouchEvent && (event instanceof window.TouchEvent)) ? event.touches[0] : (event as MouseEvent | PointerEvent);
|
const point = (!!this.window.TouchEvent && (event instanceof this.window.TouchEvent)) ? event.touches[0] : (event as MouseEvent | PointerEvent);
|
||||||
|
|
||||||
// Check that we hit the right element at the first event, and assume all
|
// Check that we hit the right element at the first event, and assume all
|
||||||
// subsequent events will be fine.
|
// subsequent events will be fine.
|
||||||
|
|
@ -1053,7 +1059,7 @@ export class InjectedScript {
|
||||||
this._highlight.install();
|
this._highlight.install();
|
||||||
const elements = [];
|
const elements = [];
|
||||||
for (const selector of selectors)
|
for (const selector of selectors)
|
||||||
elements.push(this.querySelectorAll(selector, document.documentElement));
|
elements.push(this.querySelectorAll(selector, this.document.documentElement));
|
||||||
this._highlight.maskElements(elements.flat());
|
this._highlight.maskElements(elements.flat());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1089,31 +1095,31 @@ export class InjectedScript {
|
||||||
|
|
||||||
let seenEvent = false;
|
let seenEvent = false;
|
||||||
const handleCustomEvent = () => seenEvent = true;
|
const handleCustomEvent = () => seenEvent = true;
|
||||||
window.addEventListener(customEventName, handleCustomEvent);
|
this.window.addEventListener(customEventName, handleCustomEvent);
|
||||||
|
|
||||||
new MutationObserver(entries => {
|
new MutationObserver(entries => {
|
||||||
const newDocumentElement = entries.some(entry => Array.from(entry.addedNodes).includes(document.documentElement));
|
const newDocumentElement = entries.some(entry => Array.from(entry.addedNodes).includes(this.document.documentElement));
|
||||||
if (!newDocumentElement)
|
if (!newDocumentElement)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// New documentElement - let's check whether listeners are still here.
|
// New documentElement - let's check whether listeners are still here.
|
||||||
seenEvent = false;
|
seenEvent = false;
|
||||||
window.dispatchEvent(new CustomEvent(customEventName));
|
this.window.dispatchEvent(new CustomEvent(customEventName));
|
||||||
if (seenEvent)
|
if (seenEvent)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Listener did not fire. Reattach the listener and notify.
|
// Listener did not fire. Reattach the listener and notify.
|
||||||
window.addEventListener(customEventName, handleCustomEvent);
|
this.window.addEventListener(customEventName, handleCustomEvent);
|
||||||
for (const callback of this.onGlobalListenersRemoved)
|
for (const callback of this.onGlobalListenersRemoved)
|
||||||
callback();
|
callback();
|
||||||
}).observe(document, { childList: true });
|
}).observe(this.document, { childList: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setupHitTargetInterceptors() {
|
private _setupHitTargetInterceptors() {
|
||||||
const listener = (event: PointerEvent | MouseEvent | TouchEvent) => this._hitTargetInterceptor?.(event);
|
const listener = (event: PointerEvent | MouseEvent | TouchEvent) => this._hitTargetInterceptor?.(event);
|
||||||
const addHitTargetInterceptorListeners = () => {
|
const addHitTargetInterceptorListeners = () => {
|
||||||
for (const event of kAllHitTargetInterceptorEvents)
|
for (const event of kAllHitTargetInterceptorEvents)
|
||||||
window.addEventListener(event as any, listener, { capture: true, passive: false });
|
this.window.addEventListener(event as any, listener, { capture: true, passive: false });
|
||||||
};
|
};
|
||||||
addHitTargetInterceptorListeners();
|
addHitTargetInterceptorListeners();
|
||||||
this.onGlobalListenersRemoved.add(addHitTargetInterceptorListeners);
|
this.onGlobalListenersRemoved.add(addHitTargetInterceptorListeners);
|
||||||
|
|
@ -1220,15 +1226,15 @@ export class InjectedScript {
|
||||||
} else if (expression === 'to.have.class') {
|
} else if (expression === 'to.have.class') {
|
||||||
received = element.classList.toString();
|
received = element.classList.toString();
|
||||||
} else if (expression === 'to.have.css') {
|
} else if (expression === 'to.have.css') {
|
||||||
received = window.getComputedStyle(element).getPropertyValue(options.expressionArg);
|
received = this.window.getComputedStyle(element).getPropertyValue(options.expressionArg);
|
||||||
} else if (expression === 'to.have.id') {
|
} else if (expression === 'to.have.id') {
|
||||||
received = element.id;
|
received = element.id;
|
||||||
} else if (expression === 'to.have.text') {
|
} else if (expression === 'to.have.text') {
|
||||||
received = options.useInnerText ? (element as HTMLElement).innerText : elementText(new Map(), element).full;
|
received = options.useInnerText ? (element as HTMLElement).innerText : elementText(new Map(), element).full;
|
||||||
} else if (expression === 'to.have.title') {
|
} else if (expression === 'to.have.title') {
|
||||||
received = document.title;
|
received = this.document.title;
|
||||||
} else if (expression === 'to.have.url') {
|
} else if (expression === 'to.have.url') {
|
||||||
received = document.location.href;
|
received = this.document.location.href;
|
||||||
} else if (expression === 'to.have.value') {
|
} else if (expression === 'to.have.value') {
|
||||||
element = this.retarget(element, 'follow-label')!;
|
element = this.retarget(element, 'follow-label')!;
|
||||||
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
|
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT')
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,7 @@ function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: Componen
|
||||||
}
|
}
|
||||||
|
|
||||||
function findReactRoots(root: Document | ShadowRoot, roots: ReactVNode[] = []): ReactVNode[] {
|
function findReactRoots(root: Document | ShadowRoot, roots: ReactVNode[] = []): ReactVNode[] {
|
||||||
|
const document = root.ownerDocument || root;
|
||||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||||
do {
|
do {
|
||||||
const node = walker.currentNode;
|
const node = walker.currentNode;
|
||||||
|
|
@ -179,7 +180,7 @@ export const ReactEngine: SelectorEngine = {
|
||||||
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
||||||
const { name, attributes } = parseAttributeSelector(selector, false);
|
const { name, attributes } = parseAttributeSelector(selector, false);
|
||||||
|
|
||||||
const reactRoots = findReactRoots(document);
|
const reactRoots = findReactRoots(scope.ownerDocument || scope);
|
||||||
const trees = reactRoots.map(reactRoot => buildComponentsTree(reactRoot));
|
const trees = reactRoots.map(reactRoot => buildComponentsTree(reactRoot));
|
||||||
const treeNodes = trees.map(tree => filterComponentsTree(tree, treeNode => {
|
const treeNodes = trees.map(tree => filterComponentsTree(tree, treeNode => {
|
||||||
const props = treeNode.props ?? {};
|
const props = treeNode.props ?? {};
|
||||||
|
|
|
||||||
|
|
@ -21,16 +21,13 @@ import type { Point } from '../../common/types';
|
||||||
import type { UIState } from '@recorder/recorderTypes';
|
import type { UIState } from '@recorder/recorderTypes';
|
||||||
import { Highlight } from '../injected/highlight';
|
import { Highlight } from '../injected/highlight';
|
||||||
|
|
||||||
|
interface RecorderDelegate {
|
||||||
declare module globalThis {
|
performAction?(action: actions.Action): Promise<void>;
|
||||||
let __pw_recorderPerformAction: (action: actions.Action) => Promise<void>;
|
recordAction?(action: actions.Action): Promise<void>;
|
||||||
let __pw_recorderRecordAction: (action: actions.Action) => Promise<void>;
|
setSelector?(selector: string): Promise<void>;
|
||||||
let __pw_recorderState: () => Promise<UIState>;
|
|
||||||
let __pw_recorderSetSelector: (selector: string) => Promise<void>;
|
|
||||||
let __pw_refreshOverlay: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Recorder {
|
export class Recorder {
|
||||||
private _injectedScript: InjectedScript;
|
private _injectedScript: InjectedScript;
|
||||||
private _performingAction = false;
|
private _performingAction = false;
|
||||||
private _listeners: (() => void)[] = [];
|
private _listeners: (() => void)[] = [];
|
||||||
|
|
@ -38,45 +35,43 @@ class Recorder {
|
||||||
private _hoveredElement: HTMLElement | null = null;
|
private _hoveredElement: HTMLElement | null = null;
|
||||||
private _activeModel: HighlightModel | null = null;
|
private _activeModel: HighlightModel | null = null;
|
||||||
private _expectProgrammaticKeyUp = false;
|
private _expectProgrammaticKeyUp = false;
|
||||||
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
|
||||||
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
|
private _mode: 'none' | 'inspecting' | 'recording' = 'none';
|
||||||
private _actionPoint: Point | undefined;
|
private _actionPoint: Point | undefined;
|
||||||
private _actionSelector: string | undefined;
|
private _actionSelector: string | undefined;
|
||||||
private _highlight: Highlight;
|
private _highlight: Highlight;
|
||||||
private _testIdAttributeName: string = 'data-testid';
|
private _testIdAttributeName: string = 'data-testid';
|
||||||
|
readonly document: Document;
|
||||||
|
private _delegate: RecorderDelegate;
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript, delegate: RecorderDelegate) {
|
||||||
|
this.document = injectedScript.document;
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
|
this._delegate = delegate;
|
||||||
this._highlight = new Highlight(injectedScript);
|
this._highlight = new Highlight(injectedScript);
|
||||||
|
|
||||||
this._refreshListenersIfNeeded();
|
this.refreshListenersIfNeeded();
|
||||||
injectedScript.onGlobalListenersRemoved.add(() => this._refreshListenersIfNeeded());
|
|
||||||
|
|
||||||
globalThis.__pw_refreshOverlay = () => {
|
|
||||||
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
|
|
||||||
};
|
|
||||||
globalThis.__pw_refreshOverlay();
|
|
||||||
if (injectedScript.isUnderTest)
|
if (injectedScript.isUnderTest)
|
||||||
console.error('Recorder script ready for test'); // eslint-disable-line no-console
|
console.error('Recorder script ready for test'); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshListenersIfNeeded() {
|
refreshListenersIfNeeded() {
|
||||||
// Ensure we are attached to the current document, and we are on top (last element);
|
// Ensure we are attached to the current document, and we are on top (last element);
|
||||||
if (this._highlight.isInstalled())
|
if (this._highlight.isInstalled())
|
||||||
return;
|
return;
|
||||||
removeEventListeners(this._listeners);
|
removeEventListeners(this._listeners);
|
||||||
this._listeners = [
|
this._listeners = [
|
||||||
addEventListener(document, 'click', event => this._onClick(event as MouseEvent), true),
|
addEventListener(this.document, 'click', event => this._onClick(event as MouseEvent), true),
|
||||||
addEventListener(document, 'auxclick', event => this._onClick(event as MouseEvent), true),
|
addEventListener(this.document, 'auxclick', event => this._onClick(event as MouseEvent), true),
|
||||||
addEventListener(document, 'input', event => this._onInput(event), true),
|
addEventListener(this.document, 'input', event => this._onInput(event), true),
|
||||||
addEventListener(document, 'keydown', event => this._onKeyDown(event as KeyboardEvent), true),
|
addEventListener(this.document, 'keydown', event => this._onKeyDown(event as KeyboardEvent), true),
|
||||||
addEventListener(document, 'keyup', event => this._onKeyUp(event as KeyboardEvent), true),
|
addEventListener(this.document, 'keyup', event => this._onKeyUp(event as KeyboardEvent), true),
|
||||||
addEventListener(document, 'mousedown', event => this._onMouseDown(event as MouseEvent), true),
|
addEventListener(this.document, 'mousedown', event => this._onMouseDown(event as MouseEvent), true),
|
||||||
addEventListener(document, 'mouseup', event => this._onMouseUp(event as MouseEvent), true),
|
addEventListener(this.document, 'mouseup', event => this._onMouseUp(event as MouseEvent), true),
|
||||||
addEventListener(document, 'mousemove', event => this._onMouseMove(event as MouseEvent), true),
|
addEventListener(this.document, 'mousemove', event => this._onMouseMove(event as MouseEvent), true),
|
||||||
addEventListener(document, 'mouseleave', event => this._onMouseLeave(event as MouseEvent), true),
|
addEventListener(this.document, 'mouseleave', event => this._onMouseLeave(event as MouseEvent), true),
|
||||||
addEventListener(document, 'focus', event => event.isTrusted && this._onFocus(true), true),
|
addEventListener(this.document, 'focus', event => event.isTrusted && this._onFocus(true), true),
|
||||||
addEventListener(document, 'scroll', event => {
|
addEventListener(this.document, 'scroll', event => {
|
||||||
if (!event.isTrusted)
|
if (!event.isTrusted)
|
||||||
return;
|
return;
|
||||||
this._hoveredModel = null;
|
this._hoveredModel = null;
|
||||||
|
|
@ -87,16 +82,7 @@ class Recorder {
|
||||||
this._highlight.install();
|
this._highlight.install();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _pollRecorderMode() {
|
setUIState(state: UIState) {
|
||||||
const pollPeriod = 1000;
|
|
||||||
if (this._pollRecorderModeTimer)
|
|
||||||
clearTimeout(this._pollRecorderModeTimer);
|
|
||||||
const state = await globalThis.__pw_recorderState().catch(e => null);
|
|
||||||
if (!state) {
|
|
||||||
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { mode, actionPoint, actionSelector, language, testIdAttributeName } = state;
|
const { mode, actionPoint, actionSelector, language, testIdAttributeName } = state;
|
||||||
this._testIdAttributeName = testIdAttributeName;
|
this._testIdAttributeName = testIdAttributeName;
|
||||||
this._highlight.setLanguage(language);
|
this._highlight.setLanguage(language);
|
||||||
|
|
@ -121,11 +107,10 @@ class Recorder {
|
||||||
this._actionSelector = undefined;
|
this._actionSelector = undefined;
|
||||||
|
|
||||||
if (actionSelector !== this._actionSelector) {
|
if (actionSelector !== this._actionSelector) {
|
||||||
this._hoveredModel = actionSelector ? querySelector(this._injectedScript, actionSelector, document) : null;
|
this._hoveredModel = actionSelector ? querySelector(this._injectedScript, actionSelector, this.document) : null;
|
||||||
this._updateHighlight();
|
this._updateHighlight();
|
||||||
this._actionSelector = actionSelector;
|
this._actionSelector = actionSelector;
|
||||||
}
|
}
|
||||||
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _clearHighlight() {
|
private _clearHighlight() {
|
||||||
|
|
@ -161,7 +146,7 @@ class Recorder {
|
||||||
if (!event.isTrusted)
|
if (!event.isTrusted)
|
||||||
return;
|
return;
|
||||||
if (this._mode === 'inspecting')
|
if (this._mode === 'inspecting')
|
||||||
globalThis.__pw_recorderSetSelector(this._hoveredModel ? this._hoveredModel.selector : '');
|
this._delegate.setSelector?.(this._hoveredModel ? this._hoveredModel.selector : '');
|
||||||
if (this._shouldIgnoreMouseEvent(event))
|
if (this._shouldIgnoreMouseEvent(event))
|
||||||
return;
|
return;
|
||||||
if (this._actionInProgress(event))
|
if (this._actionInProgress(event))
|
||||||
|
|
@ -242,7 +227,7 @@ class Recorder {
|
||||||
if (!event.isTrusted)
|
if (!event.isTrusted)
|
||||||
return;
|
return;
|
||||||
// Leaving iframe.
|
// Leaving iframe.
|
||||||
if (window.top !== window && this._deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
if (this._injectedScript.window.top !== this._injectedScript.window && this._deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
|
||||||
this._hoveredElement = null;
|
this._hoveredElement = null;
|
||||||
this._updateModelForHoveredElement();
|
this._updateModelForHoveredElement();
|
||||||
}
|
}
|
||||||
|
|
@ -251,10 +236,10 @@ class Recorder {
|
||||||
private _onFocus(userGesture: boolean) {
|
private _onFocus(userGesture: boolean) {
|
||||||
if (this._mode === 'none')
|
if (this._mode === 'none')
|
||||||
return;
|
return;
|
||||||
const activeElement = this._deepActiveElement(document);
|
const activeElement = this._deepActiveElement(this.document);
|
||||||
// Firefox dispatches "focus" event to body when clicking on a backgrounded headed browser window.
|
// Firefox dispatches "focus" event to body when clicking on a backgrounded headed browser window.
|
||||||
// We'd like to ignore this stray event.
|
// We'd like to ignore this stray event.
|
||||||
if (activeElement === document.body)
|
if (activeElement === this.document.body)
|
||||||
return;
|
return;
|
||||||
const result = activeElement ? generateSelector(this._injectedScript, activeElement, this._testIdAttributeName) : null;
|
const result = activeElement ? generateSelector(this._injectedScript, activeElement, this._testIdAttributeName) : null;
|
||||||
this._activeModel = result && result.selector ? result : null;
|
this._activeModel = result && result.selector ? result : null;
|
||||||
|
|
@ -289,7 +274,7 @@ class Recorder {
|
||||||
const target = this._deepEventTarget(event);
|
const target = this._deepEventTarget(event);
|
||||||
|
|
||||||
if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') {
|
if (target.nodeName === 'INPUT' && (target as HTMLInputElement).type.toLowerCase() === 'file') {
|
||||||
globalThis.__pw_recorderRecordAction({
|
this._delegate.recordAction?.({
|
||||||
name: 'setInputFiles',
|
name: 'setInputFiles',
|
||||||
selector: this._activeModel!.selector,
|
selector: this._activeModel!.selector,
|
||||||
signals: [],
|
signals: [],
|
||||||
|
|
@ -307,7 +292,7 @@ class Recorder {
|
||||||
// Non-navigating actions are simply recorded by Playwright.
|
// Non-navigating actions are simply recorded by Playwright.
|
||||||
if (this._consumedDueWrongTarget(event))
|
if (this._consumedDueWrongTarget(event))
|
||||||
return;
|
return;
|
||||||
globalThis.__pw_recorderRecordAction({
|
this._delegate.recordAction?.({
|
||||||
name: 'fill',
|
name: 'fill',
|
||||||
selector: this._activeModel!.selector,
|
selector: this._activeModel!.selector,
|
||||||
signals: [],
|
signals: [],
|
||||||
|
|
@ -411,7 +396,7 @@ class Recorder {
|
||||||
private async _performAction(action: actions.Action) {
|
private async _performAction(action: actions.Action) {
|
||||||
this._clearHighlight();
|
this._clearHighlight();
|
||||||
this._performingAction = true;
|
this._performingAction = true;
|
||||||
await globalThis.__pw_recorderPerformAction(action).catch(() => {});
|
await this._delegate.performAction?.(action).catch(() => {});
|
||||||
this._performingAction = false;
|
this._performingAction = false;
|
||||||
|
|
||||||
// If that was a keyboard action, it similarly requires new selectors for active model.
|
// If that was a keyboard action, it similarly requires new selectors for active model.
|
||||||
|
|
@ -494,4 +479,60 @@ function removeEventListeners(listeners: (() => void)[]) {
|
||||||
listeners.splice(0, listeners.length);
|
listeners.splice(0, listeners.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Recorder;
|
interface Embedder {
|
||||||
|
__pw_recorderPerformAction(action: actions.Action): Promise<void>;
|
||||||
|
__pw_recorderRecordAction(action: actions.Action): Promise<void>;
|
||||||
|
__pw_recorderState(): Promise<UIState>;
|
||||||
|
__pw_recorderSetSelector(selector: string): Promise<void>;
|
||||||
|
__pw_refreshOverlay(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class PollingRecorder implements RecorderDelegate {
|
||||||
|
private _recorder: Recorder;
|
||||||
|
private _embedder: Embedder;
|
||||||
|
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
constructor(injectedScript: InjectedScript) {
|
||||||
|
this._recorder = new Recorder(injectedScript, this);
|
||||||
|
this._embedder = injectedScript.window as any;
|
||||||
|
|
||||||
|
injectedScript.onGlobalListenersRemoved.add(() => this._recorder.refreshListenersIfNeeded());
|
||||||
|
|
||||||
|
const refreshOverlay = () => {
|
||||||
|
this._pollRecorderMode().catch(e => console.log(e)); // eslint-disable-line no-console
|
||||||
|
};
|
||||||
|
this._embedder.__pw_refreshOverlay = refreshOverlay;
|
||||||
|
refreshOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pollRecorderMode() {
|
||||||
|
const pollPeriod = 1000;
|
||||||
|
if (this._pollRecorderModeTimer)
|
||||||
|
clearTimeout(this._pollRecorderModeTimer);
|
||||||
|
const state = await this._embedder.__pw_recorderState().catch(() => {});
|
||||||
|
if (!state) {
|
||||||
|
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._recorder.setUIState(state);
|
||||||
|
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), pollPeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
async performAction(action: actions.Action) {
|
||||||
|
await this._embedder.__pw_recorderPerformAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async recordAction(action: actions.Action): Promise<void> {
|
||||||
|
await this._embedder.__pw_recorderRecordAction(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
async __pw_recorderState(): Promise<UIState> {
|
||||||
|
return await this._embedder.__pw_recorderState();
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSelector(selector: string): Promise<void> {
|
||||||
|
await this._embedder.__pw_recorderSetSelector(selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PollingRecorder;
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export function matchesAttributePart(value: any, attr: AttributeSelectorPart) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldSkipForTextMatching(element: Element | ShadowRoot) {
|
export function shouldSkipForTextMatching(element: Element | ShadowRoot) {
|
||||||
|
const document = element.ownerDocument;
|
||||||
return element.nodeName === 'SCRIPT' || element.nodeName === 'NOSCRIPT' || element.nodeName === 'STYLE' || document.head && document.head.contains(element);
|
return element.nodeName === 'SCRIPT' || element.nodeName === 'NOSCRIPT' || element.nodeName === 'STYLE' || document.head && document.head.contains(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export class UtilityScript {
|
||||||
if (exposeUtilityScript)
|
if (exposeUtilityScript)
|
||||||
parameters.unshift(this);
|
parameters.unshift(this);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals
|
||||||
let result = globalThis.eval(expression);
|
let result = globalThis.eval(expression);
|
||||||
if (isFunction === true) {
|
if (isFunction === true) {
|
||||||
result = result(...parameters);
|
result = result(...parameters);
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,7 @@ function filterComponentsTree(treeNode: ComponentNode, searchFn: (node: Componen
|
||||||
|
|
||||||
type VueRoot = {version: number, root: VueVNode};
|
type VueRoot = {version: number, root: VueVNode};
|
||||||
function findVueRoots(root: Document | ShadowRoot, roots: VueRoot[] = []): VueRoot[] {
|
function findVueRoots(root: Document | ShadowRoot, roots: VueRoot[] = []): VueRoot[] {
|
||||||
|
const document = root.ownerDocument || root;
|
||||||
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
||||||
// Vue2 roots are referred to from elements.
|
// Vue2 roots are referred to from elements.
|
||||||
const vue2Roots: Set<VueVNode> = new Set();
|
const vue2Roots: Set<VueVNode> = new Set();
|
||||||
|
|
@ -233,6 +234,7 @@ function findVueRoots(root: Document | ShadowRoot, roots: VueRoot[] = []): VueRo
|
||||||
|
|
||||||
export const VueEngine: SelectorEngine = {
|
export const VueEngine: SelectorEngine = {
|
||||||
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
queryAll(scope: SelectorRoot, selector: string): Element[] {
|
||||||
|
const document = scope.ownerDocument || scope;
|
||||||
const { name, attributes } = parseAttributeSelector(selector, false);
|
const { name, attributes } = parseAttributeSelector(selector, false);
|
||||||
const vueRoots = findVueRoots(document);
|
const vueRoots = findVueRoots(document);
|
||||||
const trees = vueRoots.map(vueRoot => vueRoot.version === 3 ? buildComponentsTreeVue3(vueRoot.root) : buildComponentsTreeVue2(vueRoot.root));
|
const trees = vueRoots.map(vueRoot => vueRoot.version === 3 ? buildComponentsTreeVue3(vueRoot.root) : buildComponentsTreeVue2(vueRoot.root));
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
waitForContentOnStop: false,
|
waitForContentOnStop: false,
|
||||||
skipScripts: true,
|
skipScripts: true,
|
||||||
});
|
});
|
||||||
|
const testIdAttributeName = ('selectors' in context) ? context.selectors().testIdAttributeName() : undefined;
|
||||||
this._contextCreatedEvent = {
|
this._contextCreatedEvent = {
|
||||||
version,
|
version,
|
||||||
type: 'context-options',
|
type: 'context-options',
|
||||||
|
|
@ -101,6 +102,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
|
||||||
platform: process.platform,
|
platform: process.platform,
|
||||||
wallTime: 0,
|
wallTime: 0,
|
||||||
sdkLanguage: (context as BrowserContext)?._browser?.options?.sdkLanguage,
|
sdkLanguage: (context as BrowserContext)?._browser?.options?.sdkLanguage,
|
||||||
|
testIdAttributeName
|
||||||
};
|
};
|
||||||
if (context instanceof BrowserContext) {
|
if (context instanceof BrowserContext) {
|
||||||
this._snapshotter = new Snapshotter(context, this);
|
this._snapshotter = new Snapshotter(context, this);
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { CallLogView } from './callLog';
|
||||||
import './recorder.css';
|
import './recorder.css';
|
||||||
import { asLocator } from '@isomorphic/locatorGenerators';
|
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||||
import { toggleTheme } from '@web/theme';
|
import { toggleTheme } from '@web/theme';
|
||||||
|
import { copy } from '@web/uiUtils';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
@ -171,14 +172,3 @@ function renderSourceOptions(sources: Source[]): React.ReactNode {
|
||||||
|
|
||||||
return sources.map(source => renderOption(source));
|
return sources.map(source => renderOption(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
function copy(text: string) {
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.style.position = 'absolute';
|
|
||||||
textArea.style.zIndex = '-1000';
|
|
||||||
textArea.value = text;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
textArea.remove();
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import fs, { existsSync } from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns {import('vite').Plugin}
|
* @returns {import('vite').Plugin}
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export type ContextEntry = {
|
||||||
platform?: string;
|
platform?: string;
|
||||||
wallTime?: number;
|
wallTime?: number;
|
||||||
sdkLanguage?: Language;
|
sdkLanguage?: Language;
|
||||||
|
testIdAttributeName?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
options: trace.BrowserContextEventOptions;
|
options: trace.BrowserContextEventOptions;
|
||||||
pages: PageEntry[];
|
pages: PageEntry[];
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import '@web/third_party/vscode/codicon.css';
|
import '@web/third_party/vscode/codicon.css';
|
||||||
|
import React from 'react';
|
||||||
import * as ReactDOM from 'react-dom';
|
import * as ReactDOM from 'react-dom';
|
||||||
import { applyTheme } from '@web/theme';
|
import { applyTheme } from '@web/theme';
|
||||||
import '@web/common.css';
|
import '@web/common.css';
|
||||||
|
|
|
||||||
|
|
@ -132,6 +132,7 @@ export class TraceModel {
|
||||||
this.contextEntry.wallTime = event.wallTime;
|
this.contextEntry.wallTime = event.wallTime;
|
||||||
this.contextEntry.sdkLanguage = event.sdkLanguage;
|
this.contextEntry.sdkLanguage = event.sdkLanguage;
|
||||||
this.contextEntry.options = event.options;
|
this.contextEntry.options = event.options;
|
||||||
|
this.contextEntry.testIdAttributeName = event.testIdAttributeName;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'screencast-frame': {
|
case 'screencast-frame': {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
[*]
|
[*]
|
||||||
|
@injected/**
|
||||||
@isomorphic/**
|
@isomorphic/**
|
||||||
@web/**
|
@web/**
|
||||||
../entries.ts
|
../entries.ts
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import { ListView } from '@web/components/listView';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import './actionList.css';
|
import './actionList.css';
|
||||||
import * as modelUtil from './modelUtil';
|
import * as modelUtil from './modelUtil';
|
||||||
import './tabbedPane.css';
|
|
||||||
import { asLocator } from '@isomorphic/locatorGenerators';
|
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||||
import type { Language } from '@isomorphic/locatorGenerators';
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@
|
||||||
|
|
||||||
.call-section {
|
.call-section {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|
@ -45,6 +44,10 @@
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.call-section:not(:first-child) {
|
||||||
|
border-top: 1px solid var(--vscode-panel-border);
|
||||||
|
}
|
||||||
|
|
||||||
.call-line {
|
.call-line {
|
||||||
padding: 4px 0 4px 6px;
|
padding: 4px 0 4px 6px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,14 @@ export class MultiTraceModel {
|
||||||
readonly events: trace.ActionTraceEvent[];
|
readonly events: trace.ActionTraceEvent[];
|
||||||
readonly hasSource: boolean;
|
readonly hasSource: boolean;
|
||||||
readonly sdkLanguage: Language | undefined;
|
readonly sdkLanguage: Language | undefined;
|
||||||
|
readonly testIdAttributeName: string | undefined;
|
||||||
|
|
||||||
constructor(contexts: ContextEntry[]) {
|
constructor(contexts: ContextEntry[]) {
|
||||||
contexts.forEach(contextEntry => indexModel(contextEntry));
|
contexts.forEach(contextEntry => indexModel(contextEntry));
|
||||||
|
|
||||||
this.browserName = contexts[0]?.browserName || '';
|
this.browserName = contexts[0]?.browserName || '';
|
||||||
this.sdkLanguage = contexts[0]?.sdkLanguage;
|
this.sdkLanguage = contexts[0]?.sdkLanguage;
|
||||||
|
this.testIdAttributeName = contexts[0]?.testIdAttributeName;
|
||||||
this.platform = contexts[0]?.platform || '';
|
this.platform = contexts[0]?.platform || '';
|
||||||
this.title = contexts[0]?.title || '';
|
this.title = contexts[0]?.title || '';
|
||||||
this.options = contexts[0]?.options || {};
|
this.options = contexts[0]?.options || {};
|
||||||
|
|
|
||||||
|
|
@ -150,3 +150,7 @@ iframe#snapshot {
|
||||||
body.dark-mode .window-header {
|
body.dark-mode .window-header {
|
||||||
background: #444950;
|
background: #444950;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.snapshot-tab .cm-wrapper {
|
||||||
|
line-height: 23px;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,17 +15,31 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './snapshotTab.css';
|
import './snapshotTab.css';
|
||||||
import './tabbedPane.css';
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useMeasure } from './helpers';
|
import { useMeasure } from './helpers';
|
||||||
import type { ActionTraceEvent } from '@trace/trace';
|
import type { ActionTraceEvent } from '@trace/trace';
|
||||||
import { context } from './modelUtil';
|
import { context } from './modelUtil';
|
||||||
|
import { CodeMirrorWrapper } from '@web/components/codeMirrorWrapper';
|
||||||
|
import { Toolbar } from '@web/components/toolbar';
|
||||||
|
import { ToolbarButton } from '@web/components/toolbarButton';
|
||||||
|
import { copy } from '@web/uiUtils';
|
||||||
|
import { InjectedScript } from '@injected/injectedScript';
|
||||||
|
import { Recorder } from '@injected/recorder';
|
||||||
|
import { asLocator } from '@isomorphic/locatorGenerators';
|
||||||
|
import type { Language } from '@isomorphic/locatorGenerators';
|
||||||
|
import { locatorOrSelectorAsSelector } from '@isomorphic/locatorParser';
|
||||||
|
import { TabbedPaneTab } from '@web/components/tabbedPane';
|
||||||
|
|
||||||
export const SnapshotTab: React.FunctionComponent<{
|
export const SnapshotTab: React.FunctionComponent<{
|
||||||
action: ActionTraceEvent | undefined,
|
action: ActionTraceEvent | undefined,
|
||||||
}> = ({ action }) => {
|
sdkLanguage: Language,
|
||||||
|
testIdAttributeName: string,
|
||||||
|
}> = ({ action, sdkLanguage, testIdAttributeName }) => {
|
||||||
|
const [mode, setMode] = React.useState<'none' | 'inspecting'>('none');
|
||||||
const [measure, ref] = useMeasure<HTMLDivElement>();
|
const [measure, ref] = useMeasure<HTMLDivElement>();
|
||||||
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
const [snapshotIndex, setSnapshotIndex] = React.useState(0);
|
||||||
|
const [locator, setLocator] = React.useState<string>('');
|
||||||
|
const [pickerVisible, setPickerVisible] = React.useState(false);
|
||||||
|
|
||||||
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
|
const snapshotMap = new Map<string, { title: string, snapshotName: string }>();
|
||||||
for (const snapshot of action?.metadata.snapshots || [])
|
for (const snapshot of action?.metadata.snapshots || [])
|
||||||
|
|
@ -93,6 +107,16 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
x: (measure.width - snapshotContainerSize.width) / 2,
|
x: (measure.width - snapshotContainerSize.width) / 2,
|
||||||
y: (measure.height - snapshotContainerSize.height) / 2,
|
y: (measure.height - snapshotContainerSize.height) / 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const recorderGetter = () => {
|
||||||
|
if (!iframeRef.current)
|
||||||
|
return;
|
||||||
|
return getOrCreateRecorder(iframeRef.current.contentWindow!, true, sdkLanguage, testIdAttributeName, locator => {
|
||||||
|
setLocator(locator);
|
||||||
|
setMode('none');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return <div
|
return <div
|
||||||
className='snapshot-tab'
|
className='snapshot-tab'
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|
@ -102,19 +126,45 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
if (event.key === 'ArrowLeft')
|
if (event.key === 'ArrowLeft')
|
||||||
setSnapshotIndex(Math.max(snapshotIndex - 1, 0));
|
setSnapshotIndex(Math.max(snapshotIndex - 1, 0));
|
||||||
}}
|
}}
|
||||||
><div className='tab-strip'>
|
>
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarButton title='Pick locator' disabled={!popoutUrl} toggled={pickerVisible} onClick={() => {
|
||||||
|
setPickerVisible(!pickerVisible);
|
||||||
|
setMode(mode === 'inspecting' ? 'none' : 'inspecting');
|
||||||
|
const recorder = recorderGetter();
|
||||||
|
recorder?.setUIState({ mode: pickerVisible ? 'none' : 'inspecting', language: sdkLanguage, testIdAttributeName });
|
||||||
|
}}>Pick locator</ToolbarButton>
|
||||||
|
<div style={{ width: 5 }}></div>
|
||||||
{snapshots.map((snapshot, index) => {
|
{snapshots.map((snapshot, index) => {
|
||||||
return <div className={'tab-element ' + (snapshotIndex === index ? ' selected' : '')}
|
return <TabbedPaneTab
|
||||||
onClick={() => setSnapshotIndex(index)}
|
id={snapshot.title}
|
||||||
key={snapshot.title}>
|
title={renderTitle(snapshot.title)}
|
||||||
<div className='tab-label'>{renderTitle(snapshot.title)}</div>
|
selected={snapshotIndex === index}
|
||||||
</div>;
|
onSelect={() => setSnapshotIndex(index)}
|
||||||
|
></TabbedPaneTab>;
|
||||||
})}
|
})}
|
||||||
</div>
|
<div style={{ flex: 'auto' }}></div>
|
||||||
|
<ToolbarButton icon='link-external' title='Open snapshot in a new tab' disabled={!popoutUrl} onClick={() => {
|
||||||
|
window.open(popoutUrl || '', '_blank');
|
||||||
|
}}></ToolbarButton>
|
||||||
|
</Toolbar>
|
||||||
|
{pickerVisible && <Toolbar>
|
||||||
|
<ToolbarButton icon='microscope' title='Pick locator' disabled={!popoutUrl} toggled={mode === 'inspecting'} onClick={() => {
|
||||||
|
setMode(mode === 'inspecting' ? 'none' : 'inspecting');
|
||||||
|
const recorder = recorderGetter();
|
||||||
|
recorder?.setUIState({ mode: mode === 'inspecting' ? 'none' : 'inspecting', language: sdkLanguage, testIdAttributeName });
|
||||||
|
}}></ToolbarButton>
|
||||||
|
<CodeMirrorWrapper text={locator} language={sdkLanguage} readOnly={!popoutUrl} focusOnChange={true} wrapLines={true} onChange={text => {
|
||||||
|
const recorder = recorderGetter();
|
||||||
|
const actionSelector = locatorOrSelectorAsSelector(sdkLanguage, text, testIdAttributeName);
|
||||||
|
recorder?.setUIState({ mode: 'none', language: sdkLanguage, testIdAttributeName, actionSelector });
|
||||||
|
setLocator(text);
|
||||||
|
}}></CodeMirrorWrapper>
|
||||||
|
<ToolbarButton icon='files' title='Copy locator' disabled={!popoutUrl} onClick={() => {
|
||||||
|
copy(locator);
|
||||||
|
}}></ToolbarButton>
|
||||||
|
</Toolbar>}
|
||||||
<div ref={ref} className='snapshot-wrapper'>
|
<div ref={ref} className='snapshot-wrapper'>
|
||||||
<a className={`popout-icon ${popoutUrl ? '' : 'popout-disabled'}`} href={popoutUrl} target='_blank' title='Open snapshot in a new tab'>
|
|
||||||
<span className='codicon codicon-link-external'/>
|
|
||||||
</a>
|
|
||||||
{ snapshots.length ? <div className='snapshot-container' style={{
|
{ snapshots.length ? <div className='snapshot-container' style={{
|
||||||
width: snapshotContainerSize.width + 'px',
|
width: snapshotContainerSize.width + 'px',
|
||||||
height: snapshotContainerSize.height + 'px',
|
height: snapshotContainerSize.height + 'px',
|
||||||
|
|
@ -126,7 +176,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
<span className='window-dot' style={{ backgroundColor: 'rgb(251, 190, 60)' }}></span>
|
<span className='window-dot' style={{ backgroundColor: 'rgb(251, 190, 60)' }}></span>
|
||||||
<span className='window-dot' style={{ backgroundColor: 'rgb(88, 203, 66)' }}></span>
|
<span className='window-dot' style={{ backgroundColor: 'rgb(88, 203, 66)' }}></span>
|
||||||
</div>
|
</div>
|
||||||
<div className='window-address-bar' title={snapshotInfo.url}>{snapshotInfo.url}</div>
|
<div className='window-address-bar' title={snapshotInfo.url || 'about:blank'}>{snapshotInfo.url || 'about:blank'}</div>
|
||||||
<div style={{ marginLeft: 'auto' }}>
|
<div style={{ marginLeft: 'auto' }}>
|
||||||
<div>
|
<div>
|
||||||
<span className='window-menu-bar'></span>
|
<span className='window-menu-bar'></span>
|
||||||
|
|
@ -142,6 +192,25 @@ export const SnapshotTab: React.FunctionComponent<{
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getOrCreateRecorder(contentWindow: Window, enabled: boolean, sdkLanguage: Language, testIdAttributeName: string, setLocator: (locator: string) => void): Recorder | undefined {
|
||||||
|
const win = contentWindow as any;
|
||||||
|
if (!enabled && !win._recorder)
|
||||||
|
return;
|
||||||
|
let recorder: Recorder | undefined = win._recorder;
|
||||||
|
|
||||||
|
if (!recorder) {
|
||||||
|
const injectedScript = new InjectedScript(contentWindow as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
|
||||||
|
recorder = new Recorder(injectedScript, {
|
||||||
|
async setSelector(selector: string) {
|
||||||
|
recorder!.setUIState({ mode: 'none', language: sdkLanguage, testIdAttributeName });
|
||||||
|
setLocator(asLocator('javascript', selector, false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
win._recorder = recorder;
|
||||||
|
}
|
||||||
|
return recorder;
|
||||||
|
}
|
||||||
|
|
||||||
function renderTitle(snapshotTitle: string): string {
|
function renderTitle(snapshotTitle: string): string {
|
||||||
if (snapshotTitle === 'before')
|
if (snapshotTitle === 'before')
|
||||||
return 'Before';
|
return 'Before';
|
||||||
|
|
|
||||||
|
|
@ -90,17 +90,6 @@
|
||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workbench tab-content {
|
|
||||||
padding: 25px;
|
|
||||||
contain: size;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench tab-strip {
|
|
||||||
margin-left: calc(-1*var(--sidebar-width));
|
|
||||||
padding-left: var(--sidebar-width);
|
|
||||||
box-shadow: var(--box-shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench .logo {
|
.workbench .logo {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { MultiTraceModel } from './modelUtil';
|
||||||
import { NetworkTab } from './networkTab';
|
import { NetworkTab } from './networkTab';
|
||||||
import { SnapshotTab } from './snapshotTab';
|
import { SnapshotTab } from './snapshotTab';
|
||||||
import { SourceTab } from './sourceTab';
|
import { SourceTab } from './sourceTab';
|
||||||
import { TabbedPane } from './tabbedPane';
|
import { TabbedPane } from '@web/components/tabbedPane';
|
||||||
import { Timeline } from './timeline';
|
import { Timeline } from './timeline';
|
||||||
import './workbench.css';
|
import './workbench.css';
|
||||||
import { toggleTheme } from '@web/theme';
|
import { toggleTheme } from '@web/theme';
|
||||||
|
|
@ -208,7 +208,7 @@ export const Workbench: React.FunctionComponent<{
|
||||||
</div>
|
</div>
|
||||||
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
<SplitView sidebarSize={300} orientation='horizontal' sidebarIsFirst={true}>
|
||||||
<SplitView sidebarSize={300} orientation={view === 'embedded' ? 'vertical' : 'horizontal'}>
|
<SplitView sidebarSize={300} orientation={view === 'embedded' ? 'vertical' : 'horizontal'}>
|
||||||
<SnapshotTab action={activeAction} />
|
<SnapshotTab action={activeAction} sdkLanguage={model.sdkLanguage || 'javascript'} testIdAttributeName={model.testIdAttributeName || 'data-testid'} />
|
||||||
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
<TabbedPane tabs={tabs} selectedTab={selectedPropertiesTab} setSelectedTab={setSelectedPropertiesTab}/>
|
||||||
</SplitView>
|
</SplitView>
|
||||||
<TabbedPane tabs={
|
<TabbedPane tabs={
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"@injected/*": ["../playwright-core/src/server/injected/*"],
|
||||||
"@isomorphic/*": ["../playwright-core/src/server/isomorphic/*"],
|
"@isomorphic/*": ["../playwright-core/src/server/isomorphic/*"],
|
||||||
"@protocol/*": ["../protocol/src/*"],
|
"@protocol/*": ["../protocol/src/*"],
|
||||||
"@recorder/*": ["../recorder/src/*"],
|
"@recorder/*": ["../recorder/src/*"],
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
// @ts-ignore
|
||||||
import { bundle } from './bundle';
|
import { bundle } from './bundle';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
|
@ -28,6 +29,7 @@ export default defineConfig({
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'@injected': path.resolve(__dirname, '../playwright-core/src/server/injected'),
|
||||||
'@isomorphic': path.resolve(__dirname, '../playwright-core/src/server/isomorphic'),
|
'@isomorphic': path.resolve(__dirname, '../playwright-core/src/server/isomorphic'),
|
||||||
'@protocol': path.resolve(__dirname, '../protocol/src'),
|
'@protocol': path.resolve(__dirname, '../protocol/src'),
|
||||||
'@web': path.resolve(__dirname, '../web/src'),
|
'@web': path.resolve(__dirname, '../web/src'),
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
// @ts-ignore
|
||||||
import { bundle } from './bundle';
|
import { bundle } from './bundle';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export type ContextCreatedTraceEvent = {
|
||||||
title?: string,
|
title?: string,
|
||||||
options: BrowserContextEventOptions,
|
options: BrowserContextEventOptions,
|
||||||
sdkLanguage?: Language,
|
sdkLanguage?: Language,
|
||||||
|
testIdAttributeName?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScreencastFrameTraceEvent = {
|
export type ScreencastFrameTraceEvent = {
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export const CodeMirrorWrapper: React.FC<SourceProps> = ({
|
||||||
if (language === 'csharp')
|
if (language === 'csharp')
|
||||||
mode = 'text/x-csharp';
|
mode = 'text/x-csharp';
|
||||||
|
|
||||||
if (codemirror && codemirror.getOption('mode') === mode)
|
if (codemirror && codemirror.getOption('mode') === mode && codemirror.isReadOnly() === readOnly)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!codemirrorElement.current)
|
if (!codemirrorElement.current)
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,6 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.list-view {
|
|
||||||
border-top: 1px solid var(--vscode-panel-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-view-content {
|
.list-view-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -20,29 +20,13 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tabbed-pane .tab-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: auto;
|
flex: auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-strip {
|
.tabbed-pane-tab {
|
||||||
display: flex;
|
|
||||||
background-color: var(--vscode-sideBar-background);
|
|
||||||
color: var(--vscode-sideBarTitle-foreground);
|
|
||||||
height: 32px;
|
|
||||||
align-items: center;
|
|
||||||
padding-right: 10px;
|
|
||||||
flex: none;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-strip:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tab-element {
|
|
||||||
padding: 2px 10px 0 10px;
|
padding: 2px 10px 0 10px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -56,7 +40,7 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-label {
|
.tabbed-pane-tab-label {
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
@ -64,13 +48,13 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-count {
|
.tabbed-pane-tab-count {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-element.selected {
|
.tabbed-pane-tab.selected {
|
||||||
background-color: var(--vscode-tab-activeBackground);
|
background-color: var(--vscode-tab-activeBackground);
|
||||||
}
|
}
|
||||||
|
|
@ -15,9 +15,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './tabbedPane.css';
|
import './tabbedPane.css';
|
||||||
|
import { Toolbar } from './toolbar';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
export interface TabbedPaneTab {
|
export interface TabbedPaneTabModel {
|
||||||
id: string;
|
id: string;
|
||||||
title: string | JSX.Element;
|
title: string | JSX.Element;
|
||||||
count?: number;
|
count?: number;
|
||||||
|
|
@ -25,24 +26,23 @@ export interface TabbedPaneTab {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabbedPane: React.FunctionComponent<{
|
export const TabbedPane: React.FunctionComponent<{
|
||||||
tabs: TabbedPaneTab[],
|
tabs: TabbedPaneTabModel[],
|
||||||
selectedTab: string,
|
selectedTab: string,
|
||||||
setSelectedTab: (tab: string) => void
|
setSelectedTab: (tab: string) => void
|
||||||
}> = ({ tabs, selectedTab, setSelectedTab }) => {
|
}> = ({ tabs, selectedTab, setSelectedTab }) => {
|
||||||
return <div className='tabbed-pane'>
|
return <div className='tabbed-pane'>
|
||||||
<div className='vbox'>
|
<div className='vbox'>
|
||||||
<div className='hbox' style={{ flex: 'none' }}>
|
<Toolbar>{
|
||||||
<div className='tab-strip'>{
|
tabs.map(tab => (
|
||||||
tabs.map(tab => (
|
<TabbedPaneTab
|
||||||
<div className={'tab-element ' + (selectedTab === tab.id ? 'selected' : '')}
|
id={tab.id}
|
||||||
onClick={() => setSelectedTab(tab.id)}
|
title={tab.title}
|
||||||
key={tab.id}>
|
count={tab.count}
|
||||||
<div className='tab-label'>{tab.title}</div>
|
selected={selectedTab === tab.id}
|
||||||
<div className='tab-count'>{tab.count || ''}</div>
|
onSelect={setSelectedTab}
|
||||||
</div>
|
></TabbedPaneTab>
|
||||||
))
|
))
|
||||||
}</div>
|
}</Toolbar>
|
||||||
</div>
|
|
||||||
{
|
{
|
||||||
tabs.map(tab => {
|
tabs.map(tab => {
|
||||||
if (selectedTab === tab.id)
|
if (selectedTab === tab.id)
|
||||||
|
|
@ -52,3 +52,18 @@ export const TabbedPane: React.FunctionComponent<{
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TabbedPaneTab: React.FunctionComponent<{
|
||||||
|
id: string,
|
||||||
|
title: string | JSX.Element,
|
||||||
|
count?: number,
|
||||||
|
selected?: boolean,
|
||||||
|
onSelect: (id: string) => void
|
||||||
|
}> = ({ id, title, count, selected, onSelect }) => {
|
||||||
|
return <div className={'tabbed-pane-tab ' + (selected ? 'selected' : '')}
|
||||||
|
onClick={() => onSelect(id)}
|
||||||
|
key={id}>
|
||||||
|
<div className='tabbed-pane-tab-label'>{title}</div>
|
||||||
|
<div className='tabbed-pane-tab-count'>{count || ''}</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
@ -19,9 +19,8 @@
|
||||||
box-shadow: var(--box-shadow);
|
box-shadow: var(--box-shadow);
|
||||||
background-color: var(--vscode-sideBar-background);
|
background-color: var(--vscode-sideBar-background);
|
||||||
color: var(--vscode-sideBarTitle-foreground);
|
color: var(--vscode-sideBarTitle-foreground);
|
||||||
min-height: 40px;
|
min-height: 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 10px;
|
|
||||||
flex: none;
|
flex: none;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
color: var(--vscode-sideBarTitle-foreground);
|
color: var(--vscode-sideBarTitle-foreground);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin-left: 10px;
|
margin: 0 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -39,3 +39,7 @@
|
||||||
.toolbar-button:not(:disabled):active {
|
.toolbar-button:not(:disabled):active {
|
||||||
background-color: var(--vscode-toolbar-activeBackground);
|
background-color: var(--vscode-toolbar-activeBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar-button.toggled {
|
||||||
|
color: var(--vscode-inputOption-activeBorder);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import * as React from 'react';
|
||||||
|
|
||||||
export interface ToolbarButtonProps {
|
export interface ToolbarButtonProps {
|
||||||
title: string,
|
title: string,
|
||||||
icon: string,
|
icon?: string,
|
||||||
disabled?: boolean,
|
disabled?: boolean,
|
||||||
toggled?: boolean,
|
toggled?: boolean,
|
||||||
onClick: () => void,
|
onClick: () => void,
|
||||||
|
|
@ -29,7 +29,7 @@ export interface ToolbarButtonProps {
|
||||||
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>> = ({
|
||||||
children,
|
children,
|
||||||
title = '',
|
title = '',
|
||||||
icon = '',
|
icon,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
toggled = false,
|
toggled = false,
|
||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
|
|
@ -38,7 +38,7 @@ export const ToolbarButton: React.FC<React.PropsWithChildren<ToolbarButtonProps>
|
||||||
if (toggled)
|
if (toggled)
|
||||||
className += ' toggled';
|
className += ' toggled';
|
||||||
return <button className={className} onClick={onClick} title={title} disabled={!!disabled}>
|
return <button className={className} onClick={onClick} title={title} disabled={!!disabled}>
|
||||||
<span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>
|
{icon && <span className={`codicon codicon-${icon}`} style={children ? { marginRight: 5 } : {}}></span>}
|
||||||
{children}
|
{children}
|
||||||
</button>;
|
</button>;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ body {
|
||||||
--vscode-widget-shadow: rgba(0, 0, 0, 0.16);
|
--vscode-widget-shadow: rgba(0, 0, 0, 0.16);
|
||||||
--vscode-input-background: #ffffff;
|
--vscode-input-background: #ffffff;
|
||||||
--vscode-input-foreground: #616161;
|
--vscode-input-foreground: #616161;
|
||||||
--vscode-inputOption-activeBorder: rgba(0, 122, 204, 0);
|
--vscode-inputOption-activeBorder: #007acc;
|
||||||
--vscode-inputOption-hoverBackground: rgba(184, 184, 184, 0.31);
|
--vscode-inputOption-hoverBackground: rgba(184, 184, 184, 0.31);
|
||||||
--vscode-inputOption-activeBackground: rgba(0, 144, 241, 0.2);
|
--vscode-inputOption-activeBackground: rgba(0, 144, 241, 0.2);
|
||||||
--vscode-inputOption-activeForeground: #000000;
|
--vscode-inputOption-activeForeground: #000000;
|
||||||
|
|
@ -567,7 +567,7 @@ body.dark-mode {
|
||||||
--vscode-widget-shadow: rgba(0, 0, 0, 0.36);
|
--vscode-widget-shadow: rgba(0, 0, 0, 0.36);
|
||||||
--vscode-input-background: #3c3c3c;
|
--vscode-input-background: #3c3c3c;
|
||||||
--vscode-input-foreground: #cccccc;
|
--vscode-input-foreground: #cccccc;
|
||||||
--vscode-inputOption-activeBorder: rgba(0, 122, 204, 0);
|
--vscode-inputOption-activeBorder: #007acc;
|
||||||
--vscode-inputOption-hoverBackground: rgba(90, 93, 94, 0.5);
|
--vscode-inputOption-hoverBackground: rgba(90, 93, 94, 0.5);
|
||||||
--vscode-inputOption-activeBackground: rgba(0, 127, 212, 0.4);
|
--vscode-inputOption-activeBackground: rgba(0, 127, 212, 0.4);
|
||||||
--vscode-inputOption-activeForeground: #ffffff;
|
--vscode-inputOption-activeForeground: #ffffff;
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,14 @@ export function upperBound<S, T>(array: S[], object: T, comparator: (object: T,
|
||||||
}
|
}
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function copy(text: string) {
|
||||||
|
const textArea = document.createElement('textarea');
|
||||||
|
textArea.style.position = 'absolute';
|
||||||
|
textArea.style.zIndex = '-1000';
|
||||||
|
textArea.value = text;
|
||||||
|
document.body.appendChild(textArea);
|
||||||
|
textArea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
textArea.remove();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ class TraceViewerPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
async selectSnapshot(name: string) {
|
async selectSnapshot(name: string) {
|
||||||
await this.page.click(`.snapshot-tab .tab-label:has-text("${name}")`);
|
await this.page.click(`.snapshot-tab .tabbed-pane-tab-label:has-text("${name}")`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async showConsoleTab() {
|
async showConsoleTab() {
|
||||||
|
|
|
||||||
|
|
@ -683,7 +683,7 @@ test('should include requestUrl in route.fulfill', async ({ page, runAndTrace, b
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
// Render snapshot, check expectations.
|
||||||
await traceViewer.selectAction('route.fulfill');
|
await traceViewer.selectAction('route.fulfill');
|
||||||
await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click();
|
await traceViewer.page.locator('.tabbed-pane-tab-label', { hasText: 'Call' }).click();
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
const callLine = traceViewer.page.locator('.call-line');
|
||||||
await expect(callLine.getByText('status')).toContainText('200');
|
await expect(callLine.getByText('status')).toContainText('200');
|
||||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
||||||
|
|
@ -699,7 +699,7 @@ test('should include requestUrl in route.continue', async ({ page, runAndTrace,
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
// Render snapshot, check expectations.
|
||||||
await traceViewer.selectAction('route.continue');
|
await traceViewer.selectAction('route.continue');
|
||||||
await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click();
|
await traceViewer.page.locator('.tabbed-pane-tab-label', { hasText: 'Call' }).click();
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
const callLine = traceViewer.page.locator('.call-line');
|
||||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
||||||
await expect(callLine.getByText(/^url:.*/)).toContainText(server.EMPTY_PAGE);
|
await expect(callLine.getByText(/^url:.*/)).toContainText(server.EMPTY_PAGE);
|
||||||
|
|
@ -715,7 +715,7 @@ test('should include requestUrl in route.abort', async ({ page, runAndTrace, ser
|
||||||
|
|
||||||
// Render snapshot, check expectations.
|
// Render snapshot, check expectations.
|
||||||
await traceViewer.selectAction('route.abort');
|
await traceViewer.selectAction('route.abort');
|
||||||
await traceViewer.page.locator('.tab-label', { hasText: 'Call' }).click();
|
await traceViewer.page.locator('.tabbed-pane-tab-label', { hasText: 'Call' }).click();
|
||||||
const callLine = traceViewer.page.locator('.call-line');
|
const callLine = traceViewer.page.locator('.call-line');
|
||||||
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
await expect(callLine.getByText('requestUrl')).toContainText('http://test.com');
|
||||||
});
|
});
|
||||||
|
|
@ -765,3 +765,26 @@ test('should display language-specific locators', async ({ runAndTrace, server,
|
||||||
/locator.clickget_by_role\("button", name="Submit"\)/,
|
/locator.clickget_by_role\("button", name="Submit"\)/,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should pick locator', async ({ page, runAndTrace, server }) => {
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.setContent('<button>Submit</button>');
|
||||||
|
});
|
||||||
|
const snapshot = await traceViewer.snapshotFrame('page.setContent');
|
||||||
|
await traceViewer.page.getByTitle('Pick locator').click();
|
||||||
|
await snapshot.click('button');
|
||||||
|
await expect(traceViewer.page.locator('.cm-wrapper')).toContainText(`getByRole('button', { name: 'Submit' })`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update highlight when typing', async ({ page, runAndTrace, server }) => {
|
||||||
|
const traceViewer = await runAndTrace(async () => {
|
||||||
|
await page.goto(server.EMPTY_PAGE);
|
||||||
|
await page.setContent('<button>Submit</button>');
|
||||||
|
});
|
||||||
|
const snapshot = await traceViewer.snapshotFrame('page.setContent');
|
||||||
|
await traceViewer.page.getByTitle('Pick locator').click();
|
||||||
|
await traceViewer.page.locator('.CodeMirror').click();
|
||||||
|
await traceViewer.page.keyboard.type('button');
|
||||||
|
await expect(snapshot.locator('x-pw-glass')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,13 @@
|
||||||
- foo/lib means require dependency
|
- foo/lib means require dependency
|
||||||
*/
|
*/
|
||||||
"@html-reporter/*": ["./packages/html-reporter/src/*"],
|
"@html-reporter/*": ["./packages/html-reporter/src/*"],
|
||||||
|
"@injected/*": ["./packages/playwright-core/src/server/injected/*"],
|
||||||
|
"@isomorphic/*": ["./packages/playwright-core/src/server/isomorphic/*"],
|
||||||
"@protocol/*": ["./packages/protocol/src/*"],
|
"@protocol/*": ["./packages/protocol/src/*"],
|
||||||
"@recorder/*": ["./packages/recorder/src/*"],
|
"@recorder/*": ["./packages/recorder/src/*"],
|
||||||
"@trace/*": ["./packages/trace/src/*"],
|
"@trace/*": ["./packages/trace/src/*"],
|
||||||
"playwright-core/lib/*": ["./packages/playwright-core/src/*"]
|
"@web/*": ["./packages/web/src/*"],
|
||||||
|
"playwright-core/lib/*": ["./packages/playwright-core/src/*"],
|
||||||
},
|
},
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|
@ -35,8 +38,6 @@
|
||||||
"packages/playwright-ct-svelte",
|
"packages/playwright-ct-svelte",
|
||||||
"packages/playwright-ct-vue",
|
"packages/playwright-ct-vue",
|
||||||
"packages/playwright-ct-vue2",
|
"packages/playwright-ct-vue2",
|
||||||
"packages/recorder",
|
|
||||||
"packages/trace-viewer",
|
|
||||||
"packages/web",
|
"packages/web",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const packagesDir = path.normalize(path.join(__dirname, '..', 'packages'));
|
||||||
const packages = new Map();
|
const packages = new Map();
|
||||||
for (const package of fs.readdirSync(packagesDir))
|
for (const package of fs.readdirSync(packagesDir))
|
||||||
packages.set(package, packagesDir + '/' + package + '/src/');
|
packages.set(package, packagesDir + '/' + package + '/src/');
|
||||||
|
packages.set('injected', packagesDir + '/playwright-core/src/server/injected/');
|
||||||
packages.set('isomorphic', packagesDir + '/playwright-core/src/server/isomorphic/');
|
packages.set('isomorphic', packagesDir + '/playwright-core/src/server/isomorphic/');
|
||||||
|
|
||||||
const peerDependencies = ['electron', 'react', 'react-dom', '@zip.js/zip.js'];
|
const peerDependencies = ['electron', 'react', 'react-dom', '@zip.js/zip.js'];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue