@camunda/e2e-test-suite 0.0.1129 → 0.0.1131

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 CHANGED
@@ -419,22 +419,22 @@ See [docs/ci-failure-categories.md](docs/ci-failure-categories.md) for the full
419
419
 
420
420
  When a PR opens in [camunda/camunda](https://github.com/camunda/camunda) or [camunda/connectors](https://github.com/camunda/connectors), AlwaysGreen dispatches a subset of this suite against that PR's build. This is separate from this repo's own [Automatic test runs on PR](#automatic-test-runs-on-pr) above, which cover changes to the test suite itself rather than to the product.
421
421
 
422
- | leg | scope | blocking | runtime |
423
- | --- | --- | --- | --- |
424
- | SaaS smoke | `test-setup`, `navigation`, `smoke-tests` | yes | 9–12 min |
425
- | SaaS full | remaining specs (minus exclusions below) | no, until Nov 2026 | ~40–48 min |
426
- | SM smoke | smoke suite | yes | — |
427
- | SM full | full suite | no | — |
422
+ | leg | scope |
423
+ | --- | --- |
424
+ | SaaS smoke | `test-setup`, `navigation`, `smoke-tests` |
425
+ | SaaS full | remaining specs (minus exclusions below) |
426
+ | SM smoke | smoke suite |
427
+ | SM full | full suite |
428
428
 
429
- **Excluded from PR level entirely — nightly only.** Third-party model-endpoint specs bill an external provider on every run they're included in, so they stay out of the PR-blocking path:
429
+ Whether a given leg gates a merge is decided by the caller repo's own branch protection / merge-queue configuration, not by this suite — that can change independently of anything documented here, so it's deliberately not claimed above. Runtime is likewise omitted: it shifts as specs are added, excluded, or reworked, and a stale number here is worse than none.
430
+
431
+ **Excluded from PR level entirely — nightly only.** Third-party model-endpoint specs bill an external provider on every run they're included in, so they stay out of the PR-level full-suite pass:
430
432
 
431
433
  - By spec file: `agentic-ai-user-flows.spec.ts`, `idp-user-flows.spec.ts`, `aws-connectors-user-flows.spec.ts` (AWS Bedrock, AgentCore, Vertex, Azure OpenAI)
432
- - By tag `@ai-external`: the AI-generated-form tests inside `web-modeler-user-flows.spec.ts` (`Form.js Integration with User Task and AI Generated Form`, `Form.js Integration with AI generated Start form`)
434
+ - By tag `@ai-external`: both `Form.js Integration with AI generated Start form` and `Form.js Integration with User Task and AI Generated Form` inside `web-modeler-user-flows.spec.ts` -- both call `formJsPage.generateAIForm()` (a paid model call), so both are excluded.
433
435
 
434
436
  **Kept in at PR level despite touching external systems:** Connector Secrets tests (reads from our own AWS/GCP secret managers — no model endpoint, no meaningful per-run cost) and MCP tests (run against our own cluster — no cost).
435
437
 
436
- See [AlwaysGreen Iteration 2 - Alpha 5: Coverage & Optimisation](https://github.com/camunda/team-test-automation/issues/78) for the full rationale and current SaaS/SM stability data behind these boundaries.
437
-
438
438
  ## Test Suite Structure
439
439
 
440
440
  The test suite is organized using the Page Object Model design pattern, where each test file contains test scenarios related to a specific component or feature of the Camunda C8 product stack. Additionally, it leverages separate page objects for each web page or component being tested.
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const test_1 = require("@playwright/test");
7
+ const fs_1 = __importDefault(require("fs"));
4
8
  const _8_10_1 = require("../../fixtures/8.10");
5
9
  const _setup_1 = require("../../test-setup.js");
6
10
  const connectorSecrets_1 = require("../../utils/connectorSecrets");
@@ -38,26 +42,49 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
38
42
  }
39
43
  await clusterPage.clickClusterLink(clusterName);
40
44
  await clusterDetailsPage.assertComponentsHealth();
41
- if (process.env.IS_AG !== 'true') {
45
+ // API-client creation is needed whenever this run will also exercise the
46
+ // full-suite pass: many of those specs call the Zeebe REST API directly
47
+ // and need CAMUNDA_OPTIMIZE_BASE_URL for Optimize session login, even
48
+ // though IS_AG=true skips this for the usual smoke-only case.
49
+ if (process.env.IS_AG !== 'true' || process.env.RUN_FULL_SUITE === 'true') {
42
50
  await clusterDetailsPage.clickAPITab();
43
- await clusterDetailsPage.createAPIClientAndReturnVariables(apiClientName);
51
+ const zeebeCredentials = await clusterDetailsPage.createAPIClientAndReturnVariables(apiClientName);
44
52
  await clusterDetailsPage.clickCloseModalButton();
45
- await clusterPage.clickConnectorSecretsTab(clusterName);
46
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.awsSecretsValues);
47
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.openAiSecretsValues);
48
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.gcpSecretsValues);
49
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.azureSecretsValues);
50
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.connectorSecretsValues);
51
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockKbSecretsValues);
52
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreSecretsValues);
53
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreLtmSecretsValues);
54
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockCodeInterpreterSecretsValues);
55
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.anthropicBedrockMantleSecretsValues);
53
+ if (process.env.RUN_FULL_SUITE === 'true') {
54
+ // Written to disk, not just process.env: the full-suite pass runs as
55
+ // a separate `npm run test` process (see run-tests-smoke-int's
56
+ // action.yml) that does not inherit this process's env mutations.
57
+ // mode 0o600: this file holds a live Zeebe API client secret.
58
+ fs_1.default.writeFileSync('cluster-credentials.json', JSON.stringify(zeebeCredentials), { mode: 0o600 });
59
+ }
60
+ // Deliberately NOT extended to RUN_FULL_SUITE like the API client
61
+ // above: clickConnectorSecretsTab hits the same pre-existing "click
62
+ // after 3 attempts" UI-contention flakiness seen throughout this
63
+ // suite, and here it's on test-setup's blocking critical path -- a
64
+ // transient failure takes down cluster provisioning entirely (and
65
+ // with it, both smoke and the full-suite pass), confirmed on a real
66
+ // run. connectors-user-flows.spec.ts's "Connector Secrets User Flow"
67
+ // tests keep failing in the full-suite pass without this, but that's
68
+ // a bounded, non-blocking loss -- not worth risking the whole run for.
69
+ if (process.env.IS_AG !== 'true') {
70
+ await clusterPage.clickConnectorSecretsTab(clusterName);
71
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.awsSecretsValues);
72
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.openAiSecretsValues);
73
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.gcpSecretsValues);
74
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.azureSecretsValues);
75
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.connectorSecretsValues);
76
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockKbSecretsValues);
77
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreSecretsValues);
78
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreLtmSecretsValues);
79
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockCodeInterpreterSecretsValues);
80
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.anthropicBedrockMantleSecretsValues);
81
+ }
56
82
  }
57
83
  });
58
84
  (0, _8_10_1.test)('Create Agentic Orchestration Cluster', async ({ page, loginPage, homePage, clusterPage, clusterDetailsPage, clusterSecretsPage, }, testInfo) => {
59
85
  _8_10_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
60
- _8_10_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
86
+ _8_10_1.test.skip(process.env.IS_AG === 'true' && process.env.RUN_FULL_SUITE !== 'true', 'Skipping test because not required when IS_AG is true -- unless ' +
87
+ 'RUN_FULL_SUITE also needs this cluster for the MCP suite.');
61
88
  _8_10_1.test.slow();
62
89
  const clusterName = 'Agentic Orchestration Cluster';
63
90
  const apiClientName = 'Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
@@ -82,7 +109,14 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
82
109
  await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
83
110
  await clusterPage.clickClusterLink(clusterName);
84
111
  await clusterDetailsPage.clickAPITab();
85
- await clusterDetailsPage.createAPIClientAndReturnVariables(apiClientName, true, 'agentic_cluster');
112
+ const agenticCredentials = await clusterDetailsPage.createAPIClientAndReturnVariables(apiClientName, true, 'agentic_cluster');
113
+ if (process.env.RUN_FULL_SUITE === 'true') {
114
+ // Written to disk, not just process.env: the full-suite pass runs as
115
+ // a separate `npm run test` process (see run-tests-smoke-int's
116
+ // action.yml) that does not inherit this process's env mutations.
117
+ // mode 0o600: this file holds a live Zeebe API client secret.
118
+ fs_1.default.writeFileSync('agentic-cluster-credentials.json', JSON.stringify(agenticCredentials), { mode: 0o600 });
119
+ }
86
120
  await clusterDetailsPage.clickCloseModalButton();
87
121
  const mcpCredentials = await clusterDetailsPage.createMCPClientAndReturnVariables(mcpClientName);
88
122
  await clusterDetailsPage.clickCloseModalButton();
@@ -109,10 +143,22 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
109
143
  await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.connectorSecretsValues);
110
144
  }
111
145
  });
146
+ // Stagger 5x wider than the other setup tests above, capped at 60s: this
147
+ // loop logs in getTestUsers().length users (up to 27, one per worker) at
148
+ // once, so at 1000ms/worker the whole burst lands inside a ~20-30s window
149
+ // and trips Auth0's rate limiter (AUTH0_RATE_LIMIT, e.g. run 34099726060's
150
+ // User 18 failure) well before loginWithRetry's own 60-90s backoff gets a
151
+ // chance to help. An uncapped 5000ms/worker fixed the rate limit (run
152
+ // 34103380836 had none) but then burned up to 90s of the highest-indexed
153
+ // worker's fixed 12-minute test timeout on pure stagger sleep before its
154
+ // first login attempt, leaving too little budget for the retry loop to
155
+ // recover from an unrelated slow-page timeout (also run 34103380836,
156
+ // User 18 again). The 60s cap keeps most of the wider spread while
157
+ // bounding worst-case pre-login wait to 1/12 of the test budget.
112
158
  for (const [index, user] of (0, users_1.getTestUsers)().entries()) {
113
159
  (0, _8_10_1.test)(`Create Project Folder for User ${index + 1}`, async ({ page, loginPage, homePage, appsPage, modelerHomePage, }, testInfo) => {
114
160
  await _8_10_1.test.step('Log in', async () => {
115
- await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
161
+ await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, Math.min((testInfo.workerIndex + 1) * 5000, 60000));
116
162
  });
117
163
  await _8_10_1.test.step('Navigate to Web Modeler', async () => {
118
164
  await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
@@ -121,8 +167,23 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
121
167
  await appsPage.openModelerWithRetry(modelerHomePage.modelerPageBanner, 180000);
122
168
  });
123
169
  await _8_10_1.test.step('Create Cross Component Project Folder', async () => {
124
- await (0, UtilitiesPage_1.withSessionRecovery)(page, loginPage, appsPage, modelerHomePage.modelerPageBanner, user, (testInfo.workerIndex + 1) * 1000, async () => {
170
+ await (0, UtilitiesPage_1.withSessionRecovery)(page, loginPage, appsPage, modelerHomePage.modelerPageBanner, user, Math.min((testInfo.workerIndex + 1) * 5000, 60000), async () => {
125
171
  await homePage.clickSkipCustomization();
172
+ // Reverted the "only User 1 creates" leader-election attempt: it made
173
+ // things worse, not better. Run 34191243820 showed followers still
174
+ // timing out waiting for the folder even well after User 1 (2.9min)
175
+ // had already finished creating it -- not a wait-budget problem, since
176
+ // widening it wouldn't fix a folder that isn't becoming visible to
177
+ // other sessions at all. Nightly (run-tests-saas) and on-demand never
178
+ // hit this because both run test-setup.spec.ts as part of one
179
+ // unfiltered `--project=X` invocation together with hundreds of other
180
+ // tests, rather than isolating it into its own "Provision cluster"
181
+ // step the way this PR's action.yml does -- that isolation is what
182
+ // creates the every-worker-races-at-once window in the first place.
183
+ // Back to every user independently checking-and-creating: still races
184
+ // sometimes (3-7 of 18 users per run, per the create-organization
185
+ // rate-limit findings above), but that's a smaller, known failure mode
186
+ // than the 100% follower failure the leader-election attempt caused.
126
187
  await modelerHomePage.createCrossComponentProjectFolder();
127
188
  await modelerHomePage.clickHomeBreadcrumb();
128
189
  await (0, test_1.expect)(modelerHomePage.crossComponentProjectFolder).toBeVisible({ timeout: 60000 });
@@ -34,7 +34,7 @@ _8_10_1.test.describe('Web Modeler User Flow Tests', () => {
34
34
  await (0, mailSlurpClient_1.deleteInbox)(inboxes[testInfo.title]);
35
35
  }
36
36
  });
37
- (0, _8_10_1.test)('Form.js Integration with User Task and AI Generated Form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
37
+ (0, _8_10_1.test)('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
38
38
  _8_10_1.test.slow();
39
39
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
40
40
  const processName = 'User_Task_Process_With_AI_Form' + randomString;
@@ -92,7 +92,7 @@ _8_10_1.test.describe('Web Modeler User Flow Tests', () => {
92
92
  await operateTabTaskDetailsPage.clickCompleteTaskButton();
93
93
  });
94
94
  });
95
- (0, _8_10_1.test)('Form.js Integration with AI generated Start form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
95
+ (0, _8_10_1.test)('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
96
96
  _8_10_1.test.slow();
97
97
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
98
98
  const processName = 'AI-generated-Start-form' + randomString;
@@ -33,7 +33,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
33
33
  await (0, mailSlurpClient_1.deleteInbox)(inboxes[testInfo.title]);
34
34
  }
35
35
  });
36
- (0, _8_7_1.test)('Form.js Integration with User Task and AI Generated Form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
36
+ (0, _8_7_1.test)('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
37
37
  _8_7_1.test.slow();
38
38
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
39
39
  const processName = 'User_Task_Process_With_AI_Form' + randomString;
@@ -280,7 +280,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
280
280
  });
281
281
  });
282
282
  });
283
- (0, _8_7_1.test)('Form.js Integration with AI generated Start form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
283
+ (0, _8_7_1.test)('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
284
284
  _8_7_1.test.slow();
285
285
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
286
286
  const processName = 'AI-generated-Start-form' + randomString;
@@ -31,7 +31,7 @@ _8_8_1.test.describe('Web Modeler User Flow Tests', () => {
31
31
  await (0, mailSlurpClient_1.deleteInbox)(inboxes[testInfo.title]);
32
32
  }
33
33
  });
34
- (0, _8_8_1.test)('Form.js Integration with User Task and AI Generated Form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
34
+ (0, _8_8_1.test)('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
35
35
  _8_8_1.test.slow();
36
36
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
37
37
  const processName = 'User_Task_Process_With_AI_Form' + randomString;
@@ -88,7 +88,7 @@ _8_8_1.test.describe('Web Modeler User Flow Tests', () => {
88
88
  await operateTabTaskDetailsPage.clickCompleteTaskButton();
89
89
  });
90
90
  });
91
- (0, _8_8_1.test)('Form.js Integration with AI generated Start form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
91
+ (0, _8_8_1.test)('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
92
92
  _8_8_1.test.slow();
93
93
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
94
94
  const processName = 'AI-generated-Start-form' + randomString;
@@ -34,7 +34,7 @@ _8_9_1.test.describe('Web Modeler User Flow Tests', () => {
34
34
  await (0, mailSlurpClient_1.deleteInbox)(inboxes[testInfo.title]);
35
35
  }
36
36
  });
37
- (0, _8_9_1.test)('Form.js Integration with User Task and AI Generated Form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
37
+ (0, _8_9_1.test)('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
38
38
  _8_9_1.test.slow();
39
39
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
40
40
  const processName = 'User_Task_Process_With_AI_Form' + randomString;
@@ -92,7 +92,7 @@ _8_9_1.test.describe('Web Modeler User Flow Tests', () => {
92
92
  await operateTabTaskDetailsPage.clickCompleteTaskButton();
93
93
  });
94
94
  });
95
- (0, _8_9_1.test)('Form.js Integration with AI generated Start form', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
95
+ (0, _8_9_1.test)('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
96
96
  _8_9_1.test.slow();
97
97
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
98
98
  const processName = 'AI-generated-Start-form' + randomString;
@@ -98,7 +98,15 @@ function getTestUser(userKey) {
98
98
  }
99
99
  exports.getTestUser = getTestUser;
100
100
  function getTestUsers() {
101
- if (process.env.IS_AG === 'true' && process.env.MULTI_USERS === 'true') {
101
+ // test-setup.spec.ts loops over this list to provision one Cross Component
102
+ // Project folder per user. Full-suite specs each log in as a different
103
+ // named user (getTestUser('eleventhUser') etc.) via getTestUser, not one
104
+ // of the 3 smokeTestUsers -- so under IS_AG=true, RUN_FULL_SUITE=true still
105
+ // needs the full list, or every one of those users hits a folder that was
106
+ // never created for them.
107
+ if (process.env.IS_AG === 'true' &&
108
+ process.env.MULTI_USERS === 'true' &&
109
+ process.env.RUN_FULL_SUITE !== 'true') {
102
110
  return Object.values(exports.smokeTestUsers);
103
111
  }
104
112
  else if (process.env.MULTI_USERS === 'true') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1129",
3
+ "version": "0.0.1131",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,17 +10,23 @@ if (!process.env.CAMUNDA_OPTIMIZE_BASE_URL && process.env.BASE_URL) {
10
10
 
11
11
  const projectName = process.env.PROJECT! || 'Chromium'; // Default to 'default-project' if PROJECT is not set
12
12
 
13
+ // Set only by the concurrent full-suite pass in run-tests-smoke-int's action.yml,
14
+ // so it doesn't clobber the smoke pass's report files when both `npm run test`
15
+ // invocations run at the same time against the same working directory.
16
+ // Empty by default -- every other caller is unaffected.
17
+ const reportSuffix = process.env.REPORT_SUFFIX || '';
18
+
13
19
  const junitOptions = {
14
20
  embedAnnotationsAsProperties: true,
15
- outputFile: './test-results/junit-report.xml',
21
+ outputFile: `./test-results${reportSuffix}/junit-report.xml`,
16
22
  };
17
23
 
18
24
  // Define reporters without SlackReporter
19
25
  const useReportersWithoutSlack: any[] = [
20
26
  ['list'],
21
27
  ['junit', junitOptions],
22
- ['html', {outputFolder: 'html-report'}],
23
- ['json', {outputFile: `json-report/results.json`}],
28
+ ['html', {outputFolder: `html-report${reportSuffix}`}],
29
+ ['json', {outputFile: `json-report${reportSuffix}/results.json`}],
24
30
  ];
25
31
 
26
32
  // Define reporters with SlackReporter
@@ -256,6 +262,7 @@ export default defineConfig({
256
262
  workers: workerCount(),
257
263
  retries: 1,
258
264
  fullyParallel: false,
265
+ outputDir: `test-results${reportSuffix}`,
259
266
  use: {
260
267
  baseURL: getBaseURL(),
261
268
  actionTimeout: 10000,