Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/guide/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ $ cd subdir && vitest # [!code --]
$ cd subdir && vitest --config ../vitest.config.ts # [!code ++]
```

### DOM Environment Global Assignments Now Update the Underlying Window

Assignments to properties on `globalThis` or `window` in `jsdom` and `happy-dom` environments are now propagated to the underlying DOM implementation. Mutable properties such as `innerWidth` can affect APIs implemented by the DOM environment, for example `happy-dom`'s `matchMedia`.

## Migrating to Vitest 4.0 {#vitest-4}

::: warning Prerequisites
Expand Down
21 changes: 13 additions & 8 deletions packages/ui/client/components/trace/TraceView.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
import type { BrowserTraceData, BrowserTraceEntry } from '../../../../browser/src/client/tester/trace'
import type { TraceSelection } from '~/composables/trace-view'
import type { NormalizedBrowserTraceData, NormalizedBrowserTraceEntry, TraceSelection } from '~/composables/trace-view'
import { createCache, createMirror, rebuild } from 'rrweb-snapshot'
// @ts-expect-error missing types
import { Pane, Splitpanes } from 'splitpanes'
Expand All @@ -9,7 +8,7 @@ import { openLocation } from '~/composables/location'
import { getTraceEntryClass, selectActiveTraceStep } from '~/composables/trace-view'

const props = defineProps<{
trace: BrowserTraceData
trace: NormalizedBrowserTraceData
selection: TraceSelection
}>()

Expand Down Expand Up @@ -90,7 +89,7 @@ watch([selectedStep, iframeEl], ([step, iframe]) => {
}
}, { immediate: true })

function getStepButtonClass(step: BrowserTraceEntry, index: number) {
function getStepButtonClass(step: NormalizedBrowserTraceEntry, index: number) {
const selected = props.selection.selectedStepIndex === index
// TODO: move trace step state colors to shared semantic UI shortcuts.
if (isTraceStepInProgress(step)) {
Expand All @@ -112,7 +111,7 @@ function formatTraceTime(ms: number) {
: `${(ms / 1000).toFixed(1)}s`
}

function formatTraceTiming(step: BrowserTraceEntry) {
function formatTraceTiming(step: NormalizedBrowserTraceEntry) {
if (isTraceStepInProgress(step)) {
return 'running'
}
Expand All @@ -123,7 +122,7 @@ function formatTraceTiming(step: BrowserTraceEntry) {
: `${startTime} · ${formatTraceTime(step.duration)}`
}

function formatStepName(step: BrowserTraceEntry) {
function formatStepName(step: NormalizedBrowserTraceEntry) {
if (step.name === 'vitest:onAfterRetryTask') {
return 'test finished'
}
Expand All @@ -133,7 +132,7 @@ function formatStepName(step: BrowserTraceEntry) {
return step.name
}

function isTraceStepInProgress(step: BrowserTraceEntry) {
function isTraceStepInProgress(step: NormalizedBrowserTraceEntry) {
return step.range?.phase === 'start'
}
</script>
Expand All @@ -150,11 +149,17 @@ function isTraceStepInProgress(step: BrowserTraceEntry) {
type="button"
data-testid="trace-step"
:data-test-range="step.range?.phase"
class="w-full text-left px-2 py-1 rounded text-sm"
class="relative w-full text-left px-2 py-1 rounded text-sm"
:class="getStepButtonClass(step, index)"
:style="{ paddingInlineStart: `${0.5 + step.depth}rem` }"
:aria-current="selection.selectedStepIndex === index ? 'step' : undefined"
@click="onSelectStep(index)"
>
<span
v-if="step.depth > 0"
class="absolute bottom-1 top-1 border-l border-gray/40 dark:border-gray/50"
:style="{ insetInlineStart: `${step.depth - 0.05}rem` }"
/>
<div class="flex items-start gap-2">
<span class="mt-0.5 h-4 w-4 flex flex-shrink-0 items-center justify-center">
<span
Expand Down
36 changes: 27 additions & 9 deletions packages/ui/client/composables/trace-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,50 @@ export interface TraceEditorMarker {
active?: boolean
}

export interface NormalizedBrowserTraceData extends BrowserTraceData {
entries: NormalizedBrowserTraceEntry[]
}

export interface NormalizedBrowserTraceEntry extends BrowserTraceEntry {
depth: number
}

export const activeTraceView = ref<TraceSelection>()

function getTraceAttemptKey(trace: BrowserTraceData): string {
return `${trace.repeats}:${trace.retry}`
}

function mergeTraceRangeEntries(entries: BrowserTraceEntry[]): BrowserTraceEntry[] {
const merged: BrowserTraceEntry[] = []
function normalizeTraceEntries(entries: BrowserTraceEntry[]): NormalizedBrowserTraceEntry[] {
const merged: NormalizedBrowserTraceEntry[] = []
const startMap = new Map<string, number>()
const parents: string[] = []

for (const entry of entries) {
const range = entry.range
if (!range) {
merged.push(entry)
merged.push({
...entry,
depth: parents.length,
})
continue
}

if (range.phase === 'start') {
startMap.set(range.id, merged.length)
merged.push(entry)
merged.push({
...entry,
depth: parents.length,
})
parents.push(range.id)
continue
}

// when range.phase === 'end'
const index = startMap.get(range.id)
if (index == null) {
// unpaired range shouldn't happen but just leave it there
merged.push(entry)
merged.push({ ...entry, depth: 0 })
continue
}

Expand All @@ -58,12 +75,13 @@ function mergeTraceRangeEntries(entries: BrowserTraceEntry[]): BrowserTraceEntry
startTime: start.startTime,
duration: entry.startTime - start.startTime,
}
parents.pop()
}

return merged
}

export function getTraceAttemptMap(artifacts: TestArtifact[]): Record<string, BrowserTraceData> {
export function getTraceAttemptMap(artifacts: TestArtifact[]): Record<string, NormalizedBrowserTraceData> {
const grouped: Record<string, BrowserTraceData[]> = {}
for (const artifact of artifacts) {
if (artifact.type !== 'internal:browserTrace') {
Expand All @@ -75,19 +93,19 @@ export function getTraceAttemptMap(artifacts: TestArtifact[]): Record<string, Br
grouped[key].push(trace)
}

const merged: Record<string, BrowserTraceData> = {}
const merged: Record<string, NormalizedBrowserTraceData> = {}
for (const [key, traces] of Object.entries(grouped)) {
const trace = traces[0]
const entries = traces.flatMap(trace => trace.entries)
merged[key] = {
...trace,
entries: mergeTraceRangeEntries(entries),
entries: normalizeTraceEntries(entries),
}
}
return merged
}

export function getSelectedTrace(selection: TraceSelection): BrowserTraceData | undefined {
export function getSelectedTrace(selection: TraceSelection): NormalizedBrowserTraceData | undefined {
const attempts = getTraceAttemptMap(selection.test.artifacts)
return selection.attemptKey
? attempts[selection.attemptKey]
Expand Down
4 changes: 4 additions & 0 deletions packages/vitest/src/integrations/env/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ export function populateGlobal(
},
set(v) {
overrideObject.set(key, v)
// propagate changes to underlying window implementation,
// which can affect other window API behavior internally, e.g.
// updating `innerWidth` affects `matchMedia("(max-width: *)")` on happy-dom.
win[key] = v
},
configurable: true,
})
Expand Down
4 changes: 4 additions & 0 deletions test/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ pnpm test --ui
# run fixture projects
pnpm test-fixtures --root fixtures/main --ui
pnpm test-fixtures --root fixtures/trace

# generate html report and use it for UI dev
pnpm -C test/ui test-fixtures --root fixtures/trace --reporter=html --ui=false --run
HTML_REPORT_DIR="$PWD/test/ui/fixtures/trace/html" pnpm -C packages/ui dev:client
```
34 changes: 34 additions & 0 deletions test/ui/fixtures/trace-stream/nested.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, test } from 'vitest'
import { page } from 'vitest/browser'
import { waitForGate } from './helper'

test('nested', async () => {
document.body.innerHTML = `
<main>
<button>Outer</button>
<button>Inner</button>
<button>Sibling</button>
</main>
`

await page.mark('Outer group', async () => {
await page.getByRole('button', { name: 'Outer' }).mark('Outer mark')
await waitForGate('nested-inner')

await page.mark('Inner group', async () => {
await page.getByRole('button', { name: 'Inner' }).mark('Inner mark')
await Promise.all([
expect
.element(page.getByRole('button', { name: 'Leaf' }), { timeout: 10000 })
.toBeVisible(),
(async () => {
await waitForGate('nested-leaf')
document.querySelector('main')!.insertAdjacentHTML('beforeend', '<button>Leaf</button>')
})(),
])
})

await waitForGate('nested-sibling')
await page.getByRole('button', { name: 'Sibling' }).mark('Sibling mark')
})
})
22 changes: 22 additions & 0 deletions test/ui/fixtures/trace/nested.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { test } from 'vitest'
import { page } from 'vitest/browser'

test('nested', async () => {
document.body.innerHTML = `
<main>
<button>Outer</button>
<button>Inner</button>
<button>Leaf</button>
<button>Sibling</button>
</main>
`

await page.mark('Outer group', async () => {
await page.getByRole('button', { name: 'Outer' }).mark('Outer mark')
await page.mark('Inner group', async () => {
await page.getByRole('button', { name: 'Inner' }).mark('Inner mark')
await page.getByRole('button', { name: 'Leaf' }).click()
})
await page.getByRole('button', { name: 'Sibling' }).mark('Sibling mark')
})
})
61 changes: 61 additions & 0 deletions test/ui/test/trace-stream.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,65 @@ test.describe('trace stream', () => {
'test finished',
])
})

test('nested range', async ({ page }) => {
await page.goto(baseURL)

const runPromise = vitest!.runTestSpecifications(
await vitest!.globTestSpecifications(['nested.test.ts']),
)

const testItem = getExplorerItem(page, 'nested')
await expect(testItem).toBeVisible()

const traceView = page.getByTestId('trace-view')
await expect(traceView).not.toBeVisible()
await testItem.click()
await expect(traceView).toBeVisible()

const traceSteps = traceView.getByTestId('trace-step')
const traceStepNames = traceView.getByTestId('trace-step-name')

await expect.poll(() => traceStepNames.allInnerTexts()).toEqual([
'Outer group',
'Outer mark',
])
await expect(traceSteps.nth(0)).toHaveAttribute('data-test-range', 'start')

await writeFile(resolve(gatesDir, 'nested-inner.txt'), 'open')
await expect.poll(() => traceStepNames.allInnerTexts()).toEqual([
'Outer group',
'Outer mark',
'Inner group',
'Inner mark',
'toBeVisible',
])
await expect(traceSteps.nth(0)).toHaveAttribute('data-test-range', 'start')
await expect(traceSteps.nth(2)).toHaveAttribute('data-test-range', 'start')
await expect(traceSteps.nth(4)).toHaveAttribute('data-test-range', 'start')

await writeFile(resolve(gatesDir, 'nested-leaf.txt'), 'open')
await expect.poll(() => traceStepNames.allInnerTexts()).toEqual([
'Outer group',
'Outer mark',
'Inner group',
'Inner mark',
'toBeVisible',
])
await expect(traceSteps.nth(2)).toHaveAttribute('data-test-range', 'end')
await expect(traceSteps.nth(4)).toHaveAttribute('data-test-range', 'end')

await writeFile(resolve(gatesDir, 'nested-sibling.txt'), 'open')
await runPromise
await expect.poll(() => traceStepNames.allInnerTexts()).toEqual([
'Outer group',
'Outer mark',
'Inner group',
'Inner mark',
'toBeVisible',
'Sibling mark',
'test finished',
])
await expect(traceSteps.nth(0)).toHaveAttribute('data-test-range', 'end')
})
})
29 changes: 27 additions & 2 deletions test/ui/test/trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ test.describe('ui', () => {

test.beforeEach(async ({ page }) => {
await page.goto(baseURL)
await assertTestCounts(page, { pass: 11, fail: 0 })
await assertTestCounts(page, { pass: 12, fail: 0 })
})

test('basic', async ({ page }) => {
Expand All @@ -53,6 +53,10 @@ test.describe('ui', () => {
await testScroll(page)
})

test('nested', async ({ page }) => {
await testNested(page)
})

test('attempts', async ({ page }) => {
await testAttempts(page)
})
Expand Down Expand Up @@ -92,7 +96,7 @@ test.describe('html reporter', () => {

test.beforeEach(async ({ page }) => {
await page.goto(baseURL)
await assertTestCounts(page, { pass: 11, fail: 0 })
await assertTestCounts(page, { pass: 12, fail: 0 })
})

test('basic', async ({ page }) => {
Expand All @@ -119,6 +123,10 @@ test.describe('html reporter', () => {
await testScroll(page)
})

test('nested', async ({ page }) => {
await testNested(page)
})

test('attempts', async ({ page }) => {
await testAttempts(page)
})
Expand Down Expand Up @@ -301,3 +309,20 @@ async function testAttempts(page: Page) {
await expect(traceFrame.getByText('retryCount: 2')).toBeVisible()
await expect(traceFrame.getByText('repeatCount: 0')).toBeVisible()
}

async function testNested(page: Page) {
await openExplorerItem(page, 'nested')

const traceView = page.getByTestId('trace-view')
const traceStepNames = traceView.getByTestId('trace-step-name')
await expect(traceView).toBeVisible()
await expect.poll(() => traceStepNames.allInnerTexts()).toEqual([
'Outer group',
'Outer mark',
'Inner group',
'Inner mark',
'click',
'Sibling mark',
'test finished',
])
}
Loading
Loading