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/.eslintrc.js b/.eslintrc.js index 2df1f4d0a1..f7ef12e046 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -143,10 +143,39 @@ module.exports = { { // TODO: consider packaging e2e tests in a mono-repo structure for specific linting rules files: ["end-to-end-tests/**"], // Or *.test.js + extends: "plugin:playwright/recommended", rules: { "no-restricted-imports": "off", "unicorn/prefer-dom-node-dataset": "off", "unicorn/prefer-module": "off", // `import.meta.dirname` throws "cannot use 'import meta' outside a module" + "playwright/no-skipped-test": [ + "error", + { + allowConditional: true, + }, + ], + "playwright/no-wait-for-timeout": "error", + "playwright/no-useless-not": "error", + "playwright/expect-expect": [ + "error", + { assertFunctionNames: ["checkUnavailibilityForNavigationMethod"] }, + ], + "playwright/no-conditional-in-test": "error", + "playwright/no-conditional-expect": "error", + "playwright/no-commented-out-tests": "error", + "playwright/no-hooks": "error", // Use fixtures instead to share common setup / teardown code + "playwright/no-get-by-title": "error", + // Disabled because raw locators are sometimes necessary for specific test scenarios, and we don't want noisy warnings in the IDE + // However, our README encourages writing tests using the recommended built-in locators where possible + // "playwright/no-raw-locators": "warn", + "playwright/prefer-comparison-matcher": "error", + "playwright/prefer-equality-matcher": "error", + "playwright/prefer-strict-equal": "error", + "playwright/prefer-to-be": "error", + "playwright/prefer-to-contain": "error", + "playwright/prefer-to-have-count": "error", + "playwright/prefer-to-have-length": "error", + "playwright/require-to-throw-message": "error", "no-restricted-syntax": [ "error", { diff --git a/.github/workflows/beta-release.yaml b/.github/workflows/beta-release.yaml index e79d9302fc..7aaff91202 100644 --- a/.github/workflows/beta-release.yaml +++ b/.github/workflows/beta-release.yaml @@ -5,12 +5,21 @@ on: inputs: # This is the branch that will trigger the workflow branch: - description: "Branch to build. Should be a release/* branch" + description: "Branch to publish. Should be a release/* branch" required: true + skip_tests: + description: "Skip the end-to-end tests" + required: true + default: false env: # Creates and uploads sourcemaps to Application error telemetry, and save the built extension as an artifact PUBLIC_RELEASE: true + CHROME_MANIFEST_KEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9/BXaFQsYPDxqbVvi11xhWdxfygfrF5YCLiboJooQyTkqIGpGxtI3JF/hkAXDcoqI+N5ATsGPYW34CdOc7uBCU91Ig+gHFiicnkzJaoOBjIwqx452l2/mp7cqNdavtCq40YENkF13ouj5loPwMMYY0L/sSvab+6eO20i1+Ulbsn9onS/fDd16clOaIbUVJ1PhyYvrU0HGVUqW5wUIDLyRezr3aTQLtDIQp/7DTBQ60S2G5KPpAW1UEphnXRLwl6cR5MiYw20OStfTZaA2qpWQvLAQtBoPNjP0Ld6rzI/e3uaC5qUMMCusitKeCA5HOFQDz2IJ0kS8Cn5fxzhXFi6QIDAQAB + MV: 3 + PUBLIC_NAME: -beta + DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }} + DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} jobs: build: @@ -35,11 +44,6 @@ jobs: cache: npm - run: npm ci - run: npm run build - env: - MV: 3 - PUBLIC_NAME: -beta - DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }} - DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }} - run: bash scripts/upload-sourcemaps.sh env: AWS_ACCESS_KEY_ID: ${{ secrets.SOURCEMAP_USER_ID }} @@ -56,7 +60,7 @@ jobs: retention-days: 30 if-no-files-found: error - name: Create production version - run: npx dot-json@1.3.0 dist/manifest.json key 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9/BXaFQsYPDxqbVvi11xhWdxfygfrF5YCLiboJooQyTkqIGpGxtI3JF/hkAXDcoqI+N5ATsGPYW34CdOc7uBCU91Ig+gHFiicnkzJaoOBjIwqx452l2/mp7cqNdavtCq40YENkF13ouj5loPwMMYY0L/sSvab+6eO20i1+Ulbsn9onS/fDd16clOaIbUVJ1PhyYvrU0HGVUqW5wUIDLyRezr3aTQLtDIQp/7DTBQ60S2G5KPpAW1UEphnXRLwl6cR5MiYw20OStfTZaA2qpWQvLAQtBoPNjP0Ld6rzI/e3uaC5qUMMCusitKeCA5HOFQDz2IJ0kS8Cn5fxzhXFi6QIDAQAB' + run: npx dot-json@1.3.0 dist/manifest.json key '${{ env.CHROME_MANIFEST_KEY }}' - name: Save production extension uses: actions/upload-artifact@v4 with: @@ -65,9 +69,61 @@ jobs: retention-days: 30 if-no-files-found: error + end-to-end-tests: + name: end-to-end-tests + if: ${{ github.event.inputs.skip_tests != 'true' }} + timeout-minutes: 60 + runs-on: ubuntu-latest + env: + SHADOW_DOM: open + SERVICE_URL: https://app.pixiebrix.com + 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 + with: + node-version-file: package.json + cache: npm + - name: Install dependencies + run: npm ci + - name: Install playwright browsers + uses: Wandalen/wretry.action@v3.4.0 + with: + command: npx playwright install chrome msedge + with: | + fail_ci_if_error: true + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + # Installing msedge is flaky due to failed checksums from the Microsoft CDN. Unclear why this happens. + # Retry every 15 seconds, for up to 10 minutes + attempt_delay: 15000 + attempt_limit: 40 + - name: Build the extension + run: npm run build:webpack + - name: Run end to end tests + # Xvfb is required to run the tests in headed mode. Headed mode is required to run tests for browser extensions + # in Playwright, see https://playwright.dev/docs/ci#running-headed + run: xvfb-run npm run test:e2e + - uses: actions/upload-artifact@v4 + if: always() + with: + name: end-to-end-tests-report + path: end-to-end-tests/.report + retention-days: 5 + publish: # https://github.com/fregante/chrome-webstore-upload-keys - needs: build + needs: + - build + - end-to-end-tests + # https://stackoverflow.com/a/69354134 + if: | + always() && + needs.build.result == 'success' && + (needs.end-to-end-tests.result == 'success' || needs.end-to-end-tests.result == 'skipped') runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f9f3e4d73..19ff0cc197 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,12 @@ name: CI -on: push +on: [push, pull_request] env: # Creates and uploads sourcemaps to Application error telemetry, and save the built extension as an artifact 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: @@ -191,20 +189,21 @@ jobs: include: - MV: 2 PUBLIC_NAME: "" - CHROME_MANIFEST_KEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhYpgz6Nt3vv5n3d8jyrsWXjkvAxh7bz8WJW05RFrtJx9t0BiVVclO+WpAmhKanB2BiTDw4+Dnlf2lQfTo62LIBnkfTiGzukKqTu3plF0D/Tl/yG1st1xKaQ6dekeThcsgxrFD8+kIUwF4Vq0wPpQ5upl+vf6kX4t9eDev8Eg86mHzUEG/QoS/bu5evN3I1Z0HsiF84VWlrV0b/1GSqpn+dMrFFdcwo2Sn0Ec65nSNfzauDUm5n0NToQ8iYdHkuottREXKJ7/Uy4tO0eMmfokVixbm0i2m9aHEOior5CmNG9X/yGtR2CiM1N4DSEY5mTFu5hPOrALspJ+t7+Is7YnFwIDAQAB - MV: 3 PUBLIC_NAME: "-mv3" - CHROME_MANIFEST_KEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9/BXaFQsYPDxqbVvi11xhWdxfygfrF5YCLiboJooQyTkqIGpGxtI3JF/hkAXDcoqI+N5ATsGPYW34CdOc7uBCU91Ig+gHFiicnkzJaoOBjIwqx452l2/mp7cqNdavtCq40YENkF13ouj5loPwMMYY0L/sSvab+6eO20i1+Ulbsn9onS/fDd16clOaIbUVJ1PhyYvrU0HGVUqW5wUIDLyRezr3aTQLtDIQp/7DTBQ60S2G5KPpAW1UEphnXRLwl6cR5MiYw20OStfTZaA2qpWQvLAQtBoPNjP0Ld6rzI/e3uaC5qUMMCusitKeCA5HOFQDz2IJ0kS8Cn5fxzhXFi6QIDAQAB timeout-minutes: 60 runs-on: ubuntu-latest env: SHADOW_DOM: open SERVICE_URL: https://app.pixiebrix.com MV: ${{ matrix.MV }} - CHROME_MANIFEST_KEY: ${{ matrix.CHROME_MANIFEST_KEY }} + CHROME_MANIFEST_KEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhYpgz6Nt3vv5n3d8jyrsWXjkvAxh7bz8WJW05RFrtJx9t0BiVVclO+WpAmhKanB2BiTDw4+Dnlf2lQfTo62LIBnkfTiGzukKqTu3plF0D/Tl/yG1st1xKaQ6dekeThcsgxrFD8+kIUwF4Vq0wPpQ5upl+vf6kX4t9eDev8Eg86mHzUEG/QoS/bu5evN3I1Z0HsiF84VWlrV0b/1GSqpn+dMrFFdcwo2Sn0Ec65nSNfzauDUm5n0NToQ8iYdHkuottREXKJ7/Uy4tO0eMmfokVixbm0i2m9aHEOior5CmNG9X/yGtR2CiM1N4DSEY5mTFu5hPOrALspJ+t7+Is7YnFwIDAQAB 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 @@ -237,13 +236,9 @@ jobs: name: end-to-end-tests-report${{ matrix.PUBLIC_NAME }} path: end-to-end-tests/.report retention-days: 5 - # Analyzer for checking for inclusive terminology in code. For more information, see - # https://github.com/microsoft/InclusivenessAnalyzer - Inclusiveness-Analyser-scan: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Inclusiveness Analyzer - uses: microsoft/InclusivenessAnalyzer@v1.0.1 + - uses: daun/playwright-report-summary@v3 + with: + comment-title: "Playwright test results - MV${{matrix.MV}}" + report-file: end-to-end-tests/.report/report.json + report-tag: manifest-version-${{ matrix.MV }} + if: (success() || failure()) && github.event_name == 'pull_request' diff --git a/.github/workflows/inclusiveness-analyzer.yaml b/.github/workflows/inclusiveness-analyzer.yaml new file mode 100644 index 0000000000..6a111059e1 --- /dev/null +++ b/.github/workflows/inclusiveness-analyzer.yaml @@ -0,0 +1,13 @@ +name: Inclusiveness Analyzer +on: push +jobs: + # Analyzer for checking for inclusive terminology in code. For more information, see + # https://github.com/microsoft/InclusivenessAnalyzer + Inclusiveness-Analyser-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Inclusiveness Analyzer + uses: microsoft/InclusivenessAnalyzer@v1.0.1 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 0412a62e60..d4e596a1b6 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -9,6 +9,7 @@ on: env: # Creates and uploads sourcemaps to Application error telemetry, and save the built extension as an artifact PUBLIC_RELEASE: true + CHROME_MANIFEST_KEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhYpgz6Nt3vv5n3d8jyrsWXjkvAxh7bz8WJW05RFrtJx9t0BiVVclO+WpAmhKanB2BiTDw4+Dnlf2lQfTo62LIBnkfTiGzukKqTu3plF0D/Tl/yG1st1xKaQ6dekeThcsgxrFD8+kIUwF4Vq0wPpQ5upl+vf6kX4t9eDev8Eg86mHzUEG/QoS/bu5evN3I1Z0HsiF84VWlrV0b/1GSqpn+dMrFFdcwo2Sn0Ec65nSNfzauDUm5n0NToQ8iYdHkuottREXKJ7/Uy4tO0eMmfokVixbm0i2m9aHEOior5CmNG9X/yGtR2CiM1N4DSEY5mTFu5hPOrALspJ+t7+Is7YnFwIDAQAB jobs: build: @@ -25,10 +26,8 @@ jobs: include: - MV: 2 PUBLIC_NAME: "-mv2" - CHROME_MANIFEST_KEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhYpgz6Nt3vv5n3d8jyrsWXjkvAxh7bz8WJW05RFrtJx9t0BiVVclO+WpAmhKanB2BiTDw4+Dnlf2lQfTo62LIBnkfTiGzukKqTu3plF0D/Tl/yG1st1xKaQ6dekeThcsgxrFD8+kIUwF4Vq0wPpQ5upl+vf6kX4t9eDev8Eg86mHzUEG/QoS/bu5evN3I1Z0HsiF84VWlrV0b/1GSqpn+dMrFFdcwo2Sn0Ec65nSNfzauDUm5n0NToQ8iYdHkuottREXKJ7/Uy4tO0eMmfokVixbm0i2m9aHEOior5CmNG9X/yGtR2CiM1N4DSEY5mTFu5hPOrALspJ+t7+Is7YnFwIDAQAB - MV: 3 PUBLIC_NAME: "-mv3" - CHROME_MANIFEST_KEY: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhYpgz6Nt3vv5n3d8jyrsWXjkvAxh7bz8WJW05RFrtJx9t0BiVVclO+WpAmhKanB2BiTDw4+Dnlf2lQfTo62LIBnkfTiGzukKqTu3plF0D/Tl/yG1st1xKaQ6dekeThcsgxrFD8+kIUwF4Vq0wPpQ5upl+vf6kX4t9eDev8Eg86mHzUEG/QoS/bu5evN3I1Z0HsiF84VWlrV0b/1GSqpn+dMrFFdcwo2Sn0Ec65nSNfzauDUm5n0NToQ8iYdHkuottREXKJ7/Uy4tO0eMmfokVixbm0i2m9aHEOior5CmNG9X/yGtR2CiM1N4DSEY5mTFu5hPOrALspJ+t7+Is7YnFwIDAQAB name: build${{ matrix.PUBLIC_NAME }} steps: @@ -60,7 +59,7 @@ jobs: retention-days: 30 if-no-files-found: error - name: Create production version - run: npx dot-json@1.3.0 dist/manifest.json key '${{ matrix.CHROME_MANIFEST_KEY }}' + run: npx dot-json@1.3.0 dist/manifest.json key '${{ env.CHROME_MANIFEST_KEY }}' - name: Save production extension uses: actions/upload-artifact@v4 with: diff --git a/end-to-end-tests/README.md b/end-to-end-tests/README.md index b8d01166e3..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 @@ -42,9 +44,8 @@ Adhere to these principles, based on the [Playwright Best Practices](https://pla - Utilize `test` from `extensionBase.ts` for test environment setup and extension interaction. - Employ page objects from `./end-to-end-tests/pageObjects` for web page interactions. -- Ensure tests are self-contained, handling their own setup and cleanup. - Leverage [Playwright fixtures](https://playwright.dev/docs/test-fixtures) for shared code. -- Rely on Playwright's auto-waiting feature for actions like clicking or typing. +- Ensure tests are self-contained, leveraging [Playwright fixtures](https://playwright.dev/docs/test-fixtures) for shared setup/teardown. +- Use the [recommended built-in locator methods](https://playwright.dev/docs/locators#quick-guide) that have auto-waiting and retry features. When testing mod functionality, use our testing playground website, https://pbx.vercel.app, for a consistent environment. It is configured diff --git a/end-to-end-tests/auth.setup.ts b/end-to-end-tests/auth.setup.ts index 708f7ebfae..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, @@ -34,7 +34,7 @@ test("authenticate", async ({ contextAndPage: { context, page } }) => { page.getByText( "Successfully linked the Browser Extension to your PixieBrix account", ), - { timeout: 10_000 }, + { timeout: 12_000 }, ); await expect(page.getByText(E2E_TEST_USER_EMAIL_UNAFFILIATED)).toBeVisible(); await expect(page.getByText("Admin Console")).toBeVisible(); 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..875e929204 --- /dev/null +++ b/end-to-end-tests/fixtures/envSetup.ts @@ -0,0 +1,43 @@ +/* + * 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) { + // eslint-disable-next-line security/detect-object-injection -- internally controlled + 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/pageObjects/extensionConsole/localIntegrationsPage.ts b/end-to-end-tests/pageObjects/extensionConsole/localIntegrationsPage.ts index 79c2178b7c..94103fb243 100644 --- a/end-to-end-tests/pageObjects/extensionConsole/localIntegrationsPage.ts +++ b/end-to-end-tests/pageObjects/extensionConsole/localIntegrationsPage.ts @@ -40,7 +40,7 @@ export class LocalIntegrationsPage { this.page.getByRole("heading", { name: "Local Integrations" }), ).toBeVisible(); - await expect(this.page.getByTestId("loader")).not.toBeVisible(); + await expect(this.page.getByTestId("loader")).toBeHidden(); } async createNewIntegration(integrationName: string) { diff --git a/end-to-end-tests/pageObjects/pageEditorPage.ts b/end-to-end-tests/pageObjects/pageEditorPage.ts index 69531ea03e..1df54692db 100644 --- a/end-to-end-tests/pageObjects/pageEditorPage.ts +++ b/end-to-end-tests/pageObjects/pageEditorPage.ts @@ -16,11 +16,12 @@ */ import { getBasePageEditorUrl } from "./constants"; -import { type BrowserContext } from "@playwright/test"; +import { type BrowserContext, type Page } from "@playwright/test"; import { expect } from "../fixtures/extensionBase"; export class PageEditorPage { private readonly pageEditorUrl: string; + private page: Page; constructor( private readonly context: BrowserContext, @@ -40,5 +41,10 @@ export class PageEditorPage { name: "Welcome to the Page Editor!", }); await expect(heading).toBeVisible(); + this.page = pageEditorPage; + } + + getTemplateGalleryButton() { + return this.page.getByRole("button", { name: "Launch Template Gallery" }); } } diff --git a/end-to-end-tests/tests/bricks/sidebarEffects.spec.ts b/end-to-end-tests/tests/bricks/sidebarEffects.spec.ts index 30072d7cef..e9adeb533f 100644 --- a/end-to-end-tests/tests/bricks/sidebarEffects.spec.ts +++ b/end-to-end-tests/tests/bricks/sidebarEffects.spec.ts @@ -32,7 +32,6 @@ test.describe("sidebar effect bricks", () => { await page.goto("/"); // Ensure the page is focused by clicking on an element before running the keyboard shortcut, see runModViaQuickbar - await page.getByText("Index of /").click(); await runModViaQuickBar(page, "Toggle Sidebar"); // Will error if page/frame not available diff --git a/end-to-end-tests/tests/extensionConsoleActivation.spec.ts b/end-to-end-tests/tests/extensionConsoleActivation.spec.ts index 234b0c89e1..a445135ca8 100644 --- a/end-to-end-tests/tests/extensionConsoleActivation.spec.ts +++ b/end-to-end-tests/tests/extensionConsoleActivation.spec.ts @@ -22,6 +22,8 @@ import { test as base } from "@playwright/test"; import { getSidebarPage, runModViaQuickBar } 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"; test("can activate a mod with no config options", async ({ page, @@ -50,19 +52,19 @@ test("can activate a mod with built-in integration", async ({ extensionId, context, }) => { + test.skip(MV === "2", "Service worker request mocking only available in MV3"); + const modId = "@pixies/giphy/giphy-search"; + let giphyRequestPostData: Serializable; // The giphy search request is proxied through the PixieBrix server, which is kicked off in the background/service // worker. Playwright experimentally supports mocking service worker requests, see // https://playwright.dev/docs/service-workers-experimental#routing-service-worker-requests-only await context.route("https://app.pixiebrix.com/api/proxy/", async (route) => { if (route.request().serviceWorker()) { // Ensure the mod was properly activated with the built-in integration configuration - expect(route.request().postDataJSON()).toMatchObject({ - url: "https://api.giphy.com/v1/gifs/search", - auth_id: expect.stringMatching(VALID_UUID_REGEX), - service_id: "@pixies/giphy/giphy-service", - }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Serializable is any + giphyRequestPostData = route.request().postDataJSON(); return route.fulfill({ path: path.join(__dirname, "../fixtures/responses/giphy-search.json"), @@ -81,8 +83,11 @@ test("can activate a mod with built-in integration", async ({ await modActivationPage.clickActivateAndWaitForModsPageRedirect(); await page.goto("/"); - // Ensure the page is focused by clicking on an element before running the keyboard shortcut, see runModViaQuickbar - await page.getByText("Index of /").click(); + // Ensure the QuickBar is ready + await expect( + page.getByRole("button", { name: "open the PixieBrix quick bar" }), + ).toBeVisible(); + await runModViaQuickBar(page, "GIPHY Search"); // Search for "kitten" keyword @@ -95,4 +100,9 @@ test("can activate a mod with built-in integration", async ({ await expect( sidebarPage.getByRole("heading", { name: 'GIPHY Results for "kitten"' }), ).toBeVisible(); + expect(giphyRequestPostData).toMatchObject({ + url: "https://api.giphy.com/v1/gifs/search", + auth_id: expect.stringMatching(VALID_UUID_REGEX), + service_id: "@pixies/giphy/giphy-service", + }); }); diff --git a/end-to-end-tests/tests/regressions/formFlicker.spec.ts b/end-to-end-tests/tests/regressions/formFlicker.spec.ts index 9de5f116ea..ed81c90463 100644 --- a/end-to-end-tests/tests/regressions/formFlicker.spec.ts +++ b/end-to-end-tests/tests/regressions/formFlicker.spec.ts @@ -41,17 +41,25 @@ test.describe("forms flickering due to components unexpectedly unmounting/remoun await page.goto("/bootstrap-5"); - await page.getByRole("heading", { name: "PixieBrix" }).click(); await runModViaQuickBar(page, "Open Sidebar"); const sideBarPage = await getSidebarPage(page, extensionId); + await expect(sideBarPage.getByTestId("container").nth(1)).toContainText( + "Welcome to the Snippet Manager!", + ); + const input = sideBarPage.getByPlaceholder("Search snippets"); await input.click(); // Add delay to give time for the the input to lose focus - await input.pressSequentially("abc", { delay: 100 }); + await input.pressSequentially("lor", { delay: 100 }); // Validate that the input value was set correctly - await expect(input).toHaveValue("abc"); + await expect(input).toHaveValue("lor"); + + // Verify that the input value filtered the list + await expect(sideBarPage.getByTestId("container").nth(1)).toContainText( + "Lorem ipsum dolor sit amet, ", + ); }); }); diff --git a/end-to-end-tests/tests/runtime/insertAtCursor.spec.ts b/end-to-end-tests/tests/runtime/insertAtCursor.spec.ts index d402c731e9..4328212761 100644 --- a/end-to-end-tests/tests/runtime/insertAtCursor.spec.ts +++ b/end-to-end-tests/tests/runtime/insertAtCursor.spec.ts @@ -20,11 +20,11 @@ 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 { test as base } from "@playwright/test"; import { + conditionallyHoverOverMV2Sidebar, ensureVisibility, getSidebarPage, waitForSelectionMenuReadiness, } from "../../utils"; -import { MV } from "../../env"; test.describe("Insert at Cursor", () => { test("8157: can insert at cursor from side bar", async ({ @@ -82,12 +82,8 @@ test.describe("Insert at Cursor", () => { await editor.click(); - if (MV === "2") { - // Need to simulate the mouse entering the sidebar to track focus on MV2 - // https://github.com/pixiebrix/pixiebrix-extension/blob/1794863937f343fbc8e3a4434eace74191f8dfbd/src/contentScript/sidebarController.tsx#L563-L563 - const sidebarFrame = page.locator("#pixiebrix-extension"); - await sidebarFrame.dispatchEvent("mouseenter"); - } + // Need to simulate the mouse entering the sidebar to track focus on MV2 + await conditionallyHoverOverMV2Sidebar(page); await sideBarPage.getByRole("button", { name: "Insert at Cursor" }).click(); diff --git a/end-to-end-tests/tests/runtime/setInputValue.spec.ts b/end-to-end-tests/tests/runtime/setInputValue.spec.ts index d36327fe2b..2aa8d122a1 100644 --- a/end-to-end-tests/tests/runtime/setInputValue.spec.ts +++ b/end-to-end-tests/tests/runtime/setInputValue.spec.ts @@ -19,8 +19,7 @@ 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 { test as base } from "@playwright/test"; -import { getSidebarPage } from "../../utils"; -import { MV } from "../../env"; +import { conditionallyHoverOverMV2Sidebar, getSidebarPage } from "../../utils"; test("can set input value", async ({ page, extensionId }) => { const modId = "@pixies/test/field-set-value"; @@ -78,12 +77,8 @@ test("can set input value", async ({ page, extensionId }) => { await editor.click(); - if (MV === "2") { - // Need to simulate the mouse entering the sidebar to track focus on MV2 - // https://github.com/pixiebrix/pixiebrix-extension/blob/1794863937f343fbc8e3a4434eace74191f8dfbd/src/contentScript/sidebarController.tsx#L563-L563 - const sidebarFrame = page.locator("#pixiebrix-extension"); - await sidebarFrame.dispatchEvent("mouseenter"); - } + // Need to simulate the mouse entering the sidebar to track focus on MV2 + await conditionallyHoverOverMV2Sidebar(page); await editor.click(); await editor.pressSequentially("abc "); diff --git a/end-to-end-tests/tests/runtime/sidebarController.spec.ts b/end-to-end-tests/tests/runtime/sidebarController.spec.ts new file mode 100644 index 0000000000..2022281555 --- /dev/null +++ b/end-to-end-tests/tests/runtime/sidebarController.spec.ts @@ -0,0 +1,78 @@ +/* + * 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, 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 } from "@playwright/test"; +import { getSidebarPage, isSidebarOpen } from "../../utils"; +import { MV } from "../../env"; + +test.describe("sidebar controller", () => { + test("can open sidebar immediately from iframe without focus dialog", async ({ + page, + extensionId, + }) => { + const modId = "@pixies/test/frame-sidebar-actions"; + + const modActivationPage = new ActivateModPage(page, extensionId, modId); + await modActivationPage.goto(); + await modActivationPage.clickActivateAndWaitForModsPageRedirect(); + + await page.goto("/frames-builder.html"); + + const frame = page.frameLocator("iframe"); + await frame.getByRole("link", { name: "Show Sidebar Immediately" }).click(); + + // Don't use getSidebarPage because it automatically clicks the MV3 focus dialog. + await expect(() => { + expect(isSidebarOpen(page, extensionId)).toBe(false); + }).toPass({ timeout: 5000 }); + }); + + test("shows focus dialog in top-level frame", async ({ + page, + extensionId, + }) => { + test.skip(MV === "2", "This test is only relevant for MV3"); + + const modId = "@pixies/test/frame-sidebar-actions"; + + const modActivationPage = new ActivateModPage(page, extensionId, modId); + await modActivationPage.goto(); + await modActivationPage.clickActivateAndWaitForModsPageRedirect(); + + await page.goto("/frames-builder.html"); + + const frame = page.frameLocator("iframe"); + + // Mod waits 7.5 seconds before running Show Sidebar brick to ensure the user gesture dialog is shown + await frame.getByRole("link", { name: "Show Sidebar after Wait" }).click(); + // eslint-disable-next-line playwright/no-wait-for-timeout -- match wait in the mod + await page.waitForTimeout(8000); + + // Focus dialog should be visible in the top-level frame + await expect(page.getByRole("button", { name: "OK" })).toBeVisible(); + + // The focus dialog should not be shown in the iframe. Check after checking the top-level frame + // because it's a positive check for the dialog being shown. + await expect(frame.getByRole("button", { name: "OK" })).toBeHidden(); + + // Will error if page/frame not available + await getSidebarPage(page, extensionId); + }); +}); diff --git a/end-to-end-tests/tests/runtime/sidebarNavigation.spec.ts b/end-to-end-tests/tests/runtime/sidebarNavigation.spec.ts index 0d5eebe966..5f24525b73 100644 --- a/end-to-end-tests/tests/runtime/sidebarNavigation.spec.ts +++ b/end-to-end-tests/tests/runtime/sidebarNavigation.spec.ts @@ -18,11 +18,11 @@ 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 { Page, test as base } from "@playwright/test"; +import { type Page, test as base } from "@playwright/test"; import { getSidebarPage, runModViaQuickBar } from "../../utils"; import { MV, SERVICE_URL } from "../../env"; -test("sidebar is persistent during navigation", async ({ +test("sidebar mod panels are persistent during navigation", async ({ page, extensionId, }) => { @@ -38,8 +38,6 @@ test("sidebar is persistent during navigation", async ({ await page.goto("/"); - // Ensure the page is focused by clicking on an element before running the keyboard shortcut, see runModViaQuickbar - await page.getByText("Index of /").click(); await runModViaQuickBar(page, "Open Sidebar"); const sideBarPage = (await getSidebarPage(page, extensionId)) as Page; // MV3 sidebar is a separate page @@ -88,7 +86,7 @@ test("sidebar is persistent during navigation", async ({ // Sidebar 1 tab is hidden since it is not enabled in this page. await expect( sideBarPage.getByRole("tab", { name: "Test sidebar 1" }), - ).not.toBeVisible(); + ).toBeHidden(); await expect( sideBarPage.getByRole("tab", { name: "Test sidebar 2" }), ).toBeVisible(); @@ -108,3 +106,133 @@ test("sidebar is persistent during navigation", async ({ expect(sideBarPageClosed).toBe(true); }).toPass({ timeout: 5000 }); }); + +const navigationMethods: Array<{ + name: string; + navigationMethod: (page: Page) => Promise; +}> = [ + { + name: "refresh", + async navigationMethod(page: Page) { + await page.reload(); + }, + }, + { + name: "back button", + async navigationMethod(page: Page) { + await page.goBack(); + await page.goBack(); + }, + }, + { + name: "goto new page", + async navigationMethod(page: Page) { + await page.goto(SERVICE_URL); + }, + }, +]; + +// Helper method for checking that the sidebar panels are unavailable after a navigation method +async function checkUnavailibilityForNavigationMethod( + page: Page, + extensionId: string, + navigationMethod: (page: Page) => Promise, +) { + await page.goto("/advanced-fields"); + await runModViaQuickBar(page, "Open form"); + + const sideBarPage = (await getSidebarPage(page, extensionId)) as Page; // MV3 sidebar is a separate page + // Set up close listener for sidebar page + let sideBarPageClosed = false; + sideBarPage.on("close", () => { + sideBarPageClosed = true; + }); + + await expect( + sideBarPage + .frameLocator("iframe") + .getByRole("heading", { name: "Example Form" }), + ).toBeVisible(); + await expect( + sideBarPage.getByRole("tab", { name: "Example form" }), + ).toBeVisible(); + + await runModViaQuickBar(page, "Open temp panel"); + await expect( + sideBarPage.getByRole("heading", { name: "Example document" }), + ).toBeVisible(); + await expect( + sideBarPage.getByRole("tab", { name: "Example info" }), + ).toBeVisible(); + + // Click on "contentEditable" header, which updates the url to .../#contenteditable + await page.getByRole("link", { name: "contentEditable" }).click(); + expect(page.url()).toBe( + "https://pbx.vercel.app/advanced-fields/#contenteditable", + ); + // Should not cause the temporary panel to become unavailable + await expect( + sideBarPage + .getByLabel("Example Info") + .getByText("Panel no longer available"), + ).toBeHidden(); + + await navigationMethod(page); + + await expect( + sideBarPage + .getByLabel("Example Info") + .getByText("Panel no longer available"), + ).toBeVisible(); + await sideBarPage + .getByLabel("Example Info") + .getByLabel("Close the unavailable panel") + .click(); + await expect( + sideBarPage.getByRole("tab", { name: "Example info" }), + ).toBeHidden(); + + // The unavailable overlay is still displayed for the form panel + await expect( + sideBarPage + .getByLabel("Example form") + .getByText("Panel no longer available"), + ).toBeVisible(); + await sideBarPage + .getByLabel("Example form") + .getByLabel("Close the unavailable panel") + .click(); + + // Closing the last panel should close the sidebar + await expect(() => { + expect(sideBarPageClosed).toBe(true); + }).toPass({ timeout: 5000 }); +} + +test("sidebar form and temporary panels are unavailable after navigation", async ({ + page, + extensionId, +}) => { + test.skip(MV === "2", "Navigation is not supported for MV2 sidebar"); + // This mod has two quickbar actions for opening a temporary panel and a form panel in the sidebar. + const modId = "@e2e-testing/temp-panel-unavailable-on-navigation"; + + const modActivationPage = new ActivateModPage(page, extensionId, modId); + await modActivationPage.goto(); + + await modActivationPage.clickActivateAndWaitForModsPageRedirect(); + + // Prime the browser history with an initial navigation + await page.goto(SERVICE_URL); + + for (const { navigationMethod, name } of navigationMethods) { + // eslint-disable-next-line no-await-in-loop -- check each navigation method sequentially + await test.step(`Checking navigation method: ${name}`, async () => { + await checkUnavailibilityForNavigationMethod( + page, + extensionId, + navigationMethod, + ); + }); + } +}); diff --git a/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts new file mode 100644 index 0000000000..74360a015b --- /dev/null +++ b/end-to-end-tests/tests/runtime/sidebarPanelTheme.spec.ts @@ -0,0 +1,54 @@ +/* + * 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, expect } from "../../fixtures/extensionBase"; +import { ActivateModPage } from "../../pageObjects/extensionConsole/modsPage"; +import { getSidebarPage, runModViaQuickBar } from "../../utils"; +import type { Page } from "@playwright/test"; + +test("custom sidebar theme css file is applied to all levels of sidebar document", async ({ + page, + extensionId, +}) => { + const modId = "@pixies/testing/panel-theme"; + + const modActivationPage = new ActivateModPage(page, extensionId, modId); + await modActivationPage.goto(); + + await modActivationPage.clickActivateAndWaitForModsPageRedirect(); + + await page.goto("/"); + + // Ensure the page is focused by clicking on an element before running the keyboard shortcut, see runModViaQuickbar + await page.getByText("Index of /").click(); + await runModViaQuickBar(page, "Show Sidebar"); + + const sidebarPage = (await getSidebarPage(page, extensionId)) as Page; + await expect( + sidebarPage.getByText("#8347: Theme Inheritance", { exact: true }), + ).toBeVisible(); + + const green = "rgb(0, 128, 0)"; + const elementsThatShouldBeGreen = await sidebarPage + .getByText("This should be green") + .all(); + await Promise.all( + elementsThatShouldBeGreen.map(async (element) => + expect(element).toHaveCSS("color", green), + ), + ); +}); diff --git a/end-to-end-tests/tests/smoke/pageEditor.spec.ts b/end-to-end-tests/tests/smoke/pageEditor.spec.ts index df35076205..81ab3b3072 100644 --- a/end-to-end-tests/tests/smoke/pageEditor.spec.ts +++ b/end-to-end-tests/tests/smoke/pageEditor.spec.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { test } from "../../fixtures/extensionBase"; +import { test, expect } from "../../fixtures/extensionBase"; // @ts-expect-error -- https://youtrack.jetbrains.com/issue/AQUA-711/Provide-a-run-configuration-for-Playwright-tests-in-specs-with-fixture-imports-only import { test as base } from "@playwright/test"; import { PageEditorPage } from "../../pageObjects/pageEditorPage"; @@ -30,5 +30,6 @@ test.describe("page editor smoke test", () => { const pageEditorPage = new PageEditorPage(context, page.url(), extensionId); await pageEditorPage.goto(); + await expect(pageEditorPage.getTemplateGalleryButton()).toBeVisible(); }); }); diff --git a/end-to-end-tests/tests/telemetry/errors.spec.ts b/end-to-end-tests/tests/telemetry/errors.spec.ts index 7419d5b8c2..9209bc1b2b 100644 --- a/end-to-end-tests/tests/telemetry/errors.spec.ts +++ b/end-to-end-tests/tests/telemetry/errors.spec.ts @@ -1,10 +1,44 @@ import { test, expect } from "../../fixtures/extensionBase"; -import { type Request } from "playwright-core"; // @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 } from "@playwright/test"; +import { type BrowserContext, type Page, test as base } from "@playwright/test"; import { getBaseExtensionConsoleUrl } from "../../pageObjects/constants"; import { MV } from "../../env"; +async function waitForBackgroundPageRequest( + context: BrowserContext, + extensionId: string, + errorServiceEndpoint: string, +) { + if (MV === "3") { + // Due to service worker limitations with the Datadog SDK, we need to report errors via an offscreen document + // (see https://github.com/pixiebrix/pixiebrix-extension/issues/8268). The offscreen document is created when + // the first error is reported, so we need to wait for it to be created before we can interact with it. + let offscreenPage: Page | undefined; + await expect(async () => { + offscreenPage = context + .pages() + .find((value) => + value + .url() + .startsWith(`chrome-extension://${extensionId}/offscreen.html`), + ); + + expect(offscreenPage?.url()).toBeDefined(); + }).toPass({ timeout: 5000 }); + return offscreenPage?.waitForRequest(errorServiceEndpoint); + } + + const backgroundPage = context.backgroundPages()[0]; + return backgroundPage?.waitForRequest(errorServiceEndpoint); +} + +test.use({ + additionalRequiredEnvVariables: [ + "DATADOG_CLIENT_TOKEN", + "DEV_EVENT_TELEMETRY", + ], +}); + test("can report application error to telemetry service", async ({ page, context, @@ -32,32 +66,14 @@ test("can report application error to telemetry service", async ({ await page.goto(getBaseExtensionConsoleUrl(extensionId)); await expect(page.getByText("Something went wrong.")).toBeVisible(); - let waitForRequest: Promise | undefined; - if (MV === "3") { - // Due to service worker limitations with the Datadog SDK, we need to report errors via an offscreen document - // (see https://github.com/pixiebrix/pixiebrix-extension/issues/8268). The offscreen document is created when - // the first error is reported, so we need to wait for it to be created before we can interact with it. - let offscreenPage: Page | undefined; - await expect(async () => { - offscreenPage = context - .pages() - .find((value) => - value - .url() - .startsWith(`chrome-extension://${extensionId}/offscreen.html`), - ); - - expect(offscreenPage?.url()).toBeDefined(); - }).toPass({ timeout: 5000 }); - waitForRequest = offscreenPage?.waitForRequest(errorServiceEndpoint); - } else { - const backgroundPage = context.backgroundPages()[0]; - waitForRequest = backgroundPage?.waitForRequest(errorServiceEndpoint); - } - // TODO: due to Datadog SDK implementation, it will take ~30 seconds for the // request to be sent. We should figure out a way to induce the request to be sent sooner. - const request = await waitForRequest; + const request = await waitForBackgroundPageRequest( + context, + extensionId, + errorServiceEndpoint, + ); + const errorLogsJson = request ?.postData() ?.split("\n") diff --git a/end-to-end-tests/utils.ts b/end-to-end-tests/utils.ts index 6df9a78c8d..aafe70b0ab 100644 --- a/end-to-end-tests/utils.ts +++ b/end-to-end-tests/utils.ts @@ -53,7 +53,7 @@ export function checkForCriticalViolations( } // Expectation only fails if there are any criticalViolations that aren't explicitly allowed - expect(unallowedViolations).toEqual([]); + expect(unallowedViolations).toStrictEqual([]); } // This function is a workaround for the fact that `expect(locator).toBeVisible()` will immediately fail if the element is hidden or unmounted. @@ -68,11 +68,14 @@ export async function ensureVisibility( } // Run a mod via the Quickbar. -// NOTE: Page needs to be focused before running this function, e.g. by clicking on the page. -// TODO: Fix the page-focus precondition by generalizing the page-focusing logic to be page-agnostic 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 + // 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); await page.getByRole("option", { name: modName }).click(); } @@ -155,9 +158,26 @@ export async function getSidebarPage( // see: https://github.com/pixiebrix/pixiebrix-extension/blob/5693a4db1c4f3411910ef9cf6a60f5a20c132761/src/contentScript/textSelectionMenu/selectionMenuController.tsx#L336 export async function waitForSelectionMenuReadiness(page: Page) { await expect(async () => { - const pbReady = await page - .locator("html") - .getAttribute("data-pb-selection-menu-ready"); - expect(pbReady).toBeTruthy(); + await expect(page.locator("html")).toHaveAttribute( + "data-pb-selection-menu-ready", + ); }).toPass({ timeout: 5000 }); } + +// Waits for the quick bar to be ready to use +async function waitForQuickBarReadiness(page: Page) { + await expect(async () => { + await expect(page.locator("html")).toHaveAttribute( + "data-pb-quick-bar-ready", + ); + }).toPass({ timeout: 5000 }); +} + +// Simulates mouse entering the sidebar to track focus on MV2 +// https://github.com/pixiebrix/pixiebrix-extension/blob/1794863937f343fbc8e3a4434eace74191f8dfbd/src/contentScript/sidebarController.tsx#L563-L563 +export async function conditionallyHoverOverMV2Sidebar(page: Page) { + if (MV === "2") { + const sidebarFrame = page.locator("#pixiebrix-extension"); + await sidebarFrame.dispatchEvent("mouseenter"); + } +} diff --git a/package-lock.json b/package-lock.json index af06269610..6ce0bbe78a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,10 @@ "license": "AGPL-3.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", - "@atlaskit/tree": "^8.8.8", + "@atlaskit/tree": "^8.8.9", "@cfworker/json-schema": "^1.12.8", - "@datadog/browser-logs": "^5.15.0", - "@datadog/browser-rum": "^5.15.0", + "@datadog/browser-logs": "^5.16.0", + "@datadog/browser-rum": "^5.16.0", "@floating-ui/dom": "^1.6.3", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "^5.15.4", @@ -23,14 +23,14 @@ "@mozilla/readability": "^0.5.0", "@pixiebrix/jq-web": "^0.5.1", "@reduxjs/toolkit": "^1.9.7", - "@rjsf/bootstrap-4": "^5.18.2", - "@rjsf/core": "^5.18.2", - "@rjsf/utils": "^5.18.2", + "@rjsf/bootstrap-4": "^5.18.3", + "@rjsf/core": "^5.18.3", + "@rjsf/utils": "^5.18.3", "@uipath/robot": "1.3.1", "@vespaiach/axios-fetch-adapter": "^0.3.1", "@xobotyi/scrollbar-width": "^1.9.5", "abort-utils": "^1.2.0", - "ace-builds": "^1.33.0", + "ace-builds": "^1.33.1", "autocompleter": "^9.2.1", "axios": "^0.27.2", "batched-function": "^2.0.1", @@ -46,7 +46,7 @@ "csharp-helpers": "^0.9.3", "css-selector-generator": "^3.6.7", "date-fns": "^3.6.0", - "dompurify": "^3.1.0", + "dompurify": "^3.1.1", "downloadjs": "^1.4.7", "exifreader": "^4.22.1", "export-to-csv": "^1.2.4", @@ -64,7 +64,7 @@ "htmlparser2": "^9.1.0", "http-status-codes": "^2.3.0", "idb": "^8.0.0", - "iframe-resizer": "^4.3.10", + "iframe-resizer": "^4.3.11", "immer": "^9.0.18", "intrinsic-scale": "^4.0.0", "intro.js": "^7.2.0", @@ -134,7 +134,7 @@ "urlpattern-polyfill": "^5.0.3", "use-async-effect": "^2.2.7", "use-debounce": "^10.0.0", - "use-sync-external-store": "^1.2.0", + "use-sync-external-store": "^1.2.2", "uuid": "^9.0.1", "webext-content-scripts": "^2.6.1", "webext-detect-page": "^5.0.0", @@ -155,7 +155,7 @@ "@axe-core/playwright": "^4.9.0", "@fortawesome/fontawesome-common-types": "^0.2.36", "@playwright/test": "^1.43.1", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.13", "@shopify/jest-dom-mocks": "^5.0.0", "@sindresorhus/tsconfig": "^5.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -166,14 +166,14 @@ "@storybook/react": "^7.6.17", "@storybook/react-webpack5": "^7.6.17", "@svgr/webpack": "^8.1.0", - "@swc/core": "^1.4.16", + "@swc/core": "^1.4.17", "@swc/jest": "^0.2.36", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", "@total-typescript/ts-reset": "^0.5.1", - "@types/chrome": "^0.0.266", + "@types/chrome": "^0.0.267", "@types/dom-navigation": "^1.0.3", "@types/dompurify": "^3.0.5", "@types/downloadjs": "^1.4.6", @@ -220,7 +220,7 @@ "@types/webpack": "^5.28.5", "@types/webpack-env": "^1.18.4", "@types/whatwg-mimetype": "^3.0.2", - "@typescript-eslint/rule-tester": "^7.7.0", + "@typescript-eslint/rule-tester": "^7.8.0", "axios-mock-adapter": "^1.22.0", "blob-polyfill": "^7.0.20220408", "compass-mixins": "^0.12.10", @@ -234,16 +234,17 @@ "eslint": "^8.57.0", "eslint-config-pixiebrix": "^0.38.0", "eslint-plugin-local-rules": "^2.0.1", + "eslint-plugin-playwright": "^1.6.0", "fake-indexeddb": "^5.0.2", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-extended": "^4.0.2", "jest-location-mock": "^2.0.0", - "jest-webextension-mock": "^3.8.9", + "jest-webextension-mock": "^3.8.16", "jsdom": "^24.0.0", "jsdom-testing-mocks": "^1.13.0", - "knip": "^5.9.4", + "knip": "^5.11.0", "mini-css-extract-plugin": "^2.6.1", "mockdate": "^3.0.5", "msw": "^1.3.3", @@ -260,7 +261,7 @@ "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.5.1", - "type-fest": "^4.15.0", + "type-fest": "^4.18.0", "typescript": "^5.4.5", "typescript-plugin-css-modules": "^5.1.0", "webpack": "^5.91.0", @@ -339,9 +340,9 @@ } }, "node_modules/@atlaskit/tree": { - "version": "8.8.8", - "resolved": "https://registry.npmjs.org/@atlaskit/tree/-/tree-8.8.8.tgz", - "integrity": "sha512-3a0aQteJvDGbGPNm7+v7ori5cZv14K1Hy0OiQCqjfRKDPP9FSYJSiywNy2SISc3xClWwlAVUsE/fRR0HrzSMDA==", + "version": "8.8.9", + "resolved": "https://registry.npmjs.org/@atlaskit/tree/-/tree-8.8.9.tgz", + "integrity": "sha512-po2rxb1BktzLGQUDYK0ROSPR59ZoQ36HKEH1SNnPtNaHKHluc1XHVKaBym+bJvZCf7nVttqurb6enbQLZg81NA==", "dependencies": { "@babel/runtime": "^7.0.0", "css-box-model": "^1.2.0", @@ -2494,19 +2495,19 @@ } }, "node_modules/@datadog/browser-core": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-5.15.0.tgz", - "integrity": "sha512-vZeHK0aEqyQqJOal8skaGrGi788Tt+MSP+pH/+J56/Bj4tjLggeIw2SPN+j7XSS+3Q5atk1ZRoEMwvHVV7/s7Q==" + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-core/-/browser-core-5.16.0.tgz", + "integrity": "sha512-8SXjJ034T15ihdYU/9cC1UBKr8aUVAAW8MsL0imll8imjBYo95ZCovbf6e24iZ7hYCdjk26+w3WpHQkARrUNsw==" }, "node_modules/@datadog/browser-logs": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@datadog/browser-logs/-/browser-logs-5.15.0.tgz", - "integrity": "sha512-H+ZXAKSVYgcNf7ntiyoYYWlp36s71LTctkRzK/HpWubwmud2yy/hoeMhY0hWV32qSPEGXrlcxOyw6G8IhVEDsQ==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-logs/-/browser-logs-5.16.0.tgz", + "integrity": "sha512-9F57SQ1nj67JK1mST9NKUlIAwReIrfEJK6C8LdRzoBA9F4BVVZyQfSbjj/GhMJmdODNP+KWezfGvFflJ58WnJA==", "dependencies": { - "@datadog/browser-core": "5.15.0" + "@datadog/browser-core": "5.16.0" }, "peerDependencies": { - "@datadog/browser-rum": "5.15.0" + "@datadog/browser-rum": "5.16.0" }, "peerDependenciesMeta": { "@datadog/browser-rum": { @@ -2515,15 +2516,15 @@ } }, "node_modules/@datadog/browser-rum": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-5.15.0.tgz", - "integrity": "sha512-bvY8Pgb92uB9+x7oVQTLq5P/fyMBTn+xoEzYJbMmJCEhKL1QPqtYdiRqTNhOLnemZFJ5OaaFNg9az68xJ/O0gg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum/-/browser-rum-5.16.0.tgz", + "integrity": "sha512-AlKKylyILojF6Hx0Sf/HWbjpHo5BLMIPIrekC5fDNrtD7iK3EYaEIe1XNw4MbZ3RLuYVlqOFXIXdnPCvo5/7nQ==", "dependencies": { - "@datadog/browser-core": "5.15.0", - "@datadog/browser-rum-core": "5.15.0" + "@datadog/browser-core": "5.16.0", + "@datadog/browser-rum-core": "5.16.0" }, "peerDependencies": { - "@datadog/browser-logs": "5.15.0" + "@datadog/browser-logs": "5.16.0" }, "peerDependenciesMeta": { "@datadog/browser-logs": { @@ -2532,11 +2533,11 @@ } }, "node_modules/@datadog/browser-rum-core": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-5.15.0.tgz", - "integrity": "sha512-f8TghTfjEqEo/AexyfM0OCZ3aUOcN6+xg9XRohVC+kmmboo+7GcMLgNfW9zkEoCJvNOheSFN2xPZbNE6iUD3LA==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/@datadog/browser-rum-core/-/browser-rum-core-5.16.0.tgz", + "integrity": "sha512-avG+PRdJuMAAlGu1Ciwru18m/sKGik+XnwujnHMXf85+eSIngMwFlHFTZHDu24F6xq8WaxL53G7cVIGJBx/OUg==", "dependencies": { - "@datadog/browser-core": "5.15.0" + "@datadog/browser-core": "5.16.0" } }, "node_modules/@discoveryjs/json-ext": { @@ -4409,255 +4410,6 @@ "node": ">= 8" } }, - "node_modules/@npmcli/git": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz", - "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==", - "dev": true, - "dependencies": { - "@npmcli/promise-spawn": "^7.0.0", - "lru-cache": "^10.0.1", - "npm-pick-manifest": "^9.0.0", - "proc-log": "^3.0.0", - "promise-inflight": "^1.0.1", - "promise-retry": "^2.0.1", - "semver": "^7.3.5", - "which": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/git/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/git/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/map-workspaces": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-3.0.4.tgz", - "integrity": "sha512-Z0TbvXkRbacjFFLpVpV0e2mheCh+WzQpcqL+4xp49uNJOxOnIAPZyXtUxZ5Qn3QBTGKA11Exjd9a5411rBrhDg==", - "dev": true, - "dependencies": { - "@npmcli/name-from-folder": "^2.0.0", - "glob": "^10.2.2", - "minimatch": "^9.0.0", - "read-package-json-fast": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/map-workspaces/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@npmcli/map-workspaces/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/map-workspaces/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/name-from-folder": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-2.0.0.tgz", - "integrity": "sha512-pwK+BfEBZJbKdNYpHHRTNBwBoqrN/iIMO0AiGvYsp3Hoaq0WbgGSWQR6SCldZovoDpY3yje5lkFUe6gsDgJ2vg==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.0.tgz", - "integrity": "sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==", - "dev": true, - "dependencies": { - "@npmcli/git": "^5.0.0", - "glob": "^10.2.2", - "hosted-git-info": "^7.0.0", - "json-parse-even-better-errors": "^3.0.0", - "normalize-package-data": "^6.0.0", - "proc-log": "^3.0.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", - "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.3.5", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", - "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/json-parse-even-better-errors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", - "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/normalize-package-data": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", - "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", - "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", - "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", - "dev": true, - "dependencies": { - "which": "^4.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, - "engines": { - "node": ">=16" - } - }, - "node_modules/@npmcli/promise-spawn/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, "node_modules/@one-ini/wasm": { "version": "0.1.1", "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==" @@ -4748,16 +4500,14 @@ } }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.11.tgz", - "integrity": "sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==", + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.13.tgz", + "integrity": "sha512-odZVYXly+JwzYri9rKqqUAk0cY6zLpv4dxoKinhoJNShV36Gpxf+CyDIILJ4tYsJ1ZxIWs233Y39iVnynvDA/g==", "dev": true, "dependencies": { "ansi-html-community": "^0.0.8", - "common-path-prefix": "^3.0.0", "core-js-pure": "^3.23.3", "error-stack-parser": "^2.0.6", - "find-up": "^5.0.0", "html-entities": "^2.1.0", "loader-utils": "^2.0.4", "schema-utils": "^3.0.0", @@ -4772,7 +4522,7 @@ "sockjs-client": "^1.4.0", "type-fest": ">=0.17.0 <5.0.0", "webpack": ">=4.43.0 <6.0.0", - "webpack-dev-server": "3.x || 4.x", + "webpack-dev-server": "3.x || 4.x || 5.x", "webpack-hot-middleware": "2.x", "webpack-plugin-serve": "0.x || 1.x" }, @@ -4806,326 +4556,6 @@ "node": ">= 8" } }, - "node_modules/@pnpm/constants": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@pnpm/constants/-/constants-7.1.1.tgz", - "integrity": "sha512-31pZqMtjwV+Vaq7MaPrT1EoDFSYwye3dp6BiHIGRJmVThCQwySRKM7hCvqqI94epNkqFAAYoWrNynWoRYosGdw==", - "dev": true, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/core-loggers": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/@pnpm/core-loggers/-/core-loggers-9.0.6.tgz", - "integrity": "sha512-iK67SGbp+06bA/elpg51wygPFjNA7JKHtKkpLxqXXHw+AjFFBC3f2OznJsCIuDK6HdGi5UhHLYqo5QxJ2gMqJQ==", - "dev": true, - "dependencies": { - "@pnpm/types": "9.4.2" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - }, - "peerDependencies": { - "@pnpm/logger": "^5.0.0" - } - }, - "node_modules/@pnpm/error": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/error/-/error-5.0.2.tgz", - "integrity": "sha512-0TEm+tWNYm+9uh6DSKyRbv8pv/6b4NL0PastLvMxIoqZbBZ5Zj1cYi332R9xsSUi31ZOsu2wpgn/bC7DA9hrjg==", - "dev": true, - "dependencies": { - "@pnpm/constants": "7.1.1" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/fetching-types": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/fetching-types/-/fetching-types-5.0.0.tgz", - "integrity": "sha512-o9gdO1v8Uc5P2fBBuW6GSpfTqIivQmQlqjQJdFiQX0m+tgxlrMRneIg392jZuc6fk7kFqjLheInlslgJfwY+4Q==", - "dev": true, - "dependencies": { - "@zkochan/retry": "^0.2.0", - "node-fetch": "3.0.0-beta.9" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/fetching-types/node_modules/node-fetch": { - "version": "3.0.0-beta.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", - "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", - "dev": true, - "dependencies": { - "data-uri-to-buffer": "^3.0.1", - "fetch-blob": "^2.1.1" - }, - "engines": { - "node": "^10.17 || >=12.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/@pnpm/graceful-fs": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@pnpm/graceful-fs/-/graceful-fs-3.2.0.tgz", - "integrity": "sha512-vRoXJxscDpHak7YE9SqCkzfrayn+Lw+YueOeHIPEqkgokrHeYgYeONoc2kGh0ObHaRtNSsonozVfJ456kxLNvA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.11" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/logger": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/logger/-/logger-5.0.0.tgz", - "integrity": "sha512-YfcB2QrX+Wx1o6LD1G2Y2fhDhOix/bAY/oAnMpHoNLsKkWIRbt1oKLkIFvxBMzLwAEPqnYWguJrYC+J6i4ywbw==", - "dev": true, - "dependencies": { - "bole": "^5.0.0", - "ndjson": "^2.0.0" - }, - "engines": { - "node": ">=12.17" - } - }, - "node_modules/@pnpm/npm-package-arg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@pnpm/npm-package-arg/-/npm-package-arg-1.0.0.tgz", - "integrity": "sha512-oQYP08exi6mOPdAZZWcNIGS+KKPsnNwUBzSuAEGWuCcqwMAt3k/WVCqVIXzBxhO5sP2b43og69VHmPj6IroKqw==", - "dev": true, - "dependencies": { - "hosted-git-info": "^4.0.1", - "semver": "^7.3.5", - "validate-npm-package-name": "^4.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/@pnpm/npm-package-arg/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@pnpm/npm-package-arg/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@pnpm/npm-package-arg/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@pnpm/npm-resolver": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/npm-resolver/-/npm-resolver-18.1.0.tgz", - "integrity": "sha512-fUYKX/iHiHldL0VRVvkQI35YK2jWhZEkPO6rrGke8309+LKAo12v833nBttMDpQrtHefmqhB4mhCzQq6L2Xqmg==", - "dev": true, - "dependencies": { - "@pnpm/core-loggers": "9.0.6", - "@pnpm/error": "5.0.2", - "@pnpm/fetching-types": "5.0.0", - "@pnpm/graceful-fs": "3.2.0", - "@pnpm/resolve-workspace-range": "5.0.1", - "@pnpm/resolver-base": "11.1.0", - "@pnpm/types": "9.4.2", - "@zkochan/retry": "^0.2.0", - "encode-registry": "^3.0.1", - "load-json-file": "^6.2.0", - "lru-cache": "^10.0.2", - "normalize-path": "^3.0.0", - "p-limit": "^3.1.0", - "p-memoize": "4.0.1", - "parse-npm-tarball-url": "^3.0.0", - "path-temp": "^2.1.0", - "ramda": "npm:@pnpm/ramda@0.28.1", - "rename-overwrite": "^5.0.0", - "semver": "^7.5.4", - "ssri": "10.0.5", - "version-selector-type": "^3.0.0" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - }, - "peerDependencies": { - "@pnpm/logger": "^5.0.0" - } - }, - "node_modules/@pnpm/npm-resolver/node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", - "dev": true, - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/@pnpm/npm-resolver/node_modules/mem": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-6.1.1.tgz", - "integrity": "sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sindresorhus/mem?sponsor=1" - } - }, - "node_modules/@pnpm/npm-resolver/node_modules/mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@pnpm/npm-resolver/node_modules/p-memoize": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/p-memoize/-/p-memoize-4.0.1.tgz", - "integrity": "sha512-km0sP12uE0dOZ5qP+s7kGVf07QngxyG0gS8sYFvFWhqlgzOsSy+m71aUejf/0akxj5W7gE//2G74qTv6b4iMog==", - "dev": true, - "dependencies": { - "mem": "^6.0.1", - "mimic-fn": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/p-memoize?sponsor=1" - } - }, - "node_modules/@pnpm/npm-resolver/node_modules/ramda": { - "name": "@pnpm/ramda", - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@pnpm/ramda/-/ramda-0.28.1.tgz", - "integrity": "sha512-zcAG+lvU0fMziNeGXpPyCyCJYp5ZVrPElEE4t14jAmViaihohocZ+dDkcRIyAomox8pQsuZnv1EyHR+pOhmUWw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, - "node_modules/@pnpm/resolve-workspace-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@pnpm/resolve-workspace-range/-/resolve-workspace-range-5.0.1.tgz", - "integrity": "sha512-yQ0pMthlw8rTgS/C9hrjne+NEnnSNevCjtdodd7i15I59jMBYciHifZ/vjg0NY+Jl+USTc3dBE+0h/4tdYjMKg==", - "dev": true, - "dependencies": { - "semver": "^7.4.0" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/resolver-base": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/resolver-base/-/resolver-base-11.1.0.tgz", - "integrity": "sha512-y2qKaj18pwe1VWc3YXEitdYFo+WqOOt60aqTUuOVkJAirUzz0DzuYh3Ifct4znYWPdgUXHaN5DMphNF5iL85rA==", - "dev": true, - "dependencies": { - "@pnpm/types": "9.4.2" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/types": { - "version": "9.4.2", - "resolved": "https://registry.npmjs.org/@pnpm/types/-/types-9.4.2.tgz", - "integrity": "sha512-g1hcF8Nv4gd76POilz9gD4LITAPXOe5nX4ijgr8ixCbLQZfcpYiMfJ+C1RlMNRUDo8vhlNB4O3bUlxmT6EAQXA==", - "dev": true, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/workspace.pkgs-graph": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@pnpm/workspace.pkgs-graph/-/workspace.pkgs-graph-2.0.14.tgz", - "integrity": "sha512-SBXXyWDkPEoaLTjLRyQzRHoBYH+P0NLcIjX1yPUxuJiMTvGOMzjpLWTuxYNVe/P0V0VQMrjpJFaJPjlViNLhzg==", - "dev": true, - "dependencies": { - "@pnpm/npm-package-arg": "^1.0.0", - "@pnpm/npm-resolver": "18.1.0", - "@pnpm/resolve-workspace-range": "5.0.1", - "ramda": "npm:@pnpm/ramda@0.28.1" - }, - "engines": { - "node": ">=16.14" - }, - "funding": { - "url": "https://opencollective.com/pnpm" - } - }, - "node_modules/@pnpm/workspace.pkgs-graph/node_modules/ramda": { - "name": "@pnpm/ramda", - "version": "0.28.1", - "resolved": "https://registry.npmjs.org/@pnpm/ramda/-/ramda-0.28.1.tgz", - "integrity": "sha512-zcAG+lvU0fMziNeGXpPyCyCJYp5ZVrPElEE4t14jAmViaihohocZ+dDkcRIyAomox8pQsuZnv1EyHR+pOhmUWw==", - "dev": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ramda" - } - }, "node_modules/@polka/url": { "version": "1.0.0-next.21", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", @@ -5852,9 +5282,9 @@ } }, "node_modules/@rjsf/bootstrap-4": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/@rjsf/bootstrap-4/-/bootstrap-4-5.18.2.tgz", - "integrity": "sha512-0kow5RtmabsbDHIduby4ixT8YjdHDe+zkm9YgFC2dGB6iXFBS0zvjVVb3i5Vw5bysYGjDwexB6vIEEbtqXGJWA==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/@rjsf/bootstrap-4/-/bootstrap-4-5.18.3.tgz", + "integrity": "sha512-zMUyuv3tau+IALMLaaeLyCmjvMyloe/tcefGARjEimjexP8F4gZSXGdS7S8D2Z1vOgDBTQp22q4RCrZCb059Vg==", "dependencies": { "@react-icons/all-files": "^4.1.0" }, @@ -5869,9 +5299,9 @@ } }, "node_modules/@rjsf/core": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.18.2.tgz", - "integrity": "sha512-dqS8E70DJSnTpJ8tdQi4flTffJ3fr/G1Jro7Bhx/eKiB2UbYII1tR536HfZqwe8p1NvJ26DViJclOAghHPmJog==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/@rjsf/core/-/core-5.18.3.tgz", + "integrity": "sha512-HVqqoVzcjYX1BUCqHm7iUjBoVx1UDud1KP6bJ5drtOOvHChfKEbMUt2hWJcuXIlmNPuw2lRxETtcuRbEM2wsRA==", "dependencies": { "lodash": "^4.17.21", "lodash-es": "^4.17.21", @@ -5888,9 +5318,9 @@ } }, "node_modules/@rjsf/utils": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.18.2.tgz", - "integrity": "sha512-iDqwBTispZ7mAgwBuHIM0emK+Ft2xRgKD9TzB68VEUhr2hqlDRpwtH6/AgAWUKmJl4kUj3cRKVqqhIvamGLpXw==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/@rjsf/utils/-/utils-5.18.3.tgz", + "integrity": "sha512-0TzjAKAlqXSCneVnHhcL3gAr4DlIPgwzkFdNEI4A+LFjLFlECPah2o3RhEgvqJnUXFviDGF1dqhkxa/Pr59ajw==", "dependencies": { "json-schema-merge-allof": "^0.8.1", "jsonpointer": "^5.0.1", @@ -8007,9 +7437,9 @@ } }, "node_modules/@swc/core": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.16.tgz", - "integrity": "sha512-Xaf+UBvW6JNuV131uvSNyMXHn+bh6LyKN4tbv7tOUFQpXyz/t9YWRE04emtlUW9Y0qrm/GKFCbY8n3z6BpZbTA==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.17.tgz", + "integrity": "sha512-tq+mdWvodMBNBBZbwFIMTVGYHe9N7zvEaycVVjfvAx20k1XozHbHhRv+9pEVFJjwRxLdXmtvFZd3QZHRAOpoNQ==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -8024,16 +7454,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.4.16", - "@swc/core-darwin-x64": "1.4.16", - "@swc/core-linux-arm-gnueabihf": "1.4.16", - "@swc/core-linux-arm64-gnu": "1.4.16", - "@swc/core-linux-arm64-musl": "1.4.16", - "@swc/core-linux-x64-gnu": "1.4.16", - "@swc/core-linux-x64-musl": "1.4.16", - "@swc/core-win32-arm64-msvc": "1.4.16", - "@swc/core-win32-ia32-msvc": "1.4.16", - "@swc/core-win32-x64-msvc": "1.4.16" + "@swc/core-darwin-arm64": "1.4.17", + "@swc/core-darwin-x64": "1.4.17", + "@swc/core-linux-arm-gnueabihf": "1.4.17", + "@swc/core-linux-arm64-gnu": "1.4.17", + "@swc/core-linux-arm64-musl": "1.4.17", + "@swc/core-linux-x64-gnu": "1.4.17", + "@swc/core-linux-x64-musl": "1.4.17", + "@swc/core-win32-arm64-msvc": "1.4.17", + "@swc/core-win32-ia32-msvc": "1.4.17", + "@swc/core-win32-x64-msvc": "1.4.17" }, "peerDependencies": { "@swc/helpers": "^0.5.0" @@ -8045,9 +7475,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.16.tgz", - "integrity": "sha512-UOCcH1GvjRnnM/LWT6VCGpIk0OhHRq6v1U6QXuPt5wVsgXnXQwnf5k3sG5Cm56hQHDvhRPY6HCsHi/p0oek8oQ==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.17.tgz", + "integrity": "sha512-HVl+W4LezoqHBAYg2JCqR+s9ife9yPfgWSj37iIawLWzOmuuJ7jVdIB7Ee2B75bEisSEKyxRlTl6Y1Oq3owBgw==", "cpu": [ "arm64" ], @@ -8061,9 +7491,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.16.tgz", - "integrity": "sha512-t3bgqFoYLWvyVtVL6KkFNCINEoOrIlyggT/kJRgi1y0aXSr0oVgcrQ4ezJpdeahZZ4N+Q6vT3ffM30yIunELNA==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.4.17.tgz", + "integrity": "sha512-WYRO9Fdzq4S/he8zjW5I95G1zcvyd9yyD3Tgi4/ic84P5XDlSMpBDpBLbr/dCPjmSg7aUXxNQqKqGkl6dQxYlA==", "cpu": [ "x64" ], @@ -8077,9 +7507,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.16.tgz", - "integrity": "sha512-DvHuwvEF86YvSd0lwnzVcjOTZ0jcxewIbsN0vc/0fqm9qBdMMjr9ox6VCam1n3yYeRtj4VFgrjeNFksqbUejdQ==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.4.17.tgz", + "integrity": "sha512-cgbvpWOvtMH0XFjvwppUCR+Y+nf6QPaGu6AQ5hqCP+5Lv2zO5PG0RfasC4zBIjF53xgwEaaWmGP5/361P30X8Q==", "cpu": [ "arm" ], @@ -8093,9 +7523,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.16.tgz", - "integrity": "sha512-9Uu5YlPbyCvbidjKtYEsPpyZlu16roOZ5c2tP1vHfnU9bgf5Tz5q5VovSduNxPHx+ed2iC1b1URODHvDzbbDuQ==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.4.17.tgz", + "integrity": "sha512-l7zHgaIY24cF9dyQ/FOWbmZDsEj2a9gRFbmgx2u19e3FzOPuOnaopFj0fRYXXKCmtdx+anD750iBIYnTR+pq/Q==", "cpu": [ "arm64" ], @@ -8109,9 +7539,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.16.tgz", - "integrity": "sha512-/YZq/qB1CHpeoL0eMzyqK5/tYZn/rzKoCYDviFU4uduSUIJsDJQuQA/skdqUzqbheOXKAd4mnJ1hT04RbJ8FPQ==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.4.17.tgz", + "integrity": "sha512-qhH4gr9gAlVk8MBtzXbzTP3BJyqbAfUOATGkyUtohh85fPXQYuzVlbExix3FZXTwFHNidGHY8C+ocscI7uDaYw==", "cpu": [ "arm64" ], @@ -8125,9 +7555,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.16.tgz", - "integrity": "sha512-UUjaW5VTngZYDcA8yQlrFmqs1tLi1TxbKlnaJwoNhel9zRQ0yG1YEVGrzTvv4YApSuIiDK18t+Ip927bwucuVQ==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.4.17.tgz", + "integrity": "sha512-vRDFATL1oN5oZMImkwbgSHEkp8xG1ofEASBypze01W1Tqto8t+yo6gsp69wzCZBlxldsvPpvFZW55Jq0Rn+UnA==", "cpu": [ "x64" ], @@ -8141,9 +7571,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.16.tgz", - "integrity": "sha512-aFhxPifevDTwEDKPi4eRYWzC0p/WYJeiFkkpNU5Uc7a7M5iMWPAbPFUbHesdlb9Jfqs5c07oyz86u+/HySBNPQ==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.4.17.tgz", + "integrity": "sha512-zQNPXAXn3nmPqv54JVEN8k2JMEcMTQ6veVuU0p5O+A7KscJq+AGle/7ZQXzpXSfUCXlLMX4wvd+rwfGhh3J4cw==", "cpu": [ "x64" ], @@ -8157,9 +7587,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.16.tgz", - "integrity": "sha512-bTD43MbhIHL2s5QgCwyleaGwl96Gk/scF2TaVKdUe4QlJCDV/YK9h5oIBAp63ckHtE8GHlH4c8dZNBiAXn4Org==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.4.17.tgz", + "integrity": "sha512-z86n7EhOwyzxwm+DLE5NoLkxCTme2lq7QZlDjbQyfCxOt6isWz8rkW5QowTX8w9Rdmk34ncrjSLvnHOeLY17+w==", "cpu": [ "arm64" ], @@ -8173,9 +7603,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.16.tgz", - "integrity": "sha512-/lmZeAN/qV5XbK2SEvi8e2RkIg8FQNYiSA8y2/Zb4gTUMKVO5JMLH0BSWMiIKMstKDPDSxMWgwJaQHF8UMyPmQ==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.4.17.tgz", + "integrity": "sha512-JBwuSTJIgiJJX6wtr4wmXbfvOswHFj223AumUrK544QV69k60FJ9q2adPW9Csk+a8wm1hLxq4HKa2K334UHJ/g==", "cpu": [ "ia32" ], @@ -8189,9 +7619,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.4.16", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.16.tgz", - "integrity": "sha512-BPAfFfODWXtUu6SwaTTftDHvcbDyWBSI/oanUeRbQR5vVWkXoQ3cxLTsDluc3H74IqXS5z1Uyoe0vNo2hB1opA==", + "version": "1.4.17", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.4.17.tgz", + "integrity": "sha512-jFkOnGQamtVDBm3MF5Kq1lgW8vx4Rm1UvJWRUfg+0gx7Uc3Jp3QMFeMNw/rDNQYRDYPG3yunCC+2463ycd5+dg==", "cpu": [ "x64" ], @@ -8541,9 +7971,9 @@ } }, "node_modules/@types/chrome": { - "version": "0.0.266", - "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.266.tgz", - "integrity": "sha512-QSQWJTL7NjZElvq/6/E5C1+pHgEP8UAJzwoz7M4vSJ7AECt6NNehJ+tU6snnvuTqZOBjFCivvitYo5+8tNPmhg==", + "version": "0.0.267", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.267.tgz", + "integrity": "sha512-vnCWPpYjazSPRMNmybRH+0q4f738F+Pbbls4ZPFsPr9/4TTNJyK1OLZDpSnghnEWb4stfmIUtq/GegnlfD4sPA==", "dev": true, "dependencies": { "@types/filesystem": "*", @@ -9111,12 +8541,6 @@ "version": "4.0.0", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, - "node_modules/@types/picomatch": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.3.tgz", - "integrity": "sha512-Yll76ZHikRFCyz/pffKGjrCwe/le2CDwOP5F210KQo27kpRE46U2rDnzikNlVn6/ezH3Mhn46bJMTfeVTtcYMg==", - "dev": true - }, "node_modules/@types/postcss-modules-local-by-default": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.2.tgz", @@ -9621,13 +9045,13 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-7.7.0.tgz", - "integrity": "sha512-DWsSFnjR9XBLjYkHNh7/NEKRz1v9qE2GfR4qQqimh3IiHHOspyE67NiUy8Ydyxe2dyKFl7akiqJb5iVcwfzX/g==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-7.8.0.tgz", + "integrity": "sha512-f1wXWeZx8XJB/z9Oyjx0ZLmhvcFelSJ0CVvOurCkrISOZhre+imIj5FQQz1rBy/Ips0dCbVl5G4MWTuzlzj5QQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/utils": "7.7.0", + "@typescript-eslint/typescript-estree": "7.8.0", + "@typescript-eslint/utils": "7.8.0", "ajv": "^6.12.6", "lodash.merge": "4.6.2", "semver": "^7.6.0" @@ -9645,13 +9069,13 @@ } }, "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.0.tgz", - "integrity": "sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", + "integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0" + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -9662,9 +9086,9 @@ } }, "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/types": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.0.tgz", - "integrity": "sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", + "integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -9675,13 +9099,13 @@ } }, "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.0.tgz", - "integrity": "sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", + "integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/visitor-keys": "7.8.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -9703,17 +9127,17 @@ } }, "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.0.tgz", - "integrity": "sha512-LKGAXMPQs8U/zMRFXDZOzmMKgFv3COlxUQ+2NMPhbqgVm6R1w+nU1i4836Pmxu9jZAuIeyySNrN/6Rc657ggig==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", + "integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", + "@typescript-eslint/scope-manager": "7.8.0", + "@typescript-eslint/types": "7.8.0", + "@typescript-eslint/typescript-estree": "7.8.0", "semver": "^7.6.0" }, "engines": { @@ -9728,12 +9152,12 @@ } }, "node_modules/@typescript-eslint/rule-tester/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.0.tgz", - "integrity": "sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", + "integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/types": "7.8.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -10193,27 +9617,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "node_modules/@zkochan/retry": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@zkochan/retry/-/retry-0.2.0.tgz", - "integrity": "sha512-WhB+2B/ZPlW2Xy/kMJBrMbqecWXcbDDgn0K0wKBAgO2OlBTz1iLJrRWduo+DGGn0Akvz1Lu4Xvls7dJojximWw==", - "dev": true, - "engines": { - "node": ">=10" - } - }, - "node_modules/@zkochan/rimraf": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@zkochan/rimraf/-/rimraf-2.1.3.tgz", - "integrity": "sha512-mCfR3gylCzPC+iqdxEA6z5SxJeOgzgbwmyxanKriIne5qZLswDe/M43aD3p5MNzwzXRhbZg/OX+MpES6Zk1a6A==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=12.10" - } - }, "node_modules/@zxing/text-encoding": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", @@ -10275,9 +9678,9 @@ } }, "node_modules/ace-builds": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.33.0.tgz", - "integrity": "sha512-PDvytkZNvAfuh+PaP5Oy3l3sBGd7xMk4NsB+4w/w1e3gjBqEOGeJwcX+wF/SB6mLtT3VfJLrhDNPT3eaCjtR3w==" + "version": "1.33.1", + "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.33.1.tgz", + "integrity": "sha512-pj5mcXV1n3s86UI4SWUt8X0ltN8cTaYcvF76cSmvy5i2ZDtXX9KkjVcYTGkCV7ox6VUrzqHByeqH0xRsMjXi4g==" }, "node_modules/acorn": { "version": "8.11.3", @@ -11444,16 +10847,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/bole": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/bole/-/bole-5.0.11.tgz", - "integrity": "sha512-KB0Ye0iMAW5BnNbnLfMSQcnI186hKUzE2fpkZWqcxsoTR7eqzlTidSOMYPHJOn/yR7VGH7uSZp37qH9q2Et0zQ==", - "dev": true, - "dependencies": { - "fast-safe-stringify": "^2.0.7", - "individual": "^3.0.0" - } - }, "node_modules/bonjour-service": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", @@ -11767,15 +11160,6 @@ "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", "dev": true }, - "node_modules/builtins": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", - "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, "node_modules/bundle-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", @@ -13512,15 +12896,6 @@ "node": ">=4" } }, - "node_modules/data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, "node_modules/data-urls": { "version": "3.0.2", "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", @@ -14102,9 +13477,9 @@ } }, "node_modules/dompurify": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.0.tgz", - "integrity": "sha512-yoU4rhgPKCo+p5UrWWWNKiIq+ToGqmVVhk0PmMYBK4kRsR3/qhemNFL8f6CFmBd4gMwm3F4T7HBoydP5uY07fA==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.1.tgz", + "integrity": "sha512-tVP8C/GJwnABOn/7cx/ymx/hXpmBfWIPihC1aOEvS8GbMqy3pgeYtJk1HXN3CO7tu+8bpY18f6isjR5Cymj0TQ==" }, "node_modules/domutils": { "version": "2.8.0", @@ -14330,18 +13705,6 @@ "node": ">= 4" } }, - "node_modules/encode-registry": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/encode-registry/-/encode-registry-3.0.1.tgz", - "integrity": "sha512-6qOwkl1g0fv0DN3Y3ggr2EaZXN71aoAqPp3p/pVaWSBSIo+YjLOWN61Fva43oVyQNPf7kgm8lkudzlzojwE2jw==", - "dev": true, - "dependencies": { - "mem": "^8.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -14402,12 +13765,6 @@ "node": ">=4" } }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "dev": true - }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -15439,6 +14796,54 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-playwright": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-1.6.0.tgz", + "integrity": "sha512-tI1E/EDbHT4Fx5KvukUG3RTIT0gk44gvTP8bNwxLCFsUXVM98ZJG5zWU6Om5JOzH9FrmN4AhMu/UKyEsu0ZoDA==", + "dev": true, + "dependencies": { + "globals": "^13.23.0" + }, + "engines": { + "node": ">=16.6.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0", + "eslint-plugin-jest": ">=25" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-playwright/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-playwright/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", @@ -16450,14 +15855,8 @@ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" }, "node_modules/fast-levenshtein": { - "version": "2.0.6", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, - "node_modules/fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "version": "2.0.6", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "node_modules/fastest-levenshtein": { @@ -16502,20 +15901,6 @@ "pend": "~1.2.0" } }, - "node_modules/fetch-blob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz", - "integrity": "sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==", - "dev": true, - "engines": { - "node": "^10.17.0 || >=12.3.0" - }, - "peerDependenciesMeta": { - "domexception": { - "optional": true - } - } - }, "node_modules/fetch-mock": { "version": "9.11.0", "integrity": "sha512-PG1XUv+x7iag5p/iNHD4/jdpxL9FtVSqRMUQhPab4hVDt80T1MH5ehzVrL2IdXO9Q2iBggArFvPqjUbHFuI58Q==", @@ -16890,8 +16275,9 @@ } }, "node_modules/flatted": { - "version": "3.2.4", - "integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/flow-parser": { @@ -18234,9 +17620,9 @@ ] }, "node_modules/iframe-resizer": { - "version": "4.3.10", - "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.10.tgz", - "integrity": "sha512-hv1E1twrVzxdbWcNU4iTnRp/vo/XB8S4s4FwiT0nYk7Q1oCbS5tWtTU/b/TjgEE0LSAX1VhaZGYCvz1jTP/mPA==", + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/iframe-resizer/-/iframe-resizer-4.3.11.tgz", + "integrity": "sha512-5QtnsmfH11GDsuC7Gxd/eNzojudX3346Gb0E+Ku8ln8AtfSq+cWCZtnhCrthrtE7f1CI2/kwHkZ9G4sFYzHP7A==", "engines": { "node": ">=0.8.0" }, @@ -18390,12 +17776,6 @@ "node": ">=8" } }, - "node_modules/individual": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/individual/-/individual-3.0.0.tgz", - "integrity": "sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==", - "dev": true - }, "node_modules/inflight": { "version": "1.0.6", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", @@ -21245,8 +20625,9 @@ } }, "node_modules/jest-webextension-mock": { - "version": "3.8.9", - "integrity": "sha512-PglflLBEhqAtfKOmwEcP2iV2YdQK3Xwa7e/SELIK/4+y1NiKSJnba7DdPe32HavHJCJwuL8bpVwFrxGZFD2m+A==", + "version": "3.8.16", + "resolved": "https://registry.npmjs.org/jest-webextension-mock/-/jest-webextension-mock-3.8.16.tgz", + "integrity": "sha512-bFEaRBuF+QZkPsprJfCXGuTTCvBm368y9ComlwIu30Z1ia2B+cHqHr45qABgRvTwb0eojbhsawJliqhiRypQwA==", "dev": true }, "node_modules/jest-worker": { @@ -21752,6 +21133,12 @@ "node": ">=4" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" @@ -21914,6 +21301,15 @@ "node": ">=10" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, "node_modules/kind-of": { "version": "6.0.3", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", @@ -21930,9 +21326,9 @@ } }, "node_modules/knip": { - "version": "5.9.4", - "resolved": "https://registry.npmjs.org/knip/-/knip-5.9.4.tgz", - "integrity": "sha512-33TM8bSHxMMoj+wP9lzjUkIIEfpXaZsLWMYRCoHdbmYnl2HKPMNijcYTxwi1omRROobXrR/VJyH2ZsYOKM1jtg==", + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/knip/-/knip-5.11.0.tgz", + "integrity": "sha512-7TBBpLYYAqZI+FM+qHsmT0jfYNOwwZAMBvuJXq3gFJCCpDvGrYcLMu2j6yvURJNh1UlpQ4jaNgPoHw7ylGDWMA==", "dev": true, "funding": [ { @@ -21947,17 +21343,12 @@ "dependencies": { "@ericcornelissen/bash-parser": "0.5.2", "@nodelib/fs.walk": "2.0.0", - "@npmcli/map-workspaces": "^3.0.4", - "@npmcli/package-json": "^5.0.0", - "@pnpm/logger": "5.0.0", - "@pnpm/workspace.pkgs-graph": "2.0.14", "@snyk/github-codeowners": "1.1.0", - "@types/picomatch": "2.3.3", "easy-table": "1.2.0", "fast-glob": "3.3.2", + "file-entry-cache": "8.0.0", "jiti": "1.21.0", "js-yaml": "4.1.0", - "micromatch": "4.0.5", "minimist": "1.2.8", "picocolors": "1.0.0", "picomatch": "^4.0.1", @@ -21970,7 +21361,8 @@ "zod-validation-error": "^3.0.3" }, "bin": { - "knip": "bin/knip.js" + "knip": "bin/knip.js", + "knip-bun": "bin/knip-bun.js" }, "engines": { "node": ">=18.6.0" @@ -22015,6 +21407,31 @@ "node": ">=16.14.0" } }, + "node_modules/knip/node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/knip/node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/knip/node_modules/picomatch": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", @@ -22167,30 +21584,6 @@ "version": "1.2.4", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, - "node_modules/load-json-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", - "integrity": "sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.15", - "parse-json": "^5.0.0", - "strip-bom": "^4.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/load-json-file/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/loader-runner": { "version": "4.2.0", "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", @@ -22450,27 +21843,6 @@ "tmpl": "1.0.5" } }, - "node_modules/map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "dev": true, - "dependencies": { - "p-defer": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/map-age-cleaner/node_modules/p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/map-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", @@ -22560,31 +21932,6 @@ "node": ">= 0.6" } }, - "node_modules/mem": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/mem/-/mem-8.1.1.tgz", - "integrity": "sha512-qFCFUDs7U3b8mBDPyz5EToEKoAkgCzqquIgi9nkkR9bixxOVOre+09lbuH7+9Kn2NFpm56M3GUWVbU2hQgdACA==", - "dev": true, - "dependencies": { - "map-age-cleaner": "^0.1.3", - "mimic-fn": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/mem?sponsor=1" - } - }, - "node_modules/mem/node_modules/mimic-fn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", - "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/memfs": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", @@ -23122,34 +22469,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/ndjson": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ndjson/-/ndjson-2.0.0.tgz", - "integrity": "sha512-nGl7LRGrzugTtaFcJMhLbpzJM6XdivmbkdlaGcrk/LXg2KL/YBC6z1g70xh0/al+oFuVFP8N8kiWRucmeEH/qQ==", - "dev": true, - "dependencies": { - "json-stringify-safe": "^5.0.1", - "minimist": "^1.2.5", - "readable-stream": "^3.6.0", - "split2": "^3.0.0", - "through2": "^4.0.0" - }, - "bin": { - "ndjson": "cli.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ndjson/node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "dev": true, - "dependencies": { - "readable-stream": "3" - } - }, "node_modules/needle": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", @@ -23399,81 +22718,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-install-checks": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", - "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", - "dev": true, - "dependencies": { - "semver": "^7.1.1" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-normalize-package-bin": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", - "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", - "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", - "dev": true, - "dependencies": { - "hosted-git-info": "^7.0.0", - "proc-log": "^3.0.0", - "semver": "^7.3.5", - "validate-npm-package-name": "^5.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg/node_modules/hosted-git-info": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", - "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm-package-arg/node_modules/validate-npm-package-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", - "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", - "dev": true, - "dependencies": { - "builtins": "^5.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm-pick-manifest": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", - "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", - "dev": true, - "dependencies": { - "npm-install-checks": "^6.0.0", - "npm-normalize-package-bin": "^3.0.0", - "npm-package-arg": "^11.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/npm-run-path": { "version": "4.0.1", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", @@ -24232,27 +23476,6 @@ "node": ">= 0.10" } }, - "node_modules/parse-npm-tarball-url": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/parse-npm-tarball-url/-/parse-npm-tarball-url-3.0.0.tgz", - "integrity": "sha512-InpdgIdNe5xWMEUcrVQUniQKwnggBtJ7+SCwh7zQAZwbbIYZV9XdgJyhtmDSSvykFyQXoe4BINnzKTfCwWLs5g==", - "dev": true, - "dependencies": { - "semver": "^6.1.0" - }, - "engines": { - "node": ">=8.15" - } - }, - "node_modules/parse-npm-tarball-url/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -24338,18 +23561,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-temp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-temp/-/path-temp-2.1.0.tgz", - "integrity": "sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==", - "dev": true, - "dependencies": { - "unique-string": "^2.0.0" - }, - "engines": { - "node": ">=8.15" - } - }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -25177,15 +24388,6 @@ } } }, - "node_modules/proc-log": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", - "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/process": { "version": "0.11.10", "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", @@ -25216,34 +24418,6 @@ "asap": "~2.0.6" } }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "dev": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-retry/node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/prompts": { "version": "2.4.2", "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", @@ -26271,28 +25445,6 @@ "version": "5.2.1", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, - "node_modules/read-package-json-fast": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", - "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", - "dev": true, - "dependencies": { - "json-parse-even-better-errors": "^3.0.0", - "npm-normalize-package-bin": "^3.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", - "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", - "dev": true, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/read-pkg": { "version": "5.2.0", "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", @@ -26682,33 +25834,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/rename-overwrite": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/rename-overwrite/-/rename-overwrite-5.0.0.tgz", - "integrity": "sha512-vSxE5Ww7Jnyotvaxi3Dj0vOMoojH8KMkBfs9xYeW/qNfJiLTcC1fmwTjrbGUq3mQSOCxkG0DbdcvwTUrpvBN4w==", - "dev": true, - "dependencies": { - "@zkochan/rimraf": "^2.1.2", - "fs-extra": "10.1.0" - }, - "engines": { - "node": ">=12.10" - } - }, - "node_modules/rename-overwrite/node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/renderkid": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", @@ -27689,32 +26814,11 @@ "wbuf": "^1.7.3" } }, - "node_modules/split2": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", - "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", - "dev": true, - "dependencies": { - "readable-stream": "^3.0.0" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", - "dev": true, - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/stack-utils": { "version": "2.0.5", "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", @@ -28926,9 +28030,9 @@ } }, "node_modules/type-fest": { - "version": "4.15.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.15.0.tgz", - "integrity": "sha512-tB9lu0pQpX5KJq54g+oHOLumOx+pMep4RaM6liXh2PKmVRFF+/vAtUP0ZaJ0kOySfVNjF6doBWPHhBhISKdlIA==", + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.0.tgz", + "integrity": "sha512-+dbmiyliDY/2TTcjCS7NpI9yV2iEFlUDk5TKnsbkN7ZoRu5s7bT+zvYtNFhFXC2oLwURGT2frACAZvbbyNBI+w==", "engines": { "node": ">=16" }, @@ -29523,8 +28627,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.0", - "integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -29595,18 +28700,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/validate-npm-package-name": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-4.0.0.tgz", - "integrity": "sha512-mzR0L8ZDktZjpX4OB46KT+56MAhl4EIazWP/+G/HPGuvfdaqg4YsCdtOm6U9+LOFyYDoh4dpnpxZRB9MQQns5Q==", - "dev": true, - "dependencies": { - "builtins": "^5.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/validate.io-array": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", @@ -29652,18 +28745,6 @@ "node": ">= 0.8" } }, - "node_modules/version-selector-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/version-selector-type/-/version-selector-type-3.0.0.tgz", - "integrity": "sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA==", - "dev": true, - "dependencies": { - "semver": "^7.3.2" - }, - "engines": { - "node": ">=10.13" - } - }, "node_modules/vlq": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", diff --git a/package.json b/package.json index 550ce30e41..0da551a9d5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "test:e2e:debug": "PWDEBUG=console playwright test", "lint": "npm run lint:full -- --rule '{\"import/no-cycle\": \"off\"}'", "lint:fast": "ESLINT_NO_IMPORTS=1 eslint src --ext js,jsx,ts,tsx --quiet", - "lint:full": "eslint src --ext js,jsx,ts,tsx --quiet --report-unused-disable-directives", + "lint:full": "eslint src end-to-end-tests --ext js,jsx,ts,tsx --quiet --report-unused-disable-directives", "fix": "npm run lint -- --fix", "watch": "concurrently npm:watch:webpack 'npm:watch:*(!webpack) -- --preserveWatchOutput' -r", "watch:webpack": "ENV_FILE='.env.development' webpack ${HMR:-watch} --mode development", @@ -36,10 +36,10 @@ "repository": "https://github.com/pixiebrix/pixiebrix-extension", "dependencies": { "@apidevtools/json-schema-ref-parser": "^10.1.0", - "@atlaskit/tree": "^8.8.8", + "@atlaskit/tree": "^8.8.9", "@cfworker/json-schema": "^1.12.8", - "@datadog/browser-logs": "^5.15.0", - "@datadog/browser-rum": "^5.15.0", + "@datadog/browser-logs": "^5.16.0", + "@datadog/browser-rum": "^5.16.0", "@floating-ui/dom": "^1.6.3", "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "^5.15.4", @@ -49,14 +49,14 @@ "@mozilla/readability": "^0.5.0", "@pixiebrix/jq-web": "^0.5.1", "@reduxjs/toolkit": "^1.9.7", - "@rjsf/bootstrap-4": "^5.18.2", - "@rjsf/core": "^5.18.2", - "@rjsf/utils": "^5.18.2", + "@rjsf/bootstrap-4": "^5.18.3", + "@rjsf/core": "^5.18.3", + "@rjsf/utils": "^5.18.3", "@uipath/robot": "1.3.1", "@vespaiach/axios-fetch-adapter": "^0.3.1", "@xobotyi/scrollbar-width": "^1.9.5", "abort-utils": "^1.2.0", - "ace-builds": "^1.33.0", + "ace-builds": "^1.33.1", "autocompleter": "^9.2.1", "axios": "^0.27.2", "batched-function": "^2.0.1", @@ -72,7 +72,7 @@ "csharp-helpers": "^0.9.3", "css-selector-generator": "^3.6.7", "date-fns": "^3.6.0", - "dompurify": "^3.1.0", + "dompurify": "^3.1.1", "downloadjs": "^1.4.7", "exifreader": "^4.22.1", "export-to-csv": "^1.2.4", @@ -90,7 +90,7 @@ "htmlparser2": "^9.1.0", "http-status-codes": "^2.3.0", "idb": "^8.0.0", - "iframe-resizer": "^4.3.10", + "iframe-resizer": "^4.3.11", "immer": "^9.0.18", "intrinsic-scale": "^4.0.0", "intro.js": "^7.2.0", @@ -160,7 +160,7 @@ "urlpattern-polyfill": "^5.0.3", "use-async-effect": "^2.2.7", "use-debounce": "^10.0.0", - "use-sync-external-store": "^1.2.0", + "use-sync-external-store": "^1.2.2", "uuid": "^9.0.1", "webext-content-scripts": "^2.6.1", "webext-detect-page": "^5.0.0", @@ -181,7 +181,7 @@ "@axe-core/playwright": "^4.9.0", "@fortawesome/fontawesome-common-types": "^0.2.36", "@playwright/test": "^1.43.1", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.13", "@shopify/jest-dom-mocks": "^5.0.0", "@sindresorhus/tsconfig": "^5.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -192,14 +192,14 @@ "@storybook/react": "^7.6.17", "@storybook/react-webpack5": "^7.6.17", "@svgr/webpack": "^8.1.0", - "@swc/core": "^1.4.16", + "@swc/core": "^1.4.17", "@swc/jest": "^0.2.36", "@testing-library/jest-dom": "^6.4.2", "@testing-library/react": "^12.1.5", "@testing-library/react-hooks": "^8.0.1", "@testing-library/user-event": "^14.5.2", "@total-typescript/ts-reset": "^0.5.1", - "@types/chrome": "^0.0.266", + "@types/chrome": "^0.0.267", "@types/dom-navigation": "^1.0.3", "@types/dompurify": "^3.0.5", "@types/downloadjs": "^1.4.6", @@ -246,7 +246,7 @@ "@types/webpack": "^5.28.5", "@types/webpack-env": "^1.18.4", "@types/whatwg-mimetype": "^3.0.2", - "@typescript-eslint/rule-tester": "^7.7.0", + "@typescript-eslint/rule-tester": "^7.8.0", "axios-mock-adapter": "^1.22.0", "blob-polyfill": "^7.0.20220408", "compass-mixins": "^0.12.10", @@ -260,16 +260,17 @@ "eslint": "^8.57.0", "eslint-config-pixiebrix": "^0.38.0", "eslint-plugin-local-rules": "^2.0.1", + "eslint-plugin-playwright": "^1.6.0", "fake-indexeddb": "^5.0.2", "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "jest-extended": "^4.0.2", "jest-location-mock": "^2.0.0", - "jest-webextension-mock": "^3.8.9", + "jest-webextension-mock": "^3.8.16", "jsdom": "^24.0.0", "jsdom-testing-mocks": "^1.13.0", - "knip": "^5.9.4", + "knip": "^5.11.0", "mini-css-extract-plugin": "^2.6.1", "mockdate": "^3.0.5", "msw": "^1.3.3", @@ -286,7 +287,7 @@ "style-loader": "^4.0.0", "terser-webpack-plugin": "^5.3.10", "ts-loader": "^9.5.1", - "type-fest": "^4.15.0", + "type-fest": "^4.18.0", "typescript": "^5.4.5", "typescript-plugin-css-modules": "^5.1.0", "webpack": "^5.91.0", diff --git a/playwright.config.ts b/playwright.config.ts index 5f4fb7caef..fa24f9de56 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,7 +25,10 @@ export default defineConfig<{ chromiumChannel: string }>({ }, reportSlowTests: null, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [["html", { outputFolder: "./end-to-end-tests/.report" }]], + reporter: [ + ["html", { outputFolder: "./end-to-end-tests/.report" }], + ["json", { outputFile: "./end-to-end-tests/.report/report.json" }], + ], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ diff --git a/scripts/__snapshots__/manifest.test.js.snap b/scripts/__snapshots__/manifest.test.js.snap index 98bae6d7bd..8b1e8ea04e 100644 --- a/scripts/__snapshots__/manifest.test.js.snap +++ b/scripts/__snapshots__/manifest.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`customizeManifest beta 1`] = ` +exports[`customizeManifest release builds beta 1`] = ` { "action": { "default_icon": { @@ -124,6 +124,8 @@ exports[`customizeManifest beta 1`] = ` "storage": { "managed_schema": "managedStorageSchema.json", }, + "version": "1.8.13.3000", + "version_name": "1.8.13", "web_accessible_resources": [ { "matches": [ @@ -149,7 +151,7 @@ exports[`customizeManifest beta 1`] = ` } `; -exports[`customizeManifest mv2 1`] = ` +exports[`customizeManifest release builds mv2 1`] = ` { "author": "PixieBrix, Inc.", "background": { @@ -264,6 +266,8 @@ exports[`customizeManifest mv2 1`] = ` "storage": { "managed_schema": "managedStorageSchema.json", }, + "version": "1.8.13.3000", + "version_name": "1.8.13", "web_accessible_resources": [ "css/*", "bundles/*", @@ -282,7 +286,7 @@ exports[`customizeManifest mv2 1`] = ` } `; -exports[`customizeManifest mv3 1`] = ` +exports[`customizeManifest release builds mv3 1`] = ` { "action": { "default_icon": { @@ -406,6 +410,8 @@ exports[`customizeManifest mv3 1`] = ` "storage": { "managed_schema": "managedStorageSchema.json", }, + "version": "1.8.13.3000", + "version_name": "1.8.13", "web_accessible_resources": [ { "matches": [ diff --git a/scripts/manifest.mjs b/scripts/manifest.mjs index c8c641e550..462b4bc68c 100644 --- a/scripts/manifest.mjs +++ b/scripts/manifest.mjs @@ -19,10 +19,11 @@ import Policy from "csp-parse"; import { normalizeManifestPermissions } from "webext-permissions"; import { excludeDuplicatePatterns } from "webext-patterns"; -function getVersion(env, isBetaListing) { +function getVersion(env) { const stageMap = { alpha: 1000, beta: 2000, + release: 3000, }; // `manifest.json` only supports numbers in the version, so use the semver @@ -32,19 +33,16 @@ function getVersion(env, isBetaListing) { ); const { version, stage, stageNumber } = match.groups; - // Add 4th digit for alpha/beta release builds. Used to update the extension BETA listing in the Chrome Web Store. - if (isBetaListing) { - if (stage && stageNumber) { - // Ex: 1.8.13-alpha.1 -> 1.8.13.1001 - // Ex: 1.8.13-beta.55 -> 1.8.13.2055 - return `${version}.${stageMap[stage] + Number(stageNumber)}`; - } - - // Ex: 1.8.13.3000 -- Ensures that the release build version number is greater than the alpha/beta build version numbers - return `${version}.3000`; + // Add 4th digit for differentiating alpha/beta/stable release builds. + // Used primarily to update the extension BETA listing in the Chrome Web Store. + if (stage && stageNumber) { + // Ex: 1.8.13-alpha.1 -> 1.8.13.1001 + // Ex: 1.8.13-beta.55 -> 1.8.13.2055 + return `${version}.${stageMap[stage] + Number(stageNumber)}`; } - return version; + // Ex: 1.8.13.3000 -- Ensures that the release build version number is greater than the alpha/beta build version numbers + return `${version}.${stageMap.release}`; } function getVersionName(env, isProduction) { @@ -146,7 +144,7 @@ function addInternalUrlsToContentScripts(manifest, internal) { function customizeManifest(manifestV2, options = {}) { const { isProduction, manifestVersion, env = {}, isBeta } = options; const manifest = structuredClone(manifestV2); - manifest.version = getVersion(env, isBeta); + manifest.version = getVersion(env); manifest.version_name = getVersionName(env, isProduction); if (!isProduction) { diff --git a/scripts/manifest.test.js b/scripts/manifest.test.js index 3d71e281e1..b61212e83c 100644 --- a/scripts/manifest.test.js +++ b/scripts/manifest.test.js @@ -15,9 +15,6 @@ * along with this program. If not, see . */ -/* eslint-disable @shopify/jest/no-snapshots -- We want to specifically commit the entire customized manifest as a snapshot */ -/* eslint-disable no-restricted-imports -- Aliases don't work outside built files */ - import { omit } from "lodash"; import manifest from "../src/manifest.json"; import { loadEnv } from "./env.mjs"; @@ -25,35 +22,76 @@ import customizeManifest from "./manifest.mjs"; loadEnv(); -const cleanCustomize = (...args) => - omit(customizeManifest(...args), ["version", "version_name", "key"]); +const cleanCustomize = (...args) => omit(customizeManifest(...args), ["key"]); describe("customizeManifest", () => { - test("mv2", () => { - expect( - cleanCustomize(manifest, { - env: process.env, - isProduction: true, - }), - ).toMatchSnapshot(); - }); - test("mv3", () => { - expect( - cleanCustomize(manifest, { - env: process.env, - isProduction: true, - manifestVersion: 3, - }), - ).toMatchSnapshot(); + describe("release builds", () => { + test("mv2", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + }), + ).toMatchSnapshot(); + }); + + test("mv3", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + manifestVersion: 3, + }), + ).toMatchSnapshot(); + }); + + test("beta", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + manifestVersion: 3, + isBeta: true, + }), + ).toMatchSnapshot(); + }); }); - test("beta", () => { - expect( - cleanCustomize(manifest, { - env: process.env, - isProduction: true, - manifestVersion: 3, - isBeta: true, - }), - ).toMatchSnapshot(); + + describe("four digit versioning", () => { + test("alpha version", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13-alpha.123" }, + isProduction: true, + manifestVersion: 3, + }), + ).toContainEntry(["version", "1.8.13.1123"]); + }); + + test("beta version", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13-beta.123" }, + isProduction: true, + manifestVersion: 3, + }), + ).toContainEntry(["version", "1.8.13.2123"]); + }); + + test("release version", () => { + expect( + cleanCustomize(manifest, { + // eslint-disable-next-line camelcase -- auto-inserted + env: { ...process.env, npm_package_version: "1.8.13" }, + isProduction: true, + manifestVersion: 3, + }), + ).toContainEntry(["version", "1.8.13.3000"]); + }); }); }); diff --git a/src/analysis/analysisVisitors/varAnalysis/varAnalysis.test.ts b/src/analysis/analysisVisitors/varAnalysis/varAnalysis.test.ts index 222c8e98ef..80c30b5d4d 100644 --- a/src/analysis/analysisVisitors/varAnalysis/varAnalysis.test.ts +++ b/src/analysis/analysisVisitors/varAnalysis/varAnalysis.test.ts @@ -56,6 +56,7 @@ import { CustomFormRenderer } from "@/bricks/renderers/customForm"; import { toExpression } from "@/utils/expressionUtils"; import IdentityTransformer from "@/bricks/transformers/IdentityTransformer"; import { createNewConfiguredBrick } from "@/pageEditor/exampleBrickConfigs"; +import pixiebrixIntegrationDependencyFactory from "@/integrations/util/pixiebrixIntegrationDependencyFactory"; jest.mocked(services.locate).mockResolvedValue( sanitizedIntegrationConfigFactory({ @@ -134,13 +135,14 @@ describe("Collecting available vars", () => { const extension = formStateFactory( { - // Let this extension have an integration dependency + // Test both the PixieBrix api integration and a sample third-party service integrationDependencies: [ integrationDependencyFactory({ integrationId: validateRegistryId("@test/service"), - outputKey: validateOutputKey("pixiebrix"), + outputKey: validateOutputKey("testService"), configId: uuidSequence, }), + pixiebrixIntegrationDependencyFactory(), ], optionsArgs: { foo: "bar", @@ -164,6 +166,11 @@ describe("Collecting available vars", () => { expect(foundationKnownVars.isVariableDefined("@options.foo")).toBeTrue(); + expect( + foundationKnownVars.isVariableDefined( + "@testService.__service.serviceId", + ), + ).toBeTrue(); expect( foundationKnownVars.isVariableDefined("@pixiebrix.__service.serviceId"), ).toBeTrue(); diff --git a/src/analysis/analysisVisitors/varAnalysis/varAnalysis.ts b/src/analysis/analysisVisitors/varAnalysis/varAnalysis.ts index 73a19ab873..2782264d2d 100644 --- a/src/analysis/analysisVisitors/varAnalysis/varAnalysis.ts +++ b/src/analysis/analysisVisitors/varAnalysis/varAnalysis.ts @@ -52,7 +52,6 @@ import { MOD_VARIABLE_REFERENCE } from "@/runtime/extendModVariableContext"; import { joinPathParts } from "@/utils/formUtils"; import makeIntegrationsContextFromDependencies from "@/integrations/util/makeIntegrationsContextFromDependencies"; import { getOutputReference, isOutputKey } from "@/runtime/runtimeTypes"; -import { PIXIEBRIX_INTEGRATION_ID } from "@/integrations/constants"; export const INVALID_VARIABLE_GENERIC_MESSAGE = "Invalid variable name"; @@ -114,13 +113,9 @@ async function setIntegrationDependencyVars( { integrationDependencies = [] }: ModComponentFormState, contextVars: VarMap, ): Promise { - // We don't want to make the pixiebrix api integration available as a variable - const nonPbDependencies = integrationDependencies.filter( - (dependency) => dependency.integrationId !== PIXIEBRIX_INTEGRATION_ID, - ); // Loop through all the dependencies, so we can set the source for each dependency variable properly await Promise.all( - nonPbDependencies.map(async (integrationDependency) => { + integrationDependencies.map(async (integrationDependency) => { const serviceContext = await makeIntegrationsContextFromDependencies([ integrationDependency, ]); diff --git a/src/background/backgroundPlatform.ts b/src/background/backgroundPlatform.ts index 2ec60d2f57..f828fdf19a 100644 --- a/src/background/backgroundPlatform.ts +++ b/src/background/backgroundPlatform.ts @@ -23,8 +23,8 @@ import type { NetworkRequestConfig } from "@/types/networkTypes"; import type { RemoteResponse } from "@/types/contract"; import { performConfiguredRequest } from "@/background/requests"; import BackgroundLogger from "@/telemetry/BackgroundLogger"; -import { validateSemVerString } from "@/types/helpers"; import { PlatformBase } from "@/platform/platformBase"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * Background platform implementation. Currently, just makes API requests. @@ -40,10 +40,7 @@ class BackgroundPlatform extends PlatformBase { }); constructor() { - super( - "background", - validateSemVerString(browser.runtime.getManifest().version), - ); + super("background", getExtensionVersion()); } override get logger(): PlatformProtocol["logger"] { diff --git a/src/background/deploymentUpdater.test.ts b/src/background/deploymentUpdater.test.ts index 1d5fcff8a4..8252856bac 100644 --- a/src/background/deploymentUpdater.test.ts +++ b/src/background/deploymentUpdater.test.ts @@ -19,7 +19,7 @@ import { getModComponentState, saveModComponentState, } from "@/store/extensionsStorage"; -import { uuidv4, validateSemVerString } from "@/types/helpers"; +import { uuidv4, normalizeSemVerString } from "@/types/helpers"; import { appApiMock } from "@/testUtils/appApiMock"; import { omit } from "lodash"; import { syncDeployments } from "@/background/deploymentUpdater"; @@ -68,8 +68,8 @@ jest.mock("@/store/settings/settingsStorage"); jest.mock("@/hooks/useRefreshRegistries"); jest.mock("@/utils/extensionUtils", () => ({ + ...jest.requireActual("@/utils/extensionUtils"), forEachTab: jest.fn(), - getExtensionVersion: () => browser.runtime.getManifest().version, })); // Override manual mock to support `expect` assertions @@ -380,7 +380,7 @@ describe("syncDeployments", () => { _recipe: { id: deployment.package.package_id, name: deployment.package.name, - version: validateSemVerString("0.0.1"), + version: normalizeSemVerString("0.0.1"), updated_at: deployment.updated_at, sharing: sharingDefinitionFactory(), }, @@ -438,7 +438,7 @@ describe("syncDeployments", () => { _recipe: { id: deployment.package.package_id, name: deployment.package.name, - version: validateSemVerString("0.0.1"), + version: normalizeSemVerString("0.0.1"), updated_at: deployment.updated_at, sharing: sharingDefinitionFactory(), }, diff --git a/src/background/deploymentUpdater.ts b/src/background/deploymentUpdater.ts index 6c953e76e2..df0d574922 100644 --- a/src/background/deploymentUpdater.ts +++ b/src/background/deploymentUpdater.ts @@ -608,7 +608,7 @@ async function activateDeploymentsInBackground({ ); // Version to report to the server. - const { version: extensionVersionString } = browser.runtime.getManifest(); + const extensionVersionString = getExtensionVersion(); const extensionVersion = parseSemVer(extensionVersionString); const deploymentsByActivationMethod = await Promise.all( diff --git a/src/background/installer.ts b/src/background/installer.ts index a5829b561b..fce835fc4b 100644 --- a/src/background/installer.ts +++ b/src/background/installer.ts @@ -36,7 +36,10 @@ import { import { Events } from "@/telemetry/events"; import { DEFAULT_SERVICE_URL, UNINSTALL_URL } from "@/urlConstants"; import { CONTROL_ROOM_TOKEN_INTEGRATION_ID } from "@/integrations/constants"; -import { getExtensionConsoleUrl } from "@/utils/extensionUtils"; +import { + getExtensionConsoleUrl, + getExtensionVersion, +} from "@/utils/extensionUtils"; import { oncePerSession } from "@/mv3/SessionStorage"; import { resetFeatureFlagsCache } from "@/auth/featureFlagStorage"; @@ -69,8 +72,8 @@ async function isLikelyEndUserInstall(): Promise { // The CWS install URL differs based on the extension listing slug. So instead, only match on the runtime id. return likelyOnboardingTabs.some( (tab) => - tab.url.includes(DEFAULT_SERVICE_URL) || - tab.url.includes(browser.runtime.id), + tab.url?.includes(DEFAULT_SERVICE_URL) || + tab.url?.includes(browser.runtime.id), ); } @@ -102,9 +105,11 @@ export async function openInstallPage() { // Case 3: there's no Admin Console onboarding tab open if (appOnboardingTab) { - const appOnboardingTabUrl = new URL(appOnboardingTab.url); + const appOnboardingTabUrl = appOnboardingTab?.url + ? new URL(appOnboardingTab.url) + : null; - if (appOnboardingTabUrl.pathname === "/start") { + if (appOnboardingTabUrl?.pathname === "/start") { // Case 1a/1b: Admin Console is showing a partner onboarding flow const controlRoomHostname = @@ -223,7 +228,7 @@ export async function showInstallPage({ // https://developer.chrome.com/docs/extensions/reference/runtime/#event-onInstalled // https://developer.chrome.com/docs/extensions/reference/runtime/#type-OnInstalledReason console.debug("onInstalled", { reason, previousVersion }); - const { version } = browser.runtime.getManifest(); + const version = getExtensionVersion(); if (reason === "install") { void recordEvent({ @@ -309,10 +314,13 @@ export function getAvailableVersion(): typeof _availableVersion { */ export function isUpdateAvailable(): boolean { const available = getAvailableVersion(); - const installed = browser.runtime.getManifest().version; - return ( - Boolean(available) && installed !== available && gt(available, installed) - ); + + if (!available) { + return false; + } + + const installed = getExtensionVersion(); + return installed !== available && gt(available, installed); } async function setUninstallURL(): Promise { diff --git a/src/background/messenger/api.ts b/src/background/messenger/api.ts index a087d8093a..1ea5bd88dd 100644 --- a/src/background/messenger/api.ts +++ b/src/background/messenger/api.ts @@ -22,7 +22,6 @@ import { getNotifier, } from "webext-messenger"; -export const getAvailableVersion = getMethod("GET_AVAILABLE_VERSION", bg); export const setPartnerCopilotData = getNotifier( "SET_PARTNER_COPILOT_DATA", bg, @@ -53,22 +52,9 @@ export const contextMenus = { preload: getMethod("PRELOAD_CONTEXT_MENUS", bg), }; -// Use this instead: `import reportError from "@/telemetry/reportError"` -// export const recordError = getNotifier("RECORD_ERROR", bg); - -export const initTelemetry = getNotifier("INIT_TELEMETRY", bg); -export const sendDeploymentAlert = getNotifier("SEND_DEPLOYMENT_ALERT", bg); - export const getUserData = getMethod("GET_USER_DATA", bg); export const installStarterBlueprints = getMethod( "INSTALL_STARTER_BLUEPRINTS", bg, ); - -export const ping = getMethod("PING", bg); - -export const collectPerformanceDiagnostics = getMethod( - "COLLECT_PERFORMANCE_DIAGNOSTICS", - bg, -); diff --git a/src/background/messenger/external/api.ts b/src/background/messenger/external/api.ts index 83801cd14f..6b519a90c8 100644 --- a/src/background/messenger/external/api.ts +++ b/src/background/messenger/external/api.ts @@ -23,9 +23,14 @@ import { _liftBackground as liftExternal } from "@/background/externalProtocol"; import * as local from "@/background/messenger/external/_implementation"; import { readPartnerAuthData } from "@/auth/authStorage"; +import { getExtensionVersion } from "@/utils/extensionUtils"; export const connectPage = liftExternal("CONNECT_PAGE", async () => - browser.runtime.getManifest(), + // Ensure the version we send to the app is a valid semver. + ({ + ...browser.runtime.getManifest(), + version: getExtensionVersion(), + }), ); export const setExtensionAuth = liftExternal( diff --git a/src/background/messenger/registration.ts b/src/background/messenger/registration.ts index 39f643e5df..da03082d72 100644 --- a/src/background/messenger/registration.ts +++ b/src/background/messenger/registration.ts @@ -28,7 +28,7 @@ import { ensureContextMenu, preloadContextMenus, uninstallContextMenu, -} from "@/background/contextMenus"; // 213 strictNullCheck errors +} from "@/background/contextMenus"; // 201 strictNullCheck errors import { requestRunInAllFrames, requestRunInOtherTabs, @@ -36,16 +36,8 @@ import { requestRunInTarget, requestRunInTop, } from "@/background/executor"; // Depends on contentScript/messenger to pass strictNullCheck -import { getAvailableVersion } from "@/background/installer"; // 196 strictNullCheck errors -import { removeExtensionForEveryTab } from "@/background/removeExtensionForEveryTab"; // 213 strictNullCheck errors -import { debouncedActivateStarterMods as installStarterBlueprints } from "@/background/starterMods"; // 219 strictNullCheck errors -import { - collectPerformanceDiagnostics, - initTelemetry, - pong, - recordEvent, - sendDeploymentAlert, -} from "@/background/telemetry"; // Depends on contentScript/messenger to pass strictNullCheck +import { removeExtensionForEveryTab } from "@/background/removeExtensionForEveryTab"; // 203 strictNullCheck errors +import { debouncedActivateStarterMods as installStarterBlueprints } from "@/background/starterMods"; // 209 strictNullCheck errors import { setCopilotProcessData } from "@/background/partnerHandlers"; // Depends on contentScript/messenger to pass strictNullCheck @@ -53,7 +45,6 @@ expectContext("background"); declare global { interface MessengerMethods { - GET_AVAILABLE_VERSION: typeof getAvailableVersion; PRELOAD_CONTEXT_MENUS: typeof preloadContextMenus; UNINSTALL_CONTEXT_MENU: typeof uninstallContextMenu; ENSURE_CONTEXT_MENU: typeof ensureContextMenu; @@ -62,9 +53,6 @@ declare global { INSTALL_STARTER_BLUEPRINTS: typeof installStarterBlueprints; - PING: typeof pong; - COLLECT_PERFORMANCE_DIAGNOSTICS: typeof collectPerformanceDiagnostics; - REMOVE_EXTENSION_EVERY_TAB: typeof removeExtensionForEveryTab; REQUEST_RUN_IN_OPENER: typeof requestRunInOpener; @@ -72,10 +60,6 @@ declare global { REQUEST_RUN_IN_TOP: typeof requestRunInTop; REQUEST_RUN_IN_OTHER_TABS: typeof requestRunInOtherTabs; REQUEST_RUN_IN_ALL_FRAMES: typeof requestRunInAllFrames; - - RECORD_EVENT: typeof recordEvent; - INIT_TELEMETRY: typeof initTelemetry; - SEND_DEPLOYMENT_ALERT: typeof sendDeploymentAlert; } } @@ -85,15 +69,10 @@ export default function registerMessenger(): void { INSTALL_STARTER_BLUEPRINTS: installStarterBlueprints, - GET_AVAILABLE_VERSION: getAvailableVersion, - PRELOAD_CONTEXT_MENUS: preloadContextMenus, UNINSTALL_CONTEXT_MENU: uninstallContextMenu, ENSURE_CONTEXT_MENU: ensureContextMenu, - PING: pong, - COLLECT_PERFORMANCE_DIAGNOSTICS: collectPerformanceDiagnostics, - REMOVE_EXTENSION_EVERY_TAB: removeExtensionForEveryTab, REQUEST_RUN_IN_OPENER: requestRunInOpener, @@ -101,9 +80,5 @@ export default function registerMessenger(): void { REQUEST_RUN_IN_TOP: requestRunInTop, REQUEST_RUN_IN_OTHER_TABS: requestRunInOtherTabs, REQUEST_RUN_IN_ALL_FRAMES: requestRunInAllFrames, - - RECORD_EVENT: recordEvent, - INIT_TELEMETRY: initTelemetry, - SEND_DEPLOYMENT_ALERT: sendDeploymentAlert, }); } diff --git a/src/background/messenger/strict/api.ts b/src/background/messenger/strict/api.ts index 05c6d965db..43ca08887b 100644 --- a/src/background/messenger/strict/api.ts +++ b/src/background/messenger/strict/api.ts @@ -26,6 +26,8 @@ import type { RemoteResponse } from "@/types/contract"; import { type SanitizedIntegrationConfig } from "@/integrations/integrationTypes"; import { type Nullishable } from "@/utils/nullishUtils"; +export const getAvailableVersion = getMethod("GET_AVAILABLE_VERSION", bg); + export const showMySidePanel = getMethod("SHOW_MY_SIDE_PANEL", bg); export const waitForContentScript = getMethod("WAIT_FOR_CONTENT_SCRIPT", bg); @@ -109,3 +111,15 @@ export const performConfiguredRequestInBackground = getMethod( export const getPartnerPrincipals = getMethod("GET_PARTNER_PRINCIPALS", bg); export const launchAuthIntegration = getMethod("LAUNCH_AUTH_INTEGRATION", bg); + +export const ping = getMethod("PING", bg); +export const collectPerformanceDiagnostics = getMethod( + "COLLECT_PERFORMANCE_DIAGNOSTICS", + bg, +); + +// Use this instead: `import reportError from "@/telemetry/reportError"` +// export const recordError = getNotifier("RECORD_ERROR", bg); + +export const initTelemetry = getNotifier("INIT_TELEMETRY", bg); +export const sendDeploymentAlert = getNotifier("SEND_DEPLOYMENT_ALERT", bg); diff --git a/src/background/messenger/strict/registration.ts b/src/background/messenger/strict/registration.ts index 11ecf81ad3..6c15f69831 100644 --- a/src/background/messenger/strict/registration.ts +++ b/src/background/messenger/strict/registration.ts @@ -56,6 +56,14 @@ import { getPartnerPrincipals, launchAuthIntegration, } from "@/background/partnerIntegrations"; +import { getAvailableVersion } from "@/background/installer"; +import { + collectPerformanceDiagnostics, + initTelemetry, + pong, + recordEvent, + sendDeploymentAlert, +} from "@/background/telemetry"; expectContext("background"); @@ -107,6 +115,14 @@ declare global { GET_PARTNER_PRINCIPALS: typeof getPartnerPrincipals; LAUNCH_AUTH_INTEGRATION: typeof launchAuthIntegration; + + GET_AVAILABLE_VERSION: typeof getAvailableVersion; + + PING: typeof pong; + COLLECT_PERFORMANCE_DIAGNOSTICS: typeof collectPerformanceDiagnostics; + RECORD_EVENT: typeof recordEvent; + INIT_TELEMETRY: typeof initTelemetry; + SEND_DEPLOYMENT_ALERT: typeof sendDeploymentAlert; } } @@ -158,5 +174,13 @@ export default function registerMessenger(): void { GET_PARTNER_PRINCIPALS: getPartnerPrincipals, LAUNCH_AUTH_INTEGRATION: launchAuthIntegration, + + GET_AVAILABLE_VERSION: getAvailableVersion, + + PING: pong, + COLLECT_PERFORMANCE_DIAGNOSTICS: collectPerformanceDiagnostics, + RECORD_EVENT: recordEvent, + INIT_TELEMETRY: initTelemetry, + SEND_DEPLOYMENT_ALERT: sendDeploymentAlert, }); } diff --git a/src/background/restrictUnauthenticatedUrlAccess.test.ts b/src/background/restrictUnauthenticatedUrlAccess.test.ts index aca607ff0b..d8d317d39e 100644 --- a/src/background/restrictUnauthenticatedUrlAccess.test.ts +++ b/src/background/restrictUnauthenticatedUrlAccess.test.ts @@ -36,6 +36,7 @@ jest.mock("@/auth/authStorage", () => ({ })); jest.mock("@/utils/extensionUtils", () => ({ + ...jest.requireActual("@/utils/extensionUtils"), forEachTab: jest.fn(), })); diff --git a/src/background/telemetry.ts b/src/background/telemetry.ts index a7e1c8db18..5fc3a01cfc 100644 --- a/src/background/telemetry.ts +++ b/src/background/telemetry.ts @@ -29,7 +29,7 @@ import { count as registrySize } from "@/registry/packageRegistry"; import { count as logSize } from "@/telemetry/logging"; import { count as traceSize } from "@/telemetry/trace"; import { getUUID } from "@/telemetry/telemetryHelpers"; -import { getTabsWithAccess } from "@/utils/extensionUtils"; +import { getExtensionVersion, getTabsWithAccess } from "@/utils/extensionUtils"; import { type Event } from "@/telemetry/events"; const EVENT_BUFFER_DEBOUNCE_MS = 2000; @@ -75,22 +75,22 @@ interface UserSummary { /** * The version_name from the manifest. */ - versionName: string; + versionName?: string; /** * The number of active mod components. */ - numActiveExtensions: number; + numActiveExtensions: number | null; /** * The number of active mods. */ - numActiveBlueprints: number; + numActiveBlueprints: number | null; /** * The number of active starer bricks. */ - numActiveExtensionPoints: number; + numActiveExtensionPoints: number | null; /** * The detected operating system. @@ -105,7 +105,7 @@ interface UserSummary { /** * The detected browser version. */ - $browser_version: number; + $browser_version: number | null; } /** @@ -274,13 +274,14 @@ export async function TEST_flushAll(): Promise { async function collectUserSummary(): Promise { const { os } = await browser.runtime.getPlatformInfo(); - const { version, version_name: versionName } = browser.runtime.getManifest(); + const { version_name: versionName } = browser.runtime.getManifest(); + const version = getExtensionVersion(); // Not supported on Chromium, and may require additional permissions // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/getBrowserInfo // const {name: browserName} = await browser.runtime.getBrowserInfo(); - let numActiveExtensions: number = null; - let numActiveExtensionPoints: number = null; - let numActiveBlueprints: number = null; + let numActiveExtensions: number | null = null; + let numActiveExtensionPoints: number | null = null; + let numActiveBlueprints: number | null = null; try { const { extensions } = await getModComponentState(); @@ -336,11 +337,9 @@ export async function recordEvent({ data: UnknownObject | undefined; }): Promise { if (await allowsTrack()) { - const { - version, - version_name: versionName, - manifest_version: manifestVersion, - } = browser.runtime.getManifest(); + const { version_name: versionName, manifest_version: manifestVersion } = + browser.runtime.getManifest(); + const version = getExtensionVersion(); const telemetryEvent = { uid: await getUUID(), event, diff --git a/src/bricks/renderers/CustomFormComponent.tsx b/src/bricks/renderers/CustomFormComponent.tsx index 9d081cb6eb..66263fa042 100644 --- a/src/bricks/renderers/CustomFormComponent.tsx +++ b/src/bricks/renderers/CustomFormComponent.tsx @@ -34,6 +34,7 @@ import DescriptionField from "@/components/formBuilder/DescriptionField"; import TextAreaWidget from "@/components/formBuilder/TextAreaWidget"; import RjsfSubmitContext from "@/components/formBuilder/RjsfSubmitContext"; import { cloneDeep } from "lodash"; +import { useStylesheetsContextWithFormDefault } from "@/components/StylesheetsContext"; const FIELDS = { DescriptionField, @@ -65,6 +66,7 @@ export type CustomFormComponentProps = { resetOnSubmit?: boolean; className?: string; stylesheets?: string[]; + disableParentStyles?: boolean; }; const CustomFormComponent: React.FunctionComponent< @@ -78,7 +80,8 @@ const CustomFormComponent: React.FunctionComponent< className, onSubmit, resetOnSubmit = false, - stylesheets, + disableParentStyles = false, + stylesheets: newStylesheets, }) => { // Use useRef instead of useState because we don't need/want a re-render when count changes // This ref is used to track the onSubmit run number for runtime tracing @@ -99,6 +102,11 @@ const CustomFormComponent: React.FunctionComponent< setKey((prev) => prev + 1); }; + const { stylesheets } = useStylesheetsContextWithFormDefault({ + newStylesheets, + disableParentStyles, + }); + const submitData = async (data: UnknownObject): Promise => { submissionCountRef.current += 1; await onSubmit(data, { diff --git a/src/bricks/renderers/documentView/DocumentView.tsx b/src/bricks/renderers/documentView/DocumentView.tsx index d1f65c5d24..fdb7cc3cba 100644 --- a/src/bricks/renderers/documentView/DocumentView.tsx +++ b/src/bricks/renderers/documentView/DocumentView.tsx @@ -23,10 +23,14 @@ import { type DocumentViewProps } from "./DocumentViewProps"; import DocumentContext from "@/components/documentBuilder/render/DocumentContext"; import { Stylesheets } from "@/components/Stylesheets"; import { joinPathParts } from "@/utils/formUtils"; +import StylesheetsContext, { + useStylesheetsContextWithDocumentDefault, +} from "@/components/StylesheetsContext"; const DocumentView: React.FC = ({ body, - stylesheets, + stylesheets: newStylesheets, + disableParentStyles, options, meta, onAction, @@ -41,26 +45,33 @@ const DocumentView: React.FC = ({ throw new Error("meta.extensionId is required for DocumentView"); } + const { stylesheets } = useStylesheetsContextWithDocumentDefault({ + newStylesheets, + disableParentStyles, + }); + return ( // Wrap in a React context provider that passes BrickOptions down to any embedded bricks - - {body.map((documentElement, index) => { - const documentBranch = buildDocumentBranch(documentElement, { - staticId: joinPathParts("body", "children"), - // Root of the document, so no branches taken yet - branches: [], - }); + + + {body.map((documentElement, index) => { + const documentBranch = buildDocumentBranch(documentElement, { + staticId: joinPathParts("body", "children"), + // Root of the document, so no branches taken yet + branches: [], + }); - if (documentBranch == null) { - return null; - } + if (documentBranch == null) { + return null; + } - const { Component, props } = documentBranch; - // eslint-disable-next-line react/no-array-index-key -- They have no other unique identifier - return ; - })} - + const { Component, props } = documentBranch; + // eslint-disable-next-line react/no-array-index-key -- They have no other unique identifier + return ; + })} + + ); }; diff --git a/src/bricks/renderers/documentView/DocumentViewProps.tsx b/src/bricks/renderers/documentView/DocumentViewProps.tsx index 65fca3255c..f677846a6f 100644 --- a/src/bricks/renderers/documentView/DocumentViewProps.tsx +++ b/src/bricks/renderers/documentView/DocumentViewProps.tsx @@ -33,6 +33,10 @@ export type DocumentViewProps = { * Remote stylesheets (URLs) to include in the document. */ stylesheets?: string[]; + /** + * Whether to disable the base (bootstrap) styles, plus any inherited styles, on the document (and children). + */ + disableParentStyles?: boolean; options: BrickOptions; meta: { diff --git a/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx b/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx index 689ba4c3e4..d4e2a42748 100644 --- a/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx +++ b/src/bricks/transformers/ephemeralForm/EphemeralFormContent.tsx @@ -32,6 +32,7 @@ import DescriptionField from "@/components/formBuilder/DescriptionField"; import RjsfSelectWidget from "@/components/formBuilder/RjsfSelectWidget"; import TextAreaWidget from "@/components/formBuilder/TextAreaWidget"; import { Stylesheets } from "@/components/Stylesheets"; +import { useStylesheetsContextWithFormDefault } from "@/components/StylesheetsContext"; export const fields = { DescriptionField, @@ -55,8 +56,21 @@ const EphemeralFormContent: React.FC = ({ nonce, isModal, }) => { - const { schema, uiSchema, cancelable, submitCaption, stylesheets } = - definition; + const { + schema, + uiSchema, + cancelable, + submitCaption, + stylesheets: newStylesheets, + disableParentStyles, + } = definition; + + // Ephemeral form can never be nested, but we use this to pull in + // the (boostrap) base themes + const { stylesheets } = useStylesheetsContextWithFormDefault({ + newStylesheets, + disableParentStyles: disableParentStyles ?? false, + }); return ( diff --git a/src/bricks/transformers/extensionDiagnostics.ts b/src/bricks/transformers/extensionDiagnostics.ts index e313d6b551..2686403ee4 100644 --- a/src/bricks/transformers/extensionDiagnostics.ts +++ b/src/bricks/transformers/extensionDiagnostics.ts @@ -18,7 +18,7 @@ import { TransformerABC } from "@/types/bricks/transformerTypes"; import { type Schema } from "@/types/schemaTypes"; import { getDiagnostics as collectFrameDiagnostics } from "@/contentScript/performanceMonitoring"; -import { collectPerformanceDiagnostics as collectExtensionDiagnostics } from "@/background/messenger/api"; +import { collectPerformanceDiagnostics as collectExtensionDiagnostics } from "@/background/messenger/strict/api"; import { propertiesToSchema } from "@/utils/schemaUtils"; class ExtensionDiagnostics extends TransformerABC { diff --git a/src/bricks/transformers/jquery/JQueryReaderOptions.test.tsx b/src/bricks/transformers/jquery/JQueryReaderOptions.test.tsx index 2d12425779..1f6fca507d 100644 --- a/src/bricks/transformers/jquery/JQueryReaderOptions.test.tsx +++ b/src/bricks/transformers/jquery/JQueryReaderOptions.test.tsx @@ -28,7 +28,7 @@ import { menuItemFormStateFactory } from "@/testUtils/factories/pageEditorFactor import { JQueryReader } from "@/bricks/transformers/jquery/JQueryReader"; import registerDefaultWidgets from "@/components/fields/schemaFields/widgets/registerDefaultWidgets"; import { waitForEffect } from "@/testUtils/testHelpers"; -import { getAttributeExamples } from "@/contentScript/messenger/api"; +import { getAttributeExamples } from "@/contentScript/messenger/strict/api"; import { screen } from "@testing-library/react"; import SchemaFieldContext from "@/components/fields/schemaFields/SchemaFieldContext"; import devtoolFieldOverrides from "@/pageEditor/fields/devtoolFieldOverrides"; diff --git a/src/bricks/transformers/jquery/JQueryReaderOptions.tsx b/src/bricks/transformers/jquery/JQueryReaderOptions.tsx index f177a23669..df9b1c0d96 100644 --- a/src/bricks/transformers/jquery/JQueryReaderOptions.tsx +++ b/src/bricks/transformers/jquery/JQueryReaderOptions.tsx @@ -36,7 +36,7 @@ import SelectWidget, { type SelectLike, } from "@/components/form/widgets/SelectWidget"; import useAsyncState from "@/hooks/useAsyncState"; -import { getAttributeExamples } from "@/contentScript/messenger/api"; +import { getAttributeExamples } from "@/contentScript/messenger/strict/api"; import { fallbackValue } from "@/utils/asyncStateUtils"; import { type AttributeExample } from "@/contentScript/pageEditor/types"; import CollapsibleFieldSection from "@/pageEditor/fields/CollapsibleFieldSection"; diff --git a/src/components/StylesheetsContext.ts b/src/components/StylesheetsContext.ts new file mode 100644 index 0000000000..d5a7c831da --- /dev/null +++ b/src/components/StylesheetsContext.ts @@ -0,0 +1,109 @@ +/* + * 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 React, { useContext } from "react"; +import bootstrap from "@/vendors/bootstrapWithoutRem.css?loadAsUrl"; +import bootstrapOverrides from "@/sidebar/sidebarBootstrapOverrides.scss?loadAsUrl"; +import custom from "@/bricks/renderers/customForm.css?loadAsUrl"; + +export type StylesheetsContextType = { + stylesheets: string[] | null; +}; + +const StylesheetsContext = React.createContext({ + stylesheets: null, +}); + +function useStylesheetsContextWithDefaultValues({ + newStylesheets, + defaultStylesheets, + disableParentStyles, +}: { + newStylesheets: string[] | undefined; + defaultStylesheets: string[]; + disableParentStyles: boolean; +}): { + stylesheets: string[]; +} { + const { stylesheets: inheritedStylesheets } = useContext(StylesheetsContext); + + const stylesheets: string[] = []; + + if (!disableParentStyles) { + if (inheritedStylesheets == null) { + stylesheets.push(...defaultStylesheets); + } else { + stylesheets.push(...inheritedStylesheets); + } + } + + if (newStylesheets != null) { + stylesheets.push(...newStylesheets); + } + + return { stylesheets }; +} + +export function useStylesheetsContextWithDocumentDefault({ + newStylesheets, + disableParentStyles, +}: { + newStylesheets: string[] | undefined; + disableParentStyles: boolean; +}): { + stylesheets: string[]; +} { + return useStylesheetsContextWithDefaultValues({ + newStylesheets, + defaultStylesheets: [ + bootstrap, + bootstrapOverrides, + // DocumentView.css is an artifact produced by webpack, see the DocumentView entrypoint included in + // `webpack.config.mjs`. We build styles needed to render documents separately from the rest of the sidebar + // in order to isolate the rendered document from the custom Bootstrap theme included in the Sidebar app + "/DocumentView.css", + // Required because it can be nested in the DocumentView. + "/CustomFormComponent.css", + ], + disableParentStyles, + }); +} + +export function useStylesheetsContextWithFormDefault({ + newStylesheets, + disableParentStyles, +}: { + newStylesheets: string[] | undefined; + disableParentStyles: boolean; +}): { + stylesheets: string[]; +} { + return useStylesheetsContextWithDefaultValues({ + newStylesheets, + defaultStylesheets: [ + bootstrap, + bootstrapOverrides, + // CustomFormComponent.css and EphemeralFormContent.css are artifacts produced by webpack, see the entrypoints. + "/EphemeralFormContent.css", + "/CustomFormComponent.css", + custom, + ], + disableParentStyles, + }); +} + +export default StylesheetsContext; diff --git a/src/components/quickBar/QuickBarApp.tsx b/src/components/quickBar/QuickBarApp.tsx index 4f12d03a16..4dc2db20f8 100644 --- a/src/components/quickBar/QuickBarApp.tsx +++ b/src/components/quickBar/QuickBarApp.tsx @@ -33,6 +33,7 @@ import { once } from "lodash"; import { MAX_Z_INDEX, PIXIEBRIX_QUICK_BAR_CONTAINER_CLASS, + QUICK_BAR_READY_ATTRIBUTE, } from "@/domConstants"; import useEventListener from "@/hooks/useEventListener"; import { Stylesheets } from "@/components/Stylesheets"; @@ -203,6 +204,11 @@ export const QuickBarApp: React.FC = () => ( ); +function markQuickBarReady() { + const html = globalThis.document?.documentElement; + html.setAttribute(QUICK_BAR_READY_ATTRIBUTE, "true"); +} + export const initQuickBarApp = once(async () => { expectContext("contentScript"); @@ -226,6 +232,8 @@ export const initQuickBarApp = once(async () => { ReactDOM.render(, container); console.debug("Initialized quick bar"); + markQuickBarReady(); + onContextInvalidated.addListener(() => { console.debug("Removed quick bar due to context invalidation"); ReactDOM.unmountComponentAtNode(container); diff --git a/src/contentScript/contentScriptCore.ts b/src/contentScript/contentScriptCore.ts index f5fc557de9..610caa7cc5 100644 --- a/src/contentScript/contentScriptCore.ts +++ b/src/contentScript/contentScriptCore.ts @@ -28,7 +28,7 @@ import registerBuiltinBricks from "@/bricks/registerBuiltinBricks"; import registerContribBlocks from "@/contrib/registerContribBlocks"; import brickRegistry from "@/bricks/registry"; import { initNavigation } from "@/contentScript/lifecycle"; -import { initTelemetry } from "@/background/messenger/api"; +import { initTelemetry } from "@/background/messenger/strict/api"; import { initToaster } from "@/utils/notify"; import { initPartnerIntegrations } from "@/contentScript/partnerIntegrations"; import { diff --git a/src/contentScript/contentScriptPlatform.ts b/src/contentScript/contentScriptPlatform.ts index c15915ebbc..908f33fd42 100644 --- a/src/contentScript/contentScriptPlatform.ts +++ b/src/contentScript/contentScriptPlatform.ts @@ -49,7 +49,6 @@ import { writeToClipboard } from "@/utils/clipboardUtils"; import { snippetRegistry } from "@/contentScript/snippetShortcutMenu/snippetShortcutMenuController"; import BackgroundLogger from "@/telemetry/BackgroundLogger"; import * as sidebarController from "@/contentScript/sidebarController"; -import { validateSemVerString } from "@/types/helpers"; import type { UUID } from "@/types/stringTypes"; import { PlatformBase } from "@/platform/platformBase"; import type { Nullishable } from "@/utils/nullishUtils"; @@ -61,6 +60,7 @@ import { InteractiveLoginRequiredError } from "@/errors/authErrors"; import { deferLogin } from "@/contentScript/integrations/deferredLoginController"; import { flagOn } from "@/auth/featureFlagStorage"; import { selectionMenuActionRegistry } from "@/contentScript/textSelectionMenu/selectionMenuController"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * @file Platform definition for mods running in a content script @@ -91,10 +91,7 @@ class ContentScriptPlatform extends PlatformBase { }); constructor() { - super( - "contentScript", - validateSemVerString(browser.runtime.getManifest().version), - ); + super("contentScript", getExtensionVersion()); } override capabilities: PlatformCapability[] = [ diff --git a/src/contentScript/messenger/api.ts b/src/contentScript/messenger/api.ts index 5c61d1fba9..f854cf8724 100644 --- a/src/contentScript/messenger/api.ts +++ b/src/contentScript/messenger/api.ts @@ -42,10 +42,6 @@ export const removeInstalledExtension = getNotifier( export const resetTab = getNotifier("RESET_TAB"); export const toggleQuickBar = getMethod("TOGGLE_QUICK_BAR"); -export const insertPanel = getMethod("INSERT_PANEL"); -export const insertButton = getMethod("INSERT_BUTTON"); -export const getAttributeExamples = getMethod("GET_ATTRIBUTE_EXAMPLES"); - export const runBlock = getMethod("RUN_SINGLE_BLOCK"); export const runRendererBlock = getMethod("RUN_RENDERER_BLOCK"); @@ -59,7 +55,6 @@ export const getInstalledExtensionPoints = getMethod( ); export const checkAvailable = getMethod("CHECK_AVAILABLE"); export const runBrick = getMethod("RUN_BRICK"); -export const selectElement = getMethod("SELECT_ELEMENT"); export const runRendererPipeline = getMethod("RUN_RENDERER_PIPELINE"); export const runHeadlessPipeline = getMethod("RUN_HEADLESS_PIPELINE"); @@ -67,8 +62,4 @@ export const runMapArgs = getMethod("RUN_MAP_ARGS"); export const getCopilotHostData = getMethod("GET_COPILOT_HOST_DATA"); -export const reloadMarketplaceEnhancements = getMethod( - "RELOAD_MARKETPLACE_ENHANCEMENTS", -); - export const showLoginBanner = getMethod("SHOW_LOGIN_BANNER"); diff --git a/src/contentScript/messenger/registration.ts b/src/contentScript/messenger/registration.ts index 3953a65f75..5747e61237 100644 --- a/src/contentScript/messenger/registration.ts +++ b/src/contentScript/messenger/registration.ts @@ -30,30 +30,25 @@ import { queueReactivateTab, reactivateTab, removePersistedExtension, -} from "@/contentScript/lifecycle"; // 214 strictNullCheck errors -import { insertPanel } from "@/contentScript/pageEditor/insertPanel"; // 216 strictNullCheck errors -import { insertButton } from "@/contentScript/pageEditor/insertButton"; // 222 strictNullCheck errors +} from "@/contentScript/lifecycle"; // 202 strictNullCheck errors import { clearDynamicElements, disableOverlay, enableOverlay, runExtensionPointReader, updateDynamicElement, -} from "@/contentScript/pageEditor/dynamic"; // 217 strictNullCheck errors +} from "@/contentScript/pageEditor/dynamic"; // 205 strictNullCheck errors import { runBlockPreview, resetTab, runRendererBlock, -} from "@/contentScript/pageEditor"; // 219 strictNullCheck errors +} from "@/contentScript/pageEditor"; // 207 strictNullCheck errors import { runBrick } from "@/contentScript/executor"; // Depends on background/messenger to pass strictNullCheck -import selectElement from "@/contentScript/pageEditor/selectElement"; // 196 strictNullCheck errors import { runHeadlessPipeline, runMapArgs, runRendererPipeline, } from "@/contentScript/pipelineProtocol"; // Depends on background/messenger to pass strictNullCheck -import { reloadActivationEnhancements } from "@/contentScript/loadActivationEnhancementsCore"; // 187 strictNullCheck errors -import { getAttributeExamples } from "@/contentScript/pageEditor/elementInformation"; // 185 strictNullCheck errors import { getCopilotHostData } from "@/contrib/automationanywhere/SetCopilotDataEffect"; // Depends on background/messenger to pass strictNullCheck import { showBannerFromConfig } from "@/contentScript/integrations/deferredLoginController"; // Depends on background/messenger to pass strictNullCheck @@ -66,10 +61,6 @@ declare global { REMOVE_INSTALLED_EXTENSION: typeof removePersistedExtension; RESET_TAB: typeof resetTab; - INSERT_PANEL: typeof insertPanel; - INSERT_BUTTON: typeof insertButton; - - GET_ATTRIBUTE_EXAMPLES: typeof getAttributeExamples; RUN_SINGLE_BLOCK: typeof runBlockPreview; RUN_RENDERER_BLOCK: typeof runRendererBlock; @@ -82,7 +73,6 @@ declare global { ENSURE_EXTENSION_POINTS_INSTALLED: typeof ensureInstalled; RUN_BRICK: typeof runBrick; - SELECT_ELEMENT: typeof selectElement; RUN_RENDERER_PIPELINE: typeof runRendererPipeline; RUN_HEADLESS_PIPELINE: typeof runHeadlessPipeline; @@ -91,8 +81,6 @@ declare global { GET_COPILOT_HOST_DATA: typeof getCopilotHostData; SHOW_LOGIN_BANNER: typeof showBannerFromConfig; - - RELOAD_MARKETPLACE_ENHANCEMENTS: typeof reloadActivationEnhancements; } } @@ -103,10 +91,6 @@ export default function registerMessenger(): void { REMOVE_INSTALLED_EXTENSION: removePersistedExtension, RESET_TAB: resetTab, - INSERT_PANEL: insertPanel, - INSERT_BUTTON: insertButton, - - GET_ATTRIBUTE_EXAMPLES: getAttributeExamples, RUN_SINGLE_BLOCK: runBlockPreview, RUN_RENDERER_BLOCK: runRendererBlock, @@ -119,7 +103,6 @@ export default function registerMessenger(): void { ENSURE_EXTENSION_POINTS_INSTALLED: ensureInstalled, RUN_BRICK: runBrick, - SELECT_ELEMENT: selectElement, RUN_RENDERER_PIPELINE: runRendererPipeline, RUN_HEADLESS_PIPELINE: runHeadlessPipeline, @@ -128,7 +111,5 @@ export default function registerMessenger(): void { GET_COPILOT_HOST_DATA: getCopilotHostData, SHOW_LOGIN_BANNER: showBannerFromConfig, - - RELOAD_MARKETPLACE_ENHANCEMENTS: reloadActivationEnhancements, }); } diff --git a/src/contentScript/messenger/strict/api.ts b/src/contentScript/messenger/strict/api.ts index 628a89ae15..d730063f4d 100644 --- a/src/contentScript/messenger/strict/api.ts +++ b/src/contentScript/messenger/strict/api.ts @@ -55,3 +55,12 @@ export const notify = { }; export const cancelSelect = getMethod("CANCEL_SELECT_ELEMENT"); + +export const reloadMarketplaceEnhancements = getMethod( + "RELOAD_MARKETPLACE_ENHANCEMENTS", +); +export const getAttributeExamples = getMethod("GET_ATTRIBUTE_EXAMPLES"); +export const selectElement = getMethod("SELECT_ELEMENT"); + +export const insertPanel = getMethod("INSERT_PANEL"); +export const insertButton = getMethod("INSERT_BUTTON"); diff --git a/src/contentScript/messenger/strict/registration.ts b/src/contentScript/messenger/strict/registration.ts index f43af5265c..b03edd3e38 100644 --- a/src/contentScript/messenger/strict/registration.ts +++ b/src/contentScript/messenger/strict/registration.ts @@ -19,7 +19,7 @@ import { hideMv2SidebarInTopFrame, - showSidebar, + showSidebarInTopFrame, sidebarWasLoaded, updateSidebar, removeExtensions as removeSidebars, @@ -46,6 +46,11 @@ import showWalkthroughModal from "@/components/walkthroughModal/showWalkthroughM import { registerMethods } from "webext-messenger"; import { toggleQuickBar } from "@/components/quickBar/QuickBarApp"; import { cancelSelect } from "@/contentScript/pageEditor/elementPicker"; +import { reloadActivationEnhancements } from "@/contentScript/loadActivationEnhancementsCore"; +import { getAttributeExamples } from "@/contentScript/pageEditor/elementInformation"; +import selectElement from "@/contentScript/pageEditor/selectElement"; +import { insertPanel } from "@/contentScript/pageEditor/insertPanel"; +import { insertButton } from "@/contentScript/pageEditor/insertButton"; declare global { interface MessengerMethods { @@ -54,7 +59,7 @@ declare global { FORM_CANCEL: typeof cancelForm; UPDATE_SIDEBAR: typeof updateSidebar; SIDEBAR_WAS_LOADED: typeof sidebarWasLoaded; - SHOW_SIDEBAR: typeof showSidebar; + SHOW_SIDEBAR: typeof showSidebarInTopFrame; HIDE_SIDEBAR: typeof hideMv2SidebarInTopFrame; REMOVE_SIDEBARS: typeof removeSidebars; HANDLE_MENU_ACTION: typeof handleMenuAction; @@ -75,6 +80,11 @@ declare global { WALKTHROUGH_MODAL_SHOW: typeof showWalkthroughModal; TOGGLE_QUICK_BAR: typeof toggleQuickBar; CANCEL_SELECT_ELEMENT: typeof cancelSelect; + RELOAD_MARKETPLACE_ENHANCEMENTS: typeof reloadActivationEnhancements; + GET_ATTRIBUTE_EXAMPLES: typeof getAttributeExamples; + SELECT_ELEMENT: typeof selectElement; + INSERT_PANEL: typeof insertPanel; + INSERT_BUTTON: typeof insertButton; } } export default function registerMessenger(): void { @@ -84,7 +94,7 @@ export default function registerMessenger(): void { FORM_CANCEL: cancelForm, UPDATE_SIDEBAR: updateSidebar, SIDEBAR_WAS_LOADED: sidebarWasLoaded, - SHOW_SIDEBAR: showSidebar, + SHOW_SIDEBAR: showSidebarInTopFrame, HIDE_SIDEBAR: hideMv2SidebarInTopFrame, REMOVE_SIDEBARS: removeSidebars, HANDLE_MENU_ACTION: handleMenuAction, @@ -105,5 +115,10 @@ export default function registerMessenger(): void { WALKTHROUGH_MODAL_SHOW: showWalkthroughModal, TOGGLE_QUICK_BAR: toggleQuickBar, CANCEL_SELECT_ELEMENT: cancelSelect, + RELOAD_MARKETPLACE_ENHANCEMENTS: reloadActivationEnhancements, + GET_ATTRIBUTE_EXAMPLES: getAttributeExamples, + SELECT_ELEMENT: selectElement, + INSERT_PANEL: insertPanel, + INSERT_BUTTON: insertButton, }); } diff --git a/src/contentScript/pageEditor/insertButton.tsx b/src/contentScript/pageEditor/insertButton.tsx index 9ea7dce263..feb15b6291 100644 --- a/src/contentScript/pageEditor/insertButton.tsx +++ b/src/contentScript/pageEditor/insertButton.tsx @@ -36,7 +36,7 @@ export async function insertButton( "./beautify" ); - let selected; + let selected: HTMLElement[]; if (useNewFilter) { const { elements } = await userSelectElement({ filter: `:is(a, button):not(${PRIVATE_ATTRIBUTES_SELECTOR})`, @@ -52,14 +52,20 @@ export async function insertButton( // if the parent is BUTTON, the user probably just selected the wrong thing if ( selected.length === 1 && - ["A", "BUTTON"].includes(selected[0].parentElement?.tagName) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- length check + ["A", "BUTTON"].includes(selected[0]!.parentElement?.tagName ?? "") ) { - selected = [selected[0].parentElement]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- verified above + selected = [selected[0]!.parentElement!]; } } const { container, selectors: containerSelectors } = findContainer(selected); + if (!containerSelectors[0]) { + throw new Error("No selector found for the button"); + } + console.debug("insertButton", { container, selected }); const element: ButtonSelectionResult = { @@ -75,7 +81,6 @@ export async function insertButton( wrap_line_length: 80, wrap_attributes: "force", }), - shadowDOM: null, position: "append", }, containerInfo: await pageScript.getElementInfo({ diff --git a/src/contentScript/pageEditor/insertPanel.tsx b/src/contentScript/pageEditor/insertPanel.tsx index 41c6c93075..6602525605 100644 --- a/src/contentScript/pageEditor/insertPanel.tsx +++ b/src/contentScript/pageEditor/insertPanel.tsx @@ -36,6 +36,10 @@ export async function insertPanel(): Promise { const { elements: selected } = await userSelectElement(); const { container, selectors } = findContainer(selected); + if (!selectors[0]) { + throw new Error("No selector found for the panel"); + } + return { uuid: uuidv4(), panel: { diff --git a/src/contentScript/pageEditor/selectElement.ts b/src/contentScript/pageEditor/selectElement.ts index ef2846e7b3..c025c2eea6 100644 --- a/src/contentScript/pageEditor/selectElement.ts +++ b/src/contentScript/pageEditor/selectElement.ts @@ -31,14 +31,14 @@ export default async function selectElement({ mode = "element", root, isMulti: initialIsMulti = false, - excludeRandomClasses, + excludeRandomClasses = false, }: { mode: SelectMode; isMulti?: boolean; root?: string; excludeRandomClasses?: boolean; }): Promise { - const rootElements = $safeFind(root).get(); + const rootElements = root ? $safeFind(root).get() : []; if (root && rootElements.length === 0) { throw new NoElementsFoundError(root); @@ -59,10 +59,10 @@ export default async function selectElement({ const { selectors } = findContainer(elements); - findSingleElement(selectors[0]); + findSingleElement(selectors[0] ?? ""); return pageScript.getElementInfo({ - selector: selectors[0], + selector: selectors[0] ?? "", }); } @@ -71,9 +71,10 @@ export default async function selectElement({ if (isMulti) { // If there are rootElements, the elements must all be contained within the same root - activeRoot = rootElements?.find((rootElement) => - elements.every((element) => rootElement.contains(element)), - ); + activeRoot = + rootElements?.find((rootElement) => + elements.every((element) => rootElement.contains(element)), + ) ?? null; return inferMultiElementSelector({ elements, @@ -89,11 +90,12 @@ export default async function selectElement({ ); } - const element = elements[0]; - // At least one much match, otherwise userSelectElement would have thrown - activeRoot = rootElements?.find((rootElement) => - rootElement.contains(element), - ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- at least one element must be present + const element = elements[0]!; + // At least one must match, otherwise userSelectElement would have thrown + activeRoot = + rootElements?.find((rootElement) => rootElement.contains(element)) ?? + null; return inferSingleElementSelector({ root: activeRoot, diff --git a/src/contentScript/performanceMonitoring.ts b/src/contentScript/performanceMonitoring.ts index 3313536497..f61c024a42 100644 --- a/src/contentScript/performanceMonitoring.ts +++ b/src/contentScript/performanceMonitoring.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { ping } from "@/background/messenger/api"; +import { ping } from "@/background/messenger/strict/api"; import { isContextInvalidatedError } from "@/errors/contextInvalidated"; import { isLoadedInIframe } from "@/utils/iframeUtils"; import { getSettingsState } from "@/store/settings/settingsStorage"; diff --git a/src/contentScript/sidebarController.tsx b/src/contentScript/sidebarController.tsx index 76fd19aa87..eae83f83ef 100644 --- a/src/contentScript/sidebarController.tsx +++ b/src/contentScript/sidebarController.tsx @@ -19,10 +19,12 @@ import reportEvent from "@/telemetry/reportEvent"; import { Events } from "@/telemetry/events"; import { expectContext } from "@/utils/expectContext"; import sidebarInThisTab from "@/sidebar/messenger/api"; +import * as contentScriptApi from "@/contentScript/messenger/strict/api"; import { isEmpty, throttle } from "lodash"; import { signalFromEvent } from "abort-utils"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import * as sidebarMv2 from "@/contentScript/sidebarDomControllerLite"; +import { getSidebarElement } from "@/contentScript/sidebarDomControllerLite"; import { type Except } from "type-fest"; import { type RunArgs, RunReason } from "@/types/runtimeTypes"; import { type UUID } from "@/types/stringTypes"; @@ -30,8 +32,8 @@ import { type RegistryId } from "@/types/registryTypes"; import { type ModComponentRef } from "@/types/modComponentTypes"; import type { ActivatePanelOptions, - ModActivationPanelEntry, FormPanelEntry, + ModActivationPanelEntry, PanelEntry, PanelPayload, TemporaryPanelEntry, @@ -41,18 +43,29 @@ import { getFormPanelSidebarEntries } from "@/platform/forms/formController"; import { memoizeUntilSettled } from "@/utils/promiseUtils"; import { getTimedSequence } from "@/types/helpers"; import { isMV3 } from "@/mv3/api"; -import { getErrorMessage } from "@/errors/errorHelpers"; import { focusCaptureDialog } from "@/contentScript/focusCaptureDialog"; import { isLoadedInIframe } from "@/utils/iframeUtils"; import { showMySidePanel } from "@/background/messenger/strict/api"; -import { getSidebarElement } from "@/contentScript/sidebarDomControllerLite"; import focusController from "@/utils/focusController"; import selectionController from "@/utils/selectionController"; -import { messenger } from "webext-messenger"; -import { getSidebarTargetForCurrentTab } from "@/utils/sidePanelUtils"; +import { getTopLevelFrame, messenger } from "webext-messenger"; +import { + getSidebarTargetForCurrentTab, + isUserGestureRequiredError, +} from "@/utils/sidePanelUtils"; const HIDE_SIDEBAR_EVENT_NAME = "pixiebrix:hideSidebar"; +/** + * Event listeners triggered when the sidebar shows and is ready to receive messages. + */ +export const sidebarShowEvents = new SimpleEventTarget(); + +// eslint-disable-next-line local-rules/persistBackgroundData -- Unused there +const panels: PanelEntry[] = []; + +let modActivationPanelEntry: ModActivationPanelEntry | null = null; + /* * Only one check at a time * Cannot throttle because subsequent checks need to be able to be made immediately @@ -88,30 +101,37 @@ const pingSidebar = memoizeUntilSettled( }, 1000) as () => Promise, ); -/** - * Event listeners triggered when the sidebar shows and is ready to receive messages. - */ -export const sidebarShowEvents = new SimpleEventTarget(); - export function sidebarWasLoaded(): void { sidebarShowEvents.emit({ reason: RunReason.MANUAL }); } -// eslint-disable-next-line local-rules/persistBackgroundData -- Unused there -const panels: PanelEntry[] = []; - -let modActivationPanelEntry: ModActivationPanelEntry | null = null; - /** - * Attach the sidebar to the page if it's not already attached. Then re-renders all panels. + * Content script handler for showing the sidebar in the top-level frame. Regular callers should call + * showSidebar instead, which handles calls from iframes. + * + * - Resolves when the sidebar is initialized (responds to a ping) + * - Shows focusCaptureDialog if a user gesture is required + * + * @see showSidebar + * @see pingSidebar + * @throws Error if the sidebar ping fails or does not respond in time */ -export async function showSidebar(): Promise { +// Don't memoizeUntilSettled this method. focusCaptureDialog is memoized which prevents this method from showing +// the focus dialog from multiple times. By allowing multiple concurrent calls to showSidebarInTopFrame, +// a subsequent call might succeed, which will then automatically close the focusCaptureDialog (via it's abort signal) +export async function showSidebarInTopFrame() { reportEvent(Events.SIDEBAR_SHOW); + + if (isLoadedInIframe()) { + console.warn("showSidebarInTopFrame should not be called in an iframe"); + } + + // Defensively handle accidental calls from iframes if (isMV3() || isLoadedInIframe()) { try { await showMySidePanel(); } catch (error) { - if (!getErrorMessage(error).includes("user gesture")) { + if (!isUserGestureRequiredError(error)) { throw error; } @@ -137,6 +157,18 @@ export async function showSidebar(): Promise { } } +/** + * Attach the sidebar to the page if it's not already attached. Safe to call from any frame. Resolves when the + * sidebar is initialized. + * @see showSidebarInTopFrame + */ +export async function showSidebar(): Promise { + // Could consider explicitly calling showSidebarInTopFrame directly if we're already in the top frame. + // But the messenger will already handle that case automatically. + const topLevelFrame = await getTopLevelFrame(); + await contentScriptApi.showSidebar(topLevelFrame); +} + /** * Force-show the panel for the given extension id * @param extensionId the extension UUID diff --git a/src/data/service/errorService.ts b/src/data/service/errorService.ts index 7c0e464bd8..5b43e09930 100644 --- a/src/data/service/errorService.ts +++ b/src/data/service/errorService.ts @@ -22,7 +22,7 @@ import { selectSpecificError, } from "@/errors/errorHelpers"; import { allowsTrack } from "@/telemetry/dnt"; -import { uuidv4, validateSemVerString } from "@/types/helpers"; +import { uuidv4 } from "@/types/helpers"; import { getUserData } from "@/auth/authStorage"; import { isAppRequestError, @@ -42,6 +42,7 @@ import { isObject } from "@/utils/objectUtils"; import type { Timestamp } from "@/types/stringTypes"; import { flagOn } from "@/auth/featureFlagStorage"; import { selectAbsoluteUrl } from "@/utils/urlUtils"; +import { getExtensionVersion } from "@/utils/extensionUtils"; const EVENT_BUFFER_DEBOUNCE_MS = 2000; const EVENT_BUFFER_MAX_MS = 10_000; @@ -75,9 +76,8 @@ async function flush(): Promise { export async function selectExtraContext( error: Error | SerializedError, ): Promise { - const { version, manifest_version: manifestVersion } = - browser.runtime.getManifest(); - const extensionVersion = validateSemVerString(version); + const { manifest_version: manifestVersion } = browser.runtime.getManifest(); + const extensionVersion = getExtensionVersion(); const extraContext: UnknownObject & { extensionVersion: SemVerString } = { extensionVersion, manifestVersion, diff --git a/src/domConstants.ts b/src/domConstants.ts index 0a14cb3a0c..e3a594ab11 100644 --- a/src/domConstants.ts +++ b/src/domConstants.ts @@ -23,6 +23,8 @@ export const MAX_Z_INDEX = NOTIFICATIONS_Z_INDEX - 1; // Let notifications alway export const SELECTION_MENU_READY_ATTRIBUTE = "data-pb-selection-menu-ready"; +export const QUICK_BAR_READY_ATTRIBUTE = "data-pb-quick-bar-ready"; + export const PANEL_FRAME_ID = "pixiebrix-extension"; export const PIXIEBRIX_DATA_ATTR = "data-pb-uuid"; diff --git a/src/errors/businessErrors.ts b/src/errors/businessErrors.ts index 47d6655575..841f0dee6e 100644 --- a/src/errors/businessErrors.ts +++ b/src/errors/businessErrors.ts @@ -27,16 +27,40 @@ import { JQUERY_INVALID_SELECTOR_ERROR } from "@/errors/knownErrorMessages"; */ /** - * Base class for Errors arising from business logic in the brick, not the PixieBrix application/extension itself. + * Base class for Errors arising from user-defined logic/inputs, not PixieBrix itself. * - * Used for blame analysis for reporting and alerting. + * Where possible, use a more specific subclass of BusinessError. Some subclasses have an enriched error view in + * the mod log viewer. + * + * "Business" Errors vs. "Application" Errors: + * - Application errors (subclasses of Error) indicate a bug or failure in PixieBrix itself, which must be addressed + * by the PixieBrix team + * - Business errors indicate a problem with user-defined content or 3rd party services. They must be addressed + * by the customer + * - Business errors are not reported to Datadog, but are reported to the PixieBrix error telemetry service. See + * recordError and reportToErrorService. + * + * Throw a BusinessError (or a BusinessError subclass) to indicate: + * - A logic error in a package definition + * - A logic error in a brick configuration (i.e., a mod definition, or custom brick definition) + * - A runtime error due to user-provided values (e.g., bad configuration options) + * - A failed 3rd-party API call + * + * Use an Application error (i.e., Error subclass) to indicate: + * - A bug in PixieBrix itself + * - A failed API call to a PixieBrix service (e.g., fetching packages) + * + * Other guidelines: + * - Throw an application error for assertions that should never fail, e.g., that the function caller should have + * checked the precondition before calling a function. This guideline applies even if the condition is due to + * package definition/user input, because it represents a bug in our code. */ export class BusinessError extends Error { override name = "BusinessError"; } /** - * Error that a registry definition is invalid + * Error that a registry package definition is invalid. */ export class InvalidDefinitionError extends BusinessError { override name = "InvalidDefinitionError"; @@ -120,8 +144,11 @@ export class InvalidSelectorError extends BusinessError { } /** - * An error indicating an invalid input was provided to a brick. Used for checks that cannot be performed as part - * of JSONSchema input validation + * An error indicating an invalid input was provided to a brick. Used for runtime checks that cannot be performed as + * part of JSONSchema input validation. + * + * Throwing PropError instead of BusinessError allows the Page Editor to show the error on the associated field + * in the brick configuration UI. * * @see InputValidationError */ diff --git a/src/extensionConsole/options.tsx b/src/extensionConsole/options.tsx index 69dfdd1067..572a603441 100644 --- a/src/extensionConsole/options.tsx +++ b/src/extensionConsole/options.tsx @@ -27,7 +27,7 @@ import { render } from "react-dom"; import React from "react"; import App from "@/extensionConsole/App"; import { initToaster } from "@/utils/notify"; -import { initTelemetry } from "@/background/messenger/api"; +import { initTelemetry } from "@/background/messenger/strict/api"; import { initMessengerLogging } from "@/development/messengerLogging"; import { initPerformanceMonitoring } from "@/telemetry/performance"; import { initRuntimeLogging } from "@/development/runtimeLogging"; diff --git a/src/extensionConsole/pages/UpdateBanner.tsx b/src/extensionConsole/pages/UpdateBanner.tsx index 23b9d01492..6394ac3e86 100644 --- a/src/extensionConsole/pages/UpdateBanner.tsx +++ b/src/extensionConsole/pages/UpdateBanner.tsx @@ -17,18 +17,19 @@ import React from "react"; import { Button } from "react-bootstrap"; -import { getAvailableVersion } from "@/background/messenger/api"; +import { getAvailableVersion } from "@/background/messenger/strict/api"; import reportError from "@/telemetry/reportError"; import Banner from "@/components/banner/Banner"; import { gt } from "semver"; import useAsyncState from "@/hooks/useAsyncState"; +import { getExtensionVersion } from "@/utils/extensionUtils"; // XXX: move this kind of async state to the Redux state. export function useUpdateAvailable(): boolean { const { data: updateAvailable } = useAsyncState(async () => { try { const available = await getAvailableVersion(); - const installed = browser.runtime.getManifest().version; + const installed = getExtensionVersion(); return available && installed !== available && gt(available, installed); } catch (error) { reportError(error); @@ -36,7 +37,7 @@ export function useUpdateAvailable(): boolean { } }, []); - return updateAvailable; + return Boolean(updateAvailable); } const UpdateBanner: React.FunctionComponent = () => { diff --git a/src/extensionConsole/pages/mods/ModsPageContent.tsx b/src/extensionConsole/pages/mods/ModsPageContent.tsx index e8557c6991..085189afcd 100644 --- a/src/extensionConsole/pages/mods/ModsPageContent.tsx +++ b/src/extensionConsole/pages/mods/ModsPageContent.tsx @@ -27,14 +27,8 @@ import OnboardingView from "@/extensionConsole/pages/mods/onboardingView/Onboard import EmptyView from "@/extensionConsole/pages/mods/emptyView/EmptyView"; import GetStartedView from "@/extensionConsole/pages/mods/GetStartedView"; import useOnboarding from "@/extensionConsole/pages/mods/onboardingView/useOnboarding"; -import { type TableInstance } from "react-table"; -import { type ModViewItem } from "@/types/modTypes"; +import { type ModsPageContentProps } from "@/extensionConsole/pages/mods/modsPageTypes"; -export type ModsPageContentProps = { - tableInstance: TableInstance; - width: number; - height: number; -}; const ModsPageContent: React.VoidFunctionComponent = ({ tableInstance, width, diff --git a/src/extensionConsole/pages/mods/gridView/GridView.tsx b/src/extensionConsole/pages/mods/gridView/GridView.tsx index f596f4eca2..32764ae687 100644 --- a/src/extensionConsole/pages/mods/gridView/GridView.tsx +++ b/src/extensionConsole/pages/mods/gridView/GridView.tsx @@ -32,7 +32,7 @@ import ListGroupHeader from "@/extensionConsole/pages/mods/listView/ListGroupHea import { uuidv4 } from "@/types/helpers"; import { getUniqueId } from "@/utils/modUtils"; import GridCardErrorBoundary from "@/extensionConsole/pages/mods/gridView/GridCardErrorBoundary"; -import { type ModsPageContentProps } from "@/extensionConsole/pages/mods/ModsPageContent"; +import { type ModsPageContentProps } from "@/extensionConsole/pages/mods/modsPageTypes"; /** * Expands `react-table` rows recursively in chunks of diff --git a/src/extensionConsole/pages/mods/listView/ListView.tsx b/src/extensionConsole/pages/mods/listView/ListView.tsx index 2cf883f04d..1c25dcd424 100644 --- a/src/extensionConsole/pages/mods/listView/ListView.tsx +++ b/src/extensionConsole/pages/mods/listView/ListView.tsx @@ -22,7 +22,7 @@ import { VariableSizeList as List } from "react-window"; import ListGroupHeader from "@/extensionConsole/pages/mods/listView/ListGroupHeader"; import { uuidv4 } from "@/types/helpers"; import ListItemErrorBoundary from "@/extensionConsole/pages/mods/listView/ListItemErrorBoundary"; -import { type ModsPageContentProps } from "@/extensionConsole/pages/mods/ModsPageContent"; +import { type ModsPageContentProps } from "@/extensionConsole/pages/mods/modsPageTypes"; const ROW_HEIGHT_PX = 90; const HEADER_ROW_HEIGHT_PX = 43; diff --git a/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx b/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx index dee4d8fffc..395e770853 100644 --- a/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx +++ b/src/extensionConsole/pages/mods/modals/convertToRecipeModal/ConvertToRecipeModalBody.tsx @@ -26,7 +26,7 @@ import * as Yup from "yup"; import { PACKAGE_REGEX, testIsSemVerString, - validateSemVerString, + normalizeSemVerString, } from "@/types/helpers"; import { pick } from "lodash"; import Form from "@/components/form/Form"; @@ -136,7 +136,7 @@ const ConvertToRecipeModalBody: React.FunctionComponent = () => { () => ({ blueprintId: generatePackageId(scope, extension.label), name: extension.label, - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Created with the PixieBrix Page Editor", }), // eslint-disable-next-line react-hooks/exhaustive-deps -- initial values for the form, we calculate them once diff --git a/src/extensionConsole/pages/mods/modsPageTypes.ts b/src/extensionConsole/pages/mods/modsPageTypes.ts new file mode 100644 index 0000000000..9a85ddd44a --- /dev/null +++ b/src/extensionConsole/pages/mods/modsPageTypes.ts @@ -0,0 +1,25 @@ +/* + * 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 { type ModViewItem } from "@/types/modTypes"; +import { type TableInstance } from "react-table"; + +export type ModsPageContentProps = { + tableInstance: TableInstance; + width: number; + height: number; +}; diff --git a/src/extensionConsole/pages/settings/useDiagnostics.ts b/src/extensionConsole/pages/settings/useDiagnostics.ts index 378e820d3b..2f9fee6529 100644 --- a/src/extensionConsole/pages/settings/useDiagnostics.ts +++ b/src/extensionConsole/pages/settings/useDiagnostics.ts @@ -21,7 +21,7 @@ import useExtensionPermissions, { type DetailedPermissions, } from "@/permissions/useExtensionPermissions"; import { type UnresolvedModComponent } from "@/types/modComponentTypes"; -import { compact, pick, uniqBy } from "lodash"; +import { compact, uniqBy } from "lodash"; import { type StorageEstimate } from "@/types/browserTypes"; import { count as registrySize } from "@/registry/packageRegistry"; import { count as logSize } from "@/telemetry/logging"; @@ -30,6 +30,7 @@ import { count as eventsSize } from "@/background/telemetry"; import useUserAction from "@/hooks/useUserAction"; import download from "downloadjs"; import filenamify from "filenamify"; +import { getExtensionVersion } from "@/utils/extensionUtils"; async function collectDiagnostics({ extensions, @@ -38,10 +39,11 @@ async function collectDiagnostics({ extensions: UnresolvedModComponent[]; permissions: DetailedPermissions; }) { - const manifest = browser.runtime.getManifest(); + const { version_name } = browser.runtime.getManifest(); + const version = getExtensionVersion(); return { userAgent: window.navigator.userAgent, - manifest: pick(manifest, ["version", "version_name"]), + manifest: { version, version_name }, permissions, storage: { storageEstimate: (await navigator.storage.estimate()) as StorageEstimate, diff --git a/src/extensionPages/extensionPagePlatform.ts b/src/extensionPages/extensionPagePlatform.ts index fc4b37844e..0ba10c8f8e 100644 --- a/src/extensionPages/extensionPagePlatform.ts +++ b/src/extensionPages/extensionPagePlatform.ts @@ -19,7 +19,6 @@ import { type PlatformProtocol } from "@/platform/platformProtocol"; import { hideNotification, showNotification } from "@/utils/notify"; import type { PlatformCapability } from "@/platform/capabilities"; import BackgroundLogger from "@/telemetry/BackgroundLogger"; -import { validateSemVerString } from "@/types/helpers"; import type { UUID } from "@/types/stringTypes"; import { traces, @@ -33,6 +32,7 @@ import type { NetworkRequestConfig } from "@/types/networkTypes"; import type { RemoteResponse } from "@/types/contract"; import integrationRegistry from "@/integrations/registry"; import { performConfiguredRequest } from "@/background/requests"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * The extension page platform. @@ -56,10 +56,7 @@ class ExtensionPagePlatform extends PlatformBase { }); constructor() { - super( - "extension", - validateSemVerString(browser.runtime.getManifest().version), - ); + super("extension", getExtensionVersion()); } override alert = window.alert; diff --git a/src/pageEditor/fields/SelectorSelectorWidget.tsx b/src/pageEditor/fields/SelectorSelectorWidget.tsx index 1681d1f707..38b5ff74d8 100644 --- a/src/pageEditor/fields/SelectorSelectorWidget.tsx +++ b/src/pageEditor/fields/SelectorSelectorWidget.tsx @@ -29,11 +29,7 @@ import CreatableAutosuggest, { import SelectorListItem from "@/pageEditor/fields/selectorListItem/SelectorListItem"; import { type Framework } from "@/pageScript/messenger/constants"; import { useField } from "formik"; -import { - disableOverlay, - enableOverlay, - selectElement, -} from "@/contentScript/messenger/api"; +import { disableOverlay, enableOverlay } from "@/contentScript/messenger/api"; import { type SelectMode } from "@/contentScript/pageEditor/types"; import { useSelector } from "react-redux"; import { type SettingsState } from "@/store/settings/settingsTypes"; @@ -48,7 +44,10 @@ import { import WorkshopMessageWidget from "@/components/fields/schemaFields/widgets/WorkshopMessageWidget"; import { type ElementInfo } from "@/utils/inference/selectorTypes"; import { inspectedTab } from "@/pageEditor/context/connection"; -import { cancelSelect } from "@/contentScript/messenger/strict/api"; +import { + cancelSelect, + selectElement, +} from "@/contentScript/messenger/strict/api"; interface ElementSuggestion extends SuggestionTypeBase { value: string; diff --git a/src/pageEditor/panes/save/saveHelpers.test.ts b/src/pageEditor/panes/save/saveHelpers.test.ts index 9653d4d777..059ccec207 100644 --- a/src/pageEditor/panes/save/saveHelpers.test.ts +++ b/src/pageEditor/panes/save/saveHelpers.test.ts @@ -22,7 +22,7 @@ import { replaceModComponent, selectExtensionPointIntegrations, } from "@/pageEditor/panes/save/saveHelpers"; -import { validateRegistryId, validateSemVerString } from "@/types/helpers"; +import { validateRegistryId, normalizeSemVerString } from "@/types/helpers"; import menuItemExtensionAdapter from "@/pageEditor/starterBricks/menuItem"; import { internalStarterBrickMetaFactory, @@ -190,7 +190,7 @@ describe("replaceModComponent round trip", () => { metadata: { id: makeInternalId(modDefinition.definitions.extensionPoint), name: "Internal Starter Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }, } as any); @@ -240,7 +240,7 @@ describe("replaceModComponent round trip", () => { metadata: { id: makeInternalId(modDefinition.definitions.extensionPoint), name: "Internal Starter Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }, } as any); @@ -300,7 +300,7 @@ describe("replaceModComponent round trip", () => { metadata: { id: makeInternalId(modDefinition.definitions.extensionPoint), name: "Internal Starter Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }, } as any); diff --git a/src/pageEditor/sidebar/ModListItem.test.tsx b/src/pageEditor/sidebar/ModListItem.test.tsx index e65369c906..c73ad116da 100644 --- a/src/pageEditor/sidebar/ModListItem.test.tsx +++ b/src/pageEditor/sidebar/ModListItem.test.tsx @@ -23,7 +23,7 @@ import { render } from "@/pageEditor/testHelpers"; import { Accordion, ListGroup } from "react-bootstrap"; import { appApiMock } from "@/testUtils/appApiMock"; import { modDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; describe("ModListItem", () => { it("renders expanded", async () => { @@ -99,7 +99,7 @@ describe("ModListItem", () => { const modDefinition = modDefinitionFactory({ metadata: { ...modMetadata, - version: validateSemVerString("1.0.1"), + version: normalizeSemVerString("1.0.1"), }, }); appApiMock diff --git a/src/pageEditor/sidebar/modals/CreateModModal.tsx b/src/pageEditor/sidebar/modals/CreateModModal.tsx index fd3b020e68..5232609b91 100644 --- a/src/pageEditor/sidebar/modals/CreateModModal.tsx +++ b/src/pageEditor/sidebar/modals/CreateModModal.tsx @@ -20,7 +20,7 @@ import { PACKAGE_REGEX, testIsSemVerString, validateRegistryId, - validateSemVerString, + normalizeSemVerString, } from "@/types/helpers"; import { useDispatch, useSelector } from "react-redux"; import { @@ -88,7 +88,7 @@ function useInitialFormState({ return { id: newModId, name: `${modMetadata.name} (Copy)`, - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: modMetadata.description, }; } @@ -98,7 +98,7 @@ function useInitialFormState({ return { id: generatePackageId(scope, activeElement.label), name: activeElement.label, - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Created with the PixieBrix Page Editor", }; } diff --git a/src/pageEditor/starterBricks/base.ts b/src/pageEditor/starterBricks/base.ts index 11dc13c687..3a92333891 100644 --- a/src/pageEditor/starterBricks/base.ts +++ b/src/pageEditor/starterBricks/base.ts @@ -34,7 +34,7 @@ import { isInnerDefinitionRegistryId, uuidv4, validateRegistryId, - validateSemVerString, + normalizeSemVerString, } from "@/types/helpers"; import { type BrickPipeline, @@ -316,7 +316,7 @@ export function baseSelectExtensionPoint( id: metadata.id, // The server requires the version to save the brick, even though it's not marked as required // in the front-end schemas - version: metadata.version ?? validateSemVerString("1.0.0"), + version: metadata.version ?? normalizeSemVerString("1.0.0"), name: metadata.name, // The server requires the description to save the brick, even though it's not marked as required // in the front-end schemas diff --git a/src/pageEditor/starterBricks/menuItem.ts b/src/pageEditor/starterBricks/menuItem.ts index f9aec3c088..542687f761 100644 --- a/src/pageEditor/starterBricks/menuItem.ts +++ b/src/pageEditor/starterBricks/menuItem.ts @@ -38,7 +38,7 @@ import { getDomain } from "@/permissions/patterns"; import { faMousePointer } from "@fortawesome/free-solid-svg-icons"; import { type ElementConfig } from "@/pageEditor/starterBricks/elementConfig"; import MenuItemConfiguration from "@/pageEditor/tabs/menuItem/MenuItemConfiguration"; -import { insertButton } from "@/contentScript/messenger/api"; +import { insertButton } from "@/contentScript/messenger/strict/api"; import { type ButtonDefinition, type ButtonSelectionResult, diff --git a/src/pageEditor/starterBricks/panel.ts b/src/pageEditor/starterBricks/panel.ts index e74c445312..477c46d3e7 100644 --- a/src/pageEditor/starterBricks/panel.ts +++ b/src/pageEditor/starterBricks/panel.ts @@ -37,7 +37,7 @@ import { getDomain } from "@/permissions/patterns"; import { faWindowMaximize } from "@fortawesome/free-solid-svg-icons"; import { type ElementConfig } from "@/pageEditor/starterBricks/elementConfig"; import PanelConfiguration from "@/pageEditor/tabs/panel/PanelConfiguration"; -import { insertPanel } from "@/contentScript/messenger/api"; +import { insertPanel } from "@/contentScript/messenger/strict/api"; import { type DynamicDefinition, type PanelSelectionResult, diff --git a/src/platform/platformContext.ts b/src/platform/platformContext.ts index 1ab1695704..2a2c66bcae 100644 --- a/src/platform/platformContext.ts +++ b/src/platform/platformContext.ts @@ -21,14 +21,14 @@ import { PlatformCapabilityNotAvailableError, } from "@/platform/capabilities"; import { PlatformBase } from "@/platform/platformBase"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; /** * A platform protocol with no available capabilities. */ export const uninitializedPlatform = new PlatformBase( "uninitialized", - validateSemVerString("0.0.0"), + normalizeSemVerString("0.0.0"), ); /** diff --git a/src/registry/packageRegistry.test.ts b/src/registry/packageRegistry.test.ts index cfd60d5bfb..4cfeee83d1 100644 --- a/src/registry/packageRegistry.test.ts +++ b/src/registry/packageRegistry.test.ts @@ -26,7 +26,7 @@ import { produce } from "immer"; import { appApiMock } from "@/testUtils/appApiMock"; import { defaultModDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories"; import pDefer from "p-defer"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; describe("localRegistry", () => { beforeEach(() => { @@ -65,7 +65,7 @@ describe("localRegistry", () => { it("should return latest version", async () => { const definition = defaultModDefinitionFactory(); const updated = produce(definition, (draft) => { - draft.metadata.version = validateSemVerString("9.9.9"); + draft.metadata.version = normalizeSemVerString("9.9.9"); }); appApiMock.onGet("/api/registry/bricks/").reply(200, [updated, definition]); diff --git a/src/runtime/pipelineTests/component.test.ts b/src/runtime/pipelineTests/component.test.ts index 76b799d48f..21f919dd8d 100644 --- a/src/runtime/pipelineTests/component.test.ts +++ b/src/runtime/pipelineTests/component.test.ts @@ -26,7 +26,7 @@ import { } from "./pipelineTestHelpers"; import { fromJS } from "@/bricks/transformers/brickFactory"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; import { TEST_setContext } from "webext-detect-page"; import { toExpression } from "@/utils/expressionUtils"; @@ -43,7 +43,7 @@ const componentBlock = fromJS(blockRegistry, { metadata: { id: "test/component", name: "Component Brick", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Component block using v1 runtime", }, inputSchema: { diff --git a/src/runtime/pipelineTests/deploymentAlert.test.ts b/src/runtime/pipelineTests/deploymentAlert.test.ts index b0ec110bbc..a86a235092 100644 --- a/src/runtime/pipelineTests/deploymentAlert.test.ts +++ b/src/runtime/pipelineTests/deploymentAlert.test.ts @@ -24,7 +24,7 @@ import { testOptions, throwBrick, } from "./pipelineTestHelpers"; -import { sendDeploymentAlert } from "@/background/messenger/api"; +import { sendDeploymentAlert } from "@/background/messenger/strict/api"; import { type ApiVersion } from "@/types/runtimeTypes"; import { uuidv4 } from "@/types/helpers"; import ConsoleLogger from "@/utils/ConsoleLogger"; diff --git a/src/runtime/reducePipeline.ts b/src/runtime/reducePipeline.ts index 45821f908c..e6fd44ef58 100644 --- a/src/runtime/reducePipeline.ts +++ b/src/runtime/reducePipeline.ts @@ -17,7 +17,7 @@ import { type Logger } from "@/types/loggerTypes"; import { castArray, isPlainObject, once } from "lodash"; -import { requestRun, sendDeploymentAlert } from "@/background/messenger/api"; +import { requestRun } from "@/background/messenger/api"; import { hideNotification, showNotification } from "@/utils/notify"; import { serializeError } from "serialize-error"; import { HeadlessModeError } from "@/bricks/errors"; @@ -73,6 +73,7 @@ import type { RegistryId, RegistryProtocol } from "@/types/registryTypes"; import type { Brick } from "@/types/brickTypes"; import getType from "@/runtime/getType"; import { getPlatform } from "@/platform/platformContext"; +import { sendDeploymentAlert } from "@/background/messenger/strict/api"; // Introduce a layer of indirection to avoid cyclical dependency between runtime and registry // eslint-disable-next-line local-rules/persistBackgroundData -- Static diff --git a/src/sidebar/ConnectedSidebar.test.tsx b/src/sidebar/ConnectedSidebar.test.tsx index 8b674a0df3..260f562884 100644 --- a/src/sidebar/ConnectedSidebar.test.tsx +++ b/src/sidebar/ConnectedSidebar.test.tsx @@ -32,8 +32,17 @@ import { } from "@/testUtils/factories/authFactories"; import { appApiMock } from "@/testUtils/appApiMock"; import { valueToAsyncState } from "@/utils/asyncStateUtils"; +import { isMV3 } from "@/mv3/api"; jest.mock("@/auth/useLinkState"); +jest.mock("@/mv3/api"); + +browser.webNavigation.onBeforeNavigate = { + addListener: jest.fn(), + removeListener: jest.fn(), + hasListener: jest.fn(), + hasListeners: jest.fn(), +}; jest.mock("@/contentScript/messenger/strict/api", () => ({ ensureExtensionPointsInstalled: jest.fn(), @@ -96,8 +105,45 @@ describe("SidebarApp", () => { }, ); + // The navigation listener should not be added for MV2 + expect( + browser.webNavigation.onBeforeNavigate.addListener, + ).not.toHaveBeenCalled(); + await waitForEffect(); expect(asFragment()).toMatchSnapshot(); }); + + describe("mv3", () => { + beforeEach(() => { + jest.mocked(isMV3).mockReturnValue(true); + }); + + test("it registers the navigation listener", async () => { + await mockAuthenticatedMeApiResponse(); + const { unmount } = render( + + + , + { + setupRedux(dispatch) { + dispatch(authActions.setAuth(authStateFactory())); + }, + }, + ); + + // The navigation listener should be added for MV3 + expect( + browser.webNavigation.onBeforeNavigate.addListener, + ).toHaveBeenCalledWith(expect.any(Function)); + + unmount(); + + // Removed on unmount + expect( + browser.webNavigation.onBeforeNavigate.removeListener, + ).toHaveBeenCalledWith(expect.any(Function)); + }); + }); }); diff --git a/src/sidebar/ConnectedSidebar.tsx b/src/sidebar/ConnectedSidebar.tsx index f46dc02a41..d303d77dfb 100644 --- a/src/sidebar/ConnectedSidebar.tsx +++ b/src/sidebar/ConnectedSidebar.tsx @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import React, { useMemo } from "react"; +import React, { useEffect, useMemo } from "react"; import { addListener, removeListener, @@ -40,7 +40,10 @@ import DefaultPanel from "@/sidebar/DefaultPanel"; import { MOD_LAUNCHER } from "@/store/sidebar/constants"; import { ensureExtensionPointsInstalled } from "@/contentScript/messenger/api"; import { getReservedSidebarEntries } from "@/contentScript/messenger/strict/api"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { + getConnectedTabIdMv3, + getConnectedTarget, +} from "@/sidebar/connectedTarget"; import useAsyncEffect from "use-async-effect"; import activateLinkClickHandler from "@/activation/activateLinkClickHandler"; import addFormPanel from "@/store/sidebar/thunks/addFormPanel"; @@ -48,6 +51,9 @@ import addTemporaryPanel from "@/store/sidebar/thunks/addTemporaryPanel"; import removeTemporaryPanel from "@/store/sidebar/thunks/removeTemporaryPanel"; import { type AsyncDispatch } from "@/sidebar/store"; import useEventListener from "@/hooks/useEventListener"; +import { WebNavigation } from "webextension-polyfill"; +import OnBeforeNavigateDetailsType = WebNavigation.OnBeforeNavigateDetailsType; +import { isMV3 } from "@/mv3/api"; /** * Listeners to update the Sidebar's Redux state upon receiving messages from the contentScript. @@ -98,7 +104,30 @@ const ConnectedSidebar: React.VFC = () => { const listener = useConnectedListener(); const sidebarIsEmpty = useSelector(selectIsSidebarEmpty); - // `useAsyncEffect` will run once on component mount since listener and formsRef don't change on renders. + // Listen for navigation events to mark temporary panels as unavailable. + // Not used in MV2 because the sidebar closes automatically on navigation. + useEffect(() => { + const navigationListenerMV3 = (details: OnBeforeNavigateDetailsType) => { + const { frameId, tabId } = details; + const connectedTabId = getConnectedTabIdMv3(); + if (tabId === connectedTabId && frameId === 0) { + console.log("navigationListener:connectedTabId", connectedTabId); + dispatch(sidebarSlice.actions.markTemporaryPanelsAsUnavailable()); + } + }; + + if (isMV3()) { + browser.webNavigation.onBeforeNavigate.addListener(navigationListenerMV3); + } + + return () => { + browser.webNavigation.onBeforeNavigate.removeListener( + navigationListenerMV3, + ); + }; + }, [dispatch]); + + // `useAsyncEffect` will run once on component mount since listeners and formsRef don't change on renders. // We could instead consider moving the initial panel logic to SidebarApp.tsx and pass the entries as the // initial state to the sidebarSlice reducer. useAsyncEffect(async () => { diff --git a/src/sidebar/Tabs.module.scss b/src/sidebar/Tabs.module.scss index a2758d42c5..c8fb4cb6b7 100644 --- a/src/sidebar/Tabs.module.scss +++ b/src/sidebar/Tabs.module.scss @@ -24,6 +24,8 @@ .tabContainer { flex-wrap: nowrap; + // Position relative is needed so that the blur overlay only covers the panel content area, and does not cover the whole sidebar (including the tabs and logo). + position: relative; } .tabWrapper { diff --git a/src/sidebar/Tabs.tsx b/src/sidebar/Tabs.tsx index 1c37f44fa9..4f102ce359 100644 --- a/src/sidebar/Tabs.tsx +++ b/src/sidebar/Tabs.tsx @@ -57,12 +57,12 @@ import { selectEventData } from "@/telemetry/deployments"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import { TemporaryPanelTabPane } from "./TemporaryPanelTabPane"; import { MOD_LAUNCHER } from "@/store/sidebar/constants"; -import { getConnectedTarget } from "@/sidebar/connectedTarget"; -import { cancelForm } from "@/contentScript/messenger/strict/api"; import { useHideEmptySidebar } from "@/sidebar/useHideEmptySidebar"; import removeTemporaryPanel from "@/store/sidebar/thunks/removeTemporaryPanel"; import { type AsyncDispatch } from "@/sidebar/store"; import useOnMountOnly from "@/hooks/useOnMountOnly"; +import UnavailableOverlay from "@/sidebar/UnavailableOverlay"; +import removeFormPanel from "@/store/sidebar/thunks/removeFormPanel"; const ActivateModPanel = lazy( async () => @@ -172,8 +172,7 @@ const Tabs: React.FC = () => { if (isTemporaryPanelEntry(panel)) { await dispatch(removeTemporaryPanel(panel.nonce)); } else if (isFormPanelEntry(panel)) { - const frame = await getConnectedTarget(); - cancelForm(frame, panel.nonce); + await dispatch(removeFormPanel(panel.nonce)); } else if (isModActivationPanelEntry(panel)) { dispatch(sidebarSlice.actions.hideModActivationPanel()); } else { @@ -350,6 +349,11 @@ const Tabs: React.FC = () => { }); }} > + {form.isUnavailable && ( + dispatch(removeFormPanel(form.nonce))} + /> + )} diff --git a/src/sidebar/TemporaryPanelTabPane.tsx b/src/sidebar/TemporaryPanelTabPane.tsx index 73922ea27e..f04f64d7a7 100644 --- a/src/sidebar/TemporaryPanelTabPane.tsx +++ b/src/sidebar/TemporaryPanelTabPane.tsx @@ -29,6 +29,8 @@ import { useDispatch } from "react-redux"; import ErrorBoundary from "@/sidebar/SidebarErrorBoundary"; import resolveTemporaryPanel from "@/store/sidebar/thunks/resolveTemporaryPanel"; import { type AsyncDispatch } from "@/sidebar/store"; +import UnavailableOverlay from "@/sidebar/UnavailableOverlay"; +import removeTemporaryPanel from "@/store/sidebar/thunks/removeTemporaryPanel"; // Need to memoize this to make sure it doesn't rerender unless its entry actually changes // This was part of the fix for issue: https://github.com/pixiebrix/pixiebrix-extension/issues/5646 @@ -64,6 +66,11 @@ export const TemporaryPanelTabPane: React.FC<{ }); }} > + {panel.isUnavailable && ( + dispatch(removeTemporaryPanel(panel.nonce))} + /> + )} . + */ + +import React from "react"; +import styles from "./unavailableOverlay.module.scss"; +import cx from "classnames"; +import { Button, Modal } from "react-bootstrap"; + +const UnavailableOverlay = ({ onClose }: { onClose: () => void }) => ( +
+
+ + + Panel no longer available + + + +

The browser navigated away from the page

+ +
+
+
+
+); + +export default UnavailableOverlay; diff --git a/src/sidebar/activateMod/ActivateModPanel.tsx b/src/sidebar/activateMod/ActivateModPanel.tsx index 168123d64c..5fc20008c6 100644 --- a/src/sidebar/activateMod/ActivateModPanel.tsx +++ b/src/sidebar/activateMod/ActivateModPanel.tsx @@ -28,7 +28,7 @@ import styles from "./ActivateModPanel.module.scss"; import AsyncButton from "@/components/AsyncButton"; import { useDispatch } from "react-redux"; import sidebarSlice from "@/store/sidebar/sidebarSlice"; -import { reloadMarketplaceEnhancements as reloadMarketplaceEnhancementsInContentScript } from "@/contentScript/messenger/api"; +import { reloadMarketplaceEnhancements as reloadMarketplaceEnhancementsInContentScript } from "@/contentScript/messenger/strict/api"; import { getConnectedTarget } from "@/sidebar/connectedTarget"; import cx from "classnames"; import { isEmpty } from "lodash"; diff --git a/src/sidebar/connectedTarget.tsx b/src/sidebar/connectedTarget.tsx index 530482ee95..a97d832a66 100644 --- a/src/sidebar/connectedTarget.tsx +++ b/src/sidebar/connectedTarget.tsx @@ -22,7 +22,7 @@ import { once } from "lodash"; import { type TopLevelFrame, getTopLevelFrame } from "webext-messenger"; import { getTabUrl } from "webext-tools"; -function getConnectedTabIdMv3(): number { +export function getConnectedTabIdMv3(): number { expectContext("sidebar"); const tabId = new URLSearchParams(window.location.search).get("tabId"); assertNotNullish( diff --git a/src/sidebar/unavailableOverlay.module.scss b/src/sidebar/unavailableOverlay.module.scss new file mode 100644 index 0000000000..a55e6418a7 --- /dev/null +++ b/src/sidebar/unavailableOverlay.module.scss @@ -0,0 +1,58 @@ +/*! + * 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 . + */ + +.unavailableOverlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.5); + justify-content: center; + align-items: flex-start; + backdrop-filter: blur(2.5px); + pointer-events: all; + z-index: 1; // ensure overlay is on top of the sidebar panel content + > div { + display: block; + margin-top: 10vh; + } +} + +.modalDialog { + text-align: center; + margin: 23px; + padding: 16px; + border-radius: 12px; + background: white; + border: 1px solid #cfcbd6; + > div { + border: 0; + } +} + +.modalHeader { + display: block; + padding-bottom: 0; + background: white; + border: 0; +} + +.modalBody { + background: white; + border: 0; +} diff --git a/src/store/editorMigrations.ts b/src/store/editorMigrations.ts index 7beabeac98..ac335106f0 100644 --- a/src/store/editorMigrations.ts +++ b/src/store/editorMigrations.ts @@ -18,7 +18,7 @@ import { type MigrationManifest, type PersistedState } from "redux-persist"; import { type Except } from "type-fest"; import { type EditorState } from "@/pageEditor/pageEditorTypes"; -import { isEmpty, mapValues, omit } from "lodash"; +import { mapValues, omit } from "lodash"; import { type BaseFormStateV1, type BaseFormStateV2, @@ -55,9 +55,9 @@ export const migrations: MigrationManifest = { }; export function migrateIntegrationDependenciesV1toV2( - services: IntegrationDependencyV1[], + services?: IntegrationDependencyV1[], ): IntegrationDependencyV2[] { - if (isEmpty(services)) { + if (!services) { return []; } diff --git a/src/store/enterprise/managedStorage.test.ts b/src/store/enterprise/managedStorage.test.ts index 5de2be25e3..a50649756e 100644 --- a/src/store/enterprise/managedStorage.test.ts +++ b/src/store/enterprise/managedStorage.test.ts @@ -32,11 +32,7 @@ beforeEach(async () => { describe("readManagedStorage", () => { it("reads immediately if managed storage is already initialized", async () => { await initializationTimestamp.set(new Date().toISOString() as Timestamp); - await expect(readManagedStorage()).resolves.toStrictEqual({ - // `jest-webextension-mock`'s storage is shared across sources, the call ends up with the managed storage - // and the local storage mixed together. See https://github.com/clarkbw/jest-webextension-mock/issues/183 - managedStorageInitTimestamp: expect.any(String), - }); + await expect(readManagedStorage()).resolves.toStrictEqual({}); // Should only be called once vs. polling expect(browser.storage.managed.get).toHaveBeenCalledOnce(); @@ -49,9 +45,6 @@ describe("readManagedStorage", () => { it("reads managed storage", async () => { await browser.storage.managed.set({ partnerId: "taco-bell" }); await expect(readManagedStorage()).resolves.toStrictEqual({ - // `jest-webextension-mock`'s storage is shared across sources, the call ends up with the managed storage - // and the local storage mixed together. See https://github.com/clarkbw/jest-webextension-mock/issues/183 - managedStorageInitTimestamp: expect.any(String), partnerId: "taco-bell", }); }); diff --git a/src/store/enterprise/useManagedStorageState.test.ts b/src/store/enterprise/useManagedStorageState.test.ts index aafa345a52..973e208944 100644 --- a/src/store/enterprise/useManagedStorageState.test.ts +++ b/src/store/enterprise/useManagedStorageState.test.ts @@ -66,9 +66,6 @@ describe("useManagedStorageState", () => { expect(result.current).toStrictEqual({ data: { - // `jest-webextension-mock`'s storage is shared across sources, the call ends up with the managed storage - // and the local storage mixed together. See https://github.com/clarkbw/jest-webextension-mock/issues/183 - managedStorageInitTimestamp: expect.any(String), partnerId: "taco-bell", }, isLoading: false, diff --git a/src/store/extensionsMigrations.ts b/src/store/extensionsMigrations.ts index 63f28c0593..9a0b5e6e28 100644 --- a/src/store/extensionsMigrations.ts +++ b/src/store/extensionsMigrations.ts @@ -117,4 +117,6 @@ export function inferModComponentStateVersion( if (isModComponentStateV0(state)) { return 0; } + + throw new Error("Unknown ModComponentState version", { cause: state }); } diff --git a/src/store/sidebar/sidebarSlice.ts b/src/store/sidebar/sidebarSlice.ts index abf183f3cd..a2013beb09 100644 --- a/src/store/sidebar/sidebarSlice.ts +++ b/src/store/sidebar/sidebarSlice.ts @@ -45,6 +45,7 @@ import addTemporaryPanel from "@/store/sidebar/thunks/addTemporaryPanel"; import removeTemporaryPanel from "@/store/sidebar/thunks/removeTemporaryPanel"; import resolveTemporaryPanel from "@/store/sidebar/thunks/resolveTemporaryPanel"; import { initialSidebarState } from "@/store/sidebar/initialState"; +import removeFormPanel from "@/store/sidebar/thunks/removeFormPanel"; function eventKeyExists( state: SidebarState, @@ -217,6 +218,15 @@ const sidebarSlice = createSlice({ fixActiveTabOnRemove(state, entry); }, + markTemporaryPanelsAsUnavailable(state) { + for (const form of state.forms) { + form.isUnavailable = true; + } + + for (const temporaryPanel of state.temporaryPanels) { + temporaryPanel.isUnavailable = true; + } + }, updateTemporaryPanel( state, action: PayloadAction<{ panel: TemporaryPanelEntry }>, @@ -336,6 +346,14 @@ const sidebarSlice = createSlice({ state.activeKey = eventKeyForEntry(newForm); } }) + .addCase(removeFormPanel.fulfilled, (state, action) => { + if (action.payload) { + const { removedEntry, forms } = action.payload; + + state.forms = castDraft(forms); + fixActiveTabOnRemove(state, removedEntry); + } + }) .addCase(addTemporaryPanel.fulfilled, (state, action) => { const { temporaryPanels, activeKey } = action.payload; diff --git a/src/store/sidebar/thunks/removeFormPanel.ts b/src/store/sidebar/thunks/removeFormPanel.ts new file mode 100644 index 0000000000..9431f36dce --- /dev/null +++ b/src/store/sidebar/thunks/removeFormPanel.ts @@ -0,0 +1,57 @@ +/* + * 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 { cancelForm } from "@/contentScript/messenger/strict/api"; +import { getConnectedTarget } from "@/sidebar/connectedTarget"; +import { type SidebarState } from "@/types/sidebarTypes"; +import { type UUID } from "@/types/stringTypes"; +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { partition } from "lodash"; + +type RemoveFormPanelReturn = + | { + removedEntry: SidebarState["forms"][number]; + forms: SidebarState["forms"]; + } + | undefined; + +const removeFormPanel = createAsyncThunk< + RemoveFormPanelReturn, + UUID, + { state: { sidebar: SidebarState } } +>("sidebar/removeFormPanel", async (nonce, { getState }) => { + const { forms } = getState().sidebar; + + const [[removedEntry], otherFormPanels] = partition( + forms, + (panel) => panel.nonce === nonce, + ); + + if (!removedEntry) { + return; + } + + const topLevelFrame = await getConnectedTarget(); + cancelForm(topLevelFrame, nonce); + + return { + removedEntry, + forms: otherFormPanels, + }; +}); + +export default removeFormPanel; diff --git a/src/testUtils/factories/brickFactories.ts b/src/testUtils/factories/brickFactories.ts index c5e3696fe1..7f1f9110f9 100644 --- a/src/testUtils/factories/brickFactories.ts +++ b/src/testUtils/factories/brickFactories.ts @@ -23,7 +23,7 @@ import { registryIdSequence, uuidSequence, } from "@/testUtils/factories/stringFactories"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; import { emptyPermissionsFactory } from "@/permissions/permissionsUtils"; import { minimalSchemaFactory } from "@/utils/schemaUtils"; import type { BrickDefinition } from "@/bricks/transformers/brickFactory"; @@ -35,7 +35,7 @@ import type { ModDefinition } from "@/types/modDefinitionTypes"; export const brickFactory = define({ id: registryIdSequence, name: derive((x: Brick) => `Brick ${x.id}`, "id"), - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), inputSchema: minimalSchemaFactory, permissions: emptyPermissionsFactory, run: jest.fn(), diff --git a/src/testUtils/factories/deploymentFactories.ts b/src/testUtils/factories/deploymentFactories.ts index 0f57a116c5..2e95bc5d12 100644 --- a/src/testUtils/factories/deploymentFactories.ts +++ b/src/testUtils/factories/deploymentFactories.ts @@ -21,7 +21,7 @@ import { type ActivatableDeployment } from "@/types/deploymentTypes"; import { uuidSequence } from "@/testUtils/factories/stringFactories"; import { validateRegistryId, - validateSemVerString, + normalizeSemVerString, validateTimestamp, } from "@/types/helpers"; import { defaultModDefinitionFactory } from "@/testUtils/factories/modDefinitionFactories"; @@ -32,7 +32,7 @@ import { type ModDefinition } from "@/types/modDefinitionTypes"; const deploymentPackageFactory = define({ id: uuidSequence, name: "Test Starter Brick", - version: (n: number) => validateSemVerString(`1.0.${n}`), + version: (n: number) => normalizeSemVerString(`1.0.${n}`), package_id: (n: number) => validateRegistryId(`test/starter-brick-${n}`), }); diff --git a/src/testUtils/factories/metadataFactory.ts b/src/testUtils/factories/metadataFactory.ts index b3a12c2792..54a603e57c 100644 --- a/src/testUtils/factories/metadataFactory.ts +++ b/src/testUtils/factories/metadataFactory.ts @@ -17,11 +17,11 @@ import { define } from "cooky-cutter"; import { type Metadata } from "@/types/registryTypes"; -import { validateRegistryId, validateSemVerString } from "@/types/helpers"; +import { validateRegistryId, normalizeSemVerString } from "@/types/helpers"; export const metadataFactory = define({ id: (n: number) => validateRegistryId(`test/mod-${n}`), name: (n: number) => `Mod ${n}`, description: "Mod generated from factory", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), }); diff --git a/src/testUtils/factories/sidebarEntryFactories.ts b/src/testUtils/factories/sidebarEntryFactories.ts index a242960b81..cd38d2c025 100644 --- a/src/testUtils/factories/sidebarEntryFactories.ts +++ b/src/testUtils/factories/sidebarEntryFactories.ts @@ -17,9 +17,9 @@ import { define, type FactoryConfig } from "cooky-cutter"; import { - type ModActivationPanelEntry, type EntryType, type FormPanelEntry, + type ModActivationPanelEntry, type PanelEntry, type SidebarEntry, type StaticPanelEntry, diff --git a/src/testUtils/platformMock.ts b/src/testUtils/platformMock.ts index 2a0f70b9b7..15a25d778b 100644 --- a/src/testUtils/platformMock.ts +++ b/src/testUtils/platformMock.ts @@ -21,7 +21,7 @@ import ConsoleLogger from "@/utils/ConsoleLogger"; import type { Logger } from "@/types/loggerTypes"; import { SimpleEventTarget } from "@/utils/SimpleEventTarget"; import type { RunArgs } from "@/types/runtimeTypes"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; import type { ToastProtocol } from "@/platform/platformTypes/toastProtocol"; /** @@ -29,7 +29,7 @@ import type { ToastProtocol } from "@/platform/platformTypes/toastProtocol"; */ export const platformMock: PlatformProtocol = { platformName: "mock", - version: validateSemVerString("0.0.0"), + version: normalizeSemVerString("0.0.0"), capabilities: platformCapabilities, open: jest.fn(), alert: jest.fn(), diff --git a/src/testUtils/testAfterEnv.ts b/src/testUtils/testAfterEnv.ts index da5f8c4b6c..99483e84d4 100644 --- a/src/testUtils/testAfterEnv.ts +++ b/src/testUtils/testAfterEnv.ts @@ -35,7 +35,8 @@ browser.runtime.onMessage.addListener = jest.fn(); browser.runtime.id = "mpjjildTESTIDkfjnepo"; browser.runtime.getManifest = jest.fn().mockReturnValue({ - version: "1.5.2", + // Validate that 4-digit version numbers are supported + version: "1.5.2.3000", }); browser.runtime.getURL = (path) => `chrome-extension://abcxyz/${path}`; diff --git a/src/tsconfig.strictNullChecks.json b/src/tsconfig.strictNullChecks.json index 53f6938771..c41dfe3f8a 100644 --- a/src/tsconfig.strictNullChecks.json +++ b/src/tsconfig.strictNullChecks.json @@ -4,9 +4,12 @@ "strictNullChecks": true }, "files": [ + "./contentScript/pageEditor/selectElement.ts", + "./types/helpers.test.ts", "../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", @@ -75,6 +78,7 @@ "./background/dataStore.ts", "./background/externalProtocol.ts", "./background/initTheme.ts", + "./background/installer.ts", "./background/locator.ts", "./background/messenger/strict/api.ts", "./background/messenger/strict/registration.ts", @@ -90,6 +94,7 @@ "./background/setToolbarIconFromTheme.ts", "./background/sidePanel.ts", "./background/tabs.ts", + "./background/telemetry.ts", "./background/toolbarBadge.ts", "./background/walkthroughModalTrigger.ts", "./bricks/available.ts", @@ -184,6 +189,7 @@ "./bricks/transformers/encode.ts", "./bricks/transformers/ephemeralForm/EphemeralFormContent.tsx", "./bricks/transformers/ephemeralForm/formTransformer.ts", + "./bricks/transformers/extensionDiagnostics.ts", "./bricks/transformers/httpGet.ts", "./bricks/transformers/javascript.ts", "./bricks/transformers/jq.ts", @@ -246,6 +252,7 @@ "./components/StopPropagation.tsx", "./components/Stylesheets.test.tsx", "./components/Stylesheets.tsx", + "./components/StylesheetsContext.ts", "./components/TooltipIconButton.tsx", "./components/UnstyledButton.tsx", "./components/addBlockModal/TagList.tsx", @@ -377,6 +384,8 @@ "./contentScript/integrations/LoginBanners.tsx", "./contentScript/integrations/bannerDomController.tsx", "./contentScript/integrations/deferredLoginTypes.ts", + "./contentScript/loadActivationEnhancements.ts", + "./contentScript/loadActivationEnhancementsCore.ts", "./contentScript/messenger/runBrickTypes.ts", "./contentScript/messenger/strict/api.ts", "./contentScript/messenger/strict/registration.ts", @@ -384,8 +393,12 @@ "./contentScript/pageEditor/beautify.ts", "./contentScript/pageEditor/elementInformation.ts", "./contentScript/pageEditor/elementPicker.ts", + "./contentScript/pageEditor/insertButton", + "./contentScript/pageEditor/insertPanel", + "./contentScript/pageEditor/selectElement.ts", "./contentScript/pageEditor/types.ts", "./contentScript/partnerIntegrations.ts", + "./contentScript/performanceMonitoring.ts", "./contentScript/popoverDom.ts", "./contentScript/ready.ts", "./contentScript/setExtensionIdInApp.ts", @@ -495,8 +508,10 @@ "./extensionConsole/Navbar.tsx", "./extensionConsole/Sidebar.tsx", "./extensionConsole/SidebarLink.tsx", + "./extensionConsole/components/IDBErrorDisplay.tsx", "./extensionConsole/components/ServiceFieldError.tsx", "./extensionConsole/pages/InvitationBanner.tsx", + "./extensionConsole/pages/UpdateBanner.tsx", "./extensionConsole/pages/activateExtension/activateTypes.ts", "./extensionConsole/pages/activateMod/IntegrationDescriptor.tsx", "./extensionConsole/pages/activateMod/UrlPermissionsList.tsx", @@ -528,6 +543,7 @@ "./extensionConsole/pages/mods/modals/shareModals/useSortOrganizations.ts", "./extensionConsole/pages/mods/modsPageSelectors.ts", "./extensionConsole/pages/mods/modsPageSlice.ts", + "./extensionConsole/pages/mods/modsPageTypes.ts", "./extensionConsole/pages/mods/utils/exportBlueprint.ts", "./extensionConsole/pages/onboarding/DefaultSetupCard.tsx", "./extensionConsole/pages/onboarding/partner/partnerOnboardingUtils.ts", @@ -536,6 +552,7 @@ "./extensionConsole/pages/settings/LoggingSettings.tsx", "./extensionConsole/pages/settings/PrivacySettings.tsx", "./extensionConsole/pages/settings/SettingToggle.tsx", + "./extensionConsole/pages/settings/StorageSettings.tsx", "./extensionConsole/pages/useRegistryIdParam.ts", "./extensionConsole/pages/workshop/workshopTypes.ts", "./extensionConsole/pages/workshop/workshopUtils.ts", @@ -770,6 +787,7 @@ "./sidebar/sidePanel.tsx", "./sidebar/sidebarSelectors.ts", "./sidebar/staticPanelUtils.tsx", + "./sidebar/UnavailableOverlay.tsx", "./sidebar/useHideEmptySidebar.ts", "./starterBricks/contextMenu/contextMenuReader.ts", "./starterBricks/contextMenu/types.ts", @@ -820,6 +838,7 @@ "./store/sidebar/sidebarStorage.ts", "./store/sidebar/thunks/addFormPanel.ts", "./store/sidebar/thunks/addTemporaryPanel.ts", + "./store/sidebar/thunks/removeFormPanel.ts", "./store/sidebar/thunks/removeTemporaryPanel.ts", "./store/sidebar/thunks/resolveTemporaryPanel.ts", "./store/sidebar/utils.ts", diff --git a/src/types/helpers.test.ts b/src/types/helpers.test.ts new file mode 100644 index 0000000000..a52101d95b --- /dev/null +++ b/src/types/helpers.test.ts @@ -0,0 +1,197 @@ +/* + * 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 { testIsSemVerString, normalizeSemVerString } from "@/types/helpers"; + +describe("types/helpers.ts", () => { + describe("testIsSemVerString", () => { + it.each([ + { + value: "1.2.3", + allowLeadingV: false, + coerce: false, + expected: true, + }, + { + value: "1.2.3", + allowLeadingV: true, + coerce: true, + expected: true, + }, + { value: "v1.2.3", allowLeadingV: true, coerce: false, expected: true }, + { value: "v1.2.3", allowLeadingV: true, coerce: true, expected: true }, + { value: "v1.2.3", allowLeadingV: false, coerce: false, expected: false }, + { + value: "1.2.3.4000", + allowLeadingV: false, + coerce: false, + expected: false, + }, + { + value: "1.2.3.4000", + allowLeadingV: false, + coerce: true, + expected: true, + }, + { + value: "v1.2.3.4000", + allowLeadingV: true, + coerce: true, + expected: true, + }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: false, + expected: false, + }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: true, + expected: false, + }, + { + value: "", + allowLeadingV: false, + coerce: false, + expected: false, + }, + { + value: "", + allowLeadingV: false, + coerce: true, + expected: false, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: false, + expected: false, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: true, + expected: false, + }, + ])( + "$value with allowLeadingV: $allowLeadingV and coerce: $coerce returns $expected", + ({ value, allowLeadingV, coerce, expected }) => { + expect(testIsSemVerString(value, { allowLeadingV, coerce })).toBe( + expected, + ); + }, + ); + }); + + describe("normalizeSemVerString", () => { + it.each([ + { + value: "1.2.3", + allowLeadingV: false, + coerce: false, + expected: "1.2.3", + }, + { + value: "v1.2.3", + allowLeadingV: true, + coerce: false, + expected: "v1.2.3", + }, + { + value: "1.2.3.4000", + allowLeadingV: false, + coerce: true, + expected: "1.2.3", + }, + { + value: "v1.2.3.4000", + allowLeadingV: true, + coerce: true, + expected: "v1.2.3", + }, + ])( + "$value with allowLeadingV: $allowLeadingV and coerce: $coerce returns $expected", + ({ value, allowLeadingV, coerce, expected }) => { + expect(normalizeSemVerString(value, { allowLeadingV, coerce })).toBe( + expected, + ); + }, + ); + + it.each([ + { + value: "v1.2.3", + allowLeadingV: false, + coerce: false, + }, + { + value: "1.2.3.4000", + allowLeadingV: false, + coerce: false, + }, + { + value: "v1.2.3.4000", + allowLeadingV: false, + coerce: true, + }, + { + value: "v1.2.3.4000", + allowLeadingV: true, + coerce: false, + }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: false, + }, + { + value: "lorem ipsum", + allowLeadingV: false, + coerce: true, + }, + { + value: "", + allowLeadingV: false, + coerce: false, + }, + { + value: "", + allowLeadingV: false, + coerce: true, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: false, + }, + { + value: "vacant", + allowLeadingV: true, + coerce: true, + }, + ])( + "$value with allowLeadingV: $allowLeadingV and coerce: $coerce throws an error", + ({ value, allowLeadingV, coerce }) => { + expect(() => + normalizeSemVerString(value, { allowLeadingV, coerce }), + ).toThrow(); + }, + ); + }); +}); diff --git a/src/types/helpers.ts b/src/types/helpers.ts index a1776d8478..8f815b4945 100644 --- a/src/types/helpers.ts +++ b/src/types/helpers.ts @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -import { valid as semVerValid } from "semver"; +import { valid as semVerValid, coerce as semVerCoerce } from "semver"; import { startsWith } from "lodash"; import { @@ -151,10 +151,19 @@ export function validateTimestamp(value: string): Timestamp { throw new TypeError("Invalid timestamp"); } -export function validateSemVerString( +/** + * @param value The string to normalize + * @param allowLeadingV If `true`, a leading `v` is allowed. This results in a semver string that is not actually valid + * @param coerce If `true`, the string will be coerced to a valid semver string. See https://www.npmjs.com/package/semver#coercion + * @returns A normalized semver string + */ +export function normalizeSemVerString( value: string, // Default to `false` to be stricter. - { allowLeadingV = false }: { allowLeadingV?: boolean } = {}, + { + allowLeadingV = false, + coerce = false, + }: { allowLeadingV?: boolean; coerce?: boolean } = {}, ): SemVerString { if (value == null) { // We don't have strictNullChecks on, so null values will find there way here. We should pass them along. Eventually @@ -162,7 +171,16 @@ export function validateSemVerString( return value as SemVerString; } - if (testIsSemVerString(value, { allowLeadingV })) { + if (testIsSemVerString(value, { allowLeadingV, coerce })) { + if (coerce) { + const coerced = semVerValid(semVerCoerce(value)); + if (value.startsWith("v")) { + return `v${coerced}` as SemVerString; + } + + return coerced as SemVerString; + } + return value; } @@ -175,9 +193,14 @@ export function testIsSemVerString( value: string, // FIXME: the SemVerString type wasn't intended to support a leading `v`. See documentation // Default to `false` to be stricter. - { allowLeadingV = false }: { allowLeadingV?: boolean } = {}, + { + allowLeadingV = false, + coerce = false, + }: { allowLeadingV?: boolean; coerce?: boolean } = {}, ): value is SemVerString { - if (semVerValid(value) != null) { + const _value = coerce ? semVerCoerce(value) : value; + + if (semVerValid(_value) != null) { return allowLeadingV || !startsWith(value, "v"); } diff --git a/src/types/sidebarTypes.ts b/src/types/sidebarTypes.ts index e833977c77..8bd99a2fcd 100644 --- a/src/types/sidebarTypes.ts +++ b/src/types/sidebarTypes.ts @@ -111,6 +111,15 @@ type BasePanelEntry = { * The panel type. */ type: EntryType; + + /** + * Determines if the panel cannot be displayed for the current tab. Used + * to show an overlay over the panel to indicate it is unavailable. Added this + * field to account for MV3 side panel that persists across page navigation + * + * @since 1.8.14 + */ + isUnavailable?: boolean; }; /** diff --git a/src/utils/deploymentUtils.test.ts b/src/utils/deploymentUtils.test.ts index 2c97242627..55ce6ba8d8 100644 --- a/src/utils/deploymentUtils.test.ts +++ b/src/utils/deploymentUtils.test.ts @@ -25,7 +25,7 @@ import { import { uuidv4, validateRegistryId, - validateSemVerString, + normalizeSemVerString, validateTimestamp, } from "@/types/helpers"; import { type SanitizedIntegrationConfig } from "@/integrations/integrationTypes"; @@ -42,6 +42,7 @@ import { PIXIEBRIX_INTEGRATION_ID, } from "@/integrations/constants"; import getModDefinitionIntegrationIds from "@/integrations/util/getModDefinitionIntegrationIds"; +import { getExtensionVersion } from "@/utils/extensionUtils"; describe("makeUpdatedFilter", () => { test.each([[{ restricted: true }, { restricted: false }]])( @@ -122,7 +123,7 @@ describe("makeUpdatedFilter", () => { _recipe: { ...modDefinition.metadata, // The factory produces version "1.0.1" - version: validateSemVerString("1.0.1"), + version: normalizeSemVerString("1.0.1"), updated_at: validateTimestamp(deployment.updated_at), // `sharing` doesn't impact the predicate. Pass an arbitrary value sharing: undefined, @@ -151,9 +152,8 @@ describe("checkExtensionUpdateRequired", () => { test("update not required", () => { const { deployment, modDefinition } = activatableDeploymentFactory(); - (modDefinition.metadata.extensionVersion as any) = `>=${ - browser.runtime.getManifest().version - }`; + (modDefinition.metadata.extensionVersion as any) = + `>=${getExtensionVersion()}`; expect( checkExtensionUpdateRequired([{ deployment, modDefinition }]), diff --git a/src/utils/deploymentUtils.ts b/src/utils/deploymentUtils.ts index 27300ce05e..cd32cc310b 100644 --- a/src/utils/deploymentUtils.ts +++ b/src/utils/deploymentUtils.ts @@ -30,6 +30,7 @@ import { type Except } from "type-fest"; import { PIXIEBRIX_INTEGRATION_ID } from "@/integrations/constants"; import getUnconfiguredComponentIntegrations from "@/integrations/util/getUnconfiguredComponentIntegrations"; import type { ActivatableDeployment } from "@/types/deploymentTypes"; +import { getExtensionVersion } from "@/utils/extensionUtils"; /** * Returns `true` if a managed deployment is active (i.e., has not been remotely paused by an admin) @@ -124,7 +125,8 @@ export function checkExtensionUpdateRequired( activatableDeployments: ActivatableDeployment[] = [], ): boolean { // Check that the user's extension can run the deployment - const { version: extensionVersion } = browser.runtime.getManifest(); + + const extensionVersion = getExtensionVersion(); const versionRanges = compact( activatableDeployments.map( ({ modDefinition }) => modDefinition.metadata.extensionVersion, diff --git a/src/utils/extensionUtils.ts b/src/utils/extensionUtils.ts index 41c0d317cb..69faeda8fc 100644 --- a/src/utils/extensionUtils.ts +++ b/src/utils/extensionUtils.ts @@ -20,6 +20,8 @@ import { foreverPendingPromise } from "@/utils/promiseUtils"; import { type Promisable } from "type-fest"; import { isScriptableUrl } from "webext-content-scripts"; import { type Runtime } from "webextension-polyfill"; +import { normalizeSemVerString } from "@/types/helpers"; +import { type SemVerString } from "@/types/registryTypes"; export const SHORTCUTS_URL = "chrome://extensions/shortcuts"; type Command = "toggle-quick-bar"; @@ -65,8 +67,23 @@ export function getExtensionConsoleUrl(page?: string): string { return url.href; } -export function getExtensionVersion(): string { - return browser.runtime.getManifest().version; +/** + * Gets the Extension version from the manifest and normalizes it to a valid semver string. + * @since 1.8.13, the Extension version is a four part format x.x.x.x + * This allows us to publish pre-release versions to the CWS, especially the BETA listing + * Each version published in CWS must have a unique version number + * + * @see manifest.mjs:getVersion() + * + * TODO: Add linting rule to prefer getExtensionVersion over browser.runtime.getManifest().version + * @see https://github.com/pixiebrix/pixiebrix-extension/issues/8349 + * + * @returns the version of the Extension in valid semver format (x.x.x) + */ +export function getExtensionVersion(): SemVerString { + return normalizeSemVerString(browser.runtime.getManifest().version, { + coerce: true, + }); } /** If no update is available and downloaded yet, it will return a string explaining why */ diff --git a/src/utils/inference/inferSingleElementSelector.ts b/src/utils/inference/inferSingleElementSelector.ts index 72c241a2e7..05e15e1cce 100644 --- a/src/utils/inference/inferSingleElementSelector.ts +++ b/src/utils/inference/inferSingleElementSelector.ts @@ -45,7 +45,7 @@ function getMatchingRequiredSelectors( */ function mapSelectorOverrideToAncestors( element: HTMLElement, - root: HTMLElement | Document, + root?: HTMLElement | Document, ): Array<{ element: HTMLElement; selectorOverride: string }> { const { requiredSelectors, selectorTemplates } = getSiteSelectorHint(element); @@ -157,7 +157,7 @@ async function inferSingleElementSelector({ // Ancestors in order from root to element const ancestorSelectorOverrides = mapSelectorOverrideToAncestors( element, - root, + root ?? undefined, ); const ancestorSelectors = ancestorSelectorOverrides.map( @@ -179,14 +179,17 @@ async function inferSingleElementSelector({ const inferredSelectors = uniq( [ selectorWithRootOverride, - ...inferSelectorsIncludingStableAncestors(element, rootOverride), + ...inferSelectorsIncludingStableAncestors( + element, + rootOverride ?? undefined, + ), ].map((selector) => [...ancestorSelectors, selector].join(" ")), ); // Filter out any malformed selectors and/or selectors that don't exactly match the element const validatedSelectors = inferredSelectors.filter((selector) => { try { - const match = $safeFind(selector, root); + const match = $safeFind(selector, root ?? undefined); return match.length === 1 && match.get(0) === element; } catch { console.warn("Invalid selector", selector); diff --git a/src/utils/inference/markupInference.ts b/src/utils/inference/markupInference.ts index 0118ee8770..e0d6c4a0ac 100644 --- a/src/utils/inference/markupInference.ts +++ b/src/utils/inference/markupInference.ts @@ -21,10 +21,11 @@ import { PIXIEBRIX_DATA_ATTR, } from "@/domConstants"; import { BUTTON_TAGS, UNIQUE_ATTRIBUTES } from "./selectorInference"; -import { intersection, unary, uniq } from "lodash"; +import { compact, intersection, unary, uniq } from "lodash"; import { BusinessError } from "@/errors/businessErrors"; import { isNullOrBlank, matchesAnyPattern } from "@/utils/stringUtils"; import { mostCommonElement } from "@/utils/arrayUtils"; +import { assertNotNullish } from "@/utils/nullishUtils"; const BUTTON_SELECTORS: string[] = ["[role='button']"]; const ICON_TAGS = ["svg", "img"]; @@ -111,7 +112,8 @@ function commonAttribute(items: Element[], attribute: string) { const classNames = attributeValues.map((x) => (x ? x.split(" ") : [])); unfiltered = intersection(...classNames); } else if (uniq(attributeValues).length === 1) { - unfiltered = attributeValues[0].split(" "); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- length check above + unfiltered = attributeValues[0]!.split(" "); } else { // Single attribute doesn't match return null; @@ -127,7 +129,8 @@ function commonAttribute(items: Element[], attribute: string) { } function setCommonAttributes(common: Element, items: Element[]) { - const { attributes } = items[0]; + const { attributes } = items[0] ?? {}; + assertNotNullish(attributes, "No attributes found"); // Find the common attributes between the elements for (const { name } of attributes) { @@ -155,7 +158,8 @@ function setCommonAttributes(common: Element, items: Element[]) { function ignoreDivChildNode(node: Node): boolean { return ( NON_RENDERED_NODES.includes(node.nodeType) || - (node.nodeType === Node.TEXT_NODE && node.textContent.trim() === "") + (node.nodeType === Node.TEXT_NODE && + (node.textContent == null || node.textContent.trim() === "")) ); } @@ -175,7 +179,8 @@ function removeUnstyledLayout(node: Node): Node | null { isNullOrBlank(element.className) && nonEmptyChildren.length === 1 ) { - return removeUnstyledLayout(nonEmptyChildren[0]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- length check above + return removeUnstyledLayout(nonEmptyChildren[0]!); } const clone = node.cloneNode(false) as Element; @@ -205,6 +210,7 @@ function commonButtonStructure( let currentCaptioned = captioned; const proto = items[0]; + assertNotNullish(proto, "No elements provided"); if (ICON_TAGS.includes(proto.tagName.toLowerCase())) { // TODO: need to provide a way of adding additional classes to the button. E.g. some classes @@ -244,10 +250,11 @@ function commonButtonStructure( } else if ( protoChild.nodeType === Node.ELEMENT_NODE && items.every((x) => elementIndex < x.children.length) && - uniq(items.map((x) => x.children.item(elementIndex).tagName)).length === 1 + uniq(items.map((x) => x.children.item(elementIndex)?.tagName)).length === + 1 ) { - const children = items.map((element) => - element.children.item(elementIndex), + const children = compact( + items.map((element) => element.children.item(elementIndex)), ); const [child, childCaptioned] = commonButtonStructure( @@ -297,6 +304,8 @@ function commonPanelStructure( }: PanelStructureState = {} as PanelStructureState, ): [Element | string, PanelStructureState] { const proto = $items.get(0); + assertNotNullish(proto, "No elements provided"); + inHeader ||= HEADER_TAGS.includes(proto.tagName.toLowerCase()); const common = newElement(proto.tagName); @@ -304,16 +313,16 @@ function commonPanelStructure( setCommonAttributes(common, $items.get()); // Heuristic that assumes tag matches from the beginning - for (let i = 0; i < proto.children.length; i++) { + for (let i = 0; i < (proto.children.length ?? 0); i++) { const protoChild = proto.children.item(i) as HTMLElement; const $protoChild = $(protoChild); if ( $items.toArray().every((x) => i < x.children.length) && - uniq($items.toArray().map((x) => x.children.item(i).tagName)).length === + uniq($items.toArray().map((x) => x.children.item(i)?.tagName)).length === 1 && (!headingInserted || - LAYOUT_TAGS.includes(proto.children.item(i).tagName.toLowerCase())) + LAYOUT_TAGS.includes(protoChild.tagName.toLowerCase())) ) { const $children = $items.map(function () { return this.children.item(i) as HTMLElement; @@ -336,7 +345,7 @@ function commonPanelStructure( } else if ( !inHeader && !bodyInserted && - !LAYOUT_TAGS.includes(proto.children.item(i).tagName.toLowerCase()) + !LAYOUT_TAGS.includes(protoChild.tagName.toLowerCase()) ) { common.append("{{{ body }}}"); bodyInserted = true; @@ -438,7 +447,9 @@ function commonButtonHTML(tag: string, items: Element[]): string { const elements = items .map(unary(removeUnstyledLayout)) - .filter((x): x is Element => x && x.nodeType === Node.ELEMENT_NODE); + .filter( + (x): x is Element => Boolean(x) && x?.nodeType === Node.ELEMENT_NODE, + ); const [common] = commonButtonStructure(elements); @@ -470,6 +481,9 @@ function inferSinglePanelHTML( ): string { const $container = $(container); const child = containerChildren($container, [selected])[0]; + + assertNotNullish(child, "Unable to find direct children of the container"); + const [$panel] = buildSinglePanelElement(child); return outerHTML($panel); } @@ -478,14 +492,20 @@ export function inferPanelHTML( container: HTMLElement, selected: HTMLElement[], ): string { + if (selected.length === 0) { + throw new Error("No selected element"); + } + const $container = $(container); if (selected.length > 1) { const children = containerChildren($container, selected); - return commonPanelHTML(selected[0].tagName, $(children)); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- length check above + return commonPanelHTML(selected[0]!.tagName, $(children)); } - return inferSinglePanelHTML(container, selected[0]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- length check above + return inferSinglePanelHTML(container, selected[0]!); } export function inferButtonHTML( @@ -507,7 +527,8 @@ export function inferButtonHTML( return commonButtonHTML(tag, children); } - const element = selected[0]; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-unnecessary-type-assertion -- length check above + const element = selected[0]!; for (const buttonTag of [...BUTTON_SELECTORS, ...BUTTON_TAGS]) { const $items = $container.children(buttonTag); if ( @@ -540,21 +561,23 @@ function containerChildren( $container: JQuery, selected: HTMLElement[], ): HTMLElement[] { - return selected.map((element) => { - const exactMatch = $container.children().filter(function () { - return this === element; - }); + return compact( + selected.map((element) => { + const exactMatch = $container.children().filter(function () { + return this === element; + }); - if (exactMatch.length > 0) { - return exactMatch.get(0); - } + if (exactMatch.length > 0) { + return exactMatch.get(0); + } - const match = $container.children().has(element); + const match = $container.children().has(element); - if (match.length === 0) { - throw new Error("element not found in container"); - } + if (match.length === 0) { + throw new Error("element not found in container"); + } - return match.get(0); - }); + return match.get(0); + }), + ); } diff --git a/src/utils/inference/selectorInference.ts b/src/utils/inference/selectorInference.ts index aefd39f6ea..20df74bf04 100644 --- a/src/utils/inference/selectorInference.ts +++ b/src/utils/inference/selectorInference.ts @@ -499,7 +499,7 @@ export type InferSelectorArgs = { /** * The root to generate the selector with respect to. */ - root: HTMLElement; + root: HTMLElement | null; /** * True to exclude class names that appear to be randomly generated. */ diff --git a/src/utils/objToYaml.test.ts b/src/utils/objToYaml.test.ts index 3ff5a8456d..08a420e768 100644 --- a/src/utils/objToYaml.test.ts +++ b/src/utils/objToYaml.test.ts @@ -16,7 +16,7 @@ */ import { brickToYaml } from "./objToYaml"; -import { validateSemVerString } from "@/types/helpers"; +import { normalizeSemVerString } from "@/types/helpers"; describe("brickToYaml", () => { test("serializes arbitrary object", () => { @@ -40,7 +40,7 @@ lorem: ipsum metadata: { id: "google/api", name: "Google API", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Generic Google API authentication via API key", }, apiVersion: "v1", @@ -104,7 +104,7 @@ outputSchema: metadata: { id: "google/api", name: "Google API", - version: validateSemVerString("1.0.0"), + version: normalizeSemVerString("1.0.0"), description: "Generic Google API authentication via API key", }, apiVersion: "v1", diff --git a/src/utils/sidePanelUtils.ts b/src/utils/sidePanelUtils.ts index 63b35dfb63..d16d7024f6 100644 --- a/src/utils/sidePanelUtils.ts +++ b/src/utils/sidePanelUtils.ts @@ -24,6 +24,13 @@ import { type PageTarget, messenger, getThisFrame } from "webext-messenger"; import { isContentScript } from "webext-detect-page"; import { showSidebar } from "@/contentScript/messenger/strict/api"; +/** + * Returns true if an error showing sidebar is due to a missing user gesture. + */ +export function isUserGestureRequiredError(error: unknown): boolean { + return getErrorMessage(error).includes("user gesture"); +} + export async function openSidePanel(tabId: number): Promise { if (isBrowserSidebar()) { console.warn( @@ -60,10 +67,7 @@ async function openSidePanelMv3(tabId: number): Promise { // it's still part of a user gesture. // If it's not, it will throw an error *even if the side panel is already open*. // The following code silences that error iff the side panel is already open. - if ( - getErrorMessage(error).includes("user gesture") && - (await isSidePanelOpen(tabId)) - ) { + if (isUserGestureRequiredError(error) && (await isSidePanelOpen(tabId))) { // The `openSidePanel` call was not required in the first place, the error can be silenced // TODO: After switching to MV3, verify whether we drop that `openSidePanel` call return; diff --git a/webpack.config.mjs b/webpack.config.mjs index 99f7e0e9ca..e3d83f079b 100644 --- a/webpack.config.mjs +++ b/webpack.config.mjs @@ -203,7 +203,7 @@ const createConfig = (env, options) => }), // Only notifies when watching. `zsh-notify` is suggested for the `build` script - options.watch && + !isProd(options) && process.env.DEV_NOTIFY !== "false" && new WebpackBuildNotifierPlugin({ title: "PB Extension",