From 0f6d16c45432d56f94e9b9a437170274849fe68e Mon Sep 17 00:00:00 2001 From: Vishnu Vardhan Date: Tue, 26 May 2026 20:28:03 +0530 Subject: [PATCH 1/8] Retain and rerun feature initial commit --- packages/service/src/types.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/packages/service/src/types.ts b/packages/service/src/types.ts index d40898f..55fe510 100644 --- a/packages/service/src/types.ts +++ b/packages/service/src/types.ts @@ -176,3 +176,26 @@ export type StepDef = { line: number column: number } + +export interface PreservedAttempt { + testUid: string + scope: 'test' | 'suite' + capturedAt: number + window: { start: number; end: number } + test: { + title?: string + fullTitle?: string + file?: string + callSource?: string + start?: number + end?: number + duration?: number + state?: 'passed' | 'failed' | 'skipped' | 'pending' | 'running' + error?: { message: string; name?: string; stack?: string } + } + commands: CommandLog[] + consoleLogs: ConsoleLogs[] + networkRequests: NetworkRequest[] + mutations: TraceMutation[] + sources: Record +} From 0a6e3e0c43d96e0f761f99423f8cd0115f8cf1e8 Mon Sep 17 00:00:00 2001 From: Vishnu Vardhan Date: Wed, 27 May 2026 16:25:34 +0530 Subject: [PATCH 2/8] Compare tab UI changes --- .../app/src/components/sidebar/explorer.ts | 57 ++ .../app/src/components/sidebar/test-suite.ts | 40 ++ packages/app/src/components/tabs.ts | 25 +- packages/app/src/components/workbench.ts | 19 +- .../app/src/components/workbench/compare.ts | 622 ++++++++++++++++++ .../workbench/compare/compareUtils.ts | 112 ++++ packages/app/src/controller/DataManager.ts | 168 ++++- packages/app/src/controller/context.ts | 12 +- packages/app/src/controller/types.ts | 18 +- packages/backend/src/baselineStore.ts | 488 ++++++++++++++ packages/backend/src/index.ts | 81 +++ packages/service/src/reporter.ts | 65 +- packages/service/src/types.ts | 17 + 13 files changed, 1674 insertions(+), 50 deletions(-) create mode 100644 packages/app/src/components/workbench/compare.ts create mode 100644 packages/app/src/components/workbench/compare/compareUtils.ts create mode 100644 packages/backend/src/baselineStore.ts diff --git a/packages/app/src/components/sidebar/explorer.ts b/packages/app/src/components/sidebar/explorer.ts index 358e470..7633a53 100644 --- a/packages/app/src/components/sidebar/explorer.ts +++ b/packages/app/src/components/sidebar/explorer.ts @@ -40,6 +40,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { #filterListener = this.#filterTests.bind(this) #runListener = this.#handleTestRun.bind(this) #stopListener = this.#handleTestStop.bind(this) + #preserveRerunListener = this.#handlePreserveAndRerun.bind(this) static styles = [ ...Element.styles, @@ -82,6 +83,10 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { window.addEventListener('app-test-filter', this.#filterListener) this.addEventListener('app-test-run', this.#runListener as EventListener) this.addEventListener('app-test-stop', this.#stopListener as EventListener) + this.addEventListener( + 'app-test-preserve-rerun', + this.#preserveRerunListener as EventListener + ) } disconnectedCallback(): void { @@ -92,6 +97,10 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { 'app-test-stop', this.#stopListener as EventListener ) + this.removeEventListener( + 'app-test-preserve-rerun', + this.#preserveRerunListener as EventListener + ) } #filterTests({ detail }: { detail: DevtoolsSidebarFilter }) { @@ -133,6 +142,54 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { await this.#postToBackend('/api/tests/stop', {}) } + async #handlePreserveAndRerun(event: Event) { + event.stopPropagation() + const detail = (event as CustomEvent).detail + if (this.#isRunDisabledDetail(detail)) { + this.#surfaceCapabilityWarning(detail) + return + } + + // Preserve the current run's captured data BEFORE clearing it, so the + // baseline can be compared against the upcoming rerun's live capture. + try { + const response = await fetch('/api/baseline/preserve', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + testUid: detail.uid, + scope: detail.entryType + }) + }) + if (!response.ok) { + const errorText = await response.text() + window.dispatchEvent( + new CustomEvent('app-logs', { + detail: `Failed to preserve baseline: ${errorText}` + }) + ) + // Don't trigger rerun if preserve failed โ€” would lose comparison value. + return + } + } catch (error) { + window.dispatchEvent( + new CustomEvent('app-logs', { + detail: `Preserve error: ${(error as Error).message}` + }) + ) + return + } + + // Hand off to the existing rerun flow now that the baseline is pinned. + this.dispatchEvent( + new CustomEvent('app-test-run', { + detail, + bubbles: true, + composed: true + }) + ) + } + async #postToBackend(path: string, body: Record) { try { const response = await fetch(path, { diff --git a/packages/app/src/components/sidebar/test-suite.ts b/packages/app/src/components/sidebar/test-suite.ts index 934bf21..ed23795 100644 --- a/packages/app/src/components/sidebar/test-suite.ts +++ b/packages/app/src/components/sidebar/test-suite.ts @@ -17,6 +17,7 @@ import '~icons/mdi/window-close.js' import '~icons/mdi/debug-step-over.js' import '~icons/mdi/check.js' import '~icons/mdi/checkbox-blank-circle-outline.js' +import '~icons/mdi/bug-play.js' const TEST_SUITE = 'wdio-test-suite' @@ -169,6 +170,31 @@ export class ExplorerTestEntry extends CollapseableEntry { ) } + #preserveAndRerun(event: Event) { + event.stopPropagation() + if (!this.uid || this.runDisabled) { + return + } + const detail: TestRunDetail = { + uid: this.uid, + entryType: this.entryType, + specFile: this.specFile, + fullTitle: this.fullTitle, + label: this.labelText, + callSource: this.callSource, + featureFile: this.featureFile, + featureLine: this.featureLine, + suiteType: this.suiteType + } + this.dispatchEvent( + new CustomEvent('app-test-preserve-rerun', { + detail, + bubbles: true, + composed: true + }) + ) + } + get hasPassed() { return this.state === TestState.PASSED } @@ -256,6 +282,20 @@ export class ExplorerTestEntry extends CollapseableEntry { : 'group-hover/button:text-chartsGreen'}" > + ${this.hasFailed && !this.runDisabled + ? html` + + ` + : nothing} ` : !this.runDisabled ? html` diff --git a/packages/app/src/components/tabs.ts b/packages/app/src/components/tabs.ts index b49c863..90d9420 100644 --- a/packages/app/src/components/tabs.ts +++ b/packages/app/src/components/tabs.ts @@ -95,14 +95,19 @@ export class DevtoolsTabs extends Element { } } + #refreshTabList() { + this.#tabList = + this.tabs + .map((el) => el.getAttribute('label') as string) + .filter(Boolean) || [] + this.requestUpdate() + } + connectedCallback() { super.connectedCallback() setTimeout(() => { // wait till innerHTML is parsed - this.#tabList = - this.tabs - .map((el) => el.getAttribute('label') as string) - .filter(Boolean) || [] + this.#refreshTabList() /** * get tab id either from local storage or a tab element that @@ -120,7 +125,7 @@ export class DevtoolsTabs extends Element { */ if (!this.#activeTab) { this.#activeTab = this.#tabList[0] - this.tabs[0].setAttribute('active', '') + this.tabs[0]?.setAttribute('active', '') } else { this.activateTab(this.#activeTab) } @@ -134,6 +139,16 @@ export class DevtoolsTabs extends Element { }) } + firstUpdated() { + // Refresh the tab list whenever the light-DOM slot contents change โ€” + // e.g. a conditionally-rendered tab like Compare mounting/unmounting + // after the user clicks Preserve & Rerun. + const slot = this.shadowRoot?.querySelector( + 'slot:not([name])' + ) as HTMLSlotElement | null + slot?.addEventListener('slotchange', () => this.#refreshTabList()) + } + disconnectedCallback() { super.disconnectedCallback() if (this.#badgeCheckInterval) { diff --git a/packages/app/src/components/workbench.ts b/packages/app/src/components/workbench.ts index f848745..7740083 100644 --- a/packages/app/src/components/workbench.ts +++ b/packages/app/src/components/workbench.ts @@ -6,8 +6,10 @@ import { consume } from '@lit/context' import { DragController, Direction } from '../utils/DragController.js' import { consoleLogContext, - networkRequestContext + networkRequestContext, + baselineContext } from '../controller/context.js' +import type { PreservedAttempt } from '@wdio/devtools-service/types' import '~icons/mdi/arrow-collapse-down.js' import '~icons/mdi/arrow-collapse-up.js' @@ -21,6 +23,7 @@ import './workbench/logs.js' import './workbench/console.js' import './workbench/metadata.js' import './workbench/network.js' +import './workbench/compare.js' import './browser/snapshot.js' import { MIN_WORKBENCH_HEIGHT, @@ -43,6 +46,10 @@ export class DevtoolsWorkbench extends Element { @state() networkRequests: NetworkRequest[] | undefined = undefined + @consume({ context: baselineContext, subscribe: true }) + @state() + baselines: Map | undefined = undefined + static styles = [ ...Element.styles, css` @@ -215,6 +222,16 @@ export class DevtoolsWorkbench extends Element { > + ${(this.baselines?.size || 0) > 0 + ? html` + + + + ` + : nothing}