/*
* Copyright (C) 2023-2025 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 .
*/
import {readFileSync} from 'fs';
import path from 'path';
import {pathToFileURL} from 'url';
import {createDictionaryArchiveData} from '../../dev/dictionary-archive-util.js';
import {expect, root, test} from './playwright-util.js';
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');
await welcome.close(); // Close the welcome tab so our main tab becomes the foreground tab -- otherwise, the screenshot can hang
});
test('welcome', async ({page, extensionId}) => {
// Open welcome page
console.log('Open welcome page');
await page.goto(`chrome-extension://${extensionId}/welcome.html`);
await expect(page.getByText('Welcome to Yomitan!')).toBeVisible();
// Take a screenshot of the welcome page
await expect.soft(page).toHaveScreenshot('welcome-page.png');
});
test.describe('settings', () => {
test('local load of jmdict_english', async ({page, extensionId}) => {
// Open settings
console.log('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
console.log('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]});
});
test('remote load and delete of jmdict_swedish', async ({page, extensionId}) => {
// Open settings
console.log('Open settings');
await page.goto(`chrome-extension://${extensionId}/settings.html`);
// Enable advanced settings
// Wait for the advanced settings to be visible
await page.locator('input#advanced-checkbox').evaluate((/** @type {HTMLInputElement} */ element) => element.click());
// Import jmdict_swedish.zip from a URL
console.log('Load in jmdict_swedish.zip');
await page.locator('.settings-item[data-modal-action="show,dictionaries"]').click();
await page.locator('button[id="dictionary-import-button"]').click();
await page.locator('textarea[id="dictionary-import-url-text"]').fill('https://github.com/yomidevs/yomitan/raw/dictionaries/jmdict_swedish.zip');
await page.locator('button[id="dictionary-import-url-button"]').click();
await expect(page.locator('id=dictionaries')).toHaveText('Dictionaries (1 installed, 1 enabled)', {timeout: 5 * 60 * 1000});
// Delete the jmdict_swedish dictionary
await page.locator('button.dictionary-menu-button').nth(0).click();
await page.locator('button.popup-menu-item[data-menu-action="delete"]').click();
await page.locator('#dictionary-confirm-delete-button').click();
await page.locator('#dictionaries-modal button[data-modal-action="hide"]').getByText('Close').click();
await expect(page.locator('id=dictionaries')).toHaveText('Dictionaries (0 installed, 0 enabled)', {timeout: 5 * 60 * 1000});
// Get page height by getting the footer and adding height and y position as other methods of calculation don't work for some reason
const footer = /** @type {import('@playwright/test').ElementHandle} */ (await page.locator('.footer-padding').elementHandle());
expect(footer).not.toBe(null);
const boundingBox = /** @type {NonNullable['boundingBox']>>>} */ (await footer.boundingBox());
expect(boundingBox).not.toBe(null);
const pageHeight = Math.ceil(boundingBox.y + boundingBox.height);
await page.setViewportSize({width: 1280, height: pageHeight});
// Wait for any animations or changes to complete
console.log('Waiting for animations to complete');
await page.waitForTimeout(500);
// 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 full page screenshot of the settings page with advanced settings enabled
await expect.soft(page).toHaveScreenshot('settings-fresh-full-advanced.png', {
fullPage: true,
mask: [storage_locator],
});
});
});
test.describe('popup', () => {
test.beforeEach(async ({page, extensionId}) => {
// Open settings
console.log('Open settings');
await page.goto(`chrome-extension://${extensionId}/settings.html`);
await expect(page.locator('id=dictionaries')).toBeVisible();
// Load in test dictionary
const dictionary = await createDictionaryArchiveData(path.join(root, 'test/data/dictionaries/valid-dictionary1'), 'valid-dictionary1');
await page.locator('input[id="dictionary-import-file-input"]').setInputFiles({
name: 'valid-dictionary1.zip',
mimeType: 'application/x-zip',
buffer: Buffer.from(dictionary),
});
await expect(page.locator('id=dictionaries')).toHaveText('Dictionaries (1 installed, 1 enabled)', {timeout: 1 * 60 * 1000});
console.log('Open popup-tests.html');
await page.goto(pathToFileURL(popupTestsPath).toString());
await page.setViewportSize({width: 700, height: 500});
await expect(page.locator('id=footer')).toBeVisible();
await page.keyboard.down('Shift');
});
const popupTestsPath = path.join(root, 'test/data/html/popup-tests.html');
const numberOfTests = (readFileSync(popupTestsPath, 'utf8').match(/hovertarget/g) || []).length;
for (let i = 1; i <= numberOfTests; i++) {
test(`test${i}`, async ({page}) => {
const frame_attached = page.waitForEvent('frameattached', {timeout: 10000});
const test_name = 'doc2-test' + i;
console.log(test_name);
// Find the test element
const hovertarget_locator = page.locator('.hovertarget').nth(i - 1);
const testcase_locator = hovertarget_locator.locator('..');
await testcase_locator.scrollIntoViewIfNeeded();
const box = (await hovertarget_locator.boundingBox()) || {x: 0, y: 0, width: 0, height: 0};
const expectedState = (await testcase_locator.getAttribute('data-expected-result')) === 'failure' ? 'hidden' : 'visible';
try {
await page.mouse.move(box.x - 5, box.y - 5); // Hover near the test
await page.mouse.move(box.x + 15, box.y + 15, {steps: 10}); // Hover over the test
if (expectedState === 'visible') {
const popup_frame = await frame_attached;
await (await /** @type {import('@playwright/test').Frame} */ (popup_frame).frameElement())
.waitForElementState(expectedState, {timeout: 1000});
} else {
expect(
await Promise.race([
frame_attached,
new Promise((resolve) => {
setTimeout(() => resolve('timeout'), 1000);
}),
]),
).toStrictEqual('timeout');
}
} catch (error) {
console.warn(test_name, 'unexpected popup state', error);
}
console.log(test_name, 'taking screenshot');
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');
});
}
});