diff --git a/.env.example b/.env.example
index c242397298..52d50fb4f8 100644
--- a/.env.example
+++ b/.env.example
@@ -10,6 +10,9 @@ CHROME_EXTENSION_ID=mpjjildhmpddojocokjkgmlkkkfjnepo
# Shadow DOM mode for all components. Default is SHADOW_DOM=closed in regular webpack builds, open elsewhere
# SHADOW_DOM=open
+# Enable telemetry in development. Default is false
+# DEV_EVENT_TELEMETRY=true
+
# This makes all optional permissions required in the manifest.json to avoid permission popups. Only required for Playwright tests.
# REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST=1
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8f9f3e4d73..79cc497a5d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,8 +7,6 @@ env:
PUBLIC_RELEASE: ${{ github.ref == 'refs/heads/main' }}
# Staging URL, also directly used by webpack
SERVICE_URL: https://app-stg.pixiebrix.com/
- E2E_TEST_USER_EMAIL_UNAFFILIATED: ${{ secrets.E2E_TEST_USER_EMAIL_UNAFFILIATED }}
- E2E_TEST_USER_PASSWORD_UNAFFILIATED: ${{ secrets.E2E_TEST_USER_PASSWORD_UNAFFILIATED }}
jobs:
test:
@@ -205,6 +203,9 @@ jobs:
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST: 1
+ E2E_TEST_USER_EMAIL_UNAFFILIATED: ${{ secrets.E2E_TEST_USER_EMAIL_UNAFFILIATED }}
+ E2E_TEST_USER_PASSWORD_UNAFFILIATED: ${{ secrets.E2E_TEST_USER_PASSWORD_UNAFFILIATED }}
+ DEV_EVENT_TELEMETRY: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
diff --git a/end-to-end-tests/README.md b/end-to-end-tests/README.md
index 504928c42b..a444566995 100644
--- a/end-to-end-tests/README.md
+++ b/end-to-end-tests/README.md
@@ -15,11 +15,13 @@ One-time setup:
- The test user password `E2E_TEST_USER_PASSWORD_UNAFFILIATED`
- Uncomment `REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST=1`
- Uncomment `SHADOW_DOM=open`
+ - Uncomment `DATADOG_CLIENT_TOKEN` (used for telemetry tests, can be set to a fake token, e.g. `secret123`)
+ - Uncomment `DEV_EVENT_TELEMETRY` (used for telemetry tests, actual telemetry requests should be mocked during testing)
- `MV` will determine the manifest version for the both the extension and the tests (defaulted to 3 if not defined.)
- Install browsers: Execute `npx playwright install chromium chrome msedge`.
1. Install dependencies: Run `npm install`
-2. Build the extension: Run: `npm run build:webpack` (or `npm run watch`)
+2. Build the extension: Run: `npm run watch`
3. Run the tests: Use the command `npm run test:e2e`.
- To run tests in interactive UI mode, use `npm run test:e2e -- --ui`. This view shows you the entire test suite and
diff --git a/end-to-end-tests/auth.setup.ts b/end-to-end-tests/auth.setup.ts
index c74ad1dd94..3435b67830 100644
--- a/end-to-end-tests/auth.setup.ts
+++ b/end-to-end-tests/auth.setup.ts
@@ -16,7 +16,7 @@
*/
import { expect, type Page } from "@playwright/test";
-import { test } from "./fixtures/authSetupFixture";
+import { test } from "./fixtures/authSetup";
import {
E2E_TEST_USER_EMAIL_UNAFFILIATED,
E2E_TEST_USER_PASSWORD_UNAFFILIATED,
diff --git a/end-to-end-tests/env.ts b/end-to-end-tests/env.ts
index 486f4c7add..a1b0ca3e94 100644
--- a/end-to-end-tests/env.ts
+++ b/end-to-end-tests/env.ts
@@ -24,6 +24,7 @@ const requiredEnvVariables = [
"SERVICE_URL",
"E2E_TEST_USER_EMAIL_UNAFFILIATED",
"E2E_TEST_USER_PASSWORD_UNAFFILIATED",
+ "SHADOW_DOM",
] as const;
// It's not strictly required for the test run itself, but the extension manifest.json must have been built with
@@ -45,20 +46,24 @@ type OptionalEnvVariables = Record<
string | undefined
>;
-for (const key of requiredEnvVariables) {
- // eslint-disable-next-line security/detect-object-injection -- key is a constant
- if (process.env[key] === undefined) {
- throw new Error(`Required environment variable is not configured: ${key}`);
- }
+export const assertRequiredEnvVariables = () => {
+ for (const key of requiredEnvVariables) {
+ // eslint-disable-next-line security/detect-object-injection -- key is a constant
+ if (process.env[key] === undefined) {
+ throw new Error(
+ `Required environment variable is not configured: ${key}`,
+ );
+ }
- // eslint-disable-next-line security/detect-object-injection -- key is a constant
- if (typeof process.env[key] !== "string") {
- // For the time being we expect all of our requiredEnvVariables to be strings
- throw new TypeError(
- `Required environment variable is not configured: ${key}`,
- );
+ // eslint-disable-next-line security/detect-object-injection -- key is a constant
+ if (typeof process.env[key] !== "string") {
+ // For the time being we expect all of our requiredEnvVariables to be strings
+ throw new TypeError(
+ `Required environment variable is not configured: ${key}`,
+ );
+ }
}
-}
+};
export const {
SERVICE_URL,
@@ -71,5 +76,4 @@ export const {
MV = "3",
SLOWMO,
PWDEBUG,
- REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST,
} = process.env as OptionalEnvVariables;
diff --git a/end-to-end-tests/fixtures/authSetup.ts b/end-to-end-tests/fixtures/authSetup.ts
new file mode 100644
index 0000000000..739ad20577
--- /dev/null
+++ b/end-to-end-tests/fixtures/authSetup.ts
@@ -0,0 +1,91 @@
+/*
+ * 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 {
+ test as base,
+ mergeTests,
+ type BrowserContext,
+ type Page,
+} from "@playwright/test";
+import {
+ getAuthProfilePathFile,
+ launchPersistentContextWithExtension,
+} from "./utils";
+import fs from "node:fs/promises";
+import path from "node:path";
+import * as os from "node:os";
+import { test as envSetup } from "./envSetup";
+
+// Create a local auth directory to store the profile paths
+const createAuthProfilePathDirectory = async () => {
+ const authPath = path.join(__dirname, "../.auth");
+ try {
+ await fs.mkdir(authPath);
+ } catch (error) {
+ if (
+ !(error instanceof Error && "code" in error && error.code === "EEXIST")
+ ) {
+ throw error;
+ }
+ }
+};
+
+export const test = mergeTests(
+ envSetup,
+ base.extend<{
+ contextAndPage: { context: BrowserContext; page: Page };
+ chromiumChannel: "chrome" | "msedge";
+ additionalRequiredEnvVariables: string[];
+ }>({
+ chromiumChannel: ["chrome", { option: true }],
+ additionalRequiredEnvVariables: [
+ "REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST",
+ ],
+ // Provides the context and the initial page together in the same fixture
+ async contextAndPage({ chromiumChannel }, use, testInfo) {
+ // Create a temp directory to store the test profile
+ const authSetupProfileDirectory = await fs.mkdtemp(
+ path.join(os.tmpdir(), "authSetup-"),
+ );
+
+ // Create a local auth directory to store the profile paths
+ await createAuthProfilePathDirectory();
+
+ const context = await launchPersistentContextWithExtension(
+ chromiumChannel,
+ authSetupProfileDirectory,
+ );
+
+ // The admin console automatically opens a new tab to log in and link the newly installed extension to the user's account.
+ const page = await context.waitForEvent("page", { timeout: 10_000 });
+
+ await use({ context, page });
+
+ // Store the profile path for future use if the auth setup test passes
+ if (testInfo.status === "passed") {
+ const authProfilePathFile = getAuthProfilePathFile(chromiumChannel);
+ await fs.writeFile(
+ authProfilePathFile,
+ authSetupProfileDirectory,
+ "utf8",
+ );
+ }
+
+ await context.close();
+ },
+ }),
+);
diff --git a/end-to-end-tests/fixtures/authSetupFixture.ts b/end-to-end-tests/fixtures/authSetupFixture.ts
deleted file mode 100644
index ede45ca861..0000000000
--- a/end-to-end-tests/fixtures/authSetupFixture.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * 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 { test as base, type BrowserContext, type Page } from "@playwright/test";
-import { REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST } from "../env";
-import {
- getAuthProfilePathFile,
- launchPersistentContextWithExtension,
-} from "./utils";
-import fs from "node:fs/promises";
-import path from "node:path";
-import * as os from "node:os";
-
-// Create a local auth directory to store the profile paths
-const createAuthProfilePathDirectory = async () => {
- const authPath = path.join(__dirname, "../.auth");
- try {
- await fs.mkdir(authPath);
- } catch (error) {
- if (
- !(error instanceof Error && "code" in error && error.code === "EEXIST")
- ) {
- throw error;
- }
- }
-};
-
-export const test = base.extend<{
- contextAndPage: { context: BrowserContext; page: Page };
- chromiumChannel: "chrome" | "msedge";
-}>({
- chromiumChannel: ["chrome", { option: true }],
- // Provides the context and the initial page together in the same fixture
- async contextAndPage({ chromiumChannel }, use, testInfo) {
- if (!REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST) {
- throw new Error(
- "This test requires optional permissions to be required in the manifest. Please set REQUIRE_OPTIONAL_PERMISSIONS_IN_MANIFEST=1 in your `.env.development` and rerun the extension build.",
- );
- }
-
- // Create a temp directory to store the test profile
- const authSetupProfileDirectory = await fs.mkdtemp(
- path.join(os.tmpdir(), "authSetup-"),
- );
-
- // Create a local auth directory to store the profile paths
- await createAuthProfilePathDirectory();
-
- const context = await launchPersistentContextWithExtension(
- chromiumChannel,
- authSetupProfileDirectory,
- );
-
- // The admin console automatically opens a new tab to log in and link the newly installed extension to the user's account.
- const page = await context.waitForEvent("page", { timeout: 10_000 });
-
- await use({ context, page });
-
- // Store the profile path for future use if the auth setup test passes
- if (testInfo.status === "passed") {
- const authProfilePathFile = getAuthProfilePathFile(chromiumChannel);
- await fs.writeFile(
- authProfilePathFile,
- authSetupProfileDirectory,
- "utf8",
- );
- }
-
- await context.close();
- },
-});
diff --git a/end-to-end-tests/fixtures/envSetup.ts b/end-to-end-tests/fixtures/envSetup.ts
new file mode 100644
index 0000000000..2ef13b00e9
--- /dev/null
+++ b/end-to-end-tests/fixtures/envSetup.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 { test as base } from "@playwright/test";
+import { assertRequiredEnvVariables } from "../env";
+
+export const test = base.extend<{
+ additionalRequiredEnvVariables: string[];
+ expectRequiredEnvVariables: void;
+}>({
+ additionalRequiredEnvVariables: [],
+ expectRequiredEnvVariables: [
+ async ({ additionalRequiredEnvVariables }, use) => {
+ assertRequiredEnvVariables();
+
+ for (const key of additionalRequiredEnvVariables) {
+ if (process.env[key] === undefined) {
+ throw new Error(
+ `This test requires additional environment variable ${key} to be configured. Configure it in your .env.development file and re-build the extension.`,
+ );
+ }
+ }
+
+ await use();
+ },
+ { auto: true },
+ ],
+});
diff --git a/end-to-end-tests/fixtures/extensionBase.ts b/end-to-end-tests/fixtures/extensionBase.ts
index f2ca1b3925..d06923a5f9 100644
--- a/end-to-end-tests/fixtures/extensionBase.ts
+++ b/end-to-end-tests/fixtures/extensionBase.ts
@@ -15,7 +15,11 @@
* along with this program. If not, see .
*/
-import { test as base, type BrowserContext } from "@playwright/test";
+import {
+ test as base,
+ mergeTests,
+ type BrowserContext,
+} from "@playwright/test";
import path from "node:path";
import fs from "node:fs/promises";
import {
@@ -24,6 +28,7 @@ import {
launchPersistentContextWithExtension,
} from "./utils";
import { ModsPage } from "../pageObjects/extensionConsole/modsPage";
+import { test as envSetup } from "./envSetup";
// This environment variable is used to attach the browser sidepanel window that opens automatically to Playwright.
// See https://github.com/microsoft/playwright/issues/26693
@@ -33,67 +38,75 @@ process.env.PW_CHROMIUM_ATTACH_TO_OTHER = "1";
// See https://playwright.dev/docs/service-workers-experimental
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS = "1";
-export const test = base.extend<{
- context: BrowserContext;
- extensionId: string;
- chromiumChannel: "chrome" | "msedge";
-}>({
- chromiumChannel: ["chrome", { option: true }],
- async context({ chromiumChannel }, use) {
- let authSetupProfileDirectory: string;
+export const test = mergeTests(
+ envSetup,
+ base.extend<
+ {
+ context: BrowserContext;
+ extensionId: string;
+ chromiumChannel: "chrome" | "msedge";
+ },
+ {
+ checkRequiredEnvironmentVariables: () => void;
+ }
+ >({
+ chromiumChannel: ["chrome", { option: true }],
+ async context({ chromiumChannel }, use) {
+ let authSetupProfileDirectory: string;
- try {
- authSetupProfileDirectory = await fs.readFile(
- getAuthProfilePathFile(chromiumChannel),
- "utf8",
- );
- } catch (error) {
- if (
- error instanceof Error &&
- "code" in error &&
- error.code === "ENOENT"
- ) {
- console.log(
- "No auth setup profile found. Make sure that the `auth.setup` project has been run first to create the " +
- "profile. (If using UI mode, make sure that the chromeSetup and/or the edgeSetup projects are not filtered out)",
+ try {
+ authSetupProfileDirectory = await fs.readFile(
+ getAuthProfilePathFile(chromiumChannel),
+ "utf8",
);
- }
+ } catch (error) {
+ if (
+ error instanceof Error &&
+ "code" in error &&
+ error.code === "ENOENT"
+ ) {
+ console.log(
+ "No auth setup profile found. Make sure that the `auth.setup` project has been run first to create the " +
+ "profile. (If using UI mode, make sure that the chromeSetup and/or the edgeSetup projects are not filtered out)",
+ );
+ }
- throw error;
- }
+ throw error;
+ }
- const temporaryProfileDirectory = await fs.mkdtemp(
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion,@typescript-eslint/no-non-null-assertion -- checked above
- path.join(path.dirname(authSetupProfileDirectory!), "e2e-test-"),
- );
- // Copy the auth setup profile to a new temp directory to avoid modifying the original auth profile
- await fs.cp(authSetupProfileDirectory, temporaryProfileDirectory, {
- recursive: true,
- });
+ const temporaryProfileDirectory = await fs.mkdtemp(
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion,@typescript-eslint/no-non-null-assertion -- checked above
+ path.join(path.dirname(authSetupProfileDirectory!), "e2e-test-"),
+ );
+ // Copy the auth setup profile to a new temp directory to avoid modifying the original auth profile
+ await fs.cp(authSetupProfileDirectory, temporaryProfileDirectory, {
+ recursive: true,
+ });
- const context = await launchPersistentContextWithExtension(
- chromiumChannel,
- temporaryProfileDirectory,
- );
+ const context = await launchPersistentContextWithExtension(
+ chromiumChannel,
+ temporaryProfileDirectory,
+ );
- await use(context);
- await context.close();
- },
- async page({ context, extensionId }, use) {
- // Re-use the initial context page if it exists
- const page = context.pages()[0] || (await context.newPage());
+ await use(context);
+ await context.close();
+ },
+ async page({ context, extensionId }, use) {
+ // Re-use the initial context page if it exists
+ const page = context.pages()[0] || (await context.newPage());
- // Start off test from the extension console, and ensure it is done loading
- const modsPage = new ModsPage(page, extensionId);
- await modsPage.goto();
+ // Start off test from the extension console, and ensure it is done loading
+ const modsPage = new ModsPage(page, extensionId);
+ await modsPage.goto();
- await use(page);
- // The page is closed by the context fixture `.close` cleanup step
- },
- async extensionId({ context }, use) {
- const extensionId = await getExtensionId(context);
- await use(extensionId);
- },
-});
+ await use(page);
+ // The page is closed by the context fixture `.close` cleanup step
+ },
+ async extensionId({ context }, use) {
+ const extensionId = await getExtensionId(context);
+ await use(extensionId);
+ },
+ }),
+);
export const { expect } = test;
diff --git a/end-to-end-tests/tests/telemetry/errors.spec.ts b/end-to-end-tests/tests/telemetry/errors.spec.ts
index 7419d5b8c2..cff34cf956 100644
--- a/end-to-end-tests/tests/telemetry/errors.spec.ts
+++ b/end-to-end-tests/tests/telemetry/errors.spec.ts
@@ -5,6 +5,13 @@ import { type Page, test as base } from "@playwright/test";
import { getBaseExtensionConsoleUrl } from "../../pageObjects/constants";
import { MV } from "../../env";
+test.use({
+ additionalRequiredEnvVariables: [
+ "DATADOG_CLIENT_TOKEN",
+ "DEV_EVENT_TELEMETRY",
+ ],
+});
+
test("can report application error to telemetry service", async ({
page,
context,
diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json
index 3806018536..182122cf49 100644
--- a/src/tsconfig.strictNullChecks.json
+++ b/src/tsconfig.strictNullChecks.json
@@ -9,6 +9,7 @@
"../end-to-end-tests/auth.setup.ts",
"../end-to-end-tests/env.ts",
"../end-to-end-tests/fixtures/extensionBase.ts",
+ "../end-to-end-tests/fixtures/envSetup.ts",
"../end-to-end-tests/pageObjects/constants.ts",
"../end-to-end-tests/pageObjects/extensionConsole/localIntegrationsPage.ts",
"../end-to-end-tests/pageObjects/extensionConsole/modsPage.ts",