From e6b937cd4bcc84d9524bbf1f071320dfc35bccce Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Tue, 17 Mar 2026 05:29:10 +0900 Subject: [PATCH 1/5] Update: Add testIds. --- .../extension/e2e/data/test-settings.json | 379 ++++++++++++++++++ .../src/components/option/Dialog.tsx | 3 + .../src/components/option/ImportExport.tsx | 3 + packages/extension/src/testIds.ts | 4 + 4 files changed, 389 insertions(+) create mode 100644 packages/extension/e2e/data/test-settings.json diff --git a/packages/extension/e2e/data/test-settings.json b/packages/extension/e2e/data/test-settings.json new file mode 100644 index 00000000..9bf3de70 --- /dev/null +++ b/packages/extension/e2e/data/test-settings.json @@ -0,0 +1,379 @@ +{ + "commands": [ + { + "iconUrl": "https://ujiro99.github.io/selection-command/favicon.ico", + "id": "13b4e831-7fac-4b72-a1ae-c82655b3819e", + "openMode": "popup", + "openModeSecondary": "tab", + "parentFolderId": "RootFolder", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://ujiro99.github.io/selection-command/en/test?k=%s", + "spaceEncoding": "plus", + "title": "テストページ検索" + }, + { + "iconUrl": "https://www.google.com/favicon.ico", + "id": "0cb9dbbc-c0cf-53c6-93e5-016363705216", + "openMode": "popup", + "openModeSecondary": "tab", + "parentFolderId": "222d6489-4eca-48fd-8590-fceb30545bab", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://google.com/search?q=%s", + "spaceEncoding": "plus", + "title": "Google" + }, + { + "iconUrl": "https://www.google.com/favicon.ico", + "id": "26c47b36-c3c8-528c-9ad2-c972dfc6f4df", + "openMode": "popup", + "openModeSecondary": "tab", + "parentFolderId": "222d6489-4eca-48fd-8590-fceb30545bab", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://google.com/search?q=%s&tbm=isch", + "spaceEncoding": "plus", + "title": "Google Image" + }, + { + "iconUrl": "https://www.amazon.co.jp/favicon.ico", + "id": "9d61d45c-36ab-5ebf-ad42-d3f3a42810bf", + "openMode": "tab", + "openModeSecondary": "tab", + "parentFolderId": "222d6489-4eca-48fd-8590-fceb30545bab", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://www.amazon.co.jp/s?k=%s", + "spaceEncoding": "plus", + "title": "Amazon" + }, + { + "iconUrl": "https://s.yimg.jp/c/icon/s/bsc/2.0/favicon.ico", + "id": "2bcb5d3a-15b6-5e3f-b59d-94b0fdc68ea9", + "openMode": "popup", + "openModeSecondary": "tab", + "parentFolderId": "222d6489-4eca-48fd-8590-fceb30545bab", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://search.yahoo.co.jp/search?p=%s", + "spaceEncoding": "plus", + "title": "Yahoo! Japan" + }, + { + "iconUrl": "https://www.youtube.com/s/desktop/f574e7a2/img/favicon_32x32.png", + "id": "2b6fee1e-6500-5421-af79-6fa53ddc25c1", + "openMode": "tab", + "openModeSecondary": "tab", + "parentFolderId": "a3495269-0a4d-4866-a519-bca75ed1c246", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://www.youtube.com/results?search_query=%s", + "spaceEncoding": "plus", + "title": "Youtube" + }, + { + "iconUrl": "https://assets.nflxext.com/ffe/siteui/common/icons/nficon2016.ico", + "id": "fb9cb6ad-76e3-5aa8-82a7-ade233edcec0", + "openMode": "tab", + "openModeSecondary": "tab", + "parentFolderId": "a3495269-0a4d-4866-a519-bca75ed1c246", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://www.netflix.com/search?q=%s", + "spaceEncoding": "plus", + "title": "Netflix" + }, + { + "aiPromptOption": { + "openMode": "popup", + "prompt": "以下について解説してください。\n{{SelectedText}}", + "serviceId": "gemini" + }, + "iconUrl": "https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg", + "id": "1d320825-1e78-5f98-b73c-1bb48412e98c", + "openMode": "aiPrompt", + "parentFolderId": "e4994c63-cfa7-4e49-9dfe-a79e6120a1ae", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "title": "Gemini - 日本語" + }, + { + "aiPromptOption": { + "openMode": "sidePanel", + "prompt": "以下のURLのページを日本語で要約してください。\n{{Url}}", + "serviceId": "gemini" + }, + "iconUrl": "https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg", + "id": "afe67f66-fc8d-555f-9e51-2d1491906faf", + "openMode": "aiPrompt", + "parentFolderId": "e4994c63-cfa7-4e49-9dfe-a79e6120a1ae", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "title": "ページの概要生成" + }, + { + "aiPromptOption": { + "openMode": "sidePanel", + "prompt": "以下のYouTube動画の内容を日本語で要約してください。\n{{Url}}", + "serviceId": "gemini" + }, + "iconUrl": "https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg", + "id": "7afd0cb7-45a4-5943-a00d-b04d12317eb1", + "openMode": "aiPrompt", + "parentFolderId": "e4994c63-cfa7-4e49-9dfe-a79e6120a1ae", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "title": "YouTubeの概要生成" + }, + { + "aiPromptOption": { + "openMode": "sidePanel", + "prompt": "以下のテキストを日本語と英語の間で翻訳してください。\n{{SelectedText}}", + "serviceId": "gemini" + }, + "iconUrl": "https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg", + "id": "a8d027bf-7926-56c4-ad4d-610ef10c22b3", + "openMode": "aiPrompt", + "parentFolderId": "e4994c63-cfa7-4e49-9dfe-a79e6120a1ae", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "title": "選択テキストの相互翻訳" + }, + { + "iconUrl": "https://web-toolbox.dev/favicon.svg", + "id": "03646140-c83f-5ee6-87ba-8feb12030af0", + "openMode": "pageAction", + "pageActionOption": { + "openMode": "popup", + "startUrl": "https://web-toolbox.dev/tools/character-counter", + "steps": [ + { + "id": "b28aqptaq", + "param": { + "label": "Start", + "type": "start", + "url": "https://web-toolbox.dev/tools/character-counter" + } + }, + { + "delayMs": 100, + "id": "xswttnk5r", + "param": { + "label": "textarea", + "selector": "//textarea", + "selectorType": "xpath", + "type": "click" + } + }, + { + "id": "pycqdt0ap", + "param": { + "label": "文字入力", + "selector": "//textarea", + "selectorType": "xpath", + "type": "input", + "value": "{{SelectedText}}" + } + }, + { + "id": "8tsr1lz9m", + "param": { + "label": "表示位置までスクロール", + "type": "scroll", + "x": 0, + "y": 812 + } + }, + { + "id": "erkxkfph0", + "param": { + "label": "End", + "type": "end" + } + } + ] + }, + "parentFolderId": "0f2167ab-2e1b-4972-954c-71eec058ab14", + "revision": 0, + "title": "Character Counter" + }, + { + "iconUrl": "https://ssl.gstatic.com/docs/doclist/images/drive_2022q3_32dp.png", + "id": "dd05d527-92db-5102-9a88-4a5b31fa7512", + "openMode": "tab", + "openModeSecondary": "tab", + "parentFolderId": "01710cf1-ec8b-497f-8d1f-9cb716567bc4", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://drive.google.com/drive/search?q=%s", + "spaceEncoding": "plus", + "title": "Drive" + }, + { + "iconUrl": "https://ssl.gstatic.com/translate/favicon.ico", + "id": "9a3fca67-e618-5dd3-9ecd-9eb2d088041a", + "openMode": "tab", + "openModeSecondary": "tab", + "parentFolderId": "01710cf1-ec8b-497f-8d1f-9cb716567bc4", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "searchUrl": "https://translate.google.co.jp/?hl=ja&sl=auto&text=%s&op=translate", + "spaceEncoding": "plus", + "title": "en to ja" + }, + { + "id": "$$drag-1", + "openMode": "previewPopup", + "popupOption": { + "height": 700, + "width": 600 + }, + "revision": 0, + "title": "Link Preview" + } + ], + "folders": [ + { + "iconUrl": "https://cdn3.iconfinder.com/data/icons/feather-5/24/search-1024.png", + "id": "222d6489-4eca-48fd-8590-fceb30545bab", + "onlyIcon": true, + "title": "Search" + }, + { + "iconSvg": "", + "iconUrl": "", + "id": "0f2167ab-2e1b-4972-954c-71eec058ab14", + "onlyIcon": true, + "title": "Action" + }, + { + "iconSvg": "", + "iconUrl": "", + "id": "e4994c63-cfa7-4e49-9dfe-a79e6120a1ae", + "onlyIcon": true, + "title": "AI" + }, + { + "iconSvg": "", + "iconUrl": "", + "id": "a3495269-0a4d-4866-a519-bca75ed1c246", + "onlyIcon": true, + "title": "Media" + }, + { + "iconSvg": "", + "iconUrl": "", + "id": "01710cf1-ec8b-497f-8d1f-9cb716567bc4", + "title": "Work" + } + ], + "linkCommand": { + "enabled": "Enable", + "openMode": "previewPopup", + "showIndicator": true, + "sidePanelAutoHide": false, + "startupMethod": { + "keyboardParam": "Shift", + "leftClickHoldParam": 200, + "method": "keyboard", + "threshold": 150 + } + }, + "pageRules": [], + "popupPlacement": { + "align": "start", + "alignOffset": 0, + "side": "top", + "sideOffset": 0 + }, + "settingVersion": "0.16.0", + "shortcuts": { + "shortcuts": [ + { + "commandId": "_placeholder_", + "id": "slot_1", + "noSelectionBehavior": "useClipboard" + }, + { + "commandId": "_placeholder_", + "id": "slot_2", + "noSelectionBehavior": "useClipboard" + }, + { + "commandId": "_placeholder_", + "id": "slot_3", + "noSelectionBehavior": "useClipboard" + } + ] + }, + "startupMethod": { + "method": "textSelection" + }, + "style": "horizontal", + "userStyles": [ + { + "name": "popup-delay", + "value": 250 + }, + { + "name": "popup-duration", + "value": 150 + }, + { + "name": "padding-scale", + "value": "1.5" + }, + { + "name": "image-scale", + "value": "1.1" + }, + { + "name": "font-scale", + "value": "1.1" + } + ], + "windowOption": { + "popupAutoCloseDelay": 0, + "sidePanelAutoHide": false + } +} \ No newline at end of file diff --git a/packages/extension/src/components/option/Dialog.tsx b/packages/extension/src/components/option/Dialog.tsx index e7475fc1..48f9152b 100644 --- a/packages/extension/src/components/option/Dialog.tsx +++ b/packages/extension/src/components/option/Dialog.tsx @@ -8,6 +8,7 @@ import { DialogTitle, DialogPortal, } from "@/components/ui/dialog" +import { TEST_IDS } from "@/testIds.ts" import { t } from "@/services/i18n" import { cn } from "@/lib/utils" @@ -46,12 +47,14 @@ export function Dialog(props: Props) { className={cn(css.button, "disabled:opacity-50")} onClick={() => props.onClose(true)} disabled={props.okDisabled} + data-testid={TEST_IDS.optionDialogOk} > {props.okText} diff --git a/packages/extension/src/components/option/ImportExport.tsx b/packages/extension/src/components/option/ImportExport.tsx index e49877b1..ebf6d57f 100644 --- a/packages/extension/src/components/option/ImportExport.tsx +++ b/packages/extension/src/components/option/ImportExport.tsx @@ -17,6 +17,7 @@ import { t } from "@/services/i18n" import { Download, Upload, Undo2, RotateCcw } from "lucide-react" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import type { BackupData } from "@/services/storage/backupManager" +import { TEST_IDS } from "@/testIds.ts" import css from "./Option.module.css" @@ -365,6 +366,7 @@ export function ImportExport() { onClick={() => setImportDialog(true)} className={css.menuButton} type="button" + data-testid={TEST_IDS.importButton} > {t("Option_Import")} @@ -426,6 +428,7 @@ export function ImportExport() { onChange={handleImport} ref={inputFile} className={`${css.buttonImport}`} + data-testid={TEST_IDS.importFileInput} /> Date: Tue, 17 Mar 2026 05:48:23 +0900 Subject: [PATCH 2/5] Update: Add OptionPage object. --- packages/extension/e2e/extension.spec.ts | 10 +++- packages/extension/e2e/pages/OptionsPage.ts | 51 +++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/extension/e2e/pages/OptionsPage.ts diff --git a/packages/extension/e2e/extension.spec.ts b/packages/extension/e2e/extension.spec.ts index 34ff78e2..ba318223 100644 --- a/packages/extension/e2e/extension.spec.ts +++ b/packages/extension/e2e/extension.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "./fixtures" import { TestPage } from "./pages/TestPage" +import { OptionsPage } from "./pages/OptionsPage" /** * E2E-01: Verify that the extension content script is injected into the test page. @@ -24,8 +25,9 @@ test("E2E-02: popup menu appears on text selection", async ({ page }) => { expect(menubar.isVisible()) }) -test("E2E-03: ポップアップメニューからコマンド実行し、PopupウィンドウでGoogle検索が実行されること", async ({ +test("E2E-03: ポップアップメニューからコマンド実行し、Popupウィンドウでテストページでの検索が実行されること", async ({ context, + extensionId, page, }) => { const testPage = new TestPage(page) @@ -33,6 +35,10 @@ test("E2E-03: ポップアップメニューからコマンド実行し、Popup await testPage.selectText() const menubar = await testPage.getMenuBar() + const optionsPage = new OptionsPage(page, extensionId) + await optionsPage.open() + await optionsPage.importSettings() + // Wait for a new popup window to be created when the button is clicked. const [popupPage] = await Promise.all([ context.waitForEvent("page"), @@ -40,7 +46,7 @@ test("E2E-03: ポップアップメニューからコマンド実行し、Popup ]) await popupPage.waitForLoadState("domcontentloaded") - expect(popupPage.url()).toContain("google.com/search?q=") + expect(popupPage.url()).toContain("k?q=") }) test("E2E-04: コンテキスメニューからコマンド実行し、PopupウィンドウでGoogle検索が実行されること", async ({ diff --git a/packages/extension/e2e/pages/OptionsPage.ts b/packages/extension/e2e/pages/OptionsPage.ts new file mode 100644 index 00000000..22feaecf --- /dev/null +++ b/packages/extension/e2e/pages/OptionsPage.ts @@ -0,0 +1,51 @@ +import path from "path" +import { type Page } from "@playwright/test" +import { TEST_IDS } from "@/testIds" +import { fileURLToPath } from "url" + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const TEST_SETTINGS_PATH = path.join(__dirname, "../data/test-settings.json") + +/** + * Page Object for the extension's options page. + * Encapsulates navigation and settings import interactions. + */ +export class OptionsPage { + constructor( + private readonly page: Page, + private readonly extensionId: string, + ) {} + + /** + * Navigate to the extension's options page. + */ + async open(): Promise { + const url = `chrome-extension://${this.extensionId}/src/options_page.html` + await this.page.goto(url) + await this.page.waitForLoadState("domcontentloaded") + } + + /** + * Import test settings from the test-settings.json file. + * + * Steps: + * 1. Click the import button to open the import dialog. + * 2. Set the test-settings.json file on the file input. + * 3. Click OK to execute the import. + */ + async importSettings(): Promise { + // Open the import dialog + await this.page.locator(`[data-testid="${TEST_IDS.importButton}"]`).click() + + // Set the file on the hidden file input + const fileInput = this.page.locator( + `[data-testid="${TEST_IDS.importFileInput}"]`, + ) + await fileInput.setInputFiles(TEST_SETTINGS_PATH) + + // Confirm the import + await this.page + .locator(`[data-testid="${TEST_IDS.optionDialogOk}"]`) + .click() + } +} From c4a08014c3893f3fb048256027e5147a864c49c6 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Tue, 17 Mar 2026 12:35:01 +0900 Subject: [PATCH 3/5] Update: Add testcase. --- packages/extension/e2e/extension.spec.ts | 41 ++++------- packages/extension/e2e/fixtures.ts | 7 +- packages/extension/e2e/pages/OptionsPage.ts | 79 ++++++++++++++++++--- packages/extension/e2e/pages/TestPage.ts | 12 ++-- 4 files changed, 96 insertions(+), 43 deletions(-) diff --git a/packages/extension/e2e/extension.spec.ts b/packages/extension/e2e/extension.spec.ts index ba318223..71c172e9 100644 --- a/packages/extension/e2e/extension.spec.ts +++ b/packages/extension/e2e/extension.spec.ts @@ -25,46 +25,31 @@ test("E2E-02: popup menu appears on text selection", async ({ page }) => { expect(menubar.isVisible()) }) -test("E2E-03: ポップアップメニューからコマンド実行し、Popupウィンドウでテストページでの検索が実行されること", async ({ +test("E2E-03: executing a command from the popup menu performs search on test page in a popup window", async ({ context, extensionId, + getUserSettings, page, }) => { + // Import test settings to ensure the first menu item is a Testpage command. + const optionsPage = new OptionsPage(context, extensionId, getUserSettings) + await optionsPage.open() + await optionsPage.importSettings() + await optionsPage.close() + + // Arrange: Open the test page and select text to show the popup menu. const testPage = new TestPage(page) await testPage.open() - await testPage.selectText() + await testPage.selectText("h2") const menubar = await testPage.getMenuBar() - const optionsPage = new OptionsPage(page, extensionId) - await optionsPage.open() - await optionsPage.importSettings() - - // Wait for a new popup window to be created when the button is clicked. + // Act: Wait for a new popup window to be created when the button is clicked. const [popupPage] = await Promise.all([ context.waitForEvent("page"), menubar.locator("button").first().click(), ]) - await popupPage.waitForLoadState("domcontentloaded") - expect(popupPage.url()).toContain("k?q=") -}) - -test("E2E-04: コンテキスメニューからコマンド実行し、PopupウィンドウでGoogle検索が実行されること", async ({ - context, - page, - getUserSettings, -}) => { - const testPage = new TestPage(page) - await testPage.open() - await testPage.selectText() - const menubar = await testPage.getMenuBar() - await menubar.locator("button").first().click() - - let [serviceWorker] = context.serviceWorkers() - if (!serviceWorker) { - serviceWorker = await context.waitForEvent("serviceworker") - } - const reuslt = await getUserSettings() - console.log("userSettings", reuslt) + // Assert + expect(popupPage.url()).toContain("?k=Browser") }) diff --git a/packages/extension/e2e/fixtures.ts b/packages/extension/e2e/fixtures.ts index 3d750acf..cbbf78ae 100644 --- a/packages/extension/e2e/fixtures.ts +++ b/packages/extension/e2e/fixtures.ts @@ -11,6 +11,11 @@ import { fileURLToPath } from "url" const __dirname = path.dirname(fileURLToPath(import.meta.url)) const pathToExtension = path.join(__dirname, "../dist") +type StorageChangeMap = { + [key: string]: chrome.storage.StorageChange +} +export type WaitForStorageChange = () => Promise + type Fixtures = { context: BrowserContext extensionId: string @@ -24,7 +29,7 @@ type Fixtures = { */ export const test = base.extend({ // eslint-disable-next-line no-empty-pattern - context: async ({}, use) => { + context: async ({ }, use) => { // When running with --debug, PWDEBUG is set; show the browser window in that case. const isDebug = !!process.env.PWDEBUG const context = await chromium.launchPersistentContext("", { diff --git a/packages/extension/e2e/pages/OptionsPage.ts b/packages/extension/e2e/pages/OptionsPage.ts index 22feaecf..5a4cf498 100644 --- a/packages/extension/e2e/pages/OptionsPage.ts +++ b/packages/extension/e2e/pages/OptionsPage.ts @@ -1,7 +1,14 @@ import path from "path" -import { type Page } from "@playwright/test" + +import { Page, type BrowserContext } from "@playwright/test" + import { TEST_IDS } from "@/testIds" import { fileURLToPath } from "url" +import type { UserSettings } from "@/types" + +function sleep(msec: number): Promise { + return new Promise((resolve) => setTimeout(resolve, msec)) +} const __dirname = path.dirname(fileURLToPath(import.meta.url)) const TEST_SETTINGS_PATH = path.join(__dirname, "../data/test-settings.json") @@ -11,29 +18,55 @@ const TEST_SETTINGS_PATH = path.join(__dirname, "../data/test-settings.json") * Encapsulates navigation and settings import interactions. */ export class OptionsPage { + private page: Page | null + constructor( - private readonly page: Page, + private readonly context: BrowserContext, private readonly extensionId: string, - ) {} + private readonly getUserSettings: () => Promise, + ) { + this.page = null + } /** * Navigate to the extension's options page. */ async open(): Promise { const url = `chrome-extension://${this.extensionId}/src/options_page.html` + this.page = await this.context.newPage() await this.page.goto(url) await this.page.waitForLoadState("domcontentloaded") } + /** + * Close the options page if it's open. + * Ensures that resources are cleaned up after tests. + */ + async close(): Promise { + if (this.page) { + await this.page.close() + this.page = null + } + } + /** * Import test settings from the test-settings.json file. * * Steps: * 1. Click the import button to open the import dialog. * 2. Set the test-settings.json file on the file input. - * 3. Click OK to execute the import. + * 3. Wait for the file to be read and OK button to be enabled. + * 4. Click OK to execute the import. + * 5. Wait for the page to reload and settings to be saved. */ async importSettings(): Promise { + if (!this.page) { + await this.open() + if (!this.page) { + throw new Error("Failed to open options page") + } + } + // Open the import dialog await this.page.locator(`[data-testid="${TEST_IDS.importButton}"]`).click() @@ -43,9 +76,39 @@ export class OptionsPage { ) await fileInput.setInputFiles(TEST_SETTINGS_PATH) - // Confirm the import - await this.page - .locator(`[data-testid="${TEST_IDS.optionDialogOk}"]`) - .click() + // Wait for the file to be read and OK button to be enabled + const okButton = this.page.locator( + `[data-testid="${TEST_IDS.optionDialogOk}"]`, + ) + await this.page.waitForFunction( + (testId) => { + const button = document.querySelector( + `[data-testid="${testId}"]`, + ) as HTMLButtonElement + return button && !button.disabled + }, + TEST_IDS.optionDialogOk, + { timeout: 5000 }, + ) + + // Confirm the import and wait for page reload + const reloadPromise = this.page.waitForLoadState("domcontentloaded") + await okButton.click() + await reloadPromise + + // Wait for the settings to be loaded with commands + let settings + let timeout = 5000 // Maximum wait time of 5 seconds + const interval = 40 // milliseconds + do { + await sleep(interval) + settings = await this.getUserSettings() + timeout -= interval + } while (settings == null && timeout > 0) + + if (settings == null) { + console.error("Failed to import settings") + throw new Error("Failed to import settings") + } } } diff --git a/packages/extension/e2e/pages/TestPage.ts b/packages/extension/e2e/pages/TestPage.ts index 789a1a8b..823d0670 100644 --- a/packages/extension/e2e/pages/TestPage.ts +++ b/packages/extension/e2e/pages/TestPage.ts @@ -9,7 +9,7 @@ const APP_ID = "selection-command" * Encapsulates navigation and user interactions specific to this page. */ export class TestPage { - constructor(private readonly page: Page) {} + constructor(private readonly page: Page) { } /** * Navigate to the test page and wait until the extension content script is injected. @@ -39,10 +39,10 @@ export class TestPage { * listeners are registered the popup appears within 250ms and the next poll * detects it. */ - async selectText(): Promise { + async selectText(cssSelector = "h1, h2, h3"): Promise { await this.page.waitForFunction( - (testIds) => { - const heading = document.querySelector("h1, h2, h3") + ({ testIds, appId, cssSelector }) => { + const heading = document.querySelector(cssSelector) if (!heading) return false // Scroll into view so getBoundingClientRect() returns valid coordinates. @@ -87,13 +87,13 @@ export class TestPage { // The popup portals into document.body via Radix UI. It appears after a // ~250ms delay. Polling at 300ms gives the popup time to render before // the next check. - const el = document.getElementById(testIds.appId) + const el = document.getElementById(appId) return ( el?.shadowRoot?.querySelector(`[data-testid='${testIds.menuBar}']`) != null ) }, - { ...TEST_IDS, appId: APP_ID }, + { testIds: TEST_IDS, appId: APP_ID, cssSelector }, { polling: 300, timeout: 10_000 }, ) } From 9a9e48952eb949436b6609198c6914388d0a3c67 Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Wed, 18 Mar 2026 12:40:10 +0900 Subject: [PATCH 4/5] Update: Icon of AI folder. --- packages/extension/src/services/option/defaultSettings.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/src/services/option/defaultSettings.ts b/packages/extension/src/services/option/defaultSettings.ts index 5057b2ed..c28d061d 100644 --- a/packages/extension/src/services/option/defaultSettings.ts +++ b/packages/extension/src/services/option/defaultSettings.ts @@ -107,7 +107,7 @@ export default { title: "AI", iconUrl: "", iconSvg: - '', + '', id: FOLDER_AI, onlyIcon: true, }, From df3411b1b85d3ebe3b2ca8c77d6b6af06c7c7ddb Mon Sep 17 00:00:00 2001 From: ujiro99 Date: Sat, 21 Mar 2026 10:13:29 +0900 Subject: [PATCH 5/5] Update: Review comments. --- packages/extension/e2e/pages/OptionsPage.ts | 42 +++++++------------ .../src/components/option/Dialog.tsx | 2 +- .../src/components/option/ImportExport.tsx | 10 ++--- 3 files changed, 20 insertions(+), 34 deletions(-) diff --git a/packages/extension/e2e/pages/OptionsPage.ts b/packages/extension/e2e/pages/OptionsPage.ts index 5a4cf498..75d53bb7 100644 --- a/packages/extension/e2e/pages/OptionsPage.ts +++ b/packages/extension/e2e/pages/OptionsPage.ts @@ -1,15 +1,11 @@ import path from "path" -import { Page, type BrowserContext } from "@playwright/test" +import { expect, Page, type BrowserContext } from "@playwright/test" import { TEST_IDS } from "@/testIds" import { fileURLToPath } from "url" import type { UserSettings } from "@/types" -function sleep(msec: number): Promise { - return new Promise((resolve) => setTimeout(resolve, msec)) -} - const __dirname = path.dirname(fileURLToPath(import.meta.url)) const TEST_SETTINGS_PATH = path.join(__dirname, "../data/test-settings.json") @@ -62,25 +58,21 @@ export class OptionsPage { async importSettings(): Promise { if (!this.page) { await this.open() - if (!this.page) { - throw new Error("Failed to open options page") - } } + const page = this.page! // Open the import dialog - await this.page.locator(`[data-testid="${TEST_IDS.importButton}"]`).click() + await page.locator(`[data-testid="${TEST_IDS.importButton}"]`).click() // Set the file on the hidden file input - const fileInput = this.page.locator( + const fileInput = page.locator( `[data-testid="${TEST_IDS.importFileInput}"]`, ) await fileInput.setInputFiles(TEST_SETTINGS_PATH) // Wait for the file to be read and OK button to be enabled - const okButton = this.page.locator( - `[data-testid="${TEST_IDS.optionDialogOk}"]`, - ) - await this.page.waitForFunction( + const okButton = page.locator(`[data-testid="${TEST_IDS.optionDialogOk}"]`) + await page.waitForFunction( (testId) => { const button = document.querySelector( `[data-testid="${testId}"]`, @@ -92,23 +84,17 @@ export class OptionsPage { ) // Confirm the import and wait for page reload - const reloadPromise = this.page.waitForLoadState("domcontentloaded") + const reloadPromise = page.waitForLoadState("domcontentloaded") await okButton.click() await reloadPromise // Wait for the settings to be loaded with commands - let settings - let timeout = 5000 // Maximum wait time of 5 seconds - const interval = 40 // milliseconds - do { - await sleep(interval) - settings = await this.getUserSettings() - timeout -= interval - } while (settings == null && timeout > 0) - - if (settings == null) { - console.error("Failed to import settings") - throw new Error("Failed to import settings") - } + await expect + .poll(async () => await this.getUserSettings(), { + message: "User settings should be loaded with commands after import", + timeout: 5000, + intervals: [40], + }) + .not.toBeUndefined() } } diff --git a/packages/extension/src/components/option/Dialog.tsx b/packages/extension/src/components/option/Dialog.tsx index 48f9152b..25dce37f 100644 --- a/packages/extension/src/components/option/Dialog.tsx +++ b/packages/extension/src/components/option/Dialog.tsx @@ -8,7 +8,7 @@ import { DialogTitle, DialogPortal, } from "@/components/ui/dialog" -import { TEST_IDS } from "@/testIds.ts" +import { TEST_IDS } from "@/testIds" import { t } from "@/services/i18n" import { cn } from "@/lib/utils" diff --git a/packages/extension/src/components/option/ImportExport.tsx b/packages/extension/src/components/option/ImportExport.tsx index ebf6d57f..85f807f0 100644 --- a/packages/extension/src/components/option/ImportExport.tsx +++ b/packages/extension/src/components/option/ImportExport.tsx @@ -17,7 +17,7 @@ import { t } from "@/services/i18n" import { Download, Upload, Undo2, RotateCcw } from "lucide-react" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import type { BackupData } from "@/services/storage/backupManager" -import { TEST_IDS } from "@/testIds.ts" +import { TEST_IDS } from "@/testIds" import css from "./Option.module.css" @@ -264,7 +264,7 @@ export function ImportExport() { const handleImportClose = (ret: boolean) => { if (ret && importJson != null) { - ; (async () => { + ;(async () => { const { commandExecutionCount = 0, hasShownReviewRequest = false, @@ -290,7 +290,7 @@ export function ImportExport() { const handleRestoreClose = (ret: boolean) => { if (ret) { - ; (async () => { + ;(async () => { try { let backupCommands: any[] = [] @@ -385,8 +385,8 @@ export function ImportExport() { ) ? t("Option_RestoreFromBackup_checking") : !Object.values(backupData).some( - (backup) => backup.status === BACKUP_STATUS.AVAILABLE, - ) + (backup) => backup.status === BACKUP_STATUS.AVAILABLE, + ) ? t("Option_RestoreFromBackup_no_backup") : t("Option_RestoreFromBackup_tooltip") }