Skip to content

Commit 58f02ae

Browse files
hi-ogawaOpenCode (gpt-5.6-sol)
andauthored
feat(ui): add link to open playwright trace (#11059)
Co-authored-by: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Co-authored-by: OpenCode (gpt-5.6-sol) <noreply@opencode.ai>
1 parent dc10f5f commit 58f02ae

6 files changed

Lines changed: 198 additions & 2 deletions

File tree

packages/ui/client/components/views/ViewEditor.vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { RunnerTestFile, RunnerTask as Task, TestAnnotation, TestError } fr
44
import { until, useResizeObserver, watchDebounced } from '@vueuse/core'
55
import { createTooltip, destroyTooltip } from 'floating-vue'
66
import { computed, nextTick, onBeforeUnmount, ref, shallowRef, watch } from 'vue'
7-
import { getAttachmentUrl, sanitizeFilePath } from '~/composables/attachments'
7+
import { getAttachmentUrl, openPlaywrightTrace, sanitizeFilePath } from '~/composables/attachments'
88
import { client, config, isReport } from '~/composables/client'
99
import { finished } from '~/composables/client/state'
1010
import { codemirrorRef } from '~/composables/codemirror'
@@ -304,6 +304,19 @@ function createAnnotationElement(annotation: TestAnnotation) {
304304
notice.append(link)
305305
}
306306
else {
307+
if (annotation.type === 'traces') {
308+
const open = document.createElement('button')
309+
open.type = 'button'
310+
open.ariaLabel = 'Open trace'
311+
open.addEventListener('click', () => openPlaywrightTrace(attachment))
312+
open.classList.add('flex', 'w-min', 'gap-2', 'items-center', 'font-sans', 'underline', 'cursor-pointer')
313+
const openIcon = document.createElement('div')
314+
openIcon.classList.add('i-carbon:launch', 'block')
315+
const openText = document.createElement('span')
316+
openText.textContent = 'Open'
317+
open.append(openIcon, openText)
318+
notice.append(open)
319+
}
307320
const download = document.createElement('a')
308321
download.href = getAttachmentUrl(attachment)
309322
download.download = sanitizeFilePath(annotation.message, attachment.contentType)

packages/ui/client/components/views/ViewTestReport.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import type { RunnerTestCase } from 'vitest'
33
import { computed } from 'vue'
4-
import { getAttachmentUrl, sanitizeFilePath } from '~/composables/attachments'
4+
import { getAttachmentUrl, openPlaywrightTrace, sanitizeFilePath } from '~/composables/attachments'
55
import { config } from '~/composables/client'
66
import { getLocationString, openLocation } from '~/composables/location'
77
import AnnotationAttachmentImage from '../AnnotationAttachmentImage.vue'
@@ -79,6 +79,16 @@ const meta = computed(() => {
7979
<div flex="~ gap-2 items-center justify-between" overflow-hidden>
8080
<div class="flex gap-2" overflow-hidden>
8181
<span class="font-bold" ws-nowrap truncate>{{ annotation.type }}</span>
82+
<button
83+
v-if="annotation.type === 'traces' && annotation.attachment"
84+
class="flex gap-1 items-center text-yellow-500/80 cursor-pointer"
85+
type="button"
86+
aria-label="Open trace"
87+
@click="openPlaywrightTrace(annotation.attachment)"
88+
>
89+
<span class="i-carbon:launch block" />
90+
Open
91+
</button>
8292
<a
8393
v-if="annotation.attachment && !annotation.attachment.contentType?.startsWith('image/')"
8494
class="flex gap-1 items-center text-yellow-500/80 cursor-pointer"

packages/ui/client/composables/attachments.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,55 @@ export function internalOrExternalUrl(attachment: TestAttachment): string {
3838

3939
return getAttachmentUrl(attachment)
4040
}
41+
42+
export async function openPlaywrightTrace(attachment: TestAttachment): Promise<void> {
43+
const popup = window.open('', '_blank')
44+
if (!popup) {
45+
// eslint-disable-next-line no-alert
46+
window.alert('Unable to open Playwright Trace Viewer. Please allow pop-ups and try again.')
47+
return
48+
}
49+
50+
popup.document.write('<!doctype html><title>Opening Playwright Trace</title><body>Opening Playwright trace...</body>')
51+
popup.document.close()
52+
popup.focus()
53+
54+
try {
55+
const response = await fetch(getAttachmentUrl(attachment))
56+
if (!response.ok) {
57+
throw new Error(`Failed to load Playwright trace: ${response.statusText}`)
58+
}
59+
const trace = await response.blob()
60+
if (popup.closed) {
61+
return
62+
}
63+
64+
// Playwright signals when the trace viewer is ready to receive messages: https://github.com/microsoft/playwright/pull/42451
65+
const ready = new Promise<void>((resolve, reject) => {
66+
const timeout = setTimeout(() => {
67+
window.removeEventListener('message', onMessage)
68+
reject(new Error('Timed out waiting for Playwright Trace Viewer'))
69+
}, 10_000)
70+
function onMessage(event: MessageEvent) {
71+
if (event.origin !== 'https://trace.playwright.dev' || event.source !== popup || event.data?.method !== 'ready') {
72+
return
73+
}
74+
clearTimeout(timeout)
75+
window.removeEventListener('message', onMessage)
76+
resolve()
77+
}
78+
window.addEventListener('message', onMessage)
79+
})
80+
popup.location.href = 'https://trace.playwright.dev/next/'
81+
await ready
82+
if (!popup.closed) {
83+
popup.postMessage({ method: 'load', params: { trace } }, 'https://trace.playwright.dev')
84+
}
85+
}
86+
catch {
87+
if (!popup.closed) {
88+
const errorPage = new Blob(['<!doctype html><title>Failed to Open Playwright Trace</title><body>Failed to load Playwright trace attachment.</body>'], { type: 'text/html' })
89+
popup.location.href = URL.createObjectURL(errorPage)
90+
}
91+
}
92+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { expect, test } from 'vitest'
2+
import { page } from 'vitest/browser'
3+
4+
test('playwright trace', async () => {
5+
document.body.innerHTML = '<button>Click me</button>'
6+
const button = document.querySelector('button')!
7+
button.addEventListener('click', () => {
8+
button.textContent = 'Clicked'
9+
})
10+
await page.getByRole('button', { name: 'Click me' }).click()
11+
await expect.element(page.getByRole('button')).toHaveTextContent('Clicked')
12+
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { playwright } from '@vitest/browser-playwright'
2+
import { defineConfig } from 'vitest/config'
3+
4+
export default defineConfig({
5+
test: {
6+
browser: {
7+
enabled: true,
8+
provider: playwright(),
9+
instances: [{ browser: 'chromium', viewport: { width: 320, height: 240 } }],
10+
headless: true,
11+
trace: 'on',
12+
},
13+
},
14+
})
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import type { Page } from '@playwright/test'
2+
import type { PreviewServer } from 'vite'
3+
import type { Vitest } from 'vitest/node'
4+
import { expect, test } from '@playwright/test'
5+
import { assertTestCounts, openExplorerItem, startHtmlReportPreview, startVitestUi } from './helper'
6+
7+
test.describe('ui', () => {
8+
let vitest: Vitest | undefined
9+
let baseURL: string
10+
11+
test.beforeAll(async ({}, testInfo) => {
12+
const server = await startVitestUi({
13+
root: './fixtures/playwright-trace',
14+
watch: true,
15+
ui: true,
16+
open: false,
17+
browser: {
18+
// Playwright Test otherwise injects one worker-shared trace directory into nested browser launches.
19+
trace: { mode: 'on', tracesDir: testInfo.outputPath('vitest-traces') },
20+
},
21+
})
22+
vitest = server.vitest
23+
baseURL = server.url
24+
})
25+
26+
test.afterAll(async () => {
27+
await vitest?.close()
28+
})
29+
30+
test('opens Playwright trace viewer', async ({ page }) => {
31+
await testPlaywrightTrace(page, baseURL)
32+
})
33+
})
34+
35+
test.describe('html reporter', () => {
36+
let previewServer: PreviewServer
37+
let baseURL: string
38+
39+
test.beforeAll(async ({}, testInfo) => {
40+
const server = await startHtmlReportPreview(
41+
{
42+
root: './fixtures/playwright-trace',
43+
run: true,
44+
ui: false,
45+
reporters: 'html',
46+
browser: {
47+
// Playwright Test otherwise injects one worker-shared trace directory into nested browser launches.
48+
trace: { mode: 'on', tracesDir: testInfo.outputPath('vitest-traces') },
49+
},
50+
},
51+
{
52+
root: './fixtures/playwright-trace',
53+
build: { outDir: '.vitest' },
54+
},
55+
)
56+
previewServer = server.previewServer
57+
baseURL = `${server.url}/`
58+
})
59+
60+
test.afterAll(async () => {
61+
await previewServer.close()
62+
})
63+
64+
test('opens Playwright trace viewer', async ({ page }) => {
65+
await testPlaywrightTrace(page, baseURL)
66+
})
67+
})
68+
69+
async function testPlaywrightTrace(page: Page, baseURL: string) {
70+
await mockTraceViewer(page)
71+
await page.goto(baseURL)
72+
await assertTestCounts(page, { pass: 1, fail: 0 })
73+
await openExplorerItem(page, 'playwright trace')
74+
75+
const popupPromise = page.waitForEvent('popup')
76+
await page.getByRole('button', { name: 'Open trace' }).click()
77+
const popup = await popupPromise
78+
await expect(popup).toHaveURL('https://trace.playwright.dev/next/')
79+
await expect(popup).toHaveTitle('Loaded Playwright Trace')
80+
}
81+
82+
async function mockTraceViewer(page: Page) {
83+
await page.context().route('https://trace.playwright.dev/next/', async (route) => {
84+
await route.fulfill({
85+
contentType: 'text/html',
86+
body: `<script>
87+
addEventListener('message', (event) => {
88+
if (event.data?.method === 'load' && event.data.params?.trace instanceof Blob && event.data.params.trace.size > 0)
89+
document.title = 'Loaded Playwright Trace'
90+
})
91+
opener.postMessage({ method: 'ready' }, '*')
92+
</script>`,
93+
})
94+
})
95+
}

0 commit comments

Comments
 (0)
Sponsor
SponsoredKunjungi sekarang
Promo