Skip to content

Commit af8ccaf

Browse files
committed
Snapshot feature intial commit
1 parent 9c2f75a commit af8ccaf

File tree

1 file changed

+69
-47
lines changed

1 file changed

+69
-47
lines changed

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

Lines changed: 69 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const COMPONENT = 'wdio-devtools-browser'
3838
export class DevtoolsBrowser extends Element {
3939
#vdom = document.createDocumentFragment()
4040
#activeUrl?: string
41+
#resizeTimer?: number
42+
#boundResize = () => this.#debouncedResize()
43+
#checkpoints = new Map<number, DocumentFragment>()
44+
#checkpointStride = 50
4145

4246
@consume({ context: metadataContext, subscribe: true })
4347
metadata: Metadata | undefined = undefined
@@ -84,13 +88,20 @@ export class DevtoolsBrowser extends Element {
8488

8589
async connectedCallback() {
8690
super.connectedCallback()
87-
window.addEventListener('resize', this.#setIframeSize.bind(this))
88-
window.addEventListener('window-drag', this.#setIframeSize.bind(this))
91+
window.addEventListener('resize', this.#boundResize)
92+
window.addEventListener('window-drag', this.#boundResize)
8993
window.addEventListener('app-mutation-highlight', this.#highlightMutation.bind(this))
9094
window.addEventListener('app-mutation-select', (ev) => this.#renderBrowserState(ev.detail))
9195
await this.updateComplete
9296
}
9397

98+
#debouncedResize() {
99+
if (this.#resizeTimer) {
100+
window.clearTimeout(this.#resizeTimer)
101+
}
102+
this.#resizeTimer = window.setTimeout(() => this.#setIframeSize(), 80)
103+
}
104+
94105
#setIframeSize () {
95106
const metadata = this.metadata
96107
if (!this.section || !this.iframe || !this.header || !metadata) {
@@ -155,19 +166,10 @@ export class DevtoolsBrowser extends Element {
155166
}
156167

157168
async #handleMutation (mutation: TraceMutation) {
158-
if (!this.iframe) {
159-
await this.updateComplete
160-
}
161-
162-
if (mutation.type === 'attributes') {
163-
return this.#handleAttributeMutation(mutation)
164-
}
165-
if (mutation.type === 'childList') {
166-
return this.#handleChildListMutation(mutation)
167-
}
168-
if (mutation.type === 'characterData') {
169-
return this.#handleCharacterDataMutation(mutation)
170-
}
169+
if (!this.iframe) await this.updateComplete
170+
if (mutation.type === 'attributes') return this.#handleAttributeMutation(mutation)
171+
if (mutation.type === 'childList') return this.#handleChildListMutation(mutation)
172+
if (mutation.type === 'characterData') return this.#handleCharacterDataMutation(mutation)
171173
}
172174

173175
#handleCharacterDataMutation (mutation: TraceMutation) {
@@ -180,16 +182,17 @@ export class DevtoolsBrowser extends Element {
180182
}
181183

182184
#handleAttributeMutation (mutation: TraceMutation) {
183-
if (!mutation.attributeName || !mutation.attributeValue) {
185+
if (!mutation.attributeName) {
184186
return
185187
}
186-
187188
const el = this.#queryElement(mutation.target!)
188-
if (!el) {
189-
return
190-
}
189+
if (!el) return
191190

192-
el.setAttribute(mutation.attributeName, mutation.attributeValue || '')
191+
if (mutation.attributeValue === undefined || mutation.attributeValue === null) {
192+
el.removeAttribute(mutation.attributeName)
193+
} else {
194+
el.setAttribute(mutation.attributeName, mutation.attributeValue)
195+
}
193196
}
194197

195198
#handleChildListMutation (mutation: TraceMutation) {
@@ -259,45 +262,64 @@ export class DevtoolsBrowser extends Element {
259262

260263
async #renderBrowserState (mutationEntry?: TraceMutation) {
261264
const mutations = this.mutations
262-
if (!mutations || !mutations.length) {
263-
return
265+
if (!mutations?.length) return
266+
267+
const targetIndex = mutationEntry ? mutations.indexOf(mutationEntry) : 0
268+
if (targetIndex < 0) return
269+
270+
// locate nearest checkpoint (<= targetIndex)
271+
const checkpointIndices = [...this.#checkpoints.keys()].sort((a,b) => a - b)
272+
const nearest = checkpointIndices.filter(i => i <= targetIndex).pop()
273+
274+
if (nearest !== undefined) {
275+
// start from checkpoint clone
276+
this.#vdom = this.#checkpoints.get(nearest)!.cloneNode(true) as DocumentFragment
277+
} else {
278+
this.#vdom = document.createDocumentFragment()
264279
}
265280

266-
const mutationIndex = mutationEntry
267-
? mutations.indexOf(mutationEntry)
268-
: 0
269-
this.#vdom = document.createDocumentFragment()
270-
const rootIndex = mutations
271-
.map((m, i) => [
272-
// is document loaded
273-
m.addedNodes.length === 1 && Boolean(m.url),
274-
// index
275-
i
276-
] as const)
277-
.filter(([isDocLoaded, docLoadedIndex]) => isDocLoaded && docLoadedIndex <= mutationIndex)
278-
.map(([, i]) => i)
279-
.pop() || 0
281+
// find root after checkpoint (initial full doc mutation)
282+
const startIndex = nearest !== undefined ? nearest + 1 : 0
283+
let rootIndex = startIndex
284+
for (let i = startIndex; i <= targetIndex; i++) {
285+
const m = mutations[i]
286+
if (m.addedNodes.length === 1 && Boolean(m.url)) rootIndex = i
287+
}
288+
if (rootIndex !== startIndex) {
289+
this.#vdom = document.createDocumentFragment()
290+
}
280291

281292
this.#activeUrl = mutations[rootIndex].url || this.metadata?.url || 'unknown'
282-
for (let i = rootIndex; i <= mutationIndex; i++) {
283-
await this.#handleMutation(mutations[i]).catch(
284-
(err) => console.warn(`Failed to render mutation: ${err.message}`))
293+
294+
for (let i = rootIndex; i <= targetIndex; i++) {
295+
try {
296+
await this.#handleMutation(mutations[i])
297+
// create checkpoint
298+
if (i % this.#checkpointStride === 0 && !this.#checkpoints.has(i)) {
299+
this.#checkpoints.set(i, this.#vdom.cloneNode(true) as DocumentFragment)
300+
}
301+
} catch (err: any) {
302+
console.warn(`Failed to render mutation ${i}: ${err?.message}`)
303+
}
285304
}
286305

287-
/**
288-
* scroll changed element into view
289-
*/
290-
const mutation = mutations[mutationIndex]
306+
const mutation = mutations[targetIndex]
291307
if (mutation.target) {
292308
const el = this.#queryElement(mutation.target)
293-
if (el) {
294-
el.scrollIntoView({ block: 'center', inline: 'center' })
295-
}
309+
el?.scrollIntoView({ block: 'center', inline: 'center' })
296310
}
297311

298312
this.requestUpdate()
299313
}
300314

315+
/**
316+
* Public API: jump to mutation index
317+
*/
318+
goToMutation(index: number) {
319+
const m = this.mutations[index]
320+
if (m) this.#renderBrowserState(m)
321+
}
322+
301323
render() {
302324
/**
303325
* render a browser state if it hasn't before

0 commit comments

Comments
 (0)