aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json2
-rw-r--r--.github/workflows/playwright.yml2
-rw-r--r--.vscode/settings.json5
-rw-r--r--dev/data/manifest-variants.json21
-rw-r--r--playwright.config.js12
-rw-r--r--test/playwright/global.setup.js32
-rw-r--r--test/playwright/global.teardown.js28
-rw-r--r--test/playwright/integration.spec.js91
-rw-r--r--test/playwright/playwright-util.js109
-rw-r--r--test/playwright/visual.spec.js38
10 files changed, 309 insertions, 31 deletions
diff --git a/.eslintrc.json b/.eslintrc.json
index 06a2be34..56bbcf09 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -461,6 +461,8 @@
},
{
"files": [
+ "integration.spec.js",
+ "playwright-util.js",
"visual.spec.js"
],
"env": {
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 8d4adca8..be3e2c19 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -66,6 +66,8 @@ jobs:
- name: "[PR] Generate new screenshots & compare against master"
id: playwright
+ env:
+ PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS: 1
run: |
npx playwright test 2>&1 | tee ./playwright-output || true
continue-on-error: true
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d3738141..9318d9f6 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -3,5 +3,8 @@
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
- "eslint.format.enable": true
+ "eslint.format.enable": true,
+ "playwright.env": {
+ "PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS": 1
+ }
}
diff --git a/dev/data/manifest-variants.json b/dev/data/manifest-variants.json
index 139814fd..1eae2112 100644
--- a/dev/data/manifest-variants.json
+++ b/dev/data/manifest-variants.json
@@ -171,6 +171,27 @@
]
},
{
+ "name": "chrome-playwright",
+ "inherit": "chrome-dev",
+ "fileName": "yomitan-chrome-playwright.zip",
+ "modifications": [
+ {
+ "action": "remove",
+ "path": [
+ "optional_permissions"
+ ],
+ "item": "clipboardRead"
+ },
+ {
+ "action": "add",
+ "path": [
+ "permissions"
+ ],
+ "items": ["clipboardRead"]
+ }
+ ]
+ },
+ {
"name": "firefox",
"inherit": "base",
"fileName": "yomitan-firefox.zip",
diff --git a/playwright.config.js b/playwright.config.js
index 0f15ff59..11d79e72 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -62,8 +62,18 @@ module.exports = defineConfig({
/* Configure projects for major browsers */
projects: [
{
+ name: 'playwright setup',
+ testMatch: /global\.setup\.js/,
+ teardown: 'playwright teardown'
+ },
+ {
+ name: 'playwright teardown',
+ testMatch: /global\.teardown\.js/
+ },
+ {
name: 'chromium',
- use: {...devices['Desktop Chrome']}
+ use: {...devices['Desktop Chrome']},
+ dependencies: ['playwright setup']
}
// {
diff --git a/test/playwright/global.setup.js b/test/playwright/global.setup.js
new file mode 100644
index 00000000..442647f8
--- /dev/null
+++ b/test/playwright/global.setup.js
@@ -0,0 +1,32 @@
+/*
+ * 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 {test: setup} = require('@playwright/test');
+const {ManifestUtil} = require('../../dev/manifest-util');
+const {root} = require('./playwright-util');
+const path = require('path');
+const fs = require('fs');
+
+const manifestPath = path.join(root, 'ext/manifest.json');
+const copyManifestPath = path.join(root, 'ext/manifest-old.json');
+
+setup('use test manifest', () => {
+ const manifestUtil = new ManifestUtil();
+ const variant = manifestUtil.getManifest('chrome-playwright');
+ fs.renameSync(manifestPath, copyManifestPath);
+ fs.writeFileSync(manifestPath, ManifestUtil.createManifestString(variant).replace('$YOMITAN_VERSION', '0.0.0.0'));
+}); \ No newline at end of file
diff --git a/test/playwright/global.teardown.js b/test/playwright/global.teardown.js
new file mode 100644
index 00000000..2fb29ebe
--- /dev/null
+++ b/test/playwright/global.teardown.js
@@ -0,0 +1,28 @@
+/*
+ * 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 {test: teardown} = require('@playwright/test');
+const {root} = require('./playwright-util');
+const path = require('path');
+const fs = require('fs');
+
+const manifestPath = path.join(root, 'ext/manifest.json');
+const copyManifestPath = path.join(root, 'ext/manifest-old.json');
+
+teardown('bring back original manifest', () => {
+ fs.renameSync(copyManifestPath, manifestPath);
+}); \ No newline at end of file
diff --git a/test/playwright/integration.spec.js b/test/playwright/integration.spec.js
new file mode 100644
index 00000000..4e4663d6
--- /dev/null
+++ b/test/playwright/integration.spec.js
@@ -0,0 +1,91 @@
+/*
+ * 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,
+ expect,
+ root,
+ mockModelFieldNames,
+ mockModelFieldsToAnkiValues,
+ expectedAddNoteBody,
+ mockAnkiRouteHandler,
+ writeToClipboardFromPage
+} = require('./playwright-util');
+const {createDictionaryArchive} = require('../../dev/util');
+
+test.beforeEach(async ({context}) => {
+ // 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
+});
+
+test('search clipboard', async ({page, extensionId}) => {
+ await page.goto(`chrome-extension://${extensionId}/search.html`);
+ await page.locator('#search-option-clipboard-monitor-container > label').click();
+ await page.waitForTimeout(200); // race
+
+ await writeToClipboardFromPage(page, 'あ');
+ await expect(page.locator('#search-textbox')).toHaveValue('あ');
+});
+
+test('anki add', async ({context, page, extensionId}) => {
+ // mock anki routes
+ let resolve;
+ const addNotePromise = new Promise((res) => {
+ resolve = res;
+ });
+ await context.route(/127.0.0.1:8765\/*/, (route) => {
+ mockAnkiRouteHandler(route);
+ const req = route.request();
+ if (req.url().includes('127.0.0.1:8765') && req.postDataJSON().action === 'addNote') {
+ resolve(req.postDataJSON());
+ }
+ });
+
+ // open settings
+ await page.goto(`chrome-extension://${extensionId}/settings.html`);
+
+ // load in test dictionary
+ const dictionary = createDictionaryArchive(path.join(root, 'test/data/dictionaries/valid-dictionary1'), 'valid-dictionary1');
+ const testDictionarySource = await dictionary.generateAsync({type: 'arraybuffer'});
+ await page.locator('input[id="dictionary-import-file-input"]').setInputFiles({name: 'valid-dictionary1.zip', buffer: Buffer.from(testDictionarySource)});
+ await expect(page.locator('id=dictionaries')).toHaveText('Dictionaries (1 installed, 1 enabled)', {timeout: 5 * 60 * 1000});
+
+ // connect to anki
+ await page.locator('.toggle', {has: page.locator('[data-setting="anki.enable"]')}).click();
+ await expect(page.locator('#anki-error-message')).toHaveText('Connected');
+
+ // prep anki deck
+ await page.locator('[data-modal-action="show,anki-cards"]').click();
+ await page.locator('select.anki-card-deck').selectOption('Mock Deck');
+ await page.locator('select.anki-card-model').selectOption('Mock Model');
+ for (const modelField of mockModelFieldNames) {
+ await page.locator(`[data-setting="anki.terms.fields.${modelField}"]`).fill(mockModelFieldsToAnkiValues[modelField]);
+ }
+ await page.locator('#anki-cards-modal > div > div.modal-footer > button:nth-child(2)').click();
+ await writeToClipboardFromPage(page, '読むの例文');
+
+ // add to anki deck
+ await page.goto(`chrome-extension://${extensionId}/search.html`);
+ await page.waitForTimeout(500); // race
+ await page.locator('#search-textbox').fill('読む');
+ await page.locator('#search-textbox').press('Enter');
+ await page.locator('[data-mode="term-kanji"]').click();
+ const addNoteReqBody = await addNotePromise;
+ expect(addNoteReqBody).toMatchObject(expectedAddNoteBody);
+}); \ No newline at end of file
diff --git a/test/playwright/playwright-util.js b/test/playwright/playwright-util.js
new file mode 100644
index 00000000..e28f16eb
--- /dev/null
+++ b/test/playwright/playwright-util.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/>.
+ */
+
+const path = require('path');
+const {test: base, chromium} = require('@playwright/test');
+
+export 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);
+ }
+});
+export const expect = test.expect;
+
+export const mockModelFieldNames = [
+ 'Word',
+ 'Reading',
+ 'Audio',
+ 'Sentence'
+];
+
+export const mockModelFieldsToAnkiValues = {
+ 'Word': '{expression}',
+ 'Reading': '{furigana-plain}',
+ 'Sentence': '{clipboard-text}',
+ 'Audio': '{audio}'
+};
+
+export const mockAnkiRouteHandler = (route) => {
+ const reqBody = route.request().postDataJSON();
+ const respBody = ankiRouteResponses[reqBody.action];
+ if (!respBody) {
+ return route.abort();
+ }
+ route.fulfill(respBody);
+};
+
+export const writeToClipboardFromPage = async (page, text) => {
+ await page.evaluate(`navigator.clipboard.writeText('${text}')`);
+};
+
+export const expectedAddNoteBody = {
+ 'action': 'addNote',
+ 'params':
+ {
+ 'note': {
+ 'fields': {
+ 'Word': '読む', 'Reading': '読[よ]む', 'Audio': '[sound:mock_audio.mp3]', 'Sentence': '読むの例文'
+ },
+ 'tags': ['yomitan'],
+ 'deckName': 'Mock Deck',
+ 'modelName': 'Mock Model',
+ 'options': {
+ 'allowDuplicate': false, 'duplicateScope': 'collection', 'duplicateScopeOptions': {
+ 'deckName': null, 'checkChildren': false, 'checkAllModels': false
+ }
+ }
+ }
+ }, 'version': 2
+};
+
+const baseAnkiResp = {
+ status: 200,
+ contentType: 'text/json'
+};
+
+const ankiRouteResponses = {
+ 'version': Object.assign({body: JSON.stringify(6)}, baseAnkiResp),
+ 'deckNames': Object.assign({body: JSON.stringify(['Mock Deck'])}, baseAnkiResp),
+ 'modelNames': Object.assign({body: JSON.stringify(['Mock Model'])}, baseAnkiResp),
+ 'modelFieldNames': Object.assign({body: JSON.stringify(mockModelFieldNames)}, baseAnkiResp),
+ 'canAddNotes': Object.assign({body: JSON.stringify([true, true])}, baseAnkiResp),
+ 'storeMediaFile': Object.assign({body: JSON.stringify('mock_audio.mp3')}, baseAnkiResp),
+ 'addNote': Object.assign({body: JSON.stringify(102312488912)}, baseAnkiResp)
+}; \ No newline at end of file
diff --git a/test/playwright/visual.spec.js b/test/playwright/visual.spec.js
index acb12e97..001f329f 100644
--- a/test/playwright/visual.spec.js
+++ b/test/playwright/visual.spec.js
@@ -16,40 +16,20 @@
*/
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;
+const {
+ test,
+ expect,
+ root
+} = require('./playwright-util');
-test('visual', async ({context, page, extensionId}) => {
+test.beforeEach(async ({context}) => {
// 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
+});
+test('visual', async ({page, extensionId}) => {
// open settings
await page.goto(`chrome-extension://${extensionId}/settings.html`);
@@ -117,4 +97,4 @@ test('visual', async ({context, page, extensionId}) => {
await screenshot(2, i, el, {x: 15, y: 15});
i++;
}
-});
+}); \ No newline at end of file