From 2e95805ce7fedd2d513596b16c449a47b2f92fb6 Mon Sep 17 00:00:00 2001
From: Simon Elsbrock <simon@iodev.org>
Date: Sun, 27 Oct 2024 22:54:14 +0100
Subject: [PATCH] feat: playwright e2e testing

---
 .github/workflows/playwright.yml |  27 +++++++
 .gitignore                       |   4 ++
 package-lock.json                |  79 +++++++++++++++++++--
 package.json                     | 118 ++++++++++++++++---------------
 playwright.config.ts             |  80 +++++++++++++++++++++
 tests/analyze.spec.ts            |  13 ++++
 tests/landing.spec.ts            |   7 ++
 7 files changed, 265 insertions(+), 63 deletions(-)
 create mode 100644 .github/workflows/playwright.yml
 create mode 100644 playwright.config.ts
 create mode 100644 tests/analyze.spec.ts
 create mode 100644 tests/landing.spec.ts

diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
new file mode 100644
index 00000000..3eb13143
--- /dev/null
+++ b/.github/workflows/playwright.yml
@@ -0,0 +1,27 @@
+name: Playwright Tests
+on:
+  push:
+    branches: [ main, master ]
+  pull_request:
+    branches: [ main, master ]
+jobs:
+  test:
+    timeout-minutes: 60
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - uses: actions/setup-node@v4
+      with:
+        node-version: lts/*
+    - name: Install dependencies
+      run: npm ci
+    - name: Install Playwright Browsers
+      run: npx playwright install --with-deps
+    - name: Run Playwright tests
+      run: npx playwright test
+    - uses: actions/upload-artifact@v4
+      if: ${{ !cancelled() }}
+      with:
+        name: playwright-report
+        path: playwright-report/
+        retention-days: 30
diff --git a/.gitignore b/.gitignore
index 9e586abe..9c0a588e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,7 @@ node_modules
 data
 build
 *.wasm
+/test-results/
+/playwright-report/
+/blob-report/
+/playwright/.cache/
diff --git a/package-lock.json b/package-lock.json
index ead6ef91..92909dae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,12 +20,14 @@
 			},
 			"devDependencies": {
 				"@duckdb/duckdb-wasm": "^1.28.1-dev106.0",
+				"@playwright/test": "^1.48.2",
 				"@sveltejs/adapter-auto": "^3.0.0",
 				"@sveltejs/adapter-static": "^3.0.4",
 				"@sveltejs/kit": "^2.0.0",
 				"@sveltejs/vite-plugin-svelte": "^3.0.0",
 				"@types/d3": "^7.4.3",
 				"@types/eslint": "^9.0.0",
+				"@types/node": "^22.8.1",
 				"@types/svelte-range-slider-pips": "^2.0.4",
 				"autoprefixer": "^10.4.19",
 				"eslint": "^9.0.0",
@@ -865,6 +867,21 @@
 				"node": ">=14"
 			}
 		},
+		"node_modules/@playwright/test": {
+			"version": "1.48.2",
+			"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz",
+			"integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==",
+			"dev": true,
+			"dependencies": {
+				"playwright": "1.48.2"
+			},
+			"bin": {
+				"playwright": "cli.js"
+			},
+			"engines": {
+				"node": ">=18"
+			}
+		},
 		"node_modules/@polka/url": {
 			"version": "1.0.0-next.25",
 			"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
@@ -1567,13 +1584,12 @@
 			"dev": true
 		},
 		"node_modules/@types/node": {
-			"version": "20.16.11",
-			"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
-			"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
+			"version": "22.8.1",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.1.tgz",
+			"integrity": "sha512-k6Gi8Yyo8EtrNtkHXutUu2corfDf9su95VYVP10aGYMMROM6SAItZi0w1XszA6RtWTHSVp5OeFof37w0IEqCQg==",
 			"dev": true,
-			"license": "MIT",
 			"dependencies": {
-				"undici-types": "~6.19.2"
+				"undici-types": "~6.19.8"
 			}
 		},
 		"node_modules/@types/resolve": {
@@ -2049,6 +2065,15 @@
 				"arrow2csv": "bin/arrow2csv.cjs"
 			}
 		},
+		"node_modules/apache-arrow/node_modules/@types/node": {
+			"version": "20.17.1",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.1.tgz",
+			"integrity": "sha512-j2VlPv1NnwPJbaCNv69FO/1z4lId0QmGvpT41YxitRtWlg96g/j8qcv2RKsLKe2F6OJgyXhupN1Xo17b2m139Q==",
+			"dev": true,
+			"dependencies": {
+				"undici-types": "~6.19.2"
+			}
+		},
 		"node_modules/apexcharts": {
 			"version": "3.54.1",
 			"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.54.1.tgz",
@@ -3888,6 +3913,50 @@
 				"node": ">= 6"
 			}
 		},
+		"node_modules/playwright": {
+			"version": "1.48.2",
+			"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz",
+			"integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==",
+			"dev": true,
+			"dependencies": {
+				"playwright-core": "1.48.2"
+			},
+			"bin": {
+				"playwright": "cli.js"
+			},
+			"engines": {
+				"node": ">=18"
+			},
+			"optionalDependencies": {
+				"fsevents": "2.3.2"
+			}
+		},
+		"node_modules/playwright-core": {
+			"version": "1.48.2",
+			"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz",
+			"integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==",
+			"dev": true,
+			"bin": {
+				"playwright-core": "cli.js"
+			},
+			"engines": {
+				"node": ">=18"
+			}
+		},
+		"node_modules/playwright/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,
+			"optional": true,
+			"os": [
+				"darwin"
+			],
+			"engines": {
+				"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+			}
+		},
 		"node_modules/postcss": {
 			"version": "8.4.47",
 			"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
diff --git a/package.json b/package.json
index 37568bf4..5aec6244 100644
--- a/package.json
+++ b/package.json
@@ -1,60 +1,62 @@
 {
-	"name": "web",
-	"version": "0.0.1",
-	"private": true,
-	"scripts": {
-		"dev": "vite dev",
-		"build": "vite build",
-		"preview": "vite preview",
-		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
-		"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
-		"test": "vitest",
-		"lint": "prettier --check . && eslint .",
-		"format": "prettier --write ."
-	},
-	"devDependencies": {
-		"@duckdb/duckdb-wasm": "^1.28.1-dev106.0",
-		"@sveltejs/adapter-auto": "^3.0.0",
-		"@sveltejs/adapter-static": "^3.0.4",
-		"@sveltejs/kit": "^2.0.0",
-		"@sveltejs/vite-plugin-svelte": "^3.0.0",
-		"@types/d3": "^7.4.3",
-		"@types/eslint": "^9.0.0",
-		"@types/svelte-range-slider-pips": "^2.0.4",
-		"autoprefixer": "^10.4.19",
-		"eslint": "^9.0.0",
-		"eslint-config-prettier": "^9.1.0",
-		"eslint-plugin-svelte": "^2.36.0",
-		"flowbite": "^2.4.1",
-		"flowbite-svelte": "^0.47.0",
-		"flowbite-svelte-icons": "^1.6.1",
-		"globals": "^15.0.0",
-		"postcss": "^8.4.38",
-		"prettier": "^3.1.1",
-		"prettier-plugin-svelte": "^3.1.2",
-		"prettier-plugin-tailwindcss": "^0.6.4",
-		"svelte": "^4.2.7",
-		"svelte-check": "^4.0.0",
-		"svelte-echarts": "^1.0.0-rc3",
-		"svelte-range-slider-pips": "^3.0.0",
-		"tailwindcss": "^3.4.4",
-		"tslib": "^2.4.1",
-		"typescript": "^5.0.0",
-		"typescript-eslint": "^8.0.0-alpha.20",
-		"vite": "^5.0.3",
-		"vite-plugin-remove-console": "^2.2.0",
-		"vitest": "^2.0.0"
-	},
-	"type": "module",
-	"dependencies": {
-		"@fortawesome/fontawesome-svg-core": "^6.6.0",
-		"@fortawesome/free-brands-svg-icons": "^6.6.0",
-		"@fortawesome/free-solid-svg-icons": "^6.6.0",
-		"@fortawesome/svelte-fontawesome": "^0.2.2",
-		"dayjs": "^1.11.12",
-		"deepmerge": "^4.3.1",
-		"filesize": "^10.1.4",
-		"lz-string": "^1.5.0",
-		"sql-template-strings": "^2.2.2"
-	}
+  "name": "web",
+  "version": "0.0.1",
+  "private": true,
+  "scripts": {
+    "dev": "vite dev",
+    "build": "vite build",
+    "preview": "vite preview",
+    "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+    "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+    "test": "vitest",
+    "lint": "prettier --check . && eslint .",
+    "format": "prettier --write ."
+  },
+  "devDependencies": {
+    "@duckdb/duckdb-wasm": "^1.28.1-dev106.0",
+    "@playwright/test": "^1.48.2",
+    "@sveltejs/adapter-auto": "^3.0.0",
+    "@sveltejs/adapter-static": "^3.0.4",
+    "@sveltejs/kit": "^2.0.0",
+    "@sveltejs/vite-plugin-svelte": "^3.0.0",
+    "@types/d3": "^7.4.3",
+    "@types/eslint": "^9.0.0",
+    "@types/node": "^22.8.1",
+    "@types/svelte-range-slider-pips": "^2.0.4",
+    "autoprefixer": "^10.4.19",
+    "eslint": "^9.0.0",
+    "eslint-config-prettier": "^9.1.0",
+    "eslint-plugin-svelte": "^2.36.0",
+    "flowbite": "^2.4.1",
+    "flowbite-svelte": "^0.47.0",
+    "flowbite-svelte-icons": "^1.6.1",
+    "globals": "^15.0.0",
+    "postcss": "^8.4.38",
+    "prettier": "^3.1.1",
+    "prettier-plugin-svelte": "^3.1.2",
+    "prettier-plugin-tailwindcss": "^0.6.4",
+    "svelte": "^4.2.7",
+    "svelte-check": "^4.0.0",
+    "svelte-echarts": "^1.0.0-rc3",
+    "svelte-range-slider-pips": "^3.0.0",
+    "tailwindcss": "^3.4.4",
+    "tslib": "^2.4.1",
+    "typescript": "^5.0.0",
+    "typescript-eslint": "^8.0.0-alpha.20",
+    "vite": "^5.0.3",
+    "vite-plugin-remove-console": "^2.2.0",
+    "vitest": "^2.0.0"
+  },
+  "type": "module",
+  "dependencies": {
+    "@fortawesome/fontawesome-svg-core": "^6.6.0",
+    "@fortawesome/free-brands-svg-icons": "^6.6.0",
+    "@fortawesome/free-solid-svg-icons": "^6.6.0",
+    "@fortawesome/svelte-fontawesome": "^0.2.2",
+    "dayjs": "^1.11.12",
+    "deepmerge": "^4.3.1",
+    "filesize": "^10.1.4",
+    "lz-string": "^1.5.0",
+    "sql-template-strings": "^2.2.2"
+  }
 }
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 00000000..243585e5
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,80 @@
+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',
+  testMatch: /(.+\.)?(test|spec)\.[jt]s/,
+  /* 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: 'http://127.0.0.1:3000',
+
+    /* 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: 'npm run build && npm run preview',
+    port: 4173,
+    reuseExistingServer: !process.env.CI,
+  },
+});
diff --git a/tests/analyze.spec.ts b/tests/analyze.spec.ts
new file mode 100644
index 00000000..87231502
--- /dev/null
+++ b/tests/analyze.spec.ts
@@ -0,0 +1,13 @@
+import { test, expect } from '@playwright/test';
+
+test('analyze: we have data', async ({ page }) => {
+  await page.goto('/analyze');
+  await page.getByRole('cell', { name: /We have observed \d+ unique/ }).waitFor();
+  expect(page.getByText('Volume', { exact: true })).toBeVisible();
+  expect(page.getByText('Price (€)', { exact: true })).toBeDefined();
+  expect(page.getByLabel('Clear Filter')).not.toBeVisible();
+  await page.getByLabel('Save Filter').click();
+  expect(page.getByLabel('Clear Filter')).toBeDefined();
+  await page.getByLabel('Clear Filter').click();
+  await page.getByLabel('Clear Filter').waitFor({ state: 'hidden' });
+});
\ No newline at end of file
diff --git a/tests/landing.spec.ts b/tests/landing.spec.ts
new file mode 100644
index 00000000..24264cd4
--- /dev/null
+++ b/tests/landing.spec.ts
@@ -0,0 +1,7 @@
+import { expect, test } from '@playwright/test';
+
+test('landing page', async ({ page }) => {
+	await page.goto('/');
+	expect(page.getByRole('heading', { name: 'Find the Best Deals on' })).toBeVisible();
+	expect(page.getByText('Server Radar monitors Hetzner')).toBeVisible();
+});
\ No newline at end of file