feat: add browser artifact transformer pipeline

This commit is contained in:
Neko Ayaka
2026-04-04 04:04:28 +08:00
parent 404b7371fa
commit a909d1eb3e
7 changed files with 127 additions and 14 deletions
+9 -2
View File
@@ -1,6 +1,13 @@
export { parseCaptureBrowserCliArguments } from './cli/capture'
export { applyArtifactTransformers, createImageArtifact } from './runtime/artifacts'
export { captureBrowserRoots } from './runtime/capture'
export { assertUniqueCaptureFilePaths, captureFilePath, sanitizeOutputName } from './runtime/files'
export { artifactFilePath, assertUniqueCaptureFilePaths, captureFilePath, sanitizeOutputName } from './runtime/files'
export { captureRootSelector } from './runtime/selectors'
export type { CaptureBrowserCliArguments } from './runtime/types'
export type { BrowserCaptureRequest, CapturedRootArtifact } from './runtime/types'
export type {
ArtifactTransformer,
BrowserCaptureRequest,
VishotArtifact,
VishotArtifactKind,
VishotArtifactStage,
} from './runtime/types'
@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from 'vitest'
import {
applyArtifactTransformers,
createImageArtifact,
} from './artifacts'
describe('applyArtifactTransformers', () => {
it('returns the original artifact when no transformers are configured', async () => {
const artifact = createImageArtifact({
artifactName: 'intro-chat-window',
filePath: '/tmp/intro-chat-window.png',
stage: 'browser-final',
})
expect(artifact).not.toHaveProperty('rootName')
await expect(applyArtifactTransformers(artifact, [])).resolves.toEqual([artifact])
})
it('passes the generated artifact through each transformer in order', async () => {
const first = vi.fn(async artifact => ({
...artifact,
filePath: '/tmp/intro-chat-window.avif',
format: 'avif',
}))
const second = vi.fn(async artifact => ({
...artifact,
metadata: { optimized: true },
}))
const artifact = createImageArtifact({
artifactName: 'intro-chat-window',
filePath: '/tmp/intro-chat-window.png',
stage: 'browser-final',
})
expect(artifact).not.toHaveProperty('rootName')
const result = await applyArtifactTransformers(artifact, [first, second])
expect(first).toHaveBeenCalledTimes(1)
expect(second).toHaveBeenCalledTimes(1)
expect(result).toEqual([
expect.objectContaining({
filePath: '/tmp/intro-chat-window.avif',
format: 'avif',
metadata: { optimized: true },
}),
])
})
})
@@ -0,0 +1,37 @@
import type { ArtifactTransformer, VishotArtifact, VishotArtifactStage } from './types'
export function createImageArtifact(options: {
artifactName: string
filePath: string
stage: VishotArtifactStage
metadata?: Record<string, unknown>
}): VishotArtifact {
return {
artifactName: options.artifactName,
filePath: options.filePath,
format: 'png',
kind: 'image',
metadata: options.metadata,
stage: options.stage,
}
}
export async function applyArtifactTransformers(
artifact: VishotArtifact,
transformers: ArtifactTransformer[] | undefined,
): Promise<VishotArtifact[]> {
let currentArtifacts: VishotArtifact[] = [artifact]
for (const transformer of transformers ?? []) {
const nextArtifacts: VishotArtifact[] = []
for (const currentArtifact of currentArtifacts) {
const transformed = await transformer(currentArtifact)
nextArtifacts.push(...(Array.isArray(transformed) ? transformed : [transformed]))
}
currentArtifacts = nextArtifacts
}
return currentArtifacts
}
@@ -1,6 +1,6 @@
import type { Page } from 'playwright'
import type { BrowserCaptureRequest, CapturedRootArtifact } from './types'
import type { BrowserCaptureRequest, VishotArtifact } from './types'
import path from 'node:path'
@@ -8,6 +8,7 @@ import { mkdir } from 'node:fs/promises'
import { chromium } from 'playwright'
import { applyArtifactTransformers, createImageArtifact } from './artifacts'
import { assertUniqueCaptureFilePaths, captureFilePath } from './files'
import { captureRootSelector } from './selectors'
import { startSceneViteServer } from './vite-server'
@@ -34,7 +35,7 @@ async function waitForScenarioReady(page: Page): Promise<void> {
})
}
async function captureRoot(page: Page, outputDir: string, rootName: string): Promise<CapturedRootArtifact> {
async function captureRoot(page: Page, outputDir: string, rootName: string): Promise<VishotArtifact> {
const filePath = captureFilePath(outputDir, rootName)
const locator = page.locator(captureRootSelector(rootName))
@@ -44,10 +45,11 @@ async function captureRoot(page: Page, outputDir: string, rootName: string): Pro
path: filePath,
})
return {
rootName,
return createImageArtifact({
artifactName: rootName,
filePath,
}
stage: 'browser-final',
})
}
async function resolveBaseUrl(request: BrowserCaptureRequest): Promise<{ baseUrl: string, closeServer?: () => Promise<void> }> {
@@ -67,7 +69,7 @@ async function resolveBaseUrl(request: BrowserCaptureRequest): Promise<{ baseUrl
throw new Error('Browser capture requires either "baseUrl" or "sceneAppRoot"')
}
export async function captureBrowserRoots(request: BrowserCaptureRequest): Promise<CapturedRootArtifact[]> {
export async function captureBrowserRoots(request: BrowserCaptureRequest): Promise<VishotArtifact[]> {
const { baseUrl, closeServer } = await resolveBaseUrl(request)
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined
@@ -95,10 +97,11 @@ export async function captureBrowserRoots(request: BrowserCaptureRequest): Promi
assertUniqueCaptureFilePaths(rootNames)
const artifacts: CapturedRootArtifact[] = []
const artifacts: VishotArtifact[] = []
for (const rootName of rootNames) {
artifacts.push(await captureRoot(page, request.outputDir, rootName))
const artifact = await captureRoot(page, request.outputDir, rootName)
artifacts.push(...await applyArtifactTransformers(artifact, request.imageTransformers))
}
return artifacts
@@ -13,8 +13,12 @@ export function sanitizeOutputName(name: string): string {
return sanitized.length > 0 ? sanitized : 'capture'
}
export function artifactFilePath(outputDir: string, artifactName: string, format: string): string {
return path.resolve(outputDir, `${sanitizeOutputName(artifactName)}.${format}`)
}
export function captureFilePath(outputDir: string, rootName: string): string {
return path.resolve(outputDir, `${sanitizeOutputName(rootName)}.png`)
return artifactFilePath(outputDir, rootName, 'png')
}
export function assertUniqueCaptureFilePaths(rootNames: string[]): void {
@@ -193,7 +193,7 @@ describe('captureBrowserRoots', () => {
outputDir,
})
expect(artifacts.map(item => item.rootName)).toEqual([
expect(artifacts.map(item => item.artifactName)).toEqual([
'intro-chat-window',
'intro-websocket-settings',
])
@@ -10,6 +10,7 @@ export interface BrowserCaptureRequest {
routePath: string
outputDir: string
rootNames?: string[]
imageTransformers?: ArtifactTransformer[]
viewport?: {
width: number
height: number
@@ -17,7 +18,18 @@ export interface BrowserCaptureRequest {
}
}
export interface CapturedRootArtifact {
rootName: string
export type VishotArtifactKind = 'image' | 'video'
export type VishotArtifactStage = 'browser-final' | 'electron-raw'
export interface VishotArtifact {
kind: VishotArtifactKind
stage: VishotArtifactStage
artifactName: string
filePath: string
format: string
metadata?: Record<string, unknown>
}
export type ArtifactTransformer = (
artifact: VishotArtifact,
) => Promise<VishotArtifact | VishotArtifact[]>