@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 +227 -0
- package/index.cjs.d.ts +1 -0
- package/index.cjs.js +395 -0
- package/index.esm.d.ts +1 -0
- package/index.esm.js +369 -0
- package/package.json +19 -0
- package/src/commands/defaultCommands.d.ts +5 -0
- package/src/commands/index.d.ts +1 -0
- package/src/index.d.ts +3 -0
- package/src/plugins/createLogFile.d.ts +12 -0
- package/src/plugins/defaultPlugins.d.ts +48 -0
- package/src/plugins/index.d.ts +3 -0
- package/src/plugins/writeFileWithPrettier.d.ts +11 -0
- package/src/setup/defaultE2ESetup.d.ts +4 -0
- package/src/setup/index.d.ts +2 -0
- package/src/setup/setupHarRecording.d.ts +5 -0
- package/src/types/index.d.ts +0 -0
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
|
+

|
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 @@
|
|
|
1
|
+
export * from "./defaultCommands";
|
package/src/index.d.ts
ADDED
|
@@ -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,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>;
|
|
File without changes
|