feat(trace viewer): popout snapshot in a new tab (#20475)
This commit is contained in:
parent
a03f3223c4
commit
b39079b51e
|
@ -38,6 +38,8 @@ export async function showTraceViewer(traceUrls: string[], browserName: string,
|
|||
server.routePrefix('/trace', (request, response) => {
|
||||
const url = new URL('http://localhost' + request.url!);
|
||||
const relativePath = url.pathname.slice('/trace'.length);
|
||||
if (relativePath.endsWith('/stall.js'))
|
||||
return true;
|
||||
if (relativePath.startsWith('/file')) {
|
||||
try {
|
||||
return server.serveFile(request, response, url.searchParams.get('path')!);
|
||||
|
|
|
@ -181,6 +181,8 @@ export function startHtmlReportServer(folder: string): HttpServer {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
if (relativePath.endsWith('/stall.js'))
|
||||
return true;
|
||||
if (relativePath === '/')
|
||||
relativePath = '/index.html';
|
||||
const absolutePath = path.join(folder, ...relativePath.split('/'));
|
||||
|
|
|
@ -31,7 +31,8 @@ export function bundle() {
|
|||
transform(html, ctx) {
|
||||
if (!ctx || !ctx.bundle)
|
||||
return html;
|
||||
return html.replace(/(?=<!--)([\s\S]*?)-->/, '');
|
||||
// Workaround vite issue that we cannot exclude some scripts from preprocessing.
|
||||
return html.replace(/(?=<!--)([\s\S]*?)-->/, '').replace('<!-- <script src="stall.js"></script> -->', '<script src="stall.js"></script>');
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<!--
|
||||
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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<body>
|
||||
<script>
|
||||
(async () => {
|
||||
navigator.serviceWorker.register('sw.bundle.js');
|
||||
if (!navigator.serviceWorker.controller)
|
||||
await new Promise(f => navigator.serviceWorker.oncontrollerchange = f);
|
||||
const traceUrl = new URL(location.href).searchParams.get('trace');
|
||||
const params = new URLSearchParams();
|
||||
params.set('trace', traceUrl);
|
||||
await fetch('context?' + params.toString()).then(r => r.json());
|
||||
await location.reload();
|
||||
})();
|
||||
</script>
|
||||
<!-- <script src="stall.js"></script> -->
|
||||
</body>
|
||||
</html>
|
|
@ -180,7 +180,7 @@ function snapshotNodes(snapshot: FrameSnapshot): NodeSnapshot[] {
|
|||
}
|
||||
|
||||
function snapshotScript() {
|
||||
function applyPlaywrightAttributes() {
|
||||
function applyPlaywrightAttributes(unwrapPopoutUrl: (url: string) => string) {
|
||||
const scrollTops: Element[] = [];
|
||||
const scrollLefts: Element[] = [];
|
||||
|
||||
|
@ -210,7 +210,7 @@ function snapshotScript() {
|
|||
iframe.setAttribute('src', 'data:text/html,<body style="background: #ddd"></body>');
|
||||
} else {
|
||||
// Append query parameters to inherit ?name= or ?time= values from parent.
|
||||
const url = new URL(window.location.href);
|
||||
const url = new URL(unwrapPopoutUrl(window.location.href));
|
||||
url.searchParams.delete('pointX');
|
||||
url.searchParams.delete('pointY');
|
||||
// We can be loading iframe from within iframe, reset base to be absolute.
|
||||
|
@ -278,7 +278,7 @@ function snapshotScript() {
|
|||
window.addEventListener('DOMContentLoaded', onDOMContentLoaded);
|
||||
}
|
||||
|
||||
return `\n(${applyPlaywrightAttributes.toString()})()`;
|
||||
return `\n(${applyPlaywrightAttributes.toString()})(${unwrapPopoutUrl.toString()})`;
|
||||
}
|
||||
|
||||
|
||||
|
@ -329,3 +329,11 @@ function rewriteURLsInStyleSheetForCustomProtocol(text: string): string {
|
|||
return match.replace(protocol + '//', `https://pw-${protocol.slice(0, -1)}--`);
|
||||
});
|
||||
}
|
||||
|
||||
// <base>/popout.html?r=<snapshotUrl> is used for "pop out snapshot" feature.
|
||||
export function unwrapPopoutUrl(url: string) {
|
||||
const u = new URL(url);
|
||||
if (u.pathname.endsWith('/popout.html'))
|
||||
return u.searchParams.get('r')!;
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
|
||||
import { MultiMap } from './multimap';
|
||||
import { unwrapPopoutUrl } from './snapshotRenderer';
|
||||
import { SnapshotServer } from './snapshotServer';
|
||||
import { TraceModel } from './traceModel';
|
||||
|
||||
|
@ -65,7 +66,7 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
|||
const client = await self.clients.get(event.clientId);
|
||||
|
||||
if (request.url.startsWith(self.registration.scope)) {
|
||||
const url = new URL(request.url);
|
||||
const url = new URL(unwrapPopoutUrl(request.url));
|
||||
const relativePath = url.pathname.substring(scopePath.length - 1);
|
||||
if (relativePath === '/ping') {
|
||||
await gc();
|
||||
|
@ -101,7 +102,7 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
|||
if (relativePath.startsWith('/snapshot/')) {
|
||||
if (!snapshotServer)
|
||||
return new Response(null, { status: 404 });
|
||||
return snapshotServer.serveSnapshot(relativePath, url.searchParams, request.url);
|
||||
return snapshotServer.serveSnapshot(relativePath, url.searchParams, url.href);
|
||||
}
|
||||
|
||||
if (relativePath.startsWith('/sha1/')) {
|
||||
|
@ -118,7 +119,7 @@ async function doFetch(event: FetchEvent): Promise<Response> {
|
|||
return fetch(event.request);
|
||||
}
|
||||
|
||||
const snapshotUrl = client!.url;
|
||||
const snapshotUrl = unwrapPopoutUrl(client!.url);
|
||||
const traceUrl = new URL(snapshotUrl).searchParams.get('trace')!;
|
||||
const { snapshotServer } = loadedTraces.get(traceUrl) || {};
|
||||
if (!snapshotServer)
|
||||
|
|
|
@ -63,6 +63,7 @@
|
|||
flex: auto;
|
||||
margin: 1px;
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.snapshot-container {
|
||||
|
@ -82,6 +83,22 @@ iframe#snapshot {
|
|||
padding: 50px;
|
||||
}
|
||||
|
||||
.popout-icon {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
color: var(--gray);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.popout-icon:not(.popout-disabled):hover {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.popout-icon.popout-disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.window-dot {
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
|
|
|
@ -34,6 +34,7 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
const snapshots = [actionSnapshot ? { ...actionSnapshot, title: 'action' } : undefined, snapshotMap.get('before'), snapshotMap.get('after')].filter(Boolean) as { title: string, snapshotName: string }[];
|
||||
|
||||
let snapshotUrl = 'data:text/html,<body style="background: #ddd"></body>';
|
||||
let popoutUrl: string | undefined;
|
||||
let snapshotInfoUrl: string | undefined;
|
||||
let pointX: number | undefined;
|
||||
let pointY: number | undefined;
|
||||
|
@ -49,6 +50,10 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
pointX = action.metadata.point?.x;
|
||||
pointY = action.metadata.point?.y;
|
||||
}
|
||||
const popoutParams = new URLSearchParams();
|
||||
popoutParams.set('r', snapshotUrl);
|
||||
popoutParams.set('trace', context(action).traceUrl);
|
||||
popoutUrl = new URL(`popout.html?${popoutParams.toString()}`, window.location.href).toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,6 +112,9 @@ export const SnapshotTab: React.FunctionComponent<{
|
|||
})}
|
||||
</div>
|
||||
<div ref={ref} className='snapshot-wrapper'>
|
||||
<a className={`popout-icon ${popoutUrl ? '' : 'popout-disabled'}`} href={popoutUrl} target='_blank' title='Open snapshot in a new tab'>
|
||||
<span className='codicon codicon-link-external'/>
|
||||
</a>
|
||||
{ snapshots.length ? <div className='snapshot-container' style={{
|
||||
width: snapshotContainerSize.width + 'px',
|
||||
height: snapshotContainerSize.height + 'px',
|
||||
|
|
|
@ -38,6 +38,10 @@ export default defineConfig({
|
|||
// Output dir is shared with vite.sw.config.ts, clearing it here is racy.
|
||||
emptyOutDir: false,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: path.resolve(__dirname, 'index.html'),
|
||||
popout: path.resolve(__dirname, 'popout.html'),
|
||||
},
|
||||
output: {
|
||||
entryFileNames: () => '[name].[hash].js',
|
||||
assetFileNames: () => '[name].[hash][extname]',
|
||||
|
|
|
@ -223,6 +223,18 @@ test('should show snapshot URL', async ({ page, runAndTrace, server }) => {
|
|||
await expect(traceViewer.page.locator('.window-address-bar')).toHaveText(server.EMPTY_PAGE);
|
||||
});
|
||||
|
||||
test('should popup snapshot', async ({ page, runAndTrace, server }) => {
|
||||
const traceViewer = await runAndTrace(async () => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.setContent('hello');
|
||||
});
|
||||
await traceViewer.snapshotFrame('page.setContent');
|
||||
const popupPromise = traceViewer.page.context().waitForEvent('page');
|
||||
await traceViewer.page.getByTitle('Open snapshot in a new tab').click();
|
||||
const popup = await popupPromise;
|
||||
await expect(popup.getByText('hello')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should capture iframe with sandbox attribute', async ({ page, server, runAndTrace }) => {
|
||||
await page.route('**/empty.html', route => {
|
||||
route.fulfill({
|
||||
|
|
Loading…
Reference in New Issue