@trackunit/iris-app-e2e 0.0.2-alpha-49e9177e1b3.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 ADDED
@@ -0,0 +1,227 @@
1
+ # @trackunit/iris-app-e2e
2
+
3
+ A comprehensive E2E testing utilities library for Trackunit's Iris platform. This package provides reusable Cypress commands, setup utilities, and configuration helpers to streamline E2E testing for both internal and external developers.
4
+
5
+ This library is exposed publicly for use in the Trackunit [Iris App SDK](https://www.npmjs.com/package/@trackunit/iris-app).
6
+
7
+ To browse all available components visit our [Public Storybook](https://apps.iris.trackunit.com/storybook/).
8
+
9
+ For more info and a full guide on Iris App SDK Development, please visit our [Developer Hub](https://developers.trackunit.com/).
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install @trackunit/iris-app-e2e --save-dev
15
+ ```
16
+
17
+ ## Peer Dependencies
18
+
19
+ This package requires the following peer dependencies:
20
+
21
+ ```bash
22
+ npm install cypress @testing-library/cypress --save-dev
23
+ ```
24
+
25
+ ## Features
26
+
27
+ ### 🔧 Cypress Commands
28
+
29
+ - **`getByTestId`** - Enhanced data-testid selector with timeout options
30
+ - **`login`** - Automated login with support for multiple user fixtures
31
+ - **`switchToLocalDevMode`** - Switch to local development mode in Iris SDK portal
32
+ - **`enterIrisApp`** - Navigate into Iris app iframes for testing
33
+ - **`enterStorybookPreview`** - Access Storybook preview iframes
34
+ - **`getValidateFeatureFlags`** - Set up feature flag validation intercepts
35
+ - **`configCat`** - Retrieve ConfigCat feature flag states
36
+
37
+ ### 🚀 E2E Setup Utilities
38
+
39
+ - **`setupE2E`** - Complete E2E test environment setup
40
+ - **`setupHarRecording`** - HAR file recording for failed tests
41
+ - **Terminal logging** - Comprehensive test logging and error capture
42
+
43
+ ### ⚙️ Plugin Configuration
44
+
45
+ - **`defaultCypressConfig`** - Configurable Cypress configuration
46
+ - **`setupPlugins`** - Plugin setup with logging and HAR generation
47
+ - **File utilities** - Log file creation and formatting tools
48
+
49
+ ## Usage
50
+
51
+ ### Basic Setup
52
+
53
+ Create a `cypress/support/e2e.ts` file:
54
+
55
+ ```typescript
56
+ import { setupDefaultCommands, setupE2E } from '@trackunit/iris-app-e2e';
57
+
58
+ // Set up E2E environment
59
+ setupE2E();
60
+
61
+ // Register custom commands
62
+ setupDefaultCommands();
63
+ ```
64
+
65
+ ### Cypress Configuration
66
+
67
+ Create a `cypress.config.ts` file:
68
+
69
+ #### Simple Configuration (Backward Compatible)
70
+ ```typescript
71
+ import { defineConfig } from 'cypress';
72
+ import { defaultCypressConfig, setupPlugins } from '@trackunit/iris-app-e2e';
73
+ import { install } from '@neuralegion/cypress-har-generator';
74
+ import { format, resolveConfig } from 'prettier';
75
+
76
+ export default defineConfig({
77
+ e2e: {
78
+ ...defaultCypressConfig(__dirname),
79
+ setupNodeEvents(on, config) {
80
+ setupPlugins(
81
+ on,
82
+ config,
83
+ { format, resolveConfig }, // Prettier formatter
84
+ install // HAR generator installer
85
+ );
86
+ }
87
+ }
88
+ });
89
+ ```
90
+
91
+ #### Advanced Configuration (New API)
92
+ ```typescript
93
+ import { defineConfig } from 'cypress';
94
+ import { defaultCypressConfig, setupPlugins } from '@trackunit/iris-app-e2e';
95
+ import { install } from '@neuralegion/cypress-har-generator';
96
+ import { format, resolveConfig } from 'prettier';
97
+
98
+ export default defineConfig({
99
+ e2e: {
100
+ ...defaultCypressConfig({
101
+ // Optional: Custom configuration
102
+ behaviorConfig: {
103
+ defaultCommandTimeout: 30000,
104
+ retries: { runMode: 2, openMode: 0 }
105
+ }
106
+ }),
107
+ setupNodeEvents(on, config) {
108
+ setupPlugins(
109
+ on,
110
+ config,
111
+ { format, resolveConfig }, // Prettier formatter
112
+ install // HAR generator installer
113
+ );
114
+ }
115
+ }
116
+ });
117
+ ```
118
+
119
+ ### Using Commands in Tests
120
+
121
+ ```typescript
122
+ describe('Iris App E2E Tests', () => {
123
+ it('should login and navigate to app', () => {
124
+ // Login with default user
125
+ cy.login();
126
+
127
+ // Or login with specific fixture
128
+ cy.login('managere2e-admin');
129
+
130
+ // Find elements by test ID
131
+ cy.getByTestId('navigation-menu').should('be.visible');
132
+
133
+ // Switch to local development mode
134
+ cy.switchToLocalDevMode();
135
+
136
+ // Enter an Iris app iframe
137
+ cy.enterIrisApp().then(app => {
138
+ app().getByTestId('app-content').should('exist');
139
+ });
140
+ });
141
+
142
+ it('should check feature flags', () => {
143
+ cy.getValidateFeatureFlags();
144
+ cy.configCat('my-feature-flag').then(isEnabled => {
145
+ if (isEnabled) {
146
+ cy.getByTestId('feature-content').should('be.visible');
147
+ }
148
+ });
149
+ });
150
+ });
151
+ ```
152
+
153
+ ## Configuration Options
154
+
155
+ ### E2EConfigOptions
156
+
157
+ ```typescript
158
+ interface E2EConfigOptions {
159
+ nxRoot?: string; // NX workspace root (auto-detected)
160
+ outputDirOverride?: string; // Custom output directory
161
+ projectConfig?: E2EProjectConfig; // Project-specific settings
162
+ behaviorConfig?: E2EBehaviorConfig; // Timeout and retry settings
163
+ pluginConfig?: E2EPluginConfig; // Output path configuration
164
+ }
165
+ ```
166
+
167
+ ### Environment Variables
168
+
169
+ - **`NX_FEATURE_BRANCH_BASE_URL`** - Override base URL for testing
170
+ - **`NX_E2E_OUTPUT_DIR`** - Custom E2E output directory
171
+
172
+ ## Authentication
173
+
174
+ The library uses Trackunit's standard authentication flow:
175
+
176
+ 1. Fetches environment configuration from `/env` endpoint
177
+ 2. Authenticates with Okta using session tokens
178
+ 3. Redirects to Manager
179
+
180
+ ### User Fixtures
181
+
182
+ Supported user fixture files:
183
+
184
+ - `auth` (default)
185
+
186
+ Example fixture file (`cypress/fixtures/auth.json`):
187
+
188
+ ```json
189
+ {
190
+ "username": "test@example.com",
191
+ "password": "your-password"
192
+ }
193
+ ```
194
+
195
+ ## Host Configuration
196
+
197
+ Tests run against the same Trackunit infrastructure:
198
+
199
+ - Dynamic environment discovery via `/env` endpoint
200
+ - Consistent authentication endpoints
201
+ - Same Manager routes and functionality
202
+
203
+ Set the base URL via Cypress configuration or environment variables to target different environments (staging, production, etc.).
204
+
205
+ ## HAR Recording
206
+
207
+ Failed tests automatically generate HAR files for debugging:
208
+
209
+ - Stored in configured output directory
210
+ - Named with test name and attempt number
211
+ - Includes full network traffic for analysis
212
+
213
+ ## TypeScript Support
214
+
215
+ The package includes comprehensive TypeScript definitions for all Cypress commands and configuration options. Your IDE will provide full autocomplete and type checking.
216
+
217
+ ## Development
218
+
219
+ At this point this library is only developed by Trackunit Employees.
220
+ For development related information see the [development readme](https://github.com/Trackunit/manager/blob/master/libs/react/components/DEVELOPMENT.md).
221
+
222
+ ## Trackunit
223
+
224
+ This package was developed by Trackunit ApS.
225
+ Trackunit is the leading SaaS-based IoT solution for the construction industry, offering an ecosystem of hardware, fleet management software & telematics.
226
+
227
+ ![The Trackunit logo](https://trackunit.com/wp-content/uploads/2022/03/top-logo.svg)
package/index.cjs.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/index.cjs.js ADDED
@@ -0,0 +1,395 @@
1
+ 'use strict';
2
+
3
+ var fs = require('fs');
4
+ var path = require('path');
5
+ var crypto = require('crypto');
6
+ var nodeXlsx = require('node-xlsx');
7
+
8
+ function _interopNamespaceDefault(e) {
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
26
+
27
+ /**
28
+ * Sets up default Cypress commands for E2E testing.
29
+ * Adds custom commands like getByTestId, login, enterIrisApp, etc.
30
+ */
31
+ function setupDefaultCommands() {
32
+ Cypress.Commands.add("getByTestId", {
33
+ prevSubject: ["optional"],
34
+ },
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ (subject, testId, options = {}) => {
37
+ const selector = `[data-testid="${testId}"]`;
38
+ const timeout = options.timeout ?? 15000;
39
+ if (subject) {
40
+ if (Cypress.dom.isElement(subject) || Cypress.dom.isJquery(subject)) {
41
+ return cy.wrap(subject, { timeout }).find(selector, { timeout });
42
+ }
43
+ else if (Cypress.dom.isWindow(subject)) {
44
+ return cy.get(selector, { timeout });
45
+ }
46
+ else if (Array.isArray(subject)) {
47
+ const element = subject.map(el => cy.wrap(el, { timeout }).find(selector, { timeout }));
48
+ if (element[0]) {
49
+ return element[0];
50
+ }
51
+ return cy.wrap(null);
52
+ }
53
+ return cy.get(selector, { timeout });
54
+ }
55
+ else {
56
+ return cy.get(selector, { timeout });
57
+ }
58
+ });
59
+ Cypress.Commands.add("login", fixture => {
60
+ const envUrl = `${Cypress.config().baseUrl}/env`;
61
+ cy.log(`Getting env from: ${envUrl}`);
62
+ cy.request({
63
+ method: "GET",
64
+ url: envUrl,
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ },
68
+ }).then(envResponse => {
69
+ const env = envResponse.body;
70
+ const domain = env.auth?.url;
71
+ if (!domain) {
72
+ throw new Error(`No domain found from servers /env found env: ${JSON.stringify(env)}`);
73
+ }
74
+ cy.log(`Using: ${domain}`);
75
+ cy.clearCookies();
76
+ cy.fixture(fixture ?? "auth").then(({ username, password }) => {
77
+ const options = {
78
+ warnBeforePasswordExpired: true,
79
+ multiOptionalFactorEnroll: false,
80
+ };
81
+ cy.request({
82
+ method: "POST",
83
+ url: `${domain}/api/v1/authn`,
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ },
87
+ body: {
88
+ username,
89
+ password,
90
+ options,
91
+ },
92
+ })
93
+ .then(response => {
94
+ if (response.isOkStatusCode) {
95
+ const sessionToken = response.body.sessionToken;
96
+ return cy.visit(`/auth/manager-classic#session_token=${sessionToken}&fleetHome=true`);
97
+ }
98
+ else {
99
+ throw new Error(`Could not get a session token for user: ${username}, ${JSON.stringify(response)}`);
100
+ }
101
+ })
102
+ .url()
103
+ .should("contain", `${Cypress.config().baseUrl}`)
104
+ .should("contain", `/map`)
105
+ .getByTestId("map-page", { timeout: 30000 })
106
+ .should("be.visible");
107
+ });
108
+ });
109
+ });
110
+ Cypress.Commands.add("switchToLocalDevMode", () => {
111
+ cy.getByTestId("developerPortalNav").click();
112
+ cy.url({ timeout: 15000, log: true }).should("contain", "/iris-sdk-portal");
113
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
114
+ cy.getByTestId("localDevModeSwitch-input").then(($ele) => {
115
+ if ($ele && !$ele.is(":checked")) {
116
+ cy.getByTestId("localDevModeSwitch-thumb").click();
117
+ }
118
+ });
119
+ });
120
+ Cypress.Commands.add("enterIrisApp", options => {
121
+ return cy
122
+ .get(`iframe[data-testid="${options?.testId ?? "app-iframe"}"]`, { timeout: 30000 })
123
+ .first()
124
+ .its("0.contentDocument.body", { timeout: 30000, log: true })
125
+ .should("not.be.empty")
126
+ .then($body => {
127
+ return () => cy.wrap($body);
128
+ });
129
+ });
130
+ Cypress.Commands.add("enterStorybookPreview", options => {
131
+ return cy
132
+ .get(`iframe[id="${options?.testId ?? "storybook-preview-iframe"}"]`, { timeout: 30000 })
133
+ .first()
134
+ .its("0.contentDocument.body", { timeout: 30000, log: true })
135
+ .should("not.be.empty")
136
+ .then($body => {
137
+ return () => cy.wrap($body);
138
+ });
139
+ });
140
+ Cypress.Commands.add("getValidateFeatureFlags", () => {
141
+ cy.intercept({ url: "**/ValidateFeatureFlags" }).as("ValidateFeatureFlags");
142
+ cy.intercept({ url: "**/UserPermissions" }).as("UserPermissions");
143
+ cy.intercept({ url: "**/ActiveSubscription" }).as("ActiveSubscription");
144
+ });
145
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
+ let ccData = null;
147
+ Cypress.Commands.add("configCat", value => {
148
+ if (!ccData) {
149
+ cy.wait("@ValidateFeatureFlags").then(intercept => {
150
+ ccData = intercept.response?.body?.data?.featureFlags?.find((ff) => ff.key === value);
151
+ return ccData?.state;
152
+ });
153
+ }
154
+ else {
155
+ return ccData?.state;
156
+ }
157
+ });
158
+ }
159
+
160
+ /* eslint-disable no-console */
161
+ /**
162
+ * Writes a file with Prettier formatting applied.
163
+ * Automatically detects parser based on file extension.
164
+ */
165
+ const writeFileWithPrettier = async (nxRoot, filePath, content, writeOptions = { encoding: "utf-8" }, writer) => {
166
+ const prettierConfigPath = path__namespace.join(nxRoot, ".prettierrc");
167
+ const options = await writer
168
+ .resolveConfig(prettierConfigPath)
169
+ .catch(error => console.log("Prettier config error: ", error));
170
+ if (!options) {
171
+ throw new Error("Could not find prettier config");
172
+ }
173
+ if (filePath.endsWith("json")) {
174
+ options.parser = "json";
175
+ }
176
+ else {
177
+ options.parser = "typescript";
178
+ }
179
+ try {
180
+ const prettySrc = await writer.format(content, options);
181
+ fs.writeFileSync(filePath, prettySrc, writeOptions);
182
+ }
183
+ catch (error) {
184
+ console.error("Error in prettier.format:", error);
185
+ }
186
+ };
187
+
188
+ const isNetworkCall = (log) => {
189
+ return log.type === "cy:fetch" || log.type === "cy:request" || log.type === "cy:response" || log.type === "cy:xrh";
190
+ };
191
+ /**
192
+ * Creates log files for Cypress test runs.
193
+ * Generates separate files for all logs, errors, and network errors.
194
+ */
195
+ function createLogFile(nxRoot, logsPath, fileNameWithoutExtension, logs, logWriter) {
196
+ if (!fs.existsSync(logsPath)) {
197
+ fs.mkdirSync(logsPath, { recursive: true });
198
+ }
199
+ const logFilePath = path.join(logsPath, fileNameWithoutExtension);
200
+ writeFileWithPrettier(nxRoot, logFilePath + "-all.json", JSON.stringify(logs), { flag: "a" }, logWriter);
201
+ const errorCmds = logs.filter(log => log.severity === "error" &&
202
+ // This seems to be when apollo has cancelled requests it marks it as failed to fetch only on cypress ?!??
203
+ // might be fixed by https://github.com/Trackunit/manager/pull/12917
204
+ !log.message.includes("TypeError: Failed to fetch"));
205
+ if (errorCmds.length > 0) {
206
+ writeFileWithPrettier(nxRoot, logFilePath + "-errors.json", JSON.stringify(errorCmds), {
207
+ flag: "a",
208
+ }, logWriter);
209
+ }
210
+ const networkErrorsCmds = logs.filter(log => isNetworkCall(log) &&
211
+ !log.message.includes("Status: 200") &&
212
+ log.severity !== "success" &&
213
+ !log.message.includes("sentry.io/api/") &&
214
+ // This seems to be when apollo has cancelled requests it marks it as failed to fetch only on cypress ?!??
215
+ !log.message.includes("TypeError: Failed to fetch"));
216
+ if (networkErrorsCmds.length > 0) {
217
+ writeFileWithPrettier(nxRoot, logFilePath + "-network-errors.json", JSON.stringify(networkErrorsCmds), {
218
+ flag: "a",
219
+ }, logWriter);
220
+ }
221
+ }
222
+
223
+ /**
224
+ * Utility function to find NX workspace root by looking for nx.json or workspace.json.
225
+ * This is more reliable than hardcoded relative paths and works from any directory.
226
+ *
227
+ * @param startDir Starting directory for the search (defaults to current working directory)
228
+ * @returns {string} Absolute path to the workspace root
229
+ * @throws Error if workspace root cannot be found
230
+ */
231
+ function findWorkspaceRoot(startDir = process.cwd()) {
232
+ let currentDir = startDir;
233
+ while (currentDir !== path.dirname(currentDir)) {
234
+ if (fs.existsSync(path.join(currentDir, "nx.json")) || fs.existsSync(path.join(currentDir, "workspace.json"))) {
235
+ return currentDir;
236
+ }
237
+ currentDir = path.dirname(currentDir);
238
+ }
239
+ throw new Error("Could not find NX workspace root (nx.json or workspace.json not found)");
240
+ }
241
+ /**
242
+ * Creates default Cypress configuration for E2E testing.
243
+ * Supports both legacy string parameter (dirname) and new options object for backward compatibility.
244
+ */
245
+ const defaultCypressConfig = optionsOrDirname => {
246
+ // Support both old API (string/undefined) and new API (object) for backward compatibility
247
+ const options = typeof optionsOrDirname === "object" ? optionsOrDirname : {};
248
+ const { nxRoot: providedNxRoot, outputDirOverride, projectConfig = {}, behaviorConfig = {}, pluginConfig = {}, } = options;
249
+ // Use NX workspace detection for reliable root finding
250
+ const nxRoot = providedNxRoot ?? findWorkspaceRoot();
251
+ // For output path calculation, determine the relative path from caller to workspace root
252
+ const callerDirname = typeof optionsOrDirname === "string" ? optionsOrDirname : process.cwd();
253
+ const relativePath = path.relative(nxRoot, callerDirname);
254
+ const dotsToNxRoot = relativePath
255
+ .split(path.sep)
256
+ .map(_ => "..")
257
+ .join("/");
258
+ const envOutputDirOverride = process.env.NX_E2E_OUTPUT_DIR || outputDirOverride;
259
+ // Function to build output paths that respects the override
260
+ const buildOutputPath = (subPath) => {
261
+ if (envOutputDirOverride) {
262
+ return path.join(dotsToNxRoot, envOutputDirOverride, subPath);
263
+ }
264
+ return `${dotsToNxRoot}/dist/cypress/${relativePath}/${subPath}`;
265
+ };
266
+ return {
267
+ projectId: projectConfig.projectId ?? process.env.CYPRESS_PROJECT_ID,
268
+ defaultCommandTimeout: behaviorConfig.defaultCommandTimeout ?? 20000,
269
+ execTimeout: behaviorConfig.execTimeout ?? 300000,
270
+ taskTimeout: behaviorConfig.taskTimeout ?? 35000,
271
+ pageLoadTimeout: behaviorConfig.pageLoadTimeout ?? 35000,
272
+ // setting to undefined makes no effect on the baseUrl so child projects can override it
273
+ baseUrl: process.env.NX_FEATURE_BRANCH_BASE_URL ?? undefined,
274
+ requestTimeout: behaviorConfig.requestTimeout ?? 25000,
275
+ responseTimeout: behaviorConfig.responseTimeout ?? 150000,
276
+ retries: behaviorConfig.retries ?? {
277
+ runMode: 3,
278
+ openMode: 0,
279
+ },
280
+ fixturesFolder: projectConfig.fixturesFolder ?? "./src/fixtures",
281
+ downloadsFolder: pluginConfig.outputPath ?? buildOutputPath("downloads"),
282
+ logsFolder: pluginConfig.logsFolder ?? buildOutputPath("logs"),
283
+ chromeWebSecurity: false,
284
+ reporter: "junit",
285
+ reporterOptions: {
286
+ mochaFile: buildOutputPath("results-[hash].xml"),
287
+ },
288
+ specPattern: projectConfig.specPattern ?? "src/e2e/**/*.e2e.{js,jsx,ts,tsx}",
289
+ supportFile: projectConfig.supportFile ?? "src/support/e2e.ts",
290
+ nxRoot,
291
+ fileServerFolder: ".",
292
+ video: true,
293
+ videosFolder: pluginConfig.videosFolder ?? buildOutputPath("videos"),
294
+ screenshotsFolder: pluginConfig.screenshotsFolder ?? buildOutputPath("screenshots"),
295
+ env: {
296
+ hars_folders: pluginConfig.harFolder ?? buildOutputPath("hars"),
297
+ CYPRESS_RUN_UNIQUE_ID: crypto.randomUUID(),
298
+ },
299
+ };
300
+ };
301
+ /**
302
+ * Sets up Cypress plugins for logging, tasks, and HAR generation.
303
+ * Configures terminal reporting, XLSX parsing, and HTTP archive recording.
304
+ */
305
+ const setupPlugins = (on, config, logWriter, installHarGenerator) => {
306
+ /* ---- BEGIN: Logging setup ---- */
307
+ // Read options https://github.com/archfz/cypress-terminal-report
308
+ const options = {
309
+ printLogsToFile: "always", // Ensures logs are always printed to a file
310
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
311
+ collectTestLogs: (context, logs) => {
312
+ const testName = context.state.toUpperCase() +
313
+ "_" +
314
+ (config.currentRetry ? `( attempt ${config.currentRetry})_` : "") +
315
+ context.test;
316
+ createLogFile(config.nxRoot, config.logsFolder, testName, logs, logWriter);
317
+ },
318
+ };
319
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
320
+ require("cypress-terminal-report/src/installLogsPrinter")(on, options);
321
+ /* ---- END: Logging setup ---- */
322
+ /* ---- BEGIN: Task setup ---- */
323
+ on("task", {
324
+ parseXlsx(filePath) {
325
+ return new Promise((resolve, reject) => {
326
+ try {
327
+ const jsonData = nodeXlsx.parse(fs.readFileSync(filePath));
328
+ resolve(jsonData);
329
+ }
330
+ catch (e) {
331
+ reject(e);
332
+ }
333
+ });
334
+ },
335
+ fileExists(filename) {
336
+ return fs.existsSync(filename);
337
+ },
338
+ });
339
+ /* ---- END: Task setup ---- */
340
+ /* ---- BEGIN: HAR setup ---- */
341
+ // Installing the HAR geneartor should happen last according to the documentation
342
+ // https://github.com/NeuraLegion/cypress-har-generator?tab=readme-ov-file#setting-up-the-plugin
343
+ installHarGenerator(on);
344
+ /* ---- END: HAR setup ---- */
345
+ };
346
+
347
+ /* eslint-disable local-rules/no-typescript-assertion, @typescript-eslint/no-explicit-any */
348
+ /**
349
+ * Sets up HAR (HTTP Archive) recording for E2E tests.
350
+ * Records network activity and saves HAR files for failed tests.
351
+ */
352
+ function setupHarRecording() {
353
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
354
+ require("@neuralegion/cypress-har-generator/commands");
355
+ beforeEach(() => {
356
+ const harDir = Cypress.env("hars_folders");
357
+ cy.recordHar({ rootDir: harDir });
358
+ });
359
+ afterEach(function () {
360
+ if (this.currentTest.state === "failed") {
361
+ const harDir = Cypress.env("hars_folders");
362
+ const testName = "FAILED_" + Cypress.currentTest.title + (Cypress.currentRetry ? `_( attempt ${Cypress.currentRetry})_` : "");
363
+ cy.saveHar({ outDir: harDir, fileName: testName });
364
+ }
365
+ });
366
+ }
367
+
368
+ /**
369
+ * Sets up the E2E testing environment with HAR recording and terminal logging.
370
+ */
371
+ function setupE2E() {
372
+ setupHarRecording();
373
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
374
+ require("cypress-terminal-report/src/installLogsCollector")({
375
+ xhr: {
376
+ printHeaderData: true,
377
+ printRequestData: true,
378
+ },
379
+ });
380
+ Cypress.on("uncaught:exception", (err) => {
381
+ // eslint-disable-next-line no-console
382
+ console.log("Caught Error: ", err);
383
+ // returning false here prevents Cypress from
384
+ // failing the test
385
+ return false;
386
+ });
387
+ }
388
+
389
+ exports.createLogFile = createLogFile;
390
+ exports.defaultCypressConfig = defaultCypressConfig;
391
+ exports.setupDefaultCommands = setupDefaultCommands;
392
+ exports.setupE2E = setupE2E;
393
+ exports.setupHarRecording = setupHarRecording;
394
+ exports.setupPlugins = setupPlugins;
395
+ exports.writeFileWithPrettier = writeFileWithPrettier;
package/index.esm.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index";
package/index.esm.js ADDED
@@ -0,0 +1,369 @@
1
+ import fs, { writeFileSync, existsSync, readFileSync } from 'fs';
2
+ import * as path from 'path';
3
+ import path__default from 'path';
4
+ import crypto from 'crypto';
5
+ import { parse } from 'node-xlsx';
6
+
7
+ /**
8
+ * Sets up default Cypress commands for E2E testing.
9
+ * Adds custom commands like getByTestId, login, enterIrisApp, etc.
10
+ */
11
+ function setupDefaultCommands() {
12
+ Cypress.Commands.add("getByTestId", {
13
+ prevSubject: ["optional"],
14
+ },
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ (subject, testId, options = {}) => {
17
+ const selector = `[data-testid="${testId}"]`;
18
+ const timeout = options.timeout ?? 15000;
19
+ if (subject) {
20
+ if (Cypress.dom.isElement(subject) || Cypress.dom.isJquery(subject)) {
21
+ return cy.wrap(subject, { timeout }).find(selector, { timeout });
22
+ }
23
+ else if (Cypress.dom.isWindow(subject)) {
24
+ return cy.get(selector, { timeout });
25
+ }
26
+ else if (Array.isArray(subject)) {
27
+ const element = subject.map(el => cy.wrap(el, { timeout }).find(selector, { timeout }));
28
+ if (element[0]) {
29
+ return element[0];
30
+ }
31
+ return cy.wrap(null);
32
+ }
33
+ return cy.get(selector, { timeout });
34
+ }
35
+ else {
36
+ return cy.get(selector, { timeout });
37
+ }
38
+ });
39
+ Cypress.Commands.add("login", fixture => {
40
+ const envUrl = `${Cypress.config().baseUrl}/env`;
41
+ cy.log(`Getting env from: ${envUrl}`);
42
+ cy.request({
43
+ method: "GET",
44
+ url: envUrl,
45
+ headers: {
46
+ "Content-Type": "application/json",
47
+ },
48
+ }).then(envResponse => {
49
+ const env = envResponse.body;
50
+ const domain = env.auth?.url;
51
+ if (!domain) {
52
+ throw new Error(`No domain found from servers /env found env: ${JSON.stringify(env)}`);
53
+ }
54
+ cy.log(`Using: ${domain}`);
55
+ cy.clearCookies();
56
+ cy.fixture(fixture ?? "auth").then(({ username, password }) => {
57
+ const options = {
58
+ warnBeforePasswordExpired: true,
59
+ multiOptionalFactorEnroll: false,
60
+ };
61
+ cy.request({
62
+ method: "POST",
63
+ url: `${domain}/api/v1/authn`,
64
+ headers: {
65
+ "Content-Type": "application/json",
66
+ },
67
+ body: {
68
+ username,
69
+ password,
70
+ options,
71
+ },
72
+ })
73
+ .then(response => {
74
+ if (response.isOkStatusCode) {
75
+ const sessionToken = response.body.sessionToken;
76
+ return cy.visit(`/auth/manager-classic#session_token=${sessionToken}&fleetHome=true`);
77
+ }
78
+ else {
79
+ throw new Error(`Could not get a session token for user: ${username}, ${JSON.stringify(response)}`);
80
+ }
81
+ })
82
+ .url()
83
+ .should("contain", `${Cypress.config().baseUrl}`)
84
+ .should("contain", `/map`)
85
+ .getByTestId("map-page", { timeout: 30000 })
86
+ .should("be.visible");
87
+ });
88
+ });
89
+ });
90
+ Cypress.Commands.add("switchToLocalDevMode", () => {
91
+ cy.getByTestId("developerPortalNav").click();
92
+ cy.url({ timeout: 15000, log: true }).should("contain", "/iris-sdk-portal");
93
+ //eslint-disable-next-line @typescript-eslint/no-explicit-any
94
+ cy.getByTestId("localDevModeSwitch-input").then(($ele) => {
95
+ if ($ele && !$ele.is(":checked")) {
96
+ cy.getByTestId("localDevModeSwitch-thumb").click();
97
+ }
98
+ });
99
+ });
100
+ Cypress.Commands.add("enterIrisApp", options => {
101
+ return cy
102
+ .get(`iframe[data-testid="${options?.testId ?? "app-iframe"}"]`, { timeout: 30000 })
103
+ .first()
104
+ .its("0.contentDocument.body", { timeout: 30000, log: true })
105
+ .should("not.be.empty")
106
+ .then($body => {
107
+ return () => cy.wrap($body);
108
+ });
109
+ });
110
+ Cypress.Commands.add("enterStorybookPreview", options => {
111
+ return cy
112
+ .get(`iframe[id="${options?.testId ?? "storybook-preview-iframe"}"]`, { timeout: 30000 })
113
+ .first()
114
+ .its("0.contentDocument.body", { timeout: 30000, log: true })
115
+ .should("not.be.empty")
116
+ .then($body => {
117
+ return () => cy.wrap($body);
118
+ });
119
+ });
120
+ Cypress.Commands.add("getValidateFeatureFlags", () => {
121
+ cy.intercept({ url: "**/ValidateFeatureFlags" }).as("ValidateFeatureFlags");
122
+ cy.intercept({ url: "**/UserPermissions" }).as("UserPermissions");
123
+ cy.intercept({ url: "**/ActiveSubscription" }).as("ActiveSubscription");
124
+ });
125
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
+ let ccData = null;
127
+ Cypress.Commands.add("configCat", value => {
128
+ if (!ccData) {
129
+ cy.wait("@ValidateFeatureFlags").then(intercept => {
130
+ ccData = intercept.response?.body?.data?.featureFlags?.find((ff) => ff.key === value);
131
+ return ccData?.state;
132
+ });
133
+ }
134
+ else {
135
+ return ccData?.state;
136
+ }
137
+ });
138
+ }
139
+
140
+ /* eslint-disable no-console */
141
+ /**
142
+ * Writes a file with Prettier formatting applied.
143
+ * Automatically detects parser based on file extension.
144
+ */
145
+ const writeFileWithPrettier = async (nxRoot, filePath, content, writeOptions = { encoding: "utf-8" }, writer) => {
146
+ const prettierConfigPath = path.join(nxRoot, ".prettierrc");
147
+ const options = await writer
148
+ .resolveConfig(prettierConfigPath)
149
+ .catch(error => console.log("Prettier config error: ", error));
150
+ if (!options) {
151
+ throw new Error("Could not find prettier config");
152
+ }
153
+ if (filePath.endsWith("json")) {
154
+ options.parser = "json";
155
+ }
156
+ else {
157
+ options.parser = "typescript";
158
+ }
159
+ try {
160
+ const prettySrc = await writer.format(content, options);
161
+ writeFileSync(filePath, prettySrc, writeOptions);
162
+ }
163
+ catch (error) {
164
+ console.error("Error in prettier.format:", error);
165
+ }
166
+ };
167
+
168
+ const isNetworkCall = (log) => {
169
+ return log.type === "cy:fetch" || log.type === "cy:request" || log.type === "cy:response" || log.type === "cy:xrh";
170
+ };
171
+ /**
172
+ * Creates log files for Cypress test runs.
173
+ * Generates separate files for all logs, errors, and network errors.
174
+ */
175
+ function createLogFile(nxRoot, logsPath, fileNameWithoutExtension, logs, logWriter) {
176
+ if (!existsSync(logsPath)) {
177
+ fs.mkdirSync(logsPath, { recursive: true });
178
+ }
179
+ const logFilePath = path__default.join(logsPath, fileNameWithoutExtension);
180
+ writeFileWithPrettier(nxRoot, logFilePath + "-all.json", JSON.stringify(logs), { flag: "a" }, logWriter);
181
+ const errorCmds = logs.filter(log => log.severity === "error" &&
182
+ // This seems to be when apollo has cancelled requests it marks it as failed to fetch only on cypress ?!??
183
+ // might be fixed by https://github.com/Trackunit/manager/pull/12917
184
+ !log.message.includes("TypeError: Failed to fetch"));
185
+ if (errorCmds.length > 0) {
186
+ writeFileWithPrettier(nxRoot, logFilePath + "-errors.json", JSON.stringify(errorCmds), {
187
+ flag: "a",
188
+ }, logWriter);
189
+ }
190
+ const networkErrorsCmds = logs.filter(log => isNetworkCall(log) &&
191
+ !log.message.includes("Status: 200") &&
192
+ log.severity !== "success" &&
193
+ !log.message.includes("sentry.io/api/") &&
194
+ // This seems to be when apollo has cancelled requests it marks it as failed to fetch only on cypress ?!??
195
+ !log.message.includes("TypeError: Failed to fetch"));
196
+ if (networkErrorsCmds.length > 0) {
197
+ writeFileWithPrettier(nxRoot, logFilePath + "-network-errors.json", JSON.stringify(networkErrorsCmds), {
198
+ flag: "a",
199
+ }, logWriter);
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Utility function to find NX workspace root by looking for nx.json or workspace.json.
205
+ * This is more reliable than hardcoded relative paths and works from any directory.
206
+ *
207
+ * @param startDir Starting directory for the search (defaults to current working directory)
208
+ * @returns {string} Absolute path to the workspace root
209
+ * @throws Error if workspace root cannot be found
210
+ */
211
+ function findWorkspaceRoot(startDir = process.cwd()) {
212
+ let currentDir = startDir;
213
+ while (currentDir !== path__default.dirname(currentDir)) {
214
+ if (existsSync(path__default.join(currentDir, "nx.json")) || existsSync(path__default.join(currentDir, "workspace.json"))) {
215
+ return currentDir;
216
+ }
217
+ currentDir = path__default.dirname(currentDir);
218
+ }
219
+ throw new Error("Could not find NX workspace root (nx.json or workspace.json not found)");
220
+ }
221
+ /**
222
+ * Creates default Cypress configuration for E2E testing.
223
+ * Supports both legacy string parameter (dirname) and new options object for backward compatibility.
224
+ */
225
+ const defaultCypressConfig = optionsOrDirname => {
226
+ // Support both old API (string/undefined) and new API (object) for backward compatibility
227
+ const options = typeof optionsOrDirname === "object" ? optionsOrDirname : {};
228
+ const { nxRoot: providedNxRoot, outputDirOverride, projectConfig = {}, behaviorConfig = {}, pluginConfig = {}, } = options;
229
+ // Use NX workspace detection for reliable root finding
230
+ const nxRoot = providedNxRoot ?? findWorkspaceRoot();
231
+ // For output path calculation, determine the relative path from caller to workspace root
232
+ const callerDirname = typeof optionsOrDirname === "string" ? optionsOrDirname : process.cwd();
233
+ const relativePath = path__default.relative(nxRoot, callerDirname);
234
+ const dotsToNxRoot = relativePath
235
+ .split(path__default.sep)
236
+ .map(_ => "..")
237
+ .join("/");
238
+ const envOutputDirOverride = process.env.NX_E2E_OUTPUT_DIR || outputDirOverride;
239
+ // Function to build output paths that respects the override
240
+ const buildOutputPath = (subPath) => {
241
+ if (envOutputDirOverride) {
242
+ return path__default.join(dotsToNxRoot, envOutputDirOverride, subPath);
243
+ }
244
+ return `${dotsToNxRoot}/dist/cypress/${relativePath}/${subPath}`;
245
+ };
246
+ return {
247
+ projectId: projectConfig.projectId ?? process.env.CYPRESS_PROJECT_ID,
248
+ defaultCommandTimeout: behaviorConfig.defaultCommandTimeout ?? 20000,
249
+ execTimeout: behaviorConfig.execTimeout ?? 300000,
250
+ taskTimeout: behaviorConfig.taskTimeout ?? 35000,
251
+ pageLoadTimeout: behaviorConfig.pageLoadTimeout ?? 35000,
252
+ // setting to undefined makes no effect on the baseUrl so child projects can override it
253
+ baseUrl: process.env.NX_FEATURE_BRANCH_BASE_URL ?? undefined,
254
+ requestTimeout: behaviorConfig.requestTimeout ?? 25000,
255
+ responseTimeout: behaviorConfig.responseTimeout ?? 150000,
256
+ retries: behaviorConfig.retries ?? {
257
+ runMode: 3,
258
+ openMode: 0,
259
+ },
260
+ fixturesFolder: projectConfig.fixturesFolder ?? "./src/fixtures",
261
+ downloadsFolder: pluginConfig.outputPath ?? buildOutputPath("downloads"),
262
+ logsFolder: pluginConfig.logsFolder ?? buildOutputPath("logs"),
263
+ chromeWebSecurity: false,
264
+ reporter: "junit",
265
+ reporterOptions: {
266
+ mochaFile: buildOutputPath("results-[hash].xml"),
267
+ },
268
+ specPattern: projectConfig.specPattern ?? "src/e2e/**/*.e2e.{js,jsx,ts,tsx}",
269
+ supportFile: projectConfig.supportFile ?? "src/support/e2e.ts",
270
+ nxRoot,
271
+ fileServerFolder: ".",
272
+ video: true,
273
+ videosFolder: pluginConfig.videosFolder ?? buildOutputPath("videos"),
274
+ screenshotsFolder: pluginConfig.screenshotsFolder ?? buildOutputPath("screenshots"),
275
+ env: {
276
+ hars_folders: pluginConfig.harFolder ?? buildOutputPath("hars"),
277
+ CYPRESS_RUN_UNIQUE_ID: crypto.randomUUID(),
278
+ },
279
+ };
280
+ };
281
+ /**
282
+ * Sets up Cypress plugins for logging, tasks, and HAR generation.
283
+ * Configures terminal reporting, XLSX parsing, and HTTP archive recording.
284
+ */
285
+ const setupPlugins = (on, config, logWriter, installHarGenerator) => {
286
+ /* ---- BEGIN: Logging setup ---- */
287
+ // Read options https://github.com/archfz/cypress-terminal-report
288
+ const options = {
289
+ printLogsToFile: "always", // Ensures logs are always printed to a file
290
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
291
+ collectTestLogs: (context, logs) => {
292
+ const testName = context.state.toUpperCase() +
293
+ "_" +
294
+ (config.currentRetry ? `( attempt ${config.currentRetry})_` : "") +
295
+ context.test;
296
+ createLogFile(config.nxRoot, config.logsFolder, testName, logs, logWriter);
297
+ },
298
+ };
299
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
300
+ require("cypress-terminal-report/src/installLogsPrinter")(on, options);
301
+ /* ---- END: Logging setup ---- */
302
+ /* ---- BEGIN: Task setup ---- */
303
+ on("task", {
304
+ parseXlsx(filePath) {
305
+ return new Promise((resolve, reject) => {
306
+ try {
307
+ const jsonData = parse(readFileSync(filePath));
308
+ resolve(jsonData);
309
+ }
310
+ catch (e) {
311
+ reject(e);
312
+ }
313
+ });
314
+ },
315
+ fileExists(filename) {
316
+ return existsSync(filename);
317
+ },
318
+ });
319
+ /* ---- END: Task setup ---- */
320
+ /* ---- BEGIN: HAR setup ---- */
321
+ // Installing the HAR geneartor should happen last according to the documentation
322
+ // https://github.com/NeuraLegion/cypress-har-generator?tab=readme-ov-file#setting-up-the-plugin
323
+ installHarGenerator(on);
324
+ /* ---- END: HAR setup ---- */
325
+ };
326
+
327
+ /* eslint-disable local-rules/no-typescript-assertion, @typescript-eslint/no-explicit-any */
328
+ /**
329
+ * Sets up HAR (HTTP Archive) recording for E2E tests.
330
+ * Records network activity and saves HAR files for failed tests.
331
+ */
332
+ function setupHarRecording() {
333
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
334
+ require("@neuralegion/cypress-har-generator/commands");
335
+ beforeEach(() => {
336
+ const harDir = Cypress.env("hars_folders");
337
+ cy.recordHar({ rootDir: harDir });
338
+ });
339
+ afterEach(function () {
340
+ if (this.currentTest.state === "failed") {
341
+ const harDir = Cypress.env("hars_folders");
342
+ const testName = "FAILED_" + Cypress.currentTest.title + (Cypress.currentRetry ? `_( attempt ${Cypress.currentRetry})_` : "");
343
+ cy.saveHar({ outDir: harDir, fileName: testName });
344
+ }
345
+ });
346
+ }
347
+
348
+ /**
349
+ * Sets up the E2E testing environment with HAR recording and terminal logging.
350
+ */
351
+ function setupE2E() {
352
+ setupHarRecording();
353
+ // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
354
+ require("cypress-terminal-report/src/installLogsCollector")({
355
+ xhr: {
356
+ printHeaderData: true,
357
+ printRequestData: true,
358
+ },
359
+ });
360
+ Cypress.on("uncaught:exception", (err) => {
361
+ // eslint-disable-next-line no-console
362
+ console.log("Caught Error: ", err);
363
+ // returning false here prevents Cypress from
364
+ // failing the test
365
+ return false;
366
+ });
367
+ }
368
+
369
+ export { createLogFile, defaultCypressConfig, setupDefaultCommands, setupE2E, setupHarRecording, setupPlugins, writeFileWithPrettier };
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@trackunit/iris-app-e2e",
3
+ "version": "0.0.2-alpha-49e9177e1b3.0",
4
+ "repository": "https://github.com/Trackunit/manager",
5
+ "license": "SEE LICENSE IN LICENSE.txt",
6
+ "engines": {
7
+ "node": ">=22.x"
8
+ },
9
+ "dependencies": {
10
+ "@neuralegion/cypress-har-generator": "^5.17.0",
11
+ "cypress-terminal-report": "7.0.3",
12
+ "node-xlsx": "^0.23.0",
13
+ "prettier": "^3.4.2",
14
+ "@trackunit/react-test-setup": "1.0.33-alpha-49e9177e1b3.0"
15
+ },
16
+ "module": "./index.esm.js",
17
+ "main": "./index.cjs.js",
18
+ "types": "./index.esm.d.ts"
19
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sets up default Cypress commands for E2E testing.
3
+ * Adds custom commands like getByTestId, login, enterIrisApp, etc.
4
+ */
5
+ export declare function setupDefaultCommands(): void;
@@ -0,0 +1 @@
1
+ export * from "./defaultCommands";
package/src/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./commands";
2
+ export * from "./plugins";
3
+ export * from "./setup";
@@ -0,0 +1,12 @@
1
+ import { Formatter } from "./writeFileWithPrettier";
2
+ type Log = {
3
+ type: string;
4
+ message: string;
5
+ severity: string;
6
+ };
7
+ /**
8
+ * Creates log files for Cypress test runs.
9
+ * Generates separate files for all logs, errors, and network errors.
10
+ */
11
+ export declare function createLogFile(nxRoot: string, logsPath: string, fileNameWithoutExtension: string, logs: Array<Log>, logWriter: Formatter): void;
12
+ export {};
@@ -0,0 +1,48 @@
1
+ import { Formatter } from "./writeFileWithPrettier";
2
+ export interface CypressPluginConfig extends Cypress.PluginConfigOptions {
3
+ currentRetry: number;
4
+ nxRoot: string;
5
+ logsFolder: string;
6
+ }
7
+ export interface E2EPluginConfig {
8
+ outputPath?: string;
9
+ logsFolder?: string;
10
+ videosFolder?: string;
11
+ screenshotsFolder?: string;
12
+ harFolder?: string;
13
+ }
14
+ export interface E2EProjectConfig {
15
+ projectId?: string;
16
+ specPattern?: string;
17
+ fixturesFolder?: string;
18
+ supportFile?: string;
19
+ }
20
+ export interface E2EBehaviorConfig {
21
+ defaultCommandTimeout?: number;
22
+ retries?: {
23
+ runMode: number;
24
+ openMode: number;
25
+ };
26
+ requestTimeout?: number;
27
+ responseTimeout?: number;
28
+ execTimeout?: number;
29
+ taskTimeout?: number;
30
+ pageLoadTimeout?: number;
31
+ }
32
+ export interface E2EConfigOptions {
33
+ nxRoot?: string;
34
+ outputDirOverride?: string;
35
+ projectConfig?: E2EProjectConfig;
36
+ behaviorConfig?: E2EBehaviorConfig;
37
+ pluginConfig?: E2EPluginConfig;
38
+ }
39
+ /**
40
+ * Creates default Cypress configuration for E2E testing.
41
+ * Supports both legacy string parameter (dirname) and new options object for backward compatibility.
42
+ */
43
+ export declare const defaultCypressConfig: (optionsOrDirname?: E2EConfigOptions | string) => Partial<CypressPluginConfig>;
44
+ /**
45
+ * Sets up Cypress plugins for logging, tasks, and HAR generation.
46
+ * Configures terminal reporting, XLSX parsing, and HTTP archive recording.
47
+ */
48
+ export declare const setupPlugins: (on: Cypress.PluginEvents, config: CypressPluginConfig, logWriter: Formatter, installHarGenerator: (on: Cypress.PluginEvents) => void) => void;
@@ -0,0 +1,3 @@
1
+ export * from "./createLogFile";
2
+ export * from "./defaultPlugins";
3
+ export * from "./writeFileWithPrettier";
@@ -0,0 +1,11 @@
1
+ import { WriteFileOptions } from "fs";
2
+ import { format, resolveConfig } from "prettier";
3
+ export type Formatter = {
4
+ resolveConfig: typeof resolveConfig;
5
+ format: typeof format;
6
+ };
7
+ /**
8
+ * Writes a file with Prettier formatting applied.
9
+ * Automatically detects parser based on file extension.
10
+ */
11
+ export declare const writeFileWithPrettier: (nxRoot: string, filePath: string, content: string, writeOptions: WriteFileOptions | undefined, writer: Formatter) => Promise<void>;
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Sets up the E2E testing environment with HAR recording and terminal logging.
3
+ */
4
+ export declare function setupE2E(): void;
@@ -0,0 +1,2 @@
1
+ export * from "./defaultE2ESetup";
2
+ export * from "./setupHarRecording";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Sets up HAR (HTTP Archive) recording for E2E tests.
3
+ * Records network activity and saves HAR files for failed tests.
4
+ */
5
+ export declare function setupHarRecording(): void;
File without changes