Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/app/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,10 @@ export class WebdriverIODevtoolsApplication extends Element {
class="flex h-[calc(100%-40px)] w-full relative"
>
${
// Only render the test-suite sidebar (and its resize slider) when the
// trace came from a testrunner — the player (standalone) has no tree,
// so the slider would otherwise show a stray dragger on hover.
// Only render the test-suite sidebar (and its resize slider) for a
// live testrunner session. The player has no run/rerun affordances,
// so the tree is dead weight even for testrunner-captured zips.
!this.dataManager.playerMode &&
this.dataManager.traceType === TraceType.Testrunner
? html`<wdio-devtools-sidebar
style="${this.#drag?.getPosition()}"
Expand Down
10 changes: 4 additions & 6 deletions packages/app/src/components/browser/snapshot-styles.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { css } from 'lit'
import { css, unsafeCSS } from 'lit'

import { BROWSER_BACKDROP_GRADIENT } from '../../controller/constants.js'

/** Component styles for `<wdio-devtools-snapshot>`. Pulled out of snapshot.ts
* so the main component file stays focused on the iframe/screencast logic. */
Expand All @@ -11,11 +13,7 @@ export const snapshotStyles = css`
align-items: center;
justify-content: center;
box-sizing: border-box !important;
background: radial-gradient(
120% 120% at 50% 0%,
var(--vscode-editorWidget-background),
var(--vscode-editor-background)
);
background: ${unsafeCSS(BROWSER_BACKDROP_GRADIENT)};
}

section {
Expand Down
137 changes: 137 additions & 0 deletions packages/app/src/components/browser/trace-player-controls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Element } from '@core/element'
import { html, css, type TemplateResult } from 'lit'
import { customElement, state } from 'lit/decorators.js'

import { emit, KBD } from '../../controller/keyboard.js'
import {
PLAYER_RESTART_EVENT,
PLAYER_SPEED_EVENT,
PLAYER_STATE_EVENT,
SPEEDS,
type PlayerState
} from './trace-timeline-constants.js'
import { formatTimecode } from './trace-timeline-utils.js'

import '~icons/mdi/play.js'
import '~icons/mdi/pause.js'
import '~icons/mdi/skip-previous.js'
import '~icons/mdi/skip-next.js'
import '~icons/mdi/restart.js'

const COMPONENT = 'wdio-devtools-trace-player-controls'

/** Playback controls bar; drives the timeline via window events and mirrors its broadcast state. */
@customElement(COMPONENT)
export class TracePlayerControls extends Element {
@state() playerState: PlayerState = {
currentMs: 0,
duration: 0,
playing: false,
speed: 1
}

static styles = [
...Element.styles,
css`
:host {
display: flex;
align-items: center;
background-color: var(--vscode-editor-background);
color: var(--vscode-foreground);
}
`
]

connectedCallback(): void {
super.connectedCallback()
window.addEventListener(PLAYER_STATE_EVENT, this.#onState)
}

disconnectedCallback(): void {
super.disconnectedCallback()
window.removeEventListener(PLAYER_STATE_EVENT, this.#onState)
}

#onState = (event: Event): void => {
this.playerState = (event as CustomEvent<PlayerState>).detail
}

#button(
title: string,
icon: TemplateResult,
onClick: () => void,
extra = ''
): TemplateResult {
return html`<button
class="p-1 hover:bg-toolbarHoverBackground rounded ${extra}"
title="${title}"
@click="${onClick}"
>
${icon}
</button>`
}

#renderSpeedSelect(speed: number): TemplateResult {
return html`
<select
class="ml-1 bg-sideBarBackground border border-panelBorder rounded px-1 py-0.5"
title="Playback speed"
@change="${(event: Event) =>
emit(PLAYER_SPEED_EVENT, {
value: Number((event.target as HTMLSelectElement).value)
})}"
>
${SPEEDS.map(
(value) =>
html`<option value="${value}" ?selected="${value === speed}">
${value}×
</option>`
)}
</select>
`
}

render() {
const { currentMs, duration, playing, speed } = this.playerState
return html`
<div class="flex items-center gap-1 px-2 w-full text-[12px]">
<code class="tabular-nums text-chartsYellow"
>${formatTimecode(currentMs)}</code
>
<span class="opacity-60">/</span>
<code class="tabular-nums opacity-80">${formatTimecode(duration)}</code>
<span class="ml-auto"></span>
${this.#button(
'Restart',
html`<icon-mdi-restart></icon-mdi-restart>`,
() => emit(PLAYER_RESTART_EVENT)
)}
${this.#button(
'Previous action',
html`<icon-mdi-skip-previous></icon-mdi-skip-previous>`,
() => emit(KBD.step, { dir: -1 })
)}
${this.#button(
playing ? 'Pause' : 'Play',
playing
? html`<icon-mdi-pause></icon-mdi-pause>`
: html`<icon-mdi-play></icon-mdi-play>`,
() => emit(KBD.togglePlay),
'text-chartsBlue'
)}
${this.#button(
'Next action',
html`<icon-mdi-skip-next></icon-mdi-skip-next>`,
() => emit(KBD.step, { dir: 1 })
)}
${this.#renderSpeedSelect(speed)}
</div>
`
}
}

declare global {
interface HTMLElementTagNameMap {
[COMPONENT]: TracePlayerControls
}
}
30 changes: 17 additions & 13 deletions packages/app/src/components/browser/trace-timeline-constants.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import type { ActionCategory } from '../workbench/actionItems/category.js'

/** Playback speed multipliers offered in the timeline controls. */
export const SPEEDS = [0.5, 1, 2, 3, 5]

/** Width of the track-label gutter (px) — lanes start after it. */
export const GUTTER = 80
/** Candidate ruler intervals (ms); tickStep picks the smallest fitting one. */
export const TICK_STEPS = [
100, 250, 500, 1_000, 2_000, 5_000, 10_000, 15_000, 30_000, 60_000, 120_000,
300_000, 600_000
]

/** Ruler divisions to aim for — keeps labels readable at any duration. */
export const TICK_TARGET_DIVISIONS = 14

/** Right breathing room (px) so end-of-timeline markers don't hug the edge. */
export const INSET = 14
/** Window events linking the controls bar and the timeline strip (KBD-style). */
export const PLAYER_STATE_EVENT = 'trace-player:state'
export const PLAYER_RESTART_EVENT = 'trace-player:restart'
export const PLAYER_SPEED_EVENT = 'trace-player:speed'

/** Tailwind background class per action category, for the timeline chips. */
export const CATEGORY_BG: Record<ActionCategory, string> = {
navigation: 'bg-chartsBlue',
input: 'bg-chartsPurple',
assertion: 'bg-chartsGreen',
query: 'bg-chartsYellow',
other: 'bg-gray-500'
export interface PlayerState {
currentMs: number
duration: number
playing: boolean
speed: number
}
52 changes: 1 addition & 51 deletions packages/app/src/components/browser/trace-timeline-styles.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { css } from 'lit'

/** Styles for the trace-player timeline: host layout, hidden scrollbars, and
* the network-detail drawer. Detail-block styles come from networkStyles. */
/** Host layout for the trace-player timeline strip. */
export const timelineStyles = css`
:host {
position: relative;
Expand All @@ -12,53 +11,4 @@ export const timelineStyles = css`
background-color: var(--vscode-editor-background);
color: var(--vscode-foreground);
}
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.net-drawer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
max-height: 62%;
display: flex;
flex-direction: column;
background: var(--vscode-sideBar-background);
border-top: 1px solid var(--accent, #ff7a3c);
box-shadow: 0 -16px 40px -24px #000;
z-index: 30;
}
.net-drawer-head {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-bottom: 1px solid var(--vscode-panel-border);
font-size: 12px;
}
.net-drawer-head .url {
font-family: monospace;
font-size: 11.5px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
opacity: 0.85;
}
.net-drawer-head .close {
margin-left: auto;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
}
.net-drawer-head .close:hover {
background: var(--vscode-toolbar-hoverBackground);
}
.net-drawer-body {
overflow: auto;
padding: 4px 0;
}
`
28 changes: 28 additions & 0 deletions packages/app/src/components/browser/trace-timeline-utils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
import {
TICK_STEPS,
TICK_TARGET_DIVISIONS
} from './trace-timeline-constants.js'

/** Detect image mime from a base64 string's magic bytes — trace screenshots
* may be PNG (polling capture) or JPEG (CDP), and the zip names both `.jpeg`. */
export function imageMime(base64: string): string {
return base64.startsWith('/9j/') ? 'image/jpeg' : 'image/png'
}

export function tickStep(
durationMs: number,
targetTicks = TICK_TARGET_DIVISIONS
): number {
const raw = durationMs / targetTicks
return (
TICK_STEPS.find((step) => step >= raw) ?? TICK_STEPS[TICK_STEPS.length - 1]
)
}

/** Ruler tick label: `500ms`, `3.5s`, `1:15`. */
export function formatTickLabel(ms: number): string {
if (ms < 1_000) {
return `${ms}ms`
}
if (ms < 60_000) {
return `${(ms / 1_000).toFixed(1)}s`
}
const minutes = Math.floor(ms / 60_000)
const seconds = Math.round((ms % 60_000) / 1_000)
return `${minutes}:${String(seconds).padStart(2, '0')}`
}

/** `m:ss.cc` timecode (e.g. 32_270ms → `0:32.27`). */
export function formatTimecode(ms: number): string {
const safe = Number.isFinite(ms) && ms > 0 ? ms : 0
Expand Down
Loading
Loading