Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

search.html clipboard monitor, Anki add note Playwright tests #299

Merged
merged 2 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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",
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