summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDarius Jahandarie <djahandarie@gmail.com>2023-03-23 21:58:27 +0900
committerGitHub <noreply@github.com>2023-03-23 21:58:27 +0900
commitdaea55996662fe7907f14817ee6271fa3862479a (patch)
treebcd3651e64dbf460e675db0737fa559662720f57
parentbb9c6ec4118c1ed8fd67ff4a760c352675086f6a (diff)
parent95b2bb25d4175ff676c5a3d4e5ff0ef214f7b306 (diff)
Merge pull request #101 from themoeway/visual-comp
Add visual diffing in CI
-rw-r--r--.eslintrc.json347
-rw-r--r--.github/workflows/playwright.yml109
-rw-r--r--.gitignore5
-rw-r--r--package-lock.json74
-rw-r--r--package.json1
-rw-r--r--playwright.config.js109
-rw-r--r--test/data/html/test-document2.html229
-rw-r--r--test/playwright/visual.spec.js120
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)
diff --git a/.gitignore b/.gitignore
index ab07d570..d4e5da07 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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>&lt;iframe&gt; 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>&lt;iframe&gt; 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>&lt;iframe&gt; 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>&lt;iframe&gt; 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>&lt;iframe&gt; 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>&lt;iframe&gt; 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>&lt;iframe&gt; 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>&lt;iframe&gt; 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 @@
&lt;iframe&gt; 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 &lt;img&gt;.</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 &lt;object&gt;.</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 &lt;embed&gt;.</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 &lt;iframe&gt;.</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 &lt;svg&gt;.</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++;
+ }
+});