editor: experiments QA panel (P0 of experiments plan)

New Experiments section in the rail: lists tracking experiment rules
(status, traffic, arms, targeting), shows this session's assignment via
evaluate-experiments, per-arm analytics with winner/significance, a QA
arm override (sets the storefront variant cookie), and a two-step
pause/resume kill-switch. Admin reads degrade when the tracking gate
refuses; eligibility always works. Mock tracking endpoints + harness
tests.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
mats committed 2026-11-07 12:00:37 +01:00
1 parent 40b33f89a4
commit 922562e1c2
6 files changed
+325 -1

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
import { afterEach, describe, expect, it } from 'vitest'
import { act } from 'react'
import { setup } from './harness'
import type { Harness } from './harness'
let h: Harness
afterEach(() => h?.cleanup())
const q = (sel: string) => h.shadow.querySelector<HTMLElement>(sel)
const qa = (sel: string) => [...h.shadow.querySelectorAll<HTMLElement>(sel)]
describe('experiments QA panel', () => {
it('lists experiments with status, eligibility and analytics', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.press('Experiments')
await h.settle()
const rows = qa('.cms-exp')
expect(rows.map((r) => r.querySelector('strong')?.textContent)).toEqual(['exp_hero', 'exp_checkout'])
expect(rows[0].textContent).toContain('live')
expect(rows[1].textContent).toContain('paused')
expect(rows[0].textContent).toContain('traffic 100%')
expect(rows[0].textContent).toContain('arms a / b')
expect(rows[0].textContent).toContain('page_path prefix /')
// Public evaluate: hero eligible (arm a), disabled checkout not.
expect(rows[0].textContent).toContain('you: a')
expect(rows[1].textContent).toContain('not eligible')
// Admin analytics for the live rule.
expect(rows[0].textContent).toContain('250 sessions')
expect(rows[0].textContent).toContain('b')
expect(rows[0].textContent).toContain('significant')
})
it('forcing a QA arm sets the storefront variant cookie', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.press('Experiments')
await h.settle()
const sel = q('select[aria-label=\"Force variant for exp_hero\"]') as HTMLSelectElement
const set = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, 'value')!.set!
await act(async () => {
set.call(sel, 'b')
sel.dispatchEvent(new Event('change', { bubbles: true }))
})
expect(document.cookie).toContain('exp_exp_hero=b')
})
it('pausing asks to confirm, then flips the rule via PATCH', async () => {
h = await setup()
await h.settle()
await h.press('Panel')
await h.press('Experiments')
await h.settle()
await act(async () => void q('[aria-label=\"Pause exp_hero\"]')!.click())
await h.settle()
// First click only arms the confirm …
expect(q('[aria-label=\"Pause exp_hero\"]')?.textContent).toContain('confirm')
await act(async () => void q('[aria-label=\"Pause exp_hero\"]')!.click())
await h.settle()
const rows = qa('.cms-exp')
expect(rows[0].textContent).toContain('paused')
expect(h.cms.requests.some((r) => r === 'PATCH /api/tracking/config/experiments/exp_hero/disabled')).toBe(true)
})
})