From 61a7ca244e65dfd449f68756d6a380adb5073a75 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 29 Jan 2026 11:26:17 +0100 Subject: [PATCH 01/37] [PoC] Implement basics for E2E tests driven by playwright (JS) --- config/.env.e2e_test | 38 +++++ config/e2e_test.exs | 18 +++ config/runtime.exs | 4 + e2e/.gitignore | 8 + e2e/package-lock.json | 97 ++++++++++++ e2e/package.json | 14 ++ e2e/playwright.config.ts | 79 ++++++++++ e2e/tests/example.spec.ts | 147 +++++++++++++++++++ e2e/tests/example10.spec.ts | 147 +++++++++++++++++++ e2e/tests/example2.spec.ts | 147 +++++++++++++++++++ e2e/tests/example3.spec.ts | 147 +++++++++++++++++++ e2e/tests/example4.spec.ts | 147 +++++++++++++++++++ e2e/tests/example5.spec.ts | 147 +++++++++++++++++++ e2e/tests/example6.spec.ts | 147 +++++++++++++++++++ e2e/tests/example7.spec.ts | 147 +++++++++++++++++++ e2e/tests/example8.spec.ts | 147 +++++++++++++++++++ e2e/tests/example9.spec.ts | 147 +++++++++++++++++++ extra/lib/plausible_web/dogfood.ex | 2 +- lib/mix/tasks/clean_clickhouse.ex | 10 ++ lib/mix/tasks/clean_postgres.ex | 31 ++++ lib/plausible/auth/auth.ex | 12 +- lib/plausible/prom_ex.ex | 2 +- lib/plausible/s3.ex | 2 +- lib/plausible/session/balancer_supervisor.ex | 2 +- lib/plausible_web/router.ex | 4 +- lib/workers/clickhouse_clean_sites.ex | 2 +- mix.exs | 27 +++- priv/repo/e2e_seeds.exs | 24 +++ test/test_helper.exs | 33 +++-- 29 files changed, 1850 insertions(+), 29 deletions(-) create mode 100644 config/.env.e2e_test create mode 100644 config/e2e_test.exs create mode 100644 e2e/.gitignore create mode 100644 e2e/package-lock.json create mode 100644 e2e/package.json create mode 100644 e2e/playwright.config.ts create mode 100644 e2e/tests/example.spec.ts create mode 100644 e2e/tests/example10.spec.ts create mode 100644 e2e/tests/example2.spec.ts create mode 100644 e2e/tests/example3.spec.ts create mode 100644 e2e/tests/example4.spec.ts create mode 100644 e2e/tests/example5.spec.ts create mode 100644 e2e/tests/example6.spec.ts create mode 100644 e2e/tests/example7.spec.ts create mode 100644 e2e/tests/example8.spec.ts create mode 100644 e2e/tests/example9.spec.ts create mode 100644 lib/mix/tasks/clean_postgres.ex create mode 100644 priv/repo/e2e_seeds.exs diff --git a/config/.env.e2e_test b/config/.env.e2e_test new file mode 100644 index 000000000000..052f02eab8ae --- /dev/null +++ b/config/.env.e2e_test @@ -0,0 +1,38 @@ +BASE_URL=http://localhost:8000 +SECURE_COOKIE=false +DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/plausible_e2e +CLICKHOUSE_DATABASE_URL=http://127.0.0.1:8123/plausible_e2e +CLICKHOUSE_MAX_BUFFER_SIZE_BYTES=1000000 +SECRET_KEY_BASE=/njrhntbycvastyvtk1zycwfm981vpo/0xrvwjjvemdakc/vsvbrevlwsc6u8rcg +TOTP_VAULT_KEY=Q3BD4nddbkVJIPXgHuo5NthGKSIH0yesRfG05J88HIo= +ENVIRONMENT=dev +MAILER_ADAPTER=Bamboo.LocalAdapter +LOG_LEVEL=warning +SELFHOST=false +DISABLE_CRON=true +ADMIN_USER_IDS=1 +SHOW_CITIES=true +PADDLE_VENDOR_AUTH_CODE=895e20d4efaec0575bb857f44b183217b332d9592e76e69b8a +PADDLE_VENDOR_ID=3942 +SSO_VERIFICATION_NAMESERVERS=0.0.0.0:5354 + +GOOGLE_CLIENT_ID=875387135161-l8tp53dpt7fdhdg9m1pc3vl42si95rh0.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 + +PROMEX_DISABLED=false +SITE_DEFAULT_INGEST_THRESHOLD=1000000 + +S3_DISABLED=false +S3_ACCESS_KEY_ID=minioadmin +S3_SECRET_ACCESS_KEY=minioadmin +S3_REGION=us-east-1 +S3_ENDPOINT=http://localhost:10000 +S3_EXPORTS_BUCKET=dev-exports +S3_IMPORTS_BUCKET=dev-imports + +HELP_SCOUT_APP_ID=fake_app_id +HELP_SCOUT_APP_SECRET=fake_app_secret +HELP_SCOUT_SIGNATURE_KEY=fake_signature_key +HELP_SCOUT_VAULT_KEY=ym9ZQg0KPNGCH3C2eD5y6KpL0tFzUqAhwxQO6uEv/ZM= + +VERIFICATION_ENABLED=true diff --git a/config/e2e_test.exs b/config/e2e_test.exs new file mode 100644 index 000000000000..697727ec3a2c --- /dev/null +++ b/config/e2e_test.exs @@ -0,0 +1,18 @@ +import Config + +config :plausible, PlausibleWeb.Endpoint, + server: true, + check_origin: false + +config :plausible, paddle_api: Plausible.Billing.DevPaddleApiMock + +config :phoenix, :stacktrace_depth, 20 +config :phoenix, :plug_init_mode, :runtime + +config :bcrypt_elixir, :log_rounds, 4 + +config :plausible, Plausible.Ingestion.Counters, enabled: false + +config :plausible, Oban, testing: :manual + +config :plausible, Plausible.Session.Salts, interval: :timer.hours(1) diff --git a/config/runtime.exs b/config/runtime.exs index 0d2e62860151..0ecd2d04d558 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -14,6 +14,10 @@ if config_env() == :ce_test do Envy.load(["config/.env.test"]) end +if config_env() == :e2e_test do + Envy.load(["config/.env.e2e_test"]) +end + config_dir = System.get_env("CONFIG_DIR", "/run/secrets") log_format = diff --git a/e2e/.gitignore b/e2e/.gitignore new file mode 100644 index 000000000000..335bd46df270 --- /dev/null +++ b/e2e/.gitignore @@ -0,0 +1,8 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/e2e/package-lock.json b/e2e/package-lock.json new file mode 100644 index 000000000000..f76d66a0eace --- /dev/null +++ b/e2e/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.58.0", + "@types/node": "^25.1.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.0.tgz", + "integrity": "sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "25.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.1.0.tgz", + "integrity": "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz", + "integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz", + "integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/e2e/package.json b/e2e/package.json new file mode 100644 index 000000000000..43cb44e6d8d3 --- /dev/null +++ b/e2e/package.json @@ -0,0 +1,14 @@ +{ + "name": "e2e", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.58.0", + "@types/node": "^25.1.0" + } +} diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts new file mode 100644 index 000000000000..83e863c818a1 --- /dev/null +++ b/e2e/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* 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('')`. */ + baseURL: process.env.BASE_URL, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + // + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'cd .. && mix phx.server', + url: process.env.BASE_URL, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/e2e/tests/example.spec.ts b/e2e/tests/example.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example10.spec.ts b/e2e/tests/example10.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example10.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example2.spec.ts b/e2e/tests/example2.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example2.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example3.spec.ts b/e2e/tests/example3.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example3.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example4.spec.ts b/e2e/tests/example4.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example4.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example5.spec.ts b/e2e/tests/example5.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example5.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example6.spec.ts b/e2e/tests/example6.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example6.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example7.spec.ts b/e2e/tests/example7.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example7.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example8.spec.ts b/e2e/tests/example8.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example8.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/e2e/tests/example9.spec.ts b/e2e/tests/example9.spec.ts new file mode 100644 index 000000000000..fac75819d60f --- /dev/null +++ b/e2e/tests/example9.spec.ts @@ -0,0 +1,147 @@ +import { test, expect } from "@playwright/test"; + +const baseUrl = process.env.BASE_URL; + +test("dashboard renders", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page).toHaveTitle(/Plausible/); + + await expect( + page.getByRole("button", { name: "public.example.com" }), + ).toBeVisible(); +}); + +test("filter is applied", async ({ page }) => { + await page.goto("/public.example.com"); + + await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); + + await page.getByRole("button", { name: "Filter" }).click(); + + await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await expect( + page.getByRole("heading", { name: "Filter by Page" }), + ).toBeVisible(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await page.getByPlaceholder("Select a Page").click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: true }), + ).toHaveCount(1); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeVisible(); + + await page.getByPlaceholder("Select a Page").fill("pag"); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page1" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page2" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/page3" }), + ).toBeVisible(); + + await expect( + page.getByRole("listitem").filter({ hasText: "/other" }), + ).toBeHidden(); + + await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); + + await expect( + page.getByRole("button", { name: "Apply filter", disabled: false }), + ).toHaveCount(1); + + await page.getByRole("button", { name: "Apply filter" }).click(); + + await expect(page).toHaveURL( + baseUrl + "/public.example.com?f=is,page,/page1", + ); + + await expect( + page.getByRole("link", { name: "Page is /page1" }), + ).toHaveAttribute("title", "Edit filter: Page is /page1"); +}); + +test("tab selection user preferences are preserved across reloads", async ({ + page, +}) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Entry pages" }).click(); + + await page.goto("/public.example.com"); + + let currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("entry-pages"); + + await page.getByRole("button", { name: "Exit pages" }).click(); + + await page.goto("/public.example.com"); + + currentTab = await page.evaluate(() => + localStorage.getItem("pageTab__public.example.com"), + ); + + await expect(currentTab).toEqual("exit-pages"); +}); + +test("back navigation closes the modal", async ({ page }) => { + await page.goto("/public.example.com"); + + await page.getByRole("button", { name: "Filter" }).click(); + + await page.getByRole("link", { name: "Page" }).click(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); + + await page.goBack(); + + await expect(page).toHaveURL(baseUrl + "/public.example.com"); +}); + +test("opens for logged in user", async ({ page }) => { + await page.goto("/login"); + + await page.getByLabel("Email").fill("user@plausible.test"); + + await page.getByLabel("Password").fill("plausible"); + + await page.getByRole("button", { name: "Log in" }).click(); + + await page.goto("/private.example.com"); + + await expect( + page.getByRole("button", { name: "private.example.com" }), + ).toBeVisible(); +}); diff --git a/extra/lib/plausible_web/dogfood.ex b/extra/lib/plausible_web/dogfood.ex index ad72dbab72d3..6881e4d060a1 100644 --- a/extra/lib/plausible_web/dogfood.ex +++ b/extra/lib/plausible_web/dogfood.ex @@ -43,7 +43,7 @@ defmodule PlausibleWeb.Dogfood do "pa-invalid-script-id" - env in ["test", "ce_test"] -> + env in ["test", "ce_test", "e2e_test"] -> "" end diff --git a/lib/mix/tasks/clean_clickhouse.ex b/lib/mix/tasks/clean_clickhouse.ex index 2430272e494e..071f139be251 100644 --- a/lib/mix/tasks/clean_clickhouse.ex +++ b/lib/mix/tasks/clean_clickhouse.ex @@ -1,9 +1,17 @@ defmodule Mix.Tasks.CleanClickhouse do + @moduledoc false + use Mix.Task alias Plausible.IngestRepo def run(_) do + case Plausible.IngestRepo.start_link(pool_size: 1, log: false) do + {:ok, _} -> :pass + {:error, {:already_started, _pid}} -> :pass + {:error, _} = error -> throw(error) + end + %{rows: rows} = IngestRepo.query!("show tables") tables = Enum.map(rows, fn [table] -> table end) @@ -22,5 +30,7 @@ defmodule Mix.Tasks.CleanClickhouse do Enum.each(to_truncate, fn table -> IngestRepo.query!("truncate #{table}") end) + after + Plausible.IngestRepo.stop(500) end end diff --git a/lib/mix/tasks/clean_postgres.ex b/lib/mix/tasks/clean_postgres.ex new file mode 100644 index 000000000000..29cc7fc70af4 --- /dev/null +++ b/lib/mix/tasks/clean_postgres.ex @@ -0,0 +1,31 @@ +defmodule Mix.Tasks.CleanPostgres do + @moduledoc false + + use Mix.Task + + alias Plausible.Repo + + def run(_) do + case Plausible.Repo.start_link(pool_size: 1, log: false) do + {:ok, _} -> :pass + {:error, {:already_started, _pid}} -> :pass + {:error, _} = error -> throw(error) + end + + query = """ + SELECT tablename + FROM pg_catalog.pg_tables + WHERE schemaname != 'pg_catalog' AND + schemaname != 'information_schema'; + """ + + %{rows: rows} = Repo.query!(query) + tables = Enum.map(rows, fn [table] -> table end) + + Enum.each(tables -- ["schema_migrations"], fn table -> + Repo.query!("truncate #{table} cascade") + end) + after + Plausible.Repo.stop(500) + end +end diff --git a/lib/plausible/auth/auth.ex b/lib/plausible/auth/auth.ex index 7831aa3752bf..91606dcde5da 100644 --- a/lib/plausible/auth/auth.ex +++ b/lib/plausible/auth/auth.ex @@ -15,15 +15,23 @@ defmodule Plausible.Auth do require Logger + if Mix.env() == :e2e_test do + @ip_rate_limit 100_000 + @user_rate_limit 100_000 + else + @ip_rate_limit 5 + @user_rate_limit 5 + end + @rate_limits %{ login_ip: %{ prefix: "login:ip", - limit: 5, + limit: @ip_rate_limit, interval: :timer.seconds(60) }, login_user: %{ prefix: "login:user", - limit: 5, + limit: @user_rate_limit, interval: :timer.seconds(60) }, email_change_user: %{ diff --git a/lib/plausible/prom_ex.ex b/lib/plausible/prom_ex.ex index 8e22a4dc4309..f2186e0206a6 100644 --- a/lib/plausible/prom_ex.ex +++ b/lib/plausible/prom_ex.ex @@ -19,7 +19,7 @@ defmodule Plausible.PromEx do ] @impl true - if Mix.env() in [:test, :ce_test] do + if Mix.env() in [:test, :ce_test, :e2e_test] do # PromEx tries to query Oban's DB tables in order to retrieve metrics. # During tests, however, this is pointless as Oban is in manual mode, # and that leads to connection ownership clashes. diff --git a/lib/plausible/s3.ex b/lib/plausible/s3.ex index eca8d3c18a41..1d471a43fd24 100644 --- a/lib/plausible/s3.ex +++ b/lib/plausible/s3.ex @@ -58,7 +58,7 @@ defmodule Plausible.S3 do # to make ClickHouse see MinIO in dev and test envs we replace # the host in the S3 URL with host.docker.internal or whatever's set in $MINIO_HOST_FOR_CLICKHOUSE - if Mix.env() in [:dev, :test, :ce_dev, :ce_test] do + if Mix.env() in [:dev, :test, :ce_dev, :ce_test, :e2e_test] do defp extract_s3_url(presigned_url) do [s3_url, _] = String.split(presigned_url, "?") default_ch_host = unless System.get_env("CI"), do: "host.docker.internal" diff --git a/lib/plausible/session/balancer_supervisor.ex b/lib/plausible/session/balancer_supervisor.ex index e35969841d80..2c456b7d1ae0 100644 --- a/lib/plausible/session/balancer_supervisor.ex +++ b/lib/plausible/session/balancer_supervisor.ex @@ -2,7 +2,7 @@ defmodule Plausible.Session.BalancerSupervisor do @moduledoc "Serialize session processing to avoid explicit locks" use Supervisor - if Mix.env() in [:test, :ce_test] do + if Mix.env() in [:test, :ce_test, :e2e_test] do def size(), do: 10 else diff --git a/lib/plausible_web/router.ex b/lib/plausible_web/router.ex index 587c2e3c0899..04b4f55b07bd 100644 --- a/lib/plausible_web/router.ex +++ b/lib/plausible_web/router.ex @@ -137,7 +137,7 @@ defmodule PlausibleWeb.Router do end on_ee do - if Mix.env() in [:dev, :test] do + if Mix.env() in [:dev, :test, :e2e_test] do scope "/dev", PlausibleWeb do pipe_through :browser @@ -153,7 +153,7 @@ defmodule PlausibleWeb.Router do end # Routes for plug integration testing - if Mix.env() in [:test, :ce_test] do + if Mix.env() in [:test, :ce_test, :e2e_test] do scope "/plug-tests", PlausibleWeb do scope [] do pipe_through :browser diff --git a/lib/workers/clickhouse_clean_sites.ex b/lib/workers/clickhouse_clean_sites.ex index 3d14ec0c7ae3..a425fdfa5864 100644 --- a/lib/workers/clickhouse_clean_sites.ex +++ b/lib/workers/clickhouse_clean_sites.ex @@ -31,7 +31,7 @@ defmodule Plausible.Workers.ClickhouseCleanSites do "imported_visitors" ] - @settings if Mix.env() in [:test, :ce_test], do: [mutations_sync: 2], else: [] + @settings if Mix.env() in [:test, :ce_test, :e2e_test], do: [mutations_sync: 2], else: [] def perform(_job) do deleted_sites = get_deleted_sites_with_clickhouse_data() diff --git a/mix.exs b/mix.exs index a70666b655a6..5b3b402736d2 100644 --- a/mix.exs +++ b/mix.exs @@ -50,7 +50,7 @@ defmodule Plausible.MixProject do end # Specifies which paths to compile per environment. - defp elixirc_paths(env) when env in [:test, :dev], + defp elixirc_paths(env) when env in [:test, :e2e_test, :dev], do: ["lib", "test/support", "extra/lib"] defp elixirc_paths(env) when env in [:ce_test, :ce_dev], @@ -69,7 +69,7 @@ defmodule Plausible.MixProject do {:bamboo_smtp, "~> 4.1"}, {:bamboo_mua, "~> 0.2.0"}, {:bcrypt_elixir, "~> 3.3"}, - {:bypass, "~> 2.1", only: [:dev, :test, :ce_test]}, + {:bypass, "~> 2.1", only: [:dev, :test, :ce_test, :e2e_test]}, {:ecto_ch, "~> 0.8.4"}, {:cloak, "~> 1.1"}, {:cloak_ecto, "~> 1.2"}, @@ -77,12 +77,12 @@ defmodule Plausible.MixProject do {:cors_plug, "~> 3.0"}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, - {:double, "~> 0.8.0", only: [:dev, :test, :ce_test, :ce_dev]}, + {:double, "~> 0.8.0", only: [:dev, :test, :ce_test, :ce_dev, :e2e_test]}, {:ecto, "~> 3.13.5"}, {:ecto_sql, "~> 3.13.2"}, {:envy, "~> 1.1.1"}, {:eqrcode, "~> 0.2.1"}, - {:ex_machina, "~> 2.3", only: [:dev, :test, :ce_dev, :ce_test]}, + {:ex_machina, "~> 2.3", only: [:dev, :test, :ce_dev, :ce_test, :e2e_test]}, {:excoveralls, "~> 0.10", only: :test}, {:finch, "~> 0.20.0"}, {:floki, "~> 0.36"}, @@ -94,7 +94,7 @@ defmodule Plausible.MixProject do {:hackney, "~> 1.8"}, {:jason, "~> 1.3"}, {:location, git: "https://github.com/plausible/location.git"}, - {:mox, "~> 1.0", only: [:test, :ce_test]}, + {:mox, "~> 1.0", only: [:test, :ce_test, :e2e_test]}, {:nanoid, "~> 2.1.0"}, {:nimble_totp, "~> 1.0"}, {:oban, "~> 2.20.1"}, @@ -152,11 +152,11 @@ defmodule Plausible.MixProject do {:con_cache, git: "https://github.com/aerosol/con_cache", branch: "ensure-dirty-ops-emit-telemetry"}, {:req, "~> 0.5.16"}, - {:happy_tcp, github: "ruslandoga/happy_tcp", only: [:ce, :ce_dev, :ce_test]}, + {:happy_tcp, github: "ruslandoga/happy_tcp", only: [:ce, :ce_dev, :ce_test, :e2e_test]}, {:ex_json_schema, "~> 0.11.1"}, {:odgn_json_pointer, "~> 3.1.0"}, - {:phoenix_bakery, "~> 0.1.2", only: [:ce, :ce_dev, :ce_test]}, - {:site_encrypt, github: "sasa1977/site_encrypt", only: [:ce, :ce_dev, :ce_test]}, + {:phoenix_bakery, "~> 0.1.2", only: [:ce, :ce_dev, :ce_test, :e2e_test]}, + {:site_encrypt, github: "sasa1977/site_encrypt", only: [:ce, :ce_dev, :ce_test, :e2e_test]}, {:phoenix_storybook, "~> 0.9"}, {:libcluster, "~> 3.5"} ] @@ -168,6 +168,17 @@ defmodule Plausible.MixProject do "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test", "clean_clickhouse"], + "test.e2e": [ + "esbuild default", + "ecto.create --quiet", + "ecto.migrate", + "clean_postgres", + "clean_clickhouse", + "run priv/repo/e2e_seeds.exs", + "cmd --shell --cd e2e npm exec playwright test", + "clean_postgres", + "clean_clickhouse" + ], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], "assets.typecheck": ["cmd npm --prefix assets run typecheck"], "assets.build": [ diff --git a/priv/repo/e2e_seeds.exs b/priv/repo/e2e_seeds.exs new file mode 100644 index 000000000000..03f52bb864f6 --- /dev/null +++ b/priv/repo/e2e_seeds.exs @@ -0,0 +1,24 @@ +use Plausible + +import Plausible.Teams.Test + +hours_ago = fn hr -> + NaiveDateTime.utc_now(:second) |> NaiveDateTime.add(-hr, :hour) +end + +user = new_user(email: "user@plausible.test", password: "plausible") + +public_site = new_site(domain: "public.example.com", public: true) + +Plausible.TestUtils.populate_stats(public_site, [ + Plausible.Factory.build(:pageview, pathname: "/page1", timestamp: hours_ago.(48)), + Plausible.Factory.build(:pageview, pathname: "/page2", timestamp: hours_ago.(48)), + Plausible.Factory.build(:pageview, pathname: "/page3", timestamp: hours_ago.(48)), + Plausible.Factory.build(:pageview, pathname: "/other", timestamp: hours_ago.(48)) +]) + +private_site = new_site(domain: "private.example.com", owner: user) + +Plausible.TestUtils.populate_stats(private_site, [ + Plausible.Factory.build(:pageview, pathname: "/", timestamp: hours_ago.(48)) +]) diff --git a/test/test_helper.exs b/test/test_helper.exs index e11ea3df954d..bd6ac0e85ed4 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -3,15 +3,18 @@ if not Enum.empty?(Path.wildcard("lib/**/*_test.exs")) do end {:ok, _} = Application.ensure_all_started(:ex_machina) -Mox.defmock(Plausible.HTTPClient.Mock, for: Plausible.HTTPClient.Interface) -Mox.defmock(Plausible.DnsLookup.Mock, - for: Plausible.DnsLookupInterface -) +if Mix.env() != :e2e_test do + Mox.defmock(Plausible.HTTPClient.Mock, for: Plausible.HTTPClient.Interface) -Application.ensure_all_started(:double) + Mox.defmock(Plausible.DnsLookup.Mock, + for: Plausible.DnsLookupInterface + ) -Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual) + Application.ensure_all_started(:double) + + Ecto.Adapters.SQL.Sandbox.mode(Plausible.Repo, :manual) +end # warn about minio if it's included in tests but not running if :minio in Keyword.fetch!(ExUnit.configuration(), :include) do @@ -27,10 +30,16 @@ for {app, _, _} <- Application.loaded_applications() do end end -if Mix.env() == :ce_test do - IO.puts("Test mode: Community Edition") - ExUnit.configure(exclude: [:ee_only | default_exclude]) -else - IO.puts("Test mode: Enterprise Edition") - ExUnit.configure(exclude: [:ce_build_only | default_exclude]) +case Mix.env() do + :ce_test -> + IO.puts("Test mode: Community Edition") + ExUnit.configure(exclude: [:ee_only, :e2e | default_exclude]) + + :e2e_test -> + IO.puts("Test mode: End-to-End Tests") + ExUnit.configure(exclude: [:test], include: [:e2e]) + + _ -> + IO.puts("Test mode: Enterprise Edition") + ExUnit.configure(exclude: [:ce_build_only, :e2e | default_exclude]) end From 9d7445272050d1ab0d6a54d55884972f4aec98ae Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 13:59:50 +0100 Subject: [PATCH 02/37] Update tracker test run instructions in README --- tracker/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracker/README.md b/tracker/README.md index d10f1f131c52..df3b99311d8f 100644 --- a/tracker/README.md +++ b/tracker/README.md @@ -26,7 +26,7 @@ Use `node compile.js --web-snippet` if you need to update web snippet code. ### Tests -Tests can be run in UI mode via `npm run playwright --ui`. This helps with debugging. +Tests can be run in UI mode via `npx playwright test --ui`. This helps with debugging. ### NPM package From 3e2df3104c27a5eff2a6dda7ab010658c94db691 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:00:47 +0100 Subject: [PATCH 03/37] Update package config on e2e tests --- e2e/package.json | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/e2e/package.json b/e2e/package.json index 43cb44e6d8d3..20338fef761b 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,12 +1,10 @@ { "name": "e2e", - "version": "1.0.0", - "main": "index.js", - "scripts": {}, - "keywords": [], - "author": "", - "license": "ISC", - "description": "", + "scripts": { + "test": "npx playwright test", + "test:ui": "npx playwright test --ui" + }, + "license": "MIT", "devDependencies": { "@playwright/test": "^1.58.0", "@types/node": "^25.1.0" From ad0ca6ec958fffd0f1f6959b0a69067b3f77984c Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:01:10 +0100 Subject: [PATCH 04/37] Set playwright reporter format to list --- e2e/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 83e863c818a1..7d551edb6653 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', + reporter: 'list', /* 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('')`. */ From 3a4828535b52333064d12c6d739824205ffd6572 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:01:30 +0100 Subject: [PATCH 05/37] Add mix task for running e2e tests in UI mode --- mix.exs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 5b3b402736d2..61ea2b5e7608 100644 --- a/mix.exs +++ b/mix.exs @@ -175,7 +175,18 @@ defmodule Plausible.MixProject do "clean_postgres", "clean_clickhouse", "run priv/repo/e2e_seeds.exs", - "cmd --shell --cd e2e npm exec playwright test", + "cmd npm run --prefix ./e2e test", + "clean_postgres", + "clean_clickhouse" + ], + "test.e2e.ui": [ + "esbuild default", + "ecto.create --quiet", + "ecto.migrate", + "clean_postgres", + "clean_clickhouse", + "run priv/repo/e2e_seeds.exs", + "cmd npm run --prefix ./e2e test:ui", "clean_postgres", "clean_clickhouse" ], From ba5e3ba2f865657f2da33a90effef54f08a5c94d Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:02:18 +0100 Subject: [PATCH 06/37] Add e2e test run job to CI config --- .github/workflows/elixir.yml | 114 +++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 9d3db71650e3..d16514840080 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -113,6 +113,119 @@ jobs: env: MIX_TEST_PARTITION: ${{ matrix.mix_test_partition }} + e2e: + name: End-to-end tests + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + MIX_ENV: e2e_test + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3, 4] + shardTotal: [4] + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: marocchino/tool-versions-action@v1 + id: versions + + - uses: erlef/setup-beam@v1 + with: + elixir-version: ${{ steps.versions.outputs.elixir }} + otp-version: ${{ steps.versions.outputs.erlang }} + + - uses: actions/cache@v5 + with: + path: | + deps + _build + tracker/node_modules + priv/tracker/js + priv/tracker/installation_support + ${{ env.PERSISTENT_CACHE_DIR }} + key: e2e-${{ env.MIX_ENV }}-${{ env.CACHE_VERSION }}-${{ github.head_ref || github.ref }}-${{ hashFiles('**/mix.lock') }} + restore-keys: | + e2e-${{ env.MIX_ENV }}-${{ env.CACHE_VERSION }}-${{ github.head_ref || github.ref }}- + e2e-${{ env.MIX_ENV }}-${{ env.CACHE_VERSION }}-refs/heads/master- + + - name: Check for changes in tracker/** + uses: dorny/paths-filter@v3 + id: changes + with: + filters: | + tracker: + - 'tracker/**' + + - name: Check if tracker and verifier are built already + run: | + if [ -f priv/tracker/js/plausible-web.js ] && [ -f priv/tracker/installation_support/verifier.js ]; then + echo "HAS_BUILT_TRACKER=true" >> $GITHUB_ENV + else + echo "HAS_BUILT_TRACKER=false" >> $GITHUB_ENV + fi + + - run: npm install --prefix ./tracker + if: steps.changes.outputs.tracker == 'true' || env.HAS_BUILT_TRACKER == 'false' + + - run: npm run deploy --prefix ./tracker + if: steps.changes.outputs.tracker == 'true' || env.HAS_BUILT_TRACKER == 'false' + + - run: mix deps.get --only $MIX_ENV + - run: mix compile --warnings-as-errors --all-warnings + - run: mix do ecto.create, ecto.migrate + - run: mix run -e "Tzdata.ReleaseUpdater.poll_for_update" + + - name: Install E2E dependencies + run: npm --prefix ./e2e ci + + - name: Install E2E Playwright system dependencies + working-directory: ./e2e + run: npx playwright install-deps + + - name: Install E2E Playwright Browsers + working-directory: ./e2e + run: npx playwright install + + - name: Run E2E Playwright tests + run: npm --prefix ./e2e test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob + + - name: Upload E2E blob report to GitHub Actions Artifacts + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v6 + with: + name: e2e-blob-report-${{ matrix.shardIndex }} + path: e2e/blob-report + retention-days: 1 + + merge-sharded-e2e-test-report: + if: ${{ !cancelled() }} + needs: [e2e] + timeout-minutes: 5 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 + with: + node-version: 23.2.0 + cache: 'npm' + cache-dependency-path: e2e/package-lock.json + - name: Install dependencies + run: npm --prefix ./e2e ci + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v7 + with: + path: all-e2e-blob-reports + pattern: e2e-blob-report-* + merge-multiple: true + + - name: Merge into list report + working-directory: ./e2e + run: npx playwright merge-reports --reporter list ../all-e2e-blob-reports + static: name: Static checks (format, credo, dialyzer) env: @@ -125,6 +238,7 @@ jobs: - uses: marocchino/tool-versions-action@v1 id: versions + - uses: erlef/setup-beam@v1 with: elixir-version: ${{ steps.versions.outputs.elixir }} From 36751a2eb2b9334461910c4759f3a16ff415a652 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:09:43 +0100 Subject: [PATCH 07/37] Add missing PG and CH services to e2e CI config --- .github/workflows/elixir.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index d16514840080..4bb228c9445a 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -119,6 +119,29 @@ jobs: timeout-minutes: 5 env: MIX_ENV: e2e_test + services: + postgres: + image: "postgres:18" + ports: + - 5432:5432 + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + clickhouse: + image: clickhouse/clickhouse-server:25.11.5.8-alpine + ports: + - 8123:8123 + env: + CLICKHOUSE_SKIP_USER_SETUP: 1 + options: >- + --health-cmd nc -zw3 localhost 8124 + --health-interval 10s + --health-timeout 5s + --health-retries 5 strategy: fail-fast: false matrix: From 8a841018fe10963592dc848e7e2dbf2caad84e19 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:22:59 +0100 Subject: [PATCH 08/37] Increase timeouts for e2e CI tasks --- .github/workflows/elixir.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 4bb228c9445a..a72bf3d7a228 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -116,7 +116,7 @@ jobs: e2e: name: End-to-end tests runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 15 env: MIX_ENV: e2e_test services: @@ -226,7 +226,7 @@ jobs: merge-sharded-e2e-test-report: if: ${{ !cancelled() }} needs: [e2e] - timeout-minutes: 5 + timeout-minutes: 15 runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 From 39dbd5747eecdcea15dd1e4d0852f5bff9748a9f Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:29:43 +0100 Subject: [PATCH 09/37] Temporarily remove tzdata updater --- .github/workflows/elixir.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index a72bf3d7a228..2d8d43e1878c 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -199,7 +199,6 @@ jobs: - run: mix deps.get --only $MIX_ENV - run: mix compile --warnings-as-errors --all-warnings - run: mix do ecto.create, ecto.migrate - - run: mix run -e "Tzdata.ReleaseUpdater.poll_for_update" - name: Install E2E dependencies run: npm --prefix ./e2e ci From f93949149dc97e2107a7ff7ba18630f7da5c04bf Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:40:40 +0100 Subject: [PATCH 10/37] Revert "Temporarily remove tzdata updater" This reverts commit 610547301f3ed39aa6e9ef21767bfb868dd89893. --- .github/workflows/elixir.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 2d8d43e1878c..a72bf3d7a228 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -199,6 +199,7 @@ jobs: - run: mix deps.get --only $MIX_ENV - run: mix compile --warnings-as-errors --all-warnings - run: mix do ecto.create, ecto.migrate + - run: mix run -e "Tzdata.ReleaseUpdater.poll_for_update" - name: Install E2E dependencies run: npm --prefix ./e2e ci From d49a2692559b48af6ede9c1e6cf22fa783d635ff Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 14:59:24 +0100 Subject: [PATCH 11/37] Add step downloading geo data --- .github/workflows/elixir.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index a72bf3d7a228..d9b1eebd13b9 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -199,6 +199,7 @@ jobs: - run: mix deps.get --only $MIX_ENV - run: mix compile --warnings-as-errors --all-warnings - run: mix do ecto.create, ecto.migrate + - run: mix download_country_database - run: mix run -e "Tzdata.ReleaseUpdater.poll_for_update" - name: Install E2E dependencies From 27b57c2f25b9684ad60285059024e5357cbb1858 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 17:20:01 +0100 Subject: [PATCH 12/37] Setup E2E seeds before running CI --- .github/workflows/elixir.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index d9b1eebd13b9..58d45a6d3959 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -202,6 +202,9 @@ jobs: - run: mix download_country_database - run: mix run -e "Tzdata.ReleaseUpdater.poll_for_update" + - name: Setup E2E seeds + run: mix run priv/repo/e2e_seeds.exs + - name: Install E2E dependencies run: npm --prefix ./e2e ci From 22875122b2aae36f8c7f2c90f29557fee2976541 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 17:24:28 +0100 Subject: [PATCH 13/37] Run e2e tests via mix task to ensure setup env variables --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 58d45a6d3959..ac65ff160c1d 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -217,7 +217,7 @@ jobs: run: npx playwright install - name: Run E2E Playwright tests - run: npm --prefix ./e2e test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob + run: mix cmd npm --prefix ./e2e test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob - name: Upload E2E blob report to GitHub Actions Artifacts if: ${{ !cancelled() }} From c151a7b0d852649ca840d0f02529ff16252468b2 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 17:55:50 +0100 Subject: [PATCH 14/37] Set `BASE_URL` env var explicitly in CI config instead --- .github/workflows/elixir.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index ac65ff160c1d..cc2684dcd421 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -119,6 +119,7 @@ jobs: timeout-minutes: 15 env: MIX_ENV: e2e_test + BASE_URL: "http://localhost:8000" services: postgres: image: "postgres:18" @@ -217,7 +218,7 @@ jobs: run: npx playwright install - name: Run E2E Playwright tests - run: mix cmd npm --prefix ./e2e test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob + run: npm --prefix ./e2e test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob - name: Upload E2E blob report to GitHub Actions Artifacts if: ${{ !cancelled() }} From a44d103943f2155b0d61fe48c40a6c61e42274e2 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Tue, 3 Feb 2026 18:15:38 +0100 Subject: [PATCH 15/37] Reduce shards to 1 --- .github/workflows/elixir.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index cc2684dcd421..8fb6fd9d77f7 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -146,8 +146,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4] - shardTotal: [4] + shardIndex: [1] + shardTotal: [1] steps: - uses: actions/checkout@v6 with: From 9ed4f207314e18b730dbeb4aa95f925eca9cfc9f Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 10:56:15 +0100 Subject: [PATCH 16/37] Change how cwd is set for playwright server --- e2e/playwright.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 7d551edb6653..26d0ec46663f 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -72,7 +72,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'cd .. && mix phx.server', + cwd: '..', + command: 'mix phx.server', url: process.env.BASE_URL, reuseExistingServer: !process.env.CI, }, From d8af9b87c98bb483bcac3ddde4508a7d41fdc354 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 10:56:31 +0100 Subject: [PATCH 17/37] Show server log output in e2e tests --- config/.env.e2e_test | 2 +- e2e/playwright.config.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/config/.env.e2e_test b/config/.env.e2e_test index 052f02eab8ae..998187f39f88 100644 --- a/config/.env.e2e_test +++ b/config/.env.e2e_test @@ -7,7 +7,7 @@ SECRET_KEY_BASE=/njrhntbycvastyvtk1zycwfm981vpo/0xrvwjjvemdakc/vsvbrevlwsc6u8rcg TOTP_VAULT_KEY=Q3BD4nddbkVJIPXgHuo5NthGKSIH0yesRfG05J88HIo= ENVIRONMENT=dev MAILER_ADAPTER=Bamboo.LocalAdapter -LOG_LEVEL=warning +LOG_LEVEL=info SELFHOST=false DISABLE_CRON=true ADMIN_USER_IDS=1 diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 26d0ec46663f..b195f9bd1ed9 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -76,5 +76,6 @@ export default defineConfig({ command: 'mix phx.server', url: process.env.BASE_URL, reuseExistingServer: !process.env.CI, + stdout: 'pipe', }, }); From bd620a1dac43e9849318a6b13f48fa5f8697e4f2 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 11:08:28 +0100 Subject: [PATCH 18/37] Install asset dependencies during e2e setup --- .github/workflows/elixir.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 8fb6fd9d77f7..8efef9c61943 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -199,6 +199,7 @@ jobs: - run: mix deps.get --only $MIX_ENV - run: mix compile --warnings-as-errors --all-warnings + - run: npm install --prefix ./assets - run: mix do ecto.create, ecto.migrate - run: mix download_country_database - run: mix run -e "Tzdata.ReleaseUpdater.poll_for_update" From 296e68c63fc525c0691a50fbba2242a3e61fa99e Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 11:32:04 +0100 Subject: [PATCH 19/37] Build assets as well --- .github/workflows/elixir.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 8efef9c61943..0952913a7e6c 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -200,6 +200,7 @@ jobs: - run: mix deps.get --only $MIX_ENV - run: mix compile --warnings-as-errors --all-warnings - run: npm install --prefix ./assets + - run: mix assets.deploy - run: mix do ecto.create, ecto.migrate - run: mix download_country_database - run: mix run -e "Tzdata.ReleaseUpdater.poll_for_update" From 6810e6cdb393d2f89567c27df79c59cc3633cda3 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 11:46:27 +0100 Subject: [PATCH 20/37] Hide server output in e2e tests again --- config/.env.e2e_test | 2 +- e2e/playwright.config.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/config/.env.e2e_test b/config/.env.e2e_test index 998187f39f88..052f02eab8ae 100644 --- a/config/.env.e2e_test +++ b/config/.env.e2e_test @@ -7,7 +7,7 @@ SECRET_KEY_BASE=/njrhntbycvastyvtk1zycwfm981vpo/0xrvwjjvemdakc/vsvbrevlwsc6u8rcg TOTP_VAULT_KEY=Q3BD4nddbkVJIPXgHuo5NthGKSIH0yesRfG05J88HIo= ENVIRONMENT=dev MAILER_ADAPTER=Bamboo.LocalAdapter -LOG_LEVEL=info +LOG_LEVEL=warning SELFHOST=false DISABLE_CRON=true ADMIN_USER_IDS=1 diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index b195f9bd1ed9..26d0ec46663f 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -76,6 +76,5 @@ export default defineConfig({ command: 'mix phx.server', url: process.env.BASE_URL, reuseExistingServer: !process.env.CI, - stdout: 'pipe', }, }); From f5b03ea3197ae889f32e2b293bfb09805636b03d Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 11:46:41 +0100 Subject: [PATCH 21/37] Parallelize e2e tests again --- .github/workflows/elixir.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 0952913a7e6c..c97a21a318a2 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -146,8 +146,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1] - shardTotal: [1] + shardIndex: [1, 2, 3, 4] + shardTotal: [4] steps: - uses: actions/checkout@v6 with: From e20b9f8042de528c714be926e969e309a58e6098 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:00:24 +0100 Subject: [PATCH 22/37] Reduce test sharding from 4 to 2 --- .github/workflows/elixir.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index c97a21a318a2..96be239514dc 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -146,8 +146,8 @@ jobs: strategy: fail-fast: false matrix: - shardIndex: [1, 2, 3, 4] - shardTotal: [4] + shardIndex: [1, 2] + shardTotal: [2] steps: - uses: actions/checkout@v6 with: From 5c47bdfbd9504a5903379dcb0f96bf4ff798885a Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:00:49 +0100 Subject: [PATCH 23/37] Cache more fetched and compiled assets --- .github/workflows/elixir.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 96be239514dc..428be99e0dc0 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -166,6 +166,7 @@ jobs: path: | deps _build + assets/node_modules tracker/node_modules priv/tracker/js priv/tracker/installation_support @@ -175,6 +176,18 @@ jobs: e2e-${{ env.MIX_ENV }}-${{ env.CACHE_VERSION }}-${{ github.head_ref || github.ref }}- e2e-${{ env.MIX_ENV }}-${{ env.CACHE_VERSION }}-refs/heads/master- + - name: Cache E2E dependencies and Playwright browsers + uses: actions/cache@v5 + id: playwright-cache + with: + path: | + e2e/node_modules + ~/.cache/ms-playwright + ~/.cache/ms-playwright-github + key: playwright-${{ runner.os }}-${{ hashFiles('e2e/package-lock.json') }} + restore-keys: | + playwright-${{ runner.os }}- + - name: Check for changes in tracker/** uses: dorny/paths-filter@v3 id: changes From f50e31d9d8291460954de1de1b2ae74f2f2749b2 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:10:19 +0100 Subject: [PATCH 24/37] Remove redundant tests --- e2e/tests/example10.spec.ts | 147 ------------------ e2e/tests/example2.spec.ts | 147 ------------------ e2e/tests/example3.spec.ts | 147 ------------------ e2e/tests/example4.spec.ts | 147 ------------------ e2e/tests/example5.spec.ts | 147 ------------------ e2e/tests/example6.spec.ts | 147 ------------------ e2e/tests/example7.spec.ts | 147 ------------------ e2e/tests/example8.spec.ts | 147 ------------------ e2e/tests/example9.spec.ts | 147 ------------------ .../{example.spec.ts => general.spec.ts} | 0 10 files changed, 1323 deletions(-) delete mode 100644 e2e/tests/example10.spec.ts delete mode 100644 e2e/tests/example2.spec.ts delete mode 100644 e2e/tests/example3.spec.ts delete mode 100644 e2e/tests/example4.spec.ts delete mode 100644 e2e/tests/example5.spec.ts delete mode 100644 e2e/tests/example6.spec.ts delete mode 100644 e2e/tests/example7.spec.ts delete mode 100644 e2e/tests/example8.spec.ts delete mode 100644 e2e/tests/example9.spec.ts rename e2e/tests/{example.spec.ts => general.spec.ts} (100%) diff --git a/e2e/tests/example10.spec.ts b/e2e/tests/example10.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example10.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example2.spec.ts b/e2e/tests/example2.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example2.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example3.spec.ts b/e2e/tests/example3.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example3.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example4.spec.ts b/e2e/tests/example4.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example4.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example5.spec.ts b/e2e/tests/example5.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example5.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example6.spec.ts b/e2e/tests/example6.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example6.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example7.spec.ts b/e2e/tests/example7.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example7.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example8.spec.ts b/e2e/tests/example8.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example8.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example9.spec.ts b/e2e/tests/example9.spec.ts deleted file mode 100644 index fac75819d60f..000000000000 --- a/e2e/tests/example9.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { test, expect } from "@playwright/test"; - -const baseUrl = process.env.BASE_URL; - -test("dashboard renders", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page).toHaveTitle(/Plausible/); - - await expect( - page.getByRole("button", { name: "public.example.com" }), - ).toBeVisible(); -}); - -test("filter is applied", async ({ page }) => { - await page.goto("/public.example.com"); - - await expect(page.getByRole("link", { name: "Page" })).toBeHidden(); - - await page.getByRole("button", { name: "Filter" }).click(); - - await expect(page.getByRole("link", { name: "Page" })).toHaveCount(1); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await expect( - page.getByRole("heading", { name: "Filter by Page" }), - ).toBeVisible(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await page.getByPlaceholder("Select a Page").click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: true }), - ).toHaveCount(1); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeVisible(); - - await page.getByPlaceholder("Select a Page").fill("pag"); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page1" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page2" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/page3" }), - ).toBeVisible(); - - await expect( - page.getByRole("listitem").filter({ hasText: "/other" }), - ).toBeHidden(); - - await page.getByRole("listitem").filter({ hasText: "/page1" }).click(); - - await expect( - page.getByRole("button", { name: "Apply filter", disabled: false }), - ).toHaveCount(1); - - await page.getByRole("button", { name: "Apply filter" }).click(); - - await expect(page).toHaveURL( - baseUrl + "/public.example.com?f=is,page,/page1", - ); - - await expect( - page.getByRole("link", { name: "Page is /page1" }), - ).toHaveAttribute("title", "Edit filter: Page is /page1"); -}); - -test("tab selection user preferences are preserved across reloads", async ({ - page, -}) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Entry pages" }).click(); - - await page.goto("/public.example.com"); - - let currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("entry-pages"); - - await page.getByRole("button", { name: "Exit pages" }).click(); - - await page.goto("/public.example.com"); - - currentTab = await page.evaluate(() => - localStorage.getItem("pageTab__public.example.com"), - ); - - await expect(currentTab).toEqual("exit-pages"); -}); - -test("back navigation closes the modal", async ({ page }) => { - await page.goto("/public.example.com"); - - await page.getByRole("button", { name: "Filter" }).click(); - - await page.getByRole("link", { name: "Page" }).click(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com/filter/page"); - - await page.goBack(); - - await expect(page).toHaveURL(baseUrl + "/public.example.com"); -}); - -test("opens for logged in user", async ({ page }) => { - await page.goto("/login"); - - await page.getByLabel("Email").fill("user@plausible.test"); - - await page.getByLabel("Password").fill("plausible"); - - await page.getByRole("button", { name: "Log in" }).click(); - - await page.goto("/private.example.com"); - - await expect( - page.getByRole("button", { name: "private.example.com" }), - ).toBeVisible(); -}); diff --git a/e2e/tests/example.spec.ts b/e2e/tests/general.spec.ts similarity index 100% rename from e2e/tests/example.spec.ts rename to e2e/tests/general.spec.ts From dc2f594d6e7593afeca3189501009a012e164959 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:24:43 +0100 Subject: [PATCH 25/37] Clean up playwright config slightly --- e2e/playwright.config.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 26d0ec46663f..0cada02b4edc 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -1,13 +1,5 @@ import { defineConfig, devices } from '@playwright/test'; -/** - * Read environment variables from file. - * https://github.com/motdotla/dotenv - */ -// import dotenv from 'dotenv'; -// import path from 'path'; -// dotenv.config({ path: path.resolve(__dirname, '.env') }); - /** * See https://playwright.dev/docs/test-configuration. */ From 22df560703575f281fb58b9e7ec6b4a9c62c6779 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:24:57 +0100 Subject: [PATCH 26/37] Try reducing the time spent fetching system deps --- .github/workflows/elixir.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 428be99e0dc0..5c63c18c3031 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -224,13 +224,10 @@ jobs: - name: Install E2E dependencies run: npm --prefix ./e2e ci - - name: Install E2E Playwright system dependencies - working-directory: ./e2e - run: npx playwright install-deps - - name: Install E2E Playwright Browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' working-directory: ./e2e - run: npx playwright install + run: npx playwright install --with-deps=chromium - name: Run E2E Playwright tests run: npm --prefix ./e2e test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob From ba56896964621c9f576d3a0023e2ae20146a3993 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:47:16 +0100 Subject: [PATCH 27/37] Output screenshots on failure --- e2e/playwright.config.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 0cada02b4edc..80f2ce27488a 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -22,8 +22,12 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', + + screenshot: 'only-on-failure', }, + outputDir: './output', + /* Configure projects for major browsers */ projects: [ { From 499b01bb0f111615ea972172b7f18ce31c31f680 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:47:31 +0100 Subject: [PATCH 28/37] Upload screenshots from failed tests to GH artifacts --- .github/workflows/elixir.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 5c63c18c3031..1041664ae76d 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -240,6 +240,14 @@ jobs: path: e2e/blob-report retention-days: 1 + - name: Upload E2E screenshots of failed tests + if: failure() + uses: actions/upload-artifact@v6 + with: + name: e2e-screenshots-${{ matrix.shardIndex }} + path: e2e/output + retention-days: 1 + merge-sharded-e2e-test-report: if: ${{ !cancelled() }} needs: [e2e] From 3372a820cf9ef1041f32df493e3f1e51b0dd96ca Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:47:53 +0100 Subject: [PATCH 29/37] Make one test fail on purpose --- e2e/tests/general.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/general.spec.ts b/e2e/tests/general.spec.ts index fac75819d60f..3c3bab7760c4 100644 --- a/e2e/tests/general.spec.ts +++ b/e2e/tests/general.spec.ts @@ -8,7 +8,7 @@ test("dashboard renders", async ({ page }) => { await expect(page).toHaveTitle(/Plausible/); await expect( - page.getByRole("button", { name: "public.example.com" }), + page.getByRole("button", { name: "public.exampleaaa.com" }), ).toBeVisible(); }); From d316b35152fdcc65e3caa658d1d62e537d0c2306 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:53:18 +0100 Subject: [PATCH 30/37] Revert "Make one test fail on purpose" This reverts commit 3372a820cf9ef1041f32df493e3f1e51b0dd96ca. --- e2e/tests/general.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/tests/general.spec.ts b/e2e/tests/general.spec.ts index 3c3bab7760c4..fac75819d60f 100644 --- a/e2e/tests/general.spec.ts +++ b/e2e/tests/general.spec.ts @@ -8,7 +8,7 @@ test("dashboard renders", async ({ page }) => { await expect(page).toHaveTitle(/Plausible/); await expect( - page.getByRole("button", { name: "public.exampleaaa.com" }), + page.getByRole("button", { name: "public.example.com" }), ).toBeVisible(); }); From f48a271a9753fdd48f8d73363aa09e65673473f5 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 12:57:45 +0100 Subject: [PATCH 31/37] Update gitignore inside e2e to ignore outputDir --- e2e/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/.gitignore b/e2e/.gitignore index 335bd46df270..99a51372c2ed 100644 --- a/e2e/.gitignore +++ b/e2e/.gitignore @@ -4,5 +4,6 @@ node_modules/ /test-results/ /playwright-report/ /blob-report/ +/output/ /playwright/.cache/ /playwright/.auth/ From 5e8e851cae9dd82274863a724edbec71ad817160 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 13:01:49 +0100 Subject: [PATCH 32/37] Add notes about how to run e2e tests locally --- mix.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mix.exs b/mix.exs index 61ea2b5e7608..72c68b93ee23 100644 --- a/mix.exs +++ b/mix.exs @@ -168,6 +168,7 @@ defmodule Plausible.MixProject do "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test", "clean_clickhouse"], + # Run with `MIX_ENV=e2e_test mix test.e2e` "test.e2e": [ "esbuild default", "ecto.create --quiet", @@ -179,6 +180,8 @@ defmodule Plausible.MixProject do "clean_postgres", "clean_clickhouse" ], + # Runs tests in interactive mode + # Run with `MIX_ENV=e2e_test mix test.e2e.ui` "test.e2e.ui": [ "esbuild default", "ecto.create --quiet", From 9b5306c25b45e948d9e1a423769f1c23e4e50174 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 13:12:03 +0100 Subject: [PATCH 33/37] Add preferred envs for E2E mix tasks --- mix.exs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mix.exs b/mix.exs index 72c68b93ee23..e2f2d6495f45 100644 --- a/mix.exs +++ b/mix.exs @@ -49,6 +49,15 @@ defmodule Plausible.MixProject do ] end + def cli do + [ + preferred_envs: [ + "test.e2e": :e2e_test, + "test.e2e.ui": :e2e_test + ] + ] + end + # Specifies which paths to compile per environment. defp elixirc_paths(env) when env in [:test, :e2e_test, :dev], do: ["lib", "test/support", "extra/lib"] From 2b5a4f4fbee8f3c1c30ed85e02d93cccceb8e80b Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 15:23:46 +0100 Subject: [PATCH 34/37] Don't dump screenshots and don't upload them as GH artifacts --- .github/workflows/elixir.yml | 8 -------- e2e/playwright.config.ts | 4 ---- 2 files changed, 12 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 1041664ae76d..5c63c18c3031 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -240,14 +240,6 @@ jobs: path: e2e/blob-report retention-days: 1 - - name: Upload E2E screenshots of failed tests - if: failure() - uses: actions/upload-artifact@v6 - with: - name: e2e-screenshots-${{ matrix.shardIndex }} - path: e2e/output - retention-days: 1 - merge-sharded-e2e-test-report: if: ${{ !cancelled() }} needs: [e2e] diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index 80f2ce27488a..0cada02b4edc 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -22,12 +22,8 @@ export default defineConfig({ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', - - screenshot: 'only-on-failure', }, - outputDir: './output', - /* Configure projects for major browsers */ projects: [ { From 0564e8a1d6dd6dfdaecc2310799a08333fc9d517 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 15:28:44 +0100 Subject: [PATCH 35/37] Rely on cached `tracker/node_modules` and not install tracker conditionally --- .github/workflows/elixir.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 5c63c18c3031..5fda68df5210 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -196,20 +196,8 @@ jobs: tracker: - 'tracker/**' - - name: Check if tracker and verifier are built already - run: | - if [ -f priv/tracker/js/plausible-web.js ] && [ -f priv/tracker/installation_support/verifier.js ]; then - echo "HAS_BUILT_TRACKER=true" >> $GITHUB_ENV - else - echo "HAS_BUILT_TRACKER=false" >> $GITHUB_ENV - fi - - run: npm install --prefix ./tracker - if: steps.changes.outputs.tracker == 'true' || env.HAS_BUILT_TRACKER == 'false' - - run: npm run deploy --prefix ./tracker - if: steps.changes.outputs.tracker == 'true' || env.HAS_BUILT_TRACKER == 'false' - - run: mix deps.get --only $MIX_ENV - run: mix compile --warnings-as-errors --all-warnings - run: npm install --prefix ./assets From 0eedd338a4b37630d079f7eaea6d8b3440452d6b Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 15:36:26 +0100 Subject: [PATCH 36/37] Remove no longer relevant comments from mix.exs --- mix.exs | 3 --- 1 file changed, 3 deletions(-) diff --git a/mix.exs b/mix.exs index e2f2d6495f45..a68bb8e0058d 100644 --- a/mix.exs +++ b/mix.exs @@ -177,7 +177,6 @@ defmodule Plausible.MixProject do "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test", "clean_clickhouse"], - # Run with `MIX_ENV=e2e_test mix test.e2e` "test.e2e": [ "esbuild default", "ecto.create --quiet", @@ -189,8 +188,6 @@ defmodule Plausible.MixProject do "clean_postgres", "clean_clickhouse" ], - # Runs tests in interactive mode - # Run with `MIX_ENV=e2e_test mix test.e2e.ui` "test.e2e.ui": [ "esbuild default", "ecto.create --quiet", From 17dca4193308497a07f581c1a6b4caf0acb0a499 Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Thu, 5 Feb 2026 15:50:45 +0100 Subject: [PATCH 37/37] Fix invalid playwright install option that surfaced with pruned cache --- .github/workflows/elixir.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/elixir.yml b/.github/workflows/elixir.yml index 5fda68df5210..90b3ad2448fc 100644 --- a/.github/workflows/elixir.yml +++ b/.github/workflows/elixir.yml @@ -215,7 +215,7 @@ jobs: - name: Install E2E Playwright Browsers if: steps.playwright-cache.outputs.cache-hit != 'true' working-directory: ./e2e - run: npx playwright install --with-deps=chromium + run: npx playwright install --with-deps chromium - name: Run E2E Playwright tests run: npm --prefix ./e2e test -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob