Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 9 additions & 14 deletions packages/injected/src/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class Highlight {
private _injectedScript: InjectedScript;
private _rafRequest: number | undefined;
private _language: Language = 'javascript';
private _elementHighlightSelectors = new Map<string, { selector: ParsedSelector, cssStyle?: string }>();
private _elementHighlights: { selector: ParsedSelector, cssStyle?: string }[] = [];

constructor(injectedScript: InjectedScript) {
this._injectedScript = injectedScript;
Expand Down Expand Up @@ -126,17 +126,12 @@ export class Highlight {
this._language = language;
}

addElementHighlight(selector: ParsedSelector, cssStyle?: string) {
const key = stringifySelector(selector);
this._elementHighlightSelectors.set(key, { selector, cssStyle });
this._ensureElementHighlightRaf();
}

removeElementHighlight(selector: ParsedSelector) {
const key = stringifySelector(selector);
if (!this._elementHighlightSelectors.delete(key))
return;
if (this._elementHighlightSelectors.size === 0) {
setElementHighlights(highlights: { selector: ParsedSelector, cssStyle?: string }[]) {
const hadHighlights = this._elementHighlights.length > 0;
this._elementHighlights = highlights;
if (this._elementHighlights.length) {
this._ensureElementHighlightRaf();
} else if (hadHighlights) {
if (this._rafRequest) {
this._injectedScript.utils.builtins.cancelAnimationFrame(this._rafRequest);
this._rafRequest = undefined;
Expand All @@ -151,7 +146,7 @@ export class Highlight {
const tick = () => {
const entries: HighlightEntry[] = [];
const glassPanes = [...this._injectedScript.document.querySelectorAll('x-pw-glass')];
for (const { selector, cssStyle } of this._elementHighlightSelectors.values()) {
for (const { selector, cssStyle } of this._elementHighlights) {
let elements: Element[] = [];
try {
elements = this._injectedScript.querySelectorAll(selector, this._injectedScript.document.documentElement);
Expand All @@ -178,7 +173,7 @@ export class Highlight {
this._injectedScript.utils.builtins.cancelAnimationFrame(this._rafRequest);
this._rafRequest = undefined;
}
this._elementHighlightSelectors.clear();
this._elementHighlights = [];
this._glassPaneElement.remove();
}

Expand Down
11 changes: 4 additions & 7 deletions packages/injected/src/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1344,14 +1344,11 @@ export class InjectedScript {
return this._highlight;
}

addHighlight(selector: ParsedSelector, style?: string) {
const highlight = this._ensureHighlight();
highlight.addElementHighlight(selector, style);
}

removeHighlight(selector: ParsedSelector) {
setHighlights(highlights: { selector: ParsedSelector, cssStyle?: string }[]) {
if (!highlights.length && !this._highlight)
return;
const highlight = this._ensureHighlight();
highlight.removeElementHighlight(selector);
highlight.setElementHighlights(highlights);
}

setScreencastAnnotation(annotation: { point?: Point, box?: Rect, actionTitle?: string, duration?: number, position?: string, fontSize?: number, cursor?: 'none' | 'pointer' } | null) {
Expand Down
21 changes: 14 additions & 7 deletions packages/playwright-core/src/server/chromium/crPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,8 @@ class FrameSession {
this._firstNonInitialNavigationCommittedReject(new TargetClosedError(this._page.closeReason()));
for (const childSession of this._childSessions)
childSession.dispose();
for (const sessionId of this._workerSessions.keys())
this._removeWorkerSession(sessionId);
if (this._parentSession)
this._parentSession._childSessions.delete(this);
eventsHelper.removeEventListeners(this._eventListeners);
Expand Down Expand Up @@ -780,16 +782,21 @@ class FrameSession {
session.on('Runtime.exceptionThrown', exception => this._page.addPageError(exceptionToError(exception.exceptionDetails), stackTraceToLocation(exception.exceptionDetails.stackTrace)));
}

private _removeWorkerSession(sessionId: string): boolean {
const workerSession = this._workerSessions.get(sessionId);
if (!workerSession)
return false;
this._workerSessions.delete(sessionId);
this._crPage._networkManager.removeSession(workerSession);
workerSession.dispose();
this._page.removeWorker(sessionId);
return true;
}

_onDetachedFromTarget(event: Protocol.Target.detachedFromTargetPayload) {
// This might be a worker...
const workerSession = this._workerSessions.get(event.sessionId);
if (workerSession) {
this._workerSessions.delete(event.sessionId);
this._crPage._networkManager.removeSession(workerSession);
workerSession.dispose();
this._page.removeWorker(event.sessionId);
if (this._removeWorkerSession(event.sessionId))
return;
}

// ... or an oopif.
const childFrameSession = this._crPage._sessions.get(event.targetId!);
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/src/server/debugController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export class DebugController extends SdkObject {
for (const recorder of await progress.race(this._allRecorders()))
promises.push(recorder.hideHighlightedSelector());
// Hide all locator.highlight highlights.
promises.push(...this._playwright.allPages().map(p => p.hideHighlight().catch(() => {})));
promises.push(...this._playwright.allPages().map(p => p.highlightController.hideHighlights().catch(() => {})));
await progress.race(Promise.all(promises));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,11 +272,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameChannel, Br
}

async highlight(params: channels.FrameHighlightParams, progress: Progress): Promise<void> {
return await progress.race(this._frame.addHighlight(params.selector, params.style));
return await progress.race(this._frame._page.highlightController.addHighlight(params.selector, { style: params.style }));
}

async hideHighlight(params: channels.FrameHideHighlightParams, progress: Progress): Promise<void> {
return await progress.race(this._frame.removeHighlight(params.selector));
return await progress.race(this._frame._page.highlightController.removeHighlight(params.selector));
}

async expect(params: channels.FrameExpectParams, progress: Progress): Promise<channels.FrameExpectResult> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}

async hideHighlight(params: channels.PageHideHighlightParams, progress: Progress): Promise<void> {
await progress.race(this._page.hideHighlight());
await progress.race(this._page.highlightController.hideHighlights());
}

async screencastShowOverlay(params: channels.PageScreencastShowOverlayParams): Promise<channels.PageScreencastShowOverlayResult> {
Expand Down
10 changes: 5 additions & 5 deletions packages/playwright-core/src/server/frameSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ export class FrameSelectors {
return jumptToFrame;
}

private async _resolveFramesForSelector(selector: string, options: types.StrictOptions & { noDefaultPierce?: boolean } = {}, scope?: ElementHandle): Promise<SelectorInFrame[]> {
const pierceByDefault = !!this.frame._page.browserContext._options.pierceFrames && !options.noDefaultPierce;
async resolveFramesForSelector(selector: string, options: types.StrictOptions & { pierce?: 'default' | 'pierce' | 'no-pierce' } = {}, scope?: ElementHandle): Promise<SelectorInFrame[]> {
const pierceByDefault = options.pierce === 'pierce' || (options.pierce !== 'no-pierce' && !!this.frame._page.browserContext._options.pierceFrames);
const { pierce, chunks } = splitSelectorByFrame(selector, pierceByDefault);
for (const chunk of chunks) {
visitAllSelectorParts(chunk, (part, nested) => {
Expand Down Expand Up @@ -280,12 +280,12 @@ export class FrameSelectors {

private async _callOnSelectorInternal<Arg, R>(
selector: string,
options: types.StrictOptions & { mainWorld?: boolean, callWithoutMatches?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none', noDefaultPierce?: boolean },
options: types.StrictOptions & { mainWorld?: boolean, callWithoutMatches?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none', pierce?: 'default' | 'pierce' | 'no-pierce' },
pageFunction: MatchedElementsCallback<Arg, R>,
arg: Arg,
returnByValue: boolean,
): Promise<{ frame: Frame, info: SelectorInfo, result: R | SmartHandle<R> } | null> {
const resolved = await this._resolveFramesForSelector(selector, options, options.scope);
const resolved = await this.resolveFramesForSelector(selector, options, options.scope);
let aggregatedResult: { frame: Frame, info: SelectorInfo, result: R | SmartHandle<R> } | null = null;
const noStall = resolved.length > 1;
for (const { frame, info, scope } of resolved) {
Expand Down Expand Up @@ -336,7 +336,7 @@ export class FrameSelectors {

async callOnSelector<Arg, R>(
selector: string,
options: types.StrictOptions & { mainWorld?: boolean, callWithoutMatches?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none', noDefaultPierce?: boolean },
options: types.StrictOptions & { mainWorld?: boolean, callWithoutMatches?: boolean, scope?: ElementHandle, markTargets?: 'all' | 'first' | 'none', pierce?: 'default' | 'pierce' | 'no-pierce' },
pageFunction: MatchedElementsCallback<Arg, R>,
arg: Arg,
): Promise<{ frame: Frame, info: SelectorInfo, result: R } | null> {
Expand Down
22 changes: 1 addition & 21 deletions packages/playwright-core/src/server/frames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1373,26 +1373,6 @@ export class Frame extends SdkObject<FrameEventMap> {
return result;
}

async addHighlight(selector: string, style?: string) {
await this.selectors.callOnSelector(selector, { strict: false, callWithoutMatches: true }, ({ injected, info }, style) => {
return injected.addHighlight(info.parsed, style);
}, style);
}

async removeHighlight(selector: string) {
await this.selectors.callOnSelector(selector, { strict: false, callWithoutMatches: true }, ({ injected, info }) => {
return injected.removeHighlight(info.parsed);
}, {});
}

async hideHighlight() {
return this.raceAgainstEvaluationStallingEvents(async () => {
const context = this._contextData.get('utility')?.context;
const injectedScript = await context?.injectedScript();
await injectedScript?.evaluate(injected => injected.hideHighlight());
});
}

private async _elementState(progress: Progress, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions, scope?: dom.ElementHandle): Promise<boolean> {
const { result } = await this._waitForFunctionOnSelector(progress, selector, (injected, element, data) => {
return { result: injected.elementState(element, data.state) };
Expand Down Expand Up @@ -1568,7 +1548,7 @@ export class Frame extends SdkObject<FrameEventMap> {
let missingReceived = false;

// Non-array expectations are strict (callOnSelector throws on multiple); array ones are not.
const resolved = await progress.race(this.selectors.callOnSelector(effectiveSelector, { strict: !isArray, mainWorld, markTargets: 'all', noDefaultPierce: !selector }, async ({ injected, elements }, options) => {
const resolved = await progress.race(this.selectors.callOnSelector(effectiveSelector, { strict: !isArray, mainWorld, markTargets: 'all', pierce: selector ? 'default' : 'no-pierce' }, async ({ injected, elements }, options) => {
const isArray = options.expression === 'to.have.count' || options.expression.endsWith('.array');
const log = isArray
? ` locator resolved to ${elements.length} element${elements.length === 1 ? '' : 's'}`
Expand Down
117 changes: 117 additions & 0 deletions packages/playwright-core/src/server/highlightController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/**
* Copyright (c) Microsoft Corporation.
*
* 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 { Frame } from './frames';
import type { Page } from './page';
import type { ParsedSelector } from '@isomorphic/selectorParser';

export type HighlightOptions = {
style?: string;
pierce?: boolean; // Highlight in all the frames the selector could resolve to, instead of a single one.
};

type HighlightEntry = HighlightOptions & {
selector: string;
};

export class HighlightController {
private _page: Page;
private _entries = new Map<string, HighlightEntry>();
private _resolutionTimer: NodeJS.Timeout | undefined;
private _resolutionChain: Promise<void> = Promise.resolve();

constructor(page: Page) {
this._page = page;
}

async addHighlight(selector: string, options: HighlightOptions = {}) {
// Validate the selector upfront, so that the caller gets a synchronous error.
this._page.browserContext.selectors().parseSelector(selector, false);
this._entries.set(selector, { selector, ...options });
await this._resolveNow();
}

async removeHighlight(selector: string) {
this._entries.delete(selector);
await this._resolveNow();
}

dispose() {
if (this._resolutionTimer) {
clearTimeout(this._resolutionTimer);
this._resolutionTimer = undefined;
}
}

async hideHighlights() {
this._entries.clear();
await Promise.all(this._page.frames().map(frame => frame.raceAgainstEvaluationStallingEvents(async () => {
const context = frame.existingContext('utility');
const injectedScript = await context?.injectedScript();
await injectedScript?.evaluate(injected => injected.hideHighlight());
}).catch(() => {})));
}

private _resolveNow(): Promise<void> {
if (this._resolutionTimer) {
clearTimeout(this._resolutionTimer);
this._resolutionTimer = undefined;
}
this._resolutionChain = this._resolutionChain.then(() => this._resolve()).catch(() => {});
return this._resolutionChain;
}

private async _resolve() {
if (this._page.isClosed())
return;

const perFrame = new Map<Frame, { selector: ParsedSelector, cssStyle?: string }[]>();
for (const entry of this._entries.values()) {
const results = await this._resolveEntry(entry);
for (const { frame, info } of results) {
let list = perFrame.get(frame);
if (!list) {
list = [];
perFrame.set(frame, list);
}
list.push({ selector: info.parsed, cssStyle: entry.style });
}
}

await Promise.all(this._page.frames().map(async frame => {
const highlights = perFrame.get(frame) || [];
await frame.raceAgainstEvaluationStallingEvents(async () => {
const context = frame.existingContext('utility');
const injectedScript = await context?.injectedScript();
await injectedScript?.evaluate((injected, highlights) => injected.setHighlights(highlights), highlights);
}).catch(() => {});
}));

if (this._entries.size && !this._resolutionTimer && !this._page.isClosed())
this._resolutionTimer = setTimeout(() => this._resolveNow(), 1000);
}

private async _resolveEntry(entry: HighlightEntry) {
try {
return await this._page.mainFrame().selectors.resolveFramesForSelector(entry.selector, { strict: false, pierce: entry.pierce ? 'pierce' : 'default' });
} catch (error) {
if (!entry.pierce)
return [];
// Some selectors do not support piercing frames, e.g. composite ones - resolve without piercing.
return await this._page.mainFrame().selectors.resolveFramesForSelector(entry.selector, { strict: false }).catch(() => []);
}
}
}
14 changes: 7 additions & 7 deletions packages/playwright-core/src/server/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as input from './input';
import { SdkObject } from './instrumentation';
import * as js from './javascript';
import { Screenshotter, validateScreenshotOptions } from './screenshotter';
import { HighlightController } from './highlightController';
import { compressCallLog } from './callLog';
import * as rawBindingsControllerSource from '../generated/bindingsControllerSource';
import { Overlay } from './overlay';
Expand Down Expand Up @@ -186,6 +187,7 @@ export class Page extends SdkObject<PageEventMap> {
private readonly _pageBindings = new Map<string, PageBinding>();
initScripts: InitScript[] = [];
readonly screenshotter: Screenshotter;
readonly highlightController: HighlightController;
readonly frameManager: frames.FrameManager;
private _workers = new Map<string, Worker>();
readonly pdf: ((options: channels.PagePdfParams) => Promise<Buffer>) | undefined;
Expand Down Expand Up @@ -213,6 +215,7 @@ export class Page extends SdkObject<PageEventMap> {
this.mouse = new input.Mouse(delegate.rawMouse, this);
this.touchscreen = new input.Touchscreen(delegate.rawTouchscreen, this);
this.screenshotter = new Screenshotter(this);
this.highlightController = new HighlightController(this);
this.frameManager = new frames.FrameManager(this);
this.overlay = new Overlay(this);
this.screencast = new Screencast(this);
Expand Down Expand Up @@ -300,6 +303,7 @@ export class Page extends SdkObject<PageEventMap> {
this.frameManager.dispose(error);
this.screencast.dispose();
this.overlay.dispose();
this.highlightController.dispose();
this.openScope.close(error);
}

Expand Down Expand Up @@ -935,10 +939,6 @@ export class Page extends SdkObject<PageEventMap> {
}));
}

async hideHighlight() {
await Promise.all(this.frames().map(frame => frame.hideHighlight().catch(() => {})));
}

async setDockTile(image: Buffer) {
await this.delegate.setDockTile(image);
}
Expand Down Expand Up @@ -1112,13 +1112,13 @@ export class InitScript extends DisposableObject {
}
}

export async function ariaSnapshotJSONForFrame(progress: Progress, frame: frames.Frame, selector: string | undefined, options: { mode?: 'ai' | 'default', doNotRenderActive?: boolean, depth?: number, boxes?: boolean, strict?: boolean, noDefaultPierce?: boolean } = {}): Promise<AriaSnapshotJSON> {
export async function ariaSnapshotJSONForFrame(progress: Progress, frame: frames.Frame, selector: string | undefined, options: { mode?: 'ai' | 'default', doNotRenderActive?: boolean, depth?: number, boxes?: boolean, strict?: boolean, pierce?: 'default' | 'pierce' | 'no-pierce' } = {}): Promise<AriaSnapshotJSON> {
const snapshot = await frame.retryWithProgressAndTimeouts(progress, [1000, 2000, 4000, 8000], async (progress, continuePolling) => {
try {
// Note: the resolved frame might differ from the original |frame|.
// See https://developer.mozilla.org/en-US/docs/Web/API/Document/body for body/frameset explanation.
// Non-strict, because pages with nested framesets have multiple "frameset" elements.
const resolved = await progress.race(frame.selectors.callOnSelector(selector || 'body,frameset', { strict: options.strict ?? !!selector, noDefaultPierce: !selector || options.noDefaultPierce }, ({ injected, elements }, ariaOptions) => {
const resolved = await progress.race(frame.selectors.callOnSelector(selector || 'body,frameset', { strict: options.strict ?? !!selector, pierce: selector ? options.pierce : 'no-pierce' }, ({ injected, elements }, ariaOptions) => {
return injected.ariaSnapshotJSON(elements[0], ariaOptions);
}, {
mode: options.mode ?? 'default',
Expand Down Expand Up @@ -1148,7 +1148,7 @@ export async function ariaSnapshotJSONForFrame(progress: Progress, frame: frames
// Non-strict, because child frameset documents have multiple "frameset" elements.
const frameRootSelector = `aria-ref=${ref} >> internal:control=enter-frame >> body,frameset`;
try {
return await ariaSnapshotJSONForFrame(progress, snapshot.resolvedFrame, frameRootSelector, { ...options, depth: childDepth, strict: false, noDefaultPierce: true });
return await ariaSnapshotJSONForFrame(progress, snapshot.resolvedFrame, frameRootSelector, { ...options, depth: childDepth, strict: false, pierce: 'no-pierce' });
} catch {
return [];
}
Expand Down
Loading
Loading