Repository navigation
Expand file tree
/
Copy pathplaywright.config.ts
More file actions
369 lines (361 loc) · 20.9 KB
/
Copy pathplaywright.config.ts
File metadata and controls
369 lines (361 loc) · 20.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
import { defineConfig, devices } from "@playwright/test";
import { LAUNCH_E2E_ENV } from "./e2e/helpers/launch-token";
// Ports of every Playwright configuration, so none collides with another: this file's main server
// 3000 (E2E_PORT), offline server 3010 (E2E_OFFLINE_PORT), passkey server 3011 (E2E_PASSKEY_PORT),
// launch server 3012 (E2E_LAUNCH_PORT) and Firestore server 3013 (E2E_FIRESTORE_PORT), whose stub listens on 3014
// (E2E_FIRESTORE_STUB_PORT); playwright.base-path.config.ts's app 3020 and proxy 3021.
// Override one with its variable when that port is occupied by another instance.
const port = Number(process.env.E2E_PORT ?? 3000);
// offline-editor.spec.ts, the kafka, etcd, neo4j, milvus, qdrant and weaviate provider specs, the influxdb
// providers spec and the oxia, databend, snowflake, spark, s3, kubernetes, rabbitmq and mqtt provider specs get a second server
// process on its own port - see the projects and the webServer array below for why. Override with
// E2E_OFFLINE_PORT under the same collision circumstances as E2E_PORT.
const offlinePort = Number(process.env.E2E_OFFLINE_PORT ?? 3010);
// passkey.spec.ts gets a third server process: see the chromium-passkey project for why.
const passkeyPort = Number(process.env.E2E_PASSKEY_PORT ?? 3011);
// launch.spec.ts gets a fourth server process: see the chromium-launch project for why.
const launchPort = Number(process.env.E2E_LAUNCH_PORT ?? 3012);
// firestore-provider.spec.ts gets a fifth server process, whose FIRESTORE_EMULATOR_HOST points at the spec's own stub on a
// fixed loopback port: see the chromium-firestore project for why.
const firestorePort = Number(process.env.E2E_FIRESTORE_PORT ?? 3013);
const firestoreStubPort = Number(process.env.E2E_FIRESTORE_STUB_PORT ?? 3014);
// How long each server below may take to answer. The first one runs a cold `next build` before `next start`, and
// every other one waits for that build's .next/BUILD_ID, so each budget covers the build. Measured 2026-10-12 on
// GitHub runners: the build's first output came 65 to 114 s after the command started in ten runs of main, and
// two runs were stopped at the earlier 120 s before the build finished.
const SERVER_START_TIMEOUT_MS = 300_000;
const testCredentials = {
JWT_SECRET: "test-jwt-secret-for-e2e-tests-32ch",
ADMIN_EMAIL: "admin@libredb.org",
ADMIN_PASSWORD: "test-admin",
USER_EMAIL: "user@libredb.org",
USER_PASSWORD: "test-user",
};
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? "html" : "list",
use: {
baseURL: `http://localhost:${port}`,
trace: "on-first-retry",
screenshot: "only-on-failure",
// Kept for every test DURING A MODEL SWEEP, and only then. The agent specs are the ones
// somebody watches: a sweep runs for over an hour and the question afterwards is what the
// rail actually did, which a passing run answers as usefully as a failing one — so
// `retain-on-failure`, which deletes exactly those videos, is the wrong setting there.
//
// Scoped to the sweep's own variable rather than turned on globally, because `use` reaches
// every project and every run: `on` here would have CI keeping a video of every passing test
// in every job, which nobody watches and every artifact pays for. The comment used to claim
// the first sentence while the value did the opposite; the two now agree.
video: process.env.AGENT_MODEL_E2E ? "on" : "retain-on-failure",
// Watchable when asked for. Unset in CI and in an ordinary run, so nothing slows down by
// default; `PWSLOWMO=350` puts a beat between actions when somebody is watching the sweep.
launchOptions: { slowMo: Number(process.env.PWSLOWMO ?? 0) },
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
// offline-editor.spec.ts, the kafka, etcd, neo4j, milvus, qdrant and weaviate provider specs, the influxdb providers
// spec, the oxia, databend, snowflake, spark, s3, kubernetes, rabbitmq and mqtt provider specs and the firestore dialog spec
// run under their own projects below, against the second server, passkey.spec.ts against the third,
// launch.spec.ts against the fourth and the firestore browse spec against the fifth.
testIgnore:
/(?:offline-editor|base-path|kafka-provider|etcd-provider|neo4j-provider|milvus-provider|qdrant-provider|weaviate-provider|influxdb-providers|mtconnect-provider|modbus-provider|oxia-provider|databend-provider|snowflake-provider|spark-provider|s3-provider|kubernetes-provider|firestore-connection|firestore-provider|rabbitmq-provider|mqtt-provider|passkey|launch)\.spec\.ts/,
},
{
// Every other spec in this suite signs in as the same shared user@libredb.org account
// against the single shared server process above, and each fresh login auto-hydrates
// (health, schema/list, schema/relations, provider-meta) against that account's per-process
// "query" rate-limit bucket (src/lib/api/rate-limit.ts: 120 requests/60s by default, shared
// by every db-reaching route). That budget is sized for one real session; it is not sized
// for ~30 independent specs replaying "fresh login" back to back on one process, and by the
// time offline-editor.spec.ts's own turn came up in file order, the shared account's budget
// was already spent by everything that ran before it - a 429 on the query it runs, not a
// product defect (see e2e/offline-editor.spec.ts's header comment). Running it against a
// dedicated server process - same build output, different port - gives it rate-limit
// counters nothing else has touched, without changing the limiter, the accounts, or any
// assertion.
name: "chromium-offline-editor",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /offline-editor\.spec\.ts/,
},
{
// kafka-provider.spec.ts drives Test Connection, which spends the same per-account "query"
// bucket, and on the shared server it met the budget the specs before it had spent: CI run
// 36263561882 answered its refusal check "Too many requests. Try again in 38 seconds." on all
// three attempts. It runs against the second server for the reason offline-editor.spec.ts
// does. The specs on that server together no longer fit in that bucket's 120 requests a
// minute, so the second server raises it (#1293, see its webServer entry below).
name: "chromium-kafka",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /kafka-provider\.spec\.ts/,
},
{
// mtconnect-provider.spec.ts drives Establish Connection against the compose fixture, so it
// takes the second server for the reason kafka-provider.spec.ts does.
name: "chromium-mtconnect",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /mtconnect-provider\.spec\.ts/,
},
{
// modbus-provider.spec.ts drives Establish Connection against the compose fixture too, so it
// takes the second server for the same reason.
name: "chromium-modbus",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /modbus-provider\.spec\.ts/,
},
{
// etcd-provider.spec.ts drives Test Connection too, so it takes the second server for the reason
// kafka-provider.spec.ts does.
name: "chromium-etcd",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /etcd-provider\.spec\.ts/,
},
{
// neo4j-provider.spec.ts drives Test Connection too (two calls, one per refusal it asserts), so it
// takes the second server for the reason kafka-provider.spec.ts does.
name: "chromium-neo4j",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /neo4j-provider\.spec\.ts/,
},
{
// milvus-provider.spec.ts drives Test Connection too (one call, for the refusal it asserts), so it takes the
// second server for the reason kafka-provider.spec.ts does.
name: "chromium-milvus",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /milvus-provider\.spec\.ts/,
},
{
// qdrant-provider.spec.ts drives Test Connection too (one call, for the refusal it asserts), so it takes the
// second server for the reason kafka-provider.spec.ts does.
name: "chromium-qdrant",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /qdrant-provider\.spec\.ts/,
},
{
// weaviate-provider.spec.ts drives Test Connection too (one call, for the refusal it asserts), so it takes the
// second server for the reason kafka-provider.spec.ts does.
name: "chromium-weaviate",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /weaviate-provider\.spec\.ts/,
},
{
// influxdb-providers.spec.ts drives Test Connection too (two calls, one refusal per type it asserts), so it takes
// the second server for the reason kafka-provider.spec.ts does.
name: "chromium-influxdb",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /influxdb-providers\.spec\.ts/,
},
{
// oxia-provider.spec.ts drives Test Connection too, so it takes the second server for the reason etcd's does.
name: "chromium-oxia",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /oxia-provider\.spec\.ts/,
},
{
// databend-provider.spec.ts reaches no Databend, but each of its seven tests signs in and opens the
// connection dialog, which spends the shared account's "query" bucket: in a full local run on 2026-10-08
// three specs after it on the shared server read "Too many requests. Try again in 46 seconds.", so it
// takes the second server for the reason kafka-provider.spec.ts does.
name: "chromium-databend",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /databend-provider\.spec\.ts/,
},
{
// snowflake-provider.spec.ts reaches no Snowflake, but each of its tests signs in and opens the connection
// dialog, which spends the shared account's "query" bucket, so it takes the second server for the reason
// databend-provider.spec.ts does.
name: "chromium-snowflake",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /snowflake-provider\.spec\.ts/,
},
{
// spark-provider.spec.ts reaches no Spark server, but each of its tests signs in and opens the connection
// dialog, which spends the shared account's "query" bucket, so it takes the second server for the reason
// snowflake-provider.spec.ts does.
name: "chromium-spark",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /spark-provider\.spec\.ts/,
},
{
// s3-provider.spec.ts drives Test Connection twice (one call per refusal it asserts), so it takes the second
// server for the reason kafka-provider.spec.ts does.
name: "chromium-s3",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /s3-provider\.spec\.ts/,
},
{
// kubernetes-provider.spec.ts connects, browses and runs every console command against its stub API server,
// which spends the shared account's "query" bucket, so it takes the second server for the reason
// kafka-provider.spec.ts does.
name: "chromium-kubernetes",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /kubernetes-provider\.spec\.ts/,
},
{
// firestore-connection.spec.ts drives Test Connection for each refusal it asserts, so it takes the second server for
// the reason kafka-provider.spec.ts does. That server sets no FIRESTORE_EMULATOR_HOST, so the refusals are the ones
// a Google connection meets before any socket.
name: "chromium-firestore-dialog",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /firestore-connection\.spec\.ts/,
},
{
// firestore-provider.spec.ts browses a Firestore through a stub that replays the emulator captures of
// tests/fixtures/firestore/. FIRESTORE_EMULATOR_HOST is read from the server environment only and sends every
// Firestore connection on that server to it, so this spec has a server of its own and no other spec shares it.
name: "chromium-firestore",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${firestorePort}` },
testMatch: /firestore-provider\.spec\.ts/,
},
{
// rabbitmq-provider.spec.ts drives Test Connection and browses its stub management API, which spends the shared
// account's "query" bucket, so it takes the second server for the reason kafka-provider.spec.ts does.
name: "chromium-rabbitmq",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /rabbitmq-provider\.spec\.ts/,
},
{
// mqtt-provider.spec.ts drives Test Connection three times and each of its ten tests signs in and opens the dialog.
// On the shared server its first attempts read "Too many requests. Try again in 3 seconds." in 22 of the 30 latest CI
// runs on 2026-10-13 (main run 38292876647 among them), so it takes the second server for the reason kafka's does.
name: "chromium-mqtt",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${offlinePort}` },
testMatch: /mqtt-provider\.spec\.ts/,
},
{
// Passkeys need an account registry, so this server runs in store mode (STORAGE_PROVIDER=sqlite)
// with PASSKEY_ORIGIN set; the servers above keep the default local mode, where passkeys are off
// and the Permissions-Policy still denies publickey-credentials-get. Chromium only: the virtual
// authenticator is a Chrome DevTools Protocol feature (e2e/helpers/virtual-authenticator.ts).
name: "chromium-passkey",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${passkeyPort}` },
testMatch: /(^|\/)passkey\.spec\.ts$/,
},
{
// Launch sign-in needs the LAUNCH_TOKEN_* variables, an account registry for the launched person, and
// seeded connections for the link to open, so this server runs in store mode with a seed file of its
// own; no other server gets the launch variables, so every other spec keeps launch sign-in off.
name: "chromium-launch",
use: { ...devices["Desktop Chrome"], baseURL: `http://localhost:${launchPort}` },
testMatch: /(^|\/)launch\.spec\.ts$/,
},
{
// Scoped to the CSP spec only. The desktop shell renders under WebKitGTK, and this is the
// nearest engine available in CI; the release-time desktop smoke test remains the final
// check on the webview.eval handoff and is listed in the Phase 1 pull request description.
name: "webkit-security",
use: { ...devices["Desktop Safari"] },
testMatch: /security-headers\.spec\.ts/,
},
],
webServer: [
{
// rm -f first: without it, a local re-run reusing a stale .next/BUILD_ID from a previous
// build would let the second server below start serving mid-rebuild, before this build
// finishes overwriting it.
command: "rm -f .next/BUILD_ID && bun run build && bun start",
url: `http://localhost:${port}`,
reuseExistingServer: !process.env.CI,
timeout: SERVER_START_TIMEOUT_MS,
env: { PORT: String(port), ...testCredentials },
},
{
// Reuses the SAME build output as the server above - it only ever needs to wait for that
// build to land, never run its own. Two `next start` processes safely serving one read-only
// .next output concurrently is exactly what horizontal-scaling replicas already do.
//
// STORAGE_SQLITE_PATH is its own concern: getDataDir() (src/lib/data-dir.ts) derives the
// sample-seed directory from it regardless of STORAGE_PROVIDER, and the embedded LibreDB
// sample takes an exclusive single-writer file lock (src/lib/db/providers/embedded/libredb.ts)
// that a second process cannot also hold - pointing this server at a separate data dir avoids
// fighting the primary server for that file (and for the SQLite sample file alongside it).
//
// RATE_LIMIT_QUERY_MAX is raised because the specs here share two accounts and the
// production budget of 120 requests a minute is sized for one person, not for a suite
// (#1293). Measured on 2026-10-04: one `beforeEach` that signs in and opens the editor spends
// 7 slots before the test does anything, so the admin account's eighteen tests and their
// retries ran past 120 in one window and a Test Connection assertion read "Too many
// requests". Raised rather than set to 0, so the limiter still runs on every request; no
// spec on this server asserts its refusal (tests/unit/e2e-project-servers.test.ts).
command: "until [ -f .next/BUILD_ID ]; do sleep 1; done; bun start",
url: `http://localhost:${offlinePort}`,
reuseExistingServer: !process.env.CI,
timeout: SERVER_START_TIMEOUT_MS,
env: {
PORT: String(offlinePort),
STORAGE_SQLITE_PATH: "./data-e2e-offline/libredb-storage.db",
RATE_LIMIT_QUERY_MAX: "10000",
...testCredentials,
},
},
{
// The same shared build as the server above. rm -rf first: the account registry lives in this
// data dir, and a store left by an earlier run would keep its accounts and passkeys.
command: "rm -rf data-e2e-passkey && until [ -f .next/BUILD_ID ]; do sleep 1; done; bun start",
url: `http://localhost:${passkeyPort}`,
reuseExistingServer: !process.env.CI,
timeout: SERVER_START_TIMEOUT_MS,
env: {
PORT: String(passkeyPort),
STORAGE_PROVIDER: "sqlite",
STORAGE_SQLITE_PATH: "./data-e2e-passkey/libredb-storage.db",
// The address the browser opens; WebAuthn accepts plain http only on the host name localhost.
PASSKEY_ORIGIN: `http://localhost:${passkeyPort}`,
// Both embedded samples off: their files take a single-writer lock no spec here needs.
LIBREDB_EMBEDDED_SAMPLE: "false",
SQLITE_EMBEDDED_SAMPLE: "false",
// Without a trusted proxy every request comes from the address "unknown", so all the negative
// cases of passkey.spec.ts share one client budget; the defaults (5 and 10) would refuse them.
RATE_LIMIT_LOGIN_MAX: "100",
RATE_LIMIT_PASSKEY_MAX: "100",
...testCredentials,
},
},
{
// The same shared build as the server above. rm -rf first: a store left by an earlier run would keep
// the accounts earlier launches created.
command: "rm -rf data-e2e-launch && until [ -f .next/BUILD_ID ]; do sleep 1; done; bun start",
url: `http://localhost:${launchPort}`,
reuseExistingServer: !process.env.CI,
timeout: SERVER_START_TIMEOUT_MS,
env: {
PORT: String(launchPort),
STORAGE_PROVIDER: "sqlite",
STORAGE_SQLITE_PATH: "./data-e2e-launch/libredb-storage.db",
SEED_CONFIG_PATH: "./e2e/fixtures/launch-seed-connections.json",
// Both embedded samples off: the seed file's two connections are the whole list the spec reads.
LIBREDB_EMBEDDED_SAMPLE: "false",
SQLITE_EMBEDDED_SAMPLE: "false",
// Every request comes from the address "unknown" without a trusted proxy, so the refusal the spec
// provokes shares one client budget with every retry.
RATE_LIMIT_LOGIN_MAX: "100",
...LAUNCH_E2E_ENV,
...testCredentials,
},
},
{
// The same shared build. The stub of e2e/helpers/firestore-stub.ts listens on firestoreStubPort while the spec runs;
// the provider dials it only when a Firestore connection connects. Its own data dir, a raised query bucket for
// the reason the second server raises it, and both embedded samples off, since no spec here reads them.
command: "rm -rf data-e2e-firestore && until [ -f .next/BUILD_ID ]; do sleep 1; done; bun start",
url: `http://localhost:${firestorePort}`,
reuseExistingServer: !process.env.CI,
timeout: SERVER_START_TIMEOUT_MS,
env: {
PORT: String(firestorePort),
STORAGE_SQLITE_PATH: "./data-e2e-firestore/libredb-storage.db",
RATE_LIMIT_QUERY_MAX: "10000",
LIBREDB_EMBEDDED_SAMPLE: "false",
SQLITE_EMBEDDED_SAMPLE: "false",
FIRESTORE_EMULATOR_HOST: `127.0.0.1:${firestoreStubPort}`,
...testCredentials,
},
},
],
});