diff --git a/Jenkinsfile_CNP b/Jenkinsfile_CNP index 8cded1c34c..5a1f775d8d 100644 --- a/Jenkinsfile_CNP +++ b/Jenkinsfile_CNP @@ -531,14 +531,14 @@ withPipeline(type, product, component) { } else { echo 'Running opal component tests (no skip label present)' try { - yarnBuilder.yarn('test:opalComponent') + yarnBuilder.yarn('test:component') } catch (hudson.AbortException e) { echo "Opal component tests failed: ${e}" currentBuild.result = 'FAILURE' } finally { archiveArtifact(functionalArtifactsGlob) publishHtmlReport( - "functional-output/component-html/${resolvedBrowserToRun()}/", + "functional-output/component/${resolvedBrowserToRun()}/html/", 'component-report.html', "Component Test Report (${resolvedBrowserToRun()})" ) diff --git a/Jenkinsfile_nightly b/Jenkinsfile_nightly index d8bcacc8c4..74cc7e8129 100644 --- a/Jenkinsfile_nightly +++ b/Jenkinsfile_nightly @@ -393,12 +393,11 @@ void runComponentStage( ) { runNightlyStage(enabled, 'Component Tests', failureResultValue, 'Component tests failed') { try { - builder.yarn(shouldRunWithZephyr ? 'zephyr:test:opalComponent' : 'test:opalComponent') + builder.yarn(shouldRunWithZephyr ? 'zephyr:test:component' : 'test:component') } finally { - archiveArtifact(functionalProdArtifactsPath) - archiveArtifact("functional-output/screenshots/${browserToRun}/component/**") + archiveArtifact("functional-output/component/${browserToRun}/**") publishHtmlReport( - "functional-output/component-html/${browserToRun}/", + "functional-output/component/${browserToRun}/html/", 'component-report.html', "Component Test Report (${browserToRun})" ) @@ -521,11 +520,7 @@ void runLegacyStage( ) { runNightlyStage(enabled, 'Legacy Tests', failureResultValue, 'Legacy tests failed') { try { - builder.yarn( - 'yarn test:functionalLegacy ; ' + - 'yarn test:functionalLegacy:combine:reports ; ' + - 'yarn test:functionalLegacy:cucumber:combineParallelReport' - ) + builder.yarn('test:functional --mode=legacy') } finally { archiveArtifact("functional-output/prod/${browserToRun}/legacy/**") archiveArtifact("functional-output/screenshots/${browserToRun}/legacy/**") diff --git a/README.md b/README.md index bd06d4e698..79cef3808b 100644 --- a/README.md +++ b/README.md @@ -276,13 +276,32 @@ TAGS=@UAT-Technical yarn test:functional:tags ``` -### Legacy mode +Run `yarn test:component` to execute the Cypress component suite. -To run Opal functional tests in legacy app mode, used for UAT-Technical coverage: +All three top-level runners accept: + +- `--browser=` for an explicit browser +- `--mode=` for suite mode selection +- `--parallel` or `--serial` to override the default execution style + +Examples: + +```bash + +yarn test:component --browser=chrome --parallel +yarn test:smoke --mode=legacy --serial +yarn test:functional --browser=firefox --mode=opal --parallel + +``` + +### Legacy app mode + +To run the UAT-Technical-tagged functional tests against legacy app mode locally: +This keeps the functional suite on the normal OPAL spec tree and only switches the app/helpers into legacy mode. ```bash -yarn test:functional:uat-legacy +yarn test:functional:uat_legacy ``` @@ -302,7 +321,77 @@ yarn cypress ### Reports -Artifacts and reports are written to `smoke-output/` and `functional-output/`, using browser-specific subdirectories where applicable. +After a clean run, artifacts and reports are written to `functional-output/` and `smoke-output/`. +Replace `` with `chrome`, `edge`, or `firefox`. + +```text +functional-output/ + component/ + / + html/ + component-report.html + assets/... + json/ + .jsons/ + mochawesome*.json + junit/ + component-test-output-*.xml + screenshots/... + prod/ + / + opal-mode-test-output-*.xml + -test-result.xml + cucumber/ + OPAL-report-*.ndjson + -report.ndjson + -report.html + legacy/ + legacy-mode-test-output-*.xml + legacy-test-result.xml + cucumber/ + LEGACY-report-*.ndjson + legacy-report.ndjson + legacy-report.html + screenshots/ + /... + /legacy/... + videos/... + zephyr/ + cypress-report-1.json + cucumber-report.json + temp/... + account_evidence/... + +smoke-output/ + prod/ + / + opal-mode-test-output-*.xml + -test-result.xml + cucumber/ + OPAL-report-*.ndjson + smoke-report.ndjson + smoke-report.html + legacy/ + legacy-mode-test-output-*.xml + legacy-test-result.xml + cucumber/ + LEGACY-report-*.ndjson + legacy-report.ndjson + legacy-report.html + screenshots/ + /... + /legacy/... + zephyr/ + cucumber-report.json +``` + +Notes: + +- `functional-output/component//json/.jsons/` is the raw Mochawesome JSON used to build `html/component-report.html`. +- `functional-output/prod//legacy/` and `smoke-output/prod//legacy/` are only created for legacy-mode runs. +- `videos/` is only expected when using `yarn test:functionalOpalVideo`. +- `account_evidence/` is only expected when legacy evidence capture is enabled. +- These older component paths should not be recreated on a clean run: `functional-output/component-report/`, `functional-output/component-html/`, and `functional-output/prod//component/`. ## Running accessibility tests @@ -496,7 +585,7 @@ Zephyr Automation is a tool for integrating test results and ticket management b - `zephyr:cucumber:smoke:jira-create`: Create Jira tickets from the smoke Cucumber JSON report at `smoke-output/zephyr/cucumber-report.json`. - `zephyr:cucumber:smoke:jira-update`: Update Jira tickets using the smoke Cucumber JSON report at `smoke-output/zephyr/cucumber-report.json`. - `zephyr:cucumber:smoke:jira-execute`: Create a Zephyr execution from the smoke Cucumber JSON report at `smoke-output/zephyr/cucumber-report.json`. -- `zephyr:test:opalComponent`: Reset outputs, run component tests, then create a Zephyr execution from the Cypress JSON report. +- `zephyr:test:component`: Reset outputs, run component tests, then create a Zephyr execution from the Cypress JSON report. - `zephyr:test:functional`: Reset outputs, run functional tests, then create a Zephyr execution from the functional Cucumber JSON report. - `zephyr:test:smoke`: Reset outputs, run smoke tests, then create a Zephyr execution from the smoke Cucumber JSON report. diff --git a/cspell.json b/cspell.json index ba7c08cf23..ff604e85cf 100644 --- a/cspell.json +++ b/cspell.json @@ -62,6 +62,7 @@ "impositions", "inputname", "inputvalue", + "JCDE", "lastname", "linters", "LJA", @@ -121,12 +122,26 @@ "VPFPO", "ZWNJ" ], - "ignorePaths": ["node_modules", "dist", "coverage", ".husky", "*.lock", "**/*.snap", "**/generated/**"], + "ignorePaths": [ + "node_modules", + "dist", + "coverage", + ".husky", + "*.lock", + "**/*.snap", + "**/generated/**" + ], "ignoreRegExpList": [ "/^\\s*\\/\\/\\s*cspell:disable-next-line.*$/", "/[A-F0-9]{8,}/", // hex IDs, hashes "/\\|[^\\n]*\\|/" // Cucumber tables: ignore anything between pipes ], - "files": ["**/*.{ts,tsx,js,jsx,json,md,html,css,scss,feature}", "cypress/**/*"], - "flagWords": ["teh", "adn"] + "files": [ + "**/*.{ts,tsx,js,jsx,json,md,html,css,scss,feature}", + "cypress/**/*" + ], + "flagWords": [ + "teh", + "adn" + ] } diff --git a/cypress.config.ts b/cypress.config.ts index 8ef740b5c6..ae0bcfafbd 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -284,11 +284,11 @@ export default defineConfig({ reporterEnabled: 'cypress-mochawesome-reporter, mocha-junit-reporter, @hmcts/zephyr-automation-nodejs/cypress/ZephyrReporter', mochaJunitReporterReporterOptions: { - mochaFile: `functional-output/prod/${resolvedBrowserToRun}/component/component-test-output-[hash].xml`, + mochaFile: `functional-output/component/${resolvedBrowserToRun}/junit/component-test-output-[hash].xml`, toConsole: false, }, cypressMochawesomeReporterReporterOptions: { - reportDir: `functional-output/component-report/${resolvedBrowserToRun}`, + reportDir: `functional-output/component/${resolvedBrowserToRun}/json`, overwrite: false, html: false, json: true, diff --git a/package.json b/package.json index aaa0ab8212..19ef34a5b5 100644 --- a/package.json +++ b/package.json @@ -10,58 +10,22 @@ "test:watch": "ng test --runner=vitest --watch", "test:coverage": "ng test --runner=vitest --watch=false --coverage", "test:a11y": "echo 'Covered in cypress testing' && exit 0", - "test:dev:smoke": "BROWSER=$(node scripts/resolve-browser.js); TEST_URL=https://localhost:4000/ BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --spec 'cypress/e2e/smoke/*-spec.cy.ts' --config \"screenshotsFolder=smoke-output/screenshots/$BROWSER/dev\" --reporter-options \"mochaFile=smoke-output/dev/$BROWSER/test-output-[hash].xml\" && yarn test:dev:smoke:combine:reports", - "test:smoke": "node scripts/run-smoke.js", - "test:smokeOpalParallel": "BROWSER=$(node scripts/resolve-browser.js); CYPRESS_messagesEnabled=true TEST_STAGE=smoke BROWSER_TO_RUN=$BROWSER cypress-parallel -s test:smokeOpal -d 'cypress/e2e/smoke/opal/**/*.feature' -m false -t 2 -w 'cypress/parallel/weights/smoke-parallel-weights.json' -r mocha-junit-reporter -o \"mochaFile=smoke-output/prod/$BROWSER/opal-mode-test-output-[hash].xml\"", - "test:smokeOpal": "BROWSER=$(node scripts/resolve-browser.js); TEST_MODE=OPAL BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --spec 'cypress/e2e/smoke/opal/*.feature' --config \"screenshotsFolder=smoke-output/screenshots/$BROWSER\" --reporter-options \"mochaFile=smoke-output/prod/$BROWSER/opal-mode-test-output-[hash].xml\"", - "test:smokeLegacy": "BROWSER=$(node scripts/resolve-browser.js); TEST_MODE=LEGACY BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --spec 'cypress/e2e/smoke/legacy/*.feature' --config \"screenshotsFolder=smoke-output/screenshots/$BROWSER/legacy\" --reporter-options \"mochaFile=smoke-output/prod/$BROWSER/legacy/legacy-mode-test-output-[hash].xml\"", - "test:dev:smoke:combine:reports": "BROWSER=$(node scripts/resolve-browser.js); jrm smoke-output/dev/${BROWSER}/${BROWSER}-test-result.xml \"smoke-output/dev/${BROWSER}/*.xml\" && cp smoke-output/dev/${BROWSER}/${BROWSER}-test-result.xml smoke-output/dev/test-result.xml", - "test:smoke:combine:reports": "BROWSER=$(node scripts/resolve-browser.js); jrm smoke-output/prod/${BROWSER}/${BROWSER}-test-result.xml \"smoke-output/prod/${BROWSER}/*.xml\" && cp smoke-output/prod/${BROWSER}/${BROWSER}-test-result.xml smoke-output/prod/test-result.xml", - "test:smoke:cucumber:combineParallelReport": "node scripts/build-cucumber-report.js smoke", - "test:smoke:cucumber:jsonReport": "BROWSER=$(node scripts/resolve-browser.js); yarn cucumber-merge-messages smoke-output/prod/$BROWSER/cucumber/*.ndjson > smoke-output/prod/$BROWSER/cucumber/smoke-report.ndjson; mkdir -p smoke-output/zephyr && cucumber-json-formatter < smoke-output/prod/$BROWSER/cucumber/smoke-report.ndjson > smoke-output/zephyr/cucumber-report.json", - "test:smoke:cucumber:htmlReport": "BROWSER=$(node scripts/resolve-browser.js); yarn cucumber-html-formatter < smoke-output/prod/$BROWSER/cucumber/smoke-report.ndjson > smoke-output/prod/$BROWSER/cucumber/smoke-report.html", - "test:dev:functional": "BROWSER=$(node scripts/resolve-browser.js); TEST_URL=https://localhost:4000/ BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --spec 'cypress/e2e/functional/*-spec.cy.ts' --config \"screenshotsFolder=functional-output/screenshots/$BROWSER/dev\" --reporter-options \"mochaFile=functional-output/dev/$BROWSER/test-output-[hash].xml\" && yarn test:dev:functional:combine:reports", - "test:functional": "node scripts/run-functional.js", - "test:Chrome": "node scripts/run-yarn-sequence.js --require-browser=chrome test:functionalChromeParallel test:functionalChrome:combine:reports test:functionalChrome:cucumber:combineParallelReport", - "test:Edge": "node scripts/run-yarn-sequence.js --require-browser=edge test:functionalEdgeParallel test:functionalEdge:combine:reports test:functionalEdge:cucumber:combineParallelReport", - "test:Firefox": "node scripts/run-yarn-sequence.js --require-browser=firefox test:functionalFirefoxParallel test:functionalFirefox:combine:reports test:functionalFirefox:cucumber:combineParallelReport", - "test:opalComponent": "yarn reset-test-outputs; yarn test:opalComponent:noReset", - "test:opalComponent:noReset": "BROWSER=$(node scripts/resolve-browser.js); CODE=0; BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --component --spec 'cypress/component/**/**.cy.ts' --config \"screenshotsFolder=functional-output/screenshots/$BROWSER/component\" --reporter-options \"mochaFile=functional-output/prod/$BROWSER/component/component-test-output-[hash].xml\" || CODE=$?; BROWSER_TO_RUN=$BROWSER yarn test:component:htmlReport; exit $CODE", - "test:component:htmlReport": "node scripts/build-component-report.js", - "test:opalComponentParallel": "yarn reset-test-outputs; BROWSER=$(node scripts/resolve-browser.js); TEST_STAGE=component BROWSER_TO_RUN=$BROWSER cypress-parallel -s test:opalComponent:noReset -d 'cypress/component/**/**.cy.ts' -m false -t 3 -w 'cypress/parallel/weights/component-parallel-weights.json' -r mocha-junit-reporter -o \"mochaFile=functional-output/prod/$BROWSER/component/component-test-output-[hash].xml\"", - "test:functionalOpalParallel": "BROWSER=$(node scripts/resolve-browser.js); CYPRESS_messagesEnabled=true TEST_STAGE=functional BROWSER_TO_RUN=$BROWSER cypress-parallel -s test:functionalOpal -d $TEST_SPECS -m false -t 3 -w 'cypress/parallel/weights/functional-parallel-weights.json' -r mocha-junit-reporter -o \"mochaFile=functional-output/prod/$BROWSER/opal-mode-test-output-[hash].xml\"", - "test:functionalOpalParallel:tagged": "BROWSER=$(node scripts/resolve-browser.js); CYPRESS_messagesEnabled=true TEST_STAGE=functional BROWSER_TO_RUN=$BROWSER cypress-parallel -s test:functionalOpal:tagged -d $TEST_SPECS -m false -t 3 -w 'cypress/parallel/weights/functional-parallel-weights.json' -r mocha-junit-reporter -o \"mochaFile=functional-output/prod/$BROWSER/opal-mode-test-output-[hash].xml\"", - "test:functionalOpal:tagged": "BROWSER=$(node scripts/resolve-browser.js); TEST_MODE=OPAL BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --env \"TAGS=$TAGS\" --spec 'cypress/e2e/functional/opal/**/*.feature' --config \"screenshotsFolder=functional-output/screenshots/$BROWSER\" --reporter-options \"mochaFile=functional-output/prod/$BROWSER/opal-mode-test-output-[hash].xml\"", - "test:functionalChromeParallel": "CYPRESS_messagesEnabled=true TEST_STAGE=functional cypress-parallel -s test:functionalChrome -d $TEST_SPECS -m false -t 3 -w 'cypress/parallel/weights/functional-parallel-weights.json' -r mocha-junit-reporter -o 'mochaFile=functional-output/prod/chrome/opal-mode-test-output-[hash].xml'", - "test:functionalEdgeParallel": "CYPRESS_messagesEnabled=true TEST_STAGE=functional cypress-parallel -s test:functionalEdge -d $TEST_SPECS -m false -t 3 -w 'cypress/parallel/weights/functional-parallel-weights.json' -r mocha-junit-reporter -o 'mochaFile=functional-output/prod/edge/opal-mode-test-output-[hash].xml'", - "test:functionalFirefoxParallel": "CYPRESS_messagesEnabled=true TEST_STAGE=functional cypress-parallel -s test:functionalFirefox -d $TEST_SPECS -m false -t 3 -w 'cypress/parallel/weights/functional-parallel-weights.json' -r mocha-junit-reporter -o 'mochaFile=functional-output/prod/firefox/opal-mode-test-output-[hash].xml'", - "test:functionalOpal": "BROWSER=$(node scripts/resolve-browser.js); TEST_MODE=OPAL BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --spec 'cypress/e2e/functional/opal/**/*.feature' --config \"screenshotsFolder=functional-output/screenshots/$BROWSER\" --reporter-options \"mochaFile=functional-output/prod/$BROWSER/opal-mode-test-output-[hash].xml\"", - "test:functionalChrome": "TEST_MODE=OPAL BROWSER_TO_RUN=chrome cypress run --browser chrome --spec 'cypress/e2e/functional/opal/**/*.feature' --config screenshotsFolder='functional-output/screenshots/chrome' --reporter-options 'mochaFile=functional-output/prod/chrome/opal-mode-test-output-[hash].xml'", - "test:functionalEdge": "TEST_MODE=OPAL BROWSER_TO_RUN=edge cypress run --browser edge --spec 'cypress/e2e/functional/opal/**/*.feature' --config screenshotsFolder='functional-output/screenshots/edge' --reporter-options 'mochaFile=functional-output/prod/edge/opal-mode-test-output-[hash].xml'", - "test:functionalFirefox": "TEST_MODE=OPAL BROWSER_TO_RUN=firefox cypress run --browser firefox --spec 'cypress/e2e/functional/opal/**/*.feature' --config screenshotsFolder='functional-output/screenshots/firefox' --reporter-options 'mochaFile=functional-output/prod/firefox/opal-mode-test-output-[hash].xml'", - "test:functionalOpalRecord": "TEST_MODE=OPAL BROWSER_TO_RUN=edge CYPRESS_VIDEO=true cypress run --browser edge --spec 'cypress/e2e/functional/opal/**/PO-530*.feature' --config videosFolder='functional-output/videos' screenshotsFolder='functional-output/screenshots/edge' --reporter-options 'mochaFile=functional-output/prod/edge/opal-mode-test-output-[hash].xml'", - "test:functionalLegacy": "BROWSER=$(node scripts/resolve-browser.js); CYPRESS_messagesEnabled=true TEST_STAGE=functional TEST_MODE=LEGACY BROWSER_TO_RUN=$BROWSER cypress run --browser $BROWSER --spec 'cypress/e2e/functional/legacy/**/*.feature' --config \"screenshotsFolder=functional-output/screenshots/$BROWSER/legacy\" --reporter-options \"mochaFile=functional-output/prod/$BROWSER/legacy/legacy-mode-test-output-[hash].xml\"", - "test:functional:combine:reports": "BROWSER=$(node scripts/resolve-browser.js); jrm functional-output/prod/${BROWSER}/${BROWSER}-test-result.xml \"functional-output/prod/${BROWSER}/*.xml\" && cp functional-output/prod/${BROWSER}/${BROWSER}-test-result.xml functional-output/prod/test-result.xml", - "test:functional:cucumber:combineParallelReport": "node scripts/build-cucumber-report.js functional", - "test:functional:cucumber:jsonReport": "BROWSER=$(node scripts/resolve-browser.js); yarn cucumber-merge-messages functional-output/prod/$BROWSER/cucumber/*.ndjson > functional-output/prod/$BROWSER/cucumber/$BROWSER-report.ndjson; mkdir -p functional-output/zephyr && yarn cucumber-json-formatter < functional-output/prod/$BROWSER/cucumber/$BROWSER-report.ndjson > functional-output/zephyr/cucumber-report.json", - "test:functional:cucumber:htmlReport": "BROWSER=$(node scripts/resolve-browser.js); yarn cucumber-html-formatter < functional-output/prod/$BROWSER/cucumber/$BROWSER-report.ndjson > functional-output/prod/$BROWSER/cucumber/$BROWSER-report.html", - "test:functionalChrome:combine:reports": "jrm functional-output/prod/chrome/chrome-test-result.xml \"functional-output/prod/chrome/*.xml\"", - "test:functionalEdge:combine:reports": "jrm functional-output/prod/edge/edge-test-result.xml \"functional-output/prod/edge/*.xml\"", - "test:functionalChrome:cucumber:combineParallelReport": "node scripts/build-cucumber-report.js functional --browser=chrome", - "test:functionalEdge:cucumber:combineParallelReport": "node scripts/build-cucumber-report.js functional --browser=edge", - "test:functionalEdge:cucumber:jsonReport": "yarn cucumber-merge-messages functional-output/prod/edge/cucumber/*.ndjson > functional-output/prod/edge/cucumber/edge-report.ndjson; mkdir -p functional-output/zephyr && yarn cucumber-json-formatter < functional-output/prod/edge/cucumber/edge-report.ndjson > functional-output/zephyr/cucumber-report.json", - "test:functionalEdge:cucumber:htmlReport": "yarn cucumber-html-formatter < functional-output/prod/edge/cucumber/edge-report.ndjson > functional-output/prod/edge/cucumber/edge-report.html", - "test:functionalFirefox:combine:reports": "jrm functional-output/prod/firefox/firefox-test-result.xml \"functional-output/prod/firefox/*.xml\"", - "test:functionalFirefox:cucumber:combineParallelReport": "node scripts/build-cucumber-report.js functional --browser=firefox", - "test:functionalFirefox:cucumber:jsonReport": "yarn cucumber-merge-messages functional-output/prod/firefox/cucumber/*.ndjson > functional-output/prod/firefox/cucumber/firefox-report.ndjson; mkdir -p functional-output/zephyr && yarn cucumber-json-formatter < functional-output/prod/firefox/cucumber/firefox-report.ndjson > functional-output/zephyr/cucumber-report.json", - "test:functionalFirefox:cucumber:htmlReport": "yarn cucumber-html-formatter < functional-output/prod/firefox/cucumber/firefox-report.ndjson > functional-output/prod/firefox/cucumber/firefox-report.html", - "test:functionalLegacy:combine:reports": "BROWSER=$(node scripts/resolve-browser.js); jrm functional-output/prod/${BROWSER}/legacy/legacy-test-result.xml \"functional-output/prod/${BROWSER}/legacy/*.xml\"", - "test:functionalLegacy:cucumber:combineParallelReport": "node scripts/build-cucumber-report.js legacy", - "test:functionalLegacy:cucumber:jsonReport": "BROWSER=$(node scripts/resolve-browser.js); yarn cucumber-merge-messages functional-output/prod/$BROWSER/legacy/cucumber/*.ndjson > functional-output/prod/$BROWSER/legacy/cucumber/legacy-report.ndjson; mkdir -p functional-output/zephyr && yarn cucumber-json-formatter < functional-output/prod/$BROWSER/legacy/cucumber/legacy-report.ndjson > functional-output/zephyr/cucumber-report.json", - "test:functionalLegacy:cucumber:htmlReport": "BROWSER=$(node scripts/resolve-browser.js); yarn cucumber-html-formatter < functional-output/prod/$BROWSER/legacy/cucumber/legacy-report.ndjson > functional-output/prod/$BROWSER/legacy/cucumber/legacy-report.html", - "test:dev:functional:combine:reports": "BROWSER=$(node scripts/resolve-browser.js); jrm functional-output/dev/${BROWSER}/${BROWSER}-test-result.xml \"functional-output/dev/${BROWSER}/*.xml\" && cp functional-output/dev/${BROWSER}/${BROWSER}-test-result.xml functional-output/dev/test-result.xml", - "test:fullfunctional": "yarn test:opalComponent ; yarn test:functional", - "test:functional:uat-legacy": "TAGS=@UAT-Technical DEV_DEFAULT_APP_MODE=legacy yarn test:functional", + "test:smoke": "node scripts/run-test-suite.js smoke", + "test:smoke:serial": "node scripts/run-test-suite.js smoke --serial", + "test:smoke:parallel": "node scripts/run-test-suite.js smoke --parallel", + "test:smoke:leaf": "node scripts/run-test-suite.js smoke --serial --no-reports --no-reset", + "test:functional": "node scripts/run-test-suite.js functional", + "test:functional:serial": "node scripts/run-test-suite.js functional --serial", + "test:functional:parallel": "node scripts/run-test-suite.js functional --parallel", + "test:functional:leaf": "node scripts/run-test-suite.js functional --serial --no-reports --no-reset", + "test:Chrome": "yarn test:functional --browser=chrome --parallel", + "test:Edge": "yarn test:functional --browser=edge --parallel", + "test:Firefox": "yarn test:functional --browser=firefox --parallel", + "test:component": "node scripts/run-test-suite.js component", + "test:component:parallel": "node scripts/run-test-suite.js component --parallel", + "test:component:leaf": "node scripts/run-test-suite.js component --serial --no-reports --no-reset", + "test:functionalOpalVideo": "CYPRESS_VIDEO=true node scripts/run-test-suite.js functional --mode=opal --browser=edge --serial --spec 'cypress/e2e/functional/opal/**/PO-530*.feature' --config 'videosFolder=functional-output/videos,screenshotsFolder=functional-output/screenshots/edge'", + "test:fullfunctional": "node scripts/run-test-suite.js fullfunctional", "dev:ssr": "ng run opal-frontend:serve-ssr", "dev:local-lib:ssr": "yarn run import:local:common-ui-lib && yarn run import:local:common-node-lib && ng run opal-frontend:serve-ssr", "serve:ssr": "node dist/opal-frontend/server/main.js", @@ -85,7 +49,8 @@ "audit:save": "yarn npm audit --recursive --environment production --json > yarn-audit-known-issues", "postinstall": "patch-package", "check:steps": "cypress run --env cucumberOnlyMissingSteps=true", - "test:functional:tags": "node scripts/run-functional.js --tags", + "test:functional:tags": "node scripts/run-test-suite.js functional --tags", + "test:functional:uat_legacy": "TAGS=@UAT-Technical DEV_DEFAULT_APP_MODE=legacy LEGACY_ENABLED=true yarn test:functional:tags", "find:duplicate:scenarios": "FEATURE_GLOB=\"cypress/e2e/functional/opal/features\" node scripts/find-duplicate-scenarios.js", "reset-test-outputs": "tsx ./scripts/reset-test-outputs.ts", "zephyr:cypress:jira-create": "tsx ./scripts/run-zephyr.ts --action-type=CREATE_TICKETS --process-type=CYPRESS_JSON_REPORT --report-path=functional-output/zephyr/cypress-report-1.json", @@ -97,9 +62,9 @@ "zephyr:cucumber:smoke:jira-create": "tsx ./scripts/run-zephyr.ts --action-type=CREATE_TICKETS --process-type=CUCUMBER_JSON_REPORT --report-path=smoke-output/zephyr/cucumber-report.json", "zephyr:cucumber:smoke:jira-update": "tsx ./scripts/run-zephyr.ts --action-type=UPDATE_TICKETS --process-type=CUCUMBER_JSON_REPORT --report-path=smoke-output/zephyr/cucumber-report.json", "zephyr:cucumber:smoke:jira-execute": "tsx ./scripts/run-zephyr.ts --action-type=CREATE_EXECUTION --process-type=CUCUMBER_JSON_REPORT --report-path=smoke-output/zephyr/cucumber-report.json", - "zephyr:test:opalComponent": "CODE=0; yarn reset-test-outputs; yarn test:opalComponent:noReset || CODE=$?; yarn zephyr:cypress:jira-execute; exit $CODE", - "zephyr:test:functional": "CODE=0; yarn reset-test-outputs; yarn test:functional || CODE=$?; yarn zephyr:cucumber:functional:jira-execute; exit $CODE", - "zephyr:test:smoke": "CODE=0; yarn reset-test-outputs; yarn test:smoke || CODE=$?; yarn zephyr:cucumber:smoke:jira-execute; exit $CODE" + "zephyr:test:component": "CODE=0; yarn test:component || CODE=$?; yarn zephyr:cypress:jira-execute; exit $CODE", + "zephyr:test:functional": "CODE=0; yarn test:functional --reset || CODE=$?; yarn zephyr:cucumber:functional:jira-execute; exit $CODE", + "zephyr:test:smoke": "CODE=0; yarn test:smoke --reset || CODE=$?; yarn zephyr:cucumber:smoke:jira-execute; exit $CODE" }, "private": true, "dependencies": { diff --git a/scripts/build-component-report.js b/scripts/build-component-report.js index 5cc167be3c..e48b0c05e1 100644 --- a/scripts/build-component-report.js +++ b/scripts/build-component-report.js @@ -4,8 +4,8 @@ /** * @fileoverview Builds the Jenkins HTML report for Cypress component test results. * @description Used after component runs to merge Mochawesome JSON files for the selected browser and to skip - * report generation cleanly when no component JSON artifacts were produced. This now reads the reporter's `.jsons` - * output folder directly so Jenkins continues to publish component reports after the default-browser fallback work. + * report generation cleanly when no component JSON artifacts were produced. This reads the reporter's `.jsons` + * output folder directly from the consolidated component artifact tree. */ const fs = require('node:fs'); @@ -36,11 +36,10 @@ function parseBrowser(args) { function resolvePaths(browser) { return { inputDirs: [ - path.join('functional-output', 'component-report', browser, '.jsons'), - path.join('functional-output', 'component-report', '.jsons'), - path.join('functional-output', 'component-report', browser), + path.join('functional-output', 'component', browser, 'json', '.jsons'), + path.join('functional-output', 'component', browser, 'json'), ], - htmlDir: path.join('functional-output', 'component-html', browser), + htmlDir: path.join('functional-output', 'component', browser, 'html'), }; } diff --git a/scripts/build-cucumber-report.js b/scripts/build-cucumber-report.js index 67cdc9dabb..95eb277dc3 100644 --- a/scripts/build-cucumber-report.js +++ b/scripts/build-cucumber-report.js @@ -3,9 +3,9 @@ /** * @fileoverview Builds merged Cucumber ndjson, Zephyr JSON, and Jenkins HTML reports from raw Cypress outputs. - * @description Used by smoke, functional, and legacy report-combine scripts after parallel test execution because - * the default-browser fallback changes exposed a brittle shell-based report path that could reuse stale merged ndjson - * files or mask combine failures, leaving Jenkins to publish blank reports for the selected browser. + * @description Used by smoke and functional report-combine steps for both OPAL and Legacy modes after parallel or + * serial Cypress execution. This avoids brittle shell-based merge commands and keeps the generated HTML, merged + * ndjson, and Zephyr JSON outputs aligned with the selected suite/browser/mode combination. */ const fs = require('node:fs'); @@ -23,11 +23,12 @@ const { /** * Parse CLI arguments into a simple key-value object. * @param {string[]} args - * @returns {{ suite: string, browser: string }} + * @returns {{ suite: string, browser: string, mode: string }} */ function parseArgs(args) { const options = { browser: '', + mode: '', suite: '', }; @@ -37,11 +38,25 @@ function parseArgs(args) { continue; } + if (arg.startsWith('--mode=')) { + options.mode = arg.split('=')[1].trim().toLowerCase(); + continue; + } + if (!options.suite) { options.suite = arg.trim().toLowerCase(); } } + if (options.suite === 'legacy') { + options.suite = 'functional'; + options.mode = 'legacy'; + } + + if (!options.mode) { + options.mode = 'opal'; + } + return options; } @@ -49,33 +64,41 @@ function parseArgs(args) { * Resolve report paths for the requested suite/browser combination. * @param {string} suite * @param {string} browser + * @param {string} mode * @returns {{ inputDir: string, htmlPath: string, mergedPath: string, zephyrJsonPath: string }} */ -function resolveReportPaths(suite, browser) { - switch (suite) { - case 'functional': +function resolveReportPaths(suite, browser, mode) { + switch (`${suite}:${mode}`) { + case 'functional:opal': return { inputDir: path.join('functional-output', 'prod', browser, 'cucumber'), mergedPath: path.join('functional-output', 'prod', browser, 'cucumber', `${browser}-report.ndjson`), htmlPath: path.join('functional-output', 'prod', browser, 'cucumber', `${browser}-report.html`), zephyrJsonPath: path.join('functional-output', 'zephyr', 'cucumber-report.json'), }; - case 'legacy': + case 'functional:legacy': return { inputDir: path.join('functional-output', 'prod', browser, 'legacy', 'cucumber'), mergedPath: path.join('functional-output', 'prod', browser, 'legacy', 'cucumber', 'legacy-report.ndjson'), htmlPath: path.join('functional-output', 'prod', browser, 'legacy', 'cucumber', 'legacy-report.html'), zephyrJsonPath: path.join('functional-output', 'zephyr', 'cucumber-report.json'), }; - case 'smoke': + case 'smoke:opal': return { inputDir: path.join('smoke-output', 'prod', browser, 'cucumber'), mergedPath: path.join('smoke-output', 'prod', browser, 'cucumber', 'smoke-report.ndjson'), htmlPath: path.join('smoke-output', 'prod', browser, 'cucumber', 'smoke-report.html'), zephyrJsonPath: path.join('smoke-output', 'zephyr', 'cucumber-report.json'), }; + case 'smoke:legacy': + return { + inputDir: path.join('smoke-output', 'prod', browser, 'legacy', 'cucumber'), + mergedPath: path.join('smoke-output', 'prod', browser, 'legacy', 'cucumber', 'legacy-report.ndjson'), + htmlPath: path.join('smoke-output', 'prod', browser, 'legacy', 'cucumber', 'legacy-report.html'), + zephyrJsonPath: path.join('smoke-output', 'zephyr', 'cucumber-report.json'), + }; default: - throw new Error(`Unsupported Cucumber report suite: ${suite || '(empty)'}`); + throw new Error(`Unsupported Cucumber report suite/mode: ${suite || '(empty)'}/${mode || '(empty)'}`); } } @@ -177,17 +200,18 @@ async function writeHtmlReport(outputPath, messages) { } async function main() { - const { suite, browser: requestedBrowser } = parseArgs(process.argv.slice(2)); + const { suite, browser: requestedBrowser, mode } = parseArgs(process.argv.slice(2)); if (!suite) { - throw new Error('A report suite must be provided: smoke, functional, or legacy'); + throw new Error('A report suite must be provided: smoke or functional'); } const browser = requestedBrowser || resolveGenericBrowser(process.env.BROWSER_TO_RUN); - const reportPaths = resolveReportPaths(suite, browser); + const reportPaths = resolveReportPaths(suite, browser, mode); const { messages, sourceFiles } = loadMessages(reportPaths.inputDir, reportPaths.mergedPath); console.log(`[build-cucumber-report] suite=${suite}`); + console.log(`[build-cucumber-report] mode=${mode}`); console.log(`[build-cucumber-report] browser=${browser}`); console.log(`[build-cucumber-report] inputs=${sourceFiles.length}`); diff --git a/scripts/run-functional.js b/scripts/run-functional.js deleted file mode 100644 index a9de83fb92..0000000000 --- a/scripts/run-functional.js +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env node -/** - * @fileoverview Orchestrates Opal functional test runs with consistent browser selection and report combining. - * @description Used by `yarn test:functional` and `yarn test:functional:tags` in both local and CI environments. - */ -'use strict'; - -const { spawnSync } = require('node:child_process'); - -const args = process.argv.slice(2); -const withTags = args.includes('--tags'); - -/** - * Resolve the browser under test, defaulting to edge when unset. - * @returns {string} Normalized browser name. - */ -const resolveBrowser = () => { - const raw = (process.env.BROWSER_TO_RUN || '').trim().toLowerCase(); - return raw || 'edge'; -}; - -const browser = resolveBrowser(); -process.env.BROWSER_TO_RUN = browser; -process.env.TEST_SPECS = (process.env.TEST_SPECS || '').trim() || 'cypress/e2e/functional/opal/**/*.feature'; -console.log(`[run-functional] TEST_SPECS=${process.env.TEST_SPECS}`); - -if (withTags) { - if (!process.env.CYPRESS_TAGS && process.env.TAGS) { - process.env.CYPRESS_TAGS = process.env.TAGS; - } - process.env.CYPRESS_filterSpecs = process.env.CYPRESS_filterSpecs || 'true'; - process.env.CYPRESS_filterSpecsMixedMode = process.env.CYPRESS_filterSpecsMixedMode || 'hide'; -} - -const yarnCmd = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; - -/** - * Execute a yarn script and return its exit code. - * @param {string} scriptName - The yarn script name to execute. - * @returns {number} Exit status for the yarn script. - */ -const runYarn = (scriptName) => { - const result = spawnSync(yarnCmd, [scriptName], { stdio: 'inherit', env: process.env }); - if (typeof result.status === 'number') { - return result.status; - } - return result.error ? 1 : 0; -}; - -const parallelScript = withTags ? 'test:functionalOpalParallel:tagged' : 'test:functionalOpalParallel'; - -const testExitCode = runYarn(parallelScript); -const combineReportsExitCode = runYarn('test:functional:combine:reports'); -const combineCucumberExitCode = runYarn('test:functional:cucumber:combineParallelReport'); - -process.exit(testExitCode || combineReportsExitCode || combineCucumberExitCode || 0); diff --git a/scripts/run-smoke.js b/scripts/run-smoke.js deleted file mode 100644 index ebad19acb9..0000000000 --- a/scripts/run-smoke.js +++ /dev/null @@ -1,90 +0,0 @@ -#!/usr/bin/env node -'use strict'; - -/** - * @fileoverview Orchestrates smoke test runs with tag-aware report handling. - * @description Used by `yarn test:smoke` so tagged reruns that match no smoke scenarios skip report merging cleanly - * instead of failing on missing JUnit or Cucumber artifacts. - */ - -const fs = require('node:fs'); -const path = require('node:path'); -const { spawnSync } = require('node:child_process'); -const { resolveGenericBrowser } = require('./browser-support'); - -const browser = resolveGenericBrowser(process.env.BROWSER_TO_RUN); -const tagExpression = (process.env.CYPRESS_TAGS || process.env.TAGS || '').trim(); -const hasTagFiltering = Boolean(tagExpression); -const skipSmoke = - String(process.env.SKIP_SMOKE || '') - .trim() - .toLowerCase() === 'true'; -const yarnCmd = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; - -process.env.BROWSER_TO_RUN = browser; - -if (skipSmoke) { - console.log('[run-smoke] skipping smoke stage because run_tag reruns target functional coverage only.'); - process.exit(0); -} - -/** - * Execute a yarn script and return its exit status. - * @param {string} scriptName - * @returns {number} - */ -function runYarn(scriptName) { - const result = spawnSync(yarnCmd, [scriptName], { env: process.env, stdio: 'inherit' }); - - if (typeof result.status === 'number') { - return result.status; - } - - return result.error ? 1 : 0; -} - -/** - * Return whether the given directory contains files with the provided extension. - * @param {string} dirPath - * @param {string} extension - * @returns {boolean} - */ -function hasFilesWithExtension(dirPath, extension) { - if (!fs.existsSync(dirPath)) { - return false; - } - - return fs.readdirSync(dirPath).some((filename) => filename.endsWith(extension)); -} - -/** - * Return whether smoke artifacts were produced for the selected browser. - * @param {string} resolvedBrowser - * @returns {{ hasXml: boolean, hasNdjson: boolean }} - */ -function getSmokeArtifacts(resolvedBrowser) { - const smokeDir = path.join('smoke-output', 'prod', resolvedBrowser); - const cucumberDir = path.join(smokeDir, 'cucumber'); - - return { - hasXml: hasFilesWithExtension(smokeDir, '.xml'), - hasNdjson: hasFilesWithExtension(cucumberDir, '.ndjson'), - }; -} - -const testExitCode = runYarn('test:smokeOpalParallel'); -const { hasXml, hasNdjson } = getSmokeArtifacts(browser); - -if (!hasXml && !hasNdjson) { - if (testExitCode === 0 && hasTagFiltering) { - console.log(`[run-smoke] no smoke scenarios matched TAGS=${tagExpression}; skipping smoke report generation.`); - process.exit(0); - } - - process.exit(testExitCode); -} - -const combineReportsExitCode = hasXml ? runYarn('test:smoke:combine:reports') : 0; -const combineCucumberExitCode = hasNdjson ? runYarn('test:smoke:cucumber:combineParallelReport') : 0; - -process.exit(testExitCode || combineReportsExitCode || combineCucumberExitCode || 0); diff --git a/scripts/run-test-suite.js b/scripts/run-test-suite.js new file mode 100644 index 0000000000..7c4d1e2b0c --- /dev/null +++ b/scripts/run-test-suite.js @@ -0,0 +1,629 @@ +#!/usr/bin/env node +'use strict'; + +/** + * @fileoverview Runs Cypress suites with a consistent suite/browser/mode/parallel matrix. + * @description Handles component, smoke, functional, and fullfunctional entry points so top-level scripts always + * generate the expected reports after execution instead of scattering combine logic across many package scripts. + */ + +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const { normalizeBrowser, resolveGenericBrowser, requireInstalledBrowser } = require('./browser-support'); + +const yarnCommand = process.platform === 'win32' ? 'yarn.cmd' : 'yarn'; +const nodeCommand = process.execPath; + +/** + * Parse CLI arguments into runner options and passthrough Cypress arguments. + * @param {string[]} argv + * @returns {{ + * suite: string, + * browser: string, + * mode: string, + * parallel: boolean | null, + * tags: boolean, + * reset: boolean | null, + * noReports: boolean, + * passthroughArgs: string[], + * }} + */ +function parseArgs(argv) { + const options = { + suite: '', + browser: '', + mode: '', + parallel: null, + tags: false, + reset: null, + noReports: false, + passthroughArgs: [], + }; + + const valueFlags = new Set(['--spec', '--config', '--env', '--reporter', '--reporter-options']); + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + + if (!options.suite && !arg.startsWith('--')) { + options.suite = arg.trim().toLowerCase(); + continue; + } + + if (arg === '--parallel') { + options.parallel = true; + continue; + } + + if (arg === '--serial') { + options.parallel = false; + continue; + } + + if (arg === '--tags') { + options.tags = true; + continue; + } + + if (arg === '--reset') { + options.reset = true; + continue; + } + + if (arg === '--no-reset') { + options.reset = false; + continue; + } + + if (arg === '--no-reports') { + options.noReports = true; + continue; + } + + if (arg.startsWith('--browser=')) { + options.browser = arg.split('=')[1] || ''; + continue; + } + + if (arg === '--browser') { + options.browser = argv[index + 1] || ''; + index += 1; + continue; + } + + if (arg.startsWith('--mode=')) { + options.mode = arg.split('=')[1] || ''; + continue; + } + + if (arg === '--mode') { + options.mode = argv[index + 1] || ''; + index += 1; + continue; + } + + options.passthroughArgs.push(arg); + + if (valueFlags.has(arg) && index + 1 < argv.length) { + options.passthroughArgs.push(argv[index + 1]); + index += 1; + } + } + + return options; +} + +/** + * Normalize the requested test mode. + * @param {string} mode + * @returns {'opal' | 'legacy'} + */ +function normalizeMode(mode) { + const normalizedMode = String(mode || process.env.TEST_MODE || 'OPAL') + .trim() + .toLowerCase(); + + if (!normalizedMode || normalizedMode === 'opal') { + return 'opal'; + } + + if (normalizedMode === 'legacy') { + return 'legacy'; + } + + throw new Error(`Unsupported TEST_MODE requested: ${mode}`); +} + +/** + * Resolve the requested browser, validating explicit requests. + * @param {string} browser + * @returns {string} + */ +function resolveBrowser(browser) { + const normalizedBrowser = normalizeBrowser(browser); + + if (normalizedBrowser) { + return requireInstalledBrowser(normalizedBrowser); + } + + return resolveGenericBrowser(process.env.BROWSER_TO_RUN); +} + +/** + * Execute a child command and return its exit code. + * @param {string} command + * @param {string[]} args + * @param {NodeJS.ProcessEnv} env + * @returns {number} + */ +function runCommand(command, args, env) { + const result = spawnSync(command, args, { + env, + stdio: 'inherit', + }); + + if (typeof result.status === 'number') { + return result.status; + } + + return result.error ? 1 : 0; +} + +/** + * Execute a locally installed CLI via `yarn exec`. + * @param {string} toolName + * @param {string[]} args + * @param {NodeJS.ProcessEnv} env + * @returns {number} + */ +function runYarnTool(toolName, args, env) { + return runCommand(yarnCommand, ['exec', toolName, ...args], env); +} + +/** + * Remove generated Cypress outputs. + */ +function resetOutputs() { + for (const outputDir of ['functional-output', 'smoke-output']) { + if (fs.existsSync(outputDir)) { + fs.rmSync(outputDir, { recursive: true, force: true }); + } + } +} + +/** + * Return a sorted list of files with the requested extension. + * @param {string} directory + * @param {string} extension + * @returns {string[]} + */ +function listFiles(directory, extension) { + if (!fs.existsSync(directory)) { + return []; + } + + return fs + .readdirSync(directory) + .filter((filename) => filename.endsWith(extension)) + .map((filename) => path.join(directory, filename)) + .sort(); +} + +/** + * Return whether the argument list already overrides a Cypress option. + * @param {string[]} args + * @param {string} optionName + * @returns {boolean} + */ +function hasOption(args, optionName) { + return args.some((arg) => arg === optionName || arg.startsWith(`${optionName}=`)); +} + +/** + * Apply shared environment settings for the selected suite. + * @param {NodeJS.ProcessEnv} env + * @param {{ mode: 'opal' | 'legacy', suite: string, tags: boolean }} context + */ +function applyRunnerEnv(env, context) { + env.TEST_MODE = context.mode.toUpperCase(); + + if (context.mode === 'legacy') { + env.LEGACY_ENABLED = 'true'; + } + + if (context.suite === 'functional' || context.suite === 'smoke') { + env.TEST_STAGE = context.suite; + } else if (context.suite === 'component') { + env.TEST_STAGE = 'component'; + } + + const tagExpression = String(env.CYPRESS_TAGS || env.TAGS || '').trim(); + if ((context.tags || tagExpression) && context.suite !== 'component') { + if (!env.CYPRESS_TAGS && env.TAGS) { + env.CYPRESS_TAGS = env.TAGS; + } + env.CYPRESS_filterSpecs = env.CYPRESS_filterSpecs || 'true'; + env.CYPRESS_filterSpecsMixedMode = env.CYPRESS_filterSpecsMixedMode || 'hide'; + } +} + +/** + * Return the effective default for parallel execution. + * @param {string} suite + * @param {boolean | null} requestedParallel + * @returns {boolean} + */ +function resolveParallelMode(suite, requestedParallel) { + if (requestedParallel !== null) { + return requestedParallel; + } + + return suite === 'smoke' || suite === 'functional'; +} + +/** + * Resolve suite configuration for execution and reporting. + * @param {{ suite: string, mode: 'opal' | 'legacy', browser: string }} options + * @returns {object} + */ +function buildSuiteConfig(options) { + const { suite, mode, browser } = options; + const isLegacy = mode === 'legacy'; + + if (suite === 'component') { + return { + suite, + mode, + browser, + outputRoot: 'functional-output', + leafScript: 'test:component:leaf', + specPattern: 'cypress/component/**/**.cy.ts', + screenshotsFolder: `functional-output/component/${browser}/screenshots`, + componentJsonDir: `functional-output/component/${browser}/json`, + junitDir: `functional-output/component/${browser}/junit`, + junitMochaFile: `functional-output/component/${browser}/junit/component-test-output-[hash].xml`, + threads: 3, + weightsJson: 'cypress/parallel/weights/component-parallel-weights.json', + isComponent: true, + isLegacy: false, + combinedXmlPath: '', + combinedXmlCopyPath: '', + cucumberDir: '', + cucumberSuite: '', + }; + } + + const outputRoot = suite === 'smoke' ? 'smoke-output' : 'functional-output'; + const specPattern = + suite === 'smoke' + ? isLegacy + ? 'cypress/e2e/smoke/legacy/**/*.feature' + : 'cypress/e2e/smoke/opal/**/*.feature' + : isLegacy + ? 'cypress/e2e/functional/legacy/**/*.feature' + : (process.env.TEST_SPECS || '').trim() || 'cypress/e2e/functional/opal/**/*.feature'; + + const screenshotsFolder = isLegacy + ? `${outputRoot}/screenshots/${browser}/legacy` + : `${outputRoot}/screenshots/${browser}`; + const junitDir = isLegacy ? `${outputRoot}/prod/${browser}/legacy` : `${outputRoot}/prod/${browser}`; + const junitMochaFile = isLegacy + ? `${junitDir}/legacy-mode-test-output-[hash].xml` + : `${junitDir}/opal-mode-test-output-[hash].xml`; + const combinedXmlPath = isLegacy + ? `${junitDir}/legacy-test-result.xml` + : `${outputRoot}/prod/${browser}/${browser}-test-result.xml`; + const combinedXmlCopyPath = isLegacy ? '' : `${outputRoot}/prod/test-result.xml`; + const cucumberDir = `${junitDir}/cucumber`; + const weightsJson = + suite === 'smoke' + ? 'cypress/parallel/weights/smoke-parallel-weights.json' + : isLegacy + ? '' + : 'cypress/parallel/weights/functional-parallel-weights.json'; + + return { + suite, + mode, + browser, + outputRoot, + leafScript: `test:${suite}:leaf`, + specPattern, + screenshotsFolder, + componentJsonDir: '', + junitDir, + junitMochaFile, + threads: suite === 'smoke' ? 2 : 3, + weightsJson, + isComponent: false, + isLegacy, + combinedXmlPath, + combinedXmlCopyPath, + cucumberDir, + cucumberSuite: suite, + }; +} + +/** + * Execute a suite serially via Cypress. + * @param {ReturnType} config + * @param {NodeJS.ProcessEnv} env + * @param {string[]} passthroughArgs + * @returns {number} + */ +function runSerialSuite(config, env, passthroughArgs) { + const commandArgs = ['run', '--browser', config.browser]; + + if (config.isComponent) { + commandArgs.push('--component'); + } + + if (!hasOption(passthroughArgs, '--spec')) { + commandArgs.push('--spec', config.specPattern); + } + + commandArgs.push('--config', `screenshotsFolder=${config.screenshotsFolder}`); + + if (!config.isComponent && !hasOption(passthroughArgs, '--reporter-options')) { + commandArgs.push('--reporter-options', `mochaFile=${config.junitMochaFile}`); + } + + commandArgs.push(...passthroughArgs); + + return runYarnTool('cypress', commandArgs, env); +} + +/** + * Create a cypress-parallel reporter config that preserves component HTML and Zephyr artifacts. + * @param {ReturnType} config + * @returns {string} + */ +function createComponentParallelReporterConfig(config) { + const reporterConfigPath = path.join(os.tmpdir(), `opal-component-parallel-${config.browser}.json`); + const reporterConfig = { + reporterEnabled: + 'cypress-parallel/json-stream.reporter.js,cypress-mochawesome-reporter,mocha-junit-reporter,@hmcts/zephyr-automation-nodejs/cypress/ZephyrReporter', + mochaJunitReporterReporterOptions: { + mochaFile: config.junitMochaFile, + toConsole: false, + }, + cypressMochawesomeReporterReporterOptions: { + reportDir: config.componentJsonDir, + overwrite: false, + html: false, + json: true, + }, + }; + + fs.writeFileSync(reporterConfigPath, JSON.stringify(reporterConfig, null, 2)); + return reporterConfigPath; +} + +/** + * Execute a suite in parallel via cypress-parallel. + * @param {ReturnType} config + * @param {NodeJS.ProcessEnv} env + * @param {string[]} passthroughArgs + * @returns {number} + */ +function runParallelSuite(config, env, passthroughArgs) { + const commandArgs = ['-s', config.leafScript, '-d', config.specPattern, '-m', 'false', '-t', String(config.threads)]; + + if (config.weightsJson) { + commandArgs.push('-w', config.weightsJson); + } + + if (config.isComponent) { + commandArgs.push('-p', createComponentParallelReporterConfig(config)); + } else { + commandArgs.push('-r', 'mocha-junit-reporter', '-o', `mochaFile=${config.junitMochaFile}`); + } + + if (passthroughArgs.length > 0) { + commandArgs.push('-a', passthroughArgs.join(' ')); + } + + return runYarnTool('cypress-parallel', commandArgs, env); +} + +/** + * Return whether a smoke run should be skipped entirely. + * @param {NodeJS.ProcessEnv} env + * @param {string} suite + * @returns {boolean} + */ +function shouldSkipSuite(env, suite) { + if (suite !== 'smoke') { + return false; + } + + return ( + String(env.SKIP_SMOKE || '') + .trim() + .toLowerCase() === 'true' + ); +} + +/** + * Merge JUnit XML files into the suite summary output. + * @param {ReturnType} config + * @param {NodeJS.ProcessEnv} env + * @returns {number} + */ +function mergeJUnitReports(config, env) { + const excludedFilename = path.basename(config.combinedXmlPath); + const xmlFiles = listFiles(config.junitDir, '.xml').filter( + (filePath) => path.basename(filePath) !== excludedFilename, + ); + + if (xmlFiles.length === 0) { + return 0; + } + + const exitCode = runYarnTool('jrm', [config.combinedXmlPath, ...xmlFiles], env); + if (exitCode !== 0) { + return exitCode; + } + + if (config.combinedXmlCopyPath) { + fs.mkdirSync(path.dirname(config.combinedXmlCopyPath), { recursive: true }); + fs.copyFileSync(config.combinedXmlPath, config.combinedXmlCopyPath); + } + + return 0; +} + +/** + * Build the suite's combined Cucumber report outputs. + * @param {ReturnType} config + * @param {NodeJS.ProcessEnv} env + * @returns {number} + */ +function buildCucumberReports(config, env) { + return runCommand( + nodeCommand, + ['scripts/build-cucumber-report.js', config.cucumberSuite, `--browser=${config.browser}`, `--mode=${config.mode}`], + env, + ); +} + +/** + * Build the component HTML report. + * @param {string} browser + * @param {NodeJS.ProcessEnv} env + * @returns {number} + */ +function buildComponentReport(browser, env) { + return runCommand(nodeCommand, ['scripts/build-component-report.js', `--browser=${browser}`], env); +} + +/** + * Return whether the selected suite produced JUnit or ndjson artifacts. + * @param {ReturnType} config + * @returns {{ hasNdjson: boolean, hasXml: boolean }} + */ +function getSuiteArtifacts(config) { + return { + hasXml: listFiles(config.junitDir, '.xml').length > 0, + hasNdjson: listFiles(config.cucumberDir, '.ndjson').length > 0, + }; +} + +/** + * Execute one suite and combine reports when appropriate. + * @param {string} suite + * @param {ReturnType} options + * @param {NodeJS.ProcessEnv} baseEnv + * @returns {number} + */ +function executeSuite(suite, options, baseEnv) { + const env = { ...baseEnv }; + const mode = normalizeMode(options.mode); + const browser = resolveBrowser(options.browser); + const config = buildSuiteConfig({ suite, mode, browser }); + + env.BROWSER_TO_RUN = browser; + applyRunnerEnv(env, { mode, suite, tags: options.tags }); + + if (options.reset === true || (options.reset === null && suite === 'component')) { + resetOutputs(); + } + + if (shouldSkipSuite(env, suite)) { + console.log('[run-test-suite] skipping smoke stage because SKIP_SMOKE=true.'); + return 0; + } + + const parallel = resolveParallelMode(suite, options.parallel); + const testExitCode = parallel + ? runParallelSuite(config, env, options.passthroughArgs) + : runSerialSuite(config, env, options.passthroughArgs); + + if (options.noReports) { + return testExitCode; + } + + if (config.isComponent) { + const reportExitCode = buildComponentReport(browser, env); + return testExitCode || reportExitCode || 0; + } + + const { hasXml, hasNdjson } = getSuiteArtifacts(config); + const hasTagFiltering = Boolean(String(env.CYPRESS_TAGS || env.TAGS || '').trim()); + + if (!hasXml && !hasNdjson) { + if (testExitCode === 0 && hasTagFiltering) { + console.log(`[run-test-suite] no ${suite} scenarios matched the active tag filter; skipping report generation.`); + return 0; + } + + return testExitCode; + } + + const combineXmlExitCode = hasXml ? mergeJUnitReports(config, env) : 0; + const cucumberExitCode = hasNdjson ? buildCucumberReports(config, env) : 0; + + return testExitCode || combineXmlExitCode || cucumberExitCode || 0; +} + +/** + * Execute the combined component + functional workflow. + * @param {ReturnType} options + * @param {NodeJS.ProcessEnv} baseEnv + * @returns {number} + */ +function executeFullFunctional(options, baseEnv) { + resetOutputs(); + + const componentParallel = options.parallel === true; + const functionalParallel = options.parallel === false ? false : true; + + const componentExitCode = executeSuite( + 'component', + { + ...options, + parallel: componentParallel, + reset: false, + }, + baseEnv, + ); + + const functionalExitCode = executeSuite( + 'functional', + { + ...options, + parallel: functionalParallel, + reset: false, + }, + baseEnv, + ); + + return componentExitCode || functionalExitCode || 0; +} + +function main() { + const options = parseArgs(process.argv.slice(2)); + + if (!options.suite) { + throw new Error('A suite is required: component, smoke, functional, or fullfunctional'); + } + + if (options.suite === 'fullfunctional') { + process.exit(executeFullFunctional(options, process.env)); + } + + if (!['component', 'smoke', 'functional'].includes(options.suite)) { + throw new Error(`Unsupported suite requested: ${options.suite}`); + } + + process.exit(executeSuite(options.suite, options, process.env)); +} + +try { + main(); +} catch (error) { + console.error(error.message); + process.exit(1); +}