dceky 1.2.1-beta-setup-node-events.1 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +23 -17
  2. package/lib/start/constants/DEFAULT_THREADS_PER_COMBO.d.ts +1 -1
  3. package/lib/start/constants/DEFAULT_THREADS_PER_COMBO.js +1 -1
  4. package/lib/start/helpers/embedScreenshotsInReportData.d.ts +14 -0
  5. package/lib/start/helpers/embedScreenshotsInReportData.js +139 -0
  6. package/lib/start/helpers/embedScreenshotsInReportData.js.map +1 -0
  7. package/lib/start/helpers/executeCypress.js +1 -1
  8. package/lib/start/helpers/executeCypress.js.map +1 -1
  9. package/lib/start/helpers/generateHtmlReport.js +7 -0
  10. package/lib/start/helpers/generateHtmlReport.js.map +1 -1
  11. package/lib/start/helpers/getCypressDetectedBrowsersForChooser.d.ts +2 -1
  12. package/lib/start/helpers/getCypressDetectedBrowsersForChooser.js +34 -22
  13. package/lib/start/helpers/getCypressDetectedBrowsersForChooser.js.map +1 -1
  14. package/lib/start/helpers/mergeAllReportsAndGenerateHtml.js +52 -0
  15. package/lib/start/helpers/mergeAllReportsAndGenerateHtml.js.map +1 -1
  16. package/lib/start/helpers/reportHomepage.ejs +18 -7
  17. package/lib/start/index.d.ts +2 -2
  18. package/lib/start/index.js +214 -175
  19. package/lib/start/index.js.map +1 -1
  20. package/package.json +2 -1
  21. package/src/genConfiguration/index.ts +0 -13
  22. package/start/constants/DEFAULT_THREADS_PER_COMBO.ts +1 -1
  23. package/start/helpers/embedScreenshotsInReportData.ts +171 -0
  24. package/start/helpers/executeCypress.ts +1 -1
  25. package/start/helpers/generateHtmlReport.ts +12 -0
  26. package/start/helpers/getCypressDetectedBrowsersForChooser.ts +41 -23
  27. package/start/helpers/mergeAllReportsAndGenerateHtml.ts +60 -0
  28. package/start/helpers/reportHomepage.ejs +18 -7
  29. package/start/index.ts +218 -181
  30. package/start/helpers/extractArgValue.ts +0 -42
package/start/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable no-console */
2
2
  /**
3
- * Checks arguments and environment variables (for PROFILE, BROWSER, and HEADLESS),
4
- * asks user for missing selections, and launches Cypress
3
+ * Checks environment variables (for PROFILE, BROWSER, and HEADLESS),
4
+ * asks the user for missing selections, and launches Cypress
5
5
  * @author Gardenia Liu
6
6
  * @author Yuen Ler Chow
7
7
  */
@@ -9,14 +9,17 @@
9
9
  // Import libs
10
10
  import fs from 'fs';
11
11
  import path from 'path';
12
+ import dotenv from 'dotenv';
13
+ import clear from 'clear';
12
14
 
13
15
  // Import helpers
14
16
  import showChooser from './helpers/showChooser';
15
17
  import parseCommaSeparated from './helpers/parseCommaSeparated';
16
18
  import findProfilesByNames from './helpers/findProfilesByNames';
17
19
  import validateBrowsers from './helpers/validateBrowsers';
18
- import extractArgValue from './helpers/extractArgValue';
19
20
  import getRootPath from './helpers/getRootPath';
21
+ import prompt from './helpers/prompt';
22
+ import print from './helpers/print';
20
23
  import executeCypress from './helpers/executeCypress';
21
24
  import getCypressDetectedBrowsersForChooser from './helpers/getCypressDetectedBrowsersForChooser';
22
25
  import getE2ETestFoldersForChooser from './helpers/getE2ETestFoldersForChooser';
@@ -30,6 +33,9 @@ import ALL_E2E_TESTS_SPEC_PATTERN from './constants/ALL_E2E_TESTS_SPEC_PATTERN';
30
33
  // Get the project directory
31
34
  const pwd = getRootPath();
32
35
 
36
+ // Load .env vars before reading startup configuration
37
+ dotenv.config({ path: path.join(pwd, '.env') });
38
+
33
39
  // Find available profiles under /cypress/profiles
34
40
  const profilesDir = path.join(pwd, 'cypress/profiles');
35
41
  if (!fs.existsSync(profilesDir)) {
@@ -58,232 +64,263 @@ if (profiles.length === 0) {
58
64
  process.exit(1);
59
65
  }
60
66
 
61
- // Parse CLI arguments
62
- const args = process.argv.slice(2);
63
-
64
- // Get selections from environment variables or CLI arguments
67
+ // Get selections from environment variables
65
68
  let selectedProfiles: { file: string; profileName: string }[] = [];
66
69
  let selectedBrowsers: string[] = [];
67
70
  let isHeadless = false;
68
71
  let selectedE2ETestFolder = ALL_E2E_TESTS_LABEL;
69
72
  let selectedE2ESpecPattern = ALL_E2E_TESTS_SPEC_PATTERN;
70
73
 
71
- // Check for PROFILE (env var or CLI arg)
72
- const envProfileRaw = process.env.PROFILE;
73
- const argProfileRaw = extractArgValue(args, '--profile');
74
- const profileRaw = envProfileRaw || argProfileRaw;
74
+ // Check for PROFILE
75
+ const profileRaw = process.env.PROFILE;
75
76
 
76
77
  if (profileRaw) {
77
78
  const profileNames = parseCommaSeparated(profileRaw);
78
79
  selectedProfiles = findProfilesByNames(profileNames, profiles);
79
80
  }
80
81
 
81
- // Check for BROWSER (env var or CLI arg)
82
- const envBrowserRaw = process.env.BROWSER;
83
- const argBrowserRaw = extractArgValue(args, '--browser');
84
- const browserRaw = envBrowserRaw || argBrowserRaw;
82
+ // Check for BROWSER
83
+ const browserRaw = process.env.BROWSER;
85
84
 
86
85
  if (browserRaw) {
87
86
  const browserNames = parseCommaSeparated(browserRaw);
88
87
  selectedBrowsers = validateBrowsers(browserNames);
89
88
  }
90
89
 
91
- // Check for E2E_TEST_FOLDER (env var or CLI arg)
92
- const envE2ETestFolderRaw = process.env.E2E_TEST_FOLDER;
93
- const argE2ETestFolderRaw = extractArgValue(args, '--e2eFolder');
94
- const e2eTestFolderRaw = envE2ETestFolderRaw || argE2ETestFolderRaw;
90
+ // Check for E2E_TEST_FOLDER
91
+ const e2eTestFolderRaw = process.env.E2E_TEST_FOLDER;
95
92
 
96
- // Check for HEADLESS (env var or CLI flag)
93
+ // Check for HEADLESS
97
94
  const envHeadlessRaw = process.env.HEADLESS;
98
- const headlessFlagValue = extractArgValue(args, '--headless');
99
- const hasHeadlessFlag = headlessFlagValue !== undefined;
100
- isHeadless = !!(envHeadlessRaw && envHeadlessRaw.toLowerCase() !== 'false') || hasHeadlessFlag;
101
-
102
- // Check for THREADS_PER_COMBO (env var or CLI arg)
103
- const envThreadsRaw = process.env.THREADS_PER_COMBO;
104
- const argThreadsRaw = extractArgValue(args, '--threadsPerCombo');
105
- const threadsRaw = envThreadsRaw || argThreadsRaw;
95
+ isHeadless = !!(envHeadlessRaw && envHeadlessRaw.toLowerCase() !== 'false');
96
+
97
+ // Check for THREADS_PER_COMBO
98
+ const threadsRaw = process.env.THREADS_PER_COMBO;
106
99
  let threadsPerCombo = DEFAULT_THREADS_PER_COMBO;
107
- if (threadsRaw) {
108
- const parsed = Number.parseInt(threadsRaw, 10);
109
- if (!Number.isNaN(parsed) && parsed > 0) {
110
- threadsPerCombo = parsed;
111
- } else {
112
- console.warn(`⚠️ Invalid THREADS_PER_COMBO value: "${threadsRaw}". Using default (${DEFAULT_THREADS_PER_COMBO}).`);
113
- }
114
- }
115
100
 
101
+ const start = async (): Promise<void> => {
116
102
  // If headless wasn't specified, ask the user to choose
117
- if (!envHeadlessRaw && !hasHeadlessFlag) {
118
- const choices = showChooser({
119
- question: 'Run in headless mode?',
120
- options: [
121
- { tag: 'Y', description: 'Yes (headless)' },
122
- { tag: 'N', description: 'No (interactive)' },
123
- ],
124
- title: 'Select mode',
125
- allowMulti: false,
126
- });
127
-
128
- isHeadless = choices[0].tag === 'Y';
129
- }
130
-
131
- // If running headless, optionally narrow tests to one e2e folder
132
- if (isHeadless) {
133
- const choosableE2ETestFolders = getE2ETestFoldersForChooser();
134
-
135
- if (e2eTestFolderRaw) {
136
- const e2eSelection = findE2ETestFolderByName(e2eTestFolderRaw, choosableE2ETestFolders);
137
- selectedE2ETestFolder = e2eSelection.description;
138
- selectedE2ESpecPattern = e2eSelection.specPattern;
139
- } else if (choosableE2ETestFolders.length > 0) {
103
+ if (!envHeadlessRaw) {
140
104
  const choices = showChooser({
141
- question: 'Choose e2e test folder:',
105
+ question: 'Run in headless mode?',
142
106
  options: [
143
- {
144
- tag: 'A',
145
- description: ALL_E2E_TESTS_LABEL,
146
- },
147
- ...choosableE2ETestFolders,
107
+ { tag: 'Y', description: 'Yes (headless)' },
108
+ { tag: 'N', description: 'No (interactive)' },
148
109
  ],
149
- title: 'Select E2E Tests',
110
+ title: 'Select mode',
150
111
  allowMulti: false,
151
112
  });
152
113
 
153
- const allTestsChosen = choices.some((choice) => {
154
- return choice.tag === 'A';
155
- });
156
- if (!allTestsChosen) {
157
- const selectedFolder = choosableE2ETestFolders.find((folder) => {
158
- return folder.tag === choices[0].tag;
159
- });
160
- if (selectedFolder) {
161
- selectedE2ETestFolder = selectedFolder.relativePath;
162
- selectedE2ESpecPattern = selectedFolder.specPattern;
114
+ isHeadless = choices[0].tag === 'Y';
115
+ }
116
+
117
+ // This takes 5+ seconds to run, so start it now while we wait for the user to answer other questions
118
+ const cypressDetectedBrowsersPromise = (
119
+ isHeadless && selectedBrowsers.length === 0
120
+ ? getCypressDetectedBrowsersForChooser()
121
+ : null
122
+ );
123
+
124
+ if (isHeadless) {
125
+ if (threadsRaw) {
126
+ const trimmedThreads = threadsRaw.trim();
127
+ const parsed = Number.parseInt(trimmedThreads, 10);
128
+ if (/^\d+$/.test(trimmedThreads) && parsed > 0) {
129
+ threadsPerCombo = parsed;
130
+ } else {
131
+ console.warn(
132
+ `⚠️ Invalid THREADS_PER_COMBO value: "${threadsRaw}". `
133
+ + 'Use a positive whole number. '
134
+ + `Using default (${DEFAULT_THREADS_PER_COMBO}).`,
135
+ );
136
+ }
137
+ } else {
138
+ let parsedThreads: number | null = null;
139
+ clear();
140
+ print.title('Num Test Files in Parallel Per Combo');
141
+ console.log('');
142
+ print.subtitle('How many test files should run in parallel per profile/browser combo? (Press Enter for 1)');
143
+ while (parsedThreads === null) {
144
+ const response = prompt('> ', true).trim();
145
+ if (!response) {
146
+ parsedThreads = DEFAULT_THREADS_PER_COMBO;
147
+ } else {
148
+ const parsed = Number.parseInt(response, 10);
149
+ parsedThreads = /^\d+$/.test(response) && parsed > 0 ? parsed : null;
150
+ }
151
+
152
+ if (parsedThreads === null) {
153
+ console.log('Please enter a positive whole number, or press Enter for 1.');
154
+ console.log('');
155
+ }
163
156
  }
157
+
158
+ threadsPerCombo = parsedThreads;
164
159
  }
165
160
  }
166
- }
167
161
 
168
- // If running headless and no browsers were selected, ask the user to choose
169
- if (isHeadless && selectedBrowsers.length === 0) {
170
- const choosableBrowsers = getCypressDetectedBrowsersForChooser();
171
- if (choosableBrowsers.length === 0) {
172
- console.error('No browsers detected. Run `npx cypress info`, install a supported browser, and try again.');
173
- process.exit(1);
162
+ // If running headless, optionally narrow tests to one e2e folder
163
+ if (isHeadless) {
164
+ const choosableE2ETestFolders = getE2ETestFoldersForChooser();
165
+
166
+ if (e2eTestFolderRaw) {
167
+ const e2eSelection = findE2ETestFolderByName(e2eTestFolderRaw, choosableE2ETestFolders);
168
+ selectedE2ETestFolder = e2eSelection.description;
169
+ selectedE2ESpecPattern = e2eSelection.specPattern;
170
+ } else if (choosableE2ETestFolders.length > 0) {
171
+ const choices = showChooser({
172
+ question: 'Choose e2e test folder:',
173
+ options: [
174
+ {
175
+ tag: 'A',
176
+ description: ALL_E2E_TESTS_LABEL,
177
+ },
178
+ ...choosableE2ETestFolders,
179
+ ],
180
+ title: 'Select E2E Tests',
181
+ allowMulti: false,
182
+ });
183
+
184
+ const allTestsChosen = choices.some((choice) => {
185
+ return choice.tag === 'A';
186
+ });
187
+ if (!allTestsChosen) {
188
+ const selectedFolder = choosableE2ETestFolders.find((folder) => {
189
+ return folder.tag === choices[0].tag;
190
+ });
191
+ if (selectedFolder) {
192
+ selectedE2ETestFolder = selectedFolder.relativePath;
193
+ selectedE2ESpecPattern = selectedFolder.specPattern;
194
+ }
195
+ }
196
+ }
174
197
  }
175
- const options = choosableBrowsers.map(({ tag, name }) => {
176
- return {
177
- tag,
178
- description: name,
179
- };
180
- });
181
198
 
182
- // Add "All" option if there are multiple browsers
183
- if (choosableBrowsers.length > 1) {
184
- options.unshift({
185
- tag: 'A',
186
- description: 'All browsers',
199
+ // If running headless and no browsers were selected, ask the user to choose
200
+ if (isHeadless && selectedBrowsers.length === 0) {
201
+ const choosableBrowsers = await cypressDetectedBrowsersPromise;
202
+ if (choosableBrowsers.length === 0) {
203
+ console.error('No browsers detected. Run `npx cypress info`, install a supported browser, and try again.');
204
+ process.exit(1);
205
+ }
206
+ const options = choosableBrowsers.map(({ tag, name }) => {
207
+ return {
208
+ tag,
209
+ description: name,
210
+ };
187
211
  });
188
- }
189
212
 
190
- const choices = showChooser({
191
- question: 'Choose browser(s) (commas for multiple: C,F)',
192
- options,
193
- title: 'Select Browser(s)',
194
- allowMulti: true,
195
- });
213
+ // Add "All" option if there are multiple browsers
214
+ if (choosableBrowsers.length > 1) {
215
+ options.unshift({
216
+ tag: 'A',
217
+ description: 'All browsers',
218
+ });
219
+ }
196
220
 
197
- const allBrowsersChosen = choices.some((choice) => {
198
- return choice.tag === 'A';
199
- });
200
- if (allBrowsersChosen) {
201
- selectedBrowsers = choosableBrowsers.map((b) => {
202
- return b.name;
221
+ const choices = showChooser({
222
+ question: 'Choose browser(s) (commas for multiple: C,F)',
223
+ options,
224
+ title: 'Select Browser(s)',
225
+ allowMulti: true,
226
+ });
227
+
228
+ const allBrowsersChosen = choices.some((choice) => {
229
+ return choice.tag === 'A';
203
230
  });
204
- } else {
205
- selectedBrowsers = (
206
- choices
207
- .map((choice) => {
208
- const selectedBrowser = choosableBrowsers.find((browser) => {
209
- return browser.tag.toLowerCase() === choice.tag.toLowerCase();
210
- });
211
- return selectedBrowser?.name ?? null;
212
- })
213
- .filter((name) => {
214
- return name !== null;
215
- })
216
- );
231
+ if (allBrowsersChosen) {
232
+ selectedBrowsers = choosableBrowsers.map((b) => {
233
+ return b.name;
234
+ });
235
+ } else {
236
+ selectedBrowsers = (
237
+ choices
238
+ .map((choice) => {
239
+ const selectedBrowser = choosableBrowsers.find((browser) => {
240
+ return browser.tag.toLowerCase() === choice.tag.toLowerCase();
241
+ });
242
+ return selectedBrowser?.name ?? null;
243
+ })
244
+ .filter((name) => {
245
+ return name !== null;
246
+ })
247
+ );
248
+ }
217
249
  }
218
- }
219
250
 
220
- // If no profiles were selected, ask the user to choose
221
- // - In headless mode, allow selecting multiple profiles
222
- // - In visible mode, only allow selecting a single profile
223
- // In the case where their environment selected more than one profile, but they are not headless, make them re-choose
224
- if (
225
- selectedProfiles.length === 0
251
+ // If no profiles were selected, ask the user to choose
252
+ // - In headless mode, allow selecting multiple profiles
253
+ // - In visible mode, only allow selecting a single profile
254
+ // In the case where their environment selected more than one profile, but they are not headless, make them re-choose
255
+ if (
256
+ selectedProfiles.length === 0
226
257
  || (!isHeadless && selectedProfiles.length > 1)
227
- ) {
258
+ ) {
228
259
  // Filter out "Default" profile from the chooser options
229
- const choosableProfiles = profiles.filter((p) => {
230
- return p.profileName.toLowerCase() !== 'default';
231
- });
260
+ const choosableProfiles = profiles.filter((p) => {
261
+ return p.profileName.toLowerCase() !== 'default';
262
+ });
232
263
 
233
- const options = choosableProfiles.map((p, idx) => {
234
- return {
235
- tag: String(idx + 1),
236
- description: p.profileName,
237
- };
238
- });
264
+ const options = choosableProfiles.map((p, idx) => {
265
+ return {
266
+ tag: String(idx + 1),
267
+ description: p.profileName,
268
+ };
269
+ });
239
270
 
240
- // If there are multiple profiles, add an "All" option for headless mode
241
- if (isHeadless && choosableProfiles.length > 1) {
242
- options.unshift({
243
- tag: 'A',
244
- description: 'All profiles',
271
+ // If there are multiple profiles, add an "All" option for headless mode
272
+ if (isHeadless && choosableProfiles.length > 1) {
273
+ options.unshift({
274
+ tag: 'A',
275
+ description: 'All profiles',
276
+ });
277
+ }
278
+
279
+ const choices = showChooser({
280
+ question: (
281
+ isHeadless
282
+ ? 'Choose profile(s) (commas for multiple: 1,2):'
283
+ : 'Choose profile:'
284
+ ),
285
+ options,
286
+ title: `Choose profile${isHeadless ? 's' : ''}`,
287
+ allowMulti: isHeadless,
245
288
  });
246
- }
247
289
 
248
- const choices = showChooser({
249
- question: (
250
- isHeadless
251
- ? 'Choose profile(s) (commas for multiple: 1,2):'
252
- : 'Choose profile:'
253
- ),
254
- options,
255
- title: `Choose profile${isHeadless ? 's' : ''}`,
256
- allowMulti: isHeadless,
257
- });
290
+ const allProfilesChosen = choices.some((choice) => {
291
+ return choice.tag === 'A';
292
+ });
293
+ if (allProfilesChosen) {
294
+ selectedProfiles = choosableProfiles;
295
+ } else {
296
+ selectedProfiles = (
297
+ choices
298
+ .map((choice) => {
299
+ const profileIndex = Number.parseInt(choice.tag, 10) - 1;
300
+ if (Number.isNaN(profileIndex)) {
301
+ return null;
302
+ }
303
+ return choosableProfiles[profileIndex] ?? null;
304
+ })
305
+ .filter((profile) => {
306
+ return profile !== null;
307
+ })
308
+ );
309
+ }
310
+ }
258
311
 
259
- const allProfilesChosen = choices.some((choice) => {
260
- return choice.tag === 'A';
312
+ // Execute Cypress with selected configuration
313
+ await executeCypress({
314
+ selectedProfiles,
315
+ selectedBrowsers,
316
+ isHeadless,
317
+ threadsPerCombo,
318
+ e2eTestFolder: selectedE2ETestFolder,
319
+ e2eSpecPattern: selectedE2ESpecPattern,
261
320
  });
262
- if (allProfilesChosen) {
263
- selectedProfiles = choosableProfiles;
264
- } else {
265
- selectedProfiles = (
266
- choices
267
- .map((choice) => {
268
- const profileIndex = Number.parseInt(choice.tag, 10) - 1;
269
- if (Number.isNaN(profileIndex)) {
270
- return null;
271
- }
272
- return choosableProfiles[profileIndex] ?? null;
273
- })
274
- .filter((profile) => {
275
- return profile !== null;
276
- })
277
- );
278
- }
279
- }
321
+ };
280
322
 
281
- // Execute Cypress with selected configuration
282
- executeCypress({
283
- selectedProfiles,
284
- selectedBrowsers,
285
- isHeadless,
286
- threadsPerCombo,
287
- e2eTestFolder: selectedE2ETestFolder,
288
- e2eSpecPattern: selectedE2ESpecPattern,
323
+ start().catch((error) => {
324
+ console.error(error);
325
+ process.exit(1);
289
326
  });
@@ -1,42 +0,0 @@
1
- /**
2
- * Extract CLI argument value or check if flag exists
3
- * @author Gardenia Liu
4
- * @author Yuen Ler Chow
5
- * @param args array of CLI arguments
6
- * @param argName the argument name to look for (e.g., '--profile' or '--headless')
7
- * @returns the argument value if found, empty string if flag exists without value, undefined otherwise
8
- */
9
- const extractArgValue = (args: string[], argName: string): string | undefined => {
10
- let matchedValue: string | undefined;
11
- args.some((rawArg, index) => {
12
- const normalizedArg = rawArg.toLowerCase();
13
-
14
- // Case 1: flag and value combined, e.g. "--profile=stage"
15
- if (normalizedArg.startsWith(`${argName}=`)) {
16
- const [, value] = rawArg.split('=');
17
- matchedValue = value;
18
- return true; // stop scanning
19
- }
20
-
21
- // Case 2: flag by itself, possibly followed by a separate value
22
- if (normalizedArg === argName) {
23
- const nextArg = args[index + 1];
24
-
25
- // If the next token exists and isn't another flag, treat it as the value
26
- if (nextArg && !nextArg.startsWith('--')) {
27
- matchedValue = nextArg;
28
- } else {
29
- // Flag exists but no value given (boolean-style flag) (e.g. "--headless")
30
- matchedValue = '';
31
- }
32
-
33
- return true; // stop scanning
34
- }
35
-
36
- return false;
37
- });
38
-
39
- return matchedValue;
40
- };
41
-
42
- export default extractArgValue;