diff options
| author | Darius Jahandarie <djahandarie@gmail.com> | 2023-03-21 23:18:44 +0900 | 
|---|---|---|
| committer | Darius Jahandarie <djahandarie@gmail.com> | 2023-03-23 21:52:22 +0900 | 
| commit | 95b2bb25d4175ff676c5a3d4e5ff0ef214f7b306 (patch) | |
| tree | bcd3651e64dbf460e675db0737fa559662720f57 | |
| parent | bb9c6ec4118c1ed8fd67ff4a760c352675086f6a (diff) | |
Add visual diffing in CI
| -rw-r--r-- | .eslintrc.json | 347 | ||||
| -rw-r--r-- | .github/workflows/playwright.yml | 109 | ||||
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | package-lock.json | 74 | ||||
| -rw-r--r-- | package.json | 1 | ||||
| -rw-r--r-- | playwright.config.js | 109 | ||||
| -rw-r--r-- | test/data/html/test-document2.html | 229 | ||||
| -rw-r--r-- | test/playwright/visual.spec.js | 120 | 
8 files changed, 823 insertions, 171 deletions
| diff --git a/.eslintrc.json b/.eslintrc.json index 756acbda..b5328f41 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,7 +5,7 @@          "plugin:jsonc/recommended-with-json"      ],      "parserOptions": { -        "ecmaVersion": 8, +        "ecmaVersion": 9,          "sourceType": "script",          "ecmaFeatures": {              "globalReturn": false, @@ -14,7 +14,7 @@      },      "env": {          "browser": true, -        "es2017": true, +        "es2018": true,          "webextensions": true      },      "plugins": [ @@ -27,12 +27,24 @@          "/ext/lib/"      ],      "rules": { -        "arrow-parens": ["error", "always"], -        "comma-dangle": ["error", "never"], -        "curly": ["error", "all"], +        "arrow-parens": [ +            "error", +            "always" +        ], +        "comma-dangle": [ +            "error", +            "never" +        ], +        "curly": [ +            "error", +            "all" +        ],          "dot-notation": "error",          "eqeqeq": "error", -        "func-names": ["error", "always"], +        "func-names": [ +            "error", +            "always" +        ],          "guard-for-in": "error",          "new-parens": "error",          "no-case-declarations": "error", @@ -41,57 +53,184 @@          "no-global-assign": "error",          "no-param-reassign": "off",          "no-prototype-builtins": "error", -        "no-shadow": ["error", {"builtinGlobals": false}], +        "no-shadow": [ +            "error", +            { +                "builtinGlobals": false +            } +        ],          "no-undef": "error",          "no-undefined": "error", -        "no-underscore-dangle": ["error", {"allowAfterThis": true, "allowAfterSuper": false, "allowAfterThisConstructor": false}], +        "no-underscore-dangle": [ +            "error", +            { +                "allowAfterThis": true, +                "allowAfterSuper": false, +                "allowAfterThisConstructor": false +            } +        ],          "no-unexpected-multiline": "error",          "no-unneeded-ternary": "error", -        "no-unused-vars": ["error", {"vars": "local", "args": "after-used", "argsIgnorePattern": "^_", "caughtErrors": "none"}], +        "no-unused-vars": [ +            "error", +            { +                "vars": "local", +                "args": "after-used", +                "argsIgnorePattern": "^_", +                "caughtErrors": "none" +            } +        ],          "no-unused-expressions": "error",          "no-var": "error", -        "prefer-const": ["error", {"destructuring": "all"}], -        "quote-props": ["error", "consistent"], -        "quotes": ["error", "single", "avoid-escape"], +        "prefer-const": [ +            "error", +            { +                "destructuring": "all" +            } +        ], +        "quote-props": [ +            "error", +            "consistent" +        ], +        "quotes": [ +            "error", +            "single", +            "avoid-escape" +        ],          "require-atomic-updates": "off",          "semi": "error", -        "wrap-iife": ["error", "inside"], - -        "brace-style": ["error", "1tbs", {"allowSingleLine": true}], -        "indent": ["error", 4, {"SwitchCase": 1, "MemberExpression": 1, "flatTernaryExpressions": true, "ignoredNodes": ["ConditionalExpression"]}], +        "wrap-iife": [ +            "error", +            "inside" +        ], +        "brace-style": [ +            "error", +            "1tbs", +            { +                "allowSingleLine": true +            } +        ], +        "indent": [ +            "error", +            4, +            { +                "SwitchCase": 1, +                "MemberExpression": 1, +                "flatTernaryExpressions": true, +                "ignoredNodes": [ +                    "ConditionalExpression" +                ] +            } +        ],          "object-curly-newline": "error", -        "padded-blocks": ["error", "never"], - -        "array-bracket-spacing": ["error", "never"], -        "arrow-spacing": ["error", {"before": true, "after": true}], -        "block-spacing": ["error", "always"], -        "comma-spacing": ["error", {"before": false, "after": true}], -        "computed-property-spacing": ["error", "never"], -        "func-call-spacing": ["error", "never"], -        "function-paren-newline": ["error", "multiline-arguments"], -        "generator-star-spacing": ["error", "before"], -        "key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}], -        "keyword-spacing": ["error", {"before": true, "after": true}], +        "padded-blocks": [ +            "error", +            "never" +        ], +        "array-bracket-spacing": [ +            "error", +            "never" +        ], +        "arrow-spacing": [ +            "error", +            { +                "before": true, +                "after": true +            } +        ], +        "block-spacing": [ +            "error", +            "always" +        ], +        "comma-spacing": [ +            "error", +            { +                "before": false, +                "after": true +            } +        ], +        "computed-property-spacing": [ +            "error", +            "never" +        ], +        "func-call-spacing": [ +            "error", +            "never" +        ], +        "function-paren-newline": [ +            "error", +            "multiline-arguments" +        ], +        "generator-star-spacing": [ +            "error", +            "before" +        ], +        "key-spacing": [ +            "error", +            { +                "beforeColon": false, +                "afterColon": true, +                "mode": "strict" +            } +        ], +        "keyword-spacing": [ +            "error", +            { +                "before": true, +                "after": true +            } +        ],          "no-trailing-spaces": "error",          "no-whitespace-before-property": "error", -        "object-curly-spacing": ["error", "never"], -        "rest-spread-spacing": ["error", "never"], -        "semi-spacing": ["error", {"before": false, "after": true}], -        "space-before-function-paren": ["error", { -            "anonymous": "never", -            "named": "never", -            "asyncArrow": "always" -        }], -        "space-in-parens": ["error", "never"], +        "object-curly-spacing": [ +            "error", +            "never" +        ], +        "rest-spread-spacing": [ +            "error", +            "never" +        ], +        "semi-spacing": [ +            "error", +            { +                "before": false, +                "after": true +            } +        ], +        "space-before-function-paren": [ +            "error", +            { +                "anonymous": "never", +                "named": "never", +                "asyncArrow": "always" +            } +        ], +        "space-in-parens": [ +            "error", +            "never" +        ],          "space-unary-ops": "error", -        "spaced-comment": ["error", "always"], -        "switch-colon-spacing": ["error", {"after": true, "before": false}], -        "template-curly-spacing": ["error", "never"], -        "template-tag-spacing": ["error", "never"], - +        "spaced-comment": [ +            "error", +            "always" +        ], +        "switch-colon-spacing": [ +            "error", +            { +                "after": true, +                "before": false +            } +        ], +        "template-curly-spacing": [ +            "error", +            "never" +        ], +        "template-tag-spacing": [ +            "error", +            "never" +        ],          "no-unsanitized/method": "error",          "no-unsanitized/property": "error", -          "jsdoc/check-access": "error",          "jsdoc/check-alignment": "error",          "jsdoc/check-line-alignment": "error", @@ -103,11 +242,17 @@          "jsdoc/empty-tags": "error",          "jsdoc/implements-on-classes": "error",          "jsdoc/multiline-blocks": "error", -        "jsdoc/newline-after-description": ["error", "never"], +        "jsdoc/newline-after-description": [ +            "error", +            "never" +        ],          "jsdoc/no-bad-blocks": "error",          "jsdoc/no-multi-asterisks": "error",          "jsdoc/require-asterisk-prefix": "error", -        "jsdoc/require-hyphen-before-param-description": ["error", "never"], +        "jsdoc/require-hyphen-before-param-description": [ +            "error", +            "never" +        ],          "jsdoc/require-jsdoc": "off",          "jsdoc/require-param": "error",          "jsdoc/require-param-description": "error", @@ -126,17 +271,51 @@          "jsdoc/require-yields-check": "error",          "jsdoc/tag-lines": "error",          "jsdoc/valid-types": "error", - -        "jsonc/indent": ["error", 4], -        "jsonc/array-bracket-newline": ["error", "consistent"], -        "jsonc/array-bracket-spacing": ["error", "never"], -        "jsonc/array-element-newline": ["error", "consistent"], -        "jsonc/comma-style": ["error", "last"], -        "jsonc/key-spacing": ["error", {"beforeColon": false, "afterColon": true, "mode": "strict"}], +        "jsonc/indent": [ +            "error", +            4 +        ], +        "jsonc/array-bracket-newline": [ +            "error", +            "consistent" +        ], +        "jsonc/array-bracket-spacing": [ +            "error", +            "never" +        ], +        "jsonc/array-element-newline": [ +            "error", +            "consistent" +        ], +        "jsonc/comma-style": [ +            "error", +            "last" +        ], +        "jsonc/key-spacing": [ +            "error", +            { +                "beforeColon": false, +                "afterColon": true, +                "mode": "strict" +            } +        ],          "jsonc/no-octal-escape": "error", -        "jsonc/object-curly-newline": ["error", {"consistent": true}], -        "jsonc/object-curly-spacing": ["error", "never"], -        "jsonc/object-property-newline": ["error", {"allowAllPropertiesOnSameLine": true}] +        "jsonc/object-curly-newline": [ +            "error", +            { +                "consistent": true +            } +        ], +        "jsonc/object-curly-spacing": [ +            "error", +            "never" +        ], +        "jsonc/object-property-newline": [ +            "error", +            { +                "allowAllPropertiesOnSameLine": true +            } +        ]      },      "overrides": [          { @@ -152,7 +331,10 @@                  "test/data/translator-test-results.json"              ],              "rules": { -                "jsonc/indent": ["error", 2] +                "jsonc/indent": [ +                    "error", +                    2 +                ]              }          },          { @@ -160,8 +342,12 @@                  "test/data/dictionaries/valid-dictionary1/term_bank_1.json"              ],              "rules": { -                "jsonc/array-element-newline": ["off"], -                "jsonc/object-property-newline": ["off"] +                "jsonc/array-element-newline": [ +                    "off" +                ], +                "jsonc/object-property-newline": [ +                    "off" +                ]              }          },          { @@ -188,7 +374,9 @@              }          },          { -            "files": ["ext/**/*.js"], +            "files": [ +                "ext/**/*.js" +            ],              "excludedFiles": [                  "ext/js/core.js",                  "ext/js/accessibility/google-docs.js", @@ -215,7 +403,9 @@              }          },          { -            "files": ["ext/**/*.js"], +            "files": [ +                "ext/**/*.js" +            ],              "excludedFiles": [                  "ext/js/core.js",                  "ext/js/accessibility/google-docs.js", @@ -227,7 +417,9 @@              }          },          { -            "files": ["ext/js/yomichan.js"], +            "files": [ +                "ext/js/yomichan.js" +            ],              "globals": {                  "chrome": "writable"              } @@ -237,7 +429,9 @@                  "test/**/*.js",                  "dev/**/*.js"              ], -            "excludedFiles": ["test/data/html/*.js"], +            "excludedFiles": [ +                "test/data/html/*.js" +            ],              "parserOptions": {                  "ecmaVersion": 8,                  "sourceType": "module" @@ -251,6 +445,35 @@          },          {              "files": [ +                "playwright.config.js" +            ], +            "env": { +                "browser": false, +                "es2017": true, +                "node": true, +                "webextensions": false +            }, +            "rules": { +                "no-undefined": "off" +            } +        }, +        { +            "files": [ +                "visual.spec.js" +            ], +            "env": { +                "browser": false, +                "es2017": true, +                "node": true, +                "webextensions": false +            }, +            "rules": { +                "no-undefined": "off", +                "no-empty-pattern": "off" +            } +        }, +        { +            "files": [                  "ext/js/core.js",                  "ext/js/yomichan.js",                  "ext/js/accessibility/accessibility-controller.js", diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..5ee786ef --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,109 @@ +name: Playwright Tests +on: +  push: +    branches: [master] +  pull_request: +permissions: +  contents: read +jobs: +  playwright: +    timeout-minutes: 60 +    runs-on: ubuntu-latest +    permissions: +      pull-requests: write +      contents: write +    steps: +      - uses: actions/checkout@v3 + +      - uses: actions/setup-node@v3 +        with: +          cache: "npm" +          node-version-file: ".node-version" + +      - name: Install dependencies +        run: npm ci + +      - name: Cache playwright browsers +        id: cache-playwright +        uses: actions/cache@v3 +        with: +          path: | +            ~/.cache/ms-playwright +          key: cache-playwright-${{ hashFiles('package-lock.json') }} # playwright version is included in package-lock, so this serves as a reasonable cache key + +      - if: ${{ steps.cache-playwright.outputs.cache-hit != 'true' }} +        name: Install Playwright Browsers +        run: npx playwright install --with-deps chromium + +      - name: Grab latest dictionaries from dictionaries branch +        uses: actions/checkout@v3 +        with: +          ref: dictionaries +          path: dictionaries + +      - name: Grab latest screenshots from master branch +        uses: dawidd6/action-download-artifact@5e780fc7bbd0cac69fc73271ed86edf5dcb72d67 # pin@v2 +        continue-on-error: true +        id: download-screenshots +        with: +          github_token: ${{ secrets.GITHUB_TOKEN }} +          name: playwright-screenshots +          branch: master +          workflow: playwright.yml +          workflow_conclusion: success +          path: test/playwright/__screenshots__/ + +      - name: "[PR] Generate new screenshots & compare against master" +        id: playwright +        run: | +          EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) +          echo "PLAYWRIGHT_OUTPUT<<$EOF" >> $GITHUB_OUTPUT +          npx playwright test 2>&1 | tee $GITHUB_OUTPUT || true +          echo "$EOF" >> $GITHUB_OUTPUT +          echo "NUM_FAILED=$(grep -c 'Screenshot comparison failed' $GITHUB_OUTPUT)" >> $GITHUB_OUTPUT +        continue-on-error: true +        if: github.event_name == 'pull_request' && steps.download-screenshots.outcome != 'failure' + +      - name: "[Push] Generate new authoritative screenshots for master" +        id: playwright-master +        run: npx playwright test -u +        if: github.event_name == 'push' + +      - uses: actions/upload-artifact@v3 +        with: +          name: playwright-screenshots +          path: test/playwright/__screenshots__/ + +      - uses: actions/upload-artifact@v3 +        with: +          name: playwright-report +          path: playwright-report/ + +      - name: "[Couldn't download screenshots] Comment results on PR" +        uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 # pin@v2 +        if: github.event_name == 'pull_request' && steps.download-screenshots.outcome == 'failure' +        with: +          message: | +            :heavy_exclamation_mark: Could not fetch screenshots from master branch, so had nothing to make a visual comparison against; please check the "download-screenshots" step in the workflow run and rerun it before merging. + +      - name: "[Success] Comment results on PR" +        uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 # pin@v2 +        if: github.event_name == 'pull_request' && steps.download-screenshots.outcome != 'failure' && steps.playwright.outputs.NUM_FAILED == 0 +        with: +          message: | +            :heavy_check_mark: No visual differences introduced by this PR. +            [View Playwright Report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts) (note: open the "playwright-report" artifact) + +      - name: "[Failure] Comment results on PR" +        uses: mshick/add-pr-comment@a65df5f64fc741e91c59b8359a4bc56e57aaf5b1 # pin@v2 +        if: github.event_name == 'pull_request' && steps.download-screenshots.outcome != 'failure' && steps.playwright.outputs.NUM_FAILED != 0 +        with: +          message: | +            :warning: {{ steps.playwright.outputs.NUM_FAILED }} visual differences introduced by this PR; please validate if they are desirable. +            <details> +            <summary>Playwright Test Results</summary> +            <pre> +            ${{ steps.playwright.outputs.PLAYWRIGHT_OUTPUT }} +            </pre> +            </details> +            [View Playwright Report](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts) (note: open the "playwright-report" artifact) @@ -1,3 +1,8 @@  node_modules/  builds/  .DS_Store +dictionaries/ +/test-results/ +/playwright-report/ +/playwright/.cache/ +/test/playwright/__screenshots__/ diff --git a/package-lock.json b/package-lock.json index 5dd54bc6..4878437a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@  { -    "name": "yomichan", +    "name": "yomitan",      "version": "0.0.0",      "lockfileVersion": 2,      "requires": true,      "packages": {          "": { -            "name": "yomichan", +            "name": "yomitan",              "version": "0.0.0",              "hasInstallScript": true,              "license": "GPL-3.0-or-later",              "devDependencies": { +                "@playwright/test": "^1.31.2",                  "ajv": "^8.11.0",                  "browserify": "^17.0.0",                  "css": "^3.0.0", @@ -332,6 +333,25 @@                  "node": ">= 8"              }          }, +        "node_modules/@playwright/test": { +            "version": "1.31.2", +            "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.2.tgz", +            "integrity": "sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==", +            "dev": true, +            "dependencies": { +                "@types/node": "*", +                "playwright-core": "1.31.2" +            }, +            "bin": { +                "playwright": "cli.js" +            }, +            "engines": { +                "node": ">=14" +            }, +            "optionalDependencies": { +                "fsevents": "2.3.2" +            } +        },          "node_modules/@pnpm/network.ca-file": {              "version": "1.0.1",              "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.1.tgz", @@ -3474,6 +3494,20 @@              "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",              "dev": true          }, +        "node_modules/fsevents": { +            "version": "2.3.2", +            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", +            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", +            "dev": true, +            "hasInstallScript": true, +            "optional": true, +            "os": [ +                "darwin" +            ], +            "engines": { +                "node": "^8.16.0 || ^10.6.0 || >=11.0.0" +            } +        },          "node_modules/function-bind": {              "version": "1.1.1",              "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6409,6 +6443,18 @@              "integrity": "sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==",              "dev": true          }, +        "node_modules/playwright-core": { +            "version": "1.31.2", +            "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.2.tgz", +            "integrity": "sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==", +            "dev": true, +            "bin": { +                "playwright": "cli.js" +            }, +            "engines": { +                "node": ">=14" +            } +        },          "node_modules/postcss": {              "version": "8.4.18",              "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", @@ -9389,6 +9435,17 @@                  "fastq": "^1.6.0"              }          }, +        "@playwright/test": { +            "version": "1.31.2", +            "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.2.tgz", +            "integrity": "sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==", +            "dev": true, +            "requires": { +                "@types/node": "*", +                "fsevents": "2.3.2", +                "playwright-core": "1.31.2" +            } +        },          "@pnpm/network.ca-file": {              "version": "1.0.1",              "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.1.tgz", @@ -11834,6 +11891,13 @@              "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",              "dev": true          }, +        "fsevents": { +            "version": "2.3.2", +            "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", +            "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", +            "dev": true, +            "optional": true +        },          "function-bind": {              "version": "1.1.1",              "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -14049,6 +14113,12 @@              "integrity": "sha512-mMMOwSKrmyl+Y12Ri2xhH1lbzQxwwpuru9VjyJpgFIH4asSj88F2csdMwN6+M5g1Ll4rmsYghHLQJw81tgZ7LQ==",              "dev": true          }, +        "playwright-core": { +            "version": "1.31.2", +            "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.2.tgz", +            "integrity": "sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==", +            "dev": true +        },          "postcss": {              "version": "8.4.18",              "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.18.tgz", diff --git a/package.json b/package.json index 6ec3eecf..2de7bcef 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@          "sourceDir": "ext"      },      "devDependencies": { +        "@playwright/test": "^1.31.2",          "ajv": "^8.11.0",          "browserify": "^17.0.0",          "css": "^3.0.0", diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 00000000..0f15ff59 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023  Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ +// @ts-check +const {defineConfig, devices} = require('@playwright/test'); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ +    testDir: './test/playwright', +    snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}', +    /* Maximum time one test can run for. */ +    timeout: 60 * 60 * 1000, +    expect: { +        /** +         * Maximum time expect() should wait for the condition to be met. +         * For example in `await expect(locator).toHaveText();` +         */ +        timeout: 5000 +    }, +    /* Run tests in files in parallel */ +    fullyParallel: true, +    /* Fail the build on CI if you accidentally left test.only in the source code. */ +    forbidOnly: !!process.env.CI, +    /* Retry on CI only */ +    retries: process.env.CI ? 2 : 0, +    /* Opt out of parallel tests on CI. */ +    workers: process.env.CI ? 1 : undefined, +    /* Reporter to use. See https://playwright.dev/docs/test-reporters */ +    reporter: 'html', +    /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ +    use: { +        /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ +        actionTimeout: 60 * 1000, +        /* Base URL to use in actions like `await page.goto('/')`. */ +        // baseURL: 'http://localhost:3000', + +        /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ +        trace: 'on-first-retry' +    }, + +    /* Configure projects for major browsers */ +    projects: [ +        { +            name: 'chromium', +            use: {...devices['Desktop Chrome']} +        } + +        // { +        //   name: 'firefox', +        //   use: { ...devices['Desktop Firefox'] }, +        // }, + +        // { +        //   name: 'webkit', +        //   use: { ...devices['Desktop Safari'] }, +        // }, + +        /* Test against mobile viewports. */ +        // { +        //   name: 'Mobile Chrome', +        //   use: { ...devices['Pixel 5'] }, +        // }, +        // { +        //   name: 'Mobile Safari', +        //   use: { ...devices['iPhone 12'] }, +        // }, + +        /* Test against branded browsers. */ +        // { +        //   name: 'Microsoft Edge', +        //   use: { channel: 'msedge' }, +        // }, +        // { +        //   name: 'Google Chrome', +        //   use: { channel: 'chrome' }, +        // }, +    ] + +    /* Folder for test artifacts such as screenshots, videos, traces, etc. */ +    // outputDir: 'test-results/', + +    /* Run your local dev server before starting the tests */ +    // webServer: { +    //   command: 'npm run start', +    //   port: 3000, +    // }, +}); + diff --git a/test/data/html/test-document2.html b/test/data/html/test-document2.html index fb3adb59..399c3bd8 100644 --- a/test/data/html/test-document2.html +++ b/test/data/html/test-document2.html @@ -1,34 +1,40 @@  <!DOCTYPE html>  <html> -    <head> -        <meta charset="UTF-8"> -        <meta name="viewport" content="width=device-width,initial-scale=1"> -        <title>Yomichan Manual Tests</title> -        <link rel="icon" type="image/gif" href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw=="> -        <link rel="stylesheet" href="test-stylesheet.css"> -        <script src="test-document2-script.js"></script> -    </head> -    <style id="container-styles"> -.container { -    width: 100%; -    height: 200px; -    border: 1px solid #d8d8d8; -    position: relative; -    box-sizing: border-box; -} -.container-inner { -    background-color: #f8f8f8; -    padding: 0.5em; -    position: absolute; -    left: 0; -    top: 0; -    bottom: 0; -    right: 0; -} -.danger { -    color: #c83c28; -} -    </style> + +<head> +    <meta charset="UTF-8"> +    <meta name="viewport" content="width=device-width,initial-scale=1"> +    <title>Yomichan Manual Tests</title> +    <link rel="icon" type="image/gif" +        href="data:image/gif;base64,R0lGODlhEAAQAKEBAAAAAP///////////yH5BAEKAAIALAAAAAAQABAAAAImFI6Zpt0B4YkS0TCpq07xbmEgcGVRUpLaI46ZG7ppalY0jDCwUAAAOw=="> +    <link rel="stylesheet" href="test-stylesheet.css"> +    <script src="test-document2-script.js"></script> +</head> +<style id="container-styles"> +    .container { +        width: 100%; +        height: 200px; +        border: 1px solid #d8d8d8; +        position: relative; +        box-sizing: border-box; +    } + +    .container-inner { +        background-color: #f8f8f8; +        padding: 0.5em; +        position: absolute; +        left: 0; +        top: 0; +        bottom: 0; +        right: 0; +    } + +    .danger { +        color: #c83c28; +    } + +</style> +  <body>      <h1>Yomichan Manual Tests</h1> @@ -36,56 +42,62 @@      <y-test>          <y-description>Standard content.</y-description> -        <div class="fullscreen-element container"><div class="container-inner"> -            <div> -                ありがとう -            </div> -            <div> -                <a href="#" class="fullscreen-link">Toggle fullscreen</a> +        <div class="fullscreen-element container hovertarget"> +            <div class="container-inner"> +                <div> +                    ありがとう +                </div> +                <div> +                    <a href="#" class="fullscreen-link">Toggle fullscreen</a> +                </div>              </div> -        </div></div> +        </div>      </y-test>      <y-test data-shadow-mode="open">          <y-description>Content inside of an open shadow DOM.</y-description> -        <div class="template-content-container"></div> +        <div class="template-content-container hovertarget"></div>          <template>              <link rel="stylesheet" href="test-stylesheet.css"> -            <div class="fullscreen-element container"><div class="container-inner"> -                <div> -                    ありがとう -                </div> -                <div> -                    <a href="#" class="fullscreen-link">Toggle fullscreen</a> +            <div class="fullscreen-element container"> +                <div class="container-inner"> +                    <div> +                        ありがとう +                    </div> +                    <div> +                        <a href="#" class="fullscreen-link">Toggle fullscreen</a> +                    </div>                  </div> -            </div></div> +            </div>          </template>      </y-test>      <y-test data-shadow-mode="closed">          <y-description>Content inside of a closed shadow DOM.</y-description> -        <div class="template-content-container"></div> +        <div class="template-content-container hovertarget"></div>          <template>              <link rel="stylesheet" href="test-stylesheet.css"> -            <div class="fullscreen-element container"><div class="container-inner"> -                <div> -                    ありがとう +            <div class="fullscreen-element container"> +                <div class="container-inner"> +                    <div> +                        ありがとう +                    </div> +                    <div> +                        <a href="#" class="fullscreen-link">Toggle fullscreen</a> +                    </div>                  </div> -                <div> -                    <a href="#" class="fullscreen-link">Toggle fullscreen</a> -                </div> -            </div></div> +            </div>          </template>      </y-test>      <y-test>          <y-description><iframe> element.</y-description> -        <iframe src="test-document2-frame1.html" allowfullscreen="true" class="container"></iframe> +        <iframe src="test-document2-frame1.html" allowfullscreen="true" class="container hovertarget"></iframe>      </y-test>      <y-test data-shadow-mode="open">          <y-description><iframe> element inside of an open shadow DOM.</y-description> -        <div class="template-content-container"></div> +        <div class="template-content-container hovertarget"></div>          <template>              <iframe src="test-document2-frame1.html" allowfullscreen="true" class="container"></iframe>          </template> @@ -93,7 +105,7 @@      <y-test data-shadow-mode="closed">          <y-description><iframe> element inside of a closed shadow DOM.</y-description> -        <div class="template-content-container"></div> +        <div class="template-content-container hovertarget"></div>          <template>              <iframe src="test-document2-frame1.html" allowfullscreen="true" class="container"></iframe>          </template> @@ -101,22 +113,26 @@      <y-test>          <y-description><iframe> element with data URL.</y-description> -        <iframe id="iframe-with-data-url" src="data:text/html;base64,PCFET0NUWVBFIGh0bWw+DQo8aHRtbD4NCiAgICA8aGVhZD4NCiAgICAgICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPg0KICAgICAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLGluaXRpYWwtc2NhbGU9MSI+DQogICAgICAgIDx0aXRsZT5Zb21pY2hhbiBUZXN0czwvdGl0bGU+DQogICAgICAgIDxzY3JpcHQ+DQogZnVuY3Rpb24gcmVxdWVzdEZ1bGxzY3JlZW4oZWxlbWVudCkgew0KICAgIGlmIChlbGVtZW50LnJlcXVlc3RGdWxsc2NyZWVuKSB7DQogICAgICAgIGVsZW1lbnQucmVxdWVzdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2UgaWYgKGVsZW1lbnQubW96UmVxdWVzdEZ1bGxTY3JlZW4pIHsNCiAgICAgICAgZWxlbWVudC5tb3pSZXF1ZXN0RnVsbFNjcmVlbigpOw0KICAgIH0gZWxzZSBpZiAoZWxlbWVudC53ZWJraXRSZXF1ZXN0RnVsbHNjcmVlbikgew0KICAgICAgICBlbGVtZW50LndlYmtpdFJlcXVlc3RGdWxsc2NyZWVuKCk7DQogICAgfSBlbHNlIGlmIChlbGVtZW50Lm1zUmVxdWVzdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZWxlbWVudC5tc1JlcXVlc3RGdWxsc2NyZWVuKCk7DQogICAgfQ0KfQ0KDQpmdW5jdGlvbiBleGl0RnVsbHNjcmVlbigpIHsNCiAgICBpZiAoZG9jdW1lbnQuZXhpdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQuZXhpdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2UgaWYgKGRvY3VtZW50Lm1vekNhbmNlbEZ1bGxTY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQubW96Q2FuY2VsRnVsbFNjcmVlbigpOw0KICAgIH0gZWxzZSBpZiAoZG9jdW1lbnQud2Via2l0RXhpdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQud2Via2l0RXhpdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2UgaWYgKGRvY3VtZW50Lm1zRXhpdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQubXNFeGl0RnVsbHNjcmVlbigpOw0KICAgIH0NCn0NCg0KZnVuY3Rpb24gZ2V0RnVsbHNjcmVlbkVsZW1lbnQoKSB7DQogICAgcmV0dXJuICgNCiAgICAgICAgZG9jdW1lbnQuZnVsbHNjcmVlbkVsZW1lbnQgfHwNCiAgICAgICAgZG9jdW1lbnQubXNGdWxsc2NyZWVuRWxlbWVudCB8fA0KICAgICAgICBkb2N1bWVudC5tb3pGdWxsU2NyZWVuRWxlbWVudCB8fA0KICAgICAgICBkb2N1bWVudC53ZWJraXRGdWxsc2NyZWVuRWxlbWVudCB8fA0KICAgICAgICBudWxsDQogICAgKTsNCn0NCg0KZnVuY3Rpb24gdG9nZ2xlRnVsbHNjcmVlbihlbGVtZW50KSB7DQogICAgaWYgKGdldEZ1bGxzY3JlZW5FbGVtZW50KCkpIHsNCiAgICAgICAgZXhpdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2Ugew0KICAgICAgICByZXF1ZXN0RnVsbHNjcmVlbihlbGVtZW50KTsNCiAgICB9DQp9DQoNCmZ1bmN0aW9uIHNldHVwKGNvbnRhaW5lciwgZnVsbHNjcmVlbkVsZW1lbnQ9bnVsbCkgew0KICAgIGNvbnN0IGZ1bGxzY3JlZW5MaW5rID0gY29udGFpbmVyLnF1ZXJ5U2VsZWN0b3IoJy5mdWxsc2NyZWVuLWxpbmsnKTsNCiAgICBpZiAoZnVsbHNjcmVlbkxpbmsgIT09IG51bGwpIHsNCiAgICAgICAgaWYgKGZ1bGxzY3JlZW5FbGVtZW50ID09PSBudWxsKSB7DQogICAgICAgICAgICBmdWxsc2NyZWVuRWxlbWVudCA9IGNvbnRhaW5lci5xdWVyeVNlbGVjdG9yKCcuZnVsbHNjcmVlbi1lbGVtZW50Jyk7DQogICAgICAgIH0NCiAgICAgICAgZnVsbHNjcmVlbkxpbmsuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoZSkgPT4gew0KICAgICAgICAgICAgdG9nZ2xlRnVsbHNjcmVlbihmdWxsc2NyZWVuRWxlbWVudCk7DQogICAgICAgICAgICBlLnByZXZlbnREZWZhdWx0KCk7DQogICAgICAgICAgICByZXR1cm4gZmFsc2U7DQogICAgICAgIH0sIGZhbHNlKTsNCiAgICB9DQoNCiAgICBjb25zdCB0ZW1wbGF0ZSA9IGNvbnRhaW5lci5xdWVyeVNlbGVjdG9yKCd0ZW1wbGF0ZScpOw0KICAgIGNvbnN0IHRlbXBsYXRlQ29udGVudENvbnRhaW5lciA9IGNvbnRhaW5lci5xdWVyeVNlbGVjdG9yKCcudGVtcGxhdGUtY29udGVudC1jb250YWluZXInKTsNCiAgICBpZiAodGVtcGxhdGUgIT09IG51bGwgJiYgdGVtcGxhdGVDb250ZW50Q29udGFpbmVyICE9PSBudWxsKSB7DQogICAgICAgIGNvbnN0IG1vZGUgPSBjb250YWluZXIuZGF0YXNldC5zaGFkb3dNb2RlOw0KICAgICAgICBjb25zdCBzaGFkb3cgPSB0ZW1wbGF0ZUNvbnRlbnRDb250YWluZXIuYXR0YWNoU2hhZG93KHttb2RlfSk7DQoNCiAgICAgICAgY29uc3QgY29udGFpbmVyU3R5bGVzID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcignI2NvbnRhaW5lci1zdHlsZXMnKTsNCiAgICAgICAgc2hhZG93LmFwcGVuZENoaWxkKGNvbnRhaW5lclN0eWxlcy5jbG9uZU5vZGUodHJ1ZSkpOw0KDQogICAgICAgIGNvbnN0IGNvbnRlbnQgPSBkb2N1bWVudC5pbXBvcnROb2RlKHRlbXBsYXRlLmNvbnRlbnQsIHRydWUpOw0KICAgICAgICBzZXR1cChjb250ZW50KTsNCiAgICAgICAgc2hhZG93LmFwcGVuZENoaWxkKGNvbnRlbnQpOw0KICAgIH0NCn0NCiAgICAgICAgPC9zY3JpcHQ+DQogICAgICAgIDxzdHlsZT4NCmJvZHkgew0KICAgIGZvbnQtZmFtaWx5OiAiSGVsdmV0aWNhIE5ldWUiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmOw0KICAgIGZvbnQtc2l6ZTogMTRweDsNCiAgICBwYWRkaW5nOiAwOw0KICAgIG1hcmdpbjogMDsNCiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjhmOGY4Ow0KfQ0KYSwgYTp2aXNpdGVkIHsNCiAgICBjb2xvcjogIzEwODBjMDsNCiAgICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsNCn0NCi5jb250ZW50IHsNCiAgICBwb3NpdGlvbjogYWJzb2x1dGU7DQogICAgbGVmdDogMDsNCiAgICB0b3A6IDA7DQogICAgcmlnaHQ6IDA7DQogICAgYm90dG9tOiAwOw0KICAgIHBhZGRpbmc6IDAuNWVtOw0KICAgIGJhY2tncm91bmQtY29sb3I6ICNmOGY4Zjg7DQp9DQogICAgICAgIDwvc3R5bGU+DQogICAgPC9oZWFkPg0KPGJvZHk+PGRpdiBjbGFzcz0iY29udGVudCI+DQo8ZGl2Pg0KICAgIOOBguOCiuOBjOOBqOOBhg0KPC9kaXY+DQo8ZGl2Pg0KICAgIDxhIGhyZWY9IiMiIGNsYXNzPSJmdWxsc2NyZWVuLWxpbmsiPlRvZ2dsZSBmdWxsc2NyZWVuPC9hPg0KICAgIDxzY3JpcHQ+c2V0dXAoZG9jdW1lbnQuYm9keSwgZG9jdW1lbnQuYm9keSk7PC9zY3JpcHQ+DQo8L2Rpdj4NCjwvZGl2PjwvYm9keT4NCjwvaHRtbD4=" allowfullscreen="true" class="container"></iframe> +        <iframe id="iframe-with-data-url" +            src="data:text/html;base64,PCFET0NUWVBFIGh0bWw+DQo8aHRtbD4NCiAgICA8aGVhZD4NCiAgICAgICAgPG1ldGEgY2hhcnNldD0iVVRGLTgiPg0KICAgICAgICA8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLGluaXRpYWwtc2NhbGU9MSI+DQogICAgICAgIDx0aXRsZT5Zb21pY2hhbiBUZXN0czwvdGl0bGU+DQogICAgICAgIDxzY3JpcHQ+DQogZnVuY3Rpb24gcmVxdWVzdEZ1bGxzY3JlZW4oZWxlbWVudCkgew0KICAgIGlmIChlbGVtZW50LnJlcXVlc3RGdWxsc2NyZWVuKSB7DQogICAgICAgIGVsZW1lbnQucmVxdWVzdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2UgaWYgKGVsZW1lbnQubW96UmVxdWVzdEZ1bGxTY3JlZW4pIHsNCiAgICAgICAgZWxlbWVudC5tb3pSZXF1ZXN0RnVsbFNjcmVlbigpOw0KICAgIH0gZWxzZSBpZiAoZWxlbWVudC53ZWJraXRSZXF1ZXN0RnVsbHNjcmVlbikgew0KICAgICAgICBlbGVtZW50LndlYmtpdFJlcXVlc3RGdWxsc2NyZWVuKCk7DQogICAgfSBlbHNlIGlmIChlbGVtZW50Lm1zUmVxdWVzdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZWxlbWVudC5tc1JlcXVlc3RGdWxsc2NyZWVuKCk7DQogICAgfQ0KfQ0KDQpmdW5jdGlvbiBleGl0RnVsbHNjcmVlbigpIHsNCiAgICBpZiAoZG9jdW1lbnQuZXhpdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQuZXhpdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2UgaWYgKGRvY3VtZW50Lm1vekNhbmNlbEZ1bGxTY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQubW96Q2FuY2VsRnVsbFNjcmVlbigpOw0KICAgIH0gZWxzZSBpZiAoZG9jdW1lbnQud2Via2l0RXhpdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQud2Via2l0RXhpdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2UgaWYgKGRvY3VtZW50Lm1zRXhpdEZ1bGxzY3JlZW4pIHsNCiAgICAgICAgZG9jdW1lbnQubXNFeGl0RnVsbHNjcmVlbigpOw0KICAgIH0NCn0NCg0KZnVuY3Rpb24gZ2V0RnVsbHNjcmVlbkVsZW1lbnQoKSB7DQogICAgcmV0dXJuICgNCiAgICAgICAgZG9jdW1lbnQuZnVsbHNjcmVlbkVsZW1lbnQgfHwNCiAgICAgICAgZG9jdW1lbnQubXNGdWxsc2NyZWVuRWxlbWVudCB8fA0KICAgICAgICBkb2N1bWVudC5tb3pGdWxsU2NyZWVuRWxlbWVudCB8fA0KICAgICAgICBkb2N1bWVudC53ZWJraXRGdWxsc2NyZWVuRWxlbWVudCB8fA0KICAgICAgICBudWxsDQogICAgKTsNCn0NCg0KZnVuY3Rpb24gdG9nZ2xlRnVsbHNjcmVlbihlbGVtZW50KSB7DQogICAgaWYgKGdldEZ1bGxzY3JlZW5FbGVtZW50KCkpIHsNCiAgICAgICAgZXhpdEZ1bGxzY3JlZW4oKTsNCiAgICB9IGVsc2Ugew0KICAgICAgICByZXF1ZXN0RnVsbHNjcmVlbihlbGVtZW50KTsNCiAgICB9DQp9DQoNCmZ1bmN0aW9uIHNldHVwKGNvbnRhaW5lciwgZnVsbHNjcmVlbkVsZW1lbnQ9bnVsbCkgew0KICAgIGNvbnN0IGZ1bGxzY3JlZW5MaW5rID0gY29udGFpbmVyLnF1ZXJ5U2VsZWN0b3IoJy5mdWxsc2NyZWVuLWxpbmsnKTsNCiAgICBpZiAoZnVsbHNjcmVlbkxpbmsgIT09IG51bGwpIHsNCiAgICAgICAgaWYgKGZ1bGxzY3JlZW5FbGVtZW50ID09PSBudWxsKSB7DQogICAgICAgICAgICBmdWxsc2NyZWVuRWxlbWVudCA9IGNvbnRhaW5lci5xdWVyeVNlbGVjdG9yKCcuZnVsbHNjcmVlbi1lbGVtZW50Jyk7DQogICAgICAgIH0NCiAgICAgICAgZnVsbHNjcmVlbkxpbmsuYWRkRXZlbnRMaXN0ZW5lcignY2xpY2snLCAoZSkgPT4gew0KICAgICAgICAgICAgdG9nZ2xlRnVsbHNjcmVlbihmdWxsc2NyZWVuRWxlbWVudCk7DQogICAgICAgICAgICBlLnByZXZlbnREZWZhdWx0KCk7DQogICAgICAgICAgICByZXR1cm4gZmFsc2U7DQogICAgICAgIH0sIGZhbHNlKTsNCiAgICB9DQoNCiAgICBjb25zdCB0ZW1wbGF0ZSA9IGNvbnRhaW5lci5xdWVyeVNlbGVjdG9yKCd0ZW1wbGF0ZScpOw0KICAgIGNvbnN0IHRlbXBsYXRlQ29udGVudENvbnRhaW5lciA9IGNvbnRhaW5lci5xdWVyeVNlbGVjdG9yKCcudGVtcGxhdGUtY29udGVudC1jb250YWluZXInKTsNCiAgICBpZiAodGVtcGxhdGUgIT09IG51bGwgJiYgdGVtcGxhdGVDb250ZW50Q29udGFpbmVyICE9PSBudWxsKSB7DQogICAgICAgIGNvbnN0IG1vZGUgPSBjb250YWluZXIuZGF0YXNldC5zaGFkb3dNb2RlOw0KICAgICAgICBjb25zdCBzaGFkb3cgPSB0ZW1wbGF0ZUNvbnRlbnRDb250YWluZXIuYXR0YWNoU2hhZG93KHttb2RlfSk7DQoNCiAgICAgICAgY29uc3QgY29udGFpbmVyU3R5bGVzID0gZG9jdW1lbnQucXVlcnlTZWxlY3RvcignI2NvbnRhaW5lci1zdHlsZXMnKTsNCiAgICAgICAgc2hhZG93LmFwcGVuZENoaWxkKGNvbnRhaW5lclN0eWxlcy5jbG9uZU5vZGUodHJ1ZSkpOw0KDQogICAgICAgIGNvbnN0IGNvbnRlbnQgPSBkb2N1bWVudC5pbXBvcnROb2RlKHRlbXBsYXRlLmNvbnRlbnQsIHRydWUpOw0KICAgICAgICBzZXR1cChjb250ZW50KTsNCiAgICAgICAgc2hhZG93LmFwcGVuZENoaWxkKGNvbnRlbnQpOw0KICAgIH0NCn0NCiAgICAgICAgPC9zY3JpcHQ+DQogICAgICAgIDxzdHlsZT4NCmJvZHkgew0KICAgIGZvbnQtZmFtaWx5OiAiSGVsdmV0aWNhIE5ldWUiLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmOw0KICAgIGZvbnQtc2l6ZTogMTRweDsNCiAgICBwYWRkaW5nOiAwOw0KICAgIG1hcmdpbjogMDsNCiAgICBiYWNrZ3JvdW5kLWNvbG9yOiAjZjhmOGY4Ow0KfQ0KYSwgYTp2aXNpdGVkIHsNCiAgICBjb2xvcjogIzEwODBjMDsNCiAgICB0ZXh0LWRlY29yYXRpb246IHVuZGVybGluZTsNCn0NCi5jb250ZW50IHsNCiAgICBwb3NpdGlvbjogYWJzb2x1dGU7DQogICAgbGVmdDogMDsNCiAgICB0b3A6IDA7DQogICAgcmlnaHQ6IDA7DQogICAgYm90dG9tOiAwOw0KICAgIHBhZGRpbmc6IDAuNWVtOw0KICAgIGJhY2tncm91bmQtY29sb3I6ICNmOGY4Zjg7DQp9DQogICAgICAgIDwvc3R5bGU+DQogICAgPC9oZWFkPg0KPGJvZHk+PGRpdiBjbGFzcz0iY29udGVudCI+DQo8ZGl2Pg0KICAgIOOBguOCiuOBjOOBqOOBhg0KPC9kaXY+DQo8ZGl2Pg0KICAgIDxhIGhyZWY9IiMiIGNsYXNzPSJmdWxsc2NyZWVuLWxpbmsiPlRvZ2dsZSBmdWxsc2NyZWVuPC9hPg0KICAgIDxzY3JpcHQ+c2V0dXAoZG9jdW1lbnQuYm9keSwgZG9jdW1lbnQuYm9keSk7PC9zY3JpcHQ+DQo8L2Rpdj4NCjwvZGl2PjwvYm9keT4NCjwvaHRtbD4=" +            allowfullscreen="true" class="container hovertarget"></iframe>      </y-test>      <y-test>          <y-description><iframe> element with blob URL.</y-description> -        <iframe id="iframe-with-blob-url" allowfullscreen="true" class="container"></iframe> +        <iframe id="iframe-with-blob-url" allowfullscreen="true" class="container hovertarget"></iframe>      </y-test>      <y-test>          <y-description><iframe> element with srcdoc.</y-description> -        <iframe allowfullscreen="true" class="iframe-with-srcdoc container"></iframe> +        <iframe allowfullscreen="true" class="iframe-with-srcdoc container hovertarget"></iframe>      </y-test>      <y-test> -        <y-description><iframe> element with srcdoc and <code>sandbox="allow-same-origin allow-scripts"</code>.</y-description> -        <iframe allowfullscreen="true" class="iframe-with-srcdoc container" sandbox="allow-same-origin allow-scripts"></iframe> +        <y-description><iframe> element with srcdoc and +            <code>sandbox="allow-same-origin allow-scripts"</code>.</y-description> +        <iframe allowfullscreen="true" class="iframe-with-srcdoc container hovertarget" +            sandbox="allow-same-origin allow-scripts"></iframe>      </y-test>      <y-test> @@ -124,41 +140,39 @@              <iframe> element with srcdoc and <code>sandbox="allow-scripts"</code>.<br>              <span class="danger">This element is expected to not work.</span>          </y-description> -        <iframe allowfullscreen="true" class="iframe-with-srcdoc container" sandbox="allow-scripts"></iframe> +        <iframe allowfullscreen="true" class="iframe-with-srcdoc container hovertarget" +            sandbox="allow-scripts"></iframe>      </y-test>      <y-test>          <y-description>SVG <img>.</y-description> -        <img src="test-document2-frame2.svg" class="container" alt=""> +        <img src="test-document2-frame2.svg" class="container hovertarget" alt="">      </y-test>      <y-test>          <y-description>SVG <object>.</y-description> -        <object data="test-document2-frame2.svg" type="image/svg+xml" class="container"></object> +        <object data="test-document2-frame2.svg" type="image/svg+xml" class="container hovertarget"></object>      </y-test>      <y-test>          <y-description>SVG <embed>.</y-description> -        <embed type="image/svg+xml" src="test-document2-frame2.svg" class="container"> +        <embed type="image/svg+xml" src="test-document2-frame2.svg" class="container hovertarget">      </y-test>      <y-test>          <y-description>SVG <iframe>.</y-description> -        <iframe src="test-document2-frame2.svg" allowfullscreen="true" class="container"></iframe> +        <iframe src="test-document2-frame2.svg" allowfullscreen="true" class="container hovertarget"></iframe>      </y-test>      <y-test>          <y-description>SVG <svg>.</y-description> -        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="container" style="background-color: #f8f8f8;" focusable="false"> -            <text -                x="7" -                y="12" -                style=" +        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="container hovertarget" +            style="background-color: #f8f8f8;" focusable="false"> +            <text x="7" y="12" style="                  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;                  font-size: 14px;                  fill: #000000; -                dominant-baseline: hanging;" -            > +                dominant-baseline: hanging;">                  ありがとう              </text>          </svg> @@ -166,42 +180,43 @@      <script> -(() => { -    function stringToTypedArray(string) { -        const array = new Uint8Array(string.length); -        for (let i = 0; i < string.length; ++i) { -            array[i] = string.charCodeAt(i); -        } -        return array; -    } - -    function dataUrlToContent(dataUrl) { -        const [, type, isBase64, data] = /^data:([^;]*);(base64,)?([\w\W]*)$/.exec(dataUrl); -        const content = ( -            isBase64 ? -            new TextDecoder().decode(stringToTypedArray(atob(data))) : -            data -        ); -        return {content, type}; -    } - -    function dataUrlToBlob(dataUrl) { -        const {content, type} = dataUrlToContent(dataUrl); -        return new Blob([content], {type}); -    } - -    for (const element of document.querySelectorAll('y-test')) { -        setup(element); -    } - -    const iframeWithDataUrl = document.querySelector('#iframe-with-data-url'); -    const iframeWithBlobUrl = document.querySelector('#iframe-with-blob-url'); -    iframeWithBlobUrl.src = URL.createObjectURL(dataUrlToBlob(iframeWithDataUrl.src)); -    for (const iframeWithSrcdoc of document.querySelectorAll('.iframe-with-srcdoc')) { -        iframeWithSrcdoc.srcdoc = dataUrlToContent(iframeWithDataUrl.src).content; -    } -})(); +        (() => { +            function stringToTypedArray(string) { +                const array = new Uint8Array(string.length); +                for (let i = 0; i < string.length; ++i) { +                    array[i] = string.charCodeAt(i); +                } +                return array; +            } + +            function dataUrlToContent(dataUrl) { +                const [, type, isBase64, data] = /^data:([^;]*);(base64,)?([\w\W]*)$/.exec(dataUrl); +                const content = ( +                    isBase64 ? +                        new TextDecoder().decode(stringToTypedArray(atob(data))) : +                        data +                ); +                return { content, type }; +            } + +            function dataUrlToBlob(dataUrl) { +                const { content, type } = dataUrlToContent(dataUrl); +                return new Blob([content], { type }); +            } + +            for (const element of document.querySelectorAll('y-test')) { +                setup(element); +            } + +            const iframeWithDataUrl = document.querySelector('#iframe-with-data-url'); +            const iframeWithBlobUrl = document.querySelector('#iframe-with-blob-url'); +            iframeWithBlobUrl.src = URL.createObjectURL(dataUrlToBlob(iframeWithDataUrl.src)); +            for (const iframeWithSrcdoc of document.querySelectorAll('.iframe-with-srcdoc')) { +                iframeWithSrcdoc.srcdoc = dataUrlToContent(iframeWithDataUrl.src).content; +            } +        })();      </script>  </body> +  </html> diff --git a/test/playwright/visual.spec.js b/test/playwright/visual.spec.js new file mode 100644 index 00000000..acb12e97 --- /dev/null +++ b/test/playwright/visual.spec.js @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023  Yomitan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program.  If not, see <https://www.gnu.org/licenses/>. + */ + +const path = require('path'); +const {test: base, chromium} = require('@playwright/test'); +const root = path.join(__dirname, '..', '..'); + +export const test = base.extend({ +    context: async ({ }, use) => { +        const pathToExtension = path.join(root, 'ext'); +        const context = await chromium.launchPersistentContext('', { +            // headless: false, +            args: [ +                '--headless=new', +                `--disable-extensions-except=${pathToExtension}`, +                `--load-extension=${pathToExtension}` +            ] +        }); +        await use(context); +        await context.close(); +    }, +    extensionId: async ({context}, use) => { +        let [background] = context.serviceWorkers(); +        if (!background) { +            background = await context.waitForEvent('serviceworker'); +        } + +        const extensionId = background.url().split('/')[2]; +        await use(extensionId); +    } +}); +const expect = test.expect; + +test('visual', async ({context, page, extensionId}) => { +    // wait for the on-install welcome.html tab to load, which becomes the foreground tab +    const welcome = await context.waitForEvent('page'); +    welcome.close(); // close the welcome tab so our main tab becomes the foreground tab -- otherwise, the screenshot can hang + +    // open settings +    await page.goto(`chrome-extension://${extensionId}/settings.html`); + +    await expect(page.locator('id=dictionaries')).toBeVisible(); + +    // get the locator for the disk usage indicator so we can later mask it out of the screenshot +    const storage_locator = page.locator('.storage-use-finite >> xpath=..'); + +    // take a simple screenshot of the settings page +    await expect.soft(page).toHaveScreenshot('settings-fresh.png', {mask: [storage_locator]}); + +    // load in jmdict_english.zip +    await page.locator('input[id="dictionary-import-file-input"]').setInputFiles(path.join(root, 'dictionaries/jmdict_english.zip')); +    await expect(page.locator('id=dictionaries')).toHaveText('Dictionaries (1 installed, 1 enabled)', {timeout: 5 * 60 * 1000}); + +    // take a screenshot of the settings page with jmdict loaded +    await expect.soft(page).toHaveScreenshot('settings-jmdict-loaded.png', {mask: [storage_locator]}); + +    const screenshot = async (doc_number, test_number, el, offset) => { +        const test_name = 'doc' + doc_number + '-test' + test_number; + +        const box = await el.boundingBox(); + +        // find the popup frame if it exists +        let popup_frame = page.frames().find((f) => f.url().includes('popup.html')); + +        // otherwise prepare for it to be attached +        let frame_attached; +        if (popup_frame === undefined) { +            frame_attached = page.waitForEvent('frameattached'); +        } +        await page.mouse.move(box.x + offset.x, box.y + offset.y, {steps: 10}); // hover over the test +        if (popup_frame === undefined) { +            popup_frame = await frame_attached; // wait for popup to be attached +        } +        try { +            await (await popup_frame.frameElement()).waitForElementState('visible', {timeout: 500});  // some tests don't have a popup, so don't fail if it's not there; TODO: check if the popup is expected to be there +        } catch (error) { +            console.log(test_name + ' has no popup'); +        } + +        await page.bringToFront(); // bring the page to the foreground so the screenshot doesn't hang; for some reason the frames result in page being in the background +        await expect.soft(page).toHaveScreenshot(test_name + '.png'); + +        await page.mouse.click(0, 0); // click away so popup disappears +        await (await popup_frame.frameElement()).waitForElementState('hidden'); // wait for popup to disappear +    }; + +    // Load test-document1.html +    await page.goto('file://' + path.join(root, 'test/data/html/test-document1.html')); +    await page.setViewportSize({width: 1000, height: 1800}); +    await page.keyboard.down('Shift'); +    let i = 1; +    for (const el of await page.locator('div > *:nth-child(1)').elementHandles()) { +        await screenshot(1, i, el, {x: 6, y: 6}); +        i++; +    } + +    // Load test-document2.html +    await page.goto('file://' + path.join(root, 'test/data/html/test-document2.html')); +    await page.setViewportSize({width: 1000, height: 4500}); +    await page.keyboard.down('Shift'); +    i = 1; +    for (const el of await page.locator('.hovertarget').elementHandles()) { +        await screenshot(2, i, el, {x: 15, y: 15}); +        i++; +    } +}); |