From 5c45643772e6669dc9945c038ebf06c634b19b25 Mon Sep 17 00:00:00 2001 From: jbukl Date: Tue, 31 Oct 2023 22:06:14 -0400 Subject: search, anki playwright tests prep tests for cicd reorganize exports in playwright utils --- .eslintrc.json | 2 + .vscode/settings.json | 5 +- dev/data/manifest-variants.json | 21 +++++++ playwright.config.js | 12 +++- test/playwright/global.setup.js | 32 +++++++++++ test/playwright/global.teardown.js | 28 +++++++++ test/playwright/integration.spec.js | 91 ++++++++++++++++++++++++++++++ test/playwright/playwright-util.js | 109 ++++++++++++++++++++++++++++++++++++ test/playwright/visual.spec.js | 38 +++---------- 9 files changed, 307 insertions(+), 31 deletions(-) create mode 100644 test/playwright/global.setup.js create mode 100644 test/playwright/global.teardown.js create mode 100644 test/playwright/integration.spec.js create mode 100644 test/playwright/playwright-util.js 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/.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 c9e7cd03..73b2dc13 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", diff --git a/playwright.config.js b/playwright.config.js index 0f15ff59..11d79e72 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -61,9 +61,19 @@ 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 . + */ + +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 . + */ + +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 . + */ + +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 . + */ + +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 -- cgit v1.2.3 From 193437fed0f4ee756fd62902947afce54f54dfe6 Mon Sep 17 00:00:00 2001 From: jbukl Date: Wed, 1 Nov 2023 23:10:50 -0400 Subject: add playwright sw env variable in action workflow --- .github/workflows/playwright.yml | 2 ++ 1 file changed, 2 insertions(+) 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 -- cgit v1.2.3