From 1bbc278c2744639a8ef608e9e5660d0334a5088b Mon Sep 17 00:00:00 2001 From: lris-af Date: Fri, 21 Aug 2026 17:55:10 +1000 Subject: [PATCH] fix(browser): preserve trace popover state (fix #10906) (#11021) Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Co-authored-by: OpenCode (gpt-5.6-sol) Co-authored-by: Hiroshi Ogawa --- packages/browser/src/client/tester/trace.ts | 8 ++++-- .../ui/client/components/trace/TraceView.vue | 7 +++-- test/ui/fixtures/trace/basic.test.ts | 9 ++++++ test/ui/test/trace.spec.ts | 28 +++++++++++++++++-- 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/packages/browser/src/client/tester/trace.ts b/packages/browser/src/client/tester/trace.ts index 28cca3921..e5df35302 100644 --- a/packages/browser/src/client/tester/trace.ts +++ b/packages/browser/src/client/tester/trace.ts @@ -55,14 +55,15 @@ interface TraceSnapshot { pseudoClassIds: Record } -// rrweb-snapshot rewrites pseudo-class selectors in serialized styles so replay can -// reproduce snapshot-time states. For example: +// Dynamic pseudo-class state isn't preserved in the serialized DOM. rrweb-snapshot +// rewrites user-action selectors in serialized styles. For example: // some-selector:hover { ... } // becomes: // some-selector:hover, some-selector.\:hover { ... } // Vitest side integration then adds matching pseudo-state classes in the replay DOM. // rrweb-snapshot only handles `:hover` upstream, so we patch it locally for the -// other user-action pseudo-classes as well. +// other user-action pseudo-classes as well. Native states such as `:popover-open` +// are restored through their DOM API instead. // https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-classes#user_action_pseudo-classes const PSEUDO_CLASS_NAMES = [ ':hover', @@ -70,6 +71,7 @@ const PSEUDO_CLASS_NAMES = [ ':focus', ':focus-visible', ':focus-within', + ':popover-open', ] as const type PseudoClassName = (typeof PSEUDO_CLASS_NAMES)[number] diff --git a/packages/ui/client/components/trace/TraceView.vue b/packages/ui/client/components/trace/TraceView.vue index 744debad2..303adbbd8 100644 --- a/packages/ui/client/components/trace/TraceView.vue +++ b/packages/ui/client/components/trace/TraceView.vue @@ -83,8 +83,11 @@ watch([selectedStep, iframeEl], ([step, iframe]) => { }) for (const [className, ids] of Object.entries(pseudoClassIds)) { for (const id of ids) { - const el = mirror.getNode(id) as Element | null - if (el?.classList) { + const el = mirror.getNode(id) as HTMLElement | null + if (className === ':popover-open') { + el?.showPopover?.() + } + else if (el?.classList) { el.classList.add(className) } } diff --git a/test/ui/fixtures/trace/basic.test.ts b/test/ui/fixtures/trace/basic.test.ts index 173f2f731..46b5dbd1d 100644 --- a/test/ui/fixtures/trace/basic.test.ts +++ b/test/ui/fixtures/trace/basic.test.ts @@ -18,6 +18,15 @@ test('switch-target', async () => { await page.getByRole('button').mark('Render switch target') }) +test('popover', async () => { + document.body.innerHTML = '
Popover content
' + const popover = document.querySelector('[popover]')! + const popoverContent = page.getByText('Popover content') + await popoverContent.mark('Render closed popover') + popover.showPopover() + await popoverContent.mark('Render open popover') +}) + test('pseudo-state', async () => { document.body.innerHTML = `