From 4bc8cf0d4790edb7137db403a19c4a3510d35bf9 Mon Sep 17 00:00:00 2001 From: Adam Gastineau Date: Fri, 7 Feb 2025 12:05:04 -0800 Subject: [PATCH] feat(recorder): display primary page URL in recorder title (#34637) --- .../playwright-core/src/server/recorder.ts | 5 +- .../src/server/recorder/recorderApp.ts | 10 +-- .../src/server/recorder/recorderFrontend.ts | 2 +- packages/recorder/src/main.tsx | 38 +++++---- packages/recorder/src/recorderTypes.d.ts | 2 +- tests/library/inspector/title.spec.ts | 83 +++++++++++++++++++ 6 files changed, 115 insertions(+), 25 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 59705c2eb4832..9fbe4962d2ea7 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 type { Page } from './page'; const recorderSymbol = Symbol('recorderSymbol'); @@ -148,6 +149,7 @@ export class Recorder implements InstrumentationListener, IRecorder { this._context.instrumentation.removeListener(this); this._recorderApp?.close().catch(() => {}); }); + this._contextRecorder.on(ContextRecorder.Events.Change, (data: { sources: Source[], actions: actions.ActionInContext[] }) => { this._recorderSources = data.sources; recorderApp.setActions(data.actions, data.sources); @@ -346,7 +348,8 @@ export class Recorder implements InstrumentationListener, IRecorder { } private _pushAllSources() { - this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()]); + const primaryPage: Page | undefined = this._context.pages()[0]; + this._recorderApp?.setSources([...this._recorderSources, ...this._userSources.values()], primaryPage?.mainFrame().url()); } async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata) { diff --git a/packages/playwright-core/src/server/recorder/recorderApp.ts b/packages/playwright-core/src/server/recorder/recorderApp.ts index f8971531cf81e..05ac4cd0dd3dd 100644 --- a/packages/playwright-core/src/server/recorder/recorderApp.ts +++ b/packages/playwright-core/src/server/recorder/recorderApp.ts @@ -37,7 +37,7 @@ export class EmptyRecorderApp extends EventEmitter implements IRecorderApp { async setRunningFile(file: string | undefined): Promise {} async elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise {} async updateCallLogs(callLogs: CallLog[]): Promise {} - async setSources(sources: Source[]): Promise {} + async setSources(sources: Source[], primaryPageURL: string | undefined): Promise {} async setActions(actions: actions.ActionInContext[], sources: Source[]): Promise {} } @@ -143,10 +143,10 @@ export class RecorderApp extends EventEmitter implements IRecorderApp { }).toString(), { isFunction: true }, paused).catch(() => {}); } - async setSources(sources: Source[]): Promise { - await this._page.mainFrame().evaluateExpression(((sources: Source[]) => { - window.playwrightSetSources(sources); - }).toString(), { isFunction: true }, sources).catch(() => {}); + async setSources(sources: Source[], primaryPageURL: string | undefined): Promise { + await this._page.mainFrame().evaluateExpression((({ sources, primaryPageURL }: { sources: Source[], primaryPageURL: string | undefined }) => { + window.playwrightSetSources(sources, primaryPageURL); + }).toString(), { isFunction: true }, { sources, primaryPageURL }).catch(() => {}); // Testing harness for runCLI mode. if (process.env.PWTEST_CLI_IS_UNDER_TEST && sources.length) { diff --git a/packages/playwright-core/src/server/recorder/recorderFrontend.ts b/packages/playwright-core/src/server/recorder/recorderFrontend.ts index b3dc0daad9bd8..c6fb6e0a1cc2f 100644 --- a/packages/playwright-core/src/server/recorder/recorderFrontend.ts +++ b/packages/playwright-core/src/server/recorder/recorderFrontend.ts @@ -32,7 +32,7 @@ export interface IRecorderApp extends EventEmitter { setRunningFile(file: string | undefined): Promise; elementPicked(elementInfo: ElementInfo, userGesture?: boolean): Promise; updateCallLogs(callLogs: CallLog[]): Promise; - setSources(sources: Source[]): Promise; + setSources(sources: Source[], primaryPageURL: string | undefined): Promise; setActions(actions: actions.ActionInContext[], sources: Source[]): Promise; } diff --git a/packages/recorder/src/main.tsx b/packages/recorder/src/main.tsx index 61ac9da67fb7e..4ecfc977600ab 100644 --- a/packages/recorder/src/main.tsx +++ b/packages/recorder/src/main.tsx @@ -19,29 +19,33 @@ import * as React from 'react'; import { Recorder } from './recorder'; import './recorder.css'; -export const Main: React.FC = ({ -}) => { +export const Main: React.FC = ({}) => { const [sources, setSources] = React.useState([]); const [paused, setPaused] = React.useState(false); 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, primaryPageURL) => { + setSources(sources); + window.playwrightSourcesEchoForTest = sources; + document.title = primaryPageURL + ? `Playwright Inspector - ${primaryPageURL}` + : `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; + }); + }; }, []); - 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 22561608aed97..ae4c41f4f7fc8 100644 --- a/packages/recorder/src/recorderTypes.d.ts +++ b/packages/recorder/src/recorderTypes.d.ts @@ -101,7 +101,7 @@ declare global { interface Window { playwrightSetMode: (mode: Mode) => void; playwrightSetPaused: (paused: boolean) => void; - playwrightSetSources: (sources: Source[]) => void; + playwrightSetSources: (sources: Source[], primaryPageURL: string | undefined) => void; playwrightSetOverlayVisible: (visible: boolean) => void; playwrightUpdateLogs: (callLogs: CallLog[]) => void; playwrightSetRunningFile: (file: string | undefined) => void; diff --git a/tests/library/inspector/title.spec.ts b/tests/library/inspector/title.spec.ts new file mode 100644 index 0000000000000..edd73be21ab98 --- /dev/null +++ b/tests/library/inspector/title.spec.ts @@ -0,0 +1,83 @@ +/** + * 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(recorder.recorderPage).toHaveTitle( + 'Playwright Inspector - about:blank', + ); + + await recorder.setContentAndWait('', server.EMPTY_PAGE); + await expect(recorder.recorderPage).toHaveTitle( + `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(recorder.recorderPage).toHaveTitle( + `Playwright Inspector - ${server.PREFIX}/background-color.html`, + ); + + const page2 = await context.newPage(); + await page2.goto(`${server.PREFIX}/empty.html`); + await expect(recorder.recorderPage).toHaveTitle( + `Playwright Inspector - ${server.PREFIX}/background-color.html`, + ); + + const page3 = await context.newPage(); + await page3.goto(`${server.PREFIX}/dom.html`); + await expect(recorder.recorderPage).toHaveTitle( + `Playwright Inspector - ${server.PREFIX}/background-color.html`, + ); + + const page4 = await context.newPage(); + await page4.goto(`${server.PREFIX}/grid.html`); + await expect(recorder.recorderPage).toHaveTitle( + `Playwright Inspector - ${server.PREFIX}/background-color.html`, + ); + + await page2.close(); + await expect(recorder.recorderPage).toHaveTitle( + `Playwright Inspector - ${server.PREFIX}/background-color.html`, + ); + + await recorder.page.close(); + // URL will not update without performing some action + await page3.getByRole('checkbox').click(); + await expect(recorder.recorderPage).toHaveTitle( + `Playwright Inspector - ${server.PREFIX}/dom.html`, + ); + + await page3.close(); + await expect(recorder.recorderPage).toHaveTitle( + `Playwright Inspector - ${server.PREFIX}/grid.html`, + ); +});