test: extract seeded asset file builder

This commit is contained in:
Benjamin Lu
2026-04-09 13:35:31 -07:00
parent a721bdc67b
commit 467c556da3
4 changed files with 256 additions and 207 deletions

View File

@@ -1,6 +1,4 @@
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import type { Page, Route } from '@playwright/test'
import type { JobDetailResponse, JobEntry } from '@comfyorg/ingest-types'
@@ -13,16 +11,15 @@ import type {
} from './assetScenarioTypes'
import { InMemoryJobsBackend } from './InMemoryJobsBackend'
import { getMimeType } from './mimeTypeUtil'
import {
buildSeededFileKey,
buildSeededFiles,
defaultFileFor
} from './seededAssetFiles'
import type { SeededAssetFile } from './seededAssetFiles'
const inputFilesRoutePattern = /\/internal\/files\/input(?:\?.*)?$/
const viewRoutePattern = /\/api\/view(?:\?.*)?$/
const helperDir = path.dirname(fileURLToPath(import.meta.url))
type SeededAssetFile = {
filePath?: string
contentType?: string
textContent?: string
}
type MockPreviewOutput = NonNullable<JobEntry['preview_output']> & {
filename?: string
@@ -33,79 +30,6 @@ 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()
if (normalized.endsWith('.png')) {
return {
filePath: getFixturePath('workflowInMedia/workflow_itxt.png'),
contentType: 'image/png'
}
}
if (normalized.endsWith('.webp')) {
return {
filePath: getFixturePath('example.webp'),
contentType: 'image/webp'
}
}
if (normalized.endsWith('.webm')) {
return {
filePath: getFixturePath('workflowInMedia/workflow.webm'),
contentType: 'video/webm'
}
}
if (normalized.endsWith('.mp4')) {
return {
filePath: getFixturePath('workflowInMedia/workflow.mp4'),
contentType: 'video/mp4'
}
}
if (normalized.endsWith('.glb')) {
return {
filePath: getFixturePath('workflowInMedia/workflow.glb'),
contentType: 'model/gltf-binary'
}
}
if (normalized.endsWith('.json')) {
return {
textContent: JSON.stringify({ mocked: true }, null, 2),
contentType: 'application/json'
}
}
return {
textContent: 'mocked asset content',
contentType: getMimeType(filename)
}
}
function normalizeOutputFixture(
output: GeneratedOutputFixture
): GeneratedOutputFixture {
@@ -210,24 +134,6 @@ 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 =
@@ -289,27 +195,10 @@ export class AssetScenarioHelper {
}): Promise<void> {
this.generatedJobs = [...generated]
this.importedFiles = [...imported]
this.seededFiles = new Map()
for (const job of this.generatedJobs) {
for (const output of job.outputs) {
const fallback = defaultFileFor(output.filename)
this.seededFiles.set(buildSeededFileKey(outputLocation(output)), {
filePath: output.filePath ?? fallback.filePath,
contentType: output.contentType ?? fallback.contentType,
textContent: fallback.textContent
})
}
}
for (const asset of this.importedFiles) {
const fallback = defaultFileFor(asset.name)
this.seededFiles.set(buildSeededFileKey(importedAssetLocation(asset)), {
filePath: asset.filePath ?? fallback.filePath,
contentType: asset.contentType ?? fallback.contentType,
textContent: fallback.textContent
})
}
this.seededFiles = buildSeededFiles({
generated: this.generatedJobs,
imported: this.importedFiles
})
await this.jobsBackend.seed(this.generatedJobs.map(buildSeededJob))
await this.ensureInputFilesRoute()

View File

@@ -0,0 +1,142 @@
import { fileURLToPath } from 'node:url'
import path from 'node:path'
import type {
GeneratedJobFixture,
GeneratedOutputFixture,
ImportedAssetFixture
} from './assetScenarioTypes'
import { getMimeType } from './mimeTypeUtil'
const helperDir = path.dirname(fileURLToPath(import.meta.url))
export type SeededAssetFile = {
filePath?: string
contentType?: string
textContent?: string
}
export type SeededFileLocation = {
filename: string
type: string
subfolder: string
}
function getFixturePath(relativePath: string): string {
return path.resolve(helperDir, '../../assets', relativePath)
}
export function buildSeededFileKey({
filename,
type,
subfolder
}: SeededFileLocation): string {
return new URLSearchParams({
filename,
type,
subfolder
}).toString()
}
export function defaultFileFor(filename: string): SeededAssetFile {
const normalized = filename.toLowerCase()
if (normalized.endsWith('.png')) {
return {
filePath: getFixturePath('workflowInMedia/workflow_itxt.png'),
contentType: 'image/png'
}
}
if (normalized.endsWith('.webp')) {
return {
filePath: getFixturePath('example.webp'),
contentType: 'image/webp'
}
}
if (normalized.endsWith('.webm')) {
return {
filePath: getFixturePath('workflowInMedia/workflow.webm'),
contentType: 'video/webm'
}
}
if (normalized.endsWith('.mp4')) {
return {
filePath: getFixturePath('workflowInMedia/workflow.mp4'),
contentType: 'video/mp4'
}
}
if (normalized.endsWith('.glb')) {
return {
filePath: getFixturePath('workflowInMedia/workflow.glb'),
contentType: 'model/gltf-binary'
}
}
if (normalized.endsWith('.json')) {
return {
textContent: JSON.stringify({ mocked: true }, null, 2),
contentType: 'application/json'
}
}
return {
textContent: 'mocked asset content',
contentType: getMimeType(filename)
}
}
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 function buildSeededFiles({
generated,
imported
}: {
generated: readonly GeneratedJobFixture[]
imported: readonly ImportedAssetFixture[]
}): Map<string, SeededAssetFile> {
const seededFiles = new Map<string, SeededAssetFile>()
for (const job of generated) {
for (const output of job.outputs) {
const fallback = defaultFileFor(output.filename)
seededFiles.set(buildSeededFileKey(outputLocation(output)), {
filePath: output.filePath ?? fallback.filePath,
contentType: output.contentType ?? fallback.contentType,
textContent: fallback.textContent
})
}
}
for (const asset of imported) {
const fallback = defaultFileFor(asset.name)
seededFiles.set(buildSeededFileKey(importedAssetLocation(asset)), {
filePath: asset.filePath ?? fallback.filePath,
contentType: asset.contentType ?? fallback.contentType,
textContent: fallback.textContent
})
}
return seededFiles
}

View File

@@ -1,15 +1,8 @@
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 { describe, expect, it, vi } from 'vitest'
import { AssetScenarioHelper } from '../../browser_tests/fixtures/helpers/AssetScenarioHelper'
import type {
GeneratedJobFixture,
ImportedAssetFixture
} from '../../browser_tests/fixtures/helpers/assetScenarioTypes'
import { createMockJob } from '../../browser_tests/fixtures/helpers/jobFixtures'
type RouteHandler = (route: Route) => Promise<void>
@@ -20,13 +13,6 @@ type RegisteredRoute = {
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(): {
@@ -106,69 +92,23 @@ async function invokeViewRoute(
}
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')
])
it('serves generated outputs and imported files through the view route', async () => {
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'
}
]
await helper.seedGeneratedHistory([
createMockJob({
id: 'job-generated',
preview_output: {
filename: 'generated.json',
subfolder: '',
type: 'output',
nodeId: '1',
mediaType: 'images'
}
],
imported: [
{
name: 'shared-name.txt',
filePath: inputFile,
contentType: 'text/plain'
}
]
})
})
])
await helper.seedImportedFiles(['imported.txt'])
const viewRouteHandler = getRouteHandler(
routes,
@@ -179,22 +119,15 @@ describe('AssetScenarioHelper', () => {
await expect(
invokeViewRoute(
viewRouteHandler,
'http://localhost/api/view?filename=shared-name.txt&type=output&subfolder='
'http://localhost/api/view?filename=generated.json&type=output&subfolder='
)
).resolves.toBe('root output')
).resolves.toBe(JSON.stringify({ mocked: true }, null, 2))
await expect(
invokeViewRoute(
viewRouteHandler,
'http://localhost/api/view?filename=shared-name.txt&type=output&subfolder=nested%2Ffolder'
'http://localhost/api/view?filename=imported.txt&type=input&subfolder='
)
).resolves.toBe('nested output')
await expect(
invokeViewRoute(
viewRouteHandler,
'http://localhost/api/view?filename=shared-name.txt&type=input&subfolder='
)
).resolves.toBe('input asset')
).resolves.toBe('mocked asset content')
})
})

View File

@@ -0,0 +1,85 @@
import { describe, expect, it } from 'vitest'
import {
buildSeededFileKey,
buildSeededFiles
} from '../../browser_tests/fixtures/helpers/seededAssetFiles'
describe('buildSeededFiles', () => {
it('keys seeded files by filename, type, and subfolder', () => {
const seededFiles = buildSeededFiles({
generated: [
{
jobId: 'job-root',
outputs: [
{
filename: 'shared-name.txt',
type: 'output',
subfolder: '',
filePath: '/tmp/root-output.txt',
contentType: 'text/plain'
}
]
},
{
jobId: 'job-nested',
outputs: [
{
filename: 'shared-name.txt',
type: 'output',
subfolder: 'nested/folder',
filePath: '/tmp/nested-output.txt',
contentType: 'text/plain'
}
]
}
],
imported: [
{
name: 'shared-name.txt',
filePath: '/tmp/input-asset.txt',
contentType: 'text/plain'
}
]
})
expect(
seededFiles.get(
buildSeededFileKey({
filename: 'shared-name.txt',
type: 'output',
subfolder: ''
})
)
).toMatchObject({
filePath: '/tmp/root-output.txt',
contentType: 'text/plain'
})
expect(
seededFiles.get(
buildSeededFileKey({
filename: 'shared-name.txt',
type: 'output',
subfolder: 'nested/folder'
})
)
).toMatchObject({
filePath: '/tmp/nested-output.txt',
contentType: 'text/plain'
})
expect(
seededFiles.get(
buildSeededFileKey({
filename: 'shared-name.txt',
type: 'input',
subfolder: ''
})
)
).toMatchObject({
filePath: '/tmp/input-asset.txt',
contentType: 'text/plain'
})
})
})