From c6644f92bd882a0dabfff90e80b15bda742addae Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Wed, 5 Feb 2025 10:38:20 -0800 Subject: [PATCH] feat(recorder): display primary page URL in recorder title --- .../playwright-core/src/server/recorder.ts | 26 +++++++++ .../src/server/recorder/recorderApp.ts | 7 +++ .../src/server/recorder/recorderFrontend.ts | 1 + packages/recorder/src/main.tsx | 39 ++++++++------ packages/recorder/src/recorderTypes.d.ts | 1 + tests/library/inspector/title.spec.ts | 53 +++++++++++++++++++ 6 files changed, 112 insertions(+), 15 deletions(-) create mode 100644 tests/library/inspector/title.spec.ts diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index 59705c2eb4..e7d7ea01d4 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -32,6 +32,7 @@ import type * as actions from '@recorder/actions'; import { stringifySelector } from '../utils/isomorphic/selectorParser'; import type { Frame } from './frames'; import type { AriaTemplateNode } from '@isomorphic/ariaSnapshot'; +import { Page } from './page'; const recorderSymbol = Symbol('recorderSymbol'); @@ -148,6 +149,31 @@ export class Recorder implements InstrumentationListener, IRecorder { this._context.instrumentation.removeListener(this); this._recorderApp?.close().catch(() => {}); }); + + // Report the URL of the first page in the context + let primaryPage: Page | undefined; + const registerNavigationListener = (page: Page) => { + if (primaryPage === undefined) { + primaryPage = page; + recorderApp.setBasePageURL(page.mainFrame().url()); + } + + page.on(Page.Events.InternalFrameNavigatedToNewDocument, (frame: Frame) => { + if (page === primaryPage && page.mainFrame() === frame) + recorderApp.setBasePageURL(frame.url()); + }); + page.on(Page.Events.Close, () => { + if (page === primaryPage) { + primaryPage = this._context.pages()[0]; + if (primaryPage) + recorderApp.setBasePageURL(primaryPage.mainFrame().url()); + } + }); + }; + for (const page of this._context.pages()) + registerNavigationListener(page); + this._context.on(BrowserContext.Events.Page, registerNavigationListener); + this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => { this._recorderSources = data.sources; recorderApp.setActions(data.actions, data.sources); diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index f8971531cf..6842c73730 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -39,6 +39,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { async updateCallLogs(callLogs: CallLog[]): Promise {} async setSources(sources: Source[]): Promise {} async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise {} + async setBasePageURL(url: string): Promise {} } export class RecorderApp extends EventEmitter implements IRecorderApp { @@ -158,6 +159,12 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise { } + async setBasePageURL(url: string): Promise { + await this._page.mainFrame().evaluateExpression(((url: string) => { + window.playwrightSetBasePageURL(url); + }).toString(), { isFunction: true }, url).catch(() => {}); + } + async elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise { if (userGesture) this._page.bringToFront(); diff --git a/packages/playwright-core/src/server/recorder/recorderFrontend.ts b/packages/playwright-core/src/server/recorder/recorderFrontend.ts index b3dc0daad9..acf884696c 100644 --- a/packages/playwright-core/src/server/recorder/recorderFrontend.ts +++ b/packages/playwright-core/src/server/recorder/recorderFrontend.ts @@ -34,6 +34,7 @@ export interface IRecorderApp extends EventEmitter { updateCallLogs(callLogs: CallLog[]): Promise; setSources(sources: Source[]): Promise; setActions(actions: actions.ActionInContext[], sources: Source[]): Promise; + setBasePageURL(url: string): Promise; } export type IRecorderAppFactory = (recorder: IRecorder) => Promise; diff --git a/packages/recorder/src/main.tsx b/packages/recorder/src/main.tsx index 61ac9da67f..f94b468464 100644 --- a/packages/recorder/src/main.tsx +++ b/packages/recorder/src/main.tsx @@ -26,22 +26,31 @@ export const Main: React.FC = ({ const [log, setLog] = React.useState(new Map()); const [mode, setMode] = React.useState('none'); - window.playwrightSetMode = setMode; - window.playwrightSetSources = React.useCallback((sources: Source[]) => { - setSources(sources); - window.playwrightSourcesEchoForTest = sources; + React.useLayoutEffect(() => { + window.playwrightSetMode = setMode; + window.playwrightSetSources = (sources: Source[]) => { + setSources(sources); + window.playwrightSourcesEchoForTest = sources; + }; + window.playwrightSetPaused = setPaused; + window.playwrightUpdateLogs = callLogs => { + setLog(log => { + const newLog = new Map(log); + for (const callLog of callLogs) { + callLog.reveal = !log.has(callLog.id); + newLog.set(callLog.id, callLog); + } + return newLog; + }); + }; + window.playwrightSetBasePageURL = (url: string) => { + if (url) + document.title = `Playwright Inspector - ${url}`; + else + document.title = `Playwright Inspector`; + }; }, []); - window.playwrightSetPaused = setPaused; - window.playwrightUpdateLogs = callLogs => { - setLog(log => { - const newLog = new Map(log); - for (const callLog of callLogs) { - callLog.reveal = !log.has(callLog.id); - newLog.set(callLog.id, callLog); - } - return newLog; - }); - }; + return ; }; diff --git a/packages/recorder/src/recorderTypes.d.ts b/packages/recorder/src/recorderTypes.d.ts index 22561608ae..9157e92e87 100644 --- a/packages/recorder/src/recorderTypes.d.ts +++ b/packages/recorder/src/recorderTypes.d.ts @@ -103,6 +103,7 @@ declare global { playwrightSetPaused: (paused: boolean) => void; playwrightSetSources: (sources: Source[]) => void; playwrightSetOverlayVisible: (visible: boolean) => void; + playwrightSetBasePageURL: (url: string) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void; playwrightSetRunningFile: (file: string | undefined) => void; playwrightElementPicked: (elementInfo: ElementInfo, userGesture?: boolean) => void; diff --git a/tests/library/inspector/title.spec.ts b/tests/library/inspector/title.spec.ts new file mode 100644 index 0000000000..cdb259bf95 --- /dev/null +++ b/tests/library/inspector/title.spec.ts @@ -0,0 +1,53 @@ +/** + * 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 { test, expect } from './inspectorTest'; + +test('should reflect formatted URL of the page', async ({ openRecorder, server }) => { + const { recorder } = await openRecorder(); + await recorder.setContentAndWait(''); + await expect(await recorder.recorderPage.title()).toBe('Playwright Inspector - about:blank'); + + await recorder.setContentAndWait('', server.EMPTY_PAGE); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.EMPTY_PAGE}`); +}); + +test('should update primary page URL when original primary closes', async ({ context, openRecorder, server }) => { + const { recorder } = await openRecorder(); + await recorder.setContentAndWait('', `${server.PREFIX}/background-color.html`); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.PREFIX}/background-color.html`); + + const page2 = await context.newPage(); + await page2.goto(`${server.PREFIX}/empty.html`); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.PREFIX}/background-color.html`); + + const page3 = await context.newPage(); + await page3.goto(`${server.PREFIX}/dom.html`); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.PREFIX}/background-color.html`); + + const page4 = await context.newPage(); + await page4.goto(`${server.PREFIX}/grid.html`); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.PREFIX}/background-color.html`); + + await page2.close(); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.PREFIX}/background-color.html`); + + await recorder.page.close(); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.PREFIX}/dom.html`); + + await page3.close(); + await expect(await recorder.recorderPage.title()).toBe(`Playwright Inspector - ${server.PREFIX}/grid.html`); +});