@emesoft_company/playwright-core 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +62 -0
  2. package/dist/api/index.cjs +248 -0
  3. package/dist/api/index.cjs.map +1 -0
  4. package/dist/api/index.d.cts +12 -0
  5. package/dist/api/index.d.ts +12 -0
  6. package/dist/api/index.js +245 -0
  7. package/dist/api/index.js.map +1 -0
  8. package/dist/api-client-C_StjpPX.d.ts +58 -0
  9. package/dist/api-client-CpXo_NOU.d.cts +58 -0
  10. package/dist/auth/index.cjs +310 -0
  11. package/dist/auth/index.cjs.map +1 -0
  12. package/dist/auth/index.d.cts +98 -0
  13. package/dist/auth/index.d.ts +98 -0
  14. package/dist/auth/index.js +304 -0
  15. package/dist/auth/index.js.map +1 -0
  16. package/dist/config/index.cjs +268 -0
  17. package/dist/config/index.cjs.map +1 -0
  18. package/dist/config/index.d.cts +105 -0
  19. package/dist/config/index.d.ts +105 -0
  20. package/dist/config/index.js +254 -0
  21. package/dist/config/index.js.map +1 -0
  22. package/dist/data/index.cjs +59 -0
  23. package/dist/data/index.cjs.map +1 -0
  24. package/dist/data/index.d.cts +25 -0
  25. package/dist/data/index.d.ts +25 -0
  26. package/dist/data/index.js +50 -0
  27. package/dist/data/index.js.map +1 -0
  28. package/dist/di/index.cjs +47 -0
  29. package/dist/di/index.cjs.map +1 -0
  30. package/dist/di/index.d.cts +19 -0
  31. package/dist/di/index.d.ts +19 -0
  32. package/dist/di/index.js +45 -0
  33. package/dist/di/index.js.map +1 -0
  34. package/dist/errors/index.cjs +90 -0
  35. package/dist/errors/index.cjs.map +1 -0
  36. package/dist/errors/index.d.cts +38 -0
  37. package/dist/errors/index.d.ts +38 -0
  38. package/dist/errors/index.js +83 -0
  39. package/dist/errors/index.js.map +1 -0
  40. package/dist/expect/index.cjs +36 -0
  41. package/dist/expect/index.cjs.map +1 -0
  42. package/dist/expect/index.d.cts +30 -0
  43. package/dist/expect/index.d.ts +30 -0
  44. package/dist/expect/index.js +34 -0
  45. package/dist/expect/index.js.map +1 -0
  46. package/dist/fixtures/index.cjs +660 -0
  47. package/dist/fixtures/index.cjs.map +1 -0
  48. package/dist/fixtures/index.d.cts +68 -0
  49. package/dist/fixtures/index.d.ts +68 -0
  50. package/dist/fixtures/index.js +651 -0
  51. package/dist/fixtures/index.js.map +1 -0
  52. package/dist/index.cjs +1322 -0
  53. package/dist/index.cjs.map +1 -0
  54. package/dist/index.d.cts +21 -0
  55. package/dist/index.d.ts +21 -0
  56. package/dist/index.js +1248 -0
  57. package/dist/index.js.map +1 -0
  58. package/dist/logging/index.cjs +32 -0
  59. package/dist/logging/index.cjs.map +1 -0
  60. package/dist/logging/index.d.cts +18 -0
  61. package/dist/logging/index.d.ts +18 -0
  62. package/dist/logging/index.js +25 -0
  63. package/dist/logging/index.js.map +1 -0
  64. package/dist/reporting/index.cjs +59 -0
  65. package/dist/reporting/index.cjs.map +1 -0
  66. package/dist/reporting/index.d.cts +63 -0
  67. package/dist/reporting/index.d.ts +63 -0
  68. package/dist/reporting/index.js +52 -0
  69. package/dist/reporting/index.js.map +1 -0
  70. package/dist/retry-XqnSC8YL.d.cts +27 -0
  71. package/dist/retry-XqnSC8YL.d.ts +27 -0
  72. package/dist/schema-y5w2hbT-.d.cts +292 -0
  73. package/dist/schema-y5w2hbT-.d.ts +292 -0
  74. package/dist/types-CbWnSYEK.d.cts +5 -0
  75. package/dist/types-CbWnSYEK.d.ts +5 -0
  76. package/dist/types-CopwwpSW.d.cts +8 -0
  77. package/dist/types-CopwwpSW.d.ts +8 -0
  78. package/dist/types-hInbkPXf.d.cts +59 -0
  79. package/dist/types-hInbkPXf.d.ts +59 -0
  80. package/dist/ui/index.cjs +366 -0
  81. package/dist/ui/index.cjs.map +1 -0
  82. package/dist/ui/index.d.cts +190 -0
  83. package/dist/ui/index.d.ts +190 -0
  84. package/dist/ui/index.js +348 -0
  85. package/dist/ui/index.js.map +1 -0
  86. package/dist/utils/index.cjs +72 -0
  87. package/dist/utils/index.cjs.map +1 -0
  88. package/dist/utils/index.d.cts +27 -0
  89. package/dist/utils/index.d.ts +27 -0
  90. package/dist/utils/index.js +66 -0
  91. package/dist/utils/index.js.map +1 -0
  92. package/package.json +133 -0
package/dist/index.cjs ADDED
@@ -0,0 +1,1322 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+ var fs = require('fs');
5
+ var dotenv = require('dotenv');
6
+ var test$1 = require('@playwright/test');
7
+ var pino = require('pino');
8
+ var path = require('path');
9
+
10
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
+
12
+ var pino__default = /*#__PURE__*/_interopDefault(pino);
13
+
14
+ // src/errors/app-error.ts
15
+ var AppError = class extends Error {
16
+ code;
17
+ context;
18
+ constructor(message, options) {
19
+ super(message, { cause: options.cause });
20
+ this.name = new.target.name;
21
+ this.code = options.code;
22
+ this.context = options.context;
23
+ Object.setPrototypeOf(this, new.target.prototype);
24
+ }
25
+ };
26
+ var ConfigValidationError = class extends AppError {
27
+ constructor(message, issues, context) {
28
+ super(message, { code: "CONFIG_VALIDATION_ERROR", context: { issues, ...context } });
29
+ }
30
+ };
31
+ var ApiError = class extends AppError {
32
+ status;
33
+ method;
34
+ url;
35
+ constructor(message, options) {
36
+ super(message, options);
37
+ this.status = options.status;
38
+ this.method = options.method;
39
+ this.url = options.url;
40
+ }
41
+ };
42
+ var AuthError = class extends AppError {
43
+ constructor(message, options = {}) {
44
+ super(message, { code: "AUTH_ERROR", ...options });
45
+ }
46
+ };
47
+ var ConfigMissingError = class extends AppError {
48
+ constructor(name) {
49
+ super(`Required environment variable "${name}" is not set`, {
50
+ code: "ENV_VAR_MISSING",
51
+ context: { name }
52
+ });
53
+ }
54
+ };
55
+
56
+ // src/utils/deep-merge.ts
57
+ function isPlainObject(value) {
58
+ return typeof value === "object" && value !== null && !Array.isArray(value);
59
+ }
60
+ function deepMerge(base2, override) {
61
+ if (override === void 0) return base2;
62
+ if (!isPlainObject(base2) || !isPlainObject(override)) {
63
+ return override;
64
+ }
65
+ const result = { ...base2 };
66
+ for (const [key, value] of Object.entries(override)) {
67
+ if (value === void 0) continue;
68
+ const baseValue = base2[key];
69
+ result[key] = isPlainObject(baseValue) && isPlainObject(value) ? deepMerge(baseValue, value) : value;
70
+ }
71
+ return result;
72
+ }
73
+ function pruneUndefined(value) {
74
+ if (Array.isArray(value)) return value.map(pruneUndefined);
75
+ if (isPlainObject(value)) {
76
+ const result = {};
77
+ for (const [key, val] of Object.entries(value)) {
78
+ if (val === void 0) continue;
79
+ const pruned = pruneUndefined(val);
80
+ if (isPlainObject(pruned) && Object.keys(pruned).length === 0) continue;
81
+ result[key] = pruned;
82
+ }
83
+ return result;
84
+ }
85
+ return value;
86
+ }
87
+ var KNOWN_ENVIRONMENTS = ["local", "dev", "qa", "uat", "staging", "prod"];
88
+ var EnvironmentSchema = zod.z.string().min(1);
89
+ var TimeoutsSchema = zod.z.object({
90
+ actionMs: zod.z.number().int().positive().default(15e3),
91
+ navigationMs: zod.z.number().int().positive().default(3e4),
92
+ testMs: zod.z.number().int().positive().default(6e4),
93
+ /** Default budget for `waitForCondition` and other non-locator polling. */
94
+ waitMs: zod.z.number().int().positive().default(3e4)
95
+ });
96
+ var AuthConfigSchema = zod.z.object({
97
+ tokenUrl: zod.z.string().url().optional(),
98
+ clientId: zod.z.string().optional(),
99
+ clientSecret: zod.z.string().optional(),
100
+ username: zod.z.string().optional(),
101
+ password: zod.z.string().optional(),
102
+ /** Single-session storage state file (see `buildGlobalSetup`). */
103
+ storageStatePath: zod.z.string().optional(),
104
+ /** Directory used for per-role storage state files in multi-role setups. */
105
+ storageStateDir: zod.z.string().default(".auth")
106
+ }).default({});
107
+ var ReportingConfigSchema = zod.z.object({
108
+ allureEnabled: zod.z.boolean().default(false),
109
+ resultsDir: zod.z.string().default("allure-results"),
110
+ /** `{id}` is replaced with a test-case id — see `meta()` in `@emesoft_company/playwright-core/reporting`. */
111
+ testCaseUrlTemplate: zod.z.string().optional()
112
+ }).default({});
113
+ var RetryConfigSchema = zod.z.object({
114
+ retries: zod.z.number().int().min(0).default(2),
115
+ minDelayMs: zod.z.number().int().positive().default(200),
116
+ maxDelayMs: zod.z.number().int().positive().default(5e3)
117
+ }).default({});
118
+ var DiagnosticsConfigSchema = zod.z.object({
119
+ /** Collect browser console errors + failed requests and attach them to the report. */
120
+ enabled: zod.z.boolean().default(true),
121
+ /** Fail the test if the page logged a console error. Off by default — many apps are noisy. */
122
+ failOnConsoleError: zod.z.boolean().default(false),
123
+ /** Fail the test on an uncaught page exception. */
124
+ failOnPageError: zod.z.boolean().default(false),
125
+ /** Console messages matching any of these substrings are ignored entirely. */
126
+ ignoreConsolePatterns: zod.z.array(zod.z.string()).default([])
127
+ }).default({});
128
+ var AppConfigSchema = zod.z.object({
129
+ environment: EnvironmentSchema,
130
+ baseUrl: zod.z.string().url(),
131
+ apiBaseUrl: zod.z.string().url().optional(),
132
+ tenant: zod.z.string().optional(),
133
+ timeouts: TimeoutsSchema.default({}),
134
+ auth: AuthConfigSchema,
135
+ reporting: ReportingConfigSchema,
136
+ retry: RetryConfigSchema,
137
+ diagnostics: DiagnosticsConfigSchema,
138
+ logLevel: zod.z.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"]).default("info"),
139
+ custom: zod.z.record(zod.z.string(), zod.z.unknown()).default({})
140
+ });
141
+
142
+ // src/config/loader.ts
143
+ function readEnvConfig(env) {
144
+ return {
145
+ environment: env.TEST_ENV ?? "dev",
146
+ baseUrl: env.BASE_URL,
147
+ apiBaseUrl: env.API_BASE_URL,
148
+ tenant: env.TENANT,
149
+ logLevel: env.LOG_LEVEL,
150
+ timeouts: {
151
+ actionMs: env.ACTION_TIMEOUT_MS ? Number(env.ACTION_TIMEOUT_MS) : void 0,
152
+ navigationMs: env.NAVIGATION_TIMEOUT_MS ? Number(env.NAVIGATION_TIMEOUT_MS) : void 0,
153
+ testMs: env.TEST_TIMEOUT_MS ? Number(env.TEST_TIMEOUT_MS) : void 0,
154
+ waitMs: env.WAIT_TIMEOUT_MS ? Number(env.WAIT_TIMEOUT_MS) : void 0
155
+ },
156
+ auth: {
157
+ tokenUrl: env.AUTH_TOKEN_URL,
158
+ clientId: env.AUTH_CLIENT_ID,
159
+ clientSecret: env.AUTH_CLIENT_SECRET,
160
+ username: env.AUTH_USERNAME,
161
+ password: env.AUTH_PASSWORD,
162
+ storageStatePath: env.AUTH_STORAGE_STATE_PATH,
163
+ storageStateDir: env.AUTH_STORAGE_STATE_DIR
164
+ },
165
+ reporting: {
166
+ allureEnabled: env.REPORT_ALLURE === "true",
167
+ resultsDir: env.ALLURE_RESULTS_DIR,
168
+ testCaseUrlTemplate: env.TEST_CASE_URL_TEMPLATE
169
+ },
170
+ retry: {
171
+ retries: env.RETRY_COUNT ? Number(env.RETRY_COUNT) : void 0,
172
+ minDelayMs: env.RETRY_MIN_DELAY_MS ? Number(env.RETRY_MIN_DELAY_MS) : void 0,
173
+ maxDelayMs: env.RETRY_MAX_DELAY_MS ? Number(env.RETRY_MAX_DELAY_MS) : void 0
174
+ },
175
+ diagnostics: {
176
+ enabled: env.DIAGNOSTICS === void 0 ? void 0 : env.DIAGNOSTICS !== "false",
177
+ failOnConsoleError: env.FAIL_ON_CONSOLE_ERROR === void 0 ? void 0 : env.FAIL_ON_CONSOLE_ERROR === "true",
178
+ failOnPageError: env.FAIL_ON_PAGE_ERROR === void 0 ? void 0 : env.FAIL_ON_PAGE_ERROR === "true"
179
+ }
180
+ };
181
+ }
182
+ function requireEnv(name, env = process.env) {
183
+ const value = env[name];
184
+ if (value === void 0 || value === "") {
185
+ throw new ConfigMissingError(name);
186
+ }
187
+ return value;
188
+ }
189
+ function loadConfig(options = {}) {
190
+ const { env = process.env, overrides } = options;
191
+ const fromEnv = pruneUndefined(readEnvConfig(env));
192
+ const merged = deepMerge(fromEnv, overrides);
193
+ const result = AppConfigSchema.safeParse(merged);
194
+ if (!result.success) {
195
+ throw new ConfigValidationError(
196
+ `Invalid application config: ${result.error.issues.map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`).join("; ")}`,
197
+ result.error.issues
198
+ );
199
+ }
200
+ return result.data;
201
+ }
202
+ function loadEnvFiles(files, env = process.env) {
203
+ const merged = {};
204
+ const loaded = [];
205
+ for (const file of files) {
206
+ if (!fs.existsSync(file)) continue;
207
+ Object.assign(merged, dotenv.parse(fs.readFileSync(file)));
208
+ loaded.push(file);
209
+ }
210
+ const applied = [];
211
+ for (const [key, value] of Object.entries(merged)) {
212
+ if (env[key] === void 0) {
213
+ env[key] = value;
214
+ applied.push(key);
215
+ }
216
+ }
217
+ return { loaded, applied };
218
+ }
219
+ function defaultEnvFiles(environment) {
220
+ return [".env", ".env.local", `.env.${environment}`, `.env.${environment}.local`];
221
+ }
222
+
223
+ // src/reporting/allure-setup.ts
224
+ function buildReporterConfig(config) {
225
+ const reporters = [["html", { open: "never" }]];
226
+ if (config.reporting.allureEnabled) {
227
+ reporters.push(["allure-playwright", { resultsDir: config.reporting.resultsDir, detail: true }]);
228
+ }
229
+ return reporters;
230
+ }
231
+
232
+ // src/config/define-config.ts
233
+ function resolveEnvFiles(options, environment) {
234
+ if (options.envFile === false) return [];
235
+ if (typeof options.envFile === "string") return [options.envFile];
236
+ if (Array.isArray(options.envFile)) return options.envFile;
237
+ return defaultEnvFiles(environment);
238
+ }
239
+ function defineE2EConfig(options = {}) {
240
+ const environment = process.env.TEST_ENV ?? options.defaultEnvironment ?? "dev";
241
+ loadEnvFiles(resolveEnvFiles(options, environment));
242
+ const overrides = typeof options.configOverrides === "function" ? options.configOverrides() : options.configOverrides;
243
+ const appConfig = loadConfig({ overrides: deepMerge({ environment }, overrides) });
244
+ const browsers = options.browsers ?? ["chromium"];
245
+ const deviceByBrowser = {
246
+ chromium: "Desktop Chrome",
247
+ firefox: "Desktop Firefox",
248
+ webkit: "Desktop Safari"
249
+ };
250
+ const generated = {
251
+ testDir: options.testDir ?? "./tests",
252
+ fullyParallel: true,
253
+ retries: appConfig.retry.retries,
254
+ timeout: appConfig.timeouts.testMs,
255
+ reporter: buildReporterConfig(appConfig),
256
+ // `globalSetup` is only wired when a path is given AND a storage state path is configured —
257
+ // otherwise a project that hasn't set up login yet would fail on every run.
258
+ ...options.globalSetup && appConfig.auth.storageStatePath ? { globalSetup: options.globalSetup } : {},
259
+ ...options.globalTeardown ? { globalTeardown: options.globalTeardown } : {},
260
+ use: {
261
+ baseURL: appConfig.baseUrl,
262
+ actionTimeout: appConfig.timeouts.actionMs,
263
+ navigationTimeout: appConfig.timeouts.navigationMs,
264
+ trace: "on-first-retry",
265
+ screenshot: "only-on-failure",
266
+ ...options.globalSetup && appConfig.auth.storageStatePath ? { storageState: appConfig.auth.storageStatePath } : {}
267
+ },
268
+ projects: browsers.map((browser) => ({
269
+ name: browser,
270
+ use: { ...test$1.devices[deviceByBrowser[browser]] }
271
+ }))
272
+ };
273
+ return deepMerge(generated, options.playwright);
274
+ }
275
+
276
+ // src/auth/token-provider.ts
277
+ var StaticTokenProvider = class {
278
+ constructor(token) {
279
+ this.token = token;
280
+ }
281
+ token;
282
+ async getToken() {
283
+ return this.token;
284
+ }
285
+ invalidate() {
286
+ }
287
+ };
288
+ var NullTokenProvider = class {
289
+ async getToken() {
290
+ return "";
291
+ }
292
+ invalidate() {
293
+ }
294
+ };
295
+ var JwtTokenProvider = class {
296
+ constructor(options) {
297
+ this.options = options;
298
+ }
299
+ options;
300
+ cached;
301
+ inFlight;
302
+ async getToken() {
303
+ if (this.cached && this.cached.expiresAt > Date.now()) {
304
+ return this.cached.token;
305
+ }
306
+ if (!this.inFlight) {
307
+ this.inFlight = this.fetchToken().finally(() => {
308
+ this.inFlight = void 0;
309
+ });
310
+ }
311
+ return this.inFlight;
312
+ }
313
+ invalidate() {
314
+ this.cached = void 0;
315
+ }
316
+ async fetchToken() {
317
+ const { tokenUrl, clientId, clientSecret, username, password, refreshSkewSeconds = 30, logger: logger2, fetchImpl = fetch } = this.options;
318
+ const body = new URLSearchParams();
319
+ if (username && password) {
320
+ body.set("grant_type", "password");
321
+ body.set("username", username);
322
+ body.set("password", password);
323
+ } else {
324
+ body.set("grant_type", "client_credentials");
325
+ }
326
+ if (clientId) body.set("client_id", clientId);
327
+ if (clientSecret) body.set("client_secret", clientSecret);
328
+ logger2?.debug({ tokenUrl }, "fetching auth token");
329
+ let response;
330
+ try {
331
+ response = await fetchImpl(tokenUrl, {
332
+ method: "POST",
333
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
334
+ body
335
+ });
336
+ } catch (cause) {
337
+ throw new AuthError(`Failed to reach token endpoint ${tokenUrl}`, { cause });
338
+ }
339
+ if (!response.ok) {
340
+ const text = await response.text().catch(() => "");
341
+ throw new AuthError(`Token endpoint returned ${response.status}: ${text}`, {
342
+ context: { status: response.status, tokenUrl }
343
+ });
344
+ }
345
+ const data = await response.json();
346
+ if (!data.access_token) {
347
+ throw new AuthError("Token endpoint response missing access_token", { context: { tokenUrl } });
348
+ }
349
+ const expiresInSeconds = data.expires_in ?? 300;
350
+ this.cached = {
351
+ token: data.access_token,
352
+ expiresAt: Date.now() + Math.max(expiresInSeconds - refreshSkewSeconds, 0) * 1e3
353
+ };
354
+ return this.cached.token;
355
+ }
356
+ };
357
+
358
+ // src/utils/sleep.ts
359
+ function sleep(ms) {
360
+ return new Promise((resolve) => setTimeout(resolve, ms));
361
+ }
362
+
363
+ // src/errors/retry.ts
364
+ function backoffDelay(attempt, minDelayMs, maxDelayMs, factor) {
365
+ const exponential = minDelayMs * factor ** (attempt - 1);
366
+ const jitter = Math.random() * minDelayMs;
367
+ return Math.min(exponential + jitter, maxDelayMs);
368
+ }
369
+ async function withRetry(fn, options = {}) {
370
+ const {
371
+ retries = 2,
372
+ minDelayMs = 200,
373
+ maxDelayMs = 5e3,
374
+ factor = 2,
375
+ retryOn = () => true,
376
+ onRetry,
377
+ onGiveUp
378
+ } = options;
379
+ const maxAttempts = retries + 1;
380
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
381
+ try {
382
+ return await fn();
383
+ } catch (error) {
384
+ const isLastAttempt = attempt >= maxAttempts;
385
+ if (isLastAttempt || !retryOn(error)) {
386
+ onGiveUp?.(error, attempt);
387
+ throw error;
388
+ }
389
+ onRetry?.(error, attempt);
390
+ await sleep(backoffDelay(attempt, minDelayMs, maxDelayMs, factor));
391
+ }
392
+ }
393
+ throw new Error("withRetry: unreachable state");
394
+ }
395
+
396
+ // src/api/interceptors.ts
397
+ function isRetryableStatus(status) {
398
+ return status === 429 || status >= 500 && status <= 599;
399
+ }
400
+
401
+ // src/api/api-client.ts
402
+ var TransientHttpError = class extends Error {
403
+ constructor(status) {
404
+ super(`Transient HTTP status ${status}`);
405
+ this.status = status;
406
+ }
407
+ status;
408
+ };
409
+ var ApiClient = class {
410
+ request;
411
+ baseUrl;
412
+ tokenProvider;
413
+ logger;
414
+ retryOptions;
415
+ defaultHeaders;
416
+ interceptors;
417
+ constructor(options) {
418
+ this.request = options.request;
419
+ this.baseUrl = options.baseUrl;
420
+ this.tokenProvider = options.tokenProvider ?? new NullTokenProvider();
421
+ this.logger = options.logger;
422
+ this.defaultHeaders = options.defaultHeaders ?? {};
423
+ this.interceptors = options.interceptors ?? {};
424
+ this.retryOptions = { ...options.retry };
425
+ }
426
+ get(path, options) {
427
+ return this.requestBody("GET", path, options);
428
+ }
429
+ post(path, options) {
430
+ return this.requestBody("POST", path, options);
431
+ }
432
+ put(path, options) {
433
+ return this.requestBody("PUT", path, options);
434
+ }
435
+ patch(path, options) {
436
+ return this.requestBody("PATCH", path, options);
437
+ }
438
+ delete(path, options) {
439
+ return this.requestBody("DELETE", path, options);
440
+ }
441
+ /**
442
+ * Full response including status and headers. Retries transient statuses, then returns
443
+ * whatever came back — a 404 or 500 is a normal return value here, not an exception.
444
+ * Network/timeout failures still throw.
445
+ */
446
+ async send(method, path, options = {}) {
447
+ const url = this.resolveUrl(path, options.params);
448
+ let lastResponse;
449
+ try {
450
+ await withRetry(
451
+ async () => {
452
+ lastResponse = await this.fetchOnce(method, url, options);
453
+ if (isRetryableStatus(lastResponse.status) && !this.isExpectedStatus(lastResponse.status, options)) {
454
+ throw new TransientHttpError(lastResponse.status);
455
+ }
456
+ },
457
+ {
458
+ ...this.retryOptions,
459
+ retryOn: (error) => error instanceof TransientHttpError,
460
+ onRetry: (error, attempt) => this.logger?.warn({ method, url, attempt, err: error }, "retrying API request")
461
+ }
462
+ );
463
+ } catch (error) {
464
+ if (!(error instanceof TransientHttpError)) throw error;
465
+ }
466
+ if (!lastResponse) {
467
+ throw new AppError(`API request produced no response: ${method} ${url}`, {
468
+ code: "API_NO_RESPONSE",
469
+ context: { method, url }
470
+ });
471
+ }
472
+ return lastResponse;
473
+ }
474
+ async requestBody(method, path, options = {}) {
475
+ const response = await this.send(method, path, options);
476
+ if (!this.isExpectedStatus(response.status, options)) {
477
+ if (response.status === 401) this.tokenProvider.invalidate();
478
+ this.logger?.error(
479
+ { status: response.status, method, url: response.url, body: response.data },
480
+ "API request failed"
481
+ );
482
+ throw new ApiError(
483
+ `API request failed: ${method} ${response.url} -> ${response.status}` + (options.expectStatus === void 0 ? "" : ` (expected ${String(options.expectStatus)})`),
484
+ {
485
+ code: "API_REQUEST_FAILED",
486
+ status: response.status,
487
+ method,
488
+ url: response.url,
489
+ context: { body: response.data }
490
+ }
491
+ );
492
+ }
493
+ return response.data;
494
+ }
495
+ isExpectedStatus(status, options) {
496
+ if (options.expectStatus === void 0) return status >= 200 && status < 300;
497
+ const expected = Array.isArray(options.expectStatus) ? options.expectStatus : [options.expectStatus];
498
+ return expected.includes(status);
499
+ }
500
+ async fetchOnce(method, url, options) {
501
+ const headers = await this.buildHeaders(method, url, options);
502
+ const response = await this.request.fetch(url, {
503
+ method,
504
+ headers,
505
+ data: options.data,
506
+ form: options.form,
507
+ multipart: options.multipart,
508
+ timeout: options.timeoutMs
509
+ });
510
+ await this.interceptors.onResponse?.({ method, url, status: response.status() });
511
+ return {
512
+ status: response.status(),
513
+ ok: response.ok(),
514
+ headers: response.headers(),
515
+ data: await this.parseBody(response, options),
516
+ url,
517
+ method
518
+ };
519
+ }
520
+ resolveUrl(path, params) {
521
+ const base2 = this.baseUrl ? new URL(path, this.baseUrl).toString() : path;
522
+ if (!params) return base2;
523
+ const url = new URL(base2);
524
+ for (const [key, value] of Object.entries(params)) {
525
+ if (value !== void 0) url.searchParams.set(key, String(value));
526
+ }
527
+ return url.toString();
528
+ }
529
+ async buildHeaders(method, url, options) {
530
+ let headers = { ...this.defaultHeaders, ...options.headers };
531
+ if (!options.skipAuth) {
532
+ const token = await this.tokenProvider.getToken();
533
+ if (token) headers = { ...headers, Authorization: `Bearer ${token}` };
534
+ }
535
+ const overridden = await this.interceptors.onRequest?.({ method, url, headers });
536
+ return overridden ?? headers;
537
+ }
538
+ async parseBody(response, options) {
539
+ const raw = await this.readBody(response);
540
+ if (!options.schema) return raw;
541
+ const result = options.schema.safeParse(raw);
542
+ if (!result.success) {
543
+ throw new ApiError(`API response did not match the expected schema: ${response.url()}`, {
544
+ code: "API_SCHEMA_MISMATCH",
545
+ status: response.status(),
546
+ url: response.url(),
547
+ context: { issues: result.error.issues, body: raw }
548
+ });
549
+ }
550
+ return result.data;
551
+ }
552
+ async readBody(response) {
553
+ const contentType = response.headers()["content-type"] ?? "";
554
+ if (contentType.includes("application/json")) {
555
+ const text = await response.text();
556
+ if (text.trim() === "") return void 0;
557
+ try {
558
+ return JSON.parse(text);
559
+ } catch {
560
+ return text;
561
+ }
562
+ }
563
+ return response.text();
564
+ }
565
+ };
566
+ function createLogger(options = {}) {
567
+ const {
568
+ level = process.env.LOG_LEVEL ?? "info",
569
+ name = "playwright-core",
570
+ pretty = process.env.LOG_PRETTY === "true"
571
+ } = options;
572
+ return pino__default.default({
573
+ name,
574
+ level,
575
+ ...pretty ? {
576
+ transport: {
577
+ target: "pino-pretty",
578
+ options: { colorize: true, translateTime: "SYS:standard" }
579
+ }
580
+ } : {}
581
+ });
582
+ }
583
+ var logger = createLogger();
584
+
585
+ // src/reporting/meta.ts
586
+ function toArray(value) {
587
+ if (value === void 0) return [];
588
+ return Array.isArray(value) ? value : [value];
589
+ }
590
+ function meta(options) {
591
+ const tags = toArray(options.tag).map((tag) => tag.startsWith("@") ? tag : `@${tag}`);
592
+ const testCaseAnnotations = toArray(options.testCase).map((id) => ({
593
+ type: "testCase",
594
+ description: id
595
+ }));
596
+ const annotation = [...testCaseAnnotations, ...options.annotation ?? []];
597
+ return {
598
+ ...tags.length > 0 ? { tag: tags } : {},
599
+ ...annotation.length > 0 ? { annotation } : {}
600
+ };
601
+ }
602
+ function linkTestCases(testInfo, urlTemplate) {
603
+ if (!urlTemplate) return;
604
+ for (const annotation of testInfo.annotations) {
605
+ if (annotation.type === "testCase" && annotation.description) {
606
+ annotation.description = urlTemplate.replace("{id}", annotation.description);
607
+ }
608
+ }
609
+ }
610
+
611
+ // src/fixtures/auth.fixture.ts
612
+ function resolveTokenProvider(config, logger2) {
613
+ const { auth } = config;
614
+ if (auth.tokenUrl) {
615
+ return new JwtTokenProvider({
616
+ tokenUrl: auth.tokenUrl,
617
+ clientId: auth.clientId,
618
+ clientSecret: auth.clientSecret,
619
+ username: auth.username,
620
+ password: auth.password,
621
+ logger: logger2
622
+ });
623
+ }
624
+ return new NullTokenProvider();
625
+ }
626
+
627
+ // src/reporting/attachments.ts
628
+ async function attachText(testInfo, name, body) {
629
+ await testInfo.attach(name, { body, contentType: "text/plain" });
630
+ }
631
+ async function attachJson(testInfo, name, value) {
632
+ await testInfo.attach(name, {
633
+ body: JSON.stringify(value, null, 2),
634
+ contentType: "application/json"
635
+ });
636
+ }
637
+ async function attachScreenshot(testInfo, name, buffer) {
638
+ await testInfo.attach(name, { body: buffer, contentType: "image/png" });
639
+ }
640
+
641
+ // src/fixtures/diagnostics.fixture.ts
642
+ function collectDiagnostics(page, config) {
643
+ const collected = { consoleErrors: [], pageErrors: [], failedRequests: [] };
644
+ const isIgnored = (text) => config.ignoreConsolePatterns.some((pattern) => text.includes(pattern));
645
+ page.on("console", (message) => {
646
+ if (message.type() !== "error") return;
647
+ const text = message.text();
648
+ if (!isIgnored(text)) collected.consoleErrors.push(text);
649
+ });
650
+ page.on("pageerror", (error) => {
651
+ collected.pageErrors.push(error.message);
652
+ });
653
+ page.on("requestfailed", (request) => {
654
+ collected.failedRequests.push(
655
+ `${request.method()} ${request.url()} \u2014 ${request.failure()?.errorText ?? "failed"}`
656
+ );
657
+ });
658
+ const report = async (testInfo) => {
659
+ if (collected.consoleErrors.length > 0) {
660
+ await attachText(testInfo, "console-errors", collected.consoleErrors.join("\n"));
661
+ }
662
+ if (collected.pageErrors.length > 0) {
663
+ await attachText(testInfo, "page-errors", collected.pageErrors.join("\n"));
664
+ }
665
+ if (collected.failedRequests.length > 0) {
666
+ await attachText(testInfo, "failed-requests", collected.failedRequests.join("\n"));
667
+ }
668
+ if (testInfo.status !== "passed") return;
669
+ if (config.failOnPageError && collected.pageErrors.length > 0) {
670
+ throw new Error(`Uncaught page error(s):
671
+ ${collected.pageErrors.join("\n")}`);
672
+ }
673
+ if (config.failOnConsoleError && collected.consoleErrors.length > 0) {
674
+ throw new Error(`Console error(s):
675
+ ${collected.consoleErrors.join("\n")}`);
676
+ }
677
+ };
678
+ return { collected, report };
679
+ }
680
+ var expect = test$1.expect.extend({
681
+ /**
682
+ * Asserts the status of an `ApiResponse` from `apiClient.send()`, printing the body on
683
+ * failure — otherwise a wrong status tells you nothing about why.
684
+ */
685
+ toHaveStatus(received, expected) {
686
+ const pass = received.status === expected;
687
+ return {
688
+ pass,
689
+ name: "toHaveStatus",
690
+ expected,
691
+ actual: received.status,
692
+ message: () => pass ? `Expected status not to be ${expected}` : `Expected ${received.method} ${received.url} to return ${expected}, got ${received.status}
693
+ Body: ${JSON.stringify(received.data, null, 2)}`
694
+ };
695
+ },
696
+ /** Asserts a value matches a zod schema, listing the exact field-level issues on failure. */
697
+ toMatchSchema(received, schema) {
698
+ const result = schema.safeParse(received);
699
+ return {
700
+ pass: result.success,
701
+ name: "toMatchSchema",
702
+ message: () => result.success ? "Expected value not to match the schema" : `Value did not match the schema:
703
+ ${result.error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n")}`
704
+ };
705
+ }
706
+ });
707
+
708
+ // src/fixtures/base-test.ts
709
+ var test = test$1.test.extend({
710
+ workerConfig: [
711
+ async ({}, use) => {
712
+ await use(loadConfig());
713
+ },
714
+ { scope: "worker" }
715
+ ],
716
+ appConfig: async ({ workerConfig }, use) => {
717
+ await use(workerConfig);
718
+ },
719
+ logger: async ({ appConfig }, use, testInfo) => {
720
+ await use(createLogger({ level: appConfig.logLevel, name: testInfo.title }));
721
+ },
722
+ tokenProvider: async ({ appConfig, logger: logger2 }, use) => {
723
+ await use(resolveTokenProvider(appConfig, logger2));
724
+ },
725
+ apiClient: async ({ appConfig, tokenProvider, logger: logger2, request }, use) => {
726
+ await use(
727
+ new ApiClient({
728
+ request,
729
+ baseUrl: appConfig.apiBaseUrl ?? appConfig.baseUrl,
730
+ tokenProvider,
731
+ logger: logger2,
732
+ retry: {
733
+ retries: appConfig.retry.retries,
734
+ minDelayMs: appConfig.retry.minDelayMs,
735
+ maxDelayMs: appConfig.retry.maxDelayMs
736
+ }
737
+ })
738
+ );
739
+ },
740
+ retry: async ({ logger: logger2 }, use) => {
741
+ const boundRetry = (fn, options) => withRetry(fn, {
742
+ onRetry: (error, attempt) => logger2.warn({ err: error, attempt }, "retrying operation"),
743
+ ...options
744
+ });
745
+ await use(boundRetry);
746
+ },
747
+ cleanup: async ({ logger: logger2 }, use) => {
748
+ const tasks = [];
749
+ await use((task) => {
750
+ tasks.push(task);
751
+ });
752
+ for (const task of tasks.reverse()) {
753
+ try {
754
+ await task();
755
+ } catch (error) {
756
+ logger2.error({ err: error }, "cleanup task failed");
757
+ }
758
+ }
759
+ },
760
+ // Overriding `page` (instead of an auto fixture) keeps API-only tests from launching a
761
+ // browser: the override only runs for tests that actually request a page.
762
+ page: async ({ page, appConfig }, use, testInfo) => {
763
+ if (!appConfig.diagnostics.enabled) {
764
+ await use(page);
765
+ return;
766
+ }
767
+ const diagnostics = collectDiagnostics(page, appConfig.diagnostics);
768
+ await use(page);
769
+ await diagnostics.report(testInfo);
770
+ },
771
+ // testInfo-only, so this is safe to run automatically for every test.
772
+ _testCaseLinks: [
773
+ async ({ appConfig }, use, testInfo) => {
774
+ linkTestCases(testInfo, appConfig.reporting.testCaseUrlTemplate);
775
+ await use();
776
+ },
777
+ { auto: true }
778
+ ]
779
+ });
780
+ function roleStorageState(role, storageStateDir = process.env.AUTH_STORAGE_STATE_DIR ?? ".auth") {
781
+ return path.join(storageStateDir, `${role}.json`);
782
+ }
783
+ function buildGlobalSetup(options) {
784
+ if (!options.login && !options.roles) {
785
+ throw new AppError("buildGlobalSetup requires either `login` or `roles`", {
786
+ code: "GLOBAL_SETUP_NO_LOGIN"
787
+ });
788
+ }
789
+ return async function globalSetup() {
790
+ const config = loadConfig({ overrides: options.configOverrides });
791
+ const sessions = resolveSessions(options, config);
792
+ const browser = await test$1.chromium.launch(options.launchOptions);
793
+ try {
794
+ for (const session of sessions) {
795
+ fs.mkdirSync(path.dirname(session.storageStatePath), { recursive: true });
796
+ const context = await browser.newContext({
797
+ baseURL: config.baseUrl,
798
+ ...session.contextOptions ?? options.contextOptions
799
+ });
800
+ try {
801
+ const page = await context.newPage();
802
+ await session.login(page, config);
803
+ await context.storageState({ path: session.storageStatePath });
804
+ } finally {
805
+ await context.close();
806
+ }
807
+ }
808
+ } finally {
809
+ await browser.close();
810
+ }
811
+ };
812
+ }
813
+ function resolveSessions(options, config) {
814
+ if (options.roles) {
815
+ return Object.entries(options.roles).map(([role, setup]) => ({
816
+ login: setup.login,
817
+ storageStatePath: setup.storageStatePath ?? roleStorageState(role, config.auth.storageStateDir),
818
+ contextOptions: setup.contextOptions
819
+ }));
820
+ }
821
+ const storageStatePath = config.auth.storageStatePath;
822
+ if (!storageStatePath) {
823
+ throw new AppError(
824
+ "buildGlobalSetup requires `auth.storageStatePath` to be set (env AUTH_STORAGE_STATE_PATH or a config override) when using single-session `login`",
825
+ { code: "MISSING_STORAGE_STATE_PATH" }
826
+ );
827
+ }
828
+ return [{ login: options.login, storageStatePath }];
829
+ }
830
+
831
+ // src/ui/actions.ts
832
+ async function waitForVisible(locator, timeoutMs) {
833
+ await locator.waitFor({ state: "visible", timeout: timeoutMs });
834
+ }
835
+ async function safeClick(locator, timeoutMs) {
836
+ await waitForVisible(locator, timeoutMs);
837
+ await locator.click();
838
+ }
839
+ async function safeDoubleClick(locator, timeoutMs) {
840
+ await waitForVisible(locator, timeoutMs);
841
+ await locator.dblclick();
842
+ }
843
+ async function safeFill(locator, value, timeoutMs) {
844
+ await waitForVisible(locator, timeoutMs);
845
+ await locator.fill(value);
846
+ }
847
+ async function safeType(locator, value, delayMs = 0) {
848
+ await waitForVisible(locator);
849
+ await locator.fill("");
850
+ await locator.pressSequentially(value, { delay: delayMs });
851
+ }
852
+ async function safeSelect(locator, value) {
853
+ await waitForVisible(locator);
854
+ await locator.selectOption(value);
855
+ }
856
+ async function safeCheck(locator, checked = true) {
857
+ await waitForVisible(locator);
858
+ if (checked) await locator.check();
859
+ else await locator.uncheck();
860
+ }
861
+ async function uploadFile(locator, files) {
862
+ await locator.setInputFiles(files);
863
+ }
864
+ async function scrollIntoView(locator) {
865
+ await locator.scrollIntoViewIfNeeded();
866
+ }
867
+ async function getText(locator) {
868
+ return (await locator.textContent())?.trim() ?? "";
869
+ }
870
+ async function getAllTexts(locator) {
871
+ const texts = await locator.allTextContents();
872
+ return texts.map((text) => text.trim());
873
+ }
874
+ async function getInputValue(locator) {
875
+ return locator.inputValue();
876
+ }
877
+ async function exists(locator) {
878
+ return await locator.count() > 0;
879
+ }
880
+ async function fillFields(page, fields, options = {}) {
881
+ const resolve = options.resolve ?? ((selector) => page.locator(selector));
882
+ for (const [selector, value] of Object.entries(fields)) {
883
+ const locator = resolve(selector);
884
+ if (typeof value === "boolean") {
885
+ await safeCheck(locator, value);
886
+ } else {
887
+ await safeFill(locator, String(value));
888
+ }
889
+ }
890
+ }
891
+
892
+ // src/ui/components/base-page.ts
893
+ var BasePage = class {
894
+ page;
895
+ baseUrl;
896
+ logger;
897
+ constructor(options) {
898
+ this.page = options.page;
899
+ this.baseUrl = options.baseUrl;
900
+ this.logger = options.logger;
901
+ }
902
+ /** Absolute URL for this page, with optional path override and query string. */
903
+ url(options = {}) {
904
+ const path = options.path ?? this.path;
905
+ const url = this.baseUrl ? new URL(path, this.baseUrl) : new URL(path, "http://localhost");
906
+ if (options.query) {
907
+ for (const [key, value] of Object.entries(options.query)) {
908
+ if (value !== void 0) url.searchParams.set(key, String(value));
909
+ }
910
+ }
911
+ return this.baseUrl ? url.toString() : `${url.pathname}${url.search}`;
912
+ }
913
+ async goto(options = {}) {
914
+ const url = this.url(options);
915
+ this.logger?.debug({ url }, "navigating");
916
+ return this.page.goto(url, options.waitUntil ? { waitUntil: options.waitUntil } : void 0);
917
+ }
918
+ async reload() {
919
+ await this.page.reload();
920
+ }
921
+ currentUrl() {
922
+ return this.page.url();
923
+ }
924
+ async waitForUrl(url, timeoutMs) {
925
+ await this.page.waitForURL(url, timeoutMs ? { timeout: timeoutMs } : void 0);
926
+ }
927
+ async waitForLoadState(state = "load") {
928
+ await this.page.waitForLoadState(state);
929
+ }
930
+ /**
931
+ * Waits for network activity to settle. Prefer waiting on a specific element when you can —
932
+ * `networkidle` is discouraged by Playwright for apps that poll or hold sockets open.
933
+ */
934
+ async waitForNetworkIdle(timeoutMs) {
935
+ await this.page.waitForLoadState("networkidle", timeoutMs ? { timeout: timeoutMs } : void 0);
936
+ }
937
+ async pressKey(key) {
938
+ await this.page.keyboard.press(key);
939
+ }
940
+ /**
941
+ * Screenshot as a buffer. Pair with `attachScreenshot()` from
942
+ * `@emesoft_company/playwright-core/reporting` to put it in the HTML/Allure report.
943
+ */
944
+ async screenshot(fullPage = false) {
945
+ return this.page.screenshot({ fullPage, caret: "hide" });
946
+ }
947
+ // --- locator helpers, delegated to the shared actions so components behave identically ---
948
+ waitForVisible(locator, timeoutMs) {
949
+ return waitForVisible(locator, timeoutMs);
950
+ }
951
+ async safeClick(locator, timeoutMs) {
952
+ await safeClick(locator, timeoutMs);
953
+ }
954
+ async safeFill(locator, value) {
955
+ await safeFill(locator, value);
956
+ }
957
+ async safeType(locator, value, delayMs) {
958
+ await safeType(locator, value, delayMs);
959
+ }
960
+ async safeSelect(locator, value) {
961
+ await safeSelect(locator, value);
962
+ }
963
+ async safeCheck(locator, checked = true) {
964
+ await safeCheck(locator, checked);
965
+ }
966
+ async uploadFile(locator, files) {
967
+ await uploadFile(locator, files);
968
+ }
969
+ async scrollIntoView(locator) {
970
+ await scrollIntoView(locator);
971
+ }
972
+ async getText(locator) {
973
+ return getText(locator);
974
+ }
975
+ async getAllTexts(locator) {
976
+ return getAllTexts(locator);
977
+ }
978
+ /** Fills a form from a `{ selector: value }` map. Booleans check/uncheck. */
979
+ async fillFields(fields) {
980
+ await fillFields(this.page, fields);
981
+ }
982
+ };
983
+
984
+ // src/ui/components/base-component.ts
985
+ var BaseComponent = class {
986
+ page;
987
+ root;
988
+ constructor(options) {
989
+ this.page = options.page;
990
+ this.root = options.root;
991
+ }
992
+ // --- scoped lookups ---
993
+ byTestId(testId) {
994
+ return this.root.getByTestId(testId);
995
+ }
996
+ byRole(...args) {
997
+ return this.root.getByRole(...args);
998
+ }
999
+ byText(...args) {
1000
+ return this.root.getByText(...args);
1001
+ }
1002
+ byLabel(...args) {
1003
+ return this.root.getByLabel(...args);
1004
+ }
1005
+ locator(selector) {
1006
+ return this.root.locator(selector);
1007
+ }
1008
+ // --- state ---
1009
+ async isVisible() {
1010
+ return this.root.isVisible();
1011
+ }
1012
+ async exists() {
1013
+ return exists(this.root);
1014
+ }
1015
+ async text() {
1016
+ return getText(this.root);
1017
+ }
1018
+ async waitForStable(timeoutMs) {
1019
+ await waitForVisible(this.root, timeoutMs);
1020
+ }
1021
+ async waitForHidden(timeoutMs) {
1022
+ await this.root.waitFor({ state: "hidden", timeout: timeoutMs });
1023
+ }
1024
+ // --- actions, same implementations BasePage uses ---
1025
+ async safeClick(locator, timeoutMs) {
1026
+ await safeClick(locator, timeoutMs);
1027
+ }
1028
+ async safeFill(locator, value) {
1029
+ await safeFill(locator, value);
1030
+ }
1031
+ async safeSelect(locator, value) {
1032
+ await safeSelect(locator, value);
1033
+ }
1034
+ async safeCheck(locator, checked = true) {
1035
+ await safeCheck(locator, checked);
1036
+ }
1037
+ async scrollIntoView(locator = this.root) {
1038
+ await scrollIntoView(locator);
1039
+ }
1040
+ async getText(locator) {
1041
+ return getText(locator);
1042
+ }
1043
+ async getAllTexts(locator) {
1044
+ return getAllTexts(locator);
1045
+ }
1046
+ /** Fills fields *within this component*, so selectors don't collide with the rest of the page. */
1047
+ async fillFields(fields) {
1048
+ await fillFields(this.page, fields, { resolve: (selector) => this.root.locator(selector) });
1049
+ }
1050
+ };
1051
+
1052
+ // src/ui/components/table.component.ts
1053
+ var TableComponent = class extends BaseComponent {
1054
+ headerSelector;
1055
+ rowSelector;
1056
+ cellSelector;
1057
+ constructor(options) {
1058
+ super(options);
1059
+ this.headerSelector = options.headerSelector ?? "thead th";
1060
+ this.rowSelector = options.rowSelector ?? "tbody tr";
1061
+ this.cellSelector = options.cellSelector ?? "td";
1062
+ }
1063
+ /** All data rows. A live locator — safe to use with auto-retrying assertions. */
1064
+ get rows() {
1065
+ return this.root.locator(this.rowSelector);
1066
+ }
1067
+ get headerCells() {
1068
+ return this.root.locator(this.headerSelector);
1069
+ }
1070
+ row(index) {
1071
+ return this.rows.nth(index);
1072
+ }
1073
+ cells(rowIndex) {
1074
+ return this.row(rowIndex).locator(this.cellSelector);
1075
+ }
1076
+ async rowCount() {
1077
+ return this.rows.count();
1078
+ }
1079
+ async isEmpty() {
1080
+ return await this.rowCount() === 0;
1081
+ }
1082
+ /** Trimmed header labels, in order. */
1083
+ async headers() {
1084
+ return getAllTexts(this.headerCells);
1085
+ }
1086
+ /**
1087
+ * Zero-based index of a column by its header label (exact match after trimming, falling back
1088
+ * to a case-insensitive contains match). Throws listing the available headers when not found —
1089
+ * a renamed column is otherwise a confusing "cell is empty" failure.
1090
+ */
1091
+ async columnIndex(header) {
1092
+ const headers = await this.headers();
1093
+ const exact = headers.indexOf(header.trim());
1094
+ if (exact !== -1) return exact;
1095
+ const needle = header.trim().toLowerCase();
1096
+ const loose = headers.findIndex((candidate) => candidate.toLowerCase().includes(needle));
1097
+ if (loose !== -1) return loose;
1098
+ throw new AppError(`Column "${header}" not found in table`, {
1099
+ code: "TABLE_COLUMN_NOT_FOUND",
1100
+ context: { requested: header, available: headers }
1101
+ });
1102
+ }
1103
+ /** A cell by row index and either a column index or a header label. */
1104
+ async cell(rowIndex, column) {
1105
+ const index = typeof column === "number" ? column : await this.columnIndex(column);
1106
+ return this.cells(rowIndex).nth(index);
1107
+ }
1108
+ async cellText(rowIndex, column) {
1109
+ return getText(await this.cell(rowIndex, column));
1110
+ }
1111
+ /** Every cell value of one row, in column order. */
1112
+ async rowValues(rowIndex) {
1113
+ return getAllTexts(this.cells(rowIndex));
1114
+ }
1115
+ /**
1116
+ * The whole table as `{ header: cellText }` objects — the most convenient shape for
1117
+ * assertions (`expect(await table.records()).toContainEqual({ Status: "Approved", ... })`).
1118
+ */
1119
+ async records() {
1120
+ const headers = await this.headers();
1121
+ const count = await this.rowCount();
1122
+ const records = [];
1123
+ for (let rowIndex = 0; rowIndex < count; rowIndex++) {
1124
+ const values = await this.rowValues(rowIndex);
1125
+ const record = {};
1126
+ values.forEach((value, columnIndex) => {
1127
+ record[headers[columnIndex] ?? `column${columnIndex}`] = value;
1128
+ });
1129
+ records.push(record);
1130
+ }
1131
+ return records;
1132
+ }
1133
+ /** Rows containing `text` anywhere. Still a locator, so assertions on it auto-retry. */
1134
+ rowsContaining(text) {
1135
+ return this.rows.filter({ hasText: text });
1136
+ }
1137
+ /**
1138
+ * Rows whose cell in a given column matches `value`. Returns a locator (auto-retrying);
1139
+ * only the header lookup is awaited up front.
1140
+ */
1141
+ async rowsByColumn(column, value) {
1142
+ const index = typeof column === "number" ? column : await this.columnIndex(column);
1143
+ return this.rows.filter({
1144
+ has: this.page.locator(`${this.cellSelector}:nth-child(${index + 1})`).filter({ hasText: value })
1145
+ });
1146
+ }
1147
+ /** Waits until the table has at least `min` rows (default 1) — e.g. after a search or load. */
1148
+ async waitForRows(min = 1, timeoutMs = 15e3) {
1149
+ const deadline = Date.now() + timeoutMs;
1150
+ for (; ; ) {
1151
+ if (await this.rowCount() >= min) return;
1152
+ if (Date.now() >= deadline) {
1153
+ throw new AppError(`Table did not reach ${min} row(s) within ${timeoutMs}ms`, {
1154
+ code: "TABLE_ROWS_TIMEOUT",
1155
+ context: { min, timeoutMs, actual: await this.rowCount() }
1156
+ });
1157
+ }
1158
+ await this.page.waitForTimeout(200);
1159
+ }
1160
+ }
1161
+ };
1162
+
1163
+ // src/di/container.ts
1164
+ var ServiceContainer = class {
1165
+ factories = /* @__PURE__ */ new Map();
1166
+ singletons = /* @__PURE__ */ new Map();
1167
+ register(key, factory) {
1168
+ this.factories.set(key, factory);
1169
+ this.singletons.delete(key);
1170
+ }
1171
+ /** Replaces an existing registration — this is how a project overrides a core-provided service. */
1172
+ override(key, factory) {
1173
+ this.register(key, factory);
1174
+ }
1175
+ resolve(key) {
1176
+ if (this.singletons.has(key)) {
1177
+ return this.singletons.get(key);
1178
+ }
1179
+ const factory = this.factories.get(key);
1180
+ if (!factory) {
1181
+ throw new AppError(`No provider registered for service "${key}"`, { code: "DI_UNRESOLVED" });
1182
+ }
1183
+ const instance = factory(this);
1184
+ this.singletons.set(key, instance);
1185
+ return instance;
1186
+ }
1187
+ has(key) {
1188
+ return this.factories.has(key);
1189
+ }
1190
+ };
1191
+
1192
+ // src/utils/wait.ts
1193
+ async function waitForCondition(predicate, options = {}) {
1194
+ const { timeoutMs = 3e4, intervalMs = 500, message = "Condition was not met within timeout" } = options;
1195
+ const deadline = Date.now() + timeoutMs;
1196
+ for (; ; ) {
1197
+ if (await predicate()) return;
1198
+ if (Date.now() >= deadline) {
1199
+ throw new AppError(message, { code: "WAIT_TIMEOUT", context: { timeoutMs, intervalMs } });
1200
+ }
1201
+ await sleep(intervalMs);
1202
+ }
1203
+ }
1204
+
1205
+ // src/data/generators.ts
1206
+ var counter = 0;
1207
+ var ALPHANUMERIC = "abcdefghijklmnopqrstuvwxyz0123456789";
1208
+ function uniqueId(prefix) {
1209
+ counter += 1;
1210
+ const id = `${Date.now().toString(36)}${process.pid.toString(36)}${counter.toString(36)}`;
1211
+ return prefix ? `${prefix}-${id}` : id;
1212
+ }
1213
+ function uniqueEmail(domain = "example.test", prefix = "qa") {
1214
+ return `${prefix}-${uniqueId()}@${domain}`;
1215
+ }
1216
+ function randomString(length = 8, charset = ALPHANUMERIC) {
1217
+ let result = "";
1218
+ for (let i = 0; i < length; i++) {
1219
+ result += charset.charAt(Math.floor(Math.random() * charset.length));
1220
+ }
1221
+ return result;
1222
+ }
1223
+ function randomInt(min, max) {
1224
+ if (min > max) throw new RangeError(`randomInt: min (${min}) must be <= max (${max})`);
1225
+ return Math.floor(Math.random() * (max - min + 1)) + min;
1226
+ }
1227
+ function randomBool() {
1228
+ return Math.random() < 0.5;
1229
+ }
1230
+ function pickOne(items) {
1231
+ if (items.length === 0) throw new RangeError("pickOne: cannot pick from an empty array");
1232
+ return items[randomInt(0, items.length - 1)];
1233
+ }
1234
+ function pickSome(items, count) {
1235
+ if (count > items.length) {
1236
+ throw new RangeError(`pickSome: cannot pick ${count} items from an array of ${items.length}`);
1237
+ }
1238
+ const pool = [...items];
1239
+ const picked = [];
1240
+ for (let i = 0; i < count; i++) {
1241
+ const [item] = pool.splice(randomInt(0, pool.length - 1), 1);
1242
+ picked.push(item);
1243
+ }
1244
+ return picked;
1245
+ }
1246
+ function isoDate(dayOffset = 0, from = /* @__PURE__ */ new Date()) {
1247
+ const date = new Date(from);
1248
+ date.setDate(date.getDate() + dayOffset);
1249
+ return date.toISOString().slice(0, 10);
1250
+ }
1251
+
1252
+ exports.ApiClient = ApiClient;
1253
+ exports.ApiError = ApiError;
1254
+ exports.AppConfigSchema = AppConfigSchema;
1255
+ exports.AppError = AppError;
1256
+ exports.AuthConfigSchema = AuthConfigSchema;
1257
+ exports.AuthError = AuthError;
1258
+ exports.BaseComponent = BaseComponent;
1259
+ exports.BasePage = BasePage;
1260
+ exports.ConfigMissingError = ConfigMissingError;
1261
+ exports.ConfigValidationError = ConfigValidationError;
1262
+ exports.DiagnosticsConfigSchema = DiagnosticsConfigSchema;
1263
+ exports.EnvironmentSchema = EnvironmentSchema;
1264
+ exports.JwtTokenProvider = JwtTokenProvider;
1265
+ exports.KNOWN_ENVIRONMENTS = KNOWN_ENVIRONMENTS;
1266
+ exports.NullTokenProvider = NullTokenProvider;
1267
+ exports.ReportingConfigSchema = ReportingConfigSchema;
1268
+ exports.RetryConfigSchema = RetryConfigSchema;
1269
+ exports.ServiceContainer = ServiceContainer;
1270
+ exports.StaticTokenProvider = StaticTokenProvider;
1271
+ exports.TableComponent = TableComponent;
1272
+ exports.TimeoutsSchema = TimeoutsSchema;
1273
+ exports.attachJson = attachJson;
1274
+ exports.attachScreenshot = attachScreenshot;
1275
+ exports.attachText = attachText;
1276
+ exports.buildGlobalSetup = buildGlobalSetup;
1277
+ exports.buildReporterConfig = buildReporterConfig;
1278
+ exports.collectDiagnostics = collectDiagnostics;
1279
+ exports.createLogger = createLogger;
1280
+ exports.deepMerge = deepMerge;
1281
+ exports.defaultEnvFiles = defaultEnvFiles;
1282
+ exports.defineE2EConfig = defineE2EConfig;
1283
+ exports.exists = exists;
1284
+ exports.expect = expect;
1285
+ exports.fillFields = fillFields;
1286
+ exports.getAllTexts = getAllTexts;
1287
+ exports.getInputValue = getInputValue;
1288
+ exports.getText = getText;
1289
+ exports.isPlainObject = isPlainObject;
1290
+ exports.isRetryableStatus = isRetryableStatus;
1291
+ exports.isoDate = isoDate;
1292
+ exports.linkTestCases = linkTestCases;
1293
+ exports.loadConfig = loadConfig;
1294
+ exports.loadEnvFiles = loadEnvFiles;
1295
+ exports.logger = logger;
1296
+ exports.meta = meta;
1297
+ exports.pickOne = pickOne;
1298
+ exports.pickSome = pickSome;
1299
+ exports.pruneUndefined = pruneUndefined;
1300
+ exports.randomBool = randomBool;
1301
+ exports.randomInt = randomInt;
1302
+ exports.randomString = randomString;
1303
+ exports.requireEnv = requireEnv;
1304
+ exports.resolveTokenProvider = resolveTokenProvider;
1305
+ exports.roleStorageState = roleStorageState;
1306
+ exports.safeCheck = safeCheck;
1307
+ exports.safeClick = safeClick;
1308
+ exports.safeDoubleClick = safeDoubleClick;
1309
+ exports.safeFill = safeFill;
1310
+ exports.safeSelect = safeSelect;
1311
+ exports.safeType = safeType;
1312
+ exports.scrollIntoView = scrollIntoView;
1313
+ exports.sleep = sleep;
1314
+ exports.test = test;
1315
+ exports.uniqueEmail = uniqueEmail;
1316
+ exports.uniqueId = uniqueId;
1317
+ exports.uploadFile = uploadFile;
1318
+ exports.waitForCondition = waitForCondition;
1319
+ exports.waitForVisible = waitForVisible;
1320
+ exports.withRetry = withRetry;
1321
+ //# sourceMappingURL=index.cjs.map
1322
+ //# sourceMappingURL=index.cjs.map