@appliqation/automation-sdk 2.5.1 → 2.8.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.
- package/README.md +330 -1169
- package/package.json +13 -34
- package/src/AppliqationClient.js +125 -25
- package/src/cli/auth-setup.js +308 -0
- package/src/constants.js +21 -1
- package/src/core/AuthManager.js +5 -0
- package/src/index.d.ts +159 -6
- package/src/login/index.d.ts +77 -0
- package/src/login/index.js +49 -0
- package/src/playwright/fixture.js +4 -1
- package/src/reporters/playwright/AppliqationReporter.js +159 -11
- package/src/services/OrphanTestService.js +21 -2
- package/src/services/ResultService.js +76 -45
- package/src/services/RunMatrixService.js +21 -1
- package/src/services/ScopeValidator.js +314 -0
- package/src/services/TaggingService.js +5 -5
- package/src/utils/PayloadBuilder.js +112 -21
- package/src/utils/index.js +12 -0
- package/src/utils/logger.js +59 -5
- package/src/utils/setupAuth.js +99 -0
- package/src/reporters/cypress/CypressReporter.js +0 -434
- package/src/reporters/cypress/UuidExtractor.js +0 -139
- package/src/reporters/cypress/index.js +0 -30
- package/src/reporters/jest/JestReporter.js +0 -408
- package/src/reporters/jest/UuidExtractor.js +0 -174
- package/src/reporters/jest/index.js +0 -28
package/src/index.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TypeScript definitions for @appliqation/automation-sdk
|
|
3
|
-
* @version 2.
|
|
3
|
+
* @version 2.8.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Scope-mismatch resolution strategy (audit finding C5).
|
|
8
|
+
*
|
|
9
|
+
* When `scenarioId` or `testSetId` is set in config and the executed
|
|
10
|
+
* suite contains UUIDs outside that scope, the SDK applies one of
|
|
11
|
+
* these strategies before submitting results.
|
|
12
|
+
* 'cancel' — fail fast, submit nothing, exit 1 (default in CI)
|
|
13
|
+
* 'adhoc' — override config, create an ad-hoc run, submit everything
|
|
14
|
+
* 'filter' — submit only in-scope results, drop the rest locally
|
|
15
|
+
*/
|
|
16
|
+
export type ScopeMismatchStrategy = 'cancel' | 'adhoc' | 'filter';
|
|
17
|
+
|
|
6
18
|
// ============================================================================
|
|
7
19
|
// Core Types
|
|
8
20
|
// ============================================================================
|
|
@@ -49,10 +61,16 @@ export interface AppliqationConfig {
|
|
|
49
61
|
/** Project key */
|
|
50
62
|
projectKey?: string;
|
|
51
63
|
|
|
52
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* @deprecated Legacy CSRF authentication. Slated for removal in a
|
|
66
|
+
* future major version (audit finding D4). Use `apiKey` instead.
|
|
67
|
+
*/
|
|
53
68
|
username?: string;
|
|
54
69
|
|
|
55
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* @deprecated Legacy CSRF authentication. Slated for removal in a
|
|
72
|
+
* future major version (audit finding D4). Use `apiKey` instead.
|
|
73
|
+
*/
|
|
56
74
|
password?: string;
|
|
57
75
|
|
|
58
76
|
/** Scenario ID (optional, 0 for generic automation runs) */
|
|
@@ -88,9 +106,25 @@ export interface AppliqationConfig {
|
|
|
88
106
|
/** Batch size for result submission */
|
|
89
107
|
batchSize?: number;
|
|
90
108
|
|
|
91
|
-
/**
|
|
109
|
+
/**
|
|
110
|
+
* SSL certificate verification (default: true).
|
|
111
|
+
*
|
|
112
|
+
* H3 — setting this to `false` in a production-like environment
|
|
113
|
+
* (`CI=true`, `NODE_ENV=production`/`prod`/`staging`, or any well-known
|
|
114
|
+
* CI env var) will throw `ConfigurationError` at client construction.
|
|
115
|
+
* Explicit override via `APPLIQATION_INSECURE=allow` for the rare case
|
|
116
|
+
* where a customer genuinely needs to bypass TLS in CI.
|
|
117
|
+
*/
|
|
92
118
|
rejectUnauthorized?: boolean;
|
|
93
119
|
|
|
120
|
+
/**
|
|
121
|
+
* C5 — scope-mismatch resolution strategy. Applied when the executed
|
|
122
|
+
* suite contains UUIDs outside the configured `scenarioId`/`testSetId`.
|
|
123
|
+
* Also settable via `APPLIQATION_ON_SCOPE_MISMATCH` env var.
|
|
124
|
+
* Defaults to 'cancel' in CI (fail safe), interactive prompt in TTY.
|
|
125
|
+
*/
|
|
126
|
+
onScopeMismatch?: ScopeMismatchStrategy;
|
|
127
|
+
|
|
94
128
|
/** Additional options */
|
|
95
129
|
options?: {
|
|
96
130
|
/** Request timeout in milliseconds (default: 30000) */
|
|
@@ -232,9 +266,16 @@ export class AppliqationClient {
|
|
|
232
266
|
constructor(config: AppliqationConfig);
|
|
233
267
|
|
|
234
268
|
/**
|
|
235
|
-
* Create a new automation run
|
|
269
|
+
* Create a new automation run.
|
|
270
|
+
*
|
|
271
|
+
* The optional `uuids` field (C4 / E3) pre-populates the run
|
|
272
|
+
* document's `data[]` array so the UI grid and orphan detector know
|
|
273
|
+
* which TCs belong to the run before any result lands. Pass plain
|
|
274
|
+
* string UUIDs matching PR appq/#532's schema (not `{uuid}` objects).
|
|
236
275
|
*/
|
|
237
|
-
createRun(
|
|
276
|
+
createRun(
|
|
277
|
+
config?: Partial<AppliqationConfig> & { uuids?: string[] }
|
|
278
|
+
): Promise<RunMatrixResponse>;
|
|
238
279
|
|
|
239
280
|
/**
|
|
240
281
|
* Submit a single test result
|
|
@@ -299,6 +340,61 @@ export class PayloadBuilder {
|
|
|
299
340
|
status: TestStatus,
|
|
300
341
|
options?: { comment?: string; attachments?: string[]; metadata?: Record<string, any> }
|
|
301
342
|
): TestResult;
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* S8 — validate a fully assembled automation result payload against
|
|
346
|
+
* backend shape constraints (InputValidator regexes + status enum).
|
|
347
|
+
* Runs client-side immediately before submission so schema drift is
|
|
348
|
+
* caught with a precise field-path error instead of a generic 400.
|
|
349
|
+
*/
|
|
350
|
+
static validateAutomationResultPayload(payload: {
|
|
351
|
+
run_id?: unknown;
|
|
352
|
+
test_case_uuid?: unknown;
|
|
353
|
+
status?: unknown;
|
|
354
|
+
source?: unknown;
|
|
355
|
+
browser?: unknown;
|
|
356
|
+
environment?: unknown;
|
|
357
|
+
duration?: unknown;
|
|
358
|
+
timestamp?: unknown;
|
|
359
|
+
[key: string]: unknown;
|
|
360
|
+
}): { valid: boolean; errors: string[] };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* C5 — Pre-execution scope validator. Verifies UUIDs about to execute
|
|
365
|
+
* match the configured scenarioId/testSetId; applies cancel/adhoc/filter
|
|
366
|
+
* strategy on mismatch. Wired automatically by AppliqationReporter in
|
|
367
|
+
* `onBegin`; exposed for programmatic use.
|
|
368
|
+
*/
|
|
369
|
+
export class ScopeValidator {
|
|
370
|
+
constructor(options: {
|
|
371
|
+
scenarioId?: number | string;
|
|
372
|
+
testSetId?: number | string;
|
|
373
|
+
strategy?: ScopeMismatchStrategy;
|
|
374
|
+
fetchTestSetScope?: (testSetId: number | string) => Promise<Set<string> | string[]>;
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
static readonly STRATEGY: {
|
|
378
|
+
CANCEL: 'cancel';
|
|
379
|
+
ADHOC: 'adhoc';
|
|
380
|
+
FILTER: 'filter';
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
isScoped(): boolean;
|
|
384
|
+
extractUuidsFromSuite(suite: unknown): string[];
|
|
385
|
+
validate(uuids: string[]): Promise<{
|
|
386
|
+
hasMismatch: boolean;
|
|
387
|
+
inScope: string[];
|
|
388
|
+
outOfScope: Array<{ uuid: string; actualScenarioId?: number }>;
|
|
389
|
+
reason: 'scenario' | 'testset' | null;
|
|
390
|
+
}>;
|
|
391
|
+
resolve(validation: unknown): Promise<{
|
|
392
|
+
action: 'cancel' | 'adhoc' | 'filter' | 'pass';
|
|
393
|
+
submitUuids: string[];
|
|
394
|
+
droppedUuids: string[];
|
|
395
|
+
overrideToAdhoc: boolean;
|
|
396
|
+
message: string;
|
|
397
|
+
}>;
|
|
302
398
|
}
|
|
303
399
|
|
|
304
400
|
export const logger: {
|
|
@@ -331,3 +427,60 @@ export const logger: {
|
|
|
331
427
|
* ```
|
|
332
428
|
*/
|
|
333
429
|
export function mapAppqUuid(testInfo: any, uuid: string): void;
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Returns the canonical Playwright `storageState` file path for a given
|
|
433
|
+
* Appliqation project + role. The same path resolves regardless of
|
|
434
|
+
* execution context, so the same generated/authored test runs unchanged
|
|
435
|
+
* in:
|
|
436
|
+
* - Appliqation's executor (LoginHelper writes the file at this path)
|
|
437
|
+
* - Customer CI / local (the `appq-auth-setup` CLI writes the file)
|
|
438
|
+
*
|
|
439
|
+
* Does NOT perform login itself — only computes the path. Pair with
|
|
440
|
+
* `npx appq-auth-setup --project-id X --role Y` (or Appliqation's
|
|
441
|
+
* executor) to populate the file before tests run.
|
|
442
|
+
*
|
|
443
|
+
* @example
|
|
444
|
+
* ```typescript
|
|
445
|
+
* import { test } from '@playwright/test';
|
|
446
|
+
* import { mapAppqUuid, setupAuth } from '@appliqation/automation-sdk/utils';
|
|
447
|
+
*
|
|
448
|
+
* test.use({ storageState: setupAuth({ project_id: 126, role: 'default' }) });
|
|
449
|
+
*
|
|
450
|
+
* test('manager dashboard loads', async ({ page }, testInfo) => {
|
|
451
|
+
* mapAppqUuid(testInfo, '1141-...');
|
|
452
|
+
* await page.goto('/dashboard'); // already authenticated
|
|
453
|
+
* });
|
|
454
|
+
* ```
|
|
455
|
+
*/
|
|
456
|
+
export function setupAuth(options: { project_id: number | string; role?: string }): string;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Computes the canonical storageState file path. Same as setupAuth, but
|
|
460
|
+
* usable by tooling that needs the path without going through Playwright
|
|
461
|
+
* (e.g. the appq-auth-setup CLI writes to this path).
|
|
462
|
+
*/
|
|
463
|
+
export function authStatePath(options: { project_id: number | string; role?: string }): string;
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Base directory for storageState files. Defaults to `~/.appq-auth/`;
|
|
467
|
+
* overridable via APPQ_AUTH_STATE_DIR env (Appliqation's executor sets
|
|
468
|
+
* this per-run for tenant isolation).
|
|
469
|
+
*/
|
|
470
|
+
export function authStateDir(): string;
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* The canonical env-var names the CLI looks up for credentials, given
|
|
474
|
+
* a project_id + role. Customers populate these from the `.env` template
|
|
475
|
+
* downloaded from Appliqation's project settings.
|
|
476
|
+
*/
|
|
477
|
+
export function envVarNames(options: { project_id: number | string; role?: string }): {
|
|
478
|
+
username: string;
|
|
479
|
+
password: string;
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Normalize a role name to lowercase with only alphanumerics, underscore,
|
|
484
|
+
* and hyphen. Mirrors the role-name validation in the Appliqation UI.
|
|
485
|
+
*/
|
|
486
|
+
export function sanitizeRole(role: string): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript definitions for @appliqation/automation-sdk/login.
|
|
3
|
+
*
|
|
4
|
+
* Customers writing `tests/automan/auth/login.ts` (default path;
|
|
5
|
+
* configurable in Project Settings) import these
|
|
6
|
+
* types for autocomplete + compile-time checking on the login flow
|
|
7
|
+
* function shape. Same file gets imported (untyped, dynamically) by
|
|
8
|
+
* the Appliqation executor's LoginHelper and the appq-auth-setup CLI.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { Page } from '@playwright/test';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Per-call context passed to the customer's login function. Sourced
|
|
15
|
+
* differently in each runtime:
|
|
16
|
+
*
|
|
17
|
+
* - Appliqation executor: username + password from AWS Secrets
|
|
18
|
+
* Manager (read-only IAM); baseURL from the project's env
|
|
19
|
+
* configuration; role from snapshot.auth_role.
|
|
20
|
+
* - Customer CI (`npx appq-auth-setup`): username + password from
|
|
21
|
+
* APPQ_PROJECT_<id>_<ROLE>_USERNAME/PASSWORD env vars; baseURL
|
|
22
|
+
* from APPLIQATION_SUT_BASE_URL env var; role from --role flag.
|
|
23
|
+
*/
|
|
24
|
+
export interface LoginContext {
|
|
25
|
+
/** Username / email for the configured role. */
|
|
26
|
+
username: string;
|
|
27
|
+
/** Password for the configured role. */
|
|
28
|
+
password: string;
|
|
29
|
+
/**
|
|
30
|
+
* Role name as configured in Project Settings. Lets the customer
|
|
31
|
+
* branch login flow per role (e.g. an admin's login may include a
|
|
32
|
+
* "Switch to admin view" step that other roles skip).
|
|
33
|
+
*/
|
|
34
|
+
role: string;
|
|
35
|
+
/**
|
|
36
|
+
* SUT base URL for the env this run targets. Use `await
|
|
37
|
+
* page.goto('/login')` — Playwright resolves the relative path
|
|
38
|
+
* against the context's baseURL, which is set from this value.
|
|
39
|
+
* Provided here for cases where the customer needs the full URL
|
|
40
|
+
* (e.g. cross-origin OAuth redirects).
|
|
41
|
+
*/
|
|
42
|
+
baseURL: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Customer's login function. Should leave the page in an
|
|
47
|
+
* authenticated state — typically by navigating to the login URL,
|
|
48
|
+
* filling credentials, submitting, and waiting for a post-login URL
|
|
49
|
+
* or visible-element indicator. The caller (LoginHelper or
|
|
50
|
+
* appq-auth-setup) captures the resulting storageState immediately
|
|
51
|
+
* after this resolves.
|
|
52
|
+
*
|
|
53
|
+
* Throw on failure. The caller will surface the error verbatim in
|
|
54
|
+
* the executor's run audit / the CLI's exit code.
|
|
55
|
+
*/
|
|
56
|
+
export type LoginFunction = (
|
|
57
|
+
page: Page,
|
|
58
|
+
ctx: LoginContext,
|
|
59
|
+
) => Promise<void>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Identity helper for type inference. Wrap your login function with
|
|
63
|
+
* this so your IDE provides autocomplete on the LoginContext fields:
|
|
64
|
+
*
|
|
65
|
+
* ```typescript
|
|
66
|
+
* import { defineLogin } from '@appliqation/automation-sdk/login';
|
|
67
|
+
*
|
|
68
|
+
* export default defineLogin(async (page, { username, password }) => {
|
|
69
|
+
* await page.goto('/login');
|
|
70
|
+
* await page.getByLabel('Email').fill(username);
|
|
71
|
+
* await page.getByLabel('Password').fill(password);
|
|
72
|
+
* await page.getByRole('button', { name: 'Sign in' }).click();
|
|
73
|
+
* await page.waitForURL('**\/dashboard');
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function defineLogin<F extends LoginFunction>(fn: F): F;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @appliqation/automation-sdk/login — types + helpers for customer-
|
|
3
|
+
* supplied login flows.
|
|
4
|
+
*
|
|
5
|
+
* Customers define their SUT's login flow as a Playwright function in
|
|
6
|
+
* their own repo (canonical path: `tests/automan/auth/login.ts`, default;
|
|
7
|
+
* configurable in Project Settings).
|
|
8
|
+
* The function is imported by:
|
|
9
|
+
* 1. Appliqation's executor (LoginHelper) — pulled from MongoDB
|
|
10
|
+
* after a GitHub-webhook upsert into automan_canonical_scripts
|
|
11
|
+
* with script_type:'login', dynamic-imported per role per cache
|
|
12
|
+
* miss.
|
|
13
|
+
* 2. Customer CI (`npx appq-auth-setup`) — dynamic-imported from
|
|
14
|
+
* disk, run with role-specific creds from env vars, output a
|
|
15
|
+
* Playwright storageState file all tests load via
|
|
16
|
+
* test.use({ storageState: setupAuth({ ... }) }).
|
|
17
|
+
*
|
|
18
|
+
* Same source file, two readers. Both run the customer's own code.
|
|
19
|
+
*
|
|
20
|
+
* Why offline GitOps and not in-app upload/edit:
|
|
21
|
+
* - Customer's IDE + lint + PR review + git history come for free
|
|
22
|
+
* - No in-app editor to build, no sandboxing concern beyond what we
|
|
23
|
+
* already have for test scripts
|
|
24
|
+
* - The login function is just Playwright code — it should live next
|
|
25
|
+
* to the Playwright tests
|
|
26
|
+
*
|
|
27
|
+
* The selector-fill model in the legacy auth_config schema (login_url
|
|
28
|
+
* + 3 CSS selectors + success_indicator) only handled the simplest
|
|
29
|
+
* form-login case. SSO / MFA / OAuth / multi-step / captcha all
|
|
30
|
+
* require custom code anyway. Customer login.ts handles everything.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Identity helper that gives type inference + auto-complete for
|
|
35
|
+
* customers writing their login.ts. Mirrors Playwright's
|
|
36
|
+
* `defineConfig` pattern — pure function, no runtime side-effects.
|
|
37
|
+
*
|
|
38
|
+
* @template {LoginFunction} F
|
|
39
|
+
* @param {F} fn
|
|
40
|
+
* @returns {F}
|
|
41
|
+
*/
|
|
42
|
+
function defineLogin(fn) {
|
|
43
|
+
if (typeof fn !== 'function') {
|
|
44
|
+
throw new TypeError('defineLogin: argument must be a function');
|
|
45
|
+
}
|
|
46
|
+
return fn;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { defineLogin };
|
|
@@ -9,7 +9,10 @@ try {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const JwtBrowserAuth = require('./JwtBrowserAuth');
|
|
12
|
-
|
|
12
|
+
// L3 — dotenv removed from the fixture. In enterprise CI, env vars are
|
|
13
|
+
// injected by the platform (GitHub Actions, Jenkins, GitLab); a stray
|
|
14
|
+
// committed .env file would otherwise silently override them. Env var
|
|
15
|
+
// loading is the customer's responsibility in playwright.config.ts.
|
|
13
16
|
const { DEFAULT_APPLIQATION_BASE_URL } = require('../constants');
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const AppliqationClient = require('../../AppliqationClient');
|
|
3
|
+
const ScopeValidator = require('../../services/ScopeValidator');
|
|
3
4
|
const UuidExtractor = require('./helpers/UuidExtractor');
|
|
4
5
|
const DeviceOsDetector = require('./helpers/DeviceOsDetector');
|
|
5
6
|
const PayloadBuilder = require('../../utils/PayloadBuilder');
|
|
@@ -176,6 +177,11 @@ class AppliqationReporter {
|
|
|
176
177
|
this.executionStartTime = null;
|
|
177
178
|
this.executionEndTime = null;
|
|
178
179
|
this.playwrightOutputDir = null;
|
|
180
|
+
|
|
181
|
+
// C5 — scope validation state
|
|
182
|
+
this.scopeFilterEnabled = false; // true when strategy === 'filter'
|
|
183
|
+
this.inScopeUuids = null; // Set<string>, null = no filter
|
|
184
|
+
this.droppedOutOfScopeCount = 0;
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
/**
|
|
@@ -211,6 +217,37 @@ class AppliqationReporter {
|
|
|
211
217
|
}
|
|
212
218
|
}
|
|
213
219
|
|
|
220
|
+
// Extract every Appliqation-mapped UUID from the suite once, then
|
|
221
|
+
// share with scope validation (C5) and run pre-population (C4/E3).
|
|
222
|
+
// Single walk over the test tree, single source of truth.
|
|
223
|
+
const allSuiteUuids = ScopeValidator.prototype.extractUuidsFromSuite.call(
|
|
224
|
+
{ /* no scope needed for extraction */ }, suite
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
// C5 — pre-execution scope validation.
|
|
228
|
+
// If the user set scenarioId or testSetId, verify every test about to
|
|
229
|
+
// run actually belongs to that scope. Mismatch → apply the configured
|
|
230
|
+
// resolution strategy (cancel / adhoc / filter). This is the SDK's
|
|
231
|
+
// primary defence against silent data corruption: without it, a
|
|
232
|
+
// tag-based selection that spans scenarios writes to the wrong run.
|
|
233
|
+
const scopeOutcome = await this._runScopeValidation(suite, allSuiteUuids);
|
|
234
|
+
if (scopeOutcome.cancelled) {
|
|
235
|
+
// User chose cancel (or default in CI on mismatch). Skip run
|
|
236
|
+
// creation and result submission entirely; tests still execute
|
|
237
|
+
// locally so the user keeps their own Playwright output.
|
|
238
|
+
this.appqEnabled = false;
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// C4 / E3 — UUIDs to pre-populate on the run document. If the filter
|
|
243
|
+
// strategy applied, only the in-scope subset is sent; otherwise the
|
|
244
|
+
// full extracted set. Backend stores these in data[] so the UI grid
|
|
245
|
+
// and orphan detector know which TCs belong to the run before any
|
|
246
|
+
// result lands.
|
|
247
|
+
this.preExecutionUuids = this.scopeFilterEnabled && this.inScopeUuids
|
|
248
|
+
? Array.from(this.inScopeUuids)
|
|
249
|
+
: allSuiteUuids;
|
|
250
|
+
|
|
214
251
|
if (!this.config.autoCreateRun) {
|
|
215
252
|
logger.info('Auto-create run disabled. Skipping run matrix creation.');
|
|
216
253
|
return;
|
|
@@ -276,7 +313,11 @@ class AppliqationReporter {
|
|
|
276
313
|
browsers: matrixConfig.browsers,
|
|
277
314
|
device: matrixConfig.device,
|
|
278
315
|
os: matrixConfig.os,
|
|
279
|
-
title: this.config.title
|
|
316
|
+
title: this.config.title,
|
|
317
|
+
// C4 / E3 — pre-populate the run's data[] array with the
|
|
318
|
+
// UUIDs about to execute. Same set across all matrix configs
|
|
319
|
+
// (Playwright reruns the same tests per browser project).
|
|
320
|
+
uuids: this.preExecutionUuids
|
|
280
321
|
};
|
|
281
322
|
|
|
282
323
|
const run = await this.client.createRun(runOptions);
|
|
@@ -372,6 +413,16 @@ class AppliqationReporter {
|
|
|
372
413
|
try {
|
|
373
414
|
const uuid = UuidExtractor.extractFromAnnotations(result.annotations || []) || UuidExtractor.extractFromTest(test);
|
|
374
415
|
|
|
416
|
+
// C5 filter strategy: scope validation in onBegin flagged this test
|
|
417
|
+
// as out-of-scope for the configured scenarioId/testSetId. The test
|
|
418
|
+
// still executed (the customer has their local report), but its
|
|
419
|
+
// verdict must not land in the wrong run.
|
|
420
|
+
if (this.scopeFilterEnabled && uuid && this.inScopeUuids && !this.inScopeUuids.has(uuid)) {
|
|
421
|
+
this.droppedOutOfScopeCount++;
|
|
422
|
+
logger.debug('Dropping out-of-scope result (C5 filter strategy)', { uuid });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
375
426
|
const project = test.parent?.project?.() || test.parent?.project;
|
|
376
427
|
const deviceInfo = DeviceOsDetector.getDeviceInfo(project, null);
|
|
377
428
|
const projectKey = `${deviceInfo.device}-${deviceInfo.os}`;
|
|
@@ -405,7 +456,15 @@ class AppliqationReporter {
|
|
|
405
456
|
return;
|
|
406
457
|
}
|
|
407
458
|
|
|
408
|
-
// Duplicate detection
|
|
459
|
+
// Duplicate detection — user-facing warning only, NOT a data-integrity
|
|
460
|
+
// guard. The backend's upsert key is (run_id, uuid, parent_uuid,
|
|
461
|
+
// browser), so re-submitting the same UUID for the same browser is
|
|
462
|
+
// idempotent at the DB level (see appq PR #532's automation.attempt
|
|
463
|
+
// $inc semantics). This tracking exists purely so the end-of-run
|
|
464
|
+
// summary can point out authoring mistakes (two tests annotated with
|
|
465
|
+
// the same UUID), and it's per-reporter-instance — cross-shard
|
|
466
|
+
// duplicates from `--shard` are not caught here, and don't need to
|
|
467
|
+
// be, because the backend's upsert still handles them correctly.
|
|
409
468
|
const trackingKey = `${runInfo.runId}:${deviceInfo.browser}`;
|
|
410
469
|
const submittedUuids = this.submittedUuidsByRun.get(trackingKey) || new Map();
|
|
411
470
|
|
|
@@ -462,9 +521,9 @@ class AppliqationReporter {
|
|
|
462
521
|
|
|
463
522
|
this.trackResult(runInfo.runId, testResult);
|
|
464
523
|
|
|
465
|
-
if (testResult.status === '
|
|
466
|
-
else if (testResult.status === '
|
|
467
|
-
else if (testResult.status === '
|
|
524
|
+
if (testResult.status === 'passed') this.passedTests++;
|
|
525
|
+
else if (testResult.status === 'failed') this.failedTests++;
|
|
526
|
+
else if (testResult.status === 'skipped') this.skippedTests++;
|
|
468
527
|
|
|
469
528
|
if (!this.config.batchSubmit) {
|
|
470
529
|
await this.client.submitResult(runInfo.runId, testResult);
|
|
@@ -660,20 +719,33 @@ class AppliqationReporter {
|
|
|
660
719
|
|
|
661
720
|
/** @private */
|
|
662
721
|
mapStatus(status) {
|
|
722
|
+
// Playwright's `interrupted` means the entire test run was aborted
|
|
723
|
+
// (Ctrl+C, CI timeout, OOM). Mapping it to 'skipped' would mask
|
|
724
|
+
// failures and inflate the skipped counter — surface as failed instead
|
|
725
|
+
// so the run summary honestly reflects an aborted execution.
|
|
726
|
+
//
|
|
727
|
+
// Values MUST match PayloadBuilder.validateAutomationResultPayload's
|
|
728
|
+
// ALLOWED_STATUSES ({passed, failed, skipped}, lowercase past-tense —
|
|
729
|
+
// mirrors the backend's InputValidator). This reporter's own mapped
|
|
730
|
+
// value is what ultimately reaches that validator (via trackResult →
|
|
731
|
+
// submitBatch → PayloadBuilder.buildResultPayload → normalizeStatus,
|
|
732
|
+
// which passes an already-recognized lowercase value straight through),
|
|
733
|
+
// so drifting from that vocabulary here silently fails every submission.
|
|
663
734
|
const statusMap = {
|
|
664
|
-
'passed': '
|
|
665
|
-
'failed': '
|
|
666
|
-
'timedOut': '
|
|
667
|
-
'skipped': '
|
|
668
|
-
'interrupted': '
|
|
735
|
+
'passed': 'passed',
|
|
736
|
+
'failed': 'failed',
|
|
737
|
+
'timedOut': 'failed',
|
|
738
|
+
'skipped': 'skipped',
|
|
739
|
+
'interrupted': 'failed'
|
|
669
740
|
};
|
|
670
|
-
return statusMap[status] || '
|
|
741
|
+
return statusMap[status] || 'failed';
|
|
671
742
|
}
|
|
672
743
|
|
|
673
744
|
/** @private */
|
|
674
745
|
buildComment(test, result) {
|
|
675
746
|
const parts = [];
|
|
676
747
|
if (result.duration) parts.push(`Duration: ${(result.duration / 1000).toFixed(2)}s`);
|
|
748
|
+
if (result.status === 'interrupted') parts.push('Test run interrupted');
|
|
677
749
|
if (result.error) {
|
|
678
750
|
const errorMsg = result.error.message || result.error.toString();
|
|
679
751
|
parts.push(`Error: ${errorMsg.substring(0, 500)}`);
|
|
@@ -868,6 +940,82 @@ class AppliqationReporter {
|
|
|
868
940
|
return Array.from(projectNames);
|
|
869
941
|
}
|
|
870
942
|
|
|
943
|
+
/**
|
|
944
|
+
* Run C5 scope validation against the Playwright suite. Mutates
|
|
945
|
+
* `this.config` if the user chooses the 'adhoc' strategy (clears
|
|
946
|
+
* scenarioId/testSetId so subsequent run creation is ad-hoc).
|
|
947
|
+
* Sets `this.scopeFilterEnabled` and `this.inScopeUuids` for the
|
|
948
|
+
* 'filter' strategy so `onTestEnd` can drop out-of-scope results.
|
|
949
|
+
*
|
|
950
|
+
* @private
|
|
951
|
+
* @param {Object} suite - Playwright Suite from onBegin
|
|
952
|
+
* @returns {Promise<{cancelled: boolean}>}
|
|
953
|
+
*/
|
|
954
|
+
async _runScopeValidation(suite, preExtractedUuids) {
|
|
955
|
+
const validator = new ScopeValidator({
|
|
956
|
+
scenarioId: this.config.scenarioId,
|
|
957
|
+
testSetId: this.config.testSetId,
|
|
958
|
+
strategy: this.config.onScopeMismatch,
|
|
959
|
+
fetchTestSetScope: this._fetchTestSetScope.bind(this)
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
if (!validator.isScoped()) {
|
|
963
|
+
return { cancelled: false };
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
const uuids = preExtractedUuids || validator.extractUuidsFromSuite(suite);
|
|
967
|
+
const validation = await validator.validate(uuids);
|
|
968
|
+
if (!validation.hasMismatch) {
|
|
969
|
+
return { cancelled: false };
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const outcome = await validator.resolve(validation);
|
|
973
|
+
// Surface the resolution message at WARN so it's visible at default
|
|
974
|
+
// log level — this is operational signal, not noise.
|
|
975
|
+
logger.warn(outcome.message);
|
|
976
|
+
|
|
977
|
+
if (outcome.action === ScopeValidator.STRATEGY.CANCEL) {
|
|
978
|
+
// Set non-zero exit code so CI fails. Don't throw — we still want
|
|
979
|
+
// Playwright to finish executing tests locally for the user's logs.
|
|
980
|
+
process.exitCode = 1;
|
|
981
|
+
return { cancelled: true };
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (outcome.overrideToAdhoc) {
|
|
985
|
+
// Drop the configured scope so createRun produces an ad-hoc run.
|
|
986
|
+
delete this.config.scenarioId;
|
|
987
|
+
delete this.config.testSetId;
|
|
988
|
+
return { cancelled: false };
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
if (outcome.action === ScopeValidator.STRATEGY.FILTER) {
|
|
992
|
+
this.scopeFilterEnabled = true;
|
|
993
|
+
this.inScopeUuids = new Set(outcome.submitUuids);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return { cancelled: false };
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/**
|
|
1000
|
+
* Resolve a testSet's UUID membership. Requires a backend endpoint
|
|
1001
|
+
* (GET /api/automation/testset/{id}/scope) that returns the UUID list.
|
|
1002
|
+
* Until that endpoint ships, this throws — ScopeValidator catches and
|
|
1003
|
+
* gracefully degrades to skip testset validation with a warning.
|
|
1004
|
+
*
|
|
1005
|
+
* @private
|
|
1006
|
+
* @param {number} testSetId
|
|
1007
|
+
* @returns {Promise<Set<string>>}
|
|
1008
|
+
*/
|
|
1009
|
+
async _fetchTestSetScope(testSetId) {
|
|
1010
|
+
if (!this.client || typeof this.client.fetchTestSetScope !== 'function') {
|
|
1011
|
+
throw new Error(
|
|
1012
|
+
'TestSet scope endpoint not yet available in AppliqationClient — '
|
|
1013
|
+
+ 'testset scope validation will be skipped'
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
return this.client.fetchTestSetScope(testSetId);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
871
1019
|
/**
|
|
872
1020
|
* Auto-detect browser versions
|
|
873
1021
|
* @private
|
|
@@ -44,10 +44,29 @@ class OrphanTestService {
|
|
|
44
44
|
|
|
45
45
|
return response;
|
|
46
46
|
} catch (error) {
|
|
47
|
-
|
|
47
|
+
// S5 — as of appq_automation_api.routing.yml, /api/automation/
|
|
48
|
+
// orphan-tests is not a registered route. Backend implementation
|
|
49
|
+
// is pending. Until it lands, a 404 here is expected — demote to
|
|
50
|
+
// debug so it doesn't spam customer CI logs. Orphans are still
|
|
51
|
+
// tracked locally and shown in the reporter's end-of-run summary.
|
|
52
|
+
const status = error.status
|
|
53
|
+
|| error.response?.status
|
|
54
|
+
|| error.details?.status
|
|
55
|
+
|| null;
|
|
56
|
+
|
|
57
|
+
if (status === 404) {
|
|
58
|
+
logger.debug('Orphan tests endpoint not available on backend; skipping network log', {
|
|
59
|
+
runId,
|
|
60
|
+
count: orphanTests.length
|
|
61
|
+
});
|
|
62
|
+
return { success: false, notImplemented: true, count: orphanTests.length };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
logger.warn('Failed to log orphan tests', {
|
|
48
66
|
error: error.message,
|
|
49
67
|
runId,
|
|
50
|
-
count: orphanTests.length
|
|
68
|
+
count: orphanTests.length,
|
|
69
|
+
status
|
|
51
70
|
});
|
|
52
71
|
throw new Error(`Failed to log orphan tests: ${error.message}`);
|
|
53
72
|
}
|