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" ? "⌘" : "⌃";
+}