chore: HMR for Trace Viewer (#33228)
This commit is contained in:
parent
2e01154bb5
commit
3641e5984f
|
@ -125,7 +125,14 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
|
||||||
for (const reporter of options.reporter || [])
|
for (const reporter of options.reporter || [])
|
||||||
params.append('reporter', 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) => {
|
server.routePath('/', (_, response) => {
|
||||||
response.statusCode = 302;
|
response.statusCode = 302;
|
||||||
response.setHeader('Location', urlPath);
|
response.setHeader('Location', urlPath);
|
||||||
|
|
|
@ -22,3 +22,5 @@ dist-ssr
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
|
public/sw.bundle.js*
|
||||||
|
|
|
@ -45,7 +45,7 @@ import { EmbeddedWorkbenchLoader } from './ui/embeddedWorkbenchLoader';
|
||||||
if (window.location.protocol !== 'file:') {
|
if (window.location.protocol !== 'file:') {
|
||||||
if (!navigator.serviceWorker)
|
if (!navigator.serviceWorker)
|
||||||
throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);
|
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) {
|
if (!navigator.serviceWorker.controller) {
|
||||||
await new Promise<void>(f => {
|
await new Promise<void>(f => {
|
||||||
navigator.serviceWorker.oncontrollerchange = () => f();
|
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { WorkbenchLoader } from './ui/workbenchLoader';
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
await new Promise(f => setTimeout(f, 1000));
|
||||||
if (!navigator.serviceWorker)
|
if (!navigator.serviceWorker)
|
||||||
throw new Error(`Service workers are not supported.\nMake sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);
|
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) {
|
if (!navigator.serviceWorker.controller) {
|
||||||
await new Promise<void>(f => {
|
await new Promise<void>(f => {
|
||||||
navigator.serviceWorker.oncontrollerchange = () => f();
|
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { RecorderView } from './ui/recorder/recorderView';
|
||||||
if (window.location.protocol !== 'file:') {
|
if (window.location.protocol !== 'file:') {
|
||||||
if (!navigator.serviceWorker)
|
if (!navigator.serviceWorker)
|
||||||
throw new Error(`Service workers are not supported.\nMake sure to serve the Recorder (${window.location}) via HTTPS or localhost.`);
|
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) {
|
if (!navigator.serviceWorker.controller) {
|
||||||
await new Promise<void>(f => {
|
await new Promise<void>(f => {
|
||||||
navigator.serviceWorker.oncontrollerchange = () => f();
|
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||||
|
|
|
@ -30,9 +30,8 @@ export class ZipTraceModelBackend implements TraceModelBackend {
|
||||||
|
|
||||||
constructor(traceURL: string, progress: Progress) {
|
constructor(traceURL: string, progress: Progress) {
|
||||||
this._traceURL = traceURL;
|
this._traceURL = traceURL;
|
||||||
zipjs.configure({ baseURL: self.location.href } as any);
|
|
||||||
this._zipReader = new zipjs.ZipReader(
|
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 });
|
{ useWebWorkers: false });
|
||||||
this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => {
|
this._entriesPromise = this._zipReader.getEntries({ onprogress: progress }).then(entries => {
|
||||||
const map = new Map<string, zip.Entry>();
|
const map = new Map<string, zip.Entry>();
|
||||||
|
@ -87,7 +86,7 @@ export class FetchTraceModelBackend implements TraceModelBackend {
|
||||||
|
|
||||||
constructor(traceURL: string) {
|
constructor(traceURL: string) {
|
||||||
this._traceURL = traceURL;
|
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 json = JSON.parse(await response.text());
|
||||||
const entries = new Map<string, string>();
|
const entries = new Map<string, string>();
|
||||||
for (const entry of json.entries)
|
for (const entry of json.entries)
|
||||||
|
@ -129,14 +128,22 @@ export class FetchTraceModelBackend implements TraceModelBackend {
|
||||||
const fileName = entries.get(entryName);
|
const fileName = entries.get(entryName);
|
||||||
if (!fileName)
|
if (!fileName)
|
||||||
return;
|
return;
|
||||||
return fetch('/trace/file?path=' + encodeURIComponent(fileName));
|
|
||||||
|
return fetch(formatTraceFileUrl(fileName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatUrl(trace: string) {
|
const baseURL = new URL(self.location.href);
|
||||||
let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `file?path=${encodeURIComponent(trace)}`;
|
baseURL.port = baseURL.searchParams.get('testServerPort') ?? baseURL.port;
|
||||||
// Dropbox does not support cors.
|
|
||||||
if (url.startsWith('https://www.dropbox.com/'))
|
function formatTraceFileUrl(trace: string) {
|
||||||
url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length);
|
if (trace.startsWith('https://www.dropbox.com/'))
|
||||||
return url;
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ const searchParams = new URLSearchParams(window.location.search);
|
||||||
const guid = searchParams.get('ws');
|
const guid = searchParams.get('ws');
|
||||||
const wsURL = new URL(`../${guid}`, window.location.toString());
|
const wsURL = new URL(`../${guid}`, window.location.toString());
|
||||||
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
|
||||||
|
wsURL.port = searchParams.get('testServerPort') ?? window.location.port;
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
args: searchParams.getAll('arg'),
|
args: searchParams.getAll('arg'),
|
||||||
grep: searchParams.get('grep') || undefined,
|
grep: searchParams.get('grep') || undefined,
|
||||||
|
@ -68,6 +69,7 @@ const isMac = navigator.platform === 'MacIntel';
|
||||||
|
|
||||||
export const UIModeView: React.FC<{}> = ({
|
export const UIModeView: React.FC<{}> = ({
|
||||||
}) => {
|
}) => {
|
||||||
|
const isJokesDay = new Date().getMonth() === 3 && new Date().getDate() === 1;
|
||||||
const [filterText, setFilterText] = React.useState<string>('');
|
const [filterText, setFilterText] = React.useState<string>('');
|
||||||
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
|
const [isShowingOutput, setIsShowingOutput] = React.useState<boolean>(false);
|
||||||
const [outputContainsError, setOutputContainsError] = React.useState(false);
|
const [outputContainsError, setOutputContainsError] = React.useState(false);
|
||||||
|
@ -440,7 +442,7 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
sidebar={<div className='vbox ui-mode-sidebar'>
|
sidebar={<div className='vbox ui-mode-sidebar'>
|
||||||
<Toolbar noShadow={true} noMinHeight={true}>
|
<Toolbar noShadow={true} noMinHeight={true}>
|
||||||
<img src='playwright-logo.svg' alt='Playwright logo' />
|
<img src='playwright-logo.svg' alt='Playwright logo' />
|
||||||
<div className='section-title'>Playwright</div>
|
<div className='section-title'>{isJokesDay ? 'Claywright' : 'Playwright'}</div>
|
||||||
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
<ToolbarButton icon='refresh' title='Reload' onClick={() => reloadTests()} disabled={isRunningTest || isLoading}></ToolbarButton>
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<ToolbarButton icon={'terminal'} title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
<ToolbarButton icon={'terminal'} title={'Toggle output — ' + (isMac ? '⌃`' : 'Ctrl + `')} toggled={isShowingOutput} onClick={() => { setIsShowingOutput(!isShowingOutput); }} />
|
||||||
|
@ -516,10 +518,11 @@ export const UIModeView: React.FC<{}> = ({
|
||||||
style={{ marginLeft: 5 }}
|
style={{ marginLeft: 5 }}
|
||||||
title={settingsVisible ? 'Hide Settings' : 'Show Settings'}
|
title={settingsVisible ? 'Hide Settings' : 'Show Settings'}
|
||||||
/>
|
/>
|
||||||
<div className='section-title'>Settings</div>
|
<div className='section-title' data-testid='settings-title'>{isJokesDay ? 'Schmettings' : 'Settings'}</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
{settingsVisible && <SettingsView settings={[
|
{settingsVisible && <SettingsView settings={[
|
||||||
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
|
{ value: darkMode, set: setDarkMode, title: 'Dark mode' },
|
||||||
|
...(isJokesDay ? [{ value: darkMode, set: setDarkMode, title: 'Fart mode' }] : [])
|
||||||
]} />}
|
]} />}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import { UIModeView } from './ui/uiModeView';
|
||||||
await new Promise(f => setTimeout(f, 1000));
|
await new Promise(f => setTimeout(f, 1000));
|
||||||
if (!navigator.serviceWorker)
|
if (!navigator.serviceWorker)
|
||||||
throw new Error(`Service workers are not supported.\nMake sure to serve the website (${window.location}) via HTTPS or localhost.`);
|
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) {
|
if (!navigator.serviceWorker.controller) {
|
||||||
await new Promise<void>(f => {
|
await new Promise<void>(f => {
|
||||||
navigator.serviceWorker.oncontrollerchange = () => f();
|
navigator.serviceWorker.oncontrollerchange = () => f();
|
||||||
|
|
|
@ -26,6 +26,9 @@ export default defineConfig({
|
||||||
react(),
|
react(),
|
||||||
bundle()
|
bundle()
|
||||||
],
|
],
|
||||||
|
define: {
|
||||||
|
'process.env': {},
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@injected': path.resolve(__dirname, '../playwright-core/src/server/injected'),
|
'@injected': path.resolve(__dirname, '../playwright-core/src/server/injected'),
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
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.
|
// Output dir is shared with vite.config.ts, clearing it here is racy.
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
|
|
|
@ -778,3 +778,14 @@ test('should respect --ignore-snapshots option', {
|
||||||
- treeitem ${/\[icon-check\] snapshot \d+ms/}
|
- 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();
|
||||||
|
});
|
|
@ -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.
|
// Build/watch web packages.
|
||||||
for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) {
|
for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) {
|
||||||
steps.push({
|
steps.push({
|
||||||
|
@ -290,6 +305,7 @@ for (const webPackage of ['html-reporter', 'recorder', 'trace-viewer']) {
|
||||||
concurrent: true,
|
concurrent: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build/watch trace viewer service worker.
|
// Build/watch trace viewer service worker.
|
||||||
steps.push({
|
steps.push({
|
||||||
command: 'npx',
|
command: 'npx',
|
||||||
|
@ -306,6 +322,16 @@ steps.push({
|
||||||
concurrent: true,
|
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.
|
// Generate injected.
|
||||||
onChanges.push({
|
onChanges.push({
|
||||||
|
|
Loading…
Reference in New Issue