|
1 | 1 | import fs from 'fs/promises' |
2 | 2 | import os from 'os' |
3 | 3 | 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' |
19 | 6 | import serve from 'koa-static' |
20 | 7 | import mount from 'koa-mount' |
21 | 8 | 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' |
30 | 14 | import { AssetWidget } from './models/AssetWidget.js' |
31 | 15 | import { NestedAssetWidget } from './models/NestedAssetWidget.js' |
32 | 16 |
|
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 {} |
42 | 21 |
|
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-') |
53 | 35 | ) |
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 | + } |
85 | 63 | } |
86 | 64 | }, |
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' |
96 | 77 | } |
97 | | - assets = true |
98 | 78 | } |
| 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) |
99 | 100 | } |
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 }) |
116 | 103 | } |
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 | + ], |
157 | 107 |
|
158 | 108 | url: async ({ workerUrl }, use) => { |
159 | 109 | await use(workerUrl) |
160 | 110 | } |
161 | 111 | }) |
162 | 112 |
|
163 | 113 | /** |
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. |
167 | 116 | */ |
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') |
174 | 119 | 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' }) |
179 | 121 | const form = new FormData() |
180 | 122 | form.append('files', file) |
181 | 123 |
|
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 | + }) |
186 | 128 | const body = await resp.json() |
187 | 129 | return body[0] |
188 | 130 | } |
189 | 131 |
|
190 | 132 | /** |
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). |
195 | 136 | */ |
196 | 137 | export async function suppressErrors<T>( |
197 | | - app: { off: Function; on: Function; |
198 | | - logError: Function }, |
| 138 | + app: { off: Function; on: Function; logError: Function }, |
199 | 139 | fn: () => Promise<T> |
200 | 140 | ): Promise<T> { |
201 | 141 | const noop = () => {} |
|
0 commit comments