test: key asset scenario view mocks by location

This commit is contained in:
Benjamin Lu
2026-04-07 16:17:35 -07:00
parent 3654bd7e08
commit c0c0f00c24
2 changed files with 247 additions and 3 deletions

View File

@@ -33,10 +33,28 @@ type MockPreviewOutput = NonNullable<JobEntry['preview_output']> & {
display_name?: string
}
type SeededFileLocation = {
filename: string
type: string
subfolder: string
}
function getFixturePath(relativePath: string): string {
return path.resolve(helperDir, '../../assets', relativePath)
}
function buildSeededFileKey({
filename,
type,
subfolder
}: SeededFileLocation): string {
return new URLSearchParams({
filename,
type,
subfolder
}).toString()
}
function defaultFileFor(filename: string): SeededAssetFile {
const normalized = filename.toLowerCase()
@@ -192,6 +210,24 @@ function buildSeededJob(job: GeneratedJobFixture) {
return { listItem, detail }
}
function outputLocation(output: GeneratedOutputFixture): SeededFileLocation {
return {
filename: output.filename,
type: output.type ?? 'output',
subfolder: output.subfolder ?? ''
}
}
function importedAssetLocation(
asset: ImportedAssetFixture
): SeededFileLocation {
return {
filename: asset.name,
type: 'input',
subfolder: ''
}
}
export class AssetScenarioHelper {
private readonly jobsBackend: InMemoryJobsBackend
private inputFilesRouteHandler: ((route: Route) => Promise<void>) | null =
@@ -258,7 +294,7 @@ export class AssetScenarioHelper {
for (const job of this.generatedJobs) {
for (const output of job.outputs) {
const fallback = defaultFileFor(output.filename)
this.seededFiles.set(output.filename, {
this.seededFiles.set(buildSeededFileKey(outputLocation(output)), {
filePath: output.filePath ?? fallback.filePath,
contentType: output.contentType ?? fallback.contentType,
textContent: fallback.textContent
@@ -268,7 +304,7 @@ export class AssetScenarioHelper {
for (const asset of this.importedFiles) {
const fallback = defaultFileFor(asset.name)
this.seededFiles.set(asset.name, {
this.seededFiles.set(buildSeededFileKey(importedAssetLocation(asset)), {
filePath: asset.filePath ?? fallback.filePath,
contentType: asset.contentType ?? fallback.contentType,
textContent: fallback.textContent
@@ -304,6 +340,8 @@ export class AssetScenarioHelper {
this.viewRouteHandler = async (route: Route) => {
const url = new URL(route.request().url())
const filename = url.searchParams.get('filename')
const type = url.searchParams.get('type') ?? 'output'
const subfolder = url.searchParams.get('subfolder') ?? ''
if (!filename) {
await route.fulfill({
@@ -315,7 +353,13 @@ export class AssetScenarioHelper {
}
const seededFile =
this.seededFiles.get(filename) ?? defaultFileFor(filename)
this.seededFiles.get(
buildSeededFileKey({
filename,
type,
subfolder
})
) ?? defaultFileFor(filename)
if (seededFile.filePath) {
const body = await readFile(seededFile.filePath)

View File

@@ -0,0 +1,200 @@
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'
import type { Page, Route } from '@playwright/test'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AssetScenarioHelper } from '../../browser_tests/fixtures/helpers/AssetScenarioHelper'
import type {
GeneratedJobFixture,
ImportedAssetFixture
} from '../../browser_tests/fixtures/helpers/assetScenarioTypes'
type RouteHandler = (route: Route) => Promise<void>
type RegisteredRoute = {
pattern: string | RegExp
handler: RouteHandler
}
type PageStub = Pick<Page, 'route' | 'unroute'>
type AssetScenarioHelperTestAccess = {
seed(args: {
generated: GeneratedJobFixture[]
imported: ImportedAssetFixture[]
}): Promise<void>
}
type FulfillOptions = NonNullable<Parameters<Route['fulfill']>[0]>
function createPageStub(): {
page: PageStub
routes: RegisteredRoute[]
} {
const routes: RegisteredRoute[] = []
const page = {
route: vi.fn(async (pattern: string | RegExp, handler: RouteHandler) => {
routes.push({ pattern, handler })
}),
unroute: vi.fn(async () => {})
} satisfies PageStub
return { page, routes }
}
function getRouteHandler(
routes: RegisteredRoute[],
matcher: (pattern: string | RegExp) => boolean
): RouteHandler {
const registeredRoute = routes.find(({ pattern }) => matcher(pattern))
if (!registeredRoute) {
throw new Error('Expected route handler to be registered')
}
return registeredRoute.handler
}
function createRouteInvocation(url: string): {
route: Route
getFulfilled: () => FulfillOptions | undefined
} {
let fulfilled: FulfillOptions | undefined
const route = {
request: () =>
({
url: () => url
}) as ReturnType<Route['request']>,
fulfill: vi.fn(async (options?: FulfillOptions) => {
if (!options) {
throw new Error('Expected route to be fulfilled with options')
}
fulfilled = options
})
} satisfies Pick<Route, 'request' | 'fulfill'>
return {
route: route as unknown as Route,
getFulfilled: () => fulfilled
}
}
function bodyToText(body: FulfillOptions['body']): string {
if (body instanceof Uint8Array) {
return Buffer.from(body).toString('utf-8')
}
return `${body ?? ''}`
}
async function invokeViewRoute(
handler: RouteHandler,
url: string
): Promise<string> {
const invocation = createRouteInvocation(url)
await handler(invocation.route)
const fulfilled = invocation.getFulfilled()
expect(fulfilled).toBeDefined()
return bodyToText(fulfilled?.body)
}
describe('AssetScenarioHelper', () => {
let tempDir: string | undefined
afterEach(async () => {
if (tempDir) {
await rm(tempDir, { recursive: true, force: true })
tempDir = undefined
}
})
it('serves seeded files using filename, type, and subfolder together', async () => {
tempDir = await mkdtemp(
path.join(tmpdir(), 'asset-scenario-helper-view-route-')
)
const outputFile = path.join(tempDir, 'output.txt')
const nestedOutputFile = path.join(tempDir, 'nested-output.txt')
const inputFile = path.join(tempDir, 'input.txt')
await Promise.all([
writeFile(outputFile, 'root output'),
writeFile(nestedOutputFile, 'nested output'),
writeFile(inputFile, 'input asset')
])
const { page, routes } = createPageStub()
const helper = new AssetScenarioHelper(page as unknown as Page)
const testAccess = helper as unknown as AssetScenarioHelperTestAccess
await testAccess.seed({
generated: [
{
jobId: 'job-root',
outputs: [
{
filename: 'shared-name.txt',
type: 'output',
subfolder: '',
filePath: outputFile,
contentType: 'text/plain'
}
]
},
{
jobId: 'job-nested',
outputs: [
{
filename: 'shared-name.txt',
type: 'output',
subfolder: 'nested/folder',
filePath: nestedOutputFile,
contentType: 'text/plain'
}
]
}
],
imported: [
{
name: 'shared-name.txt',
filePath: inputFile,
contentType: 'text/plain'
}
]
})
const viewRouteHandler = getRouteHandler(
routes,
(pattern) =>
pattern instanceof RegExp && /api\\\/view/.test(pattern.source)
)
await expect(
invokeViewRoute(
viewRouteHandler,
'http://localhost/api/view?filename=shared-name.txt&type=output&subfolder='
)
).resolves.toBe('root output')
await expect(
invokeViewRoute(
viewRouteHandler,
'http://localhost/api/view?filename=shared-name.txt&type=output&subfolder=nested%2Ffolder'
)
).resolves.toBe('nested output')
await expect(
invokeViewRoute(
viewRouteHandler,
'http://localhost/api/view?filename=shared-name.txt&type=input&subfolder='
)
).resolves.toBe('input asset')
})
})