From daf2545d57b8130bdcc348e2594006b3487a4e00 Mon Sep 17 00:00:00 2001
From: Sara <git@sarayourfriend.pictures>
Date: Mon, 28 Oct 2024 16:31:43 +1100
Subject: [PATCH] Add baseline functionality for running Playwright tests
 against live environments (#5075)

* Add HMAC signing capability for playwright tests

* Add workflow to run Playwright smoketests in CI

* Remove testing code for new workflow
---
 .github/workflows/ci_cd.yml                   |  16 ++
 .../playwright_deployment_smoketest.yml       |  44 ++++
 .../remake-fileName.spec.ts                   |   2 +-
 frontend/docker-compose.playwright.yml        |   2 +
 .../e2e/all-results-analytics.spec.ts         |   4 +-
 .../e2e/all-results-keyboard.spec.ts          | 236 +++++++++---------
 .../test/playwright/e2e/attribution.spec.ts   |   4 +-
 .../test/playwright/e2e/audio-detail.spec.ts  |   4 +-
 .../test/playwright/e2e/audio-results.spec.ts |   4 +-
 .../test/playwright/e2e/collections.spec.ts   |   4 +-
 .../playwright/e2e/external-sources.spec.ts   |   4 +-
 .../e2e/filters-sidebar-keyboard.spec.ts      |   4 +-
 frontend/test/playwright/e2e/filters.spec.ts  |   4 +-
 .../test/playwright/e2e/global-audio.spec.ts  |   4 +-
 .../playwright/e2e/header-internal.spec.ts    |   4 +-
 .../test/playwright/e2e/healthcheck.spec.ts   |   4 +-
 frontend/test/playwright/e2e/homepage.spec.ts |   4 +-
 .../test/playwright/e2e/image-detail.spec.ts  |   4 +-
 .../test/playwright/e2e/load-more.spec.ts     |   4 +-
 .../test/playwright/e2e/mobile-menu.spec.ts   |   4 +-
 .../test/playwright/e2e/preferences.spec.ts   |   4 +-
 .../playwright/e2e/recent-searches.spec.ts    |   4 +-
 .../e2e/redirect-queryless-searches.spec.ts   |   4 +-
 .../test/playwright/e2e/report-media.spec.ts  |   4 +-
 .../playwright/e2e/search-analytics.spec.ts   |   4 +-
 .../playwright/e2e/search-navigation.spec.ts  |   4 +-
 .../e2e/search-query-client.spec.ts           |   4 +-
 .../e2e/search-query-server.spec.ts           |   4 +-
 .../test/playwright/e2e/search-types.spec.ts  |   4 +-
 frontend/test/playwright/e2e/search.spec.ts   |   4 +-
 .../playwright/e2e/sensitive-results.spec.ts  |   4 +-
 frontend/test/playwright/e2e/seo.spec.ts      |   4 +-
 .../e2e/single-result-analytics.spec.ts       |   4 +-
 .../playwright/e2e/skip-to-content.spec.ts    |   4 +-
 frontend/test/playwright/e2e/sources.spec.ts  |   4 +-
 .../playwright/e2e/translation-banner.spec.ts |   4 +-
 frontend/test/playwright/playwright.config.ts |  51 ++--
 frontend/test/playwright/utils/breakpoints.ts |   2 +-
 frontend/test/playwright/utils/test.ts        |  61 +++++
 .../components/content-report-form.spec.ts    |   4 +-
 .../external-sources-section.spec.ts          |   2 +-
 .../components/filters.spec.ts                |   4 +-
 .../components/global-audio-player.spec.ts    |   2 +-
 .../components/header.spec.ts                 |   2 +-
 .../visual-regression/pages/errors.spec.ts    |   2 +-
 .../visual-regression/pages/homepage.spec.ts  |   4 +-
 .../pages/pages-single-result.spec.ts         |   4 +-
 .../visual-regression/pages/pages.spec.ts     |   4 +-
 .../pages/search-with-banners.spec.ts         |   2 +-
 .../storybook/functional/smoke-test.spec.ts   |   4 +-
 .../storybook/functional/v-checkbox.spec.ts   |   4 +-
 .../storybook/functional/v-popover.spec.ts    |   4 +-
 .../custom-button-components.spec.ts          |   2 +-
 .../storybook/visual-regression/focus.spec.ts |   4 +-
 .../visual-regression/v-button.spec.ts        |   2 +-
 .../visual-regression/v-checkbox.spec.ts      |   2 +-
 .../v-collection-header.spec.ts               |   4 +-
 .../visual-regression/v-filter-button.spec.ts |   2 +-
 .../visual-regression/v-filter-tab.spec.ts    |   4 +-
 .../visual-regression/v-footer.spec.ts        |   4 +-
 .../v-header-internal.spec.ts                 |   2 +-
 .../visual-regression/v-icon-button.spec.ts   |   4 +-
 .../visual-regression/v-image-cell.spec.ts    |   2 +-
 .../v-language-select.spec.ts                 |   4 +-
 .../visual-regression/v-media-license.spec.ts |   4 +-
 .../visual-regression/v-media-reuse.spec.ts   |   2 +-
 .../v-notitication-banner.spec.ts             |   2 +-
 .../visual-regression/v-safety-wall.spec.ts   |   2 +-
 .../v-search-bar-button.spec.ts               |   2 +-
 .../v-search-type-button.spec.ts              |   4 +-
 .../visual-regression/v-search-types.spec.ts  |   4 +-
 .../visual-regression/v-select-field.spec.ts  |   4 +-
 .../js/eslint-plugin/src/configs/index.ts     |  10 +
 73 files changed, 451 insertions(+), 199 deletions(-)
 create mode 100644 .github/workflows/playwright_deployment_smoketest.yml
 create mode 100644 frontend/test/playwright/utils/test.ts

diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml
index 32b168c6905..0e6efbcc618 100644
--- a/.github/workflows/ci_cd.yml
+++ b/.github/workflows/ci_cd.yml
@@ -1312,6 +1312,22 @@ jobs:
           wait_time: 60 # check every minute
           max_time: 1800 # allow up to 30 minutes for a deployment
 
+      - name: Trigger staging Playwright smoketests
+        uses: convictional/trigger-workflow-and-wait@v1.6.5
+        with:
+          owner: WordPress
+          repo: openverse
+          github_token: ${{ secrets.ACCESS_TOKEN }}
+          workflow_file_name: playwright_deployment_smoketest.yml
+          wait_interval: 60
+          # TODO: Set to true once we see that this test is stable, and we can fail the deployment if it fails.
+          # @see https://github.com/WordPress/openverse/pull/4991
+          propagate_failure: false
+          client_payload: |
+            {
+              "service_url": "https://staging.openverse.org/"
+            }
+
       - name: Trigger staging k6 load test
         uses: convictional/trigger-workflow-and-wait@v1.6.5
         with:
diff --git a/.github/workflows/playwright_deployment_smoketest.yml b/.github/workflows/playwright_deployment_smoketest.yml
new file mode 100644
index 00000000000..9b589668f8c
--- /dev/null
+++ b/.github/workflows/playwright_deployment_smoketest.yml
@@ -0,0 +1,44 @@
+name: Run Playwright smoketests
+
+on:
+  workflow_dispatch:
+    inputs:
+      service_url:
+        description: "The service against which to run the Playwright smoketests."
+        required: true
+        type: string
+
+run-name: Playwright smoketests ${{ inputs.service_url }}
+
+# Disallow running multiple smoketests at once against the same service
+concurrency: ${{ github.workflow }}-${{ inputs.service_url }}
+
+jobs:
+  smoketests:
+    name: "Playwright smoketests ${{ inputs.service_url }}"
+    runs-on: ubuntu-latest
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+
+      - name: Setup CI env
+        uses: ./.github/actions/setup-env
+        with:
+          setup_python: false
+          install_recipe: node-install
+
+      - name: Run playwright
+        env:
+          # The secret name is k6 but it's usable for anything sending HMAC signed requests
+          HMAC_SIGNING_SECRET: ${{ secrets.K6_SIGNING_SECRET }}
+          PLAYWRIGHT_BASE_URL: ${{ inputs.service_url }}
+        run: |
+          just p frontend test:playwright --grep '@deployment-smoketest'
+
+      - uses: actions/upload-artifact@v4
+        if: failure()
+        id: test-results
+        with:
+          name: test_results
+          path: frontend/test-results
diff --git a/frontend/.remake/component-storybook-test/remake-fileName.spec.ts b/frontend/.remake/component-storybook-test/remake-fileName.spec.ts
index 8e9eb589a86..696130e3cd2 100644
--- a/frontend/.remake/component-storybook-test/remake-fileName.spec.ts
+++ b/frontend/.remake/component-storybook-test/remake-fileName.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/docker-compose.playwright.yml b/frontend/docker-compose.playwright.yml
index f89e81ec20a..ab8911a7f56 100644
--- a/frontend/docker-compose.playwright.yml
+++ b/frontend/docker-compose.playwright.yml
@@ -20,3 +20,5 @@ services:
       - DEBUG=pw:webserver
       - UPDATE_TAPES=${UPDATE_TAPES:-false}
       - FASTSTART=${FASTSTART:-false}
+      - PLAYWRIGHT_BASE_URL=${PLAYWRIGHT_BASE_URL:-}
+      - HMAC_SIGNING_SECRET=${HMAC_SIGNING_SECRET:-}
diff --git a/frontend/test/playwright/e2e/all-results-analytics.spec.ts b/frontend/test/playwright/e2e/all-results-analytics.spec.ts
index 6c8384f141d..e8f04680367 100644
--- a/frontend/test/playwright/e2e/all-results-analytics.spec.ts
+++ b/frontend/test/playwright/e2e/all-results-analytics.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   getFirstResult,
diff --git a/frontend/test/playwright/e2e/all-results-keyboard.spec.ts b/frontend/test/playwright/e2e/all-results-keyboard.spec.ts
index 52945eed7ff..af8f9b0b6f7 100644
--- a/frontend/test/playwright/e2e/all-results-keyboard.spec.ts
+++ b/frontend/test/playwright/e2e/all-results-keyboard.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import audio from "~~/test/playwright/utils/audio"
 import {
@@ -30,116 +32,122 @@ const singleResultRegex = (
   )
 }
 
-test.describe("all results grid keyboard accessibility test", () => {
-  const searchTerm = "birds"
-  test.beforeEach(async ({ page }) => {
-    await preparePageForTests(page, "xl")
-    await goToSearchTerm(page, searchTerm)
-  })
-
-  test("should open image results as links", async ({ page }) => {
-    const mediaType = "image"
-    await walkToType(mediaType, page)
-    await page.keyboard.press("Enter")
-    const urlRegex = singleResultRegex(mediaType, searchTerm)
-
-    await page.waitForURL(urlRegex)
-    expect(page.url()).toMatch(urlRegex)
-  })
-
-  test("should open audio results as links", async ({ page }) => {
-    const mediaType = "audio"
-    await walkToType(mediaType, page)
-    await page.keyboard.press("Enter")
-    const urlRegex = singleResultRegex(mediaType, searchTerm)
-    await page.waitForURL(urlRegex)
-    expect(page.url()).toMatch(urlRegex)
-  })
-
-  test("should show instructions snackbar when focusing first audio", async ({
-    page,
-  }) => {
-    await walkToType("audio", page)
-
-    await expect(page.getByRole("alert")).toBeVisible()
-  })
-
-  test("should hide the instructions snackbar when interacted with audio", async ({
-    page,
-  }) => {
-    await walkToType("audio", page)
-
-    await expect(page.getByRole("alert")).toBeVisible()
-
-    const focusedResult = await locateFocusedResult(page)
-    const playButton = await audio.getInactive(focusedResult)
-    await playButton.click()
-
-    await expect(page.getByRole("alert")).toBeHidden()
-  })
-
-  test("should allow toggling audio playback via play/pause click", async ({
-    page,
-  }) => {
-    await walkToType("audio", page)
-    const focusedResult = await locateFocusedResult(page)
-    const playButton = await audio.getInactive(focusedResult)
-    await playButton.click()
-
-    // Get the path for comparison purposes
-    const url = new URL(page.url())
-    const path = url.pathname + url.search
-
-    // should not navigate
-    expect(path).toMatch(/\/search\/?\?q=birds$/)
-
-    const pauseButton = await audio.getActive(focusedResult)
-    await pauseButton.click()
-    await expect(playButton).toBeVisible()
-  })
-
-  test("should allow toggling audio playback via spacebar", async ({
-    page,
-  }) => {
-    await walkToType("audio", page)
-    await page.keyboard.press(keycodes.Spacebar)
-    const focusedResult = await locateFocusedResult(page)
-    await expect(await audio.getActive(focusedResult)).toBeVisible()
-    await page.keyboard.press(keycodes.Spacebar)
-    await expect(await audio.getInactive(focusedResult)).toBeVisible()
-  })
-
-  test("should pause audio after playing another", async ({ page }) => {
-    await walkToType("audio", page)
-    const focusedResult = await locateFocusedResult(page)
-    const playButton = await audio.getInactive(focusedResult)
-    await playButton.click()
-    const pauseButton = await audio.getActive(focusedResult)
-
-    await page.keyboard.press(keycodes.Tab)
-    await walkToNextOfType("audio", page)
-
-    const nextFocusedResult = await locateFocusedResult(page)
-    const nextPlayButton = await audio.getInactive(nextFocusedResult)
-    await nextPlayButton.click()
-    await audio.getActive(nextFocusedResult)
-
-    await expect(playButton).toBeVisible()
-    await expect(pauseButton).toBeHidden()
-  })
-
-  // Test for https://github.com/WordPress/openverse/issues/3940
-  test("clicking on skip-to-content should not navigate", async ({ page }) => {
-    const getResultsLabel = async (type: SupportedMediaType) => {
-      const link = await getContentLink(page, type)
-      return link.textContent()
-    }
-    const imageResultsLabel = await getResultsLabel("image")
-    const audioResultsLabel = await getResultsLabel("audio")
-
-    await skipToContent(page)
-
-    expect(await getResultsLabel("image")).toEqual(imageResultsLabel)
-    expect(await getResultsLabel("audio")).toEqual(audioResultsLabel)
-  })
-})
+test.describe(
+  "all results grid keyboard accessibility test",
+  { tag: "@deployment-smoketest" },
+  () => {
+    const searchTerm = "birds"
+    test.beforeEach(async ({ page }) => {
+      await preparePageForTests(page, "xl")
+      await goToSearchTerm(page, searchTerm)
+    })
+
+    test("should open image results as links", async ({ page }) => {
+      const mediaType = "image"
+      await walkToType(mediaType, page)
+      await page.keyboard.press("Enter")
+      const urlRegex = singleResultRegex(mediaType, searchTerm)
+
+      await page.waitForURL(urlRegex)
+      expect(page.url()).toMatch(urlRegex)
+    })
+
+    test("should open audio results as links", async ({ page }) => {
+      const mediaType = "audio"
+      await walkToType(mediaType, page)
+      await page.keyboard.press("Enter")
+      const urlRegex = singleResultRegex(mediaType, searchTerm)
+      await page.waitForURL(urlRegex)
+      expect(page.url()).toMatch(urlRegex)
+    })
+
+    test("should show instructions snackbar when focusing first audio", async ({
+      page,
+    }) => {
+      await walkToType("audio", page)
+
+      await expect(page.getByRole("alert")).toBeVisible()
+    })
+
+    test("should hide the instructions snackbar when interacted with audio", async ({
+      page,
+    }) => {
+      await walkToType("audio", page)
+
+      await expect(page.getByRole("alert")).toBeVisible()
+
+      const focusedResult = await locateFocusedResult(page)
+      const playButton = await audio.getInactive(focusedResult)
+      await playButton.click()
+
+      await expect(page.getByRole("alert")).toBeHidden()
+    })
+
+    test("should allow toggling audio playback via play/pause click", async ({
+      page,
+    }) => {
+      await walkToType("audio", page)
+      const focusedResult = await locateFocusedResult(page)
+      const playButton = await audio.getInactive(focusedResult)
+      await playButton.click()
+
+      // Get the path for comparison purposes
+      const url = new URL(page.url())
+      const path = url.pathname + url.search
+
+      // should not navigate
+      expect(path).toMatch(/\/search\/?\?q=birds$/)
+
+      const pauseButton = await audio.getActive(focusedResult)
+      await pauseButton.click()
+      await expect(playButton).toBeVisible()
+    })
+
+    test("should allow toggling audio playback via spacebar", async ({
+      page,
+    }) => {
+      await walkToType("audio", page)
+      await page.keyboard.press(keycodes.Spacebar)
+      const focusedResult = await locateFocusedResult(page)
+      await expect(await audio.getActive(focusedResult)).toBeVisible()
+      await page.keyboard.press(keycodes.Spacebar)
+      await expect(await audio.getInactive(focusedResult)).toBeVisible()
+    })
+
+    test("should pause audio after playing another", async ({ page }) => {
+      await walkToType("audio", page)
+      const focusedResult = await locateFocusedResult(page)
+      const playButton = await audio.getInactive(focusedResult)
+      await playButton.click()
+      const pauseButton = await audio.getActive(focusedResult)
+
+      await page.keyboard.press(keycodes.Tab)
+      await walkToNextOfType("audio", page)
+
+      const nextFocusedResult = await locateFocusedResult(page)
+      const nextPlayButton = await audio.getInactive(nextFocusedResult)
+      await nextPlayButton.click()
+      await audio.getActive(nextFocusedResult)
+
+      await expect(playButton).toBeVisible()
+      await expect(pauseButton).toBeHidden()
+    })
+
+    // Test for https://github.com/WordPress/openverse/issues/3940
+    test("clicking on skip-to-content should not navigate", async ({
+      page,
+    }) => {
+      const getResultsLabel = async (type: SupportedMediaType) => {
+        const link = await getContentLink(page, type)
+        return link.textContent()
+      }
+      const imageResultsLabel = await getResultsLabel("image")
+      const audioResultsLabel = await getResultsLabel("audio")
+
+      await skipToContent(page)
+
+      expect(await getResultsLabel("image")).toEqual(imageResultsLabel)
+      expect(await getResultsLabel("audio")).toEqual(audioResultsLabel)
+    })
+  }
+)
diff --git a/frontend/test/playwright/e2e/attribution.spec.ts b/frontend/test/playwright/e2e/attribution.spec.ts
index 538de279a98..c9b469be567 100644
--- a/frontend/test/playwright/e2e/attribution.spec.ts
+++ b/frontend/test/playwright/e2e/attribution.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { preparePageForTests } from "~~/test/playwright/utils/navigation"
 import {
diff --git a/frontend/test/playwright/e2e/audio-detail.spec.ts b/frontend/test/playwright/e2e/audio-detail.spec.ts
index 31f989a047c..d8ec7fd2fab 100644
--- a/frontend/test/playwright/e2e/audio-detail.spec.ts
+++ b/frontend/test/playwright/e2e/audio-detail.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   collectAnalyticsEvents,
diff --git a/frontend/test/playwright/e2e/audio-results.spec.ts b/frontend/test/playwright/e2e/audio-results.spec.ts
index ea83f05c5d4..3a2b3a1cd56 100644
--- a/frontend/test/playwright/e2e/audio-results.spec.ts
+++ b/frontend/test/playwright/e2e/audio-results.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/collections.spec.ts b/frontend/test/playwright/e2e/collections.spec.ts
index 81c2fb65129..0b991fa445d 100644
--- a/frontend/test/playwright/e2e/collections.spec.ts
+++ b/frontend/test/playwright/e2e/collections.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { preparePageForTests } from "~~/test/playwright/utils/navigation"
 import {
diff --git a/frontend/test/playwright/e2e/external-sources.spec.ts b/frontend/test/playwright/e2e/external-sources.spec.ts
index cfd8adccfa1..ee0059553ad 100644
--- a/frontend/test/playwright/e2e/external-sources.spec.ts
+++ b/frontend/test/playwright/e2e/external-sources.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/filters-sidebar-keyboard.spec.ts b/frontend/test/playwright/e2e/filters-sidebar-keyboard.spec.ts
index 177fbe9a03e..112da5f2ce8 100644
--- a/frontend/test/playwright/e2e/filters-sidebar-keyboard.spec.ts
+++ b/frontend/test/playwright/e2e/filters-sidebar-keyboard.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/filters.spec.ts b/frontend/test/playwright/e2e/filters.spec.ts
index 757d4ff7382..30953eb25bc 100644
--- a/frontend/test/playwright/e2e/filters.spec.ts
+++ b/frontend/test/playwright/e2e/filters.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   changeSearchType,
diff --git a/frontend/test/playwright/e2e/global-audio.spec.ts b/frontend/test/playwright/e2e/global-audio.spec.ts
index 0f7ce9eea87..fa71a121bc5 100644
--- a/frontend/test/playwright/e2e/global-audio.spec.ts
+++ b/frontend/test/playwright/e2e/global-audio.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/header-internal.spec.ts b/frontend/test/playwright/e2e/header-internal.spec.ts
index 57b93a6e3f5..812a171630b 100644
--- a/frontend/test/playwright/e2e/header-internal.spec.ts
+++ b/frontend/test/playwright/e2e/header-internal.spec.ts
@@ -1,4 +1,6 @@
-import { expect, Page, test } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   isDialogOpen,
diff --git a/frontend/test/playwright/e2e/healthcheck.spec.ts b/frontend/test/playwright/e2e/healthcheck.spec.ts
index 4ea524fbe7c..555b21a71a8 100644
--- a/frontend/test/playwright/e2e/healthcheck.spec.ts
+++ b/frontend/test/playwright/e2e/healthcheck.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 test("returns OK on healthcheck", async ({ page }) => {
   await page.goto("/healthcheck")
diff --git a/frontend/test/playwright/e2e/homepage.spec.ts b/frontend/test/playwright/e2e/homepage.spec.ts
index 9432c4801d0..f23c329386b 100644
--- a/frontend/test/playwright/e2e/homepage.spec.ts
+++ b/frontend/test/playwright/e2e/homepage.spec.ts
@@ -1,4 +1,6 @@
-import { expect, Page, test } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { mockProviderApis } from "~~/test/playwright/utils/route"
 import {
diff --git a/frontend/test/playwright/e2e/image-detail.spec.ts b/frontend/test/playwright/e2e/image-detail.spec.ts
index 3bc5114c1fc..e0cac853081 100644
--- a/frontend/test/playwright/e2e/image-detail.spec.ts
+++ b/frontend/test/playwright/e2e/image-detail.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { mockProviderApis } from "~~/test/playwright/utils/route"
 import {
diff --git a/frontend/test/playwright/e2e/load-more.spec.ts b/frontend/test/playwright/e2e/load-more.spec.ts
index 34d394dd341..00a924b6698 100644
--- a/frontend/test/playwright/e2e/load-more.spec.ts
+++ b/frontend/test/playwright/e2e/load-more.spec.ts
@@ -1,4 +1,6 @@
-import { expect, Page, test } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/mobile-menu.spec.ts b/frontend/test/playwright/e2e/mobile-menu.spec.ts
index 780442541c1..f60f01af37e 100644
--- a/frontend/test/playwright/e2e/mobile-menu.spec.ts
+++ b/frontend/test/playwright/e2e/mobile-menu.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, type Page } from "@playwright/test"
+import { expect, type Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/preferences.spec.ts b/frontend/test/playwright/e2e/preferences.spec.ts
index bb91b8b7478..185676cefa9 100644
--- a/frontend/test/playwright/e2e/preferences.spec.ts
+++ b/frontend/test/playwright/e2e/preferences.spec.ts
@@ -1,4 +1,6 @@
-import { type Page, expect, test } from "@playwright/test"
+import { type Page, expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { preparePageForTests } from "~~/test/playwright/utils/navigation"
 
diff --git a/frontend/test/playwright/e2e/recent-searches.spec.ts b/frontend/test/playwright/e2e/recent-searches.spec.ts
index 94485c7606e..aefd205aced 100644
--- a/frontend/test/playwright/e2e/recent-searches.spec.ts
+++ b/frontend/test/playwright/e2e/recent-searches.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test, type Page } from "@playwright/test"
+import { expect, type Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/redirect-queryless-searches.spec.ts b/frontend/test/playwright/e2e/redirect-queryless-searches.spec.ts
index a6fc32281dc..791ed028011 100644
--- a/frontend/test/playwright/e2e/redirect-queryless-searches.spec.ts
+++ b/frontend/test/playwright/e2e/redirect-queryless-searches.spec.ts
@@ -3,7 +3,9 @@
  * redirect to the homepage.
  */
 
-import { test, expect } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { searchTypes, searchPath } from "~/constants/media"
 
diff --git a/frontend/test/playwright/e2e/report-media.spec.ts b/frontend/test/playwright/e2e/report-media.spec.ts
index 8233a021682..2f9b2b73e09 100644
--- a/frontend/test/playwright/e2e/report-media.spec.ts
+++ b/frontend/test/playwright/e2e/report-media.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page, BrowserContext } from "@playwright/test"
+import { expect, Page, BrowserContext } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { mockProviderApis } from "~~/test/playwright/utils/route"
 import {
diff --git a/frontend/test/playwright/e2e/search-analytics.spec.ts b/frontend/test/playwright/e2e/search-analytics.spec.ts
index d81a62509b7..7adde23cd94 100644
--- a/frontend/test/playwright/e2e/search-analytics.spec.ts
+++ b/frontend/test/playwright/e2e/search-analytics.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/search-navigation.spec.ts b/frontend/test/playwright/e2e/search-navigation.spec.ts
index 7dc10dc4795..7b57e180760 100644
--- a/frontend/test/playwright/e2e/search-navigation.spec.ts
+++ b/frontend/test/playwright/e2e/search-navigation.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   filters,
diff --git a/frontend/test/playwright/e2e/search-query-client.spec.ts b/frontend/test/playwright/e2e/search-query-client.spec.ts
index bc644c9e0a1..16b4d9d57aa 100644
--- a/frontend/test/playwright/e2e/search-query-client.spec.ts
+++ b/frontend/test/playwright/e2e/search-query-client.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   changeSearchType,
diff --git a/frontend/test/playwright/e2e/search-query-server.spec.ts b/frontend/test/playwright/e2e/search-query-server.spec.ts
index 6aeb08a607b..65ca776f27b 100644
--- a/frontend/test/playwright/e2e/search-query-server.spec.ts
+++ b/frontend/test/playwright/e2e/search-query-server.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   currentContentType,
diff --git a/frontend/test/playwright/e2e/search-types.spec.ts b/frontend/test/playwright/e2e/search-types.spec.ts
index 06f0c3b6b82..ce169fecc36 100644
--- a/frontend/test/playwright/e2e/search-types.spec.ts
+++ b/frontend/test/playwright/e2e/search-types.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, Page } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   changeSearchType,
diff --git a/frontend/test/playwright/e2e/search.spec.ts b/frontend/test/playwright/e2e/search.spec.ts
index 2a81c32b1bb..06f238d3791 100644
--- a/frontend/test/playwright/e2e/search.spec.ts
+++ b/frontend/test/playwright/e2e/search.spec.ts
@@ -6,7 +6,9 @@
  * When pending: does not show 'No images', Safer Browsing, search rating or error message
  * On error: shows error message
  */
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   collectAnalyticsEvents,
diff --git a/frontend/test/playwright/e2e/sensitive-results.spec.ts b/frontend/test/playwright/e2e/sensitive-results.spec.ts
index 4ac1fd04d30..623f3b196c5 100644
--- a/frontend/test/playwright/e2e/sensitive-results.spec.ts
+++ b/frontend/test/playwright/e2e/sensitive-results.spec.ts
@@ -1,4 +1,6 @@
-import { expect, Page, test } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   filters,
diff --git a/frontend/test/playwright/e2e/seo.spec.ts b/frontend/test/playwright/e2e/seo.spec.ts
index 9826f7d9abe..e67ca28b469 100644
--- a/frontend/test/playwright/e2e/seo.spec.ts
+++ b/frontend/test/playwright/e2e/seo.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { preparePageForTests } from "~~/test/playwright/utils/navigation"
 
diff --git a/frontend/test/playwright/e2e/single-result-analytics.spec.ts b/frontend/test/playwright/e2e/single-result-analytics.spec.ts
index df76f08f0f4..246b72647b6 100644
--- a/frontend/test/playwright/e2e/single-result-analytics.spec.ts
+++ b/frontend/test/playwright/e2e/single-result-analytics.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/e2e/skip-to-content.spec.ts b/frontend/test/playwright/e2e/skip-to-content.spec.ts
index 7441a8528dc..4044a4970e7 100644
--- a/frontend/test/playwright/e2e/skip-to-content.spec.ts
+++ b/frontend/test/playwright/e2e/skip-to-content.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/test/playwright/e2e/sources.spec.ts b/frontend/test/playwright/e2e/sources.spec.ts
index 04fb09e06aa..169a2161d12 100644
--- a/frontend/test/playwright/e2e/sources.spec.ts
+++ b/frontend/test/playwright/e2e/sources.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { getH1 } from "~~/test/playwright/utils/components"
 import { preparePageForTests } from "~~/test/playwright/utils/navigation"
diff --git a/frontend/test/playwright/e2e/translation-banner.spec.ts b/frontend/test/playwright/e2e/translation-banner.spec.ts
index 853fa85bcfa..e3ea794ccca 100644
--- a/frontend/test/playwright/e2e/translation-banner.spec.ts
+++ b/frontend/test/playwright/e2e/translation-banner.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { preparePageForTests } from "~~/test/playwright/utils/navigation"
 
diff --git a/frontend/test/playwright/playwright.config.ts b/frontend/test/playwright/playwright.config.ts
index 2ad345677aa..b7f9090c87e 100644
--- a/frontend/test/playwright/playwright.config.ts
+++ b/frontend/test/playwright/playwright.config.ts
@@ -19,27 +19,37 @@ export const API_URL = "http://localhost:49153/"
  */
 const pwCommand = process.env.FASTSTART !== "false" ? "dev" : "prod:playwright"
 
+const localBaseURL = "http://localhost:8443"
+const baseURL = process.env.PLAYWRIGHT_BASE_URL || localBaseURL
+
+// Only run the local webserver if the baseURL is the local
+// In other words, don't bother running the webserver if the test target is a live environment
+const webServer =
+  baseURL === localBaseURL
+    ? {
+        command: `pnpm exec npm-run-all -p -r talkback ${pwCommand}`,
+        timeout: process.env.CI ? 60_000 * 5 : 60_000 * 10, // 5 minutes in CI, 10 in other envs
+        port: 8443,
+        reuseExistingServer: !process.env.CI || process.env.PWDEBUG === "1",
+        env: {
+          UPDATE_TAPES: UPDATE_TAPES,
+          NUXT_PUBLIC_API_URL: API_URL,
+          // Must be true for seo tests to receive appropriate values
+          NUXT_PUBLIC_SITE_INDEXABLE: "true",
+          NUXT_PUBLIC_DEPLOYMENT_ENV: STAGING,
+          NUXT_PUBLIC_PLAUSIBLE_DOMAIN: "localhost",
+          NUXT_PUBLIC_PLAUSIBLE_API_HOST: "http://localhost:50290",
+          NUXT_PUBLIC_PLAUSIBLE_AUTO_PAGEVIEWS: "false",
+          NUXT_PUBLIC_PLAUSIBLE_IGNORED_HOSTNAMES: "[]",
+        },
+      }
+    : undefined
+
 const config: PlaywrightTestConfig = {
   forbidOnly: !!process.env.CI,
-  webServer: {
-    command: `pnpm exec npm-run-all -p -r talkback ${pwCommand}`,
-    timeout: process.env.CI ? 60_000 * 5 : 60_000 * 10, // 5 minutes in CI, 10 in other envs
-    port: 8443,
-    reuseExistingServer: !process.env.CI || process.env.PWDEBUG === "1",
-    env: {
-      UPDATE_TAPES: UPDATE_TAPES,
-      NUXT_PUBLIC_API_URL: API_URL,
-      // Must be true for seo tests to receive appropriate values
-      NUXT_PUBLIC_SITE_INDEXABLE: "true",
-      NUXT_PUBLIC_DEPLOYMENT_ENV: STAGING,
-      NUXT_PUBLIC_PLAUSIBLE_DOMAIN: "localhost",
-      NUXT_PUBLIC_PLAUSIBLE_API_HOST: "http://localhost:50290",
-      NUXT_PUBLIC_PLAUSIBLE_AUTO_PAGEVIEWS: "false",
-      NUXT_PUBLIC_PLAUSIBLE_IGNORED_HOSTNAMES: "[]",
-    },
-  },
+  webServer,
   use: {
-    baseURL: "http://localhost:8443",
+    baseURL,
     trace: "retain-on-failure",
   },
   timeout: 60 * 1e3,
@@ -51,8 +61,11 @@ const config: PlaywrightTestConfig = {
    * and then reuse it for the others. If we run with a single worker when updating
    * tapes then we can avoid this problem. Defaulting to `undefined` means the
    * Playwright default of using 1/2 of the number of CPU cores continues to work otherwise.
+   *
+   * Also: when running Playwright against a live environment, only use a single worker
+   * to avoid the test also becoming a load test.
    */
-  workers: UPDATE_TAPES === "true" ? 1 : undefined,
+  workers: UPDATE_TAPES === "true" || baseURL !== localBaseURL ? 1 : undefined,
 }
 
 export default config
diff --git a/frontend/test/playwright/utils/breakpoints.ts b/frontend/test/playwright/utils/breakpoints.ts
index 3234c9412fa..6b574b098ba 100644
--- a/frontend/test/playwright/utils/breakpoints.ts
+++ b/frontend/test/playwright/utils/breakpoints.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import type { LanguageDirection } from "~~/test/playwright/utils/i18n"
 
diff --git a/frontend/test/playwright/utils/test.ts b/frontend/test/playwright/utils/test.ts
new file mode 100644
index 00000000000..e9c12e06605
--- /dev/null
+++ b/frontend/test/playwright/utils/test.ts
@@ -0,0 +1,61 @@
+// eslint-disable-next-line no-restricted-imports
+import { test as base } from "@playwright/test"
+
+const encoder = new TextEncoder()
+
+const signingSecret = process.env.HMAC_SIGNING_SECRET
+
+const { subtle } = globalThis.crypto
+
+let signingKey: null | CryptoKey = null
+
+async function getSigningKey() {
+  if (!signingSecret) {
+    return null
+  }
+
+  if (!signingKey) {
+    const encodedSecret = encoder.encode(signingSecret)
+    signingKey = await crypto.subtle.importKey(
+      "raw",
+      encodedSecret,
+      { name: "HMAC", hash: "SHA-256" },
+      false,
+      ["sign"]
+    )
+  }
+
+  return signingKey
+}
+
+export const test = base.extend({
+  page: async ({ page }, use) => {
+    // Only match staging; it'll just be ignored for local testing
+    await page.route(
+      (url) => url.host === "staging.openverse.org",
+      async (route) => {
+        const key = await getSigningKey()
+        if (!key) {
+          return route.continue()
+        }
+
+        const request = route.request()
+        const url = new URL(request.url())
+        const timestamp = Math.floor(Date.now() / 1000)
+        const resource = `${url.pathname}${url.search}${timestamp}`
+        const mac = await subtle.sign("HMAC", key, encoder.encode(resource))
+
+        const headers = request.headers()
+        await route.continue({
+          headers: {
+            ...headers,
+            "x-ov-cf-mac": Buffer.from(mac).toString("base64url"),
+            "x-ov-cf-timestamp": timestamp.toString(),
+          },
+        })
+      }
+    )
+
+    await use(page)
+  },
+})
diff --git a/frontend/test/playwright/visual-regression/components/content-report-form.spec.ts b/frontend/test/playwright/visual-regression/components/content-report-form.spec.ts
index bc9240e875e..aa9177bfc6e 100644
--- a/frontend/test/playwright/visual-regression/components/content-report-form.spec.ts
+++ b/frontend/test/playwright/visual-regression/components/content-report-form.spec.ts
@@ -1,4 +1,6 @@
-import { Page, test } from "@playwright/test"
+import { type Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { preparePageForTests } from "~~/test/playwright/utils/navigation"
 import breakpoints from "~~/test/playwright/utils/breakpoints"
diff --git a/frontend/test/playwright/visual-regression/components/external-sources-section.spec.ts b/frontend/test/playwright/visual-regression/components/external-sources-section.spec.ts
index 2a80711dea7..409fc35683b 100644
--- a/frontend/test/playwright/visual-regression/components/external-sources-section.spec.ts
+++ b/frontend/test/playwright/visual-regression/components/external-sources-section.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   goToSearchTerm,
diff --git a/frontend/test/playwright/visual-regression/components/filters.spec.ts b/frontend/test/playwright/visual-regression/components/filters.spec.ts
index 72535cb0ee0..5a74ddfde39 100644
--- a/frontend/test/playwright/visual-regression/components/filters.spec.ts
+++ b/frontend/test/playwright/visual-regression/components/filters.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test, type Page } from "@playwright/test"
+import { expect, type Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   filters,
diff --git a/frontend/test/playwright/visual-regression/components/global-audio-player.spec.ts b/frontend/test/playwright/visual-regression/components/global-audio-player.spec.ts
index f61c7bec583..76c02ce2f27 100644
--- a/frontend/test/playwright/visual-regression/components/global-audio-player.spec.ts
+++ b/frontend/test/playwright/visual-regression/components/global-audio-player.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import {
   pathWithDir,
diff --git a/frontend/test/playwright/visual-regression/components/header.spec.ts b/frontend/test/playwright/visual-regression/components/header.spec.ts
index 58d86c45693..82ca4e0ef41 100644
--- a/frontend/test/playwright/visual-regression/components/header.spec.ts
+++ b/frontend/test/playwright/visual-regression/components/header.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints, {
   isMobileBreakpoint,
diff --git a/frontend/test/playwright/visual-regression/pages/errors.spec.ts b/frontend/test/playwright/visual-regression/pages/errors.spec.ts
index d62bc04185b..aa9bffbd4bb 100644
--- a/frontend/test/playwright/visual-regression/pages/errors.spec.ts
+++ b/frontend/test/playwright/visual-regression/pages/errors.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import {
diff --git a/frontend/test/playwright/visual-regression/pages/homepage.spec.ts b/frontend/test/playwright/visual-regression/pages/homepage.spec.ts
index d5905d9a9ea..97bdd439339 100644
--- a/frontend/test/playwright/visual-regression/pages/homepage.spec.ts
+++ b/frontend/test/playwright/visual-regression/pages/homepage.spec.ts
@@ -1,4 +1,6 @@
-import { test, Page } from "@playwright/test"
+import { Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import { hideInputCursors } from "~~/test/playwright/utils/page"
diff --git a/frontend/test/playwright/visual-regression/pages/pages-single-result.spec.ts b/frontend/test/playwright/visual-regression/pages/pages-single-result.spec.ts
index 278b65a0c4f..c9f2294b594 100644
--- a/frontend/test/playwright/visual-regression/pages/pages-single-result.spec.ts
+++ b/frontend/test/playwright/visual-regression/pages/pages-single-result.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import {
diff --git a/frontend/test/playwright/visual-regression/pages/pages.spec.ts b/frontend/test/playwright/visual-regression/pages/pages.spec.ts
index 63449f6479d..47ad69bb74a 100644
--- a/frontend/test/playwright/visual-regression/pages/pages.spec.ts
+++ b/frontend/test/playwright/visual-regression/pages/pages.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import {
diff --git a/frontend/test/playwright/visual-regression/pages/search-with-banners.spec.ts b/frontend/test/playwright/visual-regression/pages/search-with-banners.spec.ts
index e1752cd6612..f66babd9c28 100644
--- a/frontend/test/playwright/visual-regression/pages/search-with-banners.spec.ts
+++ b/frontend/test/playwright/visual-regression/pages/search-with-banners.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import {
diff --git a/frontend/test/storybook/functional/smoke-test.spec.ts b/frontend/test/storybook/functional/smoke-test.spec.ts
index 8cc0b9998df..b144cb65fda 100644
--- a/frontend/test/storybook/functional/smoke-test.spec.ts
+++ b/frontend/test/storybook/functional/smoke-test.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect, type Page, type Locator } from "@playwright/test"
+import { expect, type Page, type Locator } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 const checkPageLoaded = async (page: Page) => {
   await expect(
diff --git a/frontend/test/storybook/functional/v-checkbox.spec.ts b/frontend/test/storybook/functional/v-checkbox.spec.ts
index 79176312cd1..df6ed7438a8 100644
--- a/frontend/test/storybook/functional/v-checkbox.spec.ts
+++ b/frontend/test/storybook/functional/v-checkbox.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { makeGotoWithArgs } from "~~/test/storybook/utils/args"
 
diff --git a/frontend/test/storybook/functional/v-popover.spec.ts b/frontend/test/storybook/functional/v-popover.spec.ts
index 0fce0d701d3..6b037129ebf 100644
--- a/frontend/test/storybook/functional/v-popover.spec.ts
+++ b/frontend/test/storybook/functional/v-popover.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 const popoverStory =
   "/iframe.html?id=components-vpopover--control&viewMode=story"
diff --git a/frontend/test/storybook/visual-regression/custom-button-components.spec.ts b/frontend/test/storybook/visual-regression/custom-button-components.spec.ts
index 02eab94f11e..e7b0d9472a2 100644
--- a/frontend/test/storybook/visual-regression/custom-button-components.spec.ts
+++ b/frontend/test/storybook/visual-regression/custom-button-components.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import { makeGotoWithArgs } from "~~/test/storybook/utils/args"
 import breakpoints from "~~/test/playwright/utils/breakpoints"
diff --git a/frontend/test/storybook/visual-regression/focus.spec.ts b/frontend/test/storybook/visual-regression/focus.spec.ts
index ccbb01cd499..28bdbfbaf8f 100644
--- a/frontend/test/storybook/visual-regression/focus.spec.ts
+++ b/frontend/test/storybook/visual-regression/focus.spec.ts
@@ -1,4 +1,6 @@
-import { test, Page } from "@playwright/test"
+import { Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { expectScreenshotAreaSnapshot } from "~~/test/playwright/utils/expect-snapshot"
 
diff --git a/frontend/test/storybook/visual-regression/v-button.spec.ts b/frontend/test/storybook/visual-regression/v-button.spec.ts
index be523d5c4a4..3368661b3a8 100644
--- a/frontend/test/storybook/visual-regression/v-button.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-button.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import { makeGotoWithArgs } from "~~/test/storybook/utils/args"
 
diff --git a/frontend/test/storybook/visual-regression/v-checkbox.spec.ts b/frontend/test/storybook/visual-regression/v-checkbox.spec.ts
index d604f8f92f5..1d37eb5696c 100644
--- a/frontend/test/storybook/visual-regression/v-checkbox.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-checkbox.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import { expectScreenshotAreaSnapshot } from "~~/test/playwright/utils/expect-snapshot"
 
diff --git a/frontend/test/storybook/visual-regression/v-collection-header.spec.ts b/frontend/test/storybook/visual-regression/v-collection-header.spec.ts
index d0393e060eb..935d1b14079 100644
--- a/frontend/test/storybook/visual-regression/v-collection-header.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-collection-header.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import { dirParam } from "~~/test/storybook/utils/args"
diff --git a/frontend/test/storybook/visual-regression/v-filter-button.spec.ts b/frontend/test/storybook/visual-regression/v-filter-button.spec.ts
index 4640cc8d797..a285238d19c 100644
--- a/frontend/test/storybook/visual-regression/v-filter-button.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-filter-button.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import { makeGotoWithArgs } from "~~/test/storybook/utils/args"
diff --git a/frontend/test/storybook/visual-regression/v-filter-tab.spec.ts b/frontend/test/storybook/visual-regression/v-filter-tab.spec.ts
index 4d5211172c5..ea94bf39806 100644
--- a/frontend/test/storybook/visual-regression/v-filter-tab.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-filter-tab.spec.ts
@@ -1,4 +1,6 @@
-import { expect, type Page, test } from "@playwright/test"
+import { expect, type Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { makeUrlWithArgs } from "~~/test/storybook/utils/args"
 import { waitForResponse } from "~~/test/storybook/utils/response"
diff --git a/frontend/test/storybook/visual-regression/v-footer.spec.ts b/frontend/test/storybook/visual-regression/v-footer.spec.ts
index 482e50cbe62..2f2db246956 100644
--- a/frontend/test/storybook/visual-regression/v-footer.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-footer.spec.ts
@@ -1,4 +1,6 @@
-import { expect, Page, test } from "@playwright/test"
+import { expect, Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/test/storybook/visual-regression/v-header-internal.spec.ts b/frontend/test/storybook/visual-regression/v-header-internal.spec.ts
index f704eebfcca..7f12d9b0e4b 100644
--- a/frontend/test/storybook/visual-regression/v-header-internal.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-header-internal.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import {
diff --git a/frontend/test/storybook/visual-regression/v-icon-button.spec.ts b/frontend/test/storybook/visual-regression/v-icon-button.spec.ts
index aa47b137bc4..06235031717 100644
--- a/frontend/test/storybook/visual-regression/v-icon-button.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-icon-button.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { expectSnapshot } from "~~/test/playwright/utils/expect-snapshot"
 
diff --git a/frontend/test/storybook/visual-regression/v-image-cell.spec.ts b/frontend/test/storybook/visual-regression/v-image-cell.spec.ts
index 12cbb04f33a..8b94222f71d 100644
--- a/frontend/test/storybook/visual-regression/v-image-cell.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-image-cell.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/test/storybook/visual-regression/v-language-select.spec.ts b/frontend/test/storybook/visual-regression/v-language-select.spec.ts
index 63548680222..9721f7cae97 100644
--- a/frontend/test/storybook/visual-regression/v-language-select.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-language-select.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { makeGotoWithArgs } from "~~/test/storybook/utils/args"
 import { expectSnapshot } from "~~/test/playwright/utils/expect-snapshot"
diff --git a/frontend/test/storybook/visual-regression/v-media-license.spec.ts b/frontend/test/storybook/visual-regression/v-media-license.spec.ts
index 4e13b506c8e..a58f0b32372 100644
--- a/frontend/test/storybook/visual-regression/v-media-license.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-media-license.spec.ts
@@ -1,4 +1,6 @@
-import { test, Page } from "@playwright/test"
+import { Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/test/storybook/visual-regression/v-media-reuse.spec.ts b/frontend/test/storybook/visual-regression/v-media-reuse.spec.ts
index fd92485bcf3..1922cf7e320 100644
--- a/frontend/test/storybook/visual-regression/v-media-reuse.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-media-reuse.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 import { sleep } from "~~/test/playwright/utils/navigation"
diff --git a/frontend/test/storybook/visual-regression/v-notitication-banner.spec.ts b/frontend/test/storybook/visual-regression/v-notitication-banner.spec.ts
index a68faecb541..422c41f6c32 100644
--- a/frontend/test/storybook/visual-regression/v-notitication-banner.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-notitication-banner.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/test/storybook/visual-regression/v-safety-wall.spec.ts b/frontend/test/storybook/visual-regression/v-safety-wall.spec.ts
index 4c35c4e26c9..c501155b2df 100644
--- a/frontend/test/storybook/visual-regression/v-safety-wall.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-safety-wall.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/test/storybook/visual-regression/v-search-bar-button.spec.ts b/frontend/test/storybook/visual-regression/v-search-bar-button.spec.ts
index a98168cdfbc..e5c87720f3e 100644
--- a/frontend/test/storybook/visual-regression/v-search-bar-button.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-search-bar-button.spec.ts
@@ -1,4 +1,4 @@
-import { test } from "@playwright/test"
+import { test } from "~~/test/playwright/utils/test"
 
 import { expectSnapshot } from "~~/test/playwright/utils/expect-snapshot"
 
diff --git a/frontend/test/storybook/visual-regression/v-search-type-button.spec.ts b/frontend/test/storybook/visual-regression/v-search-type-button.spec.ts
index 915ff4a28c1..8b5fdda62d1 100644
--- a/frontend/test/storybook/visual-regression/v-search-type-button.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-search-type-button.spec.ts
@@ -1,4 +1,6 @@
-import { expect, type Page, test } from "@playwright/test"
+import { expect, type Page } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { makeUrlWithArgs } from "~~/test/storybook/utils/args"
 import { t } from "~~/test/playwright/utils/i18n"
diff --git a/frontend/test/storybook/visual-regression/v-search-types.spec.ts b/frontend/test/storybook/visual-regression/v-search-types.spec.ts
index 3792e5ecf1e..381c0b9cb01 100644
--- a/frontend/test/storybook/visual-regression/v-search-types.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-search-types.spec.ts
@@ -1,4 +1,6 @@
-import { expect, test } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import breakpoints from "~~/test/playwright/utils/breakpoints"
 
diff --git a/frontend/test/storybook/visual-regression/v-select-field.spec.ts b/frontend/test/storybook/visual-regression/v-select-field.spec.ts
index dedabccb360..b1424359e9b 100644
--- a/frontend/test/storybook/visual-regression/v-select-field.spec.ts
+++ b/frontend/test/storybook/visual-regression/v-select-field.spec.ts
@@ -1,4 +1,6 @@
-import { test, expect } from "@playwright/test"
+import { expect } from "@playwright/test"
+
+import { test } from "~~/test/playwright/utils/test"
 
 import { makeGotoWithArgs } from "~~/test/storybook/utils/args"
 import { expectSnapshot } from "~~/test/playwright/utils/expect-snapshot"
diff --git a/packages/js/eslint-plugin/src/configs/index.ts b/packages/js/eslint-plugin/src/configs/index.ts
index c67c76c8d09..1a4d505c8b2 100644
--- a/packages/js/eslint-plugin/src/configs/index.ts
+++ b/packages/js/eslint-plugin/src/configs/index.ts
@@ -108,6 +108,16 @@ export const project: TSESLint.Linter.ConfigType = {
             ],
           },
         ],
+
+        "no-restricted-imports": [
+          "error",
+          {
+            name: "@playwright/test",
+            importNames: ["test"],
+            message:
+              "Import test from `~~/test/playwright/utils/test` to ensure global fixtures are used.",
+          },
+        ],
       },
     },
     {