From 5f5941455c85cd62296025cffe48fa735603f254 Mon Sep 17 00:00:00 2001 From: Simon Elsbrock Date: Sun, 27 Oct 2024 22:54:14 +0100 Subject: [PATCH] feat: playwright e2e testing --- .github/workflows/playwright.yml | 30 ++++++++ .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, 268 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..9e1fb32e --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,30 @@ +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: Fetch a recent database dump + run: | + curl -o static/sb.duckdb.wasm https://radar.iodev.org/sb.duckdb.wasm + - 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..ddc29371 --- /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(); + await expect(page.getByText('Volume', { exact: true })).toBeVisible(); + await expect(page.getByText('Price (€)', { exact: true })).toBeDefined(); + await expect(page.getByLabel('Clear Filter')).not.toBeVisible(); + await page.getByLabel('Save Filter').click(); + await expect(page.getByLabel('Clear Filter')).toBeDefined(); + await page.getByLabel('Clear Filter').click(); + await expect(page.getByLabel('Clear Filter')).not.toBeVisible(); +}); \ No newline at end of file diff --git a/tests/landing.spec.ts b/tests/landing.spec.ts new file mode 100644 index 00000000..9b171fa6 --- /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('/'); + await expect(page.getByRole('heading', { name: 'Find the Best Deals on' })).toBeVisible(); + await expect(page.getByText('Server Radar monitors Hetzner')).toBeVisible(); +}); \ No newline at end of file