-
Notifications
You must be signed in to change notification settings - Fork 36
OCPNETUI-56: Add Cypress E2E framework config and support files #468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| BRIDGE_BASE_ADDRESS=https://console-openshift-console.apps.your-cluster.example.com | ||
| BRIDGE_KUBEADMIN_PASSWORD=your-password | ||
| TEST_NS=cy-test-ns | ||
| UDN_NS=udn-test-ns | ||
| HIDE_XHR=true |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,12 +13,14 @@ | |
| "start-console": "./start-console.sh", | ||
| "i18n": "./i18n-scripts/build-i18n.sh && node ./i18n-scripts/set-english-defaults.js", | ||
| "ts-node": "ts-node -O '{\"module\":\"commonjs\"}'", | ||
| "lint": "eslint ./src ./integration-tests", | ||
| "lint-fix": "eslint ./src ./integration-tests --fix", | ||
| "test-cypress": "cd integration-tests s && cypress open --env openshift=true", | ||
| "lint": "eslint ./src ./integration-tests ./ui-tests-cy", | ||
| "lint-fix": "eslint ./src ./integration-tests ./ui-tests-cy --fix", | ||
| "test-cypress": "cd integration-tests && cypress open --env openshift=true", | ||
| "test-cypress-headless": "cd integration-tests && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", | ||
| "cypress-merge": "mochawesome-merge ./integration-tests/screenshots/cypress_report*.json > ./integration-tests/screenshots/cypress.json", | ||
| "cypress-generate": "marge -o ./integration-tests/screenshots/ -f cypress-report -t 'OpenShift Console Plugin Template Cypress Test Results' -p 'OpenShift Cypress Plugin Template Test Results' --showPassed false --assetsDir ./integration-tests/screenshots/cypress/assets ./integration-tests/screenshots/cypress.json", | ||
| "test-e2e": "cd cypress && node --max-old-space-size=4096 ../node_modules/.bin/cypress run --env openshift=true --browser ${BRIDGE_E2E_BROWSER_NAME:=electron}", | ||
| "test-e2e-open": "cd cypress && cypress open --env openshift=true", | ||
| "cypress-merge": "mochawesome-merge ./cypress/gui-test-screenshots/cypress_report*.json > ./cypress/gui-test-screenshots/cypress.json", | ||
| "cypress-generate": "marge -o ./cypress/gui-test-screenshots/ -f cypress-report -t 'Networking Console Plugin Test Results' -p 'Networking Plugin Test Results' --showPassed false --assetsDir ./cypress/gui-test-screenshots/assets ./cypress/gui-test-screenshots/cypress.json", | ||
|
Comment on lines
+20
to
+23
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
test -f ui-tests-cy/cypress.config.js
printf 'Cypress configuration files:\n'
fd -H '^cypress\.config\.(js|ts|mjs|cjs)$' .
printf '\nConfigured E2E scripts:\n'
jq -r '.scripts | to_entries[] | select(.key | test("^(test-e2e|test-e2e-open|cypress-merge|cypress-generate)$")) | "\(.key): \(.value)"' package.json
printf '\nLegacy cypress directory:\n'
test -d cypress && find cypress -maxdepth 2 -type f -name 'cypress.config.*' -print || trueRepository: openshift/networking-console-plugin Length of output: 944 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' 'Relevant tracked paths:'
git ls-files | grep -E '(^|/)(cypress|ui-tests-cy)/|(^|/)package(-lock)?\.json$' | head -200
printf '\nDirectory status:\n'
if test -d cypress; then find cypress -maxdepth 2 -type f -print; else echo 'cypress directory: absent'; fi
if test -d ui-tests-cy; then find ui-tests-cy -maxdepth 2 -type f -print | head -100; else echo 'ui-tests-cy directory: absent'; fi
printf '\nCypress configuration:\n'
cat -n ui-tests-cy/cypress.config.js
printf '\nPackage scripts and Cypress-related dependencies:\n'
node - <<'JS'
const fs = require('fs');
const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));
for (const [k, v] of Object.entries(p.scripts || {})) {
if (/e2e|cypress|mocha|marge/i.test(k) || /cypress|mochawesome|marge/i.test(v)) {
console.log(`${k}: ${v}`);
}
}
for (const section of ['dependencies', 'devDependencies']) {
for (const [k, v] of Object.entries(p[section] || {})) {
if (/cypress|mochawesome|dotenv/i.test(k)) console.log(`${section}.${k}: ${v}`);
}
}
JSRepository: openshift/networking-console-plugin Length of output: 3192 Change the Cypress scripts to use The repository has no 🧰 Tools🪛 ESLint[error] 22-22: Expected "cypress-merge" to come before "test-e2e-open" (perfectionist/sort-objects) [error] 23-23: Expected "cypress-generate" to come before "cypress-merge" (perfectionist/sort-objects) 🤖 Prompt for AI Agents |
||
| "cypress-postreport": "npm run cypress-merge && npm run cypress-generate", | ||
| "memsource-upload": "./i18n-scripts/memsource-upload.sh", | ||
| "memsource-download": "./i18n-scripts/memsource-download.sh", | ||
|
|
@@ -59,6 +61,7 @@ | |
| "css-loader": "^6.7.1", | ||
| "cypress": "^15.0.0", | ||
| "cypress-multi-reporters": "^2.0.0", | ||
| "dotenv": "^17.4.2", | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
VERSION="$(jq -r '.devDependencies.dotenv' package.json | sed 's/^[^0-9]*//')"
printf 'Declared dotenv version: %s\n' "$VERSION"
printf '\nLockfile entries:\n'
fd -H '^(package-lock\.json|npm-shrinkwrap\.json)$' . -x \
jq -r '.packages["node_modules/dotenv"].version // .dependencies.dotenv.version // empty' {}
printf '\nOSV result:\n'
curl -sSf -X POST https://api.osv.dev/v1/query \
-H 'content-type: application/json' \
-d "{\"package\":{\"ecosystem\":\"npm\",\"name\":\"dotenv\"},\"version\":\"${VERSION}\"}" | jq .Repository: openshift/networking-console-plugin Length of output: 247 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' 'package.json dependency context:'
sed -n '1,95p' package.json
printf '\n%s\n' 'dotenv usage:'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
'dotenv|loadEnv|\.env' .
printf '\n%s\n' 'Lockfile metadata:'
fd -H '^(package-lock\.json|npm-shrinkwrap\.json|yarn\.lock|pnpm-lock\.yaml)$' . -x sh -c '
printf "\n--- %s ---\n" "$1"
case "$1" in
*.json) jq "{lockfileVersion, packages: {dotenv: .packages[\"node_modules/dotenv\"], root: .packages[\"\"]}}" "$1" ;;
*) rg -n -A 8 -B 2 "dotenv" "$1" ;;
esac
' sh {}Repository: openshift/networking-console-plugin Length of output: 8304 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' 'dotenv lockfile record:'
jq '.packages["node_modules/dotenv"]' package-lock.json
printf '\n%s\n' 'Root dotenv spec:'
jq -r '.packages[""].devDependencies.dotenv' package-lock.json
printf '\n%s\n' 'Registry metadata:'
curl -sSf https://registry.npmjs.org/dotenv/17.4.2 |
jq '{name, version, license, dist: {integrity, tarball}}'Repository: openshift/networking-console-plugin Length of output: 726 Pin the
🤖 Prompt for AI AgentsSource: Path instructions |
||
| "esbuild-loader": "^4.4.2", | ||
| "eslint": "^8.57.1", | ||
| "eslint-config-prettier": "^8.10.0", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "env": { | ||
| "cypress/globals": true, | ||
| "node": true | ||
| }, | ||
| "extends": ["../.eslintrc.json", "plugin:cypress/recommended"], | ||
| "plugins": ["cypress"], | ||
| "rules": { | ||
| "no-console": "off", | ||
| "no-namespace": "off", | ||
| "no-redeclare": "off", | ||
| "promise/catch-or-return": "off", | ||
| "promise/no-nesting": "off", | ||
| "@typescript-eslint/no-var-requires":"off", | ||
| "@typescript-eslint/no-namespace":"off" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| const { defineConfig } = require('cypress'); | ||
|
|
||
| module.exports = defineConfig({ | ||
| defaultCommandTimeout: 60000, | ||
| e2e: { | ||
| setupNodeEvents(on, config) { | ||
| return require('./plugins/index.ts')(on, config); | ||
| }, | ||
| specPattern: 'tests/**/*.cy.ts', | ||
| supportFile: 'support/index.ts', | ||
| testIsolation: false, | ||
| }, | ||
| fixturesFolder: false, | ||
| pageLoadTimeout: 120000, | ||
| reporter: '../node_modules/cypress-multi-reporters', | ||
| reporterOptions: { | ||
| configFile: 'reporter-config.json', | ||
| }, | ||
| retries: { | ||
| openMode: 0, | ||
| runMode: 0, | ||
| }, | ||
| screenshotOnRunFailure: true, | ||
| screenshotsFolder: './gui-test-screenshots/screenshots/', | ||
| trashAssetsBeforeRuns: true, | ||
| video: true, | ||
| videosFolder: './gui-test-screenshots/videos/', | ||
| viewportHeight: 1080, | ||
| viewportWidth: 1920, | ||
| watchForFileChanges: false, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| const wp = require('@cypress/webpack-preprocessor'); | ||
| const path = require('path'); | ||
| const dotenv = require('dotenv'); | ||
|
|
||
| dotenv.config({ path: path.resolve(__dirname, '../../.env') }); | ||
|
|
||
| module.exports = (on, config) => { | ||
| const options = { | ||
| webpackOptions: { | ||
| module: { | ||
| rules: [ | ||
| { | ||
| exclude: /node_modules/, | ||
| loader: 'esbuild-loader', | ||
| test: /\.ts$/, | ||
| }, | ||
| ], | ||
| }, | ||
| resolve: { | ||
| extensions: ['.ts', '.tsx', '.js'], | ||
| }, | ||
| }, | ||
| }; | ||
| on('file:preprocessor', wp(options)); | ||
| config.baseUrl = `${process.env.BRIDGE_BASE_ADDRESS || 'http://localhost:9000/'}`; | ||
| config.env.BRIDGE_KUBEADMIN_PASSWORD = process.env.BRIDGE_KUBEADMIN_PASSWORD; | ||
| config.env.TEST_NS = process.env.TEST_NS; | ||
| config.env.UDN_NS = process.env.UDN_NS; | ||
| config.env.HIDE_XHR = process.env.HIDE_XHR; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n -C 3 'HIDE_XHR|dotenv\.config|Cypress\.env' .env.example ui-tests-cyRepository: openshift/networking-console-plugin Length of output: 2515 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- ui-tests-cy/plugins/index.ts ---'
cat -n ui-tests-cy/plugins/index.ts
printf '%s\n' '--- ui-tests-cy/support/index.ts ---'
cat -n ui-tests-cy/support/index.ts
printf '%s\n' '--- all HIDE_XHR references and test configuration ---'
rg -n -C 4 'HIDE_XHR|plugins/index|support/index' . \
-g '!node_modules' -g '!dist' -g '!build'
printf '%s\n' '--- dotenv dependency declarations ---'
rg -n -C 2 '"dotenv"|dotenv' package.json package-lock.json yarn.lock pnpm-lock.yaml ui-tests-cy 2>/dev/null || true
printf '%s\n' '--- boolean coercion probe ---'
python3 - <<'PY'
values = [None, '', 'false', 'true', 'False', 'TRUE', '0', '1']
for value in values:
js_truthy = value is not None and value != ''
parsed = value == 'true'
print(f'value={value!r:7} JS_truthy={js_truthy!s:5} parsed_true={parsed!s:5}')
PYRepository: openshift/networking-console-plugin Length of output: 7400 Parse When 🤖 Prompt for AI Agents |
||
| return config; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "reporterEnabled": "mocha-junit-reporter, mochawesome", | ||
| "mochaJunitReporterReporterOptions": { | ||
| "mochaFile": "./gui-test-screenshots/junit_cypress-[hash].xml", | ||
| "toConsole": false | ||
| }, | ||
| "mochawesomeReporterOptions": { | ||
| "reportDir": "./gui-test-screenshots/", | ||
| "reportFilename": "cypress_report", | ||
| "overwrite": false, | ||
| "html": true, | ||
| "json": true | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| import { MINUTE, SECOND } from '../utils/const/base'; | ||
|
|
||
| declare global { | ||
| namespace Cypress { | ||
| interface Chainable { | ||
| deleteResource(kind: string, name: string, namespace?: string): void; | ||
| switchProject(projectName: string): void; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Cypress.Commands.add('deleteResource', (kind: string, name: string, namespace?: string) => { | ||
| const args = ['oc', 'delete', '--ignore-not-found=true']; | ||
| if (namespace) { | ||
| args.push('-n', namespace); | ||
| } | ||
| args.push(kind, name, '--wait=true', '--timeout=300s'); | ||
| cy.exec(args.join(' '), { failOnNonZeroExit: false, timeout: 5 * MINUTE }); | ||
| }); | ||
|
|
||
| Cypress.Commands.add('switchProject', (projectName: string) => { | ||
| cy.byLegacyTestID('namespace-bar-dropdown').contains('Project:').click(); | ||
| if (projectName === 'All Projects') { | ||
| cy.contains('All Projects').click(); | ||
| return; | ||
| } | ||
| cy.get('body').then(($body) => { | ||
| if ($body.find('[data-test="showSystemSwitch"]').length) { | ||
| cy.byTestID('showSystemSwitch').check(); | ||
| cy.wait(3 * SECOND); | ||
| } | ||
| }); | ||
| cy.get( | ||
| 'input[data-test="dropdown-text-filter"], input[placeholder*="name"], input[aria-label*="filter"]', | ||
| { timeout: 10000 }, | ||
| ) | ||
| .first() | ||
| .then(($input) => { | ||
| cy.wrap($input).clear(); | ||
| cy.wrap($input).type(projectName); | ||
| }); | ||
| cy.contains('[data-test="dropdown-menu-item-link"], [role="option"], button', projectName, { | ||
| timeout: 10000, | ||
| }).click(); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import './login'; | ||
| import './selectors'; | ||
| import './commands'; | ||
| import './nav'; | ||
|
|
||
| Cypress.on('uncaught:exception', (err) => { | ||
| if (err.message.includes('ResizeObserver') || err.message.includes('Script error')) { | ||
| return false; | ||
| } | ||
| }); | ||
|
|
||
| if (Cypress.env('HIDE_XHR')) { | ||
| const origLog = Cypress.log; | ||
| Cypress.log = function (opts, ...other) { | ||
| if (opts.displayName === 'fetch' || opts.displayName === 'xhr') { | ||
| return; | ||
| } | ||
| return origLog(opts, ...other); | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { MINUTE } from '../utils/const/base'; | ||
|
|
||
| const KUBEADMIN_USERNAME = 'kubeadmin'; | ||
| const KUBEADMIN_IDP = 'kube:admin'; | ||
| const TOUR_DISMISS = '[data-test="tour-step-footer-secondary"]'; | ||
|
|
||
| declare global { | ||
| namespace Cypress { | ||
| interface Chainable { | ||
| login(providerName?: string, username?: string, password?: string): Chainable<Element>; | ||
| logout(): void; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Cypress.Commands.add('login', (provider: string, username: string, password: string) => { | ||
| const usr = username || KUBEADMIN_USERNAME; | ||
| const pwd = password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD'); | ||
| const idp = provider || KUBEADMIN_IDP; | ||
|
|
||
| cy.visit(''); | ||
|
|
||
| cy.origin( | ||
| Cypress.config('baseUrl').replace('console-openshift-console', 'oauth-openshift'), | ||
| { args: { idp, pwd, usr } }, | ||
| ({ idp: originIdp, pwd: originPwd, usr: originUsr }) => { | ||
| // Wait for either the login form or IDP selection page | ||
| cy.get('body', { timeout: 180000 }).should('be.visible'); | ||
| cy.get('body').then(($body) => { | ||
| if ($body.find('#inputUsername').length === 0) { | ||
| // IDP selection page — click the matching provider | ||
| if ($body.text().includes(originIdp)) { | ||
| cy.contains('a', originIdp).click(); | ||
| } else if ($body.text().includes('kubeadmin')) { | ||
| cy.contains('a', 'kubeadmin').click(); | ||
| } else { | ||
| cy.get('a').first().click(); | ||
| } | ||
| } | ||
| }); | ||
| cy.get('#inputUsername', { timeout: 180000 }).should('be.visible'); | ||
| cy.get('#inputUsername').type(originUsr); | ||
| cy.get('#inputPassword').type(originPwd, { log: false }); | ||
| cy.get('button[type=submit]').click(); | ||
| }, | ||
| ); | ||
|
|
||
| cy.url({ timeout: 2 * MINUTE }).should('include', 'console-openshift-console'); | ||
| cy.get('body').then(($body) => { | ||
| if ($body.find(TOUR_DISMISS).length) { | ||
| cy.get(TOUR_DISMISS).click(); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| Cypress.Commands.add('logout', () => { | ||
| cy.get('[data-test="user-dropdown"]').click(); | ||
| cy.get('[data-test="log-out"]').should('be.visible'); | ||
| cy.get('[data-test="log-out"]').click({ force: true }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| import { MINUTE } from '../utils/const/base'; | ||
|
|
||
| declare global { | ||
| namespace Cypress { | ||
| interface Chainable { | ||
| visitNAD(): void; | ||
| visitService(): void; | ||
| visitUDN(): void; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Cypress.Commands.add('visitNAD', () => { | ||
| const ns = Cypress.env('TEST_NS') || 'cy-test-ns'; | ||
| cy.visit(`/k8s/ns/${ns}/k8s.cni.cncf.io~v1~NetworkAttachmentDefinition`); | ||
|
Comment on lines
+14
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- ui-tests-cy/support/nav.ts ---'
cat -n ui-tests-cy/support/nav.ts
printf '%s\n' '--- TEST_NS references ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'TEST_NS|k8s/ns/' .
printf '%s\n' '--- namespace-related validation ---'
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' 'namespace|normalize\(|Cypress\.env' ui-tests-cyRepository: openshift/networking-console-plugin Length of output: 4915 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Cypress configuration and environment setup ---'
fd -i -t f 'cypress|package.json|README|env' . | sort | while read -r f; do
case "$f" in
*cypress*|*.env*|README*|package.json) printf '%s\n' "$f" ;;
esac
done
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
'TEST_NS|baseUrl|cy\.visit|visitNAD|namespace' \
cypress.config.* ui-tests-cy package.json .env.example README* 2>/dev/null || true
printf '%s\n' '--- Route definitions for NAD list pages ---'
sed -n '1,130p' src/views/nads/manifest.ts
rg -n --hidden --glob '!node_modules' --glob '!dist' --glob '!build' \
'NetworkAttachmentDefinition|k8s/ns/:ns|k8s/ns/:namespace' src
printf '%s\n' '--- URL path normalization behavior ---'
node - <<'JS'
for (const ns of [
'cy-test-ns',
'../../all-namespaces/services',
'a/b',
'a%2Fb',
'valid.namespace',
'A_NAMESPACE'
]) {
const path = `/k8s/ns/${ns}/k8s.cni.cncf.io~v1~NetworkAttachmentDefinition`;
console.log(JSON.stringify({ ns, path, resolved: new URL(path, 'http://example.test').pathname }));
}
JSRepository: openshift/networking-console-plugin Length of output: 33304 Validate If 🤖 Prompt for AI AgentsSource: Path instructions |
||
| cy.checkTitle('NetworkAttachmentDefinitions', MINUTE); | ||
| }); | ||
|
|
||
| Cypress.Commands.add('visitUDN', () => { | ||
| cy.visit('/k8s/all-namespaces/k8s.ovn.org~v1~UserDefinedNetwork'); | ||
| cy.checkTitle('UserDefinedNetworks', MINUTE); | ||
| cy.byButtonText('Create').should('be.visible'); | ||
| }); | ||
|
|
||
| Cypress.Commands.add('visitService', () => { | ||
| cy.visit('/k8s/all-namespaces/services'); | ||
| cy.checkTitle('Services', MINUTE); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Resolve the reported script-order errors.
Static analysis reports
perfectionist/sort-objectserrors forlint,cypress-merge, andcypress-generate. Reorder thescriptsentries to restore lint compliance.🧰 Tools
🪛 ESLint
[error] 16-16: Expected "lint" to come before "ts-node"
(perfectionist/sort-objects)
[error] 22-22: Expected "cypress-merge" to come before "test-e2e-open"
(perfectionist/sort-objects)
[error] 23-23: Expected "cypress-generate" to come before "cypress-merge"
(perfectionist/sort-objects)
🤖 Prompt for AI Agents
Source: Linters/SAST tools