diff --git a/.gitea/workflows/quality.yml b/.gitea/workflows/quality.yml index c368fc3..d07bac2 100644 --- a/.gitea/workflows/quality.yml +++ b/.gitea/workflows/quality.yml @@ -16,4 +16,13 @@ jobs: cache: npm - run: npm ci - run: npm run quality + - run: npx playwright install --with-deps chromium + - run: npm run test:browser:ci + - name: Preserve browser failure evidence + if: failure() + uses: actions/upload-artifact@v4 + with: + name: forgeflow-browser-failure-evidence + path: artifacts/ + if-no-files-found: ignore - run: npm audit --omit=dev --audit-level=high diff --git a/.gitignore b/.gitignore index 0705472..66b9267 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ dist/ .DS_Store *.log coverage/ +artifacts/ +playwright-report/ .forgeflow/ diff --git a/SOURCE_MANIFEST.txt b/SOURCE_MANIFEST.txt index 5cbdcb9..3682913 100644 --- a/SOURCE_MANIFEST.txt +++ b/SOURCE_MANIFEST.txt @@ -1,9 +1,11 @@ ForgeFlow 0.10.0 source manifest SHA-256 BYTES PATH (The manifest excludes itself, dependencies and generated release artifacts.) -a6ce7703a16e24b307af98b06e99e7f6a4230d854bc6ed9b745510603a03926d 370 .gitea/workflows/quality.yml -755f4db7d76bfec0963ef051748a82810c0d58acd4ffd823aa6928a5167fceb4 58 .gitignore +cedceb71eb846d99c7c4019031833c1c7f93b84a1c6073aec7d2435dc744ca3d 703 .gitea/workflows/quality.yml +0182dfbbf5b9fc5e62f064f094f5412bf245f9a67ec712e8807f2ce4da443717 88 .gitignore f14b4987904bcb5814e4459a057ed4d20f58a633152288a761214dcd28780b56 3 .nvmrc +f2cdfd679f074475a515fc809607154fd355a60f96e3e69ba69a04c7092c1856 514823 artifacts/browser-report/index.html +91d1c43004802cd49950d78eb11c8fa7d05da8ffffe219a8b13b2f561bc00903 45 artifacts/browser/.last-run.json 89545860bd6f7566da81edc8328cd2a1ebf33e81a4b0dcf2cec74338c05e8cac 1753 build-windows.ps1 0970821475a4452aa19e447e9397a95db836791f16890a1a83fd748ac033dc86 8830 build/icon-128.png 09112c1425ca953d8dd8b2bcfd221e5a84b9f81752f7168f360e295030cbc8f2 521 build/icon-16.png @@ -15,41 +17,41 @@ ca32a76e708d565c4af659f0f4d2615fc32114c3f75aec1454862a3ed1e72c41 2263 25048ed854e8ce8fece115e555c98d25507b002f8019b6ae717b54604c868c50 46223 build/icon.ico 16efd2fca83004f781eae40ae0f706a004ce0bddf338dd087b8adf7eb10c1d84 85704 build/icon.png 5f4aca19a35cbcaffa1a6993ce96b7d66052ec2b286022f2af74594e8a310568 15712 CHANGELOG.md -04a3058e507c938447d35564e503cf89b76670d8ea45f5209db074dfa2625b91 303090 coverage/tmp/coverage-14228-1785341379444-0.json -0f86a35f2e6a48c0789263a4e36190760b0dfa45ce9a1128555b4dd77da2df5b 352006 coverage/tmp/coverage-14596-1785341377618-0.json -4ae93dacf27d8ec7e84bd00ab52398a0d99cecb25cf2244be8e6d89824c3d5db 345122 coverage/tmp/coverage-15236-1785341392510-0.json -2607418ea469b58eb6190e4cabde794c0773b7746afb0fdd2d401c99a353752a 358520 coverage/tmp/coverage-17880-1785341380606-0.json -ea98763aaacb035c1c75fca8f50784fc0cc89b2dedf2ddfa80c0eb32d633af27 342639 coverage/tmp/coverage-19480-1785341377315-0.json -fdaef396ca19f7e1f62ea7e6f2172fa615c7e7f26b157e80c896199f5ba3e77e 329358 coverage/tmp/coverage-19612-1785341379233-0.json -d2808be6bce7d7d3d4662def01522170b8cef6c802813db92ade8c27b335fbbe 346825 coverage/tmp/coverage-21808-1785341377499-0.json -7f788bab2eb9ada8e3c3f7fc1d6948f001018721eccb54e87b98a8916e30695b 299000 coverage/tmp/coverage-24368-1785341377951-0.json -d3584daefb19adb8e0e7cd3ca6ffc7f6992636a692a4a3b186da94281950aa8f 302346 coverage/tmp/coverage-25148-1785341378806-0.json -e4045202501f53544351ae8f0aba6cdcccef9d08d735f4d0c83634af5d3033b3 344499 coverage/tmp/coverage-29628-1785341379071-0.json -bf66bf3c80bfed0d145dc2a73363e47089a74858d3cece5dbef2756bfbfd623b 360893 coverage/tmp/coverage-31472-1785341379151-0.json -0d52bfc33883af8fccdc37c436edd95eb83f54c65de87fa09ad03719995574e4 335635 coverage/tmp/coverage-36800-1785341386017-0.json -bcb44d6e6369e3cb5fd5306a02097fadd44e6d85eeac5490e7660f841ba86ea3 288320 coverage/tmp/coverage-37176-1785341378929-0.json -da4b2512633624ef659682bf16af77d92799bd228e0d69a985fa23246460ce09 111308 coverage/tmp/coverage-44380-1785341379522-0.json -96a9f4b2bb3a429dbe5286c7f6c13c39b0e0de882d46724853083ea3d9ac9f6e 317635 coverage/tmp/coverage-44612-1785341382147-0.json -705a92db802fac29c0bbe5aa5727ad35350ddfd03bc98efc24d68529f5a41a99 360058 coverage/tmp/coverage-44984-1785341422918-0.json -279e70e06e6366bf15bb60f6079c38580d006832ba200ba5cebd9bac8a4e4f02 321057 coverage/tmp/coverage-48188-1785341379614-0.json -20758be193868d8f9275dd1eaf35b6bfc652f4682ebc89e4c4aa481e63dd1664 301398 coverage/tmp/coverage-49592-1785341377478-0.json -a4275d02b93b13deadcc554844b321fe186ad272e676f93ccf8f84772bc2760c 284667 coverage/tmp/coverage-49952-1785341378477-0.json -d704dd8bab1e3fcf52d4e4762e6a3e4eb9818073a2fc1d622a2afe4425ec9cc4 336008 coverage/tmp/coverage-50036-1785341378337-0.json -e4dc4ec12d779464d974f7d5c9713f63db6776596698fdc4ce03d18e773a34d9 354684 coverage/tmp/coverage-50728-1785341378056-0.json -0487ef221d132f9ada2b57cb216fb0ff167f903cea8df46c61b39b9fa52bd38a 282952 coverage/tmp/coverage-51204-1785341379590-0.json -42be90ace3af568e4eace7f1a3a5974856360707839c793b3b092ede0f6c5ad1 344613 coverage/tmp/coverage-51236-1785341383324-0.json -1b5a01cc5508a78e28b27f7a46878d54aaca027deb0e93f34f445d9dd32116f1 335945 coverage/tmp/coverage-54004-1785341378407-0.json -b46e7b4d5f95b167803a7d0481163a01026b5ac2e9c32e2ba40afa8a4791163e 428588 coverage/tmp/coverage-55224-1785341380041-0.json -b175e49795c65f6011dbf04aefd75cb7ebee5c2b248edb50e7df1780f2a599b8 367436 coverage/tmp/coverage-55472-1785341379481-0.json -2a53a441228fb74c1f7a488d49c1545e53640af5c1ca720352919c86527e5c19 344934 coverage/tmp/coverage-56192-1785341380236-0.json -dd2c111bc050afda47ed7379f88651cda579a965cfed56ddf2b3ca0e5d748ee1 304508 coverage/tmp/coverage-5636-1785341377677-0.json -8ce33572e4a3ba6e63b85155dc0ebba61b3e6fd203ef17b762735639dab72467 360058 coverage/tmp/coverage-56392-1785341444543-0.json -9efac9c2f67e00fbc2ddd22ff17886ffa212b55b76e21597df1a85417e55ef0c 89175 coverage/tmp/coverage-56440-1785341377010-0.json -4b6892c586091b5bf3eead8d8687a318ef2d82874fc0c9efaab25430ee819600 296835 coverage/tmp/coverage-58592-1785341378685-0.json -81f7395f307d0c4776331a5125653388650dea21876ace362ec92e3ce1efefda 318326 coverage/tmp/coverage-58856-1785341378523-0.json -5370cdb8a3384f62e68314bd4bf0efa78d9db57ced656b011d3b1e995369596e 327419 coverage/tmp/coverage-59512-1785341377212-0.json -7e7571c84f4bcdcdd39ba6fcaa2ac9ea1583eeff6cf59ea4c46c601add76e40a 310269 coverage/tmp/coverage-60040-1785341379655-0.json -d19fda134998419dd45ecd943d587c26cf80742b140de46449f8ec4bcf9191a9 302513 coverage/tmp/coverage-61124-1785341378630-0.json +56691e95912beab52b9e072ad0735a706934def093d91a83a235119e03f7ef21 111308 coverage/tmp/coverage-14112-1785342182302-0.json +35e206d8e448f56715fdeaafc82a00680df74edce1bd1d74d2c626c0ebdc8dd4 344934 coverage/tmp/coverage-18504-1785342182921-0.json +cd5a1817ee7f405f72ede736ec2ccc5831a14c4ab99c3ae9e1af11c93c7f4c13 354684 coverage/tmp/coverage-23740-1785342180573-0.json +f2dbe68eb60983b2038bfda8448e148e0d762794651c8f0ef0a5985549f35d11 301398 coverage/tmp/coverage-30356-1785342180023-0.json +04e0ac6a30fdcbbc9f9856697b228626e0355b7c98c6045cf0db01de0c93fc68 344613 coverage/tmp/coverage-35156-1785342187759-0.json +0d4e6a6dcc0ce9f8d2d9e304dbbeccc2b551fbe79637fdd16b7e860a58685d68 302513 coverage/tmp/coverage-35780-1785342181205-0.json +3c5a7491afc0247568bce0824331c16e2e9af0693b216e01fe73bb41ad4fe91f 327420 coverage/tmp/coverage-38404-1785342179951-0.json +24ca89f314bacd35321b8772706e5c4658602abf90e701561d23f67a1d4dd10e 288320 coverage/tmp/coverage-38780-1785342181696-0.json +8711b58d90963d5c5239011527a8d504fde3bed93671e09b8e6f8e865837d928 358165 coverage/tmp/coverage-39472-1785342183215-0.json +5e8d4cd9c0b348618088733e133986c8bff8d822b7a0f0fb6c04a777f025e249 303090 coverage/tmp/coverage-39536-1785342182261-0.json +9aa7a9e3b5daa4a3f2139df2a8b8857d851ef2ce8e3c7679db787f6e72f5df38 302346 coverage/tmp/coverage-41596-1785342181753-0.json +cd9419f7829a06e3932ca774fb06c51b3637639e8f750fe0bbce400921c20866 321057 coverage/tmp/coverage-42088-1785342182383-0.json +5b75886d617be03df5d1271223cbd8cc660d6603368349d7f7740e7b5005d3d2 304508 coverage/tmp/coverage-42724-1785342180308-0.json +e567e65494a3fadf0effdd7058761c890ff35411532673fb6d0feb0f7d0d681f 346823 coverage/tmp/coverage-44760-1785342180156-0.json +4f2a6563b9710b23c918d10f1a2d713975f0f5d2feedb5731e87a441bb9eabae 352006 coverage/tmp/coverage-44924-1785342180167-0.json +5e355c76dab29a1ecebe30d390970397ae6ccd71798b111fa67e395a1310ad5e 361403 coverage/tmp/coverage-46136-1785342180786-0.json +1623101020fed0786a83d5244a03df9bf2124a9a01961a7739ce63ba7c941ca4 336008 coverage/tmp/coverage-47576-1785342181138-0.json +0307f0113c6b76b5044cae887f76f3c01cfd7133fd4a00a8591a979eff3ab06e 317635 coverage/tmp/coverage-48580-1785342185938-0.json +4588ba2d6ef043d6552135f874f2415865f2401a53c46c6e29229b6925deceb0 335945 coverage/tmp/coverage-48608-1785342180830-0.json +9335bccbbea8122e6bcd7190e5266e4a0fbd1e3631d882004e2d43cec41de95d 318326 coverage/tmp/coverage-49480-1785342181092-0.json +e82fa624b1ef3e7ea427b58aac1c3135d0ac1af62b5c1768a7c2bd59791765c7 367436 coverage/tmp/coverage-50664-1785342182886-0.json +0884915ee6ccec377161cb960b768bb44da6f8622ab3d9542a0c51a9fd0106b9 375705 coverage/tmp/coverage-54052-1785342191177-0.json +a0150efac6b758945f87373f8329f05d6be53d11a3201a88a1124372e8ee7d69 282952 coverage/tmp/coverage-54676-1785342182110-0.json +885fb349d550fa169fb0a0fe97c96c9f0353e62a321a2022da2de0f1c1958811 345124 coverage/tmp/coverage-56728-1785342195847-0.json +e0dbe28900b09fc36b22fbb05bc844f57ff3094b9a1f84da97f793311c37b3f9 344499 coverage/tmp/coverage-57256-1785342181830-0.json +615cc6516905c8e53e4b888bc81589ce8542e34634e5a391ba9204843f57221b 428589 coverage/tmp/coverage-58964-1785342182533-0.json +5d845ca1a0ff28f1e570dcb8a26502e902589eafc2727171f1aad0aa7a2ce52e 296835 coverage/tmp/coverage-59284-1785342181255-0.json +3afca2678ab83c1ecaba8c1abf70b89b3763016acb52df4027d61a590dcf5c19 284668 coverage/tmp/coverage-59848-1785342181206-0.json +5a39c973f886c05b9c34ca089f9d410ab5831036e83d7d5dbf525c2feb0e3670 342640 coverage/tmp/coverage-60948-1785342181050-0.json +79d1a3635a8c972ac6ab0aa75149b2a5f01b84ae2a0ae68e736490160b22bc27 360058 coverage/tmp/coverage-61560-1785342250533-0.json +03adfd7ea1161c638fe6f6a75a0ab0d860f89412ea047209ea1b15f879de9aad 299000 coverage/tmp/coverage-62420-1785342180409-0.json +8387e10a857a2195458bf576c4129b2cd516a50ac5408a605eff038376bba64b 310269 coverage/tmp/coverage-62428-1785342182350-0.json +a02d7c51b209b5967e63cec657caeb8e671b5c87d34dbe2cfbec2be891f6266e 329358 coverage/tmp/coverage-62604-1785342181856-0.json +3488e63749aed14a820a72d8fad385175f2111648bdf2c10a48c1148c419836f 330765 coverage/tmp/coverage-62652-1785342180356-0.json +3da8a2ab49221e4c837c413af878b951f1011be09824ee7dcb49485fb40550bd 89175 coverage/tmp/coverage-63428-1785342179556-0.json 21cb96e7afe71b1dc791c818dedd244d92f9a6ed4d9ffbb3022ccb187e1bdf0f 852 docs/ACCEPTANCE.md a17f95d96d3c9fbc69d870874e6fbb7472091adefc454b24f835db1279511d72 8296 docs/ARCHITECTURE.md 72e846f591c47a0291e7466e58e052d3d5afcf551c4e6c848632ac3c552a1244 3043 docs/CURRENT_STATE.md @@ -121,8 +123,9 @@ c230b931abf2293d2d44b7a69b94c35f1142c093cc46b88739a0de5cbd6d1896 1532 4a561ead5ba7cdfaf4efce91842a4308c5f2a77980205879d83835efb8a579db 1067 LICENSE 1f0f388df4397e548887bbc7579fd3c864581b86469c01703201ece7a6cbf931 13667 main.cjs 91a984a89dd57a084b9a2331763cacdb061582fb590f13df379d92c1a77a2ee1 352 OVERLAY-INSTRUCTIONS.md -f3e2a262e251eb165f342dc3c184625ec10d56e536b5a987199b9944f463eee9 181069 package-lock.json -43725a143cb927cc70bd823d789788d785d872119c008043c270bbf73948851f 4882 package.json +17eeeb1d6f18fbbbb918cdfdbc3836e46ec6be9e32c059cfc330e9e4e73e39d0 183079 package-lock.json +96ab185db639205021c5b0b6fbf8d4bf19d2cad8d1a042eab9a425613ea6a574 5018 package.json +2a597a5704c576783b8a72407fbc377fa7506b36a4596ea7f7bce126e394f837 1326 playwright.config.mjs 69318fdf054be7aa2fe86ead9847da9da65745d8d5de548c8346f3ba0afc4892 12175 preload.cjs abe5dd6fd68f2970cd19ef134094907c67219061d8fe9a1a08324c78de4ad437 484 PUBLISH-AND-ENABLE-UPDATE.cmd f018383f755352ca448e2ebb1e19b1dba412a3eb793d61e64b02953e300754fd 10538 Publish-ForgeFlow-Release.ps1 @@ -191,11 +194,11 @@ cdfaacdcd5ae04b0e5c79fefa21f5e09d5c810bcea504c5b6e1d6b744182ff84 15567 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark-dark.png 094c1b71cc2482a9db250ac175f45f3de68f53277dfbde371a03e61923d00988 75240 src/renderer/assets/itworx-wordmark-light.png 813b8cdeecac43794166f3db9d3c5d2c441e0292f9ab7bd465ba136d6201e95d 82476 src/renderer/assets/itworx-wordmark.png -5d6c87d51e55c9e976cf83ce242be0c28cf831d9c52082f02e745193500e6348 47992 src/renderer/dialogs.js +1a577af2459715cf38d49d118da8c5f897a7ff1d3eb42ad41f121675eb871732 48389 src/renderer/dialogs.js 977af9585074f0a404830c5a93de6939b4897d2eb98ba8ae75c3cf0023dea673 5782 src/renderer/events.js a84da5aecbb16ce7983dba1f6d6aab1bf47b2e9a87c2933fa1afb8123f7ef7d6 1497 src/renderer/index.html 06180d9656dd254edfb6949c397f8e313954fc560ddcb22b3a35fce3c3e35655 21350 src/renderer/mock-bridge.js -19603be9f471ba695c66f17fc027ab427b68ce146af80d52e3c94624a5ba1c35 27712 src/renderer/mock-deployment-bridge.js +4b0a64610da0c446f15a43e4753b26f29de72e07793e2fa7b7322d4f07cf8050 27806 src/renderer/mock-deployment-bridge.js 26065ffa2359cd27b9c9b5b9fb67bcad83ba0f118960c7c024e7e9392dbb16a3 20032 src/renderer/mock-repository-bridge.js 94fa265c2fe9ca8d644f0ce9b620b6f85d9b25dca5802c4e9195b66dcbe80120 6522 src/renderer/operations.js 45692591428575b518678a6b548c25d3de95f568541e5648b0f06f42c48b5bbf 77872 src/renderer/styles.css @@ -213,6 +216,7 @@ f8853dce6fdf360d5df2fbe2b6df3e5687630c807fee5ba8436679b34ec737ea 2436 058aeaa5d9bfe377c7e322f213c7871ecc4151b5d08ef790992f4ee28d857658 743 START-FORGEFLOW-OVERLAY.ps1 f5b0ea887fcdeadec78c1ad49b0ec7979723562f5c0b730703acb77a37281ee0 1009 tests/acceptance.test.mjs a4e5947204ff6878e601e32477bc85b53cd0153baf95a161c8935b6e5466c257 1155 tests/audit-service.test.mjs +33bc892963e89b868235b959498308a456b1285057bda524ba7c1d5a9ee2159e 8763 tests/browser/forgeflow.spec.mjs 454edeaccb2bd41043bc918d3e3a6127db14339031d6a1c1562ac855e90455d2 4318 tests/clone-target.test.mjs f1463326aee79842d265687ae628189ce54e92544600f2bd14073780287cfb14 2502 tests/configuration-backup.test.mjs b7e009fed4171d6dd6b4c3154ba1d3f7198e98f5b79b298687841fc8169447cd 9354 tests/deploy-key-lifecycle.test.mjs diff --git a/package-lock.json b/package-lock.json index 1800b3a..3e1d678 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ }, "devDependencies": { "@eslint/js": "9.39.5", + "@playwright/test": "1.55.0", "c8": "10.1.3", "electron": "43.2.0", "electron-builder": "26.15.3", @@ -916,6 +917,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2743,6 +2760,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4000,6 +4032,38 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", diff --git a/package.json b/package.json index 817ef64..1842ada 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "doctor": "node scripts/doctor.mjs", "acceptance": "node scripts/acceptance.mjs", "architecture:audit": "node scripts/architecture-audit.mjs", + "test:browser": "playwright test", + "test:browser:ci": "playwright test --reporter=line,html", "connections:check": "electron scripts/validate-installed-connections.cjs", "deployments:audit": "electron scripts/audit-installed-deployments.cjs", "release:binary": "electron scripts/publish-binary-release.cjs", @@ -28,6 +30,7 @@ }, "devDependencies": { "@eslint/js": "9.39.5", + "@playwright/test": "1.55.0", "c8": "10.1.3", "electron": "43.2.0", "electron-builder": "26.15.3", @@ -101,9 +104,9 @@ "docs/RELEASE_NOTES_0.9.5.md", "docs/RELEASE_NOTES_0.10.0.md", "docs/CURRENT_STATE.md", - "docs/MUTATION_MODEL.md" - ,"docs/RELEASING.md" - ,"docs/ERROR_CODES.md" + "docs/MUTATION_MODEL.md", + "docs/RELEASING.md", + "docs/ERROR_CODES.md" ], "asarUnpack": [ "scripts/apply-binary-update.ps1" diff --git a/playwright.config.mjs b/playwright.config.mjs new file mode 100644 index 0000000..d184d41 --- /dev/null +++ b/playwright.config.mjs @@ -0,0 +1,42 @@ +import { defineConfig } from "@playwright/test"; + +const matrices = [ + ["compact-dark-100", 1120, 720, "dark", 1, false], + ["desktop-light-125", 1440, 900, "light", 1.25, false], + ["wide-dark-150", 1920, 1080, "dark", 1.5, false], + ["compact-light-reduced", 1120, 720, "light", 1, true], + ["desktop-dark-reduced", 1440, 900, "dark", 1.25, true], + ["wide-light-100", 1920, 1080, "light", 1, false], +]; + +export default defineConfig({ + testDir: "./tests/browser", + outputDir: "artifacts/browser", + timeout: 45_000, + expect: { timeout: 7_000 }, + fullyParallel: false, + workers: process.env.CI ? 2 : 3, + reporter: [["line"], ["html", { outputFolder: "artifacts/browser-report", open: "never" }]], + use: { + baseURL: "http://127.0.0.1:4173", + screenshot: "only-on-failure", + trace: "retain-on-failure", + video: "retain-on-failure", + }, + webServer: { + command: "node scripts/serve-demo.mjs", + url: "http://127.0.0.1:4173", + reuseExistingServer: !process.env.CI, + timeout: 30_000, + }, + projects: matrices.map(([name, width, height, theme, scale, reduced]) => ({ + name, + metadata: { theme, scale, reduced }, + use: { + viewport: { width, height }, + deviceScaleFactor: scale, + colorScheme: theme, + reducedMotion: reduced ? "reduce" : "no-preference", + }, + })), +}); diff --git a/src/renderer/dialogs.js b/src/renderer/dialogs.js index 9c685ab..bcd92ef 100644 --- a/src/renderer/dialogs.js +++ b/src/renderer/dialogs.js @@ -289,6 +289,11 @@ function enhanceRenderedUi() { if (!control.id) control.id = `forgeflow-field-${index}`; label.htmlFor = control.id; }); + document.querySelectorAll("input:not([aria-label]), select:not([aria-label]), textarea:not([aria-label])").forEach((control) => { + if (control.labels?.length) return; + const fallback = String(control.placeholder || control.name || control.id || "Form control").replaceAll("-", " ").trim(); + control.setAttribute("aria-label", fallback.charAt(0).toUpperCase() + fallback.slice(1)); + }); } function render() { @@ -316,4 +321,3 @@ function render() { document.querySelector("#palette-input")?.focus(), ); } - diff --git a/src/renderer/mock-deployment-bridge.js b/src/renderer/mock-deployment-bridge.js index cec7a98..db8b33c 100644 --- a/src/renderer/mock-deployment-bridge.js +++ b/src/renderer/mock-deployment-bridge.js @@ -466,7 +466,7 @@ function createMockDeploymentBridge(context) { checkedAt: iso(), score: 78, grade: "Good", - policy: { id: "standard", label: "Standard", requiredScore: 70 }, + policy: state.gitValidatorPolicy || { id: "standard", label: "Standard", requiredScore: 70 }, ready: true, commitSha: "8cbaf303aa3bb9b4023a7c89aa13fb70ce612847", trend: { newlyFound: ["working-tree"], resolved: ["editorconfig"], regressions: [], suppressions: [] }, @@ -572,7 +572,8 @@ function createMockDeploymentBridge(context) { }; }, async gitValidatorSetPolicy(_fullName, policy) { - return { id: policy.id, label: policy.id[0].toUpperCase() + policy.id.slice(1) }; + state.gitValidatorPolicy = { id: policy.id, label: policy.id[0].toUpperCase() + policy.id.slice(1) }; + return clone(state.gitValidatorPolicy); }, async gitValidatorSuppress(_fullName, suppression) { return { ...suppression, id: `suppression-${Date.now()}`, createdAt: iso() }; diff --git a/tests/browser/forgeflow.spec.mjs b/tests/browser/forgeflow.spec.mjs new file mode 100644 index 0000000..b141511 --- /dev/null +++ b/tests/browser/forgeflow.spec.mjs @@ -0,0 +1,164 @@ +import { test, expect } from "@playwright/test"; +import { writeFile } from "node:fs/promises"; + +const consoleEntries = new WeakMap(); +const runtimeErrors = new WeakMap(); + +test.beforeEach(async ({ page }, testInfo) => { + const logs = []; + const errors = []; + consoleEntries.set(page, logs); + runtimeErrors.set(page, errors); + await page.emulateMedia({ colorScheme: testInfo.project.metadata.theme, reducedMotion: testInfo.project.metadata.reduced ? "reduce" : "no-preference" }); + page.on("console", (message) => logs.push({ type: message.type(), text: message.text() })); + page.on("pageerror", (error) => errors.push({ name: error.name, message: error.message, stack: error.stack })); + await page.goto("/"); + await expect(page.locator(".app-shell")).toBeVisible(); + const theme = testInfo.project.metadata.theme; + await page.evaluate((requested) => { + document.documentElement.dataset.theme = requested; + localStorage.setItem("forgeflow-demo-theme", requested); + }, theme); +}); + +test.afterEach(async ({ page }, testInfo) => { + const logs = consoleEntries.get(page) || []; + const errors = runtimeErrors.get(page) || []; + if (testInfo.status !== testInfo.expectedStatus) { + const prefix = testInfo.outputPath("failure"); + await writeFile(`${prefix}-console.json`, JSON.stringify({ test: testInfo.title, project: testInfo.project.name, metadata: testInfo.project.metadata, logs, errors }, null, 2)); + await writeFile(`${prefix}-dom.html`, await page.content()); + await writeFile(`${prefix}-fixture.json`, JSON.stringify({ url: page.url(), viewport: page.viewportSize(), theme: await page.locator("html").getAttribute("data-theme") }, null, 2)); + } + expect(errors, "page errors").toEqual([]); + expect(logs.filter((entry) => entry.type === "error"), "console errors").toEqual([]); +}); + +async function assertSurface(page) { + const audit = await page.evaluate(() => { + const interactive = [...document.querySelectorAll('button,input,select,textarea,a[href],[role="button"]')].filter((element) => { + const style = getComputedStyle(element); + return style.display !== "none" && style.visibility !== "hidden" && element.getBoundingClientRect().width > 0; + }); + const unnamed = interactive.filter((element) => !String(element.getAttribute("aria-label") || element.getAttribute("title") || element.labels?.[0]?.textContent || element.textContent || element.value || "").trim()); + const outside = interactive.filter((element) => { const rect = element.getBoundingClientRect(); const fixed = ["fixed", "sticky"].includes(getComputedStyle(element).position) || Boolean(element.closest('[role="dialog"]')); return rect.left < -1 || rect.right > innerWidth + 1 || (fixed && (rect.top < -1 || rect.bottom > innerHeight + 1)); }); + const text = document.body.innerText; + return { + horizontalOverflow: document.documentElement.scrollWidth > document.documentElement.clientWidth + 1, + unnamed: unnamed.map((element) => element.outerHTML.slice(0, 180)), + outside: outside.map((element) => element.outerHTML.slice(0, 180)), + badTokens: ["undefined", "[object Object]", "â", "Â", "Ã"].filter((token) => text.includes(token)), + nullText: /(^|\s)null($|\s)/i.test(text), + headings: [...document.querySelectorAll("h1,h2,h3")].map((heading) => Number(heading.tagName[1])), + }; + }); + expect(audit.horizontalOverflow).toBe(false); + expect(audit.unnamed).toEqual([]); + expect(audit.outside).toEqual([]); + expect(audit.badTokens).toEqual([]); + expect(audit.nullText).toBe(false); + expect(audit.headings.length).toBeGreaterThan(0); +} + +test("shell, overview, repositories and settings remain responsive and accessible", async ({ page }, testInfo) => { + await assertSurface(page); + for (const view of ["overview", "deployments", "settings"]) { + await page.locator(`.nav-button[data-action="navigate"][data-view="${view}"]`).click(); + await expect(page.locator("main")).toBeVisible(); + await assertSurface(page); + } + await expect(page.locator("html")).toHaveAttribute("data-theme", String(testInfo.project.metadata.theme)); + if (testInfo.project.metadata.reduced) { + expect(await page.evaluate(() => matchMedia("(prefers-reduced-motion: reduce)").matches)).toBe(true); + } +}); + +test("repository changes, Git tools and Git Validator complete their primary flow", async ({ page }) => { + await page.locator('[data-action="select-repo"]').first().click(); + await expect(page.locator('[data-action="repo-tab"]')).toHaveCount(6); + for (const tab of ["changes", "history", "deployments", "gittools", "validator", "settings"]) { + const control = page.locator(`[data-action="repo-tab"][data-tab="${tab}"]`); + if (await control.count()) { + await control.click(); + await expect(control).toHaveClass(/active/); + await assertSurface(page); + } + } + await page.locator('[data-action="repo-tab"][data-tab="validator"]').click(); + await expect(page.locator(".validator-score")).toBeVisible(); + await expect(page.locator("#validator-policy")).toBeVisible(); + await page.locator("#validator-policy").selectOption("production"); + await expect(page.locator(".validator-hero")).toContainText(/Production policy/i); + await page.keyboard.press("Tab"); + await expect(page.locator(":focus")).toBeVisible(); +}); + +test("deployment inventory supports dense workloads without ambiguous blank cards", async ({ page }) => { + await page.locator('.nav-button[data-action="navigate"][data-view="deployments"]').click(); + await expect(page.locator(".deploy-card, .server-inventory-panel .tool-row").first()).toBeVisible(); + const cards = page.locator(".deploy-card, .server-inventory-panel .tool-row"); + const count = await cards.count(); + expect(count).toBeGreaterThan(0); + for (let index = 0; index < Math.min(count, 25); index += 1) { + await expect(cards.nth(index)).not.toHaveText(/^\s*$/); + } + await assertSurface(page); +}); + +test("dialogs expose semantics, labels, keyboard close and focus restoration", async ({ page }) => { + await page.locator('[data-action="select-repo"]').first().click(); + const trigger = page.locator('[data-action="edit-deployment-profile"], [data-action="add-deployment-profile"]').first(); + if (await trigger.count()) { + await trigger.focus(); + await trigger.click(); + const dialog = page.locator('[role="dialog"]'); + await expect(dialog).toBeVisible(); + await expect(dialog.locator("button").first()).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(dialog).toHaveCount(0); + } + await assertSurface(page); +}); + +test("onboarding and updater states remain usable without an existing configuration", async ({ page }) => { + await page.evaluate(() => localStorage.setItem("forgeflow-demo-setup", "false")); + await page.reload(); + await expect(page.locator(".setup-window")).toBeVisible(); + await expect(page.getByRole("heading", { name: "Check this computer" })).toBeVisible(); + await page.locator('[data-action="setup-run-preflight"]').click(); + await expect(page.locator('[data-action="setup-continue"]')).toBeEnabled(); + await assertSurface(page); + await page.evaluate(() => localStorage.setItem("forgeflow-demo-setup", "true")); + await page.reload(); + await page.locator('.nav-button[data-view="settings"]').click(); + await page.locator('[data-action="check-updates"]').first().click(); + await expect(page.locator(".update-card")).toContainText(/ForgeFlow/i); + await assertSurface(page); +}); + +test("inventory, deployment safety and failure evidence dialogs are reviewable", async ({ page }) => { + await page.locator('.nav-button[data-view="deployments"]').click(); + const reconciliation = page.locator('[data-action="plan-server-reconciliation"]').first(); + if (await reconciliation.count()) { + await reconciliation.click(); + await expect(page.locator('[role="dialog"]')).toContainText(/reconciliation/i); + await page.keyboard.press("Escape"); + } + const keyLifecycle = page.locator('[data-action="manage-deploy-key"]').first(); + if (await keyLifecycle.count()) { + await keyLifecycle.click(); + await expect(page.locator('[role="dialog"]')).toContainText(/Deploy key lifecycle/i); + await page.keyboard.press("Escape"); + } + const preflight = page.locator('[data-action="run-deployment-preflight"]').first(); + await preflight.click(); + await expect(page.locator('[role="dialog"]')).toContainText(/preflight/i); + await page.keyboard.press("Escape"); + const failed = page.locator('[data-action="open-operation"]').last(); + if (await failed.count()) { + await failed.click(); + await expect(page.locator('[role="dialog"]')).toContainText(/failed|failure|healthcheck/i); + await page.keyboard.press("Escape"); + } + await assertSurface(page); +});