mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
test: key asset scenario view mocks by location
This commit is contained in:
@@ -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)
|
||||
|
||||
200
scripts/browser_tests/AssetScenarioHelper.test.ts
Normal file
200
scripts/browser_tests/AssetScenarioHelper.test.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user