diff --git a/.forgejo/workflows/publish-release.yml b/.forgejo/workflows/publish-release.yml index 66432f37a0..e4f7028ca7 100644 --- a/.forgejo/workflows/publish-release.yml +++ b/.forgejo/workflows/publish-release.yml @@ -59,6 +59,24 @@ jobs: gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} verbose: ${{ vars.VERBOSE }} + - name: get trigger mirror issue + id: mirror + uses: https://code.forgejo.org/infrastructure/issue-action/get@v1.1.0 + with: + forgejo: https://code.forgejo.org + repository: forgejo/forgejo + labels: mirror-trigger + + - name: trigger the mirror + uses: https://code.forgejo.org/infrastructure/issue-action/set@v1.1.0 + with: + forgejo: https://code.forgejo.org + repository: forgejo/forgejo + token: ${{ secrets.LABEL_ISSUE_FORGEJO_MIRROR_TOKEN }} + numbers: ${{ steps.mirror.outputs.numbers }} + label-wait-if-exists: 3600 + label: trigger + - name: upgrade v*.next.forgejo.org uses: https://code.forgejo.org/infrastructure/next-digest@v1.1.0 with: diff --git a/Makefile b/Makefile index ae7ed16846..670cb9452a 100644 --- a/Makefile +++ b/Makefile @@ -716,7 +716,6 @@ test-e2e-pgsql\#%: playwright e2e.pgsql.test generate-ini-pgsql .PHONY: test-e2e-debugserver test-e2e-debugserver: e2e.sqlite.test generate-ini-sqlite - sed -i s/3003/3000/g tests/sqlite.ini GITEA_ROOT="$(CURDIR)" GITEA_CONF=tests/sqlite.ini ./e2e.sqlite.test -test.run TestDebugserver -test.timeout 24h .PHONY: bench-sqlite diff --git a/eslint.config.mjs b/eslint.config.mjs index da1b2b5d49..73a4e0bcfa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,7 +20,7 @@ export default tseslint.config( ...tseslint.configs.recommended, eslintPluginImportX.flatConfigs.typescript, { - ignores: ['web_src/js/vendor', 'web_src/fomantic', 'public/assets/js'], + ignores: ['web_src/js/vendor', 'web_src/fomantic', 'public/assets/js', 'tests/e2e/reports/'], }, { plugins: { @@ -1112,7 +1112,7 @@ export default tseslint.config( ], }, }, { - files: ['tests/e2e/**/*.js', 'tests/e2e/**/*.ts'], + files: ['tests/e2e/**/*.ts'], languageOptions: { globals: { ...globals.browser, @@ -1125,7 +1125,8 @@ export default tseslint.config( ...playwright.configs['flat/recommended'].rules, 'playwright/no-conditional-in-test': [0], 'playwright/no-conditional-expect': [0], - 'playwright/no-networkidle': [0], + // allow grouping helper functions with tests + 'unicorn/consistent-function-scoping': [0], 'playwright/no-skipped-test': [ 2, diff --git a/go.mod b/go.mod index 9537d12204..31f1b784c0 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/alecthomas/chroma/v2 v2.14.0 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.4.2 - github.com/buildkite/terminal-to-html/v3 v3.16.3 + github.com/buildkite/terminal-to-html/v3 v3.16.4 github.com/caddyserver/certmagic v0.21.4 github.com/chi-middleware/proxy v1.1.1 github.com/djherbis/buffer v1.2.0 diff --git a/go.sum b/go.sum index a1c1056623..49b1074748 100644 --- a/go.sum +++ b/go.sum @@ -143,8 +143,8 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/buildkite/terminal-to-html/v3 v3.16.3 h1:IGuJjboHjuMLWOGsKZKNxbbn41emOLiHzXPmQZk31fk= -github.com/buildkite/terminal-to-html/v3 v3.16.3/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0= +github.com/buildkite/terminal-to-html/v3 v3.16.4 h1:QFYO8IGvRnp7tGgiQb8g9uFU8kY9wOzxsFFx17+yy6Q= +github.com/buildkite/terminal-to-html/v3 v3.16.4/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/caddyserver/certmagic v0.21.4 h1:e7VobB8rffHv8ZZpSiZtEwnLDHUwLVYLWzWSa1FfKI0= github.com/caddyserver/certmagic v0.21.4/go.mod h1:swUXjQ1T9ZtMv95qj7/InJvWLXURU85r+CfG0T+ZbDE= diff --git a/package-lock.json b/package-lock.json index 267b1de595..c5c75532c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "monaco-editor": "0.51.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", - "postcss": "8.4.48", + "postcss": "8.4.49", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "pretty-ms": "9.0.0", @@ -60,21 +60,21 @@ "wrap-ansi": "9.0.0" }, "devDependencies": { - "@axe-core/playwright": "4.10.0", + "@axe-core/playwright": "4.10.1", "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.48.2", - "@stoplight/spectral-cli": "6.13.1", + "@stoplight/spectral-cli": "6.14.0", "@stylistic/eslint-plugin-js": "2.10.1", "@stylistic/stylelint-plugin": "3.1.1", - "@typescript-eslint/parser": "8.13.0", + "@typescript-eslint/parser": "8.14.0", "@vitejs/plugin-vue": "5.1.5", "@vitest/coverage-v8": "2.1.4", - "@vitest/eslint-plugin": "1.1.7", + "@vitest/eslint-plugin": "1.1.10", "@vue/test-utils": "2.4.6", "eslint": "9.14.0", "eslint-import-resolver-typescript": "3.6.3", "eslint-plugin-array-func": "5.0.2", - "eslint-plugin-import-x": "4.4.0", + "eslint-plugin-import-x": "4.4.2", "eslint-plugin-no-jquery": "3.0.2", "eslint-plugin-no-use-extend-native": "0.7.2", "eslint-plugin-playwright": "2.0.1", @@ -82,11 +82,11 @@ "eslint-plugin-sonarjs": "2.0.4", "eslint-plugin-unicorn": "56.0.0", "eslint-plugin-vitest-globals": "1.5.0", - "eslint-plugin-vue": "9.30.0", + "eslint-plugin-vue": "9.31.0", "eslint-plugin-vue-scoped-css": "2.8.1", "eslint-plugin-wc": "2.2.0", "globals": "15.12.0", - "happy-dom": "15.11.0", + "happy-dom": "15.11.4", "license-checker-rseidelsohn": "4.4.2", "markdownlint-cli": "0.42.0", "postcss-html": "1.7.0", @@ -96,7 +96,7 @@ "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.2.0", "typescript": "5.6.3", - "typescript-eslint": "8.13.0", + "typescript-eslint": "8.14.0", "vite-string-plugin": "1.3.4", "vitest": "2.1.4" }, @@ -163,13 +163,13 @@ } }, "node_modules/@axe-core/playwright": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.0.tgz", - "integrity": "sha512-kEr3JPEVUSnKIYp/egV2jvFj+chIjCjPp3K3zlpJMza/CB3TFw8UZNbI9agEC2uMz4YbgAOyzlbUy0QS+OofFA==", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.1.tgz", + "integrity": "sha512-EV5t39VV68kuAfMKqb/RL+YjYKhfuGim9rgIaQ6Vntb2HgaCaau0h98Y3WEUqW1+PbdzxDtDNjFAipbtZuBmEA==", "dev": true, "license": "MPL-2.0", "dependencies": { - "axe-core": "~4.10.0" + "axe-core": "~4.10.2" }, "peerDependencies": { "playwright-core": ">= 1.0.0" @@ -3351,10 +3351,23 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@jsep-plugin/assignment": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz", + "integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.16.0" + }, + "peerDependencies": { + "jsep": "^0.4.0||^1.0.0" + } + }, "node_modules/@jsep-plugin/regex": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.3.tgz", - "integrity": "sha512-XfZgry4DwEZvSFtS/6Y+R48D7qJYJK6R9/yJFyUFHCIUMEEHuJ4X95TDgJp5QkmzfLYvapMPzskV5HpIDrREug==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz", + "integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==", "dev": true, "license": "MIT", "engines": { @@ -3365,9 +3378,9 @@ } }, "node_modules/@jsep-plugin/ternary": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.3.tgz", - "integrity": "sha512-qtLGzCNzPVJ3kdH6/zoLWDPjauHIKiLSBAR71Wa0+PWvGA8wODUQvRgxtpUA5YqAYL3CQ8S4qXhd/9WuWTZirg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@jsep-plugin/ternary/-/ternary-1.1.4.tgz", + "integrity": "sha512-ck5wiqIbqdMX6WRQztBL7ASDty9YLgJ3sSAK5ZpBzXeySvFGCzIvM6UiAI4hTZ22fEcYQVV/zhUbNscggW+Ukg==", "dev": true, "license": "MIT", "engines": { @@ -3992,20 +4005,20 @@ } }, "node_modules/@stoplight/spectral-cli": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.13.1.tgz", - "integrity": "sha512-v6ipX4w6wRhtbOotwdPL7RrEkP0m1OwHTIyqzVrAPi932F/zkee24jmf1CHNrTynonmfGoU6/XpeqUHtQdKDFw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-cli/-/spectral-cli-6.14.0.tgz", + "integrity": "sha512-pq1qWENLtI97afz9Ygx0TtBXj9s97oQnjMOUp4USzXdnxhKhlYwlhJIA9U3VYzktA+QpHdTXVd4GSgyxdHSBSg==", "dev": true, "license": "Apache-2.0", "dependencies": { "@stoplight/json": "~3.21.0", "@stoplight/path": "1.3.2", - "@stoplight/spectral-core": "^1.18.3", - "@stoplight/spectral-formatters": "^1.3.0", - "@stoplight/spectral-parsers": "^1.0.3", + "@stoplight/spectral-core": "^1.19.2", + "@stoplight/spectral-formatters": "^1.4.0", + "@stoplight/spectral-parsers": "^1.0.4", "@stoplight/spectral-ref-resolver": "^1.0.4", - "@stoplight/spectral-ruleset-bundler": "^1.5.4", - "@stoplight/spectral-ruleset-migrator": "^1.9.6", + "@stoplight/spectral-ruleset-bundler": "^1.6.0", + "@stoplight/spectral-ruleset-migrator": "^1.11.0", "@stoplight/spectral-rulesets": ">=1", "@stoplight/spectral-runtime": "^1.1.2", "@stoplight/types": "^13.6.0", @@ -4022,7 +4035,7 @@ "spectral": "dist/index.js" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": "^16.20 || ^18.18 || >= 20.17" } }, "node_modules/@stoplight/spectral-cli/node_modules/fast-glob": { @@ -4056,9 +4069,9 @@ } }, "node_modules/@stoplight/spectral-core": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.19.1.tgz", - "integrity": "sha512-YiWhXdjyjn4vCl3102ywzwCEJzncxapFcj4dxcj1YP/bZ62DFeGJ8cEaMP164vSw2kI3rX7EMMzI/c8XOUnTfQ==", + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-core/-/spectral-core-1.19.2.tgz", + "integrity": "sha512-Yx1j7d0VGEbsOCimPgl+L8w7ZuuOaxqGvXSUXgm9weoGR5idLQjPaTuHLdfdziR1gjqQdVTCEk/dN0cFfUKhow==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4075,17 +4088,17 @@ "ajv-errors": "~3.0.0", "ajv-formats": "~2.1.0", "es-aggregate-error": "^1.0.7", - "jsonpath-plus": "7.1.0", + "jsonpath-plus": "10.1.0", "lodash": "~4.17.21", "lodash.topath": "^4.5.2", "minimatch": "3.1.2", - "nimma": "0.2.2", + "nimma": "0.2.3", "pony-cause": "^1.0.0", - "simple-eval": "1.0.0", + "simple-eval": "1.0.1", "tslib": "^2.3.0" }, "engines": { - "node": "^12.20 || >= 14.13" + "node": "^16.20 || ^18.18 || >= 20.17" } }, "node_modules/@stoplight/spectral-core/node_modules/@stoplight/types": { @@ -4266,9 +4279,9 @@ } }, "node_modules/@stoplight/spectral-ruleset-migrator": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-migrator/-/spectral-ruleset-migrator-1.10.0.tgz", - "integrity": "sha512-nDfkVfYeWWv0UvILC4TWZSnRqQ4rHgeOJO1/lHQ7XHeG5iONanQ639B1aK6ZS6vuUc8gwuyQsrPF67b4sHIyYw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@stoplight/spectral-ruleset-migrator/-/spectral-ruleset-migrator-1.11.0.tgz", + "integrity": "sha512-FHxc/C/RhEYXW8zcp9mO50/jt+0Of6p6ZFVoV84l9y7agQchc9RGFjN6src4kO7bg6eUWQK6+5rUIV6yFKhBgg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4282,13 +4295,13 @@ "@types/node": "*", "ajv": "^8.17.1", "ast-types": "0.14.2", - "astring": "^1.7.5", + "astring": "^1.9.0", "reserved": "0.1.2", "tslib": "^2.3.1", "validate-npm-package-name": "3.0.0" }, "engines": { - "node": ">=12" + "node": "^16.20 || ^18.18 || >= 20.17" } }, "node_modules/@stoplight/spectral-ruleset-migrator/node_modules/@stoplight/yaml": { @@ -4866,17 +4879,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", - "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz", + "integrity": "sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/type-utils": "8.14.0", + "@typescript-eslint/utils": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -4900,16 +4913,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", - "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.14.0.tgz", + "integrity": "sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/typescript-estree": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "debug": "^4.3.4" }, "engines": { @@ -4929,14 +4942,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz", - "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz", + "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0" + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4947,14 +4960,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", - "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz", + "integrity": "sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/typescript-estree": "8.14.0", + "@typescript-eslint/utils": "8.14.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -4972,9 +4985,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz", - "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", + "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", "dev": true, "license": "MIT", "engines": { @@ -4986,14 +4999,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz", - "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz", + "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5031,16 +5044,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", - "integrity": "sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz", + "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0" + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/typescript-estree": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5054,13 +5067,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz", - "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz", + "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.13.0", + "@typescript-eslint/types": "8.14.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -5142,9 +5155,9 @@ } }, "node_modules/@vitest/eslint-plugin": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.7.tgz", - "integrity": "sha512-pTWGW3y6lH2ukCuuffpan6kFxG6nIuoesbhMiQxskyQMRcCN5t9SXsKrNHvEw3p8wcCsgJoRqFZVkOTn6TjclA==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.10.tgz", + "integrity": "sha512-uScH5Kz5v32vvtQYB2iodpoPg2mGASK+VKpjlc2IUgE0+16uZKqVKi2vQxjxJ6sMCQLBs4xhBFZlmZBszsmfKQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -8433,9 +8446,9 @@ } }, "node_modules/eslint-plugin-import-x": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.4.0.tgz", - "integrity": "sha512-me58aWTjdkPtgmOzPe+uP0bebpN5etH4bJRnYzy85Rn9g/3QyASg6kTCqdwNzyaJRqMI2ii2o8s01P2LZpREHg==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.4.2.tgz", + "integrity": "sha512-mDRXPSLQ0UQZQw91QdG4/qZT6hgeW2MJTczAbgPseUZuPEtIjjdPOolXroRkulnOn3fzj6gNgvk+wchMJiHElg==", "dev": true, "license": "MIT", "dependencies": { @@ -9365,9 +9378,9 @@ "license": "MIT" }, "node_modules/eslint-plugin-vue": { - "version": "9.30.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.30.0.tgz", - "integrity": "sha512-CyqlRgShvljFkOeYK8wN5frh/OGTvkj1S7wlr2Q2pUvwq+X5VYiLd6ZjujpgSgLnys2W8qrBLkXQ41SUYaoPIQ==", + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.31.0.tgz", + "integrity": "sha512-aYMUCgivhz1o4tLkRHj5oq9YgYPM4/EJc0M7TAKRLCUA5OYxRLAhYEVD2nLtTwLyixEFI+/QXSvKU9ESZFgqjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10257,9 +10270,9 @@ } }, "node_modules/happy-dom": { - "version": "15.11.0", - "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.0.tgz", - "integrity": "sha512-/zyxHbXriYJ8b9Urh43ILk/jd9tC07djURnJuAimJ3tJCOLOzOUp7dEHDwJOZyzROlrrooUhr/0INZIDBj1Bjw==", + "version": "15.11.4", + "resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-15.11.4.tgz", + "integrity": "sha512-AU6tzh3ADd28vSmXahgLsGyGGihXPGeKH0owDn9lhHolB6vIwEhag//T+TBzDoAcHhmVEwlxwSgtW1KZep+1MA==", "dev": true, "license": "MIT", "dependencies": { @@ -11417,9 +11430,9 @@ } }, "node_modules/jsep": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.3.9.tgz", - "integrity": "sha512-i1rBX5N7VPl0eYb6+mHNp52sEuaS2Wi8CDYx1X5sn9naevL78+265XJqy1qENEk7mRKwS06NHpUqiBwR7qeodw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", + "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "dev": true, "license": "MIT", "engines": { @@ -11498,13 +11511,22 @@ } }, "node_modules/jsonpath-plus": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.1.0.tgz", - "integrity": "sha512-gTaNRsPWO/K2KY6MrqaUFClF9kmuM6MFH5Dhg1VYDODgFbByw1yb7xu3hrViE/sz+dGOeMWgCzwUwQtAnCTE9g==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.1.0.tgz", + "integrity": "sha512-gHfV1IYqH8uJHYVTs8BJX1XKy2/rR93+f8QQi0xhx95aCiXn1ettYAd5T+7FU6wfqyDoX/wy0pm/fL3jOKJ9Lg==", "dev": true, "license": "MIT", + "dependencies": { + "@jsep-plugin/assignment": "^1.2.1", + "@jsep-plugin/regex": "^1.0.3", + "jsep": "^1.3.9" + }, + "bin": { + "jsonpath": "bin/jsonpath-cli.js", + "jsonpath-plus": "bin/jsonpath-cli.js" + }, "engines": { - "node": ">=12.0.0" + "node": ">=18.0.0" } }, "node_modules/jsonpointer": { @@ -12388,9 +12410,9 @@ "license": "MIT" }, "node_modules/nimma": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.2.tgz", - "integrity": "sha512-V52MLl7BU+tH2Np9tDrIXK8bql3MVUadnMIl/0/oZSGC9keuro0O9UUv9QKp0aMvtN8HRew4G7byY7H4eWsxaQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/nimma/-/nimma-0.2.3.tgz", + "integrity": "sha512-1ZOI8J+1PKKGceo/5CT5GfQOG6H8I2BencSK06YarZ2wXwH37BSSUWldqJmMJYA5JfqDqffxDXynt6f11AyKcA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -12403,21 +12425,10 @@ "node": "^12.20 || >=14.13" }, "optionalDependencies": { - "jsonpath-plus": "^6.0.1", + "jsonpath-plus": "^6.0.1 || ^10.1.0", "lodash.topath": "^4.5.2" } }, - "node_modules/nimma/node_modules/jsonpath-plus": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-6.0.1.tgz", - "integrity": "sha512-EvGovdvau6FyLexFH2OeXfIITlgIbgZoAZe3usiySeaIDm5QS+A10DKNpaPBBqqRSZr2HN6HVNXxtwUAr2apEw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/node-fetch": { "version": "2.6.13", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", @@ -13083,9 +13094,9 @@ } }, "node_modules/postcss": { - "version": "8.4.48", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.48.tgz", - "integrity": "sha512-GCRK8F6+Dl7xYniR5a4FYbpBzU8XnZVeowqsQFYdcXuSbChgiks7qybSkbvnaeqv0G0B+dd9/jJgH8kkLDQeEA==", + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", "funding": [ { "type": "opencollective", @@ -14487,13 +14498,13 @@ } }, "node_modules/simple-eval": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.0.tgz", - "integrity": "sha512-kpKJR+bqTscgC0xuAl2xHN6bB12lHjC2DCUfqjAx19bQyO3R2EVLOurm3H9AUltv/uFVcSCVNc6faegR+8NYLw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-eval/-/simple-eval-1.0.1.tgz", + "integrity": "sha512-LH7FpTAkeD+y5xQC4fzS+tFtaNlvt3Ib1zKzvhjv/Y+cioV4zIuw4IZr2yhRLu67CWL7FR9/6KXKnjRoZTvGGQ==", "dev": true, "license": "MIT", "dependencies": { - "jsep": "^1.1.2" + "jsep": "^1.3.6" }, "engines": { "node": ">=12" @@ -15893,15 +15904,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.13.0.tgz", - "integrity": "sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.14.0.tgz", + "integrity": "sha512-K8fBJHxVL3kxMmwByvz8hNdBJ8a0YqKzKDX6jRlrjMuNXyd5T2V02HIq37+OiWXvUUOXgOOGiSSOh26Mh8pC3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.13.0", - "@typescript-eslint/parser": "8.13.0", - "@typescript-eslint/utils": "8.13.0" + "@typescript-eslint/eslint-plugin": "8.14.0", + "@typescript-eslint/parser": "8.14.0", + "@typescript-eslint/utils": "8.14.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index ea2a132df5..f76fbf0764 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "monaco-editor": "0.51.0", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", - "postcss": "8.4.48", + "postcss": "8.4.49", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "pretty-ms": "9.0.0", @@ -59,21 +59,21 @@ "wrap-ansi": "9.0.0" }, "devDependencies": { - "@axe-core/playwright": "4.10.0", + "@axe-core/playwright": "4.10.1", "@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@playwright/test": "1.48.2", - "@stoplight/spectral-cli": "6.13.1", + "@stoplight/spectral-cli": "6.14.0", "@stylistic/eslint-plugin-js": "2.10.1", "@stylistic/stylelint-plugin": "3.1.1", - "@typescript-eslint/parser": "8.13.0", + "@typescript-eslint/parser": "8.14.0", "@vitejs/plugin-vue": "5.1.5", "@vitest/coverage-v8": "2.1.4", - "@vitest/eslint-plugin": "1.1.7", + "@vitest/eslint-plugin": "1.1.10", "@vue/test-utils": "2.4.6", "eslint": "9.14.0", "eslint-import-resolver-typescript": "3.6.3", "eslint-plugin-array-func": "5.0.2", - "eslint-plugin-import-x": "4.4.0", + "eslint-plugin-import-x": "4.4.2", "eslint-plugin-no-jquery": "3.0.2", "eslint-plugin-no-use-extend-native": "0.7.2", "eslint-plugin-playwright": "2.0.1", @@ -81,11 +81,11 @@ "eslint-plugin-sonarjs": "2.0.4", "eslint-plugin-unicorn": "56.0.0", "eslint-plugin-vitest-globals": "1.5.0", - "eslint-plugin-vue": "9.30.0", + "eslint-plugin-vue": "9.31.0", "eslint-plugin-vue-scoped-css": "2.8.1", "eslint-plugin-wc": "2.2.0", "globals": "15.12.0", - "happy-dom": "15.11.0", + "happy-dom": "15.11.4", "license-checker-rseidelsohn": "4.4.2", "markdownlint-cli": "0.42.0", "postcss-html": "1.7.0", @@ -95,7 +95,7 @@ "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.2.0", "typescript": "5.6.3", - "typescript-eslint": "8.13.0", + "typescript-eslint": "8.14.0", "vite-string-plugin": "1.3.4", "vitest": "2.1.4" }, diff --git a/playwright.config.ts b/playwright.config.ts index 194f7f7d36..0994c55045 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,6 +1,6 @@ -import {devices} from '@playwright/test'; +import {devices, type PlaywrightTestConfig} from '@playwright/test'; -const BASE_URL = process.env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localhost:3000'; +const BASE_URL = process.env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localhost:3003'; /** * @see https://playwright.dev/docs/test-configuration @@ -8,11 +8,11 @@ const BASE_URL = process.env.GITEA_URL?.replace?.(/\/$/g, '') || 'http://localho */ export default { testDir: './tests/e2e/', - testMatch: /.*\.test\.e2e\.js/, // Match any .test.e2e.js files + testMatch: /.*\.test\.e2e\.ts/, // Match any .test.e2e.js files // you can adjust this value locally to match your machine's power, // or pass `--workers x` to playwright - workers: process.env.CI ? 1 : 2, + workers: 1, /* Maximum time one test can run for. */ timeout: 30 * 1000, @@ -22,7 +22,7 @@ export default { * Maximum time expect() should wait for the condition to be met. * For example in `await expect(locator).toHaveText();` */ - timeout: 2000, + timeout: 3000, }, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -30,6 +30,8 @@ export default { /* Retry on CI only */ retries: process.env.CI ? 1 : 0, + /* fail fast */ + maxFailures: process.env.CI ? 1 : 0, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: process.env.CI ? 'list' : [['list'], ['html', {outputFolder: 'tests/e2e/reports/', open: 'never'}]], @@ -41,7 +43,7 @@ export default { locale: 'en-US', /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ - actionTimeout: 2000, + actionTimeout: 3000, /* Maximum time allowed for navigation, such as `page.goto()`. */ navigationTimeout: 10 * 1000, @@ -95,8 +97,8 @@ export default { }, ], - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + /* Folder for test artifacts created during test execution such as screenshots, traces, etc. */ outputDir: 'tests/e2e/test-artifacts/', - /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + /* Folder for explicit snapshots for visual testing */ snapshotDir: 'tests/e2e/test-snapshots/', -}; +} satisfies PlaywrightTestConfig; diff --git a/renovate.json b/renovate.json index cd927797f5..4e869db579 100644 --- a/renovate.json +++ b/renovate.json @@ -16,7 +16,7 @@ "prConcurrentLimit": 10, "osvVulnerabilityAlerts": true, "automergeStrategy": "squash", - "labels": ["dependency-upgrade"], + "labels": ["dependency-upgrade","test/not-needed"], "packageRules": [ { "description": "Require approval for python minor version", @@ -144,8 +144,9 @@ }, { "description": "Automerge some packages when CI succeeds", - "extends": ["packages:linters", "packages:test"], + "extends": ["packages:linters", "packages:test", "schedule:monthly"], "matchPackageNames": [ + "@axe-core/playwright", "@eslint-community/**", "@playwright/**", "@stoplight/spectral-cli", diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 4f93cfb64a..81dc0bf832 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -77,7 +77,7 @@ and playwright to perform tests on it. > (e.g. when only creating new content), > or that they restore the initial state for the next browser run. -#### With the playwright UI: +#### With the playwright UI: Playwright ships with an integrated UI mode which allows you to run individual tests and to debug them by seeing detailed traces of what playwright does. @@ -90,7 +90,7 @@ npx playwright test --ui #### Running individual tests ``` -npx playwright test actions.test.e2e.js:9 +npx playwright test actions.test.e2e.ts:9 ``` First, specify the complete test filename, @@ -144,7 +144,7 @@ TEST_PGSQL_HOST=localhost:5432 TEST_PGSQL_DBNAME=test TEST_PGSQL_USERNAME=postgr ### Running individual tests -Example command to run `example.test.e2e.js` test file: +Example command to run `example.test.e2e.ts` test file: > **Note** > Unlike integration tests, this filtering is at the file level, not function @@ -175,6 +175,70 @@ ACCEPT_VISUAL=1 will overwrite the snapshot images with new images. If you know noteworthy tests that can act as an inspiration for new tests, please add some details here. +### Understanding and waiting for page loads + +[Waiting for a load state](https://playwright.dev/docs/api/class-frame#frame-wait-for-load-state) +sound like a convenient way to ensure the page was loaded, +but it only works once and consecutive calls to it +(e.g. after clicking a button which should reload a page) +return immediately without waiting for *another* load event. + +If you match something which is on both the old and the new page, +you might succeed before the page was reloaded, +although the code using a `waitForLoadState` might intuitively suggest +the page was changed before. + +Interacting with the page before the reload +(e.g. by opening a dropdown) +might then race and result in flaky tests, +depending on the speed of the hardware running the test. + +A possible way to test that an interaction worked is by checking for a known change first. +For example: + +- you submit a form and you want to check that the content persisted +- checking for the content directly would succeed even without a page reload +- check for a success message first (will wait until it appears), then verify the content + +Alternatively, if you know the backend request that will be made before the reload, +you can explicitly wait for it: + +~~~js +const submitted = page.waitForResponse('/my/backend/post/request'); +await page.locator('button').first().click(); // perform your interaction +await submitted; +~~~ + +If the page redirects to another URL, +you can alternatively use: + +~~~js +await page.waitForURL('**/target.html'); +~~~ + +### Only sign in if necessary + +Signing in takes time and is actually executed step-by-step. +If your test does not rely on a user account, skip this step. + +~~~js +test('For anyone', async ({page}) => { + await page.goto('/somepage'); +~~~ + +If you need a user account, you can use something like: + +~~~js +import {test, login_user, login} from './utils_e2e.ts'; + +test.beforeAll(async ({browser}, workerInfo) => { + await login_user(browser, workerInfo, 'user2'); // or another user +}); + +test('For signed users only', async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); +~~~ + ### Run tests very selectively Browser testing can take some time. @@ -211,7 +275,7 @@ Feel free to improve the logic used there if you need more advanced functionalit If you can, perform automated accessibility testing using [AxeCore](https://github.com/dequelabs/axe-core-npm/blob/develop/packages/playwright/README.md). -Take a look at `shared/forms.js` and some other places for inspiration. +Take a look at `shared/forms.ts` and some other places for inspiration. ### List related files coverage @@ -264,3 +328,27 @@ and a set of files with a certain ending: The patterns are evaluated on a "first-match" basis. Under the hood, [gobwas/glob](https://github.com/gobwas/glob) is used. + +## Grouped retry for interactions + +Sometimes, it can be necessary to retry certain interactions together. +Consider the following procedure: + +1. click to open a dropdown +2. interact with content in the dropdown + +When for some reason the dropdown does not open, +for example because of it taking time to initialize after page load, +the click will succeed, +but the depending interaction won't, +although playwright repeatedly tries to find the content. + +You can [group statements using toPass]()https://playwright.dev/docs/test-assertions#expecttopass). +This code retries the dropdown click until the second item is found. + +~~~js +await expect(async () => { + await page.locator('.dropdown').click(); + await page.locator('.dropdown .item').first().click(); +}).toPass(); +~~~ diff --git a/tests/e2e/actions.test.e2e.js b/tests/e2e/actions.test.e2e.ts similarity index 93% rename from tests/e2e/actions.test.e2e.js rename to tests/e2e/actions.test.e2e.ts index 01ddb7b971..0aa1c747dc 100644 --- a/tests/e2e/actions.test.e2e.js +++ b/tests/e2e/actions.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // templates/repo/actions/** // web_src/css/actions.css @@ -12,7 +10,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; +import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); @@ -46,7 +44,6 @@ test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) => const page = await context.newPage(); await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); - await page.waitForLoadState('networkidle'); await page.locator('#workflow_dispatch_dropdown>button').click(); @@ -57,7 +54,6 @@ test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) => }); await page.locator('#workflow-dispatch-submit').click(); - await page.waitForLoadState('networkidle'); await expect(page.getByText('Require value for input "String w/o. default".')).toBeVisible(); }); @@ -70,13 +66,11 @@ test('workflow dispatch success', async ({browser}, workerInfo) => { const page = await context.newPage(); await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); - await page.waitForLoadState('networkidle'); await page.locator('#workflow_dispatch_dropdown>button').click(); await page.type('input[name="inputs[string2]"]', 'abc'); await page.locator('#workflow-dispatch-submit').click(); - await page.waitForLoadState('networkidle'); await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible(); @@ -85,7 +79,6 @@ test('workflow dispatch success', async ({browser}, workerInfo) => { test('workflow dispatch box not available for unauthenticated users', async ({page}) => { await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0'); - await page.waitForLoadState('networkidle'); await expect(page.locator('body')).not.toContainText(workflow_trigger_notification_text); }); diff --git a/tests/e2e/changes.go b/tests/e2e/changes.go index acc1a796a4..1e5533bc6b 100644 --- a/tests/e2e/changes.go +++ b/tests/e2e/changes.go @@ -29,15 +29,16 @@ func initChangedFiles() { globalPatterns := []string{ // meta and config "Makefile", - "playwright.config.js", + "playwright.config.ts", ".forgejo/workflows/testing.yml", "tests/e2e/*.go", "tests/e2e/shared/*", // frontend files - "frontend/*.js", - "frontend/{base,index}.css", - // templates + "web_src/js/{index,utils}.*", + "web_src/css/{base,index}.css", + // templates and helpers "templates/base/**", + "modules/templates/**", } fullRunPatterns := []glob.Glob{} for _, expr := range globalPatterns { diff --git a/tests/e2e/dashboard-ci-status.test.e2e.js b/tests/e2e/dashboard-ci-status.test.e2e.ts similarity index 83% rename from tests/e2e/dashboard-ci-status.test.e2e.js rename to tests/e2e/dashboard-ci-status.test.e2e.ts index 289430055c..531955209a 100644 --- a/tests/e2e/dashboard-ci-status.test.e2e.js +++ b/tests/e2e/dashboard-ci-status.test.e2e.ts @@ -1,11 +1,9 @@ -// @ts-check - // @watch start // web_src/js/components/DashboardRepoList.vue // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; +import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); @@ -17,10 +15,9 @@ test('Correct link and tooltip', async ({browser}, workerInfo) => { const response = await page.goto('/?repo-search-query=test_workflows'); expect(response?.status()).toBe(200); - await page.waitForLoadState('networkidle'); - const repoStatus = page.locator('.dashboard-repos .repo-owner-name-list > li:nth-child(1) > a:nth-child(2)'); - + // wait for network activity to cease (so status was loaded in frontend) + await page.waitForLoadState('networkidle'); // eslint-disable-line playwright/no-networkidle await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000}); await expect(repoStatus).toHaveAttribute('data-tooltip-content', 'Failure'); }); diff --git a/tests/e2e/e2e_test.go b/tests/e2e/e2e_test.go index 3e1bcefd66..b8c89625c0 100644 --- a/tests/e2e/e2e_test.go +++ b/tests/e2e/e2e_test.go @@ -79,7 +79,7 @@ func TestMain(m *testing.M) { // TestE2e should be the only test e2e necessary. It will collect all "*.test.e2e.js" files in this directory and build a test for each. func TestE2e(t *testing.T) { // Find the paths of all e2e test files in test directory. - searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.js") + searchGlob := filepath.Join(filepath.Dir(setting.AppPath), "tests", "e2e", "*.test.e2e.ts") paths, err := filepath.Glob(searchGlob) if err != nil { t.Fatal(err) diff --git a/tests/e2e/example.test.e2e.js b/tests/e2e/example.test.e2e.ts similarity index 96% rename from tests/e2e/example.test.e2e.js rename to tests/e2e/example.test.e2e.ts index a413a218c6..90fd9169a4 100644 --- a/tests/e2e/example.test.e2e.js +++ b/tests/e2e/example.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // templates/user/auth/** // web_src/js/features/user-** @@ -7,7 +5,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, save_visual} from './utils_e2e.js'; +import {test, login_user, save_visual} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); diff --git a/tests/e2e/explore.test.e2e.js b/tests/e2e/explore.test.e2e.ts similarity index 96% rename from tests/e2e/explore.test.e2e.js rename to tests/e2e/explore.test.e2e.ts index eb0c723f36..44c9b21f58 100644 --- a/tests/e2e/explore.test.e2e.js +++ b/tests/e2e/explore.test.e2e.ts @@ -1,4 +1,3 @@ -// @ts-check // document is a global in evaluate, so it's safe to ignore here // eslint playwright/no-conditional-in-test: 0 @@ -8,7 +7,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test} from './utils_e2e.js'; +import {test} from './utils_e2e.ts'; test('Explore view taborder', async ({page}) => { await page.goto('/explore/repos'); diff --git a/tests/e2e/issue-comment.test.e2e.js b/tests/e2e/issue-comment.test.e2e.ts similarity index 98% rename from tests/e2e/issue-comment.test.e2e.js rename to tests/e2e/issue-comment.test.e2e.ts index 8a1e48d75f..9a3a45f522 100644 --- a/tests/e2e/issue-comment.test.e2e.js +++ b/tests/e2e/issue-comment.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // web_src/js/features/comp/** // web_src/js/features/repo-** @@ -7,7 +5,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, login} from './utils_e2e.js'; +import {test, login_user, login} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); @@ -58,7 +56,7 @@ test('Always focus edit tab first on edit', async ({browser}, workerInfo) => { await page.locator('#issue-1 .comment-container a[data-tab-for=markdown-previewer]').click(); await page.click('#issue-1 .comment-container .save'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState(); // Edit again and assert that edit tab should be active (and not preview tab) await page.click('#issue-1 .comment-container .context-menu'); diff --git a/tests/e2e/issue-sidebar.test.e2e.js b/tests/e2e/issue-sidebar.test.e2e.ts similarity index 62% rename from tests/e2e/issue-sidebar.test.e2e.js rename to tests/e2e/issue-sidebar.test.e2e.ts index 8b5fa59331..422f3ef94e 100644 --- a/tests/e2e/issue-sidebar.test.e2e.js +++ b/tests/e2e/issue-sidebar.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // templates/repo/issue/view_content/** // web_src/css/repo/issue-** @@ -7,98 +5,143 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, login} from './utils_e2e.js'; +import {test, login_user, login} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); }); -// belongs to test: Pull: Toggle WIP -const prTitle = 'pull5'; - -async function click_toggle_wip({page}) { - await page.locator('.toggle-wip>a').click(); - await page.waitForLoadState('networkidle'); -} - -async function check_wip({page}, is) { - const elemTitle = '#issue-title-display'; - const stateLabel = '.issue-state-label'; - await expect(page.locator(elemTitle)).toContainText(prTitle); - await expect(page.locator(elemTitle)).toContainText('#5'); - if (is) { - await expect(page.locator(elemTitle)).toContainText('WIP'); - await expect(page.locator(stateLabel)).toContainText('Draft'); - } else { - await expect(page.locator(elemTitle)).not.toContainText('WIP'); - await expect(page.locator(stateLabel)).toContainText('Open'); +/* eslint-disable playwright/expect-expect */ +// some tests are reported to have no assertions, +// which is not correct, because they use the global helper function +test.describe('Pull: Toggle WIP', () => { + const prTitle = 'pull5'; + async function toggle_wip_to({page}, should) { + await page.waitForLoadState('domcontentloaded'); + if (should) { + await page.getByText('Still in progress?').click(); + } else { + await page.getByText('Ready for review?').click(); + } } -} -test('Pull: Toggle WIP', async ({browser}, workerInfo) => { - test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); - const page = await login({browser}, workerInfo); - const response = await page.goto('/user2/repo1/pulls/5'); - expect(response?.status()).toBe(200); // Status OK - // initial state - await check_wip({page}, false); - // toggle to WIP - await click_toggle_wip({page}); - await check_wip({page}, true); - // remove WIP - await click_toggle_wip({page}); - await check_wip({page}, false); + async function check_wip({page}, is) { + const elemTitle = 'h1'; + const stateLabel = '.issue-state-label'; + await page.waitForLoadState('domcontentloaded'); + await expect(page.locator(elemTitle)).toContainText(prTitle); + await expect(page.locator(elemTitle)).toContainText('#5'); + if (is) { + await expect(page.locator(elemTitle)).toContainText('WIP'); + await expect(page.locator(stateLabel)).toContainText('Draft'); + } else { + await expect(page.locator(elemTitle)).not.toContainText('WIP'); + await expect(page.locator(stateLabel)).toContainText('Open'); + } + } - // manually edit title to another prefix - await page.locator('#issue-title-edit-show').click(); - await page.locator('#issue-title-editor input').fill(`[WIP] ${prTitle}`); - await page.getByText('Save').click(); - await page.waitForLoadState('networkidle'); - await check_wip({page}, true); - // remove again - await click_toggle_wip({page}); - await check_wip({page}, false); - // check maximum title length is handled gracefully - const maxLenStr = prTitle + 'a'.repeat(240); - await page.locator('#issue-title-edit-show').click(); - await page.locator('#issue-title-editor input').fill(maxLenStr); - await page.getByText('Save').click(); - await page.waitForLoadState('networkidle'); - await click_toggle_wip({page}); - await check_wip({page}, true); - await click_toggle_wip({page}); - await check_wip({page}, false); - await expect(page.locator('h1')).toContainText(maxLenStr); - // restore original title - await page.locator('#issue-title-edit-show').click(); - await page.locator('#issue-title-editor input').fill(prTitle); - await page.getByText('Save').click(); - await check_wip({page}, false); + test.beforeEach(async ({browser}, workerInfo) => { + const page = await login({browser}, workerInfo); + const response = await page.goto('/user2/repo1/pulls/5'); + expect(response?.status()).toBe(200); // Status OK + // ensure original title + await page.locator('#issue-title-edit-show').click(); + await page.locator('#issue-title-editor input').fill(prTitle); + await page.getByText('Save').click(); + await check_wip({page}, false); + }); + + test('simple toggle', async ({browser}, workerInfo) => { + test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); + const page = await login({browser}, workerInfo); + await page.goto('/user2/repo1/pulls/5'); + // toggle to WIP + await toggle_wip_to({page}, true); + await check_wip({page}, true); + // remove WIP + await toggle_wip_to({page}, false); + await check_wip({page}, false); + }); + + test('manual edit', async ({browser}, workerInfo) => { + test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); + const page = await login({browser}, workerInfo); + await page.goto('/user2/repo1/pulls/5'); + // manually edit title to another prefix + await page.locator('#issue-title-edit-show').click(); + await page.locator('#issue-title-editor input').fill(`[WIP] ${prTitle}`); + await page.getByText('Save').click(); + await check_wip({page}, true); + // remove again + await toggle_wip_to({page}, false); + await check_wip({page}, false); + }); + + test('maximum title length', async ({browser}, workerInfo) => { + test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); + const page = await login({browser}, workerInfo); + await page.goto('/user2/repo1/pulls/5'); + // check maximum title length is handled gracefully + const maxLenStr = prTitle + 'a'.repeat(240); + await page.locator('#issue-title-edit-show').click(); + await page.locator('#issue-title-editor input').fill(maxLenStr); + await page.getByText('Save').click(); + await expect(page.locator('h1')).toContainText(maxLenStr); + await check_wip({page}, false); + await toggle_wip_to({page}, true); + await check_wip({page}, true); + await expect(page.locator('h1')).toContainText(maxLenStr); + await toggle_wip_to({page}, false); + await check_wip({page}, false); + await expect(page.locator('h1')).toContainText(maxLenStr); + }); }); +/* eslint-enable playwright/expect-expect */ test('Issue: Labels', async ({browser}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636'); + + async function submitLabels({page}) { + const submitted = page.waitForResponse('/user2/repo1/issues/labels'); + await page.locator('textarea').first().click(); // close via unrelated element + await submitted; + await page.waitForLoadState(); + } + const page = await login({browser}, workerInfo); // select label list in sidebar only const labelList = page.locator('.issue-content-right .labels-list a'); const response = await page.goto('/user2/repo1/issues/1'); expect(response?.status()).toBe(200); - // preconditions - await expect(labelList.filter({hasText: 'label1'})).toBeVisible(); + + // restore initial state + await page.locator('.select-label').click(); + const responsePromise = page.waitForResponse('/user2/repo1/issues/labels'); + await page.getByText('Clear labels').click(); + await responsePromise; + await expect(labelList.filter({hasText: 'label1'})).toBeHidden(); await expect(labelList.filter({hasText: 'label2'})).toBeHidden(); - // add label2 + + // add both labels await page.locator('.select-label').click(); // label search could be tested this way: // await page.locator('.select-label input').fill('label2'); await page.locator('.select-label .item').filter({hasText: 'label2'}).click(); - await page.locator('.select-label').click(); - await page.waitForLoadState('networkidle'); + await page.locator('.select-label .item').filter({hasText: 'label1'}).click(); + await submitLabels({page}); await expect(labelList.filter({hasText: 'label2'})).toBeVisible(); - // test removing label again - await page.locator('.select-label').click(); - await page.locator('.select-label .item').filter({hasText: 'label2'}).click(); - await page.locator('.select-label').click(); - await page.waitForLoadState('networkidle'); + await expect(labelList.filter({hasText: 'label1'})).toBeVisible(); + + // test removing label2 again + // due to a race condition, the page could still be "reloading", + // closing the dropdown after it was clicked. + // Retry the interaction as a group + // also see https://playwright.dev/docs/test-assertions#expecttopass + await expect(async () => { + await page.locator('.select-label').click(); + await page.locator('.select-label .item').filter({hasText: 'label2'}).click(); + }).toPass(); + await submitLabels({page}); await expect(labelList.filter({hasText: 'label2'})).toBeHidden(); await expect(labelList.filter({hasText: 'label1'})).toBeVisible(); }); @@ -111,11 +154,6 @@ test('Issue: Assignees', async ({browser}, workerInfo) => { const response = await page.goto('/org3/repo3/issues/1'); expect(response?.status()).toBe(200); - // preconditions - await expect(assigneesList.filter({hasText: 'user2'})).toBeVisible(); - await expect(assigneesList.filter({hasText: 'user4'})).toBeHidden(); - await expect(page.locator('.ui.assignees.list .item.no-select')).toBeHidden(); - // Clear all assignees await page.locator('.select-assignees-modify.dropdown').click(); await page.locator('.select-assignees-modify.dropdown .no-select.item').click(); diff --git a/tests/e2e/markdown-editor.test.e2e.js b/tests/e2e/markdown-editor.test.e2e.ts similarity index 99% rename from tests/e2e/markdown-editor.test.e2e.js rename to tests/e2e/markdown-editor.test.e2e.ts index 61f6fbcd4e..6272731620 100644 --- a/tests/e2e/markdown-editor.test.e2e.js +++ b/tests/e2e/markdown-editor.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // web_src/js/features/comp/ComboMarkdownEditor.js // web_src/css/editor/combomarkdowneditor.css @@ -7,7 +5,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, load_logged_in_context, login_user} from './utils_e2e.js'; +import {test, load_logged_in_context, login_user} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); diff --git a/tests/e2e/markup.test.e2e.js b/tests/e2e/markup.test.e2e.ts similarity index 85% rename from tests/e2e/markup.test.e2e.js rename to tests/e2e/markup.test.e2e.ts index f7ec31709d..2726942d57 100644 --- a/tests/e2e/markup.test.e2e.js +++ b/tests/e2e/markup.test.e2e.ts @@ -1,16 +1,13 @@ -// @ts-check - // @watch start // web_src/css/markup/** // @watch end import {expect} from '@playwright/test'; -import {test} from './utils_e2e.js'; +import {test} from './utils_e2e.ts'; test('markup with #xyz-mode-only', async ({page}) => { const response = await page.goto('/user2/repo1/issues/1'); expect(response?.status()).toBe(200); - await page.waitForLoadState('networkidle'); const comment = page.locator('.comment-body>.markup', {hasText: 'test markup light/dark-mode-only'}); await expect(comment).toBeVisible(); diff --git a/tests/e2e/org-settings.test.e2e.js b/tests/e2e/org-settings.test.e2e.ts similarity index 89% rename from tests/e2e/org-settings.test.e2e.js rename to tests/e2e/org-settings.test.e2e.ts index 21f34c123d..b645d94161 100644 --- a/tests/e2e/org-settings.test.e2e.js +++ b/tests/e2e/org-settings.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // templates/org/team/new.tmpl // web_src/css/form.css @@ -7,8 +5,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, login} from './utils_e2e.js'; -import {validate_form} from './shared/forms.js'; +import {test, login_user, login} from './utils_e2e.ts'; +import {validate_form} from './shared/forms.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); diff --git a/tests/e2e/profile_actions.test.e2e.js b/tests/e2e/profile_actions.test.e2e.ts similarity index 96% rename from tests/e2e/profile_actions.test.e2e.js rename to tests/e2e/profile_actions.test.e2e.ts index d168037041..51a690aa60 100644 --- a/tests/e2e/profile_actions.test.e2e.js +++ b/tests/e2e/profile_actions.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // routers/web/user/** // templates/shared/user/** @@ -7,7 +5,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; +import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; test('Follow actions', async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); @@ -15,7 +13,6 @@ test('Follow actions', async ({browser}, workerInfo) => { const page = await context.newPage(); await page.goto('/user1'); - await page.waitForLoadState('networkidle'); // Check if following and then unfollowing works. // This checks that the event listeners of diff --git a/tests/e2e/reaction-selectors.test.e2e.js b/tests/e2e/reaction-selectors.test.e2e.ts similarity index 99% rename from tests/e2e/reaction-selectors.test.e2e.js rename to tests/e2e/reaction-selectors.test.e2e.ts index 184f25fe18..a52b47e036 100644 --- a/tests/e2e/reaction-selectors.test.e2e.js +++ b/tests/e2e/reaction-selectors.test.e2e.ts @@ -1,12 +1,10 @@ -// @ts-check - // @watch start // web_src/js/features/comp/ReactionSelector.js // routers/web/repo/issue.go // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; +import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); diff --git a/tests/e2e/release.test.e2e.js b/tests/e2e/release.test.e2e.ts similarity index 97% rename from tests/e2e/release.test.e2e.js rename to tests/e2e/release.test.e2e.ts index 76c7ac0212..373f23dfa7 100644 --- a/tests/e2e/release.test.e2e.js +++ b/tests/e2e/release.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // models/repo/attachment.go // modules/structs/attachment.go @@ -11,8 +9,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.js'; -import {validate_form} from './shared/forms.js'; +import {test, login_user, save_visual, load_logged_in_context} from './utils_e2e.ts'; +import {validate_form} from './shared/forms.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); diff --git a/tests/e2e/repo-code.test.e2e.js b/tests/e2e/repo-code.test.e2e.ts similarity index 88% rename from tests/e2e/repo-code.test.e2e.js rename to tests/e2e/repo-code.test.e2e.ts index fdb92762ff..b22670ab76 100644 --- a/tests/e2e/repo-code.test.e2e.js +++ b/tests/e2e/repo-code.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // web_src/js/features/repo-code.js // web_src/css/repo.css @@ -7,11 +5,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; - -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +import {test} from './utils_e2e.ts'; async function assertSelectedLines(page, nums) { const pageAssertions = async () => { @@ -35,10 +29,7 @@ async function assertSelectedLines(page, nums) { return pageAssertions(); } -test('Line Range Selection', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); - +test('Line Range Selection', async ({page}) => { const filePath = '/user2/repo1/src/branch/master/README.md?display=source'; const response = await page.goto(filePath); diff --git a/tests/e2e/repo-commitgraph.test.e2e.js b/tests/e2e/repo-commitgraph.test.e2e.ts similarity index 71% rename from tests/e2e/repo-commitgraph.test.e2e.js rename to tests/e2e/repo-commitgraph.test.e2e.ts index f06c68a55d..5f0cad117a 100644 --- a/tests/e2e/repo-commitgraph.test.e2e.js +++ b/tests/e2e/repo-commitgraph.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // templates/repo/graph.tmpl // web_src/css/features/gitgraph.css @@ -7,11 +5,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; - -test.beforeAll(async ({browser}, workerInfo) => { - await login_user(browser, workerInfo, 'user2'); -}); +import {test} from './utils_e2e.ts'; test('Commit graph overflow', async ({page}) => { await page.goto('/user2/diff-test/graph'); @@ -20,9 +14,7 @@ test('Commit graph overflow', async ({page}) => { await expect(page.locator('.selection.search.dropdown')).toBeInViewport({ratio: 1}); }); -test('Switch branch', async ({browser}, workerInfo) => { - const context = await load_logged_in_context(browser, workerInfo, 'user2'); - const page = await context.newPage(); +test('Switch branch', async ({page}) => { const response = await page.goto('/user2/repo1/graph'); expect(response?.status()).toBe(200); @@ -31,7 +23,7 @@ test('Switch branch', async ({browser}, workerInfo) => { await input.pressSequentially('develop', {delay: 50}); await input.press('Enter'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState(); await expect(page.locator('#loading-indicator')).toBeHidden(); await expect(page.locator('#rel-container')).toBeVisible(); diff --git a/tests/e2e/repo-migrate.test.e2e.js b/tests/e2e/repo-migrate.test.e2e.ts similarity index 98% rename from tests/e2e/repo-migrate.test.e2e.js rename to tests/e2e/repo-migrate.test.e2e.ts index 2ad4400340..c4d1604a6f 100644 --- a/tests/e2e/repo-migrate.test.e2e.js +++ b/tests/e2e/repo-migrate.test.e2e.ts @@ -1,11 +1,9 @@ -// @ts-check - // @watch start // web_src/js/features/repo-migrate.js // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; +import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; test.beforeAll(({browser}, workerInfo) => login_user(browser, workerInfo, 'user2')); diff --git a/tests/e2e/repo-settings.test.e2e.js b/tests/e2e/repo-settings.test.e2e.ts similarity index 95% rename from tests/e2e/repo-settings.test.e2e.js rename to tests/e2e/repo-settings.test.e2e.ts index b4fdc1ae6c..8bd7299182 100644 --- a/tests/e2e/repo-settings.test.e2e.js +++ b/tests/e2e/repo-settings.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // templates/webhook/shared-settings.tmpl // templates/repo/settings/** @@ -9,8 +7,8 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, login} from './utils_e2e.js'; -import {validate_form} from './shared/forms.js'; +import {test, login_user, login} from './utils_e2e.ts'; +import {validate_form} from './shared/forms.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); diff --git a/tests/e2e/repo-wiki.test.e2e.js b/tests/e2e/repo-wiki.test.e2e.ts similarity index 89% rename from tests/e2e/repo-wiki.test.e2e.js rename to tests/e2e/repo-wiki.test.e2e.ts index eb6c033748..f581ecdab6 100644 --- a/tests/e2e/repo-wiki.test.e2e.js +++ b/tests/e2e/repo-wiki.test.e2e.ts @@ -1,17 +1,14 @@ -// @ts-check - // @watch start // templates/repo/wiki/** // web_src/css/repo** // @watch end import {expect} from '@playwright/test'; -import {test} from './utils_e2e.js'; +import {test} from './utils_e2e.ts'; test(`Search for long titles and test for no overflow`, async ({page}, workerInfo) => { test.skip(workerInfo.project.name === 'Mobile Safari', 'Fails as always, see https://codeberg.org/forgejo/forgejo/pulls/5326#issuecomment-2313275'); await page.goto('/user2/repo1/wiki'); - await page.waitForLoadState('networkidle'); await page.getByPlaceholder('Search wiki').fill('spaces'); await page.getByPlaceholder('Search wiki').click(); // workaround: HTMX listens on keyup events, playwright's fill only triggers the input event diff --git a/tests/e2e/right-settings-button.test.e2e.js b/tests/e2e/right-settings-button.test.e2e.ts similarity index 99% rename from tests/e2e/right-settings-button.test.e2e.js rename to tests/e2e/right-settings-button.test.e2e.ts index 87e10c040c..bfb1800a27 100644 --- a/tests/e2e/right-settings-button.test.e2e.js +++ b/tests/e2e/right-settings-button.test.e2e.ts @@ -1,5 +1,3 @@ -// @ts-check - // @watch start // templates/org/** // templates/repo/** @@ -7,7 +5,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, login_user, load_logged_in_context} from './utils_e2e.js'; +import {test, login_user, load_logged_in_context} from './utils_e2e.ts'; test.beforeAll(async ({browser}, workerInfo) => { await login_user(browser, workerInfo, 'user2'); diff --git a/tests/e2e/shared/forms.js b/tests/e2e/shared/forms.ts similarity index 92% rename from tests/e2e/shared/forms.js rename to tests/e2e/shared/forms.ts index 5775c40826..52432ccbe8 100644 --- a/tests/e2e/shared/forms.js +++ b/tests/e2e/shared/forms.ts @@ -1,8 +1,7 @@ -import {expect} from '@playwright/test'; +import {expect, type Page} from '@playwright/test'; import {AxeBuilder} from '@axe-core/playwright'; -export async function validate_form({page}, scope) { - scope ??= 'form'; +export async function validate_form({page}: {page: Page}, scope: 'form' | 'fieldset' = 'form') { const accessibilityScanResults = await new AxeBuilder({page}) // disable checking for link style - should be fixed, but not now .disableRules('link-in-text-block') diff --git a/tests/e2e/utils_e2e.js b/tests/e2e/utils_e2e.ts similarity index 78% rename from tests/e2e/utils_e2e.js rename to tests/e2e/utils_e2e.ts index 7cfd7388ca..89dacce8a4 100644 --- a/tests/e2e/utils_e2e.js +++ b/tests/e2e/utils_e2e.ts @@ -1,4 +1,4 @@ -import {expect, test as baseTest} from '@playwright/test'; +import {expect, test as baseTest, type Browser, type BrowserContextOptions, type APIRequestContext, type TestInfo, type Page} from '@playwright/test'; export const test = baseTest.extend({ context: async ({browser}, use) => { @@ -6,7 +6,7 @@ export const test = baseTest.extend({ }, }); -async function test_context(browser, options) { +async function test_context(browser: Browser, options?: BrowserContextOptions) { const context = await browser.newContext(options); context.on('page', (page) => { @@ -21,7 +21,7 @@ const LOGIN_PASSWORD = 'password'; // log in user and store session info. This should generally be // run in test.beforeAll(), then the session can be loaded in tests. -export async function login_user(browser, workerInfo, user) { +export async function login_user(browser: Browser, workerInfo: TestInfo, user: string) { test.setTimeout(60000); // Set up a new context const context = await test_context(browser); @@ -37,7 +37,7 @@ export async function login_user(browser, workerInfo, user) { await page.type('input[name=password]', LOGIN_PASSWORD); await page.click('form button.ui.primary.button:visible'); - await page.waitForLoadState('networkidle'); + await page.waitForLoadState(); expect(page.url(), {message: `Failed to login user ${user}`}).toBe(`${workerInfo.project.use.baseURL}/`); @@ -47,7 +47,7 @@ export async function login_user(browser, workerInfo, user) { return context; } -export async function load_logged_in_context(browser, workerInfo, user) { +export async function load_logged_in_context(browser: Browser, workerInfo: TestInfo, user: string) { let context; try { context = await test_context(browser, {storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`}); @@ -59,15 +59,15 @@ export async function load_logged_in_context(browser, workerInfo, user) { return context; } -export async function login({browser}, workerInfo) { +export async function login({browser}: {browser: Browser}, workerInfo: TestInfo) { const context = await load_logged_in_context(browser, workerInfo, 'user2'); - return await context.newPage(); + return await context?.newPage(); } -export async function save_visual(page) { +export async function save_visual(page: Page) { // Optionally include visual testing if (process.env.VISUAL_TEST) { - await page.waitForLoadState('networkidle'); + await page.waitForLoadState('domcontentloaded'); // Mock page/version string await page.locator('footer div.ui.left').evaluate((node) => node.innerHTML = 'MOCK'); await expect(page).toHaveScreenshot({ @@ -83,7 +83,7 @@ export async function save_visual(page) { // Create a temporary user and login to that user and store session info. // This should ideally run on a per test basis. -export async function create_temp_user(browser, workerInfo, request) { +export async function create_temp_user(browser: Browser, workerInfo: TestInfo, request: APIRequestContext) { const username = globalThis.crypto.randomUUID(); const newUser = await request.post(`/api/v1/admin/users`, { headers: { diff --git a/tests/e2e/webauthn.test.e2e.js b/tests/e2e/webauthn.test.e2e.ts similarity index 81% rename from tests/e2e/webauthn.test.e2e.js rename to tests/e2e/webauthn.test.e2e.ts index 7168de223a..c351b6a468 100644 --- a/tests/e2e/webauthn.test.e2e.js +++ b/tests/e2e/webauthn.test.e2e.ts @@ -1,6 +1,5 @@ // Copyright 2024 The Forgejo Authors. All rights reserved. // SPDX-License-Identifier: MIT -// @ts-check // @watch start // templates/user/auth/** @@ -9,7 +8,7 @@ // @watch end import {expect} from '@playwright/test'; -import {test, create_temp_user} from './utils_e2e.js'; +import {test, create_temp_user, login_user} from './utils_e2e.ts'; test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => { test.skip(workerInfo.project.name !== 'chromium', 'Uses Chrome protocol'); @@ -31,7 +30,7 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => transport: 'usb', automaticPresenceSimulation: true, isUserVerified: true, - backupEligibility: true, + backupEligibility: true, // TODO: this doesn't seem to be available?! }, }); @@ -39,8 +38,10 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => await page.getByText('Add security key').click(); // Logout. - await page.locator('div[aria-label="Profile and settingsā€¦"]').click(); - await page.getByText('Sign Out').click(); + await expect(async () => { + await page.locator('div[aria-label="Profile and settingsā€¦"]').click(); + await page.getByText('Sign Out').click(); + }).toPass(); await page.waitForURL(`${workerInfo.project.use.baseURL}/`); // Login. @@ -58,5 +59,8 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) => expect(response?.status()).toBe(200); await page.getByRole('button', {name: 'Remove'}).click(); await page.getByRole('button', {name: 'Yes'}).click(); - await page.waitForURL(`${workerInfo.project.use.baseURL}/user/settings/security`); + await page.waitForLoadState(); + + // verify the user can login without a key + await login_user(browser, workerInfo, username); }); diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 9cf2d4305f..dfd1f75b3c 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -328,7 +328,9 @@ jobs: sha, err := baseGitRepo.GetRefCommitID(pr.GetGitRefName()) require.NoError(t, err) // verify the commit status changes to CommitStatusSuccess when the job changes to StatusSuccess - assert.True(t, checkCommitStatus(sha, context, api.CommitStatusPending)) + require.Eventually(t, func() bool { + return checkCommitStatus(sha, context, api.CommitStatusPending) + }, 30*time.Second, 1*time.Second) for _, actionRun := range actionRuns { // verify the expected ActionRunJob was created and is StatusWaiting job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{RunID: actionRun.ID, CommitSHA: sha}) @@ -339,7 +341,9 @@ jobs: actions_service.CreateCommitStatus(db.DefaultContext, job) } // verify the commit status changed to CommitStatusSuccess because the job(s) changed to StatusSuccess - assert.True(t, checkCommitStatus(sha, context, api.CommitStatusSuccess)) + require.Eventually(t, func() bool { + return checkCommitStatus(sha, context, api.CommitStatusSuccess) + }, 30*time.Second, 1*time.Second) testCase.assert(t, sha, testCase.onType, testCase.action, actionRuns) })