mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
test: extract seeded asset file builder
This commit is contained in:
@@ -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()
|
||||
|
||||
142
browser_tests/fixtures/helpers/seededAssetFiles.ts
Normal file
142
browser_tests/fixtures/helpers/seededAssetFiles.ts
Normal 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
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
85
scripts/browser_tests/seededAssetFiles.test.ts
Normal file
85
scripts/browser_tests/seededAssetFiles.test.ts
Normal 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'
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user