From 3a1de8c9c1bab0069c14816052bca1bdc8533260 Mon Sep 17 00:00:00 2001 From: Abel Castro <26894614+abel-castro@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:04:53 +0200 Subject: [PATCH] Add e2e tests with playwright (#2) * Minor fixes * Implement e2e tests with using playwright * Use pnpm in github actions file * Use npm again * Fix playwright baseURL * Bypass vercel auth for CI e2e tests * Fix e2e actions file * Fix playwright config file * Fix wrong base url in actions file * Remove package-lock * Set retries to 3 * Add docker image to github action * Remove firefox tests as it seems to have some issues --- .github/workflows/playwright.yml | 23 +++++++++ .gitignore | 4 ++ README.md | 22 +++++++++ app/components/posts/post-pagination.tsx | 13 +++-- app/components/posts/post-search.tsx | 7 ++- app/components/posts/post-single.tsx | 2 +- app/layout.tsx | 4 +- app/lib/definitions.ts | 2 +- app/page.tsx | 2 +- e2e-tests/home.spec.ts | 52 ++++++++++++++++++++ next.config.mjs | 1 - package.json | 1 + playwright.config.ts | 59 +++++++++++++++++++++++ pnpm-lock.yaml | 61 +++++++++++++++++++----- tsconfig.json | 7 ++- 15 files changed, 235 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/playwright.yml create mode 100644 e2e-tests/home.spec.ts create mode 100644 playwright.config.ts diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..caae509 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,23 @@ +name: Playwright Tests +on: + deployment_status: +jobs: + e2e-tests: + if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' + timeout-minutes: 60 + runs-on: ubuntu-latest + container: + image: mcr.microsoft.com/playwright:v1.46.0-jammy + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20.x + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + env: + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.environment_url }} + run: pnpm exec playwright test diff --git a/.gitignore b/.gitignore index a84107a..48b872d 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,7 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/README.md b/README.md index 68b595e..6f34fb2 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,11 @@ https://tailwindcss.com/docs/background-color https://nextjs.org/docs/app/building-your-application/optimizing/metadata +### Run playwright on Gitlab + +https://playwright.dev/docs/ci-intro#on-deployment +https://cushionapp.com/journal/how-to-use-playwright-with-github-actions-for-e2e-testing-of-vercel-preview + ## Getting Started Provide somehow an Rest-API that returns blog posts as defined in `definitions.ts`. @@ -38,6 +43,7 @@ Provide somehow an Rest-API that returns blog posts as defined in `definitions.t Create a `.env` file based on the `.env.template` Install dependencies: + ```bash pnpm i ``` @@ -49,3 +55,19 @@ pnpm dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +## Tests + +This project provides e2e test with playwright. + +Headless run: + +```sh +pnpm exec playwright test +``` + +Run with UI: + +```sh +pnpm exec playwright test --ui +``` diff --git a/app/components/posts/post-pagination.tsx b/app/components/posts/post-pagination.tsx index e580f13..e541036 100644 --- a/app/components/posts/post-pagination.tsx +++ b/app/components/posts/post-pagination.tsx @@ -1,6 +1,6 @@ "use client"; -import { generatePagination } from "@/app/lib/pagination"; +import { generatePagination } from "../../lib/pagination"; import { ArrowLeftIcon, ArrowRightIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; import Link from "next/link"; @@ -20,7 +20,7 @@ export default function Pagination({ totalPages }: { totalPages: number }) { return ( <> -
+
); + const testIdValue = + direction === "left" ? "pagination-back" : "pagination-forward"; + return isDisabled ? ( -
{icon}
+
+ {icon} +
) : ( - + {icon} ); diff --git a/app/components/posts/post-search.tsx b/app/components/posts/post-search.tsx index 9f4f0a9..68e4dc9 100644 --- a/app/components/posts/post-search.tsx +++ b/app/components/posts/post-search.tsx @@ -22,7 +22,10 @@ export default function Search({ placeholder }: { placeholder: string }) { }, 300); return ( -
+
@@ -35,6 +38,6 @@ export default function Search({ placeholder }: { placeholder: string }) { defaultValue={searchParams.get("query")?.toString()} /> -
+ ); } diff --git a/app/components/posts/post-single.tsx b/app/components/posts/post-single.tsx index 4ec6ef0..866a035 100644 --- a/app/components/posts/post-single.tsx +++ b/app/components/posts/post-single.tsx @@ -27,7 +27,7 @@ export default async function PostSingle({ hasLink?: boolean; }) { return ( -
+
diff --git a/app/layout.tsx b/app/layout.tsx index 807cada..8c0d857 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -10,8 +10,8 @@ const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: { - template: "%s | AbelCastro.Dev", - default: "AbelCastro.Dev", + template: "%s | abelcastro.dev", + default: "abelcastro.dev", }, description: "My personal blog where I (and LLMs 🤖) write about coding-related topics.", diff --git a/app/lib/definitions.ts b/app/lib/definitions.ts index 30cdf3a..c86b4fa 100644 --- a/app/lib/definitions.ts +++ b/app/lib/definitions.ts @@ -13,7 +13,7 @@ export type Post = { export type PostsAPIResponse = { count: number; - next: string; + next: string | null; previous: string | null; results: Post[]; }; diff --git a/app/page.tsx b/app/page.tsx index c586c13..374b281 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -32,7 +32,7 @@ export default async function Home({ <>
-
+
}> diff --git a/e2e-tests/home.spec.ts b/e2e-tests/home.spec.ts new file mode 100644 index 0000000..b3eca11 --- /dev/null +++ b/e2e-tests/home.spec.ts @@ -0,0 +1,52 @@ +import { test, expect } from "@playwright/test"; + +test("main elements are visible", async ({ page }) => { + await page.goto("/"); + + await expect(page).toHaveTitle(/abelcastro.dev/); + await expect( + page.getByRole("heading", { name: "abelcastro.dev" }) + ).toBeVisible(); + + // The page should have 3 posts + const posts = page.locator("#post-container > .post-element"); + await expect(posts).toHaveCount(3); + + // Pagination + await expect(page.getByTestId("pagination")).toBeVisible(); + + // Back button pagination + const paginationBack = page.getByTestId("pagination-back"); + await expect(paginationBack).toBeVisible(); + // We are loading the first page, so the back button should be disabled/not clickable + await expect( + await paginationBack.evaluate((e) => + (e as HTMLInputElement).hasAttribute("href") + ) + ).toBeFalsy(); + + // Forward button pagination + const paginationForward = page.getByTestId("pagination-forward"); + await expect(paginationForward).toBeVisible(); + + // The forward button should be clickable as we are on the first page + await expect( + await paginationForward.evaluate((e) => + (e as HTMLInputElement).hasAttribute("href") + ) + ).toBeTruthy(); + + // Search form + await expect(page.getByTestId("search-form")).toBeVisible(); +}); + +test("search works", async ({ page }) => { + await page.goto("/"); + + await expect(page.getByTestId("search-form")).toBeVisible(); + await page.getByTestId("search-form").locator("input").fill("My first post"); + + // The page should have 1 posts + const posts = page.locator("#post-container > .post-element"); + await expect(posts).toHaveCount(1); +}); diff --git a/next.config.mjs b/next.config.mjs index 0833241..b26ad24 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -13,7 +13,6 @@ const mdxOptions = { const nextConfig = { pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"], - transpilePackages: ["next-mdx-remote"], }; export default withMDX(mdxOptions)(nextConfig); diff --git a/package.json b/package.json index 0732dfe..cd4ab49 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "use-debounce": "^10.0.2" }, "devDependencies": { + "@playwright/test": "^1.45.3", "@types/mapbox__rehype-prism": "^0.8.3", "@types/node": "^20.14.13", "@types/react": "^18.3.3", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..d083e92 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,59 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./e2e-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 ? 3 : 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: { + baseURL: process.env.PLAYWRIGHT_TEST_BASE_URL ?? "http://localhost:3000", + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + // https://vercel.com/docs/security/deployment-protection/methods-to-bypass-deployment-protection/protection-bypass-automation + extraHTTPHeaders: { + "x-vercel-protection-bypass": process.env + .VERCEL_AUTOMATION_BYPASS_SECRET as string, + }, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, + + /* Test against mobile viewports. */ + { + name: "Mobile Chrome", + use: { ...devices["Pixel 5"] }, + }, + { + name: "Mobile Safari", + use: { ...devices["iPhone 12"] }, + }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d733c42..b766362 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,10 +31,10 @@ importers: version: 2.0.13 '@vercel/analytics': specifier: ^1.3.1 - version: 1.3.1(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.3.1(next@14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) '@vercel/speed-insights': specifier: ^1.0.12 - version: 1.0.12(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + version: 1.0.12(next@14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -49,7 +49,7 @@ importers: version: 0.13.0 next: specifier: 14.2.3 - version: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-mdx-remote: specifier: ^5.0.0 version: 5.0.0(@types/react@18.3.3)(react@18.3.1) @@ -93,6 +93,9 @@ importers: specifier: ^10.0.2 version: 10.0.2(react@18.3.1) devDependencies: + '@playwright/test': + specifier: ^1.45.3 + version: 1.45.3 '@types/mapbox__rehype-prism': specifier: ^0.8.3 version: 0.8.3 @@ -308,6 +311,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.45.3': + resolution: {integrity: sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==} + engines: {node: '>=18'} + hasBin: true + '@rushstack/eslint-patch@1.10.4': resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} @@ -1171,6 +1179,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1982,6 +1995,16 @@ packages: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} + playwright-core@1.45.3: + resolution: {integrity: sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.45.3: + resolution: {integrity: sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -2865,6 +2888,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.45.3': + dependencies: + playwright: 1.45.3 + '@rushstack/eslint-patch@1.10.4': {} '@swc/counter@0.1.3': {} @@ -3004,16 +3031,16 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vercel/analytics@1.3.1(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/analytics@1.3.1(next@14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: server-only: 0.0.1 optionalDependencies: - next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 - '@vercel/speed-insights@1.0.12(next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + '@vercel/speed-insights@1.0.12(next@14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': optionalDependencies: - next: 14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next: 14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 '@webassemblyjs/ast@1.12.1': @@ -3639,7 +3666,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -3663,7 +3690,7 @@ snapshots: enhanced-resolve: 5.17.1 eslint: 8.57.0 eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 is-core-module: 2.15.0 @@ -3685,7 +3712,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -3923,6 +3950,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -4886,7 +4916,7 @@ snapshots: - '@types/react' - supports-color - next@14.2.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@14.2.3(@playwright/test@1.45.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 14.2.3 '@swc/helpers': 0.5.5 @@ -4907,6 +4937,7 @@ snapshots: '@next/swc-win32-arm64-msvc': 14.2.3 '@next/swc-win32-ia32-msvc': 14.2.3 '@next/swc-win32-x64-msvc': 14.2.3 + '@playwright/test': 1.45.3 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -5050,6 +5081,14 @@ snapshots: pirates@4.0.6: {} + playwright-core@1.45.3: {} + + playwright@1.45.3: + dependencies: + playwright-core: 1.45.3 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.40): diff --git a/tsconfig.json b/tsconfig.json index c87d50b..c3d47e0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,9 +22,13 @@ "name": "next" } ], + "baseUrl": "./", "paths": { "@/*": [ "./*" + ], + "@/components/*": [ + "components/*" ] } }, @@ -32,8 +36,7 @@ "next-env.d.ts", "**/*.ts", "**/*.tsx", - ".next/types/**/*.ts", - "app/components/posts/post-single.tsx" + ".next/types/**/*.ts" ], "exclude": [ "node_modules"