diff --git a/packages/trace-viewer/src/ui/snapshotTab.css b/packages/trace-viewer/src/ui/snapshotTab.css index 2677cfe53a..926685dc81 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.css +++ b/packages/trace-viewer/src/ui/snapshotTab.css @@ -15,12 +15,8 @@ */ .snapshot-tab { - display: flex; - flex: auto; - flex-direction: column; align-items: stretch; outline: none; - --browser-frame-header-height: 40px; overflow: hidden; } @@ -73,6 +69,7 @@ margin: 1px; padding: 10px; position: relative; + --browser-frame-header-height: 40px; } .snapshot-container { diff --git a/packages/trace-viewer/src/ui/snapshotTab.tsx b/packages/trace-viewer/src/ui/snapshotTab.tsx index 9dafa10b96..b845c60cd5 100644 --- a/packages/trace-viewer/src/ui/snapshotTab.tsx +++ b/packages/trace-viewer/src/ui/snapshotTab.tsx @@ -40,7 +40,7 @@ function findClosest(items: T[], metric: (v: T) => number, target: number) { }); } -export const SnapshotTab: React.FunctionComponent<{ +export const SnapshotTabsView: React.FunctionComponent<{ action: ActionTraceEvent | undefined, model?: MultiTraceModel, sdkLanguage: Language, @@ -50,63 +50,69 @@ export const SnapshotTab: React.FunctionComponent<{ highlightedLocator: string, setHighlightedLocator: (locator: string) => void, openPage?: (url: string, target?: string) => Window | any, -}> = ({ action, model, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { - const [measure, ref] = useMeasure(); +}> = ({ action, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator, openPage }) => { const [snapshotTab, setSnapshotTab] = React.useState<'action'|'before'|'after'>('action'); const [showScreenshotInsteadOfSnapshot] = useSetting('screenshot-instead-of-snapshot', false); - type Snapshot = { action: ActionTraceEvent, snapshotName: string, point?: { x: number, y: number }, hasInputTarget?: boolean }; - const { snapshots } = React.useMemo(() => { - if (!action) - return { snapshots: {} }; - - // if the action has no beforeSnapshot, use the last available afterSnapshot. - let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined; - let a = action; - while (!beforeSnapshot && a) { - a = prevInList(a); - beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined; - } - const afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot; - const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot, hasInputTarget: true } : afterSnapshot; - if (actionSnapshot) - actionSnapshot.point = action.point; - return { snapshots: { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot } }; + const snapshots = React.useMemo(() => { + return collectSnapshots(action); }, [action]); - - const { snapshotInfoUrl, snapshotUrl, popoutUrl, point } = React.useMemo(() => { + const snapshotUrls = React.useMemo(() => { const snapshot = snapshots[snapshotTab]; - if (!snapshot) - return { snapshotUrl: kBlankSnapshotUrl }; - - const params = new URLSearchParams(); - params.set('trace', context(snapshot.action).traceUrl); - params.set('name', snapshot.snapshotName); - if (snapshot.point) { - params.set('pointX', String(snapshot.point.x)); - params.set('pointY', String(snapshot.point.y)); - if (snapshot.hasInputTarget) - params.set('hasInputTarget', '1'); - } - const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); - const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); - - const popoutParams = new URLSearchParams(); - popoutParams.set('r', snapshotUrl); - popoutParams.set('trace', context(snapshot.action).traceUrl); - if (snapshot.point) { - popoutParams.set('pointX', String(snapshot.point.x)); - popoutParams.set('pointY', String(snapshot.point.y)); - if (snapshot.hasInputTarget) - params.set('hasInputTarget', '1'); - } - const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString(); - return { snapshots, snapshotInfoUrl, snapshotUrl, popoutUrl, point: snapshot.point }; + return snapshot ? extendSnapshot(snapshot) : undefined; }, [snapshots, snapshotTab]); + return
+ + setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} /> + {['action', 'before', 'after'].map(tab => { + return setSnapshotTab(tab as 'action' | 'before' | 'after')} + >; + })} +
+ { + if (!openPage) + openPage = window.open; + const win = openPage(snapshotUrls?.popoutUrl || '', '_blank'); + win?.addEventListener('DOMContentLoaded', () => { + const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); + new ConsoleAPI(injectedScript); + }); + }} /> +
+ {!showScreenshotInsteadOfSnapshot && } + {showScreenshotInsteadOfSnapshot && } +
; +}; + +export const SnapshotView: React.FunctionComponent<{ + snapshotUrls: SnapshotUrls | undefined, + sdkLanguage: Language, + testIdAttributeName: string, + isInspecting: boolean, + setIsInspecting: (isInspecting: boolean) => void, + highlightedLocator: string, + setHighlightedLocator: (locator: string) => void, +}> = ({ snapshotUrls, sdkLanguage, testIdAttributeName, isInspecting, setIsInspecting, highlightedLocator, setHighlightedLocator }) => { const iframeRef0 = React.useRef(null); const iframeRef1 = React.useRef(null); - const [snapshotInfo, setSnapshotInfo] = React.useState<{ viewport: typeof kDefaultViewport, url: string, timestamp?: number, wallTime?: undefined }>({ viewport: kDefaultViewport, url: '' }); + const [snapshotInfo, setSnapshotInfo] = React.useState({ viewport: kDefaultViewport, url: '' }); const loadingRef = React.useRef({ iteration: 0, visibleIframe: 0 }); React.useEffect(() => { @@ -115,17 +121,7 @@ export const SnapshotTab: React.FunctionComponent<{ const newVisibleIframe = 1 - loadingRef.current.visibleIframe; loadingRef.current.iteration = thisIteration; - const newSnapshotInfo = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined }; - if (snapshotInfoUrl) { - const response = await fetch(snapshotInfoUrl); - const info = await response.json(); - if (!info.error) { - newSnapshotInfo.url = info.url; - newSnapshotInfo.viewport = info.viewport; - newSnapshotInfo.timestamp = info.timestamp; - newSnapshotInfo.wallTime = info.wallTime; - } - } + const newSnapshotInfo = await fetchSnapshotInfo(snapshotUrls?.snapshotInfoUrl); // Interrupted by another load - bail out. if (loadingRef.current.iteration !== thisIteration) @@ -140,6 +136,7 @@ export const SnapshotTab: React.FunctionComponent<{ iframe.addEventListener('error', loadedCallback); // Try preventing history entry from being created. + const snapshotUrl = snapshotUrls?.snapshotUrl || kBlankSnapshotUrl; if (iframe.contentWindow) iframe.contentWindow.location.replace(snapshotUrl); else @@ -159,33 +156,10 @@ export const SnapshotTab: React.FunctionComponent<{ loadingRef.current.visibleIframe = newVisibleIframe; setSnapshotInfo(newSnapshotInfo); })(); - }, [snapshotUrl, snapshotInfoUrl]); - - const windowHeaderHeight = 40; - const snapshotContainerSize = { - width: snapshotInfo.viewport.width, - height: snapshotInfo.viewport.height + windowHeaderHeight, - }; - const scale = Math.min(measure.width / snapshotContainerSize.width, measure.height / snapshotContainerSize.height, 1); - const translate = { - x: (measure.width - snapshotContainerSize.width) / 2, - y: (measure.height - snapshotContainerSize.height) / 2, - }; - - const page = action ? pageForAction(action) : undefined; - const screencastFrame = React.useMemo( - () => { - if (snapshotInfo.wallTime && page?.screencastFrames[0]?.frameSwapWallTime) - return findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, snapshotInfo.wallTime); - - if (snapshotInfo.timestamp && page?.screencastFrames) - return findClosest(page.screencastFrames, frame => frame.timestamp, snapshotInfo.timestamp); - }, - [page?.screencastFrames, snapshotInfo.timestamp, snapshotInfo.wallTime] - ); + }, [snapshotUrls]); return
{ if (event.key === 'Escape') { @@ -210,46 +184,72 @@ export const SnapshotTab: React.FunctionComponent<{ setHighlightedLocator={setHighlightedLocator} iframe={iframeRef1.current} iteration={loadingRef.current.iteration} /> - - setIsInspecting(!isInspecting)} disabled={showScreenshotInsteadOfSnapshot} /> - {['action', 'before', 'after'].map(tab => { - return setSnapshotTab(tab as 'action' | 'before' | 'after')} - >; - })} -
- { - if (!openPage) - openPage = window.open; - const win = openPage(popoutUrl || '', '_blank'); - win?.addEventListener('DOMContentLoaded', () => { - const injectedScript = new InjectedScript(win as any, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []); - new ConsoleAPI(injectedScript); - }); - }}> -
-
-
- - {(showScreenshotInsteadOfSnapshot && screencastFrame) && ( - <> - {point && } - {`Screenshot ${renderTitle(snapshotTab)}`} src={`sha1/${screencastFrame.sha1}`} width={screencastFrame.width} height={screencastFrame.height} /> - - )} -
- - -
+ +
+ +
+
+
; +}; + +export const ScreenshotView: React.FunctionComponent<{ + action: ActionTraceEvent | undefined, + snapshotUrls: SnapshotUrls | undefined, + snapshot: Snapshot | undefined, +}> = ({ action, snapshotUrls, snapshot }) => { + const [snapshotInfo, setSnapshotInfo] = React.useState({ viewport: kDefaultViewport, url: '' }); + React.useEffect(() => { + fetchSnapshotInfo(snapshotUrls?.snapshotInfoUrl).then(setSnapshotInfo); + }, [snapshotUrls?.snapshotInfoUrl]); + + const page = action ? pageForAction(action) : undefined; + const screencastFrame = React.useMemo(() => { + if (snapshotInfo.wallTime && page?.screencastFrames[0]?.frameSwapWallTime) + return findClosest(page.screencastFrames, frame => frame.frameSwapWallTime!, snapshotInfo.wallTime); + + if (snapshotInfo.timestamp && page?.screencastFrames) + return findClosest(page.screencastFrames, frame => frame.timestamp, snapshotInfo.timestamp); + }, + [page?.screencastFrames, snapshotInfo.timestamp, snapshotInfo.wallTime]); + + const point = snapshot?.point; + + return + {screencastFrame && ( + <> + {point && } + {`Screenshot + + )} + ; +}; + +const SnapshotWrapper: React.FunctionComponent> = ({ snapshotInfo, children }) => { + const [measure, ref] = useMeasure(); + + const windowHeaderHeight = 40; + const snapshotContainerSize = { + width: snapshotInfo.viewport.width, + height: snapshotInfo.viewport.height + windowHeaderHeight, + }; + + const scale = Math.min(measure.width / snapshotContainerSize.width, measure.height / snapshotContainerSize.height, 1); + const translate = { + x: (measure.width - snapshotContainerSize.width) / 2, + y: (measure.height - snapshotContainerSize.height) / 2, + }; + + return
+
+ + {children}
; }; @@ -325,5 +325,90 @@ function createRecorders(recorders: { recorder: Recorder, frameSelector: string } } -const kDefaultViewport = { width: 1280, height: 720 }; +export type Snapshot = { + action: ActionTraceEvent; + snapshotName: string; + point?: { x: number, y: number }; + hasInputTarget?: boolean; +}; + +export type SnapshotInfo = { + url: string; + viewport: { width: number, height: number }; + timestamp?: number; + wallTime?: undefined; +}; + +export type Snapshots = { + action?: Snapshot; + before?: Snapshot; + after?: Snapshot; +}; + +export type SnapshotUrls = { + snapshotInfoUrl: string; + snapshotUrl: string; + popoutUrl: string; +}; + +export function collectSnapshots(action: ActionTraceEvent | undefined): Snapshots { + if (!action) + return {}; + + // if the action has no beforeSnapshot, use the last available afterSnapshot. + let beforeSnapshot: Snapshot | undefined = action.beforeSnapshot ? { action, snapshotName: action.beforeSnapshot } : undefined; + let a = action; + while (!beforeSnapshot && a) { + a = prevInList(a); + beforeSnapshot = a?.afterSnapshot ? { action: a, snapshotName: a?.afterSnapshot } : undefined; + } + const afterSnapshot: Snapshot | undefined = action.afterSnapshot ? { action, snapshotName: action.afterSnapshot } : beforeSnapshot; + const actionSnapshot: Snapshot | undefined = action.inputSnapshot ? { action, snapshotName: action.inputSnapshot, hasInputTarget: true } : afterSnapshot; + if (actionSnapshot) + actionSnapshot.point = action.point; + return { action: actionSnapshot, before: beforeSnapshot, after: afterSnapshot }; +} + +export function extendSnapshot(snapshot: Snapshot): SnapshotUrls { + const params = new URLSearchParams(); + params.set('trace', context(snapshot.action).traceUrl); + params.set('name', snapshot.snapshotName); + if (snapshot.point) { + params.set('pointX', String(snapshot.point.x)); + params.set('pointY', String(snapshot.point.y)); + if (snapshot.hasInputTarget) + params.set('hasInputTarget', '1'); + } + const snapshotUrl = new URL(`snapshot/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); + const snapshotInfoUrl = new URL(`snapshotInfo/${snapshot.action.pageId}?${params.toString()}`, window.location.href).toString(); + + const popoutParams = new URLSearchParams(); + popoutParams.set('r', snapshotUrl); + popoutParams.set('trace', context(snapshot.action).traceUrl); + if (snapshot.point) { + popoutParams.set('pointX', String(snapshot.point.x)); + popoutParams.set('pointY', String(snapshot.point.y)); + if (snapshot.hasInputTarget) + params.set('hasInputTarget', '1'); + } + const popoutUrl = new URL(`snapshot.html?${popoutParams.toString()}`, window.location.href).toString(); + return { snapshotInfoUrl, snapshotUrl, popoutUrl }; +} + +export async function fetchSnapshotInfo(snapshotInfoUrl: string | undefined) { + const result = { url: '', viewport: kDefaultViewport, timestamp: undefined, wallTime: undefined }; + if (snapshotInfoUrl) { + const response = await fetch(snapshotInfoUrl); + const info = await response.json(); + if (!info.error) { + result.url = info.url; + result.viewport = info.viewport; + result.timestamp = info.timestamp; + result.wallTime = info.wallTime; + } + } + return result; +} + +export const kDefaultViewport = { width: 1280, height: 720 }; const kBlankSnapshotUrl = 'data:text/html,'; diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx index 95c18d8d0a..13c5f5fd0e 100644 --- a/packages/trace-viewer/src/ui/workbench.tsx +++ b/packages/trace-viewer/src/ui/workbench.tsx @@ -25,7 +25,7 @@ import type { ConsoleEntry } from './consoleTab'; import { ConsoleTab, useConsoleTabModel } from './consoleTab'; import type * as modelUtil from './modelUtil'; import { NetworkTab, useNetworkTabModel } from './networkTab'; -import { SnapshotTab } from './snapshotTab'; +import { SnapshotTabsView } from './snapshotTab'; import { SourceTab } from './sourceTab'; import { TabbedPane } from '@web/components/tabbedPane'; import type { TabbedPaneTabModel } from '@web/components/tabbedPane'; @@ -331,7 +331,7 @@ export const Workbench: React.FunctionComponent<{ orientation='horizontal' sidebarIsFirst settingName='actionListSidebar' - main={ Action`); + const screenshot = traceViewer.page.getByAltText(`Screenshot of page.goto`); const snapshot = (await traceViewer.snapshotFrame('page.goto')).owner(); await expect(snapshot).toBeVisible(); await expect(screenshot).not.toBeVisible();