diff --git a/.eslintignore b/.eslintignore index 82855125d..c91bdd9bc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,6 @@ wt test/e2e/report test/e2e/static/basic-report tmp +coverage/** +tsc-out +test/browser-env/report/** diff --git a/.eslintrc.js b/.eslintrc.js index 89a2aae12..64f949094 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,52 @@ module.exports = { ecmaVersion: 2022, }, overrides: [ + { + files: ["src/browser/isomorphic/*.ts"], + rules: { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + patterns: ["../**"], + }, + ], + }, + }, + { + files: ["src/**/*.ts"], + excludedFiles: ["src/browser/client-scripts/**"], + rules: { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["**/client-scripts/**"], + allowTypeImports: true, + message: + "Imports from client-scripts are forbidden. Use type-only imports when needed.", + }, + ], + }, + ], + }, + }, + { + files: ["src/browser/client-scripts/**/*.ts"], + rules: { + "@typescript-eslint/no-restricted-imports": [ + "error", + { + patterns: [ + { + group: ["../../**", "!../../isomorphic", "!../../isomorphic/**", "!../../..", "!../../../isomorphic", "!../../../isomorphic/**"], + message: "Client-scripts cannot import server-side code, except isomorphic modules.", + }, + ], + }, + ], + }, + }, { files: ["*.ts"], rules: { @@ -20,13 +66,5 @@ module.exports = { "@typescript-eslint/no-var-requires": "off", }, }, - { - files: ["test/**"], - rules: { - "@typescript-eslint/no-empty-function": "off", - // For convenient casting of test objects - "@typescript-eslint/no-explicit-any": "off", - }, - }, ], }; diff --git a/.github/workflows/browser-env.yml b/.github/workflows/browser-env.yml new file mode 100644 index 000000000..22090b80b --- /dev/null +++ b/.github/workflows/browser-env.yml @@ -0,0 +1,98 @@ +name: Testplane Browser Env Tests + +on: + pull_request: + branches: [testplane@9] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + testplane-browser-env: + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + env: + DOCKER_IMAGE_NAME: html-reporter-browsers + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "npm" + + - name: Install dependencies + run: npm ci + + - name: Build the package + run: npm run build + + - name: "Prepare browser-env tests: Cache browser docker image" + uses: actions/cache@v3 + with: + path: ~/.docker/cache + key: docker-browser-image-testplane + + - name: "Prepare browser-env tests: Pull browser docker image" + run: | + mkdir -p ~/.docker/cache + if [ -f ~/.docker/cache/image.tar ]; then + docker load -i ~/.docker/cache/image.tar + else + docker pull yinfra/html-reporter-browsers + docker save yinfra/html-reporter-browsers -o ~/.docker/cache/image.tar + fi + + - name: "Prepare browser-env tests: Run browser docker image" + run: docker run -d --name ${{ env.DOCKER_IMAGE_NAME }} -it --rm --network=host $(which colima >/dev/null || echo --add-host=host.docker.internal:0.0.0.0) yinfra/html-reporter-browsers + + - name: "browser-env: Run Testplane" + id: "testplane" + continue-on-error: true + run: npm run test-browser-env + + - name: "browser-env: Stop browser docker image" + run: | + docker kill ${{ env.DOCKER_IMAGE_NAME }} || true + docker rm ${{ env.DOCKER_IMAGE_NAME }} || true + + - name: Deploy Testplane html-reporter reports + uses: jakejarvis/s3-sync-action@v0.5.1 + with: + args: --acl public-read --follow-symlinks + env: + AWS_S3_BUCKET: gh-testplane-ci + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_S3_ENDPOINT: https://s3.yandexcloud.net/ + SOURCE_DIR: "test/browser-env/report" + DEST_DIR: "testplane-ci/browser-env-reports/${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}/" + + - name: Construct PR comment + run: | + link="https://storage.yandexcloud.net/gh-testplane-ci/testplane-ci/browser-env-reports/${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}/index.html" + if [ "${{ steps.testplane.outcome }}" != "success" ]; then + comment="### ❌ Testplane browser-env run failed

[Report](${link})" + echo "PR_COMMENT=${comment}" >> $GITHUB_ENV + else + comment="### ✅ Testplane browser-env run succeed

[Report](${link})" + echo "PR_COMMENT=${comment}" >> $GITHUB_ENV + fi + + - name: Leave comment to PR with link to Testplane HTML reports + if: github.event.pull_request + uses: thollander/actions-comment-pull-request@v3 + with: + message: ${{ env.PR_COMMENT }} + comment-tag: testplane_results + + - name: Fail the job if any Testplane job is failed + if: ${{ steps.testplane.outcome != 'success' }} + run: exit 1 diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f20a2350d..d5c01c9a1 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -85,10 +85,10 @@ jobs: run: | link="https://storage.yandexcloud.net/gh-testplane-ci/testplane-ci/e2e-reports/${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}/index.html" if [ "${{ steps.testplane.outcome }}" != "success" ]; then - comment="### ❌ Testplane run failed

[Report](${link})" + comment="### ❌ Testplane E2E run failed

[Report](${link})" echo "PR_COMMENT=${comment}" >> $GITHUB_ENV else - comment="### ✅ Testplane run succeed

[Report](${link})" + comment="### ✅ Testplane E2E run succeed

[Report](${link})" echo "PR_COMMENT=${comment}" >> $GITHUB_ENV fi diff --git a/.gitignore b/.gitignore index 4c6943364..83d2c85da 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,9 @@ bundle.compat.js bundle.native.js wt/** tmp/** +coverage/** +tsc-out +testplane/** +testplane-report/** +*.tsbuildinfo +test/browser-env/report/** diff --git a/.prettierignore b/.prettierignore index fc15f0618..69bfa5405 100644 --- a/.prettierignore +++ b/.prettierignore @@ -10,5 +10,11 @@ bundle.compat.js bundle.native.js wt/** test/e2e/report +test/e2e/static/basic-report tmp/** .testplane/** +coverage/** +*.tsbuildinfo +test/browser-env/report/** +*.png +*.DS_Store diff --git a/package-lock.json b/package-lock.json index 7156fe20a..6baeb68f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "testplane", - "version": "8.40.2", + "version": "9.0.0-rc.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "testplane", - "version": "8.40.2", + "version": "9.0.0-rc.3", "license": "MIT", "dependencies": { "@babel/code-frame": "7.24.2", @@ -103,6 +103,7 @@ "aliasify": "1.9.0", "app-module-path": "2.2.0", "browserify": "13.3.0", + "c8": "10.1.3", "chai": "4.2.0", "chai-as-promised": "7.1.1", "concurrently": "8.2.2", @@ -126,6 +127,7 @@ "sinon-chai": "3.7.0", "standard-version": "9.5.0", "ts-node": "10.9.1", + "tsconfig-paths": "4.2.0", "type-fest": "3.11.1", "typescript": "5.3.2", "uglifyify": "3.0.4" @@ -238,6 +240,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@blakeembrey/deque": { "version": "1.0.5", "dev": true, @@ -1634,6 +1646,16 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jest/expect-utils": { "version": "28.1.3", "license": "MIT", @@ -4810,6 +4832,149 @@ "node": ">= 0.8" } }, + "node_modules/c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -9204,6 +9369,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/html-reporter": { "version": "11.8.3", "resolved": "https://registry.npmjs.org/html-reporter/-/html-reporter-11.8.3.tgz", @@ -9893,6 +10065,45 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -10155,6 +10366,19 @@ "dev": true, "license": "ISC" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "4.0.0", "license": "MIT", @@ -10325,14 +10549,6 @@ "node": ">=4" } }, - "node_modules/load-json-file/node_modules/strip-bom": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/local-pkg": { "version": "0.4.3", "license": "MIT", @@ -10562,6 +10778,22 @@ "dev": true, "license": "ISC" }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/make-error": { "version": "1.3.6", "dev": true, @@ -14148,6 +14380,16 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -14256,6 +14498,115 @@ "rimraf": "bin.js" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude/node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", @@ -14533,6 +14884,21 @@ "node": ">=0.3.1" } }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tslib": { "version": "2.6.2", "license": "0BSD" @@ -14983,6 +15349,39 @@ "dev": true, "license": "MIT" }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "dev": true, @@ -15997,6 +16396,12 @@ "@babel/helper-validator-identifier": "^7.28.5" } }, + "@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true + }, "@blakeembrey/deque": { "version": "1.0.5", "dev": true @@ -16792,6 +17197,12 @@ } } }, + "@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true + }, "@jest/expect-utils": { "version": "28.1.3", "requires": { @@ -19013,6 +19424,96 @@ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "dev": true }, + "c8": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz", + "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^1.0.1", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "dependencies": { + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + } + } + }, "cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -21886,6 +22387,12 @@ "whatwg-encoding": "^3.1.1" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "html-reporter": { "version": "11.8.3", "resolved": "https://registry.npmjs.org/html-reporter/-/html-reporter-11.8.3.tgz", @@ -22315,6 +22822,33 @@ "isexe": { "version": "2.0.0" }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, "jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -22498,6 +23032,12 @@ "version": "5.0.1", "dev": true }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, "jsonfile": { "version": "4.0.0", "requires": { @@ -22624,10 +23164,6 @@ "pify": { "version": "3.0.0", "dev": true - }, - "strip-bom": { - "version": "3.0.0", - "dev": true } } }, @@ -22781,6 +23317,15 @@ } } }, + "make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "requires": { + "semver": "^7.5.3" + } + }, "make-error": { "version": "1.3.6", "dev": true @@ -25152,6 +25697,12 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", @@ -25218,6 +25769,83 @@ } } }, + "test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "dependencies": { + "balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true + }, + "brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "requires": { + "balanced-match": "^4.0.2" + } + }, + "glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "dependencies": { + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.2" + } + } + } + }, + "minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "requires": { + "brace-expansion": "^5.0.5" + } + } + } + }, "text-decoder": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", @@ -25400,6 +26028,17 @@ } } }, + "tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "requires": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "tslib": { "version": "2.6.2" }, @@ -25686,6 +26325,35 @@ "version": "3.0.1", "dev": true }, + "v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + } + } + }, "validate-npm-package-license": { "version": "3.0.4", "dev": true, diff --git a/package.json b/package.json index ef5359397..f902114e7 100644 --- a/package.json +++ b/package.json @@ -19,25 +19,28 @@ }, "scripts": { "browsers:launch": "docker run -it --rm --network=host $(which colima >/dev/null || echo --add-host=host.docker.internal:0.0.0.0) yinfra/html-reporter-browsers", - "build": "tsc --build && npm run build-bundles && npm run copy-static && node scripts/create-client-scripts-symlinks.js", - "copy-static": "copyfiles 'src/browser/client-scripts/*' 'src/**/[!cache]*/autogenerated/**/*.json' build", + "build": "tsc --build && npm run build-bundles && npm run copy-static", + "copy-static": "copyfiles 'src/browser/client-scripts/**/*.js' 'src/**/[!cache]*/autogenerated/**/*.json' build", "build-node-bundle": "esbuild ./src/bundle/cjs/index.ts --outdir=./build/src/bundle/cjs --bundle --format=cjs --platform=node --target=ES2021", - "build-browser-bundle": "node ./src/browser/client-scripts/build.js", + "build-browser-bundle": "node ./src/browser/client-scripts/build.js ./src/browser/client-scripts/screen-shooter && node ./src/browser/client-scripts/build.js ./src/browser/client-scripts/browser-utils", "build-bundles": "concurrently -c 'auto' 'npm:build-browser-bundle' 'npm:build-node-bundle --minify'", - "create-client-scripts-symlinks": "node scripts/create-client-scripts-symlinks.js", "resolve-ubuntu-dependencies": "ts-node ./src/browser-installer/ubuntu-packages/collect-dependencies", "check-types": "tsc --project test/tsconfig.json", "clean": "rimraf build/ *.tsbuildinfo", "lint": "eslint --cache . && prettier --check .", "reformat": "eslint --fix . && prettier --write .", "prettier-watch": "onchange '**' --exclude-path .prettierignore -- prettier --write {{changed}}", - "test-unit": "npm run create-client-scripts-symlinks && _mocha \"test/!(integration|e2e)/**/*.js\"", + "test-unit": "_mocha \"test/!(integration|e2e|browser-env)/**/*.[jt]s\"", + "test-unit:coverage": "c8 --all --src=src --reporter=html --reporter=text-summary --exclude=\"build/**\" --exclude=\"test/**\" --exclude=\"**/*.d.ts\" _mocha \"test/!(integration|e2e)/**/*.[jt]s\"", + "test-unit:generate-fixtures": "TS_NODE_PROJECT=test/tsconfig.json node -r ts-node/register -r tsconfig-paths/register test/src/browser/screen-shooter/composite-image/fixtures/generate.ts", "test": "npm run test-unit && npm run check-types && npm run lint", - "test-integration": "npm run create-client-scripts-symlinks && mocha -r ts-node/register -r test/integration/*/**", + "test-integration": "TS_NODE_TRANSPILE_ONLY=1 mocha -r ts-node/register test/integration/*/**", "test-e2e": "npm run test-e2e:generate-fixtures && npm run test-e2e:run-tests", "test-e2e:run-tests": "node bin/testplane --config test/e2e/testplane.config.ts", "test-e2e:generate-fixtures": "node bin/testplane --config test/e2e/fixtures/basic-report/testplane.config.ts || true", "test-e2e:gui": "node bin/testplane --config test/e2e/testplane.config.ts gui", + "test-browser-env": "NODE_OPTIONS='-r tsconfig-paths/register' TS_NODE_PROJECT=./test/browser-env/tsconfig.json node bin/testplane --config test/browser-env/testplane.config.ts", + "test-browser-env:gui": "NODE_OPTIONS='-r tsconfig-paths/register' TS_NODE_PROJECT=./test/browser-env/tsconfig.json node bin/testplane gui --config test/browser-env/testplane.config.ts", "toc": "doctoc docs --title '### Contents'", "precommit": "npm run lint", "prepack": "npm run clean && npm run build", @@ -159,6 +162,7 @@ "aliasify": "1.9.0", "app-module-path": "2.2.0", "browserify": "13.3.0", + "c8": "10.1.3", "chai": "4.2.0", "chai-as-promised": "7.1.1", "concurrently": "8.2.2", @@ -182,6 +186,7 @@ "sinon-chai": "3.7.0", "standard-version": "9.5.0", "ts-node": "10.9.1", + "tsconfig-paths": "4.2.0", "type-fest": "3.11.1", "typescript": "5.3.2", "uglifyify": "3.0.4" diff --git a/test/browser-env/screens/0467357/chrome/compute-safe-area-header-and-footer.png b/test/browser-env/screens/0467357/chrome/compute-safe-area-header-and-footer.png new file mode 100644 index 000000000..ec852b087 Binary files /dev/null and b/test/browser-env/screens/0467357/chrome/compute-safe-area-header-and-footer.png differ diff --git a/test/browser-env/screens/0629e08/chrome/multiple-matches.png b/test/browser-env/screens/0629e08/chrome/multiple-matches.png new file mode 100644 index 000000000..a041300ac Binary files /dev/null and b/test/browser-env/screens/0629e08/chrome/multiple-matches.png differ diff --git a/test/browser-env/screens/06be008/chrome/multiple-selectors.png b/test/browser-env/screens/06be008/chrome/multiple-selectors.png new file mode 100644 index 000000000..7ffe91ce9 Binary files /dev/null and b/test/browser-env/screens/06be008/chrome/multiple-selectors.png differ diff --git a/test/browser-env/screens/0a7819f/chrome/pseudo-elements.png b/test/browser-env/screens/0a7819f/chrome/pseudo-elements.png new file mode 100644 index 000000000..a0e28ab94 Binary files /dev/null and b/test/browser-env/screens/0a7819f/chrome/pseudo-elements.png differ diff --git a/test/browser-env/screens/0ad6793/chrome/fractional-positions.png b/test/browser-env/screens/0ad6793/chrome/fractional-positions.png new file mode 100644 index 000000000..3059ff9a4 Binary files /dev/null and b/test/browser-env/screens/0ad6793/chrome/fractional-positions.png differ diff --git a/test/browser-env/screens/0dc056b/chrome/partially-visible-after-scroll.png b/test/browser-env/screens/0dc056b/chrome/partially-visible-after-scroll.png new file mode 100644 index 000000000..0f598c06f Binary files /dev/null and b/test/browser-env/screens/0dc056b/chrome/partially-visible-after-scroll.png differ diff --git a/test/browser-env/screens/0e38251/chrome/compute-safe-area-sticky-toolbar-in-panel.png b/test/browser-env/screens/0e38251/chrome/compute-safe-area-sticky-toolbar-in-panel.png new file mode 100644 index 000000000..b5eae3ac7 Binary files /dev/null and b/test/browser-env/screens/0e38251/chrome/compute-safe-area-sticky-toolbar-in-panel.png differ diff --git a/test/browser-env/screens/122ded8/chrome/compute-safe-area-nested-stacking-overlay-behind.png b/test/browser-env/screens/122ded8/chrome/compute-safe-area-nested-stacking-overlay-behind.png new file mode 100644 index 000000000..4293ffd97 Binary files /dev/null and b/test/browser-env/screens/122ded8/chrome/compute-safe-area-nested-stacking-overlay-behind.png differ diff --git a/test/browser-env/screens/14dd26d/chrome/box-shadow.png b/test/browser-env/screens/14dd26d/chrome/box-shadow.png new file mode 100644 index 000000000..879306570 Binary files /dev/null and b/test/browser-env/screens/14dd26d/chrome/box-shadow.png differ diff --git a/test/browser-env/screens/1da4083/chrome/compute-safe-area-stacking-context-filter-in-front.png b/test/browser-env/screens/1da4083/chrome/compute-safe-area-stacking-context-filter-in-front.png new file mode 100644 index 000000000..a9fde325e Binary files /dev/null and b/test/browser-env/screens/1da4083/chrome/compute-safe-area-stacking-context-filter-in-front.png differ diff --git a/test/browser-env/screens/2719428/chrome/compute-safe-area-sticky-footer-in-panel.png b/test/browser-env/screens/2719428/chrome/compute-safe-area-sticky-footer-in-panel.png new file mode 100644 index 000000000..3acdcaeaf Binary files /dev/null and b/test/browser-env/screens/2719428/chrome/compute-safe-area-sticky-footer-in-panel.png differ diff --git a/test/browser-env/screens/271e892/chrome/compute-safe-area-scrollable-container-with-border-radius.png b/test/browser-env/screens/271e892/chrome/compute-safe-area-scrollable-container-with-border-radius.png new file mode 100644 index 000000000..2f37433a0 Binary files /dev/null and b/test/browser-env/screens/271e892/chrome/compute-safe-area-scrollable-container-with-border-radius.png differ diff --git a/test/browser-env/screens/2b6ec19/chrome/compute-safe-area-sticky-header-with-shadow.png b/test/browser-env/screens/2b6ec19/chrome/compute-safe-area-sticky-header-with-shadow.png new file mode 100644 index 000000000..31f2f92ae Binary files /dev/null and b/test/browser-env/screens/2b6ec19/chrome/compute-safe-area-sticky-header-with-shadow.png differ diff --git a/test/browser-env/screens/2bf5101/chrome/target.png b/test/browser-env/screens/2bf5101/chrome/target.png new file mode 100644 index 000000000..dfe9f01d8 Binary files /dev/null and b/test/browser-env/screens/2bf5101/chrome/target.png differ diff --git a/test/browser-env/screens/33527ef/chrome/compute-safe-area-floating-help-button.png b/test/browser-env/screens/33527ef/chrome/compute-safe-area-floating-help-button.png new file mode 100644 index 000000000..9c588c492 Binary files /dev/null and b/test/browser-env/screens/33527ef/chrome/compute-safe-area-floating-help-button.png differ diff --git a/test/browser-env/screens/34ca123/chrome/compute-safe-area-stacking-context-filter-in-front.png b/test/browser-env/screens/34ca123/chrome/compute-safe-area-stacking-context-filter-in-front.png new file mode 100644 index 000000000..8bdc94b9c Binary files /dev/null and b/test/browser-env/screens/34ca123/chrome/compute-safe-area-stacking-context-filter-in-front.png differ diff --git a/test/browser-env/screens/3d5257a/chrome/compute-safe-area-fixed-app-header.png b/test/browser-env/screens/3d5257a/chrome/compute-safe-area-fixed-app-header.png new file mode 100644 index 000000000..af8214c02 Binary files /dev/null and b/test/browser-env/screens/3d5257a/chrome/compute-safe-area-fixed-app-header.png differ diff --git a/test/browser-env/screens/3d7960d/chrome/compute-safe-area-stacking-context-opacity-behind.png b/test/browser-env/screens/3d7960d/chrome/compute-safe-area-stacking-context-opacity-behind.png new file mode 100644 index 000000000..520125f74 Binary files /dev/null and b/test/browser-env/screens/3d7960d/chrome/compute-safe-area-stacking-context-opacity-behind.png differ diff --git a/test/browser-env/screens/4552371/chrome/compute-safe-area-absolute-inside-panel.png b/test/browser-env/screens/4552371/chrome/compute-safe-area-absolute-inside-panel.png new file mode 100644 index 000000000..2c23a955e Binary files /dev/null and b/test/browser-env/screens/4552371/chrome/compute-safe-area-absolute-inside-panel.png differ diff --git a/test/browser-env/screens/4e40d07/chrome/transformed-elements.png b/test/browser-env/screens/4e40d07/chrome/transformed-elements.png new file mode 100644 index 000000000..5d15e9342 Binary files /dev/null and b/test/browser-env/screens/4e40d07/chrome/transformed-elements.png differ diff --git a/test/browser-env/screens/51f98b1/chrome/offscreen.png b/test/browser-env/screens/51f98b1/chrome/offscreen.png new file mode 100644 index 000000000..e63fef18c Binary files /dev/null and b/test/browser-env/screens/51f98b1/chrome/offscreen.png differ diff --git a/test/browser-env/screens/5d260ee/chrome/pseudo-elements.png b/test/browser-env/screens/5d260ee/chrome/pseudo-elements.png new file mode 100644 index 000000000..c7e4f760e Binary files /dev/null and b/test/browser-env/screens/5d260ee/chrome/pseudo-elements.png differ diff --git a/test/browser-env/screens/5efd363/chrome/compute-safe-area-modal-backdrop.png b/test/browser-env/screens/5efd363/chrome/compute-safe-area-modal-backdrop.png new file mode 100644 index 000000000..02a4477e6 Binary files /dev/null and b/test/browser-env/screens/5efd363/chrome/compute-safe-area-modal-backdrop.png differ diff --git a/test/browser-env/screens/6c02c6b/chrome/inset-box-shadow.png b/test/browser-env/screens/6c02c6b/chrome/inset-box-shadow.png new file mode 100644 index 000000000..d083cbacf Binary files /dev/null and b/test/browser-env/screens/6c02c6b/chrome/inset-box-shadow.png differ diff --git a/test/browser-env/screens/7382524/chrome/fixed-parent-in-overflow-hidden-external-containing-block.png b/test/browser-env/screens/7382524/chrome/fixed-parent-in-overflow-hidden-external-containing-block.png new file mode 100644 index 000000000..e8da8dede Binary files /dev/null and b/test/browser-env/screens/7382524/chrome/fixed-parent-in-overflow-hidden-external-containing-block.png differ diff --git a/test/browser-env/screens/76b005c/chrome/nested-elements.png b/test/browser-env/screens/76b005c/chrome/nested-elements.png new file mode 100644 index 000000000..7f312f6a7 Binary files /dev/null and b/test/browser-env/screens/76b005c/chrome/nested-elements.png differ diff --git a/test/browser-env/screens/776b3f5/chrome/target.png b/test/browser-env/screens/776b3f5/chrome/target.png new file mode 100644 index 000000000..8b0aa89ff Binary files /dev/null and b/test/browser-env/screens/776b3f5/chrome/target.png differ diff --git a/test/browser-env/screens/78382b0/chrome/mixed-visibility.png b/test/browser-env/screens/78382b0/chrome/mixed-visibility.png new file mode 100644 index 000000000..01730eb75 Binary files /dev/null and b/test/browser-env/screens/78382b0/chrome/mixed-visibility.png differ diff --git a/test/browser-env/screens/79b6388/chrome/single-element.png b/test/browser-env/screens/79b6388/chrome/single-element.png new file mode 100644 index 000000000..ea58405b7 Binary files /dev/null and b/test/browser-env/screens/79b6388/chrome/single-element.png differ diff --git a/test/browser-env/screens/817287f/chrome/transformed-pseudo-elements.png b/test/browser-env/screens/817287f/chrome/transformed-pseudo-elements.png new file mode 100644 index 000000000..de6d65d8d Binary files /dev/null and b/test/browser-env/screens/817287f/chrome/transformed-pseudo-elements.png differ diff --git a/test/browser-env/screens/8957391/chrome/compute-safe-area-huge-fixed-banner.png b/test/browser-env/screens/8957391/chrome/compute-safe-area-huge-fixed-banner.png new file mode 100644 index 000000000..832ce1d63 Binary files /dev/null and b/test/browser-env/screens/8957391/chrome/compute-safe-area-huge-fixed-banner.png differ diff --git a/test/browser-env/screens/92d8952/chrome/outline.png b/test/browser-env/screens/92d8952/chrome/outline.png new file mode 100644 index 000000000..f04685c5f Binary files /dev/null and b/test/browser-env/screens/92d8952/chrome/outline.png differ diff --git a/test/browser-env/screens/a371dfa/chrome/compute-safe-area-absolute-overlay-outside-panel.png b/test/browser-env/screens/a371dfa/chrome/compute-safe-area-absolute-overlay-outside-panel.png new file mode 100644 index 000000000..8ca124d50 Binary files /dev/null and b/test/browser-env/screens/a371dfa/chrome/compute-safe-area-absolute-overlay-outside-panel.png differ diff --git a/test/browser-env/screens/aaf8f86/chrome/compute-safe-area-nested-stacking-overlay-in-front.png b/test/browser-env/screens/aaf8f86/chrome/compute-safe-area-nested-stacking-overlay-in-front.png new file mode 100644 index 000000000..eb7dd07fb Binary files /dev/null and b/test/browser-env/screens/aaf8f86/chrome/compute-safe-area-nested-stacking-overlay-in-front.png differ diff --git a/test/browser-env/screens/ad102bd/chrome/compute-safe-area-target-element-inside-fixed.png b/test/browser-env/screens/ad102bd/chrome/compute-safe-area-target-element-inside-fixed.png new file mode 100644 index 000000000..ba221c2fb Binary files /dev/null and b/test/browser-env/screens/ad102bd/chrome/compute-safe-area-target-element-inside-fixed.png differ diff --git a/test/browser-env/screens/bfcf748/chrome/overflow-hidden.png b/test/browser-env/screens/bfcf748/chrome/overflow-hidden.png new file mode 100644 index 000000000..d4504b493 Binary files /dev/null and b/test/browser-env/screens/bfcf748/chrome/overflow-hidden.png differ diff --git a/test/browser-env/screens/c5a4957/chrome/fixed-in-overflow.png b/test/browser-env/screens/c5a4957/chrome/fixed-in-overflow.png new file mode 100644 index 000000000..e8da8dede Binary files /dev/null and b/test/browser-env/screens/c5a4957/chrome/fixed-in-overflow.png differ diff --git a/test/browser-env/screens/d070381/chrome/scrollable-container-scrolled.png b/test/browser-env/screens/d070381/chrome/scrollable-container-scrolled.png new file mode 100644 index 000000000..90eb456fe Binary files /dev/null and b/test/browser-env/screens/d070381/chrome/scrollable-container-scrolled.png differ diff --git a/test/browser-env/screens/de10274/chrome/overflow-scroll.png b/test/browser-env/screens/de10274/chrome/overflow-scroll.png new file mode 100644 index 000000000..5054b2e0a Binary files /dev/null and b/test/browser-env/screens/de10274/chrome/overflow-scroll.png differ diff --git a/test/browser-env/screens/de90b0a/chrome/compute-safe-area-fixed-element-outside-of-viewport.png b/test/browser-env/screens/de90b0a/chrome/compute-safe-area-fixed-element-outside-of-viewport.png new file mode 100644 index 000000000..2a0b3f631 Binary files /dev/null and b/test/browser-env/screens/de90b0a/chrome/compute-safe-area-fixed-element-outside-of-viewport.png differ diff --git a/test/browser-env/screens/e0232e8/chrome/compute-safe-area-cookie-consent-bar.png b/test/browser-env/screens/e0232e8/chrome/compute-safe-area-cookie-consent-bar.png new file mode 100644 index 000000000..157b42f64 Binary files /dev/null and b/test/browser-env/screens/e0232e8/chrome/compute-safe-area-cookie-consent-bar.png differ diff --git a/test/browser-env/screens/e3b08bb/chrome/pseudo-elements-ancestor-cb.png b/test/browser-env/screens/e3b08bb/chrome/pseudo-elements-ancestor-cb.png new file mode 100644 index 000000000..b76296202 Binary files /dev/null and b/test/browser-env/screens/e3b08bb/chrome/pseudo-elements-ancestor-cb.png differ diff --git a/test/browser-env/screens/eb18712/chrome/margin-padding-border.png b/test/browser-env/screens/eb18712/chrome/margin-padding-border.png new file mode 100644 index 000000000..660627834 Binary files /dev/null and b/test/browser-env/screens/eb18712/chrome/margin-padding-border.png differ diff --git a/test/browser-env/testplane.config.ts b/test/browser-env/testplane.config.ts new file mode 100644 index 000000000..c3990b2e1 --- /dev/null +++ b/test/browser-env/testplane.config.ts @@ -0,0 +1,76 @@ +/// + +import path from "path"; + +const shouldUseLocalBrowser = Boolean(process.env.USE_LOCAL_BROWSER); + +export default { + gridUrl: shouldUseLocalBrowser ? "local" : "http://127.0.0.1:4444/", + baseUrl: shouldUseLocalBrowser ? "http://localhost:5173" : "http://host.docker.internal:5173", + sessionsPerBrowser: 1, + testsPerSession: 10, + + screenshotsDir: "test/browser-env/screens", + + takeScreenshotOnFails: { + testFail: false, + assertViewFail: false, + }, + + system: { + workers: 1, + testRunEnv: ["browser", { viteConfig: path.join(__dirname, "vite.config.ts") }], + }, + + sets: { + all: { + files: [path.join(__dirname, "tests/desktop/**/*.testplane.ts")], + browsers: ["chrome"], + }, + mobileDpr3: { + files: [path.join(__dirname, "tests/high-pixel-ratio/**/*.testplane.ts")], + browsers: ["chrome-mobile-dpr3"], + }, + }, + + browsers: { + chrome: { + windowSize: { width: 1280, height: 1000 }, + headless: !shouldUseLocalBrowser, + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + args: ["no-sandbox", "hide-scrollbars", "disable-dev-shm-usage"], + binary: shouldUseLocalBrowser ? undefined : "/usr/bin/chromium", + }, + }, + waitTimeout: 3000, + }, + "chrome-mobile-dpr3": { + headless: !shouldUseLocalBrowser, + desiredCapabilities: { + browserName: "chrome", + "goog:chromeOptions": { + args: ["no-sandbox", "hide-scrollbars", "disable-dev-shm-usage"], + binary: shouldUseLocalBrowser ? undefined : "/usr/bin/chromium", + mobileEmulation: { + deviceMetrics: { + width: 390, + height: 844, + pixelRatio: 3, + mobile: true, + touch: true, + }, + }, + }, + }, + }, + }, + + plugins: { + "html-reporter/testplane": { + enabled: true, + path: "test/browser-env/report", + }, + }, +}; diff --git a/test/browser-env/tests/desktop/screenshooter/computeCaptureSpecs.testplane.ts b/test/browser-env/tests/desktop/screenshooter/computeCaptureSpecs.testplane.ts new file mode 100644 index 000000000..7ff539de8 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/computeCaptureSpecs.testplane.ts @@ -0,0 +1,357 @@ +import { computeCaptureSpecs } from "../../../../../src/browser/client-scripts/screen-shooter/operations"; +import { createDebugLogger } from "../../../../../src/browser/client-scripts/shared/logger"; +import { visualizeCaptureSpecs } from "../../utils"; + +describe("computeCaptureSpecs", () => { + beforeEach(() => { + document.body.innerHTML = ""; + document.body.style.cssText = ""; + }); + + describe("error cases", () => { + it("should throw when selectors array is empty", () => { + expect(() => computeCaptureSpecs([])).toThrow("No selectors to compute capture area"); + }); + + it("should throw on invalid CSS selector", () => { + expect(() => computeCaptureSpecs(["[[[invalid"])).toThrow(); + }); + }); + + describe("empty results", () => { + it("should return empty array when selector matches nothing", () => { + const result = computeCaptureSpecs([".nonexistent"]); + expect(result.captureSpecs).toEqual([]); + }); + + it("should return empty array when all matched elements are hidden", async () => { + const { default: html } = await import("./fixtures/capture-areas/hidden-elements.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([ + ".hidden-display", + ".hidden-visibility", + ".hidden-opacity", + ".hidden-zero-size", + ]); + expect(result.captureSpecs).toEqual([]); + }); + }); + + describe("single element", () => { + it("should return rect for a single visible element", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/single-element.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("single-element"); + }); + + it("should expand rect to include box-shadow", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/box-shadow.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".shadow-target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("box-shadow"); + }); + + it("should not expand rect for inset box-shadow", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/inset-box-shadow.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".inset-shadow-target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("inset-box-shadow"); + }); + + it("should expand rect to include outline", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/outline.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".outline-target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("outline"); + }); + + it("should not include pseudo-element geometry in base element capture rect", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/pseudo-elements.html?raw"); + document.body.innerHTML = html; + + const elementResult = computeCaptureSpecs([".pseudo-target"]); + + visualizeCaptureSpecs(elementResult.captureSpecs); + await browser.assertView("pseudo-elements"); + }); + + it("should capture pseudo-elements when they are passed explicitly as selectors", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/pseudo-elements.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".pseudo-target::before", ".pseudo-target::after"]); + expect(result.captureSpecs).toHaveLength(2); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("pseudo-elements"); + }); + + it("should capture pseudo-elements positioned relative to an ancestor (not direct parent) when selected explicitly", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/capture-areas/pseudo-elements-ancestor-cb.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".parent::before", ".parent::after"]); + expect(result.captureSpecs).toHaveLength(2); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("pseudo-elements-ancestor-cb"); + }); + + it("should account for CSS transforms (translate, rotate, scale, skew) on pseudo-elements selected explicitly", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/capture-areas/transformed-pseudo-elements.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".target::before", ".target::after"]); + expect(result.captureSpecs).toHaveLength(2); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("transformed-pseudo-elements"); + }); + + it("should handle CSS transforms on the element itself (rotate, scale, translate, skew, combined)", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/capture-areas/transformed-element.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".rotated", ".scaled", ".translated", ".skewed", ".combined"]); + expect(result.captureSpecs).toHaveLength(5); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("transformed-elements"); + }); + }); + + describe("multiple elements", () => { + it("should return rects for multiple selectors each matching one element", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/multiple-elements.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".a", ".b", ".c"]); + expect(result.captureSpecs).toHaveLength(3); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("multiple-selectors"); + }); + + it("should return rect for the first element when selector matches multiple elements", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/multiple-selector-matches.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".item"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("multiple-matches"); + }); + + it("should return duplicate rects when multiple selectors match the same element", async () => { + const { default: html } = await import("./fixtures/capture-areas/single-element.html?raw"); + document.body.innerHTML = html; + + // Both selectors match the same .target element + const result = computeCaptureSpecs([".target", "div.target"]); + expect(result.captureSpecs).toHaveLength(2); + + // Both rects should be identical + expect(result.captureSpecs[0]).toEqual(result.captureSpecs[1]); + }); + + it("should only return visible elements from a mix of visible and hidden", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/hidden-elements.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([ + ".visible", + ".hidden-display", + ".hidden-visibility", + ".hidden-opacity", + ".hidden-zero-size", + ]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("mixed-visibility"); + }); + }); + + describe("scrollable container", () => { + it("should compute correct rect for element inside a scrolled container", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/scrollable-container.html?raw"); + document.body.innerHTML = html; + + const container = document.querySelector(".container")!; + container.scrollTop = 200; + + const result = computeCaptureSpecs([".target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("scrollable-container-scrolled"); + }); + }); + + describe("box model", () => { + it("should include padding and border but not margin in bounding rect", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/margin-padding-border.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".box-model-target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("margin-padding-border"); + }); + + it("should handle element partially off-screen", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/offscreen-element.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".offscreen"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("offscreen"); + }); + + it("should handle long element partially visible only after window scroll", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/partially-visible-after-scroll.html?raw"); + document.body.innerHTML = html; + + window.scrollTo(0, 560); + + const result = computeCaptureSpecs([".long-target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("partially-visible-after-scroll"); + }); + + it("should handle elements with fractional pixel positions", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/fractional-positions.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".fractional", ".transformed"]); + expect(result.captureSpecs).toHaveLength(2); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("fractional-positions"); + }); + }); + + describe("nested elements", () => { + it("should return separate rects for parent and children", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/nested-elements.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".parent", ".child"]); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("nested-elements"); + }); + }); + + describe("overflow clipping", () => { + it("should clip visible rect to overflow:hidden container while full rect extends beyond", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/capture-areas/overflow-hidden.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("overflow-hidden"); + }); + + it("should clip visible rect to overflow:scroll container", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/overflow-scroll.html?raw"); + document.body.innerHTML = html; + + const logger = createDebugLogger({ debug: ["screen-shooter"] }, "screen-shooter"); + const result = computeCaptureSpecs([".target"], logger); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("overflow-scroll"); + }); + + it("should not clip fixed-position element by overflow:hidden ancestor", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/fixed-in-overflow.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".target"]); + expect(result.captureSpecs).toHaveLength(1); + + const spec = result.captureSpecs[0]; + // Fixed element escapes overflow clipping — full and visible should be the same + expect(spec.full.width).toBe(spec.visible.width); + expect(spec.full.height).toBe(spec.visible.height); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("fixed-in-overflow"); + }); + + it("should not clip element inside fixed-position parent by overflow:hidden ancestor", async ({ browser }) => { + const { default: html } = await import("./fixtures/capture-areas/fixed-parent-in-overflow-hidden.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("fixed-parent-in-overflow-hidden-external-containing-block"); + }); + + it("should not clip absolutely positioned element when containing block is outside overflow:hidden ancestor", async ({ + browser, + }) => { + const { default: html } = await import( + "./fixtures/capture-areas/absolute-in-overflow-hidden-external-containing-block.html?raw" + ); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("target"); + }); + + it("should not clip element inside absolutely positioned parent because it escapes overflow:hidden ancestor", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/capture-areas/absolute-overflows-parent.html?raw"); + document.body.innerHTML = html; + + const result = computeCaptureSpecs([".target"]); + expect(result.captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(result.captureSpecs); + await browser.assertView("target"); + }); + }); +}); diff --git a/test/browser-env/tests/desktop/screenshooter/computePixelRatio.testplane.ts b/test/browser-env/tests/desktop/screenshooter/computePixelRatio.testplane.ts new file mode 100644 index 000000000..92ba9e049 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/computePixelRatio.testplane.ts @@ -0,0 +1,15 @@ +import { computePixelRatio } from "../../../../../src/browser/client-scripts/screen-shooter/operations"; + +describe("computePixelRatio", () => { + testplane.only.in("chrome-mobile-dpr3"); + it("returns emulated mobile pixel ratio from window.devicePixelRatio", () => { + const pixelRatio = computePixelRatio().pixelRatio; + expect(pixelRatio).toBe(3); + }); + + testplane.only.in("chrome-mobile-dpr3"); + it("returns 1 when usePixelRatio is disabled", () => { + const pixelRatio = computePixelRatio(false).pixelRatio; + expect(pixelRatio).toBe(1); + }); +}); diff --git a/test/browser-env/tests/desktop/screenshooter/computeSafeArea.testplane.ts b/test/browser-env/tests/desktop/screenshooter/computeSafeArea.testplane.ts new file mode 100644 index 000000000..fbdd46730 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/computeSafeArea.testplane.ts @@ -0,0 +1,352 @@ +import { + computeCaptureSpecs, + computeSafeArea, + computeViewportSize, +} from "../../../../../src/browser/client-scripts/screen-shooter/operations"; +import { createDebugLogger } from "../../../../../src/browser/client-scripts/shared/logger"; +import { visualizeCaptureSpecs, visualizeSafeArea } from "../../utils"; + +describe("computeSafeArea", () => { + beforeEach(() => { + document.body.innerHTML = ""; + document.body.style.cssText = ""; + }); + + it("should shrink safe area below a fixed app header", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/fixed-app-header.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + expect(captureSpecs).toHaveLength(1); + expect(safeArea.top).toBeGreaterThan(0); + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-fixed-app-header"); + }); + + it("should shrink from bottom for cookie consent bar", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/cookie-consent-bar.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + expect(captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-cookie-consent-bar"); + }); + + it("should not shrink for backdrop behind focused modal", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/modal-with-backdrop.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target-modal"]; + const safeArea = computeSafeArea(selectors).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-modal-backdrop"); + }); + + it("should ignore fixed help button with no horizontal overlap", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/floating-help-button.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-floating-help-button"); + }); + + it("should handle sticky toolbar inside scrollable panel", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/sticky-toolbar-in-panel.html?raw"); + document.body.innerHTML = html; + + const panel = document.querySelector(".panel"); + if (!panel) { + throw new Error("Failed to find .panel"); + } + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors, panel).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + expect(captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-sticky-toolbar-in-panel"); + }); + + it("should compute safe area for capture inside scrollable panel with large border radius", async ({ browser }) => { + const { default: html } = await import( + "./fixtures/safe-areas/scrollable-container-with-border-radius.html?raw" + ); + document.body.innerHTML = html; + + const panel = document.querySelector(".panel"); + if (!panel) { + throw new Error("Failed to find .panel"); + } + + const selectors = [".content"]; + const safeArea = computeSafeArea(selectors, panel).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-scrollable-container-with-border-radius"); + }); + + it("should compute safe area for target element inside fixed element", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/target-element-inside-fixed.html?raw"); + document.body.innerHTML = html; + + const logger = createDebugLogger( + { debug: ["testplane:screenshots:browser:computeSafeArea"] }, + "testplane:screenshots:browser:computeSafeArea", + ); + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors, undefined, logger).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-target-element-inside-fixed"); + + console.log(logger.log); + }); + + it("should handle sticky header with shadow", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/sticky-header-with-shadow.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-sticky-header-with-shadow"); + }); + + it("should not shrink safe area if fixed element is outside of viewport", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/fixed-element-outside-of-viewport.html?raw"); + document.body.innerHTML = html; + + const logger = createDebugLogger( + { debug: ["testplane:screenshots:browser:computeSafeArea"] }, + "testplane:screenshots:browser:computeSafeArea", + ); + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors, undefined, logger).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-fixed-element-outside-of-viewport"); + }); + + it("should ignore obstruction if shrinking would exceed half of original safe area", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/huge-fixed-banner.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-huge-fixed-banner"); + }); + + it("should return full viewport when no selectors match", () => { + document.body.innerHTML = "
some content
"; + + const { viewportSize } = computeViewportSize(); + const safeArea = computeSafeArea([".does-not-exist"]).safeArea; + + expect(safeArea.top).toBe(0); + expect(safeArea.height).toBe(viewportSize.height); + }); + + it("should shrink from bottom for sticky footer in panel", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/sticky-footer-in-panel.html?raw"); + document.body.innerHTML = html; + + const panel = document.querySelector(".panel"); + if (!panel) { + throw new Error("Failed to find .panel"); + } + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors, panel).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + expect(captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-sticky-footer-in-panel"); + }); + + it("should not shrink for absolute element whose containing block is inside the panel", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/absolute-inside-scroll-panel.html?raw"); + document.body.innerHTML = html; + + const panel = document.querySelector(".panel"); + if (!panel) { + throw new Error("Failed to find .panel"); + } + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors, panel).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-absolute-inside-panel"); + }); + + it("should shrink from both top and bottom for simultaneous header and footer", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/header-and-footer.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + const headerBcr = document.querySelector(".header")!.getBoundingClientRect(); + const footerBcr = document.querySelector(".footer")!.getBoundingClientRect(); + + // Both elements must shrink the safe area — top moves down, bottom moves up + expect(safeArea.top).toBeCloseTo(headerBcr.bottom, 0); + expect(safeArea.top + safeArea.height).toBeCloseTo(footerBcr.top, 0); + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-header-and-footer"); + }); + + it("should treat absolute overlay outside panel as interference for panel scrolling", async ({ browser }) => { + const { default: html } = await import("./fixtures/safe-areas/absolute-overlay-outside-panel.html?raw"); + document.body.innerHTML = html; + + const panel = document.querySelector(".panel"); + if (!panel) { + throw new Error("Failed to find .panel"); + } + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors, panel).safeArea; + const captureSpecs = computeCaptureSpecs(selectors).captureSpecs; + + expect(captureSpecs).toHaveLength(1); + + visualizeCaptureSpecs(captureSpecs); + visualizeSafeArea(safeArea.top, safeArea.height); + + await browser.assertView("compute-safe-area-absolute-overlay-outside-panel"); + }); + + it("should not shrink when fixed element is behind target due to z-index despite opacity stacking context", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/safe-areas/stacking-context-opacity-behind.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + + // overlay: z:5 / target's container: z:10 -> isChainBehind returns true -> no shrink + const { viewportSize } = computeViewportSize(); + expect(safeArea.top).toBe(0); + expect(safeArea.height).toBe(viewportSize.height); + + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-stacking-context-opacity-behind"); + }); + + it("should shrink for sticky header", async ({ browser }) => { + const logger = createDebugLogger( + { debug: ["testplane:screenshots:browser:computeSafeArea"] }, + "testplane:screenshots:browser:computeSafeArea", + ); + + const { default: html } = await import("./fixtures/safe-areas/simple-sticky-header.html?raw"); + document.body.innerHTML = html; + + window.scrollTo(0, document.documentElement.scrollHeight - document.documentElement.clientHeight - 200); + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors, undefined, logger).safeArea; + + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-stacking-context-filter-in-front"); + }); + + it("should shrink for fixed header that creates stacking context via filter and is in front", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/safe-areas/stacking-context-filter-in-front.html?raw"); + document.body.innerHTML = html; + + window.scrollTo(0, 5000); + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + + // header: z:10 / target: z:0 -> isChainBehind returns false -> does shrink + const headerBcr = document.querySelector(".header")!.getBoundingClientRect(); + expect(safeArea.top).toBeCloseTo(headerBcr.bottom, 0); + + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-stacking-context-filter-in-front"); + }); + + it("should not shrink when fixed overlay is behind a nested stacking context containing the target", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/safe-areas/nested-stacking-overlay-behind.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + + // overlay: z:50 / app-shell: z:100 -> common ctx = documentElement -> 50 < 100 -> behind -> no shrink + const { viewportSize } = computeViewportSize(); + expect(safeArea.top).toBe(0); + expect(safeArea.height).toBe(viewportSize.height); + + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-nested-stacking-overlay-behind"); + }); + + it("should shrink when fixed overlay is in front of a nested stacking context containing the target", async ({ + browser, + }) => { + const { default: html } = await import("./fixtures/safe-areas/nested-stacking-overlay-in-front.html?raw"); + document.body.innerHTML = html; + + const selectors = [".target"]; + const safeArea = computeSafeArea(selectors).safeArea; + + // overlay: z:50 / app-shell: z:10 -> common ctx = documentElement -> 50 < 10 is false -> in front -> does shrink + const overlayBcr = document.querySelector(".fixed-overlay")!.getBoundingClientRect(); + expect(safeArea.top).toBeCloseTo(overlayBcr.bottom, 0); + + visualizeSafeArea(safeArea.top, safeArea.height); + await browser.assertView("compute-safe-area-nested-stacking-overlay-in-front"); + }); +}); diff --git a/test/browser-env/tests/desktop/screenshooter/computeViewportSize.testplane.ts b/test/browser-env/tests/desktop/screenshooter/computeViewportSize.testplane.ts new file mode 100644 index 000000000..fa836217b --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/computeViewportSize.testplane.ts @@ -0,0 +1,10 @@ +import { computeViewportSize } from "../../../../../src/browser/client-scripts/screen-shooter/operations"; + +describe("computeViewportSize", () => { + it("should return the viewport size", () => { + const viewportSize = computeViewportSize(); + + expect(viewportSize.viewportSize.width).toBe(1280); + expect(viewportSize.viewportSize.height).toBe(1000); + }); +}); diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/absolute-in-overflow-hidden-external-containing-block.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/absolute-in-overflow-hidden-external-containing-block.html new file mode 100644 index 000000000..8e3df5844 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/absolute-in-overflow-hidden-external-containing-block.html @@ -0,0 +1,44 @@ + + +
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/absolute-overflows-parent.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/absolute-overflows-parent.html new file mode 100644 index 000000000..4ba1cdf7d --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/absolute-overflows-parent.html @@ -0,0 +1,50 @@ + + +
+
+
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/box-shadow.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/box-shadow.html new file mode 100644 index 000000000..658d609ca --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/box-shadow.html @@ -0,0 +1,14 @@ + +
Box shadow element
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fixed-in-overflow.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fixed-in-overflow.html new file mode 100644 index 000000000..08ecd42a9 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fixed-in-overflow.html @@ -0,0 +1,34 @@ + + +
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fixed-parent-in-overflow-hidden.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fixed-parent-in-overflow-hidden.html new file mode 100644 index 000000000..18855f566 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fixed-parent-in-overflow-hidden.html @@ -0,0 +1,41 @@ + + +
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fractional-positions.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fractional-positions.html new file mode 100644 index 000000000..7dbcf2361 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/fractional-positions.html @@ -0,0 +1,22 @@ + +
Fractional width
+
Transformed
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/hidden-elements.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/hidden-elements.html new file mode 100644 index 000000000..0824e7cb7 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/hidden-elements.html @@ -0,0 +1,40 @@ + +
Visible
+
Hidden (display:none)
+
Hidden (visibility:hidden)
+
Hidden (opacity:0)
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/inset-box-shadow.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/inset-box-shadow.html new file mode 100644 index 000000000..2084a46b9 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/inset-box-shadow.html @@ -0,0 +1,14 @@ + +
Inset box shadow
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/margin-padding-border.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/margin-padding-border.html new file mode 100644 index 000000000..1b49b3259 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/margin-padding-border.html @@ -0,0 +1,15 @@ + +
Box model
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/multiple-elements.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/multiple-elements.html new file mode 100644 index 000000000..6a7ce05b5 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/multiple-elements.html @@ -0,0 +1,33 @@ + +
Element A
+
Element B
+
Element C
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/multiple-selector-matches.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/multiple-selector-matches.html new file mode 100644 index 000000000..95767a96c --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/multiple-selector-matches.html @@ -0,0 +1,29 @@ + +
Item 1
+
Item 2
+
Item 3
+
Item 4
+
Item 5
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/nested-elements.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/nested-elements.html new file mode 100644 index 000000000..b4e49266c --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/nested-elements.html @@ -0,0 +1,23 @@ + +
+
Child 1
+
Child 2
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/offscreen-element.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/offscreen-element.html new file mode 100644 index 000000000..e7519b393 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/offscreen-element.html @@ -0,0 +1,15 @@ + +
Partially off-screen
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/outline.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/outline.html new file mode 100644 index 000000000..9aec36bfa --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/outline.html @@ -0,0 +1,14 @@ + +
Outline element
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/overflow-hidden.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/overflow-hidden.html new file mode 100644 index 000000000..c59795696 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/overflow-hidden.html @@ -0,0 +1,34 @@ + + +
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/overflow-scroll.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/overflow-scroll.html new file mode 100644 index 000000000..0e0a0e31a --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/overflow-scroll.html @@ -0,0 +1,49 @@ + + +
+
+
+
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/partially-visible-after-scroll.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/partially-visible-after-scroll.html new file mode 100644 index 000000000..cf5d45caf --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/partially-visible-after-scroll.html @@ -0,0 +1,36 @@ + + +
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/pseudo-elements-ancestor-cb.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/pseudo-elements-ancestor-cb.html new file mode 100644 index 000000000..96bceef26 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/pseudo-elements-ancestor-cb.html @@ -0,0 +1,50 @@ + +
+
Static parent
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/pseudo-elements.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/pseudo-elements.html new file mode 100644 index 000000000..abccf8cb6 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/pseudo-elements.html @@ -0,0 +1,37 @@ + +
Pseudo-element target
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/scrollable-container.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/scrollable-container.html new file mode 100644 index 000000000..b8b9698f3 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/scrollable-container.html @@ -0,0 +1,34 @@ + +
+
+
Scrolled target
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/single-element.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/single-element.html new file mode 100644 index 000000000..d634a13e0 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/single-element.html @@ -0,0 +1,13 @@ + +
Single element
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/transformed-element.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/transformed-element.html new file mode 100644 index 000000000..dcb63f4cc --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/transformed-element.html @@ -0,0 +1,41 @@ + +
+
Rotated
+
Scaled
+
Translated
+
Skewed
+
Combined
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/transformed-pseudo-elements.html b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/transformed-pseudo-elements.html new file mode 100644 index 000000000..cdec57eb2 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/capture-areas/transformed-pseudo-elements.html @@ -0,0 +1,46 @@ + +
+
Transformed pseudos
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/analytics-dashboard.html b/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/analytics-dashboard.html new file mode 100644 index 000000000..7b02cae6e --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/analytics-dashboard.html @@ -0,0 +1,126 @@ + + +
Analytics Dashboard
+ +
+
+
+
+ Revenue + single target +
+
+ Conversion + outer target +
+
Churn
+
+ +
+
+ Weekly report + feed target A +
+
+ Monthly report + feed target B +
+
Quarterly report
+
Annual report
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/fixed-modal.html b/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/fixed-modal.html new file mode 100644 index 000000000..19d88e918 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/fixed-modal.html @@ -0,0 +1,56 @@ + + +
+ + diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/split-panels.html b/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/split-panels.html new file mode 100644 index 000000000..c971daf01 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/common-scroll-parent/split-panels.html @@ -0,0 +1,67 @@ + + +
+
+

Build Queue

+
+ left panel target +
+
+ +
+

Logs

+
+ right panel target +
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/absolute-inside-scroll-panel.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/absolute-inside-scroll-panel.html new file mode 100644 index 000000000..13c027823 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/absolute-inside-scroll-panel.html @@ -0,0 +1,67 @@ + + +
+
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/absolute-overlay-outside-panel.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/absolute-overlay-outside-panel.html new file mode 100644 index 000000000..bc69affea --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/absolute-overlay-outside-panel.html @@ -0,0 +1,69 @@ + + +
+
+
+
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/cookie-consent-bar.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/cookie-consent-bar.html new file mode 100644 index 000000000..1657a0b6c --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/cookie-consent-bar.html @@ -0,0 +1,44 @@ + + +
Product Details
+
+ diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/fixed-app-header.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/fixed-app-header.html new file mode 100644 index 000000000..f160f31e3 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/fixed-app-header.html @@ -0,0 +1,45 @@ + + +
App Header
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/fixed-element-outside-of-viewport.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/fixed-element-outside-of-viewport.html new file mode 100644 index 000000000..877118df4 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/fixed-element-outside-of-viewport.html @@ -0,0 +1,33 @@ + + +
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/floating-help-button.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/floating-help-button.html new file mode 100644 index 000000000..85226175c --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/floating-help-button.html @@ -0,0 +1,43 @@ + + +
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/half-viewport-banner.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/half-viewport-banner.html new file mode 100644 index 000000000..2fbb0ed8d --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/half-viewport-banner.html @@ -0,0 +1,40 @@ + + + +
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/header-and-footer.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/header-and-footer.html new file mode 100644 index 000000000..433cd5d49 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/header-and-footer.html @@ -0,0 +1,62 @@ + + +
App Header
+
+
+
+ diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/huge-fixed-banner.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/huge-fixed-banner.html new file mode 100644 index 000000000..c9d848477 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/huge-fixed-banner.html @@ -0,0 +1,34 @@ + + +
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/modal-with-backdrop.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/modal-with-backdrop.html new file mode 100644 index 000000000..13d1a295f --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/modal-with-backdrop.html @@ -0,0 +1,47 @@ + + +
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/nested-stacking-overlay-behind.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/nested-stacking-overlay-behind.html new file mode 100644 index 000000000..7fffe4790 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/nested-stacking-overlay-behind.html @@ -0,0 +1,58 @@ + + +
+
+
+
Overlay (z:50) — behind app-shell (z:100)
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/nested-stacking-overlay-in-front.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/nested-stacking-overlay-in-front.html new file mode 100644 index 000000000..23d03575e --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/nested-stacking-overlay-in-front.html @@ -0,0 +1,59 @@ + + +
+
+
+
Overlay (z:50) — in front of app-shell (z:10)
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/scrollable-container-with-border-radius.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/scrollable-container-with-border-radius.html new file mode 100644 index 000000000..c76b5d205 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/scrollable-container-with-border-radius.html @@ -0,0 +1,62 @@ + + +
+
+
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/simple-sticky-header.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/simple-sticky-header.html new file mode 100644 index 000000000..f550f1a05 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/simple-sticky-header.html @@ -0,0 +1,54 @@ + + +
Header (z:10, filter) — in front of target (z:0)
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/stacking-context-filter-in-front.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/stacking-context-filter-in-front.html new file mode 100644 index 000000000..c7e1c1989 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/stacking-context-filter-in-front.html @@ -0,0 +1,58 @@ + + +
Header (z:10, filter) — in front of target (z:0)
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/stacking-context-opacity-behind.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/stacking-context-opacity-behind.html new file mode 100644 index 000000000..56d1a624c --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/stacking-context-opacity-behind.html @@ -0,0 +1,61 @@ + + +
+
+
+
Overlay (z:5, opacity:0.9) — behind target layer (z:10)
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-footer-in-panel.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-footer-in-panel.html new file mode 100644 index 000000000..044e1317e --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-footer-in-panel.html @@ -0,0 +1,57 @@ + + +
+
+
+
+ +
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-header-with-shadow.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-header-with-shadow.html new file mode 100644 index 000000000..3ca6056d7 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-header-with-shadow.html @@ -0,0 +1,35 @@ + + + + +
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-toolbar-in-panel.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-toolbar-in-panel.html new file mode 100644 index 000000000..da5e0e4c2 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/sticky-toolbar-in-panel.html @@ -0,0 +1,57 @@ + + +
+
+
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/target-element-inside-fixed.html b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/target-element-inside-fixed.html new file mode 100644 index 000000000..bd3276d1e --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/safe-areas/target-element-inside-fixed.html @@ -0,0 +1,46 @@ + + +
+
+
+ +
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-element-by/layout.html b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-element-by/layout.html new file mode 100644 index 000000000..dda82c654 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-element-by/layout.html @@ -0,0 +1,60 @@ + + +
Top content
+ +
+
+
+

Scrollable panel content

+

Large area to provide realistic scrollHeight and scrollWidth.

+
+
+
+ +
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/below-fold-window.html b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/below-fold-window.html new file mode 100644 index 000000000..d059445c3 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/below-fold-window.html @@ -0,0 +1,23 @@ + +
+
Below fold
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/labeled-scroll-panel.html b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/labeled-scroll-panel.html new file mode 100644 index 000000000..c44842a3f --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/labeled-scroll-panel.html @@ -0,0 +1,41 @@ + +
+
+
Panel target
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/nested-scroll.html b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/nested-scroll.html new file mode 100644 index 000000000..d6f8bc41f --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/nested-scroll.html @@ -0,0 +1,51 @@ + +
+
+
+
+
Nested target
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/scrollable-container.html b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/scrollable-container.html new file mode 100644 index 000000000..474aa9e2e --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/scrollable-container.html @@ -0,0 +1,34 @@ + +
+
+
Scrolled target
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/visible-target.html b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/visible-target.html new file mode 100644 index 000000000..a1fe05650 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/visible-target.html @@ -0,0 +1,22 @@ + +
+
In viewport
+
diff --git a/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/window-then-panel-scroll.html b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/window-then-panel-scroll.html new file mode 100644 index 000000000..3419d91c2 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/fixtures/scroll-to-capture/window-then-panel-scroll.html @@ -0,0 +1,53 @@ + +
+
+
+
Panel + window
+
+
+
diff --git a/test/browser-env/tests/desktop/screenshooter/getCommonScrollParent.testplane.ts b/test/browser-env/tests/desktop/screenshooter/getCommonScrollParent.testplane.ts new file mode 100644 index 000000000..6bf3a804b --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/getCommonScrollParent.testplane.ts @@ -0,0 +1,89 @@ +import { getCommonScrollParent } from "../../../../../src/browser/client-scripts/screen-shooter/utils/scroll"; + +function query(selector: string): Element { + const el = document.querySelector(selector); + if (!el) { + throw new Error(`Element not found: ${selector}`); + } + return el; +} + +describe("getCommonScrollParent", () => { + beforeEach(() => { + document.body.innerHTML = ""; + document.body.style.cssText = ""; + window.scrollTo(0, 0); + }); + + it("should return documentElement when selectors array is empty", () => { + const result = getCommonScrollParent([]); + + expect(result).toBe(document.documentElement); + }); + + it("should return documentElement when none of selectors match", async () => { + const { default: html } = await import("./fixtures/common-scroll-parent/analytics-dashboard.html?raw"); + document.body.innerHTML = html; + + const result = getCommonScrollParent([".missing-a", ".missing-b"]); + + expect(result).toBe(document.documentElement); + }); + + it("should return nearest scrollable parent for a single matched selector", async () => { + const { default: html } = await import("./fixtures/common-scroll-parent/analytics-dashboard.html?raw"); + document.body.innerHTML = html; + + const result = getCommonScrollParent(["#single-in-dashboard"]); + const expected = query(".dashboard-scroll"); + + expect(result).toBe(expected); + }); + + it("should return documentElement for a single matched selector inside fixed overlay", async () => { + const { default: html } = await import("./fixtures/common-scroll-parent/fixed-modal.html?raw"); + document.body.innerHTML = html; + + const result = getCommonScrollParent(["#modal-target"]); + expect(result).toBe(document.documentElement); + }); + + it("should return deepest common scroll parent for multiple selectors in same nested scroll container", async () => { + const { default: html } = await import("./fixtures/common-scroll-parent/analytics-dashboard.html?raw"); + document.body.innerHTML = html; + + const result = getCommonScrollParent(["#feed-item-a", "#feed-item-b"]); + const expected = query(".feed-scroll"); + + expect(result).toBe(expected); + }); + + it("should return outer shared scroll parent when elements belong to different nested scroll levels", async () => { + const { default: html } = await import("./fixtures/common-scroll-parent/analytics-dashboard.html?raw"); + document.body.innerHTML = html; + + const result = getCommonScrollParent(["#dashboard-item", "#feed-item-b"]); + const expected = query(".dashboard-scroll"); + + expect(result).toBe(expected); + }); + + it("should return documentElement when matched elements belong to different independent scroll roots", async () => { + const { default: html } = await import("./fixtures/common-scroll-parent/split-panels.html?raw"); + document.body.innerHTML = html; + + const result = getCommonScrollParent(["#left-target", "#right-target"]); + + expect(result).toBe(document.documentElement); + }); + + it("should ignore unmatched selectors and compute parent from matched ones", async () => { + const { default: html } = await import("./fixtures/common-scroll-parent/split-panels.html?raw"); + document.body.innerHTML = html; + + const result = getCommonScrollParent(["#left-target", ".non-existent-selector"]); + const expected = query(".left-panel"); + + expect(result).toBe(expected); + }); +}); diff --git a/test/browser-env/tests/desktop/screenshooter/scrollElementBy.testplane.ts b/test/browser-env/tests/desktop/screenshooter/scrollElementBy.testplane.ts new file mode 100644 index 000000000..1ebbbebfe --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/scrollElementBy.testplane.ts @@ -0,0 +1,129 @@ +import { scrollElementBy } from "../../../../../src/browser/client-scripts/screen-shooter/utils/scroll"; +import { Coord } from "../../../../../src/browser/isomorphic/geometry"; + +function query(selector: string): Element { + const el = document.querySelector(selector); + if (!el) { + throw new Error(`Element not found: ${selector}`); + } + return el; +} + +function getMaxScrollY(element: Element): number { + return element.scrollHeight - element.clientHeight; +} + +describe("scrollElementBy", () => { + beforeEach(() => { + document.body.innerHTML = ""; + document.body.style.cssText = ""; + window.scrollTo(0, 0); + }); + + describe("documentElement scroll", () => { + it("should scroll root by floored delta value", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + + scrollElementBy(document.documentElement, 120.9 as Coord<"page", "css", "y">); + + expect(window.scrollY).toBe(120); + }); + + it("should clamp root scroll at zero for large negative delta", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + window.scrollTo(0, 260); + + scrollElementBy(document.documentElement, -1000 as Coord<"page", "css", "y">); + + expect(window.scrollY).toBe(0); + }); + + it("should clamp root scroll at max for overshoot delta", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + + const maxScrollY = getMaxScrollY(document.documentElement); + window.scrollTo(0, maxScrollY - 5); + + scrollElementBy(document.documentElement, 500 as Coord<"page", "css", "y">); + + expect(window.scrollY).toBe(maxScrollY); + }); + }); + + describe("non-root element scroll", () => { + it("should scroll container by floored delta and keeps horizontal scroll", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + + const panel = query(".panel") as HTMLElement; + panel.scrollTo(90, 100); + + scrollElementBy(panel, 40.8 as Coord<"page", "css", "y">); + + expect(panel.scrollTop).toBe(140); + expect(panel.scrollLeft).toBe(90); + }); + + it("should clamp container scroll at zero", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + + const panel = query(".panel") as HTMLElement; + panel.scrollTop = 6; + + scrollElementBy(panel, -100 as Coord<"page", "css", "y">); + + expect(panel.scrollTop).toBe(0); + }); + + it("should clamp container scroll at max", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + + const panel = query(".panel") as HTMLElement; + const maxScrollY = getMaxScrollY(panel); + panel.scrollTop = maxScrollY - 3; + + scrollElementBy(panel, 200 as Coord<"page", "css", "y">); + + expect(panel.scrollTop).toBe(maxScrollY); + }); + + it("should not move when floored delta is zero", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + + const panel = query(".panel") as HTMLElement; + panel.scrollTop = 42; + + scrollElementBy(panel, 0.99 as Coord<"page", "css", "y">); + + expect(panel.scrollTop).toBe(42); + }); + }); + + describe('"almost-root" element scroll', () => { + it("should scroll page when body is passed as scroll element", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + window.scrollTo(0, 0); + + scrollElementBy(document.body, 120 as Coord<"page", "css", "y">); + + expect(window.scrollY).toBeGreaterThan(0); + }); + + it("should scroll page when html is passed as scroll element", async () => { + const { default: html } = await import("./fixtures/scroll-element-by/layout.html?raw"); + document.body.innerHTML = html; + window.scrollTo(0, 0); + + scrollElementBy(document.body.parentElement as Element, 120 as Coord<"page", "css", "y">); + + expect(window.scrollY).toBeGreaterThan(0); + }); + }); +}); diff --git a/test/browser-env/tests/desktop/screenshooter/scrollToCaptureAreaIfNeeded.testplane.ts b/test/browser-env/tests/desktop/screenshooter/scrollToCaptureAreaIfNeeded.testplane.ts new file mode 100644 index 000000000..42b5e0ce6 --- /dev/null +++ b/test/browser-env/tests/desktop/screenshooter/scrollToCaptureAreaIfNeeded.testplane.ts @@ -0,0 +1,227 @@ +import { getCoveringRect } from "../../../../../src/browser/isomorphic"; +import { OutsideOfViewportError } from "../../../../../src/browser/client-scripts/screen-shooter/errors/outside-of-viewport"; +import { + computeCaptureSpecs, + computeSafeArea, + scrollToCaptureAreaIfNeeded, +} from "../../../../../src/browser/client-scripts/screen-shooter/operations"; + +function clientRectSnapshot(el: Element): { top: number; left: number; width: number; height: number } { + const r = el.getBoundingClientRect(); + return { top: r.top, left: r.left, width: r.width, height: r.height }; +} + +function expectClientRectClose( + a: { top: number; left: number; width: number; height: number }, + b: { top: number; left: number; width: number; height: number }, + tol = 1, +): void { + expect(a.top).toBeCloseTo(b.top, tol); + expect(a.left).toBeCloseTo(b.left, tol); + expect(a.width).toBeCloseTo(b.width, tol); + expect(a.height).toBeCloseTo(b.height, tol); +} + +function coveringFullTop(selectors: string[]): number { + const specs = computeCaptureSpecs(selectors).captureSpecs; + const area = getCoveringRect(specs.map(s => s.full)); + return area.top as number; +} + +function expectCaptureAlignedToSafeArea(selectors: string[], scrollElement: Element | undefined): void { + const top = coveringFullTop(selectors); + const safe = computeSafeArea(selectors, scrollElement).safeArea; + expect(top).toBeCloseTo(safe.top as number, 0); +} + +describe("scrollToCaptureAreaIfNeeded", () => { + beforeEach(() => { + window.scrollTo(0, 0); + document.body.innerHTML = ""; + document.body.style.cssText = ""; + }); + + it("should throw OutsideOfViewportError when the target is outside the viewport and captureElementFromTop is false", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/below-fold-window.html?raw"); + document.body.innerHTML = html; + + const target = document.querySelector(".target")!; + const before = clientRectSnapshot(target); + + expect(() => scrollToCaptureAreaIfNeeded([".target"], false)).toThrow(OutsideOfViewportError); + + expectClientRectClose(clientRectSnapshot(target), before); + expect(window.scrollY).toBe(0); + }); + + it("should not scroll when the target is already in view", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/visible-target.html?raw"); + document.body.innerHTML = html; + const scrollBefore = window.scrollY; + + scrollToCaptureAreaIfNeeded([".target"], true); + + expect(window.scrollY).toBe(scrollBefore); + }); + + it("should scroll when the target starts above the viewport", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/window-then-panel-scroll.html?raw"); + document.body.innerHTML = html; + + const target = document.getElementById("capture-panel")!; + window.scrollTo(0, target!.getBoundingClientRect().top + 100); + + expect(target.getBoundingClientRect().top).toBeLessThan(0); + + scrollToCaptureAreaIfNeeded(["#capture-panel"], true); + + expect(target.getBoundingClientRect().top).toBe(0); + }); + + it("should treat omitted captureElementFromTop like false for an in-viewport target", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/visible-target.html?raw"); + document.body.innerHTML = html; + + const target = document.querySelector(".target")!; + const before = clientRectSnapshot(target); + + const result = scrollToCaptureAreaIfNeeded([".target"]); + + expect(result).toEqual({}); + expectClientRectClose(clientRectSnapshot(target), before); + }); + + it("should return {} without scrolling when captureElementFromTop is true but the safe-area intersection is already tall enough", async () => { + const { default: html } = await import("./fixtures/safe-areas/fixed-app-header.html?raw"); + document.body.innerHTML = html; + + const target = document.querySelector(".target")!; + const before = clientRectSnapshot(target); + const scrollBefore = window.scrollY; + + const result = scrollToCaptureAreaIfNeeded([".target"], true); + + expect(result).toEqual({}); + expectClientRectClose(clientRectSnapshot(target), before); + expect(window.scrollY).toBe(scrollBefore); + }); + + it("should scroll the window so the capture area aligns with the safe area when the target is below the fold", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/below-fold-window.html?raw"); + document.body.innerHTML = html; + + const target = document.querySelector(".target")!; + const before = clientRectSnapshot(target); + expect(before.top).toBeGreaterThan(700); + + const result = scrollToCaptureAreaIfNeeded([".target"], true); + + expect(result).toEqual({ readableSelectorToScrollDescr: "html" }); + expect(window.scrollY).toBeGreaterThan(0); + expectCaptureAlignedToSafeArea([".target"], document.documentElement); + expect(clientRectSnapshot(target).top).toBeLessThan(before.top); + }); + + it("should scroll the window to the panel and then scrolls inside the panel", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/window-then-panel-scroll.html?raw"); + document.body.innerHTML = html; + + const panel = document.querySelector("#capture-panel")!; + const target = document.querySelector(".target")!; + panel.scrollTop = 0; + window.scrollTo(0, 0); + + const before = clientRectSnapshot(target); + expect(before.top).toBeGreaterThan(window.innerHeight); + expect(panel.scrollTop).toBe(0); + + const result = scrollToCaptureAreaIfNeeded([".target"], true); + + expect(result.readableSelectorToScrollDescr).toBe("div#capture-panel"); + expect(window.scrollY).toBeGreaterThan(0); + expect(panel.scrollTop).toBeGreaterThan(0); + expect(clientRectSnapshot(target).top).toBeLessThan(before.top); + expectCaptureAlignedToSafeArea([".target"], panel); + }); + + it("should scroll a single overflow container without changing window scroll", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/scrollable-container.html?raw"); + document.body.innerHTML = html; + + const container = document.querySelector(".container")!; + const target = document.querySelector(".target")!; + container.scrollTop = 0; + + const before = clientRectSnapshot(target); + expect(before.top).toBeGreaterThan(container.getBoundingClientRect().bottom); + + const result = scrollToCaptureAreaIfNeeded([".target"], true); + + expect(result.readableSelectorToScrollDescr).toMatch(/container/); + expect(window.scrollY).toBe(0); + expect(container.scrollTop).toBeGreaterThan(0); + expectCaptureAlignedToSafeArea([".target"], container); + }); + + it("should walk nested scroll parents and align the capture area to the inner scroll root safe area", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/nested-scroll.html?raw"); + document.body.innerHTML = html; + + const outer = document.querySelector(".outer")!; + const inner = document.querySelector(".inner")!; + const target = document.querySelector(".target")!; + + outer.scrollTop = 0; + inner.scrollTop = 0; + window.scrollTo(0, 0); + + const before = clientRectSnapshot(target); + expect(before.top).toBeGreaterThan(inner.getBoundingClientRect().bottom - 20); + + const result = scrollToCaptureAreaIfNeeded([".target"], true); + + expect(inner.className).toContain("inner"); + expect(result.readableSelectorToScrollDescr).toMatch(/inner/); + expect(outer.scrollTop).toBeGreaterThan(0); + expect(inner.scrollTop).toBeGreaterThan(0); + expectCaptureAlignedToSafeArea([".target"], inner); + }); + + it("should use selectorToScroll as the scroll root and report it in readableSelectorToScrollDescr", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/labeled-scroll-panel.html?raw"); + document.body.innerHTML = html; + + const panel = document.querySelector("#labeled-panel")!; + const target = document.querySelector(".target")!; + panel.scrollTop = 0; + + const before = clientRectSnapshot(target); + expect(before.top).toBeGreaterThan(panel.getBoundingClientRect().bottom); + + const result = scrollToCaptureAreaIfNeeded([".target"], true, false, "#labeled-panel"); + + expect(result).toEqual({ readableSelectorToScrollDescr: "#labeled-panel" }); + expect(panel.scrollTop).toBeGreaterThan(0); + expect(window.scrollY).toBe(0); + expectCaptureAlignedToSafeArea([".target"], panel); + }); + + it("should fall back to the common scroll parent when selectorToScroll does not match any element", async () => { + const { default: html } = await import("./fixtures/scroll-to-capture/labeled-scroll-panel.html?raw"); + document.body.innerHTML = html; + + const panel = document.querySelector("#labeled-panel")!; + const target = document.querySelector(".target")!; + panel.scrollTop = 0; + + const before = clientRectSnapshot(target); + + const result = scrollToCaptureAreaIfNeeded([".target"], true, false, ".nonexistent-scroll-root"); + + expect(result).toEqual({ readableSelectorToScrollDescr: ".nonexistent-scroll-root" }); + expect(panel.scrollTop).toBeGreaterThan(0); + expect(window.scrollY).toBe(0); + expect(clientRectSnapshot(target).top).toBeLessThan(before.top); + expectCaptureAlignedToSafeArea([".target"], panel); + }); +}); diff --git a/test/browser-env/tests/high-pixel-ratio/fixtures/viewport-screenshot/dashboard-long-page.html b/test/browser-env/tests/high-pixel-ratio/fixtures/viewport-screenshot/dashboard-long-page.html new file mode 100644 index 000000000..18590b85e --- /dev/null +++ b/test/browser-env/tests/high-pixel-ratio/fixtures/viewport-screenshot/dashboard-long-page.html @@ -0,0 +1,89 @@ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/browser-env/tests/high-pixel-ratio/prepareViewportScreenshot.testplane.ts b/test/browser-env/tests/high-pixel-ratio/prepareViewportScreenshot.testplane.ts new file mode 100644 index 000000000..0ba4b201a --- /dev/null +++ b/test/browser-env/tests/high-pixel-ratio/prepareViewportScreenshot.testplane.ts @@ -0,0 +1,45 @@ +import { prepareViewportScreenshot } from "../../../../src/browser/client-scripts/screen-shooter/implementation"; +import { + computeDocumentSize, + computeViewportOffset, + computeViewportSize, +} from "../../../../src/browser/client-scripts/screen-shooter/operations"; +import type { PrepareViewportScreenshotSuccess } from "../../../../src/browser/client-scripts/screen-shooter/types"; +import { isBrowserSideError } from "../../../../src/browser/isomorphic/types"; + +function getPrepareResult(opts: Parameters[0]): PrepareViewportScreenshotSuccess { + const result = prepareViewportScreenshot(opts); + + if (isBrowserSideError(result)) { + throw new Error(`Browser-side error in prepareViewportScreenshot: ${result.errorCode}: ${result.message}`); + } + + return result; +} + +describe("prepareViewportScreenshot in high pixel ratio mode", () => { + beforeEach(async () => { + const { default: html } = await import("./fixtures/viewport-screenshot/dashboard-long-page.html?raw"); + document.body.innerHTML = html; + document.body.style.margin = "0"; + window.scrollTo(0, 0); + }); + + it("returns viewport and document dimensions translated to device pixels", async ({ browser }) => { + window.scrollTo(0, 245); + + const cssViewportSize = computeViewportSize().viewportSize; + const cssViewportOffset = computeViewportOffset().viewportOffset; + const cssDocumentSize = computeDocumentSize().documentSize; + + const result = getPrepareResult({ usePixelRatio: true }); + + expect(result.pixelRatio).toBe(3); + expect(result.viewportSize.width).toBe((cssViewportSize.width as number) * 3); + expect(result.viewportSize.height).toBe((cssViewportSize.height as number) * 3); + expect(result.viewportOffset.left).toBe(Math.floor(cssViewportOffset.left as number) * 3); + expect(result.viewportOffset.top).toBe(Math.floor(cssViewportOffset.top as number) * 3); + expect(result.documentSize.width).toBe(Math.ceil((cssDocumentSize.width as number) * 3)); + expect(result.documentSize.height).toBe(Math.ceil((cssDocumentSize.height as number) * 3)); + }); +}); diff --git a/test/browser-env/tests/utils.ts b/test/browser-env/tests/utils.ts new file mode 100644 index 000000000..e92536113 --- /dev/null +++ b/test/browser-env/tests/utils.ts @@ -0,0 +1,48 @@ +import { CaptureSpec } from "../../../src/browser/client-scripts/screen-shooter/types"; + +export function visualizeCaptureSpecs(specs: CaptureSpec<"viewport", "css">[]): void { + specs.forEach((spec, i) => { + // Full area — green + const fullDiv = document.createElement("div"); + fullDiv.style.position = "fixed"; + fullDiv.style.left = `${spec.full.left}px`; + fullDiv.style.top = `${spec.full.top}px`; + fullDiv.style.width = `${spec.full.width}px`; + fullDiv.style.height = `${spec.full.height}px`; + fullDiv.style.backgroundColor = "rgba(0, 255, 0, 0.3)"; + fullDiv.style.outline = "1px solid green"; + fullDiv.style.zIndex = "99999"; + fullDiv.style.pointerEvents = "none"; + fullDiv.dataset.captureArea = String(i); + document.body.appendChild(fullDiv); + + // Visible area — blue + const visDiv = document.createElement("div"); + visDiv.style.position = "fixed"; + visDiv.style.left = `${spec.visible.left}px`; + visDiv.style.top = `${spec.visible.top}px`; + visDiv.style.width = `${spec.visible.width}px`; + visDiv.style.height = `${spec.visible.height}px`; + visDiv.style.backgroundColor = "rgba(0, 100, 255, 0.3)"; + visDiv.style.outline = "1px dashed blue"; + visDiv.style.zIndex = "99999"; + visDiv.style.pointerEvents = "none"; + visDiv.dataset.visibleArea = String(i); + document.body.appendChild(visDiv); + }); +} + +export function visualizeSafeArea(top: number, height: number): void { + const safeDiv = document.createElement("div"); + safeDiv.style.position = "fixed"; + safeDiv.style.left = "0"; + safeDiv.style.top = `${top}px`; + safeDiv.style.width = "100vw"; + safeDiv.style.height = `${height}px`; + safeDiv.style.backgroundColor = "rgba(0, 200, 120, 0.2)"; + safeDiv.style.outline = "3px solid rgba(0, 160, 100, 0.95)"; + safeDiv.style.zIndex = "2147483645"; + safeDiv.style.pointerEvents = "none"; + safeDiv.dataset.safeArea = "1"; + document.body.appendChild(safeDiv); +} diff --git a/test/browser-env/tsconfig.json b/test/browser-env/tsconfig.json new file mode 100644 index 000000000..0c8f9666e --- /dev/null +++ b/test/browser-env/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "esnext", + "moduleResolution": "bundler", + "target": "esnext", + "lib": ["dom", "dom.iterable", "esnext"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": [], + "paths": { + "@isomorphic": ["../../src/browser/isomorphic/index.ts"] + } + }, + "include": ["testplane.config.ts", "tests", "types.d.ts"] +} diff --git a/test/browser-env/types.d.ts b/test/browser-env/types.d.ts new file mode 100644 index 000000000..c4cbfdc01 --- /dev/null +++ b/test/browser-env/types.d.ts @@ -0,0 +1,4 @@ +declare module "*.html?raw" { + const content: string; + export default content; +} diff --git a/test/browser-env/vite.config.ts b/test/browser-env/vite.config.ts new file mode 100644 index 000000000..4c244136f --- /dev/null +++ b/test/browser-env/vite.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from "vite"; +import path from "path"; + +export default defineConfig({ + plugins: [], + server: { + host: "0.0.0.0", + port: 5173, + strictPort: true, + }, + define: { + global: "globalThis", + exports: "{}", + module: "{ exports: {} }", + }, + optimizeDeps: { + include: [ + "lib/**/*.js", + "expect", + "aria-query", + "css-shorthand-properties", + "css-value", + "grapheme-splitter", + "lodash.clonedeep", + "lodash.zip", + "minimatch", + "rgb2hex", + "ws", + "debug", + ], + }, + resolve: { + alias: { + "@isomorphic": path.resolve(__dirname, "../../src/browser/isomorphic/index.ts"), + "@": path.resolve(__dirname, "../../lib"), + lib: path.resolve(__dirname, "../../lib"), + }, + }, + css: { + modules: { + localsConvention: "camelCase", + }, + }, +});