diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index a49148e061..32cbba0982 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -125,7 +125,14 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ for (const reporter of options.reporter || []) params.append('reporter', reporter); - const urlPath = `./trace/${options.webApp || 'index.html'}?${params.toString()}`; + let baseUrl = ''; + if (process.env.PW_HMR === '1') { + params.set('testServerPort', '' + server.port()); + baseUrl = 'http://localhost:44223'; // port is hardcoded in build.js + } + + const urlPath = `${baseUrl}/trace/${options.webApp || 'index.html'}?${params.toString()}`; + server.routePath('/', (_, response) => { response.statusCode = 302; response.setHeader('Location', urlPath); diff --git a/packages/trace-viewer/.gitignore b/packages/trace-viewer/.gitignore index a547bf36d8..1e3942879c 100644 --- a/packages/trace-viewer/.gitignore +++ b/packages/trace-viewer/.gitignore @@ -22,3 +22,5 @@ dist-ssr *.njsproj *.sln *.sw? + +public/sw.bundle.js* diff --git a/packages/trace-viewer/src/embedded.tsx b/packages/trace-viewer/src/embedded.tsx index 4f1503dcf2..cc61703baf 100644 --- a/packages/trace-viewer/src/embedded.tsx +++ b/packages/trace-viewer/src/embedded.tsx @@ -45,7 +45,7 @@ import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader'; if (window.location.protocol !== 'file:') { if (!navigator.serviceWorker) throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`); - navigator.serviceWorker.register('sw.bundle.js'); + navigator.serviceWorker.register('sw.bundle.js' + window.location.search); if (!navigator.serviceWorker.controller) { await new Promise(f => { navigator.serviceWorker.oncontrollerchange = () => f(); diff --git a/packages/trace-viewer/src/index.tsx b/packages/trace-viewer/src/index.tsx index a737d9017f..2296cb0090 100644 --- a/packages/trace-viewer/src/index.tsx +++ b/packages/trace-viewer/src/index.tsx @@ -27,7 +27,7 @@ import { WorkbenchLoader } from './ui/workbenchLoader'; await new Promise(f => setTimeout(f, 1000)); if (!navigator.serviceWorker) throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`); - navigator.serviceWorker.register('sw.bundle.js'); + navigator.serviceWorker.register('sw.bundle.js' + window.location.search); if (!navigator.serviceWorker.controller) { await new Promise(f => { navigator.serviceWorker.oncontrollerchange = () => f(); diff --git a/packages/trace-viewer/src/recorder.tsx b/packages/trace-viewer/src/recorder.tsx index 5e6b9764e3..6239df86ab 100644 --- a/packages/trace-viewer/src/recorder.tsx +++ b/packages/trace-viewer/src/recorder.tsx @@ -26,7 +26,7 @@ import { RecorderView } from './ui/recorder/recorderView'; if (window.location.protocol !== 'file:') { if (!navigator.serviceWorker) throw new Error(`Service workers are not supported.\nMake sure to serve the Recorder (${window.location}) via HTTPS or localhost.`); - navigator.serviceWorker.register('sw.bundle.js'); + navigator.serviceWorker.register('sw.bundle.js' + window.location.search); if (!navigator.serviceWorker.controller) { await new Promise(f => { navigator.serviceWorker.oncontrollerchange = () => f(); diff --git a/packages/trace-viewer/src/sw/traceModelBackends.ts b/packages/trace-viewer/src/sw/traceModelBackends.ts index 19c5fc2dee..4f5bd49624 100644 --- a/packages/trace-viewer/src/sw/traceModelBackends.ts +++ b/packages/trace-viewer/src/sw/traceModelBackends.ts @@ -30,9 +30,8 @@ export class ZipTraceModelBackend implements TraceModelBackend { constructor(traceURL: string, progress: Progress) { this._traceURL = traceURL; - zipjs.configure({ baseURL: self.location.href } as any); this._zipReader = new zipjs.ZipReader( - new zipjs.HttpReader(formatUrl(traceURL), { mode: 'cors', preventHeadRequest: true } as any), + new zipjs.HttpReader(formatTraceFileUrl(traceURL), { mode: 'cors', preventHeadRequest: true } as any), { useWebWorkers: false }); this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => { const map = new Map(); @@ -87,7 +86,7 @@ export class FetchTraceModelBackend implements TraceModelBackend { constructor(traceURL: string) { this._traceURL = traceURL; - this._entriesPromise = fetch('/trace/file?path=' + encodeURIComponent(traceURL)).then(async response => { + this._entriesPromise = fetch(formatTraceFileUrl(traceURL)).then(async response => { const json = JSON.parse(await response.text()); const entries = new Map(); for (const entry of json.entries) @@ -129,14 +128,22 @@ export class FetchTraceModelBackend implements TraceModelBackend { const fileName = entries.get(entryName); if (!fileName) return; - return fetch('/trace/file?path=' + encodeURIComponent(fileName)); + + return fetch(formatTraceFileUrl(fileName)); } } -function formatUrl(trace: string) { - let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `file?path=${encodeURIComponent(trace)}`; - // Dropbox does not support cors. - if (url.startsWith('https://www.dropbox.com/')) - url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length); - return url; +const baseURL = new URL(self.location.href); +baseURL.port = baseURL.searchParams.get('testServerPort') ?? baseURL.port; + +function formatTraceFileUrl(trace: string) { + if (trace.startsWith('https://www.dropbox.com/')) + return 'https://dl.dropboxusercontent.com/' + trace.substring('https://www.dropbox.com/'.length); + + if (trace.startsWith('http') || trace.startsWith('blob')) + return trace; + + const url = new URL('/trace/file', baseURL); + url.searchParams.set('path', trace); + return url.toString(); } diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 69a5988641..37b6a6bcf6 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -50,6 +50,7 @@ const searchParams = new URLSearchParams(window.location.search); const guid = searchParams.get('ws'); const wsURL = new URL(`../${guid}`, window.location.toString()); wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); +wsURL.port = searchParams.get('testServerPort') ?? window.location.port; const queryParams = { args: searchParams.getAll('arg'), grep: searchParams.get('grep') || undefined, @@ -68,6 +69,7 @@ const isMac = navigator.platform === 'MacIntel'; export const UIModeView: React.FC<{}> = ({ }) => { + const isJokesDay = new Date().getMonth() === 3 && new Date().getDate() === 1; const [filterText, setFilterText] = React.useState(''); const [isShowingOutput, setIsShowingOutput] = React.useState(false); const [outputContainsError, setOutputContainsError] = React.useState(false); @@ -440,7 +442,7 @@ export const UIModeView: React.FC<{}> = ({ sidebar={
Playwright logo -
Playwright
+
{isJokesDay ? 'Claywright' : 'Playwright'}
reloadTests()} disabled={isRunningTest || isLoading}>
{ setIsShowingOutput(!isShowingOutput); }} /> @@ -516,10 +518,11 @@ export const UIModeView: React.FC<{}> = ({ style={{ marginLeft: 5 }} title={settingsVisible ? 'Hide Settings' : 'Show Settings'} /> -
Settings
+
{isJokesDay ? 'Schmettings' : 'Settings'}
{settingsVisible && }
} diff --git a/packages/trace-viewer/src/uiMode.tsx b/packages/trace-viewer/src/uiMode.tsx index 5dac2082e8..4702621e5d 100644 --- a/packages/trace-viewer/src/uiMode.tsx +++ b/packages/trace-viewer/src/uiMode.tsx @@ -27,7 +27,7 @@ import { UIModeView } from './ui/uiModeView'; await new Promise(f => setTimeout(f, 1000)); if (!navigator.serviceWorker) throw new Error(`Service workers are not supported.\nMake sure to serve the website (${window.location}) via HTTPS or localhost.`); - navigator.serviceWorker.register('sw.bundle.js'); + navigator.serviceWorker.register('sw.bundle.js' + window.location.search); if (!navigator.serviceWorker.controller) { await new Promise(f => { navigator.serviceWorker.oncontrollerchange = () => f(); diff --git a/packages/trace-viewer/vite.config.ts b/packages/trace-viewer/vite.config.ts index 0e2e9cb642..541b1ab6b6 100644 --- a/packages/trace-viewer/vite.config.ts +++ b/packages/trace-viewer/vite.config.ts @@ -26,6 +26,9 @@ export default defineConfig({ react(), bundle() ], + define: { + 'process.env': {}, + }, resolve: { alias: { '@injected': path.resolve(__dirname, '../playwright-core/src/server/injected'), diff --git a/packages/trace-viewer/vite.sw.config.ts b/packages/trace-viewer/vite.sw.config.ts index dc621448b9..a0dd99036a 100644 --- a/packages/trace-viewer/vite.sw.config.ts +++ b/packages/trace-viewer/vite.sw.config.ts @@ -36,7 +36,7 @@ export default defineConfig({ }, }, build: { - outDir: path.resolve(__dirname, '../playwright-core/lib/vite/traceViewer'), + outDir: path.resolve(__dirname, 'public'), // Output dir is shared with vite.config.ts, clearing it here is racy. emptyOutDir: false, rollupOptions: { diff --git a/tests/playwright-test/ui-mode-test-run.spec.ts b/tests/playwright-test/ui-mode-test-run.spec.ts index 3673faab45..0da2940e96 100644 --- a/tests/playwright-test/ui-mode-test-run.spec.ts +++ b/tests/playwright-test/ui-mode-test-run.spec.ts @@ -778,3 +778,14 @@ test('should respect --ignore-snapshots option', { - treeitem ${/\[icon-check\] snapshot \d+ms/} `); }); + +test('should show funny messages', async ({ runUITest }) => { + const { page } = await runUITest(basicTestTree); + await page.clock.setFixedTime('2025-04-01'); + + await expect(page.getByText('Claywright')).toBeVisible(); + const schmettingsHeader = page.getByText('Schmettings'); + await expect(schmettingsHeader).toBeVisible(); + await schmettingsHeader.click(); + await expect(page.getByRole('checkbox', { name: 'Fart mode' })).toBeVisible(); +}); \ No newline at end of file diff --git a/utils/build/build.js b/utils/build/build.js index 7a289bd5ed..a1ba052d51 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -275,6 +275,21 @@ for (const bundle of bundles) { }); } +// initial service worker build. +steps.push({ + command: 'npx', + args: [ + 'vite', + '--config', + 'vite.sw.config.ts', + 'build', + ...(withSourceMaps ? ['--sourcemap=inline'] : []), + ], + shell: true, + cwd: path.join(__dirname, '..', '..', 'packages', 'trace-viewer'), + concurrent: false, +}); + // Build/watch web packages. for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) { steps.push({ @@ -290,6 +305,7 @@ for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) { concurrent: true, }); } + // Build/watch trace viewer service worker. steps.push({ command: 'npx', @@ -306,6 +322,16 @@ steps.push({ concurrent: true, }); +// web packages dev server +if (watchMode) { + steps.push({ + command: 'npx', + args: ['vite', '--port', '44223', '--base', '/trace/'], + shell: true, + cwd: path.join(__dirname, '..', '..', 'packages', 'trace-viewer'), + concurrent: true, + }); +} // Generate injected. onChanges.push({