dceky 1.0.0-beta-profile.1

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/.eslintrc.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "extends": "airbnb",
3
+ "env": {
4
+ "browser": true,
5
+ "node": true
6
+ },
7
+ "rules": {
8
+ "arrow-body-style": [
9
+ "warn",
10
+ "always"
11
+ ],
12
+ "arrow-parens": [ "warn", "always" ],
13
+ "comma-dangle": [ "error", {
14
+ "arrays": "always-multiline",
15
+ "objects": "always-multiline",
16
+ "imports": "always-multiline",
17
+ "exports": "always-multiline",
18
+ "functions": "never"
19
+ }
20
+ ],
21
+ "consistent-return": "off",
22
+ "id-length": "off",
23
+ "max-len": [ "error",
24
+ {
25
+ "code": 80,
26
+ "ignoreStrings": true,
27
+ "ignoreTemplateLiterals": true,
28
+ "ignoreUrls": true
29
+ }
30
+ ],
31
+ "newline-per-chained-call": [
32
+ "error",
33
+ {
34
+ "ignoreChainWithDepth": 2
35
+ }
36
+ ],
37
+ "no-plusplus": [ "warn", { "allowForLoopAfterthoughts": true }],
38
+ "prefer-template": "off",
39
+ "prefer-destructuring": [ "error",
40
+ {
41
+ "VariableDeclarator": {
42
+ "array": false,
43
+ "object": true
44
+ },
45
+ "AssignmentExpression": {
46
+ "array": false,
47
+ "object": true
48
+ }
49
+ },
50
+ {
51
+ "enforceForRenamedProperties": false
52
+ }
53
+ ],
54
+ "radix": "off",
55
+ "no-underscore-dangle": "off",
56
+ "max-params": [ "warn", { "max": 3 } ]
57
+ }
58
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # Changelog
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Harvard Edtech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # dceky
2
+
3
+ Cypress toolkit for Harvard DCE
@@ -0,0 +1,23 @@
1
+ # Globals and Profiles Configuration
2
+
3
+ ### Configuration Setup
4
+
5
+ The configuration setup involves the following steps:
6
+
7
+ 1. Split each globals/profiles map into two: `basic` values and `dependsOn` values.
8
+ 2. Merge all `basic` values into a running env object (in order so last ones processed are the highest priority).
9
+ 3. Go through each `dependsOn` value, look up the value it depends on in the running env object.
10
+ 4. Add `dependsOn` values into the running env object (in order so last ones processed are the highest priority).
11
+ 5. Wrap the env object into a config.
12
+
13
+ ### Assumptions
14
+
15
+ * `dependsOn` values must depend on a basic `value`, not on another `dependsOn` value.
16
+
17
+ ### DependsOn Value
18
+
19
+ The `dependsOn` value is an object that has the following structure:
20
+
21
+ * `dependsOn`: the key of the `basic` value that we are depending on
22
+ * `value`: the value that we are depending on (can be a string, number, boolean, etc.)
23
+ * `default`: the default value if the `dependsOn` value is not found
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Generate Cypress configuration based on global credentials,
3
+ * variables, resources, and profiles
4
+ * @author Yuen Ler Chow
5
+ * ...
6
+ */
7
+ declare const genConfiguration: () => {};
8
+ export default genConfiguration;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Generate Cypress configuration based on global credentials,
5
+ * variables, resources, and profiles
6
+ * @author Yuen Ler Chow
7
+ * ...
8
+ */
9
+ var genConfiguration = function () {
10
+ return {};
11
+ };
12
+ exports.default = genConfiguration;
13
+ //# sourceMappingURL=genConfiguration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"genConfiguration.js","sourceRoot":"","sources":["../src/genConfiguration.ts"],"names":[],"mappings":";;AAAA;;;;;GAKG;AACH,IAAM,gBAAgB,GAAG;IACvB,OAAO,EAAE,CAAC;AACZ,CAAC,CAAC;AAEF,kBAAe,gBAAgB,CAAC"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import init from './init';
2
+ import genConfiguration from './genConfiguration';
3
+ export { genConfiguration, init, };
package/lib/index.js ADDED
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.init = exports.genConfiguration = void 0;
7
+ var init_1 = __importDefault(require("./init"));
8
+ exports.init = init_1.default;
9
+ var genConfiguration_1 = __importDefault(require("./genConfiguration"));
10
+ exports.genConfiguration = genConfiguration_1.default;
11
+ // Automatically initialize upon importing the library
12
+ (0, init_1.default)();
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,gDAA0B;AASxB,eATK,cAAI,CASL;AARN,wEAAkD;AAOhD,2BAPK,0BAAgB,CAOL;AALlB,sDAAsD;AACtD,IAAA,cAAI,GAAE,CAAC"}
package/lib/init.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ declare global {
2
+ namespace Cypress {
3
+ interface Chainable {
4
+ /**
5
+ * Custom command to click an element by selector
6
+ * @example cy.clickSomething('.my-button')
7
+ */
8
+ clickSomething(selector: string): Chainable<Element>;
9
+ }
10
+ }
11
+ }
12
+ declare const init: () => void;
13
+ export default init;
package/lib/init.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /// <reference types="cypress" />
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ var init = function () {
5
+ Cypress.Commands.add('clickSomething', function (selector) {
6
+ cy.get(selector).click();
7
+ });
8
+ };
9
+ exports.default = init;
10
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":";AAAA,iCAAiC;;AAejC,IAAM,IAAI,GAAG;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,gBAAgB,EAAE,UAAC,QAAgB;QACtD,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,CAAC;IAC3B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,kBAAe,IAAI,CAAC"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "dceky",
3
+ "version": "1.0.0-beta-profile.1",
4
+ "description": "Cypress toolkit for Harvard DCE",
5
+ "main": "./lib/index.js",
6
+ "types": "./lib/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc --project ./tsconfig.json",
9
+ "cypress": "npx cypress open",
10
+ "cy:open:stage": "cross-env CYPRESS_PROFILE=stage npx cypress open",
11
+ "cy:open:prod": "cross-env CYPRESS_PROFILE=prod npx cypress open",
12
+ "cy:run:stage": "cross-env CYPRESS_PROFILE=stage npx cypress run",
13
+ "cy:run:prod": "cross-env CYPRESS_PROFILE=prod npx cypress run",
14
+ "launch-cypress": "npx tsx ./profileChooser/chooseProfile.ts"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/harvard-edtech/dceky"
19
+ },
20
+ "keywords": [
21
+ "Harvard",
22
+ "DCE",
23
+ "Cypress"
24
+ ],
25
+ "author": "Gabe Abrams <gabeabrams@gmail.com>",
26
+ "license": "MIT",
27
+ "bugs": {
28
+ "url": "https://github.com/harvard-edtech/dceky/issues"
29
+ },
30
+ "homepage": "https://github.com/harvard-edtech/dceky#readme",
31
+ "dependencies": {
32
+ "deepmerge": "4.3.1",
33
+ "clear": "^0.1.0",
34
+ "prompt-sync": "^4.2.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/deepmerge": "2.1.0",
38
+ "@types/node": "^24.6.1",
39
+ "typescript": "5.9.3"
40
+ },
41
+ "peerDependencies": {
42
+ "cypress": "14.5.4",
43
+ "@types/clear": "^0.1.4",
44
+ "@types/node": "^22.7.0",
45
+ "@types/prompt-sync": "^4.2.3",
46
+ "typescript": "^5.9.3"
47
+ }
48
+ }
@@ -0,0 +1,35 @@
1
+ // TODO: implement a *synchronous* profile chooser that executes the correct launch command
2
+
3
+ // Import libs
4
+ import path from 'path';
5
+ import fs from 'fs';
6
+
7
+ // Import helpers
8
+ import print from './helpers/print';
9
+ import showChooser from './helpers/showChooser';
10
+
11
+ // Get the project directory
12
+ const pwd = path.join(process.env.PWD || process.env.CWD || __dirname, '../..');
13
+ // NOTE: this is a dependency of the main project, so the actual project
14
+ // directory is up two levels (outside this folder, then outside node_modules)
15
+ console.log('Project directory:', pwd);
16
+
17
+ // TODO: explore the file tree to figure out which profiles exist
18
+ // (Build a chooser based on what you find)
19
+ // Maybe: /cypress/profiles/[Name].Profile.ts
20
+
21
+ print.title('Here\'s a title');
22
+
23
+ console.log('Let us do the thing.');
24
+ print.enterToContinue();
25
+
26
+ const choice = showChooser({
27
+ question: 'Choose an option:',
28
+ options: [
29
+ { tag: 'A', description: 'First option' },
30
+ { tag: 'B', description: 'Second option' },
31
+ { tag: 'C', description: 'Third option' },
32
+ ],
33
+ });
34
+
35
+ console.log('You chose:', choice);
@@ -0,0 +1,155 @@
1
+ /* eslint-disable no-console */
2
+
3
+ /**
4
+ * Get the left buffer for a centered message
5
+ * @author Gabe Abrams
6
+ * @param message message to print
7
+ * @param padding amount of padding to add
8
+ * @returns number of chars in the buffer
9
+ */
10
+ const leftBuffer = (message: string, padding: number): number => {
11
+ return (Math.floor(process.stdout.columns / 2) - padding - Math.ceil(message.length / 2));
12
+ };
13
+
14
+ /**
15
+ * Get the right buffer for a centered message
16
+ * @author Gabe Abrams
17
+ * @param message message to print
18
+ * @param padding amount of padding to add
19
+ * @returns number of chars in the buffer
20
+ */
21
+ const rightBuffer = (message: string, padding: number): number => {
22
+ return (Math.ceil(process.stdout.columns / 2) - padding - Math.floor(message.length / 2));
23
+ };
24
+
25
+ /**
26
+ * Surround text with a border and spaces
27
+ * @author Gabe Abrams
28
+ * @param str text to print
29
+ * @param border single character to use as a border
30
+ * @returns text to print
31
+ */
32
+ const surroundWithBuffer = (str: string, border: string): string => {
33
+ return (
34
+ border
35
+ + ' '.repeat(leftBuffer(str, border.length))
36
+ + str
37
+ + ' '.repeat(rightBuffer(str, border.length))
38
+ + border
39
+ );
40
+ };
41
+
42
+ /**
43
+ * Surround text with a character as the buffer
44
+ * @author Gabe Abrams
45
+ * @param str text to print
46
+ * @param char character to place as the buffer
47
+ * @returns text to print
48
+ */
49
+ const surroundWithChars = (str: string, char: string): string => {
50
+ if (str.length > process.stdout.columns) {
51
+ return str;
52
+ }
53
+ if (str.length === process.stdout.columns - 1) {
54
+ return char + str;
55
+ }
56
+ if (str.length === process.stdout.columns - 2) {
57
+ return char + str + char;
58
+ }
59
+ return (
60
+ char.repeat(leftBuffer(str, 1))
61
+ + ' '
62
+ + str
63
+ + ' '
64
+ + char.repeat(rightBuffer(str, 1))
65
+ );
66
+ };
67
+
68
+ // Prompt instance
69
+ let cachedPrompt: any;
70
+
71
+ const print = {
72
+ /**
73
+ * Print a title
74
+ * @author Gabe Abrams
75
+ * @param str text to print
76
+ */
77
+ title: (str: string) => {
78
+ if (str.length > process.stdout.columns) {
79
+ return console.log(str);
80
+ }
81
+ console.log('\u2554' + '\u2550'.repeat(process.stdout.columns - 2) + '\u2557');
82
+ console.log(surroundWithBuffer(str, '\u2551'));
83
+ console.log('\u255A' + '\u2550'.repeat(process.stdout.columns - 2) + '\u255D');
84
+ },
85
+ /**
86
+ * Print a sub title (subheading)
87
+ * @author Gabe Abrams
88
+ * @param str text to print
89
+ */
90
+ subtitle: (str: string) => {
91
+ if (str.length > process.stdout.columns) {
92
+ return console.log(str);
93
+ }
94
+ console.log(surroundWithChars(str, '\u257C'));
95
+ },
96
+ /**
97
+ * Print centered text
98
+ * @author Gabe Abrams
99
+ * @param str text to print
100
+ */
101
+ centered: (str: string) => {
102
+ const lines = [];
103
+ let index = 0;
104
+ while (index < str.length) {
105
+ lines.push(str.substring(index, Math.min(index + process.stdout.columns, str.length)));
106
+ index += process.stdout.columns;
107
+ }
108
+ lines.forEach((line, lineIndex) => {
109
+ if (lineIndex !== lines.length - 1) {
110
+ // No need to center: fills whole line
111
+ console.log(line);
112
+ } else {
113
+ // This line needs to be centered
114
+ console.log(surroundWithChars(line, ' '));
115
+ }
116
+ });
117
+ },
118
+ /**
119
+ * Print a fatal error message
120
+ * @author Gabe Abrams
121
+ * @param err error message
122
+ */
123
+ fatalError: (err: string) => {
124
+ console.log('\n');
125
+ const errLine1 = err.substring(0, process.stdout.columns - 6);
126
+ const errLine2 = err.substring(process.stdout.columns - 6);
127
+ console.log('\u2554' + '\u2550'.repeat(3) + '\u2557 ');
128
+ console.log(`\u2551 ! \u2551 ${errLine1}`);
129
+ console.log('\u255A' + '\u2550'.repeat(3) + '\u255D ' + errLine2);
130
+ process.exit(0);
131
+ },
132
+ /**
133
+ * Save a copy of the prompt instance
134
+ * @author Gabe Abrams
135
+ * @param promptInstance instance of prompt-sync
136
+ */
137
+ savePrompt: (promptInstance: any) => {
138
+ cachedPrompt = promptInstance;
139
+ },
140
+ /**
141
+ * Ask the user to press enter before continuing
142
+ * @author Gabe Abrams
143
+ */
144
+ enterToContinue: () => {
145
+ const res = cachedPrompt(
146
+ surroundWithChars('enter to continue, ctrl+c to quit', '\u257C'),
147
+ true,
148
+ );
149
+ if (res === null) {
150
+ process.exit(0);
151
+ }
152
+ },
153
+ };
154
+
155
+ export default print;
@@ -0,0 +1,23 @@
1
+ import initPrompt from 'prompt-sync';
2
+ import print from './print';
3
+
4
+ const promptSync = initPrompt();
5
+
6
+ /**
7
+ * Ask the user a question
8
+ * @param title title of the question
9
+ * @param notRequired true if question is not required
10
+ * @returns response
11
+ */
12
+ const prompt = (title: string, notRequired?: boolean): string => {
13
+ const val = promptSync(title);
14
+ if (val === null || (!notRequired && !val)) {
15
+ process.exit(0);
16
+ }
17
+ return val;
18
+ };
19
+
20
+ // Save the prompt for use later
21
+ print.savePrompt(prompt);
22
+
23
+ export default prompt;
@@ -0,0 +1,95 @@
1
+ import clear from 'clear';
2
+
3
+ // Import shared helpers
4
+ import print from './print';
5
+ import prompt from './prompt';
6
+
7
+ // Import shared types
8
+ import ChooserOption from '../types/ChooserOption';
9
+
10
+ /**
11
+ * Show a chooser between many options
12
+ * @author Gabe Abrams
13
+ * @param question the question to ask
14
+ * @param options list of options to choose from
15
+ * @param title main title to show above the choices
16
+ * @param dontClear if true, don't clear the console before
17
+ * @returns chosen option
18
+ */
19
+ const showChooser = (
20
+ opts: {
21
+ question: string,
22
+ options: ChooserOption[],
23
+ title?: string,
24
+ dontClear?: boolean,
25
+ },
26
+ ): {
27
+ description: string,
28
+ tag: string,
29
+ index: number,
30
+ } => {
31
+ const {
32
+ question,
33
+ options,
34
+ title,
35
+ dontClear,
36
+ } = opts;
37
+
38
+ // Print the header
39
+ if (!dontClear) {
40
+ clear();
41
+ }
42
+ if (title) {
43
+ print.title(title);
44
+ console.log('');
45
+ }
46
+ print.subtitle(question);
47
+
48
+ // Make sure all options have tags
49
+ let nextNumber = 1;
50
+ const optionsWithTags = options.map((option) => {
51
+ // Get/generate a tag
52
+ let { tag } = option;
53
+ if (!tag) {
54
+ tag = String(nextNumber);
55
+ nextNumber += 1;
56
+ }
57
+
58
+ // Return complete option
59
+ return {
60
+ ...option,
61
+ tag,
62
+ };
63
+ });
64
+
65
+ // Print options
66
+ for (let i = 0; i < optionsWithTags.length; i++) {
67
+ console.log(`${optionsWithTags[i].tag} - ${optionsWithTags[i].description}`);
68
+ }
69
+
70
+ // Ask user to choose
71
+ const responseTag = prompt('> ').trim();
72
+
73
+ // Find the appropriate option
74
+ for (let i = 0; i < optionsWithTags.length; i++) {
75
+ if (responseTag.toLowerCase() === optionsWithTags[i].tag.toLowerCase()) {
76
+ // Found the right option
77
+ return {
78
+ ...optionsWithTags[i],
79
+ index: i,
80
+ };
81
+ }
82
+ }
83
+
84
+ // No tag found
85
+ clear();
86
+ print.title('Invalid Choice');
87
+ console.log('');
88
+ print.enterToContinue();
89
+ return showChooser({
90
+ question,
91
+ options: optionsWithTags,
92
+ });
93
+ };
94
+
95
+ export default showChooser;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * An option that can be put into a chooser
3
+ * @author Gabe Abrams
4
+ */
5
+ type ChooserOption = {
6
+ description: string,
7
+ tag?: string,
8
+ index?: number,
9
+ };
10
+
11
+ export default ChooserOption;
@@ -0,0 +1,33 @@
1
+ /// <reference types="cypress" />
2
+
3
+ /*----------------------------------------*/
4
+ /* ---------------- Type ---------------- */
5
+ /*----------------------------------------*/
6
+
7
+ declare global {
8
+ namespace Cypress {
9
+ interface Chainable {
10
+ /**
11
+ * Custom command to click an element by selector
12
+ * @example cy.clickSomething('.my-button')
13
+ */
14
+ clickSomething(selector: string): Chainable<Element>;
15
+ }
16
+ }
17
+ }
18
+
19
+ /*----------------------------------------*/
20
+ /* --------------- Command -------------- */
21
+ /*----------------------------------------*/
22
+
23
+ const clickSomething = () => {
24
+ Cypress.Commands.add('clickSomething', (selector: string) => {
25
+ cy.get(selector).click();
26
+ });
27
+ };
28
+
29
+ /*----------------------------------------*/
30
+ /* --------------- Export --------------- */
31
+ /*----------------------------------------*/
32
+
33
+ export default clickSomething;
@@ -0,0 +1,47 @@
1
+
2
+ // Import shared types
3
+ import type GlobalsOrProfile from '../types/GlobalsOrProfile';
4
+ import type DependentValue from '../types/DependentValue';
5
+
6
+ /**
7
+ * Resolve all dependent values using already-merged basics.
8
+ * @author Gardenia Liu
9
+ * @param dependentMaps with dependencies on basics
10
+ * @param basics entries
11
+ * @returns env with all dependent values resolved
12
+ */
13
+ const resolveDependents = (
14
+ dependentMaps: { [k: string]: DependentValue }[],
15
+ basics: GlobalsOrProfile,
16
+ ): GlobalsOrProfile => {
17
+ const resolved: GlobalsOrProfile = {};
18
+
19
+ // Loop through all of the dependency maps
20
+ dependentMaps.forEach((depMap) => {
21
+ // Loop through all entries within dependency map
22
+ Object.entries(depMap).forEach(([k, depVal]) => {
23
+ // Get basic selector the value depends on
24
+ const selector = basics[depVal.dependsOn];
25
+
26
+ // Create table of values selector can be
27
+ const { dependsOn, ...table } = depVal as { [k: string]: unknown };
28
+
29
+ // Get the selector's picked value
30
+ const picked = table[selector];
31
+
32
+ if (picked !== undefined) {
33
+ resolved[k] = picked;
34
+ } else if ('default' in table) {
35
+ resolved[k] = table.default;
36
+ } else {
37
+ // eslint-disable-next-line max-len
38
+ console.warn(`[cypress.config] Could not resolve ${k}, dependsOn=${dependsOn}, selector=${String(selector)}`);
39
+ process.exit(1);
40
+ }
41
+ });
42
+ });
43
+
44
+ return resolved;
45
+ };
46
+
47
+ export default resolveDependents;
@@ -0,0 +1,30 @@
1
+ // Import shared types
2
+ import type GlobalsOrProfile from '../types/GlobalsOrProfile';
3
+ import type DependentValue from '../types/DependentValue';
4
+ import type SplitEnv from '../types/SplitEnv';
5
+
6
+ /**
7
+ * Split one map into basic and dependent values
8
+ * @author Gardenia Liu
9
+ * @param map to split
10
+ * @returns split basic and dependent values map
11
+ */
12
+ const splitEnv = (map: GlobalsOrProfile): SplitEnv => {
13
+ const basic: GlobalsOrProfile = {};
14
+ const dependent: { [k: string]: DependentValue } = {};
15
+
16
+ Object.entries(map).forEach(([k, v]) => {
17
+ // If entry has dependsOn relationship, add it to dependent dictionary
18
+ if (typeof v === 'object' && 'dependsOn' in v) {
19
+ dependent[k] = v as DependentValue;
20
+ // Otherwise, add it to basic dictionary
21
+ } else {
22
+ basic[k] = v;
23
+ }
24
+ });
25
+
26
+ // Returns separated basic and dependent values
27
+ return { basic, dependent };
28
+ };
29
+
30
+ export default splitEnv;
@@ -0,0 +1,83 @@
1
+ import path from 'path';
2
+
3
+ // Import external libraries
4
+ import { defineConfig } from 'cypress';
5
+ import deepmerge from 'deepmerge';
6
+
7
+ // Import helpers
8
+ import splitEnv from './helpers/splitEnv';
9
+ import resolveDependents from './helpers/resolveDependents';
10
+
11
+ // Import types
12
+ import type SplitEnv from './types/SplitEnv';
13
+ import type GlobalsOrProfile from './types/GlobalsOrProfile';
14
+
15
+ // Determine project directory
16
+ const CWD = (process.env.INIT_CWD || process.env.PWD);
17
+
18
+ /**
19
+ * Generate Cypress configuration based on global credentials,
20
+ * variables, resources, and profiles
21
+ * @author Gardenia Liu
22
+ * @author Gabe Abrams
23
+ */
24
+ const genConfiguration = () => {
25
+ // Define paths
26
+ const GLOBALS_PATH = path.join(CWD, '/cypress/globals');
27
+ const PROFILES_PATH = path.join(CWD, '/cypress/profiles');
28
+
29
+ // Import global env var objects
30
+ const GlobalCredentials: GlobalsOrProfile = require(path.join(GLOBALS_PATH, '/GlobalCredentials'));
31
+ const GlobalResources: GlobalsOrProfile = require(path.join(GLOBALS_PATH, '/GlobalResources'));
32
+ const GlobalValues: GlobalsOrProfile = require(path.join(GLOBALS_PATH, '/GlobalValues'));
33
+
34
+ // Determine profile file name (default is stage)
35
+ const profileName = process.env.CYPRESS_PROFILE || 'stage';
36
+
37
+ // Load in chosen profile configuration
38
+ let profileModules: GlobalsOrProfile = {};
39
+ try {
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ const mod = require(path.join(PROFILES_PATH, `/${profileName}.Profile`));
42
+ profileModules = (mod?.default ?? mod ?? {}) as GlobalsOrProfile;
43
+ } catch (e) {
44
+ console.warn(`[cypress.config] Could not load profile '${profileName}.Profile'`, e);
45
+ process.exit(1);
46
+ }
47
+
48
+ // NOTE: Order matters (later overrides earlier)
49
+ const sources: GlobalsOrProfile[] = [GlobalCredentials, GlobalResources, GlobalValues, profileModules];
50
+
51
+ // Split all sources into basic and dependent
52
+ const splits: SplitEnv[] = sources.map(source => splitEnv(source));
53
+
54
+ // Extract basic and dependent values from splits
55
+ const basicValues = splits.map((s) => s.basic);
56
+ const dependentValues = splits.map((s) => s.dependent);
57
+
58
+ // Merge all of the basics
59
+ const basics: GlobalsOrProfile = deepmerge.all<GlobalsOrProfile>(basicValues);
60
+
61
+ // Resolve dependencies
62
+ const dependents: GlobalsOrProfile = resolveDependents(dependentValues, basics);
63
+
64
+ // Add basics, depends, and profileName into the final env
65
+ const finalEnv: GlobalsOrProfile = {
66
+ ...basics,
67
+ ...dependents,
68
+ profileName,
69
+ };
70
+
71
+ const baseUrl = finalEnv.baseURL;
72
+
73
+ const config = defineConfig({
74
+ e2e: {
75
+ baseUrl,
76
+ env: finalEnv,
77
+ },
78
+ });
79
+
80
+ return config;
81
+ };
82
+
83
+ export default genConfiguration;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Value for a dependent entry
3
+ * @author Gardenia Liu
4
+ */
5
+ type DependentValue = {
6
+ dependsOn: string,
7
+ } & {
8
+ // Environment, global variables, profile values, etc.
9
+ [k: string]: any,
10
+ // Default value if the dependsOn value is not found
11
+ default?: any,
12
+ };
13
+
14
+ export default DependentValue;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Object containing global variables, profile values, etc.
3
+ * @author Gardenia Liu
4
+ */
5
+ type GlobalsOrProfile = {
6
+ // Base URL to prefix all paths with
7
+ baseURL?: string,
8
+ // Environment, global variables, profile values, etc.
9
+ [k: string]: any,
10
+ };
11
+
12
+ export default GlobalsOrProfile;
@@ -0,0 +1,18 @@
1
+ // Import shared types
2
+ import type GlobalsOrProfile from './GlobalsOrProfile';
3
+ import type DependentValue from './DependentValue';
4
+
5
+ /**
6
+ * Env type where basic and dependent entries are separated
7
+ * @author Gardenia Liu
8
+ */
9
+ type SplitEnv = {
10
+ // Basic entries
11
+ basic: GlobalsOrProfile,
12
+ // Dependent entries
13
+ dependent: {
14
+ [k: string]: DependentValue,
15
+ },
16
+ };
17
+
18
+ export default SplitEnv;
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ // Import helpers
2
+ import init from './init';
3
+ import genConfiguration from './genConfiguration';
4
+
5
+ // Import types
6
+ import GlobalsOrProfile from './genConfiguration/types/GlobalsOrProfile';
7
+
8
+ // Automatically initialize upon importing the library
9
+ init();
10
+
11
+ export {
12
+ // Helpers
13
+ init,
14
+ genConfiguration,
15
+ // Types
16
+ GlobalsOrProfile,
17
+ };
package/src/init.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Import all commands
2
+ import clickSomething from './commands/clickSomething';
3
+
4
+ /**
5
+ * Initialize custom commands
6
+ * @author Gabe Abrams
7
+ */
8
+ const init = () => {
9
+ // Execute each command adder
10
+ clickSomething();
11
+ };
12
+
13
+ export default init;
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "esModuleInterop": true,
5
+ "noImplicitAny": true,
6
+ "noEmitOnError": true,
7
+ "removeComments": false,
8
+ "declaration": true,
9
+ "sourceMap": true,
10
+ "target": "es5",
11
+ "lib": ["es5", "es2015.promise", "es2017.object"],
12
+ "outDir": "./lib"
13
+ },
14
+ "include": [
15
+ "./src"
16
+ ],
17
+ "ts-node": {
18
+ "files": true
19
+ }
20
+ }