diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 0b75758..d005cc7 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,31 +1,31 @@ /** @type { import("eslint").Linter.Config } */ module.exports = { - root: true, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:svelte/recommended', - 'prettier' - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - parserOptions: { - sourceType: 'module', - ecmaVersion: 2020, - extraFileExtensions: ['.svelte'] - }, - env: { - browser: true, - es2017: true, - node: true - }, - overrides: [ - { - files: ['*.svelte'], - parser: 'svelte-eslint-parser', - parserOptions: { - parser: '@typescript-eslint/parser' - } - } - ] -}; + root: true, + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:svelte/recommended", + "prettier", + ], + parser: "@typescript-eslint/parser", + plugins: ["@typescript-eslint"], + parserOptions: { + sourceType: "module", + ecmaVersion: 2020, + extraFileExtensions: [".svelte"], + }, + env: { + browser: true, + es2017: true, + node: true, + }, + overrides: [ + { + files: ["*.svelte"], + parser: "svelte-eslint-parser", + parserOptions: { + parser: "@typescript-eslint/parser", + }, + }, + ], +} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..8cf7224 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,51 @@ +name: E2E Tests + +env: + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + POSTMARK_API_TOKEN: ${{ secrets.POSTMARK_API_TOKEN }} + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + playwright-tests: + runs-on: ubuntu-latest + env: + POSTMARK_API_TOKEN: ${{ secrets.POSTMARK_API_TOKEN }} + PUBLIC_UUID_NAMESPACE: ${{ secrets.PUBLIC_UUID_NAMESPACE }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + SHOW_PRIVATE_DATA_PARAM: ${{ secrets.SHOW_PRIVATE_DATA_PARAM }} + EMAIL: ${{ secrets.EMAIL }} + MOBILE_NUMBER: ${{ secrets.MOBILE_NUMBER }} + ADDRESS: ${{ secrets.ADDRESS }} + + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + run: bun install + + - name: Install Playwright Browsers + run: bunx playwright install --with-deps + + - name: Run Playwright tests + run: bun test:e2e + + - name: Install Wrangler CLI + if: always() + run: npm install -g wrangler + + - name: Publish Playwright Report to Cloudflare Pages + if: always() + run: wrangler pages deploy ./playwright-report --project-name=dm-portfolio-e2e-status diff --git a/.gitignore b/.gitignore index 6635cf5..fdfd6a8 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ node_modules !.env.example vite.config.js.timestamp-* vite.config.ts.timestamp-* +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/.prettierrc b/.prettierrc index 0d325a3..ddbf73f 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,7 +1,7 @@ { - "semi": false, - "trailingComma": "es5", - "printWidth": 100, - "plugins": ["prettier-plugin-svelte"], - "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] + "semi": false, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } diff --git a/__tests__/contact.spec.ts b/__tests__/contact.spec.ts new file mode 100644 index 0000000..ee59ecb --- /dev/null +++ b/__tests__/contact.spec.ts @@ -0,0 +1,41 @@ +import { test, expect } from "@playwright/test" + +test("allows a user to submit a contact form", async ({ page }) => { + await page.goto("/") + + await page.getByRole("link", { name: "Contact" }).click() + + await page.getByRole("textbox", { name: "Name", exact: true }).click() + await page.getByRole("textbox", { name: "Name", exact: true }).fill("Joe Bloggs") + await page.getByLabel("Email").click() + await page.getByLabel("Email").fill("joe@bloggs.example") + await page.getByLabel("Message").click() + await page.getByLabel("Message").fill("Ah hey man, love your site.") + await page.getByRole("button", { name: "Send message" }).click() + + await expect(page.getByText("Thank you for your message.")).toBeVisible() + await expect(page.getByTestId("honeypot")).not.toBeVisible() +}) + +test("pretends the form is submitted when a honeypot field is filled", async ({ page }) => { + await page.goto("/") + + await page.getByRole("link", { name: "Contact" }).click() + + await page.getByRole("textbox", { name: "Name", exact: true }).click() + await page.getByRole("textbox", { name: "Name", exact: true }).fill("Joe Bloggs") + await page.getByLabel("Email").click() + await page.getByLabel("Email").fill("joe@bloggs.example") + await page.getByLabel("Message").click() + await page + .getByLabel("Message") + .fill("Ah hey man, love your site. I'm definitely not a bot ... heh") + await page.getByLabel("First name").fill("I'm a bot") + await page.getByRole("button", { name: "Send message" }).click() + + // Pretend message is sent + await expect(page.getByText("Thank you for your message.")).toBeVisible() + + // Honeypot testid is visible + await expect(page.getByTestId("honeypot")).toBeVisible() +}) diff --git a/bun.lockb b/bun.lockb index 330c3e8..c22bdaa 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 665f68b..eff4796 100644 --- a/package.json +++ b/package.json @@ -3,37 +3,43 @@ "version": "0.0.1", "private": true, "scripts": { - "dev": "vite dev", + "dev": "vite dev --port ${VITE_PORT:-5173}", "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", "lint": "prettier --check . && eslint .", - "format": "prettier --write ." + "format": "prettier --write .", + "test:e2e": "playwright test" }, "devDependencies": { - "@sveltejs/adapter-vercel": "5.3.1", - "@sveltejs/kit": "2.5.10", + "@playwright/test": "1.45.1", + "@sveltejs/adapter-cloudflare": "4.6.1", + "@sveltejs/kit": "2.5.18", "@sveltejs/vite-plugin-svelte": "3.1.1", - "@tailwindcss/vite": "4.0.0-alpha.15", + "@tailwindcss/vite": "4.0.0-alpha.17", "@types/eslint": "8.56.10", - "@typescript-eslint/eslint-plugin": "7.11.0", - "@typescript-eslint/parser": "7.11.0", + "@types/node": "^20.14.10", + "@types/uuid": "10.0.0", + "@typescript-eslint/eslint-plugin": "7.16.0", + "@typescript-eslint/parser": "7.16.0", "clsx": "2.1.1", + "dotenv": "16.4.5", "eslint": "8.56.0", "eslint-config-prettier": "9.1.0", - "eslint-plugin-svelte": "2.39.0", + "eslint-plugin-svelte": "2.42.0", "locomotive-scroll": "5.0.0-beta.12", - "postmark": "4.0.2", - "prettier": "3.2.5", - "prettier-plugin-svelte": "3.2.3", - "svelte": "5.0.0-next.144", - "svelte-check": "3.8.0", - "tailwind-merge": "2.3.0", + "postmark": "4.0.4", + "prettier": "3.3.3", + "prettier-plugin-svelte": "3.2.5", + "svelte": "5.0.0-next.182", + "svelte-check": "3.8.4", + "tailwind-merge": "2.4.0", "tailwindcss": "4.0.0-alpha.15", - "tslib": "2.6.2", - "typescript": "5.4.5", - "vite": "5.2.12" + "tslib": "2.6.3", + "typescript": "5.5.3", + "uuid": "10.0.0", + "vite": "5.3.3" }, "type": "module" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..f947f5e --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,46 @@ +import dotenv from "dotenv" +import { defineConfig, devices } from "@playwright/test" + +dotenv.config({ path: ".env.test" }) + +const port = 7777 + +export default defineConfig({ + testDir: "./__tests__", + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [["html", { outputDir: "./playwright-report" }]], + use: { trace: "on" }, + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "firefox", + use: { + ...devices["Desktop Firefox"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + { + name: "webkit", + use: { + ...devices["Desktop Safari"], + contextOptions: { reducedMotion: "reduce" }, + }, + }, + ], + webServer: { + command: process.env.CI + ? `bun run build && bunx vite preview --port ${port}` + : `VITE_PORT=${port} bun dev`, + port, + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/src/app.css b/src/app.css index e2cf1aa..36e56c1 100644 --- a/src/app.css +++ b/src/app.css @@ -81,6 +81,15 @@ opacity: 100% !important; } } + + @media (prefers-reduced-motion) { + * { + animation-duration: 0ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0ms !important; + animation-delay: 0ms !important; + } + } } @layer components { diff --git a/src/components/contact.svelte b/src/components/contact.svelte index 479b7e9..ca21e42 100644 --- a/src/components/contact.svelte +++ b/src/components/contact.svelte @@ -1,14 +1,14 @@ {#snippet input({ label, type = "text" })} - {@const id = label.toLowerCase()} - + {@const id = label_id(label)} {@const value = $page.form?.[id] || ""}