Skip to content

Commit d8fed0d

Browse files
committed
Tests: Extract worker-fixture lifecycle primitives, use them in assets/
- Split `tests/utils/fixture-app.ts` into reusable primitives: `startTestApp`, `bootTestDb`, `startAndWaitForAdmin`, `warmAdmin`, `teardownTestApp`. `createFixtureAppFixture` is now sugar that composes them. - Rewrite `tests/e2e/assets/fixtures.ts` worker fixture to compose those primitives directly, interleaving the scenario's unique storage config + middleware mounts + post-start storage URL update + tmpDir cleanup. No more inline createTestApp/stubSession/createTestDatabase/app.start/teardown duplication.
1 parent c913316 commit d8fed0d

2 files changed

Lines changed: 180 additions & 205 deletions

File tree

tests/e2e/assets/fixtures.ts

Lines changed: 102 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,201 +1,141 @@
11
import fs from 'fs/promises'
22
import os from 'os'
33
import path from 'path'
4-
import {
5-
test as base,
6-
expect
7-
} from '@playwright/test'
8-
import {
9-
AdminController,
10-
ModelController,
11-
AssetModel
12-
} from '@ditojs/server'
13-
14-
// AssetModel is created via a mixin and has an
15-
// empty .name, so we extend it to get a proper
16-
// named class that Application.addModels() can
17-
// register under the key 'Asset'.
18-
class Asset extends AssetModel {}
4+
import { test as base, expect } from '@playwright/test'
5+
import { AdminController, ModelController, AssetModel } from '@ditojs/server'
196
import serve from 'koa-static'
207
import mount from 'koa-mount'
218
import {
22-
createTestApp,
23-
getAppUrl,
24-
stubSession
25-
} from '../../utils/app.js'
26-
import {
27-
createTestDatabase
28-
} from '../../utils/database.js'
29-
import { waitForUrl } from '../../utils/net.js'
9+
startTestApp,
10+
bootTestDb,
11+
startAndWaitForAdmin,
12+
teardownTestApp
13+
} from '../../utils/fixture-app.js'
3014
import { AssetWidget } from './models/AssetWidget.js'
3115
import { NestedAssetWidget } from './models/NestedAssetWidget.js'
3216

33-
export { expect }
34-
export {
35-
getInput,
36-
getContainer
37-
} from '../../utils/admin.js'
38-
39-
export const fixturesDir = path.resolve(
40-
import.meta.dirname, 'fixtures'
41-
)
17+
// AssetModel is created via a mixin and has an empty `.name`, so we extend
18+
// it to get a proper named class that Application.addModels() can register
19+
// under the key 'Asset'.
20+
class Asset extends AssetModel {}
4221

43-
// Worker-scoped fixture: one app per worker,
44-
// shared across all spec files in that worker.
45-
export const test = base.extend<
46-
{ url: string },
47-
{ workerUrl: string }
48-
>({
49-
workerUrl: [async ({}, use) => {
50-
const tmpDir = await fs.mkdtemp(
51-
path.join(
52-
os.tmpdir(), 'dito-e2e-uploads-'
22+
export { expect }
23+
export { getInput, getContainer } from '../../utils/admin.js'
24+
25+
export const fixturesDir = path.resolve(import.meta.dirname, 'fixtures')
26+
27+
// Worker-scoped fixture: one app per worker, shared across all spec files
28+
// in that worker. Uses the shared lifecycle primitives plus this scenario's
29+
// custom storage config + middleware mounts.
30+
export const test = base.extend<{ url: string }, { workerUrl: string }>({
31+
workerUrl: [
32+
async ({}, use) => {
33+
const tmpDir = await fs.mkdtemp(
34+
path.join(os.tmpdir(), 'dito-e2e-uploads-')
5335
)
54-
)
55-
56-
const app = createTestApp({
57-
models: {
58-
AssetWidget,
59-
NestedAssetWidget,
60-
Asset
61-
},
62-
admin: {
63-
root: path.resolve(
64-
import.meta.dirname, 'app'
65-
),
66-
api: { url: '/api/' }
67-
},
68-
controllers: {
69-
admin: AdminController,
70-
api: {
71-
AssetWidgets: class extends ModelController {
72-
override modelClass = AssetWidget
73-
collection = {
74-
allow: ['get', 'post'] as const
75-
}
76-
member = {
77-
allow: [
78-
'get', 'patch', 'delete'
79-
] as const
80-
}
81-
assets = {
82-
files: { storage: 'test' },
83-
file: { storage: 'test' },
84-
filesSmall: { storage: 'test' }
36+
try {
37+
const app = startTestApp({
38+
appRoot: path.resolve(import.meta.dirname, 'app'),
39+
models: { AssetWidget, NestedAssetWidget, Asset },
40+
controllers: {
41+
admin: AdminController,
42+
api: {
43+
AssetWidgets: class extends ModelController {
44+
override modelClass = AssetWidget
45+
collection = { allow: ['get', 'post'] as const }
46+
member = {
47+
allow: ['get', 'patch', 'delete'] as const
48+
}
49+
assets = {
50+
files: { storage: 'test' },
51+
file: { storage: 'test' },
52+
filesSmall: { storage: 'test' }
53+
}
54+
},
55+
NestedAssetWidgets: class extends ModelController {
56+
override modelClass = NestedAssetWidget
57+
collection = { allow: ['get', 'post'] as const }
58+
member = {
59+
allow: ['get', 'patch', 'delete'] as const
60+
}
61+
assets = true
62+
}
8563
}
8664
},
87-
NestedAssetWidgets: class extends ModelController {
88-
override modelClass = NestedAssetWidget
89-
collection = {
90-
allow: ['get', 'post'] as const
91-
}
92-
member = {
93-
allow: [
94-
'get', 'patch', 'delete'
95-
] as const
65+
config: {
66+
storages: {
67+
test: {
68+
type: 'disk',
69+
path: tmpDir,
70+
url: '/uploads',
71+
allowedImports: [`file://${fixturesDir}/**`]
72+
}
73+
},
74+
assets: {
75+
cleanupTimeThreshold: '0s',
76+
danglingTimeThreshold: '24h'
9677
}
97-
assets = true
9878
}
79+
})
80+
81+
// Serve uploaded files at /uploads, fixture files at /fixtures
82+
// (for HTTP import tests). Mounts must run before app.start.
83+
app.use(mount('/uploads', serve(tmpDir)))
84+
app.use(mount('/fixtures', serve(fixturesDir)))
85+
86+
await bootTestDb(app)
87+
const url = await startAndWaitForAdmin(app)
88+
89+
// Update storage config with the actual port. Storage._getUrl()
90+
// requires an absolute base URL, and allowedImports uses the
91+
// actual port for HTTP import tests (B12).
92+
const storage = app.getStorage('test')
93+
storage.url = `${url}/uploads`
94+
storage.config.allowedImports.push(`${url}/fixtures/**`)
95+
96+
try {
97+
await use(url)
98+
} finally {
99+
await teardownTestApp(app)
99100
}
100-
},
101-
config: {
102-
storages: {
103-
test: {
104-
type: 'disk',
105-
path: tmpDir,
106-
url: '/uploads',
107-
allowedImports: [
108-
`file://${fixturesDir}/**`
109-
]
110-
}
111-
},
112-
assets: {
113-
cleanupTimeThreshold: '0s',
114-
danglingTimeThreshold: '24h'
115-
}
101+
} finally {
102+
await fs.rm(tmpDir, { recursive: true, force: true })
116103
}
117-
})
118-
119-
stubSession(app)
120-
121-
// Serve uploaded files at /uploads
122-
app.use(mount('/uploads', serve(tmpDir)))
123-
124-
// Serve fixture files at /fixtures (for
125-
// HTTP import tests)
126-
app.use(
127-
mount('/fixtures', serve(fixturesDir))
128-
)
129-
130-
await createTestDatabase(app)
131-
await app.start()
132-
const url = getAppUrl(app)
133-
134-
// Update storage config with actual port.
135-
// Storage._getUrl() requires an absolute base
136-
// URL, and allowedImports uses the actual port
137-
// for HTTP import tests (B12).
138-
const storage = app.getStorage('test')
139-
storage.url = `${url}/uploads`
140-
storage.config.allowedImports.push(
141-
`${url}/fixtures/**`
142-
)
143-
144-
await waitForUrl(`${url}/admin/`)
145-
await use(url)
146-
// Suppress errors during teardown — closing
147-
// connections triggers expected "Premature
148-
// close" errors from in-flight responses.
149-
app.off('error', app.logError)
150-
app.server?.closeAllConnections()
151-
await app.stop()
152-
await app.knex?.destroy()
153-
await fs.rm(
154-
tmpDir, { recursive: true, force: true }
155-
)
156-
}, { scope: 'worker' }],
104+
},
105+
{ scope: 'worker' }
106+
],
157107

158108
url: async ({ workerUrl }, use) => {
159109
await use(workerUrl)
160110
}
161111
})
162112

163113
/**
164-
* Upload a file via the server API and return
165-
* its metadata. Used by server and programmatic
166-
* tests that don't need a browser.
114+
* Upload a file via the server API and return its metadata. Used by server
115+
* and programmatic tests that don't need a browser.
167116
*/
168-
export async function uploadViaApi(
169-
url: string
170-
) {
171-
const filePath = path.resolve(
172-
fixturesDir, 'tiny.png'
173-
)
117+
export async function uploadViaApi(url: string) {
118+
const filePath = path.resolve(fixturesDir, 'tiny.png')
174119
const fileBuffer = await fs.readFile(filePath)
175-
const file = new File(
176-
[fileBuffer], 'tiny.png',
177-
{ type: 'image/png' }
178-
)
120+
const file = new File([fileBuffer], 'tiny.png', { type: 'image/png' })
179121
const form = new FormData()
180122
form.append('files', file)
181123

182-
const resp = await fetch(
183-
`${url}/api/asset-widgets/upload/files`,
184-
{ method: 'POST', body: form }
185-
)
124+
const resp = await fetch(`${url}/api/asset-widgets/upload/files`, {
125+
method: 'POST',
126+
body: form
127+
})
186128
const body = await resp.json()
187129
return body[0]
188130
}
189131

190132
/**
191-
* Temporarily suppress server error logging
192-
* during a callback. Use this around operations
193-
* that are expected to trigger server errors
194-
* (e.g. invalid uploads, premature closes).
133+
* Temporarily suppress server error logging during a callback. Use this
134+
* around operations that are expected to trigger server errors (e.g.
135+
* invalid uploads, premature closes).
195136
*/
196137
export async function suppressErrors<T>(
197-
app: { off: Function; on: Function;
198-
logError: Function },
138+
app: { off: Function; on: Function; logError: Function },
199139
fn: () => Promise<T>
200140
): Promise<T> {
201141
const noop = () => {}

0 commit comments

Comments
 (0)