mirror of
https://github.com/hanzoai/vitest.git
synced 2026-09-24 17:04:25 +00:00
Co-authored-by: Hiroshi Ogawa <[email protected]> Co-authored-by: OpenCode (claude-opus-4-8) <[email protected]>
336 lines
12 KiB
TypeScript
336 lines
12 KiB
TypeScript
import { readdirSync, rmSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
import { stripVTControlCharacters } from 'node:util'
|
|
import { resolve } from 'pathe'
|
|
import { afterEach, describe, expect, test } from 'vitest'
|
|
import * as yauzl from 'yauzl'
|
|
import { buildTestProjectTree } from '../../test-utils'
|
|
import { instances, provider, runBrowserTests } from './utils'
|
|
|
|
const tracesFolder = resolve(import.meta.dirname, '../fixtures/trace-mark/__traces__')
|
|
const basicTestTracesFolder = resolve(tracesFolder, 'basic.test.ts')
|
|
|
|
describe.runIf(provider.name === 'playwright')('playwright trace marks', () => {
|
|
afterEach(() => {
|
|
rmSync(tracesFolder, { recursive: true, force: true })
|
|
})
|
|
|
|
test('vitest mark is present in zipped trace events', async () => {
|
|
const { results, ctx } = await runBrowserTests({
|
|
root: './fixtures/trace-mark',
|
|
browser: {
|
|
trace: {
|
|
mode: 'on',
|
|
screenshots: false, // makes it lighter
|
|
},
|
|
},
|
|
})
|
|
const projectTree = buildTestProjectTree(results, (testCase) => {
|
|
const result = testCase.result()
|
|
return result.state === 'failed'
|
|
? result.errors.map(e => stripVTControlCharacters(e.message))
|
|
: result.state
|
|
})
|
|
expect(Object.keys(projectTree).sort()).toEqual(instances.map(i => i.browser).sort())
|
|
|
|
for (const [name, tree] of Object.entries(projectTree)) {
|
|
expect.soft(tree).toMatchInlineSnapshot(`
|
|
{
|
|
"basic.test.ts": {
|
|
"click": "passed",
|
|
"expect.element fail": [
|
|
"expect(element).toHaveTextContent()
|
|
|
|
Expected element to have text content:
|
|
World
|
|
Received:
|
|
Hello",
|
|
],
|
|
"expect.element pass": "passed",
|
|
"failure": [
|
|
"Test failure",
|
|
],
|
|
"helper": "passed",
|
|
"locator.mark": "passed",
|
|
"mark group": "passed",
|
|
"page.mark": "passed",
|
|
"stack": "passed",
|
|
},
|
|
}
|
|
`)
|
|
|
|
const traceFiles = readdirSync(basicTestTracesFolder)
|
|
.filter(file => file.startsWith(`${name}-`) && file.endsWith('.trace.zip'))
|
|
.sort()
|
|
expect.soft(traceFiles).toEqual([
|
|
expect.stringContaining('click'),
|
|
expect.stringContaining('expect-element-fail'),
|
|
expect.stringContaining('expect-element-pass'),
|
|
expect.stringContaining('failure'),
|
|
expect.stringContaining('helper'),
|
|
expect.stringContaining('locator-mark'),
|
|
expect.stringContaining('mark-group'),
|
|
expect.stringContaining('page-mark'),
|
|
expect.stringContaining('stack'),
|
|
])
|
|
|
|
function formatStack(event: any) {
|
|
return event.stack
|
|
?.map(
|
|
(frame: any) =>
|
|
`${path.relative(ctx.config.root, frame.file)}:${frame.line}:${frame.column}`,
|
|
)
|
|
.join('\n')
|
|
}
|
|
|
|
for (const traceFile of traceFiles) {
|
|
const zipPath = resolve(basicTestTracesFolder, traceFile)
|
|
const parsed = await readTraceZip(zipPath)
|
|
const events = parsed.events.filter(event => event.type === 'before')
|
|
|
|
if (traceFile.includes('locator-mark')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
method: 'tracingGroup',
|
|
title: 'button rendered - locator',
|
|
}),
|
|
expect.objectContaining({
|
|
method: 'expect',
|
|
params: expect.objectContaining({
|
|
selector: expect.stringContaining(`internal:describe="getByRole('button')`),
|
|
}),
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'button rendered - locator')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:10:34"`)
|
|
}
|
|
|
|
if (traceFile.includes('page-mark') && !traceFile.includes('custom-stack')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
method: 'tracingGroup',
|
|
title: 'button rendered - page',
|
|
}),
|
|
expect.objectContaining({
|
|
method: 'evaluateExpression',
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'button rendered - page')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:15:14"`)
|
|
}
|
|
|
|
if (traceFile.includes('expect-element-pass')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
method: 'tracingGroup',
|
|
title: 'toHaveTextContent',
|
|
}),
|
|
expect.objectContaining({
|
|
method: 'expect',
|
|
params: expect.objectContaining({
|
|
selector: expect.stringContaining(`internal:describe="getByRole('button')`),
|
|
}),
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'toHaveTextContent')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:20:16"`)
|
|
}
|
|
|
|
if (traceFile.includes('expect-element-fail')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
method: 'tracingGroup',
|
|
title: 'button rendered',
|
|
}),
|
|
expect.objectContaining({
|
|
method: 'tracingGroup',
|
|
title: 'toHaveTextContent [ERROR]',
|
|
}),
|
|
expect.objectContaining({
|
|
method: 'expect',
|
|
params: expect.objectContaining({
|
|
selector: expect.stringContaining(`internal:describe="getByRole('button')`),
|
|
}),
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'toHaveTextContent [ERROR]')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:26:16"`)
|
|
}
|
|
|
|
if (traceFile.includes('failure')) {
|
|
const markerEvent = events.find(e => e.title === 'onAfterRetryTask [fail]')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
if (name === 'webkit') {
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:31:13"`)
|
|
}
|
|
else {
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:31:9"`)
|
|
}
|
|
}
|
|
|
|
if (traceFile.includes('click')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
// vitest command group (with source)
|
|
expect.objectContaining({
|
|
method: 'tracingGroup',
|
|
title: 'vitest:click',
|
|
}),
|
|
// playwright action (without source)
|
|
expect.objectContaining({
|
|
method: 'click',
|
|
params: expect.objectContaining({
|
|
selector: expect.stringContaining(`internal:describe="getByRole('button')`),
|
|
}),
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'vitest:click')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:36:34"`)
|
|
}
|
|
|
|
if (traceFile.includes('helper')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
title: 'render helper',
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'render helper')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:45:9"`)
|
|
}
|
|
|
|
if (traceFile.includes('stack')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
title: 'button rendered - stack',
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'button rendered - stack')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
if (name === 'webkit') {
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:50:21"`)
|
|
}
|
|
else {
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:50:17"`)
|
|
}
|
|
}
|
|
|
|
if (traceFile.includes('mark-group')) {
|
|
expect(events).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
title: 'render group',
|
|
}),
|
|
]),
|
|
)
|
|
const markerEvent = events.find(e => e.title === 'render group')
|
|
const formattedFrame = formatStack(markerEvent)
|
|
expect(formattedFrame).toMatchInlineSnapshot(`"basic.test.ts:55:14"`)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
async function readTraceZip(zipPath: string): Promise<{ entries: string[]; events: any[] }> {
|
|
const zipFile = new ZipFile(zipPath)
|
|
try {
|
|
const entries = await zipFile.entries()
|
|
const traceText = (await zipFile.read('trace.trace')).toString('utf-8')
|
|
const events = traceText
|
|
.split('\n')
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
return JSON.parse(line)
|
|
})
|
|
return { entries, events }
|
|
}
|
|
finally {
|
|
zipFile.close()
|
|
}
|
|
}
|
|
|
|
// https://github.com/microsoft/playwright/blob/cd36dab6ecc7f4b3adeec333e55f9ac03711a9b1/packages/playwright-core/src/server/utils/zipFile.ts#L21
|
|
class ZipFile {
|
|
private readonly fileName: string
|
|
private zipFile?: yauzl.ZipFile
|
|
private readonly openedPromise: Promise<void>
|
|
private readonly entriesMap = new Map<string, yauzl.Entry>()
|
|
|
|
constructor(fileName: string) {
|
|
this.fileName = fileName
|
|
this.openedPromise = this.open()
|
|
}
|
|
|
|
private async open(): Promise<void> {
|
|
this.zipFile = await new Promise<yauzl.ZipFile>((resolve, reject) => {
|
|
yauzl.open(this.fileName, { lazyEntries: true, autoClose: false }, (error, zipFile) => {
|
|
if (error || !zipFile) {
|
|
reject(error || new Error(`Cannot open zip: ${this.fileName}`))
|
|
return
|
|
}
|
|
resolve(zipFile)
|
|
})
|
|
})
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.zipFile!.readEntry()
|
|
this.zipFile!.on('entry', (entry) => {
|
|
this.entriesMap.set(entry.fileName, entry)
|
|
this.zipFile!.readEntry()
|
|
})
|
|
this.zipFile!.on('end', resolve)
|
|
this.zipFile!.on('error', reject)
|
|
})
|
|
}
|
|
|
|
async entries(): Promise<string[]> {
|
|
await this.openedPromise
|
|
return [...this.entriesMap.keys()]
|
|
}
|
|
|
|
async read(entryPath: string): Promise<Buffer> {
|
|
await this.openedPromise
|
|
const entry = this.entriesMap.get(entryPath)
|
|
if (!entry || !this.zipFile) {
|
|
throw new Error(`${entryPath} not found in file ${this.fileName}`)
|
|
}
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
this.zipFile!.openReadStream(entry, (error, stream) => {
|
|
if (error || !stream) {
|
|
reject(error || new Error(`Cannot read ${entryPath} from file ${this.fileName}`))
|
|
return
|
|
}
|
|
|
|
const buffers: Buffer[] = []
|
|
stream.on('data', data => buffers.push(data))
|
|
stream.on('error', reject)
|
|
stream.on('end', () => resolve(Buffer.concat(buffers)))
|
|
})
|
|
})
|
|
}
|
|
|
|
close(): void {
|
|
this.zipFile?.close()
|
|
}
|
|
}
|