diff --git a/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts b/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts index cd9199652f..dde3fe6dea 100644 --- a/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts +++ b/end-to-end-tests/pageObjects/extensionConsole/modsPage.ts @@ -136,6 +136,16 @@ export class ActivateModPage { return this.page.getByRole("button", { name: "Activate" }); } + configureQuickbarShortcutLink() { + return this.page.getByRole("link", { name: "configured your Quick Bar" }); + } + + keyboardShortcutDocumentationLink() { + return this.page.getByRole("link", { + name: "configuring keyboard shortcuts", + }); + } + /** Successfully activating the mod will navigate to the "All Mods" page. */ async clickActivateAndWaitForModsPageRedirect() { await this.activateButton().click(); diff --git a/end-to-end-tests/pageObjects/extensionsShortcutsPage.ts b/end-to-end-tests/pageObjects/extensionsShortcutsPage.ts new file mode 100644 index 0000000000..73979a1593 --- /dev/null +++ b/end-to-end-tests/pageObjects/extensionsShortcutsPage.ts @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2024 PixieBrix, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { expect, type Page } from "@playwright/test"; +import { getModifierKey, getModifierSymbol } from "end-to-end-tests/utils"; + +function getExtensionShortcutsUrl(chromiumChannel: "chrome" | "msedge") { + switch (chromiumChannel) { + case "chrome": { + return "chrome://extensions/shortcuts"; + } + + case "msedge": { + return "edge://extensions/shortcuts"; + } + + default: { + const exhaustiveCheck: never = chromiumChannel; + throw new Error(`Unexpected channel: ${exhaustiveCheck}`); + } + } +} + +async function getShortcut(page: Page): Promise { + const modifierKey = await getModifierKey(page); + const modifierSymbol = await getModifierSymbol(page); + + return modifierKey === "Meta" ? `${modifierSymbol}M` : "Ctrl + M"; +} + +export class ExtensionsShortcutsPage { + private readonly pageUrl: string; + + constructor( + private readonly page: Page, + private readonly chromiumChannel: "chrome" | "msedge", + ) { + this.pageUrl = getExtensionShortcutsUrl(this.chromiumChannel); + } + + getPageUrl() { + return this.pageUrl; + } + + async goto() { + await this.page.goto(this.pageUrl); + + if (this.chromiumChannel === "chrome") { + await expect( + this.page.getByRole("heading", { name: /PixieBrix/ }), + ).toBeVisible(); + } else { + await expect(this.page.getByText(/PixieBrix/)).toBeVisible(); + } + } + + async clearQuickbarShortcut() { + await this.page.bringToFront(); + + const shortcut = await getShortcut(this.page); + + if (this.chromiumChannel === "chrome") { + await expect(this.page.getByPlaceholder(/shortcut set: /i)).toHaveValue( + shortcut, + ); + + // Clear the shortcut + await this.page.getByLabel("Edit shortcut Toggle Quick").click(); + await this.page + .locator("extensions-keyboard-shortcuts #container") + .click(); + + await expect( + this.page.getByLabel(/Shortcut Toggle Quick Bar for PixieBrix/, { + exact: true, + }), + ).toBeEmpty(); + } else { + await expect( + this.page.getByLabel( + /Type a shortcut that will Toggle Quick Bar for PixieBrix/, + ), + ).toHaveValue(shortcut); + + await this.page.getByRole("button", { name: "Clear shortcut" }).click(); + + await expect( + this.page.getByLabel( + /Type a shortcut that will Toggle Quick Bar for PixieBrix/, + ), + ).toBeEmpty(); + } + } + + async setQuickbarShortcut() { + await this.page.bringToFront(); + + const modifierKey = await getModifierKey(this.page); + const shortcut = await getShortcut(this.page); + + if (this.chromiumChannel === "chrome") { + await expect( + this.page.getByLabel(/Shortcut Toggle Quick Bar for PixieBrix/), + ).toBeEmpty(); + + await this.page.getByLabel("Edit shortcut Toggle Quick").click(); + await this.page + .getByPlaceholder("Type a shortcut") + .press(`${modifierKey}+m`); + + await this.page + .locator("extensions-keyboard-shortcuts #container") + .click(); + + await expect(this.page.getByPlaceholder(/shortcut set: /i)).toHaveValue( + shortcut, + ); + } else { + const shortcutLabel = /type a shortcut that will toggle quick bar/i; + const input = this.page.getByLabel(shortcutLabel); + + await expect(input).toBeEmpty(); + + await input.click(); + await input.press(`${modifierKey}+m`); + + await this.page.getByText("Keyboard ShortcutsPixieBrix").click(); + + await expect(input).toHaveValue(shortcut); + } + } +} diff --git a/end-to-end-tests/tests/extensionConsoleActivation.spec.ts b/end-to-end-tests/tests/extensionConsoleActivation.spec.ts index 6eee15cd8f..d7ef3915ac 100644 --- a/end-to-end-tests/tests/extensionConsoleActivation.spec.ts +++ b/end-to-end-tests/tests/extensionConsoleActivation.spec.ts @@ -19,11 +19,17 @@ import { test, expect } from "../fixtures/extensionBase"; import { ActivateModPage } from "../pageObjects/extensionConsole/modsPage"; // @ts-expect-error -- https://youtrack.jetbrains.com/issue/AQUA-711/Provide-a-run-configuration-for-Playwright-tests-in-specs-with-fixture-imports-only import { type Page, test as base, type Frame } from "@playwright/test"; -import { getSidebarPage, runModViaQuickBar } from "../utils"; +import { + getSidebarPage, + clickAndWaitForNewPage, + runModViaQuickBar, + getBrowserOs, +} from "../utils"; import path from "node:path"; import { VALID_UUID_REGEX } from "@/types/stringTypes"; import { type Serializable } from "playwright-core/types/structs"; import { MV } from "../env"; +import { ExtensionsShortcutsPage } from "end-to-end-tests/pageObjects/extensionsShortcutsPage"; test("can activate a mod with no config options", async ({ page, @@ -164,3 +170,68 @@ test("can activate a mod with a database", async ({ page, extensionId }) => { await expect(sideBarPage.getByTestId("card").getByText(note)).toBeHidden(); }); + +test("activating a mod when the quickbar shortcut is not configured", async ({ + context, + page: firstTab, + extensionId, + chromiumChannel, +}) => { + const shortcutsPage = new ExtensionsShortcutsPage(firstTab, chromiumChannel); + await shortcutsPage.goto(); + + await test.step("Clear the quickbar shortcut before activing a quickbar mod", async () => { + const os = await getBrowserOs(firstTab); + // See https://github.com/pixiebrix/pixiebrix-extension/issues/6268 + // eslint-disable-next-line playwright/no-conditional-in-test -- Existing bug where shortcut isn't set on Edge in Windows/Linux + if (os === "MacOS" || chromiumChannel === "chrome") { + await shortcutsPage.clearQuickbarShortcut(); + } + }); + + let modActivationPage: ActivateModPage; + const secondTab = await context.newPage(); + await test.step("Begin activation of a mod with a quickbar shortcut", async () => { + const modId = "@e2e-testing/show-alert"; + modActivationPage = new ActivateModPage(secondTab, extensionId, modId); + await modActivationPage.goto(); + }); + + await test.step("Verify the mod activation page has links for setting the shortcut", async () => { + await expect( + modActivationPage.keyboardShortcutDocumentationLink(), + ).toBeVisible(); + await modActivationPage.keyboardShortcutDocumentationLink().click(); + + await expect( + secondTab.getByRole("heading", { name: "Changing the Quick Bar" }), + ).toBeVisible(); + await secondTab.goBack(); + + await expect( + modActivationPage.configureQuickbarShortcutLink(), + ).toBeVisible(); + + const configureShortcutPage = await clickAndWaitForNewPage( + modActivationPage.configureQuickbarShortcutLink(), + context, + ); + + await expect(configureShortcutPage).toHaveURL(shortcutsPage.getPageUrl()); + await configureShortcutPage.close(); + }); + + await test.step("Restore the shortcut and activate the mod", async () => { + await shortcutsPage.setQuickbarShortcut(); + + await modActivationPage.clickActivateAndWaitForModsPageRedirect(); + }); + + await test.step("Verify the mod is activated and works as expected", async () => { + await firstTab.bringToFront(); + await firstTab.goto("/"); + + await runModViaQuickBar(firstTab, "Show Alert"); + await expect(firstTab.getByText("Quick Bar Action ran")).toBeVisible(); + }); +}); diff --git a/end-to-end-tests/utils.ts b/end-to-end-tests/utils.ts index 6c681db216..10942cbb91 100644 --- a/end-to-end-tests/utils.ts +++ b/end-to-end-tests/utils.ts @@ -16,7 +16,13 @@ */ import type AxeBuilder from "@axe-core/playwright"; -import { type Locator, expect, type Page, type Frame } from "@playwright/test"; +import { + type Locator, + expect, + type Page, + type Frame, + type BrowserContext, +} from "@playwright/test"; import { MV } from "./env"; type AxeResults = Awaited>; @@ -71,8 +77,10 @@ export async function ensureVisibility( export async function runModViaQuickBar(page: Page, modName: string) { await waitForQuickBarReadiness(page); await page.locator("html").focus(); // Ensure the page is focused before running the keyboard shortcut - await page.keyboard.press("Meta+M"); // MacOS - await page.keyboard.press("Control+M"); // Windows and Linux + + const modifierKey = await getModifierKey(page); + await page.keyboard.press(`${modifierKey}+M`); + // Short delay to allow the quickbar to finish opening // eslint-disable-next-line playwright/no-wait-for-timeout -- TODO: Find a better way to detect when the quickbar is done loading opening await page.waitForTimeout(500); @@ -182,3 +190,56 @@ export async function conditionallyHoverOverMV2Sidebar(page: Page) { await sidebarFrame.dispatchEvent("mouseenter"); } } + +/** + * Returns a reference to the new page that was opened. + * @param locator The anchor or button that opens the new page (must be clickable) + * @param context The browser context + */ +export async function clickAndWaitForNewPage( + locator: Locator, + context: BrowserContext, +): Promise { + const pagePromise = context.waitForEvent("page"); + + await locator.click(); + + return pagePromise; +} + +// Temporary workaround for determining which modifiers to use for keyboard shortcuts +// A permanent fix has been merged but not released +// See: https://github.com/microsoft/playwright/pull/30572 +export async function getBrowserOs(page: Page): Promise { + let OSName = ""; + + const response = String(await page.evaluate(() => navigator.userAgent)); + + if (response.includes("Win")) { + OSName = "Windows"; + } + + if (response.includes("Mac")) { + OSName = "MacOS"; + } + + if (response.includes("X11")) { + OSName = "Unix"; + } + + if (response.includes("Linux")) { + OSName = "Linux"; + } + + return OSName; +} + +export async function getModifierKey(page: Page): Promise { + const OSName = await getBrowserOs(page); + return OSName === "MacOS" ? "Meta" : "Control"; +} + +export async function getModifierSymbol(page: Page): Promise { + const OSName = await getBrowserOs(page); + return OSName === "MacOS" ? "⌘" : "⌃"; +}