Skip to content

Commit

Permalink
Merge pull request #299 from jbukl/pw-search-anki
Browse files Browse the repository at this point in the history
search.html clipboard monitor, Anki add note Playwright tests
  • Loading branch information
djahandarie authored Nov 2, 2023
2 parents 092d1be + 193437f commit caccc2f
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 31 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,8 @@
},
{
"files": [
"integration.spec.js",
"playwright-util.js",
"visual.spec.js"
],
"env": {
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
21 changes: 21 additions & 0 deletions dev/data/manifest-variants.json
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,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",
Expand Down
12 changes: 11 additions & 1 deletion playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
}

// {
Expand Down
32 changes: 32 additions & 0 deletions test/playwright/global.setup.js
Original file line number Diff line number Diff line change
@@ -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'));
});
28 changes: 28 additions & 0 deletions test/playwright/global.teardown.js
Original file line number Diff line number Diff line change
@@ -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);
});
91 changes: 91 additions & 0 deletions test/playwright/integration.spec.js
Original file line number Diff line number Diff line change
@@ -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);
});
109 changes: 109 additions & 0 deletions test/playwright/playwright-util.js
Original file line number Diff line number Diff line change
@@ -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)
};
Loading

0 comments on commit caccc2f

Please sign in to comment.