diff --git a/src/cli/traceViewer/traceModel.ts b/src/cli/traceViewer/traceModel.ts index c82518b5a6..0cb4eb59f8 100644 --- a/src/cli/traceViewer/traceModel.ts +++ b/src/cli/traceViewer/traceModel.ts @@ -64,6 +64,8 @@ export type VideoMetaInfo = { endTime: number; }; +const kInterestingActions = ['click', 'dblclick', 'hover', 'check', 'uncheck', 'tap', 'fill', 'press', 'type', 'selectOption', 'setInputFiles', 'goto', 'setContent', 'goBack', 'goForward', 'reload']; + export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel, filePath: string) { const contextEntries = new Map(); const pageEntries = new Map(); @@ -108,6 +110,8 @@ export function readTraceFile(events: trace.TraceEvent[], traceModel: TraceModel break; } case 'action': { + if (!kInterestingActions.includes(event.method)) + break; const pageEntry = pageEntries.get(event.pageId!)!; const actionId = event.contextId + '/' + event.pageId + '/' + pageEntry.actions.length; const action: ActionEntry = { diff --git a/src/web/traceViewer/ui/propertiesTabbedPane.tsx b/src/web/traceViewer/ui/propertiesTabbedPane.tsx deleted file mode 100644 index 01e2d3327c..0000000000 --- a/src/web/traceViewer/ui/propertiesTabbedPane.tsx +++ /dev/null @@ -1,135 +0,0 @@ -/** - * 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 { ActionEntry } from '../../../cli/traceViewer/traceModel'; -import { Boundaries, Size } from '../geometry'; -import { NetworkTab } from './networkTab'; -import { SourceTab } from './sourceTab'; -import './propertiesTabbedPane.css'; -import * as React from 'react'; -import { msToString, useMeasure } from './helpers'; -import { LogsTab } from './logsTab'; - -export const PropertiesTabbedPane: React.FunctionComponent<{ - actionEntry: ActionEntry | undefined, - snapshotSize: Size, - selectedTime: number | undefined, - boundaries: Boundaries, -}> = ({ actionEntry, snapshotSize, selectedTime, boundaries }) => { - const [selected, setSelected] = React.useState<'snapshot' | 'source' | 'network' | 'logs'>('snapshot'); - return
-
-
-
-
setSelected('snapshot')}> -
Snapshot
-
-
setSelected('source')}> -
Source
-
-
setSelected('network')}> -
Network
-
-
setSelected('logs')}> -
Logs
-
-
-
- { selected === 'snapshot' &&
- -
} - { selected === 'source' &&
- -
} - { selected === 'network' &&
- -
} - { selected === 'logs' &&
- -
} -
-
; -}; - -const SnapshotTab: React.FunctionComponent<{ - actionEntry: ActionEntry | undefined, - snapshotSize: Size, - selectedTime: number | undefined, - boundaries: Boundaries, -}> = ({ actionEntry, snapshotSize, selectedTime, boundaries }) => { - const [measure, ref] = useMeasure(); - const [snapshotIndex, setSnapshotIndex] = React.useState(0); - - let snapshots: { name: string, snapshotId?: string, snapshotTime?: number }[] = []; - snapshots = (actionEntry ? (actionEntry.action.snapshots || []) : []).slice(); - if (!snapshots.length || snapshots[0].name !== 'before') - snapshots.unshift({ name: 'before', snapshotTime: actionEntry ? actionEntry.action.startTime : 0 }); - if (snapshots[snapshots.length - 1].name !== 'after') - snapshots.push({ name: 'after', snapshotTime: actionEntry ? actionEntry.action.endTime : 0 }); - - const iframeRef = React.createRef(); - React.useEffect(() => { - if (!actionEntry || !iframeRef.current) - return; - - // TODO: this logic is copied from SnapshotServer. Find a way to share. - let snapshotUrl = 'data:text/html,Snapshot is not available'; - if (selectedTime) { - snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/timestamp/${selectedTime}/main`; - } else { - const snapshot = snapshots[snapshotIndex]; - if (snapshot && snapshot.snapshotTime) - snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/timestamp/${snapshot.snapshotTime}/main`; - else if (snapshot && snapshot.snapshotId) - snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/snapshotId/${snapshot.snapshotId}/main`; - } - - try { - (iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl); - } catch (e) { - } - }, [actionEntry, snapshotIndex, selectedTime]); - - const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height); - return
-
{ - selectedTime &&
- {msToString(selectedTime - boundaries.minimum)} -
- }{!selectedTime && snapshots.map((snapshot, index) => { - return
setSnapshotIndex(index)}> - {snapshot.name} -
- }) - }
-
-
- -
-
-
; -}; diff --git a/src/web/traceViewer/ui/snapshotTab.css b/src/web/traceViewer/ui/snapshotTab.css new file mode 100644 index 0000000000..de0f67ecc5 --- /dev/null +++ b/src/web/traceViewer/ui/snapshotTab.css @@ -0,0 +1,54 @@ +/* + 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. +*/ + +.snapshot-tab { + display: flex; + flex-direction: column; + align-items: stretch; +} + +.snapshot-controls { + flex: 0 0 24px; + display: flex; + flex-direction: row; + align-items: center; +} + +.snapshot-toggle { + padding: 5px 10px; + cursor: pointer; +} + +.snapshot-toggle.toggled { + background: var(--inactive-focus-ring); +} + +.snapshot-wrapper { + flex: auto; + margin: 1px; +} + +.snapshot-container { + display: block; + background: white; + outline: 1px solid #aaa; +} + +iframe#snapshot { + width: 100%; + height: 100%; + border: none; +} diff --git a/src/web/traceViewer/ui/snapshotTab.tsx b/src/web/traceViewer/ui/snapshotTab.tsx new file mode 100644 index 0000000000..55adb6efab --- /dev/null +++ b/src/web/traceViewer/ui/snapshotTab.tsx @@ -0,0 +1,90 @@ +/** + * 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 { ActionEntry } from '../../../cli/traceViewer/traceModel'; +import { Boundaries, Size } from '../geometry'; +import './snapshotTab.css'; +import * as React from 'react'; +import { msToString, useMeasure } from './helpers'; + +export const SnapshotTab: React.FunctionComponent<{ + actionEntry: ActionEntry | undefined, + snapshotSize: Size, + selection: { pageId: string, time: number } | undefined, + boundaries: Boundaries, +}> = ({ actionEntry, snapshotSize, selection, boundaries }) => { + const [measure, ref] = useMeasure(); + const [snapshotIndex, setSnapshotIndex] = React.useState(0); + + let snapshots: { name: string, snapshotId?: string, snapshotTime?: number }[] = []; + snapshots = (actionEntry ? (actionEntry.action.snapshots || []) : []).slice(); + if (actionEntry) { + if (!snapshots.length || snapshots[0].name !== 'before') + snapshots.unshift({ name: 'before', snapshotTime: actionEntry ? actionEntry.action.startTime : 0 }); + if (snapshots[snapshots.length - 1].name !== 'after') + snapshots.push({ name: 'after', snapshotTime: actionEntry ? actionEntry.action.endTime : 0 }); + } + const { pageId, time } = selection || { pageId: undefined, time: 0 }; + + const iframeRef = React.createRef(); + React.useEffect(() => { + if (!iframeRef.current) + return; + + // TODO: this logic is copied from SnapshotServer. Find a way to share. + let snapshotUrl = 'data:text/html,Snapshot is not available'; + if (pageId) { + snapshotUrl = `/snapshot/pageId/${pageId}/timestamp/${time}/main`; + } else if (actionEntry) { + const snapshot = snapshots[snapshotIndex]; + if (snapshot && snapshot.snapshotTime) + snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/timestamp/${snapshot.snapshotTime}/main`; + else if (snapshot && snapshot.snapshotId) + snapshotUrl = `/snapshot/pageId/${actionEntry.action.pageId!}/snapshotId/${snapshot.snapshotId}/main`; + } + + try { + (iframeRef.current.contentWindow as any).showSnapshot(snapshotUrl); + } catch (e) { + } + }, [actionEntry, snapshotIndex, pageId, time]); + + const scale = Math.min(measure.width / snapshotSize.width, measure.height / snapshotSize.height); + return
+
{ + selection &&
+ {msToString(selection.time - boundaries.minimum)} +
+ }{!selection && snapshots.map((snapshot, index) => { + return
setSnapshotIndex(index)}> + {snapshot.name} +
+ }) + }
+
+
+ +
+
+
; +}; diff --git a/src/web/traceViewer/ui/propertiesTabbedPane.css b/src/web/traceViewer/ui/tabbedPane.css similarity index 61% rename from src/web/traceViewer/ui/propertiesTabbedPane.css rename to src/web/traceViewer/ui/tabbedPane.css index 5518ba6a68..8060c716e5 100644 --- a/src/web/traceViewer/ui/propertiesTabbedPane.css +++ b/src/web/traceViewer/ui/tabbedPane.css @@ -14,19 +14,19 @@ limitations under the License. */ -.properties-tabbed-pane { +.tabbed-pane { display: flex; flex: auto; overflow: hidden; } -.properties-tab-content { +.tab-content { display: flex; flex: auto; overflow: hidden; } -.properties-tab-strip { +.tab-strip { flex: auto; display: flex; flex-direction: row; @@ -34,11 +34,11 @@ height: 34px; } -.properties-tab-strip:focus { +.tab-strip:focus { outline: none; } -.properties-tab-element { +.tab-element { padding: 2px 6px 0 6px; margin-right: 4px; cursor: pointer; @@ -52,7 +52,7 @@ outline: none; } -.properties-tab-label { +.tab-label { max-width: 250px; white-space: pre; overflow: hidden; @@ -60,49 +60,10 @@ display: inline-block; } -.properties-tab-element.selected { +.tab-element.selected { border-bottom-color: var(--color); } -.properties-tab-element:hover { +.tab-element:hover { font-weight: 600; } - -.snapshot-tab { - display: flex; - flex-direction: column; - align-items: stretch; -} - -.snapshot-controls { - flex: 0 0 24px; - display: flex; - flex-direction: row; - align-items: center; -} - -.snapshot-toggle { - padding: 5px 10px; - cursor: pointer; -} - -.snapshot-toggle.toggled { - background: var(--inactive-focus-ring); -} - -.snapshot-wrapper { - flex: auto; - margin: 1px; -} - -.snapshot-container { - display: block; - background: white; - outline: 1px solid #aaa; -} - -iframe#snapshot { - width: 100%; - height: 100%; - border: none; -} diff --git a/src/web/traceViewer/ui/tabbedPane.tsx b/src/web/traceViewer/ui/tabbedPane.tsx new file mode 100644 index 0000000000..14c7ee10c5 --- /dev/null +++ b/src/web/traceViewer/ui/tabbedPane.tsx @@ -0,0 +1,51 @@ +/** + * 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 './tabbedPane.css'; +import * as React from 'react'; + +export interface TabbedPaneTab { + id: string; + title: string; + render: () => React.ReactElement; +} + +export const TabbedPane: React.FunctionComponent<{ + tabs: TabbedPaneTab[], +}> = ({ tabs }) => { + const [selected, setSelected] = React.useState(tabs.length ? tabs[0].id : ''); + return
+
+
+
{ + tabs.map(tab => { + return
setSelected(tab.id)} + key={tab.id}> +
{tab.title}
+
+ }) + }
+
+ { + tabs.map(tab => { + if (selected === tab.id) + return
{tab.render()}
; + }) + } +
+
; +}; diff --git a/src/web/traceViewer/ui/timeline.tsx b/src/web/traceViewer/ui/timeline.tsx index ea22e0c9b9..d52e765cce 100644 --- a/src/web/traceViewer/ui/timeline.tsx +++ b/src/web/traceViewer/ui/timeline.tsx @@ -43,13 +43,12 @@ export const Timeline: React.FunctionComponent<{ }> = ({ context, boundaries, selectedAction, highlightedAction, onSelected, onTimeSelected }) => { const [measure, ref] = useMeasure(); const [previewX, setPreviewX] = React.useState(); - const [hoveredBar, setHoveredBar] = React.useState(); + const [hoveredBarIndex, setHoveredBarIndex] = React.useState(); const offsets = React.useMemo(() => { return calculateDividerOffsets(measure.width, boundaries); }, [measure.width, boundaries]); - let targetBar: TimelineBar | undefined = hoveredBar; const bars = React.useMemo(() => { const bars: TimelineBar[] = []; for (const page of context.pages) { @@ -67,8 +66,6 @@ export const Timeline: React.FunctionComponent<{ type: entry.action.method, priority: 0, }); - if (entry === (highlightedAction || selectedAction)) - targetBar = bars[bars.length - 1]; } let lastDialogOpened: trace.DialogOpenedEvent | undefined; for (const event of page.interestingEvents) { @@ -116,56 +113,55 @@ export const Timeline: React.FunctionComponent<{ return bars; }, [context, boundaries, measure.width]); - const findHoveredBar = (x: number) => { + const hoveredBar = hoveredBarIndex !== undefined ? bars[hoveredBarIndex] : undefined; + let targetBar: TimelineBar | undefined = bars.find(bar => bar.entry === (highlightedAction || selectedAction)); + targetBar = hoveredBar || targetBar; + + const findHoveredBarIndex = (x: number) => { const time = positionToTime(measure.width, boundaries, x); const time1 = positionToTime(measure.width, boundaries, x - 5); const time2 = positionToTime(measure.width, boundaries, x + 5); - let bar: TimelineBar | undefined; + let index: number | undefined; let distance: number | undefined; - for (const b of bars) { - const left = Math.max(b.leftTime, time1); - const right = Math.min(b.rightTime, time2); - const middle = (b.leftTime + b.rightTime) / 2; + for (let i = 0; i < bars.length; i++) { + const bar = bars[i]; + const left = Math.max(bar.leftTime, time1); + const right = Math.min(bar.rightTime, time2); + const middle = (bar.leftTime + bar.rightTime) / 2; const d = Math.abs(time - middle); - if (left <= right && (!bar || d < distance!)) { - bar = b; + if (left <= right && (index === undefined || d < distance!)) { + index = i; distance = d; } } - return bar; + return index; }; const onMouseMove = (event: React.MouseEvent) => { - if (ref.current) { - const x = event.clientX - ref.current.getBoundingClientRect().left; - setPreviewX(x); - onTimeSelected(positionToTime(measure.width, boundaries, x)); - setHoveredBar(findHoveredBar(x)); - } + if (!ref.current) + return; + const x = event.clientX - ref.current.getBoundingClientRect().left; + setPreviewX(x); + onTimeSelected(positionToTime(measure.width, boundaries, x)); + setHoveredBarIndex(findHoveredBarIndex(x)); }; const onMouseLeave = () => { setPreviewX(undefined); onTimeSelected(undefined); }; - const onActionClick = (event: React.MouseEvent) => { - if (ref.current) { - const x = event.clientX - ref.current.getBoundingClientRect().left; - const bar = findHoveredBar(x); - if (bar && bar.entry) - onSelected(bar.entry); - event.stopPropagation(); - } - }; - const onTimeClick = (event: React.MouseEvent) => { - if (ref.current) { - const x = event.clientX - ref.current.getBoundingClientRect().left; - const time = positionToTime(measure.width, boundaries, x); - onTimeSelected(time); - event.stopPropagation(); - } + const onClick = (event: React.MouseEvent) => { + if (!ref.current) + return; + const x = event.clientX - ref.current.getBoundingClientRect().left; + const index = findHoveredBarIndex(x); + if (index === undefined) + return; + const entry = bars[index].entry; + if (entry) + onSelected(entry); }; - return
+ return
{ offsets.map((offset, index) => { return
@@ -186,7 +182,7 @@ export const Timeline: React.FunctionComponent<{
; }) }
-
{ +
{ bars.map((bar, index) => { return
@@ -78,12 +83,12 @@ export const Workbench: React.FunctionComponent<{ onHighlighted={action => setHighlightedAction(action)} />
- + }, + { id: 'source', title: 'Source', render: () => }, + { id: 'network', title: 'Network', render: () => }, + { id: 'logs', title: 'Logs', render: () => }, + ]}/>
; };