Skip to content

Commit 9bc74d9

Browse files
committed
Implemented capturing browser state during each command
1 parent e782123 commit 9bc74d9

File tree

3 files changed

+73
-81
lines changed

3 files changed

+73
-81
lines changed

packages/app/src/components/browser/snapshot.ts

Lines changed: 67 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { consume } from '@lit/context'
55
import { type ComponentChildren, h, render, type VNode } from 'preact'
66
import { customElement, query } from 'lit/decorators.js'
77
import type { SimplifiedVNode } from '../../../../script/types'
8+
import type { CommandLog } from '@wdio/devtools-service/types'
89

910
import {
1011
mutationContext,
@@ -43,10 +44,6 @@ const COMPONENT = 'wdio-devtools-browser'
4344
export class DevtoolsBrowser extends Element {
4445
#vdom = document.createDocumentFragment()
4546
#activeUrl?: string
46-
#resizeTimer?: number
47-
#boundResize = () => this.#debouncedResize()
48-
#checkpoints = new Map<number, DocumentFragment>()
49-
#checkpointStride = 50
5047

5148
@consume({ context: metadataContext, subscribe: true })
5249
metadata: Metadata | undefined = undefined
@@ -96,25 +93,22 @@ export class DevtoolsBrowser extends Element {
9693

9794
async connectedCallback() {
9895
super.connectedCallback()
99-
window.addEventListener('resize', this.#boundResize)
100-
window.addEventListener('window-drag', this.#boundResize)
96+
window.addEventListener('resize', this.#setIframeSize.bind(this))
97+
window.addEventListener('window-drag', this.#setIframeSize.bind(this))
10198
window.addEventListener(
10299
'app-mutation-highlight',
103100
this.#highlightMutation.bind(this)
104101
)
105102
window.addEventListener('app-mutation-select', (ev) =>
106103
this.#renderBrowserState(ev.detail)
107104
)
105+
window.addEventListener(
106+
'show-command',
107+
this.#handleShowCommand as EventListener
108+
)
108109
await this.updateComplete
109110
}
110111

111-
#debouncedResize() {
112-
if (this.#resizeTimer) {
113-
window.clearTimeout(this.#resizeTimer)
114-
}
115-
this.#resizeTimer = window.setTimeout(() => this.#setIframeSize(), 80)
116-
}
117-
118112
#setIframeSize() {
119113
const metadata = this.metadata
120114
if (!this.section || !this.iframe || !this.header || !metadata) {
@@ -145,6 +139,31 @@ export class DevtoolsBrowser extends Element {
145139
this.iframe.style.transform = `scale(${scale})`
146140
}
147141

142+
#handleShowCommand = (event: Event) =>
143+
this.#renderCommandScreenshot(
144+
(event as CustomEvent<{ command?: CommandLog }>).detail?.command
145+
)
146+
147+
async #renderCommandScreenshot(command?: CommandLog) {
148+
const screenshot = command?.screenshot
149+
if (!screenshot) {
150+
return
151+
}
152+
153+
if (!this.iframe) {
154+
await this.updateComplete
155+
}
156+
if (!this.iframe) {
157+
return
158+
}
159+
160+
this.iframe.srcdoc = `
161+
<body style="margin:0;background:#111;display:flex;justify-content:center;align-items:flex-start;">
162+
<img src="data:image/png;base64,${screenshot}" style="max-width:100%;height:auto;display:block;" />
163+
</body>
164+
`
165+
}
166+
148167
async #renderNewDocument(doc: SimplifiedVNode, baseUrl: string) {
149168
const root = transform(doc)
150169
const baseTag = h('base', { href: baseUrl })
@@ -182,6 +201,7 @@ export class DevtoolsBrowser extends Element {
182201
if (!this.iframe) {
183202
await this.updateComplete
184203
}
204+
185205
if (mutation.type === 'attributes') {
186206
return this.#handleAttributeMutation(mutation)
187207
}
@@ -203,22 +223,16 @@ export class DevtoolsBrowser extends Element {
203223
}
204224

205225
#handleAttributeMutation(mutation: TraceMutation) {
206-
if (!mutation.attributeName) {
226+
if (!mutation.attributeName || !mutation.attributeValue) {
207227
return
208228
}
229+
209230
const el = this.#queryElement(mutation.target!)
210231
if (!el) {
211232
return
212233
}
213234

214-
if (
215-
mutation.attributeValue === undefined ||
216-
mutation.attributeValue === null
217-
) {
218-
el.removeAttribute(mutation.attributeName)
219-
} else {
220-
el.setAttribute(mutation.attributeName, mutation.attributeValue)
221-
}
235+
el.setAttribute(mutation.attributeName, mutation.attributeValue || '')
222236
}
223237

224238
#handleChildListMutation(mutation: TraceMutation) {
@@ -296,80 +310,52 @@ export class DevtoolsBrowser extends Element {
296310

297311
async #renderBrowserState(mutationEntry?: TraceMutation) {
298312
const mutations = this.mutations
299-
if (!mutations?.length) {
300-
return
301-
}
302-
303-
const targetIndex = mutationEntry ? mutations.indexOf(mutationEntry) : 0
304-
if (targetIndex < 0) {
313+
if (!mutations || !mutations.length) {
305314
return
306315
}
307316

308-
// locate nearest checkpoint (<= targetIndex)
309-
const checkpointIndices = [...this.#checkpoints.keys()].sort(
310-
(a, b) => a - b
311-
)
312-
const nearest = checkpointIndices.filter((i) => i <= targetIndex).pop()
313-
314-
if (nearest !== undefined) {
315-
// start from checkpoint clone
316-
this.#vdom = this.#checkpoints
317-
.get(nearest)!
318-
.cloneNode(true) as DocumentFragment
319-
} else {
320-
this.#vdom = document.createDocumentFragment()
321-
}
322-
323-
// find root after checkpoint (initial full doc mutation)
324-
const startIndex = nearest !== undefined ? nearest + 1 : 0
325-
let rootIndex = startIndex
326-
for (let i = startIndex; i <= targetIndex; i++) {
327-
const m = mutations[i]
328-
if (m.addedNodes.length === 1 && Boolean(m.url)) {
329-
rootIndex = i
330-
}
331-
}
332-
if (rootIndex !== startIndex) {
333-
this.#vdom = document.createDocumentFragment()
334-
}
317+
const mutationIndex = mutationEntry ? mutations.indexOf(mutationEntry) : 0
318+
this.#vdom = document.createDocumentFragment()
319+
const rootIndex =
320+
mutations
321+
.map(
322+
(m, i) =>
323+
[
324+
// is document loaded
325+
m.addedNodes.length === 1 && Boolean(m.url),
326+
// index
327+
i
328+
] as const
329+
)
330+
.filter(
331+
([isDocLoaded, docLoadedIndex]) =>
332+
isDocLoaded && docLoadedIndex <= mutationIndex
333+
)
334+
.map(([, i]) => i)
335+
.pop() || 0
335336

336337
this.#activeUrl =
337338
mutations[rootIndex].url || this.metadata?.url || 'unknown'
338-
339-
for (let i = rootIndex; i <= targetIndex; i++) {
340-
try {
341-
await this.#handleMutation(mutations[i])
342-
// create checkpoint
343-
if (i % this.#checkpointStride === 0 && !this.#checkpoints.has(i)) {
344-
this.#checkpoints.set(
345-
i,
346-
this.#vdom.cloneNode(true) as DocumentFragment
347-
)
348-
}
349-
} catch (err: any) {
350-
console.warn(`Failed to render mutation ${i}: ${err?.message}`)
351-
}
339+
for (let i = rootIndex; i <= mutationIndex; i++) {
340+
await this.#handleMutation(mutations[i]).catch((err) =>
341+
console.warn(`Failed to render mutation: ${err.message}`)
342+
)
352343
}
353344

354-
const mutation = mutations[targetIndex]
345+
/**
346+
* scroll changed element into view
347+
*/
348+
const mutation = mutations[mutationIndex]
355349
if (mutation.target) {
356350
const el = this.#queryElement(mutation.target)
357-
el?.scrollIntoView({ block: 'center', inline: 'center' })
351+
if (el) {
352+
el.scrollIntoView({ block: 'center', inline: 'center' })
353+
}
358354
}
359355

360356
this.requestUpdate()
361357
}
362358

363-
/**
364-
* Public API: jump to mutation index
365-
*/
366-
goToMutation(index: number) {
367-
const m = this.mutations[index]
368-
if (m) {
369-
this.#renderBrowserState(m)
370-
}
371-
}
372-
373359
render() {
374360
/**
375361
* render a browser state if it hasn't before

packages/service/src/session.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ export class SessionCapturer {
102102
timestamp,
103103
callSource: absPath
104104
}
105+
try {
106+
newCommand.screenshot = await browser.takeScreenshot()
107+
} catch (shotErr) {
108+
log.warn(`failed to capture screenshot: ${(shotErr as Error).message}`)
109+
}
105110
this.commandsLog.push(newCommand)
106111
this.sendUpstream('commands', [newCommand])
107112

packages/service/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface CommandLog {
99
error?: Error
1010
timestamp: number
1111
callSource: string
12+
screenshot?: string
1213
}
1314

1415
export enum TraceType {

0 commit comments

Comments
 (0)