Skip to content

Commit

Permalink
feat: use playwright instead of testing library and msw (#11)
Browse files Browse the repository at this point in the history
* feat: use playwright instead of testing library and msw

* test: complete all features

* test(todo-detail): complete

* test(todo): change baseUrl to localhost

* ci: give up test on CI, just test on local
  • Loading branch information
rifandani authored Mar 6, 2024
1 parent 7befeba commit a647116
Show file tree
Hide file tree
Showing 77 changed files with 1,040 additions and 2,602 deletions.
103 changes: 61 additions & 42 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ on:
push:
branches:
- main

pull_request:
branches:
- main

jobs:
lint-format:
lint-and-format:
runs-on: ubuntu-latest
steps:
- name: Checkout branch
Expand Down Expand Up @@ -39,45 +38,65 @@ jobs:
node-version: lts/*
cache: pnpm # Package manager should be pre-installed

- name: Setup globally
run: npm i -g @antfu/ni

- name: Install
run: nci
- name: Clean install
run: pnpm i --frozen-lockfile

- name: Typecheck
run: nr typecheck

test:
runs-on: ${{ matrix.os }}

strategy:
matrix:
node: [lts/*]
os: [ubuntu-latest] # adding "windows-latest" and "macos-latest" will only slows down CI/CD
fail-fast: false

steps:
- name: Checkout branch
uses: actions/[email protected]

- name: Install pnpm
uses: pnpm/action-setup@v3

- name: Set node version to ${{ matrix.node }}
uses: actions/[email protected]
with:
node-version: ${{ matrix.node }}
cache: pnpm

- name: Setup
run: npm i -g @antfu/ni

- name: Install
run: nci

- name: Build
run: nr build

- name: Test
run: nr test
run: pnpm typecheck

# e2e:
# timeout-minutes: 30
# runs-on: ubuntu-latest
# steps:
# - name: Checkout branch
# uses: actions/[email protected]

# - name: Install pnpm
# uses: pnpm/action-setup@v3

# - name: Install node
# uses: actions/[email protected]
# with:
# node-version: 18

# - name: Clean install
# run: pnpm i --frozen-lockfile

# - name: Install playwright browsers
# # since we are installing dependencies beforehand, we can `exec` the binaries directly, instead of `dlx`
# run: pnpm test:install

# - name: Run playwright tests
# run: pnpm test

# - name: Save playwright report as artifact
# uses: actions/[email protected]
# if: always()
# with:
# name: playwright-report
# path: playwright-report/
# retention-days: 30

# e2e:
# runs-on: ubuntu-latest
# container:
# image: mcr.microsoft.com/playwright:v1.42.0-focal
# steps:
# - name: Checkout branch
# uses: actions/[email protected]

# - name: Install pnpm
# uses: pnpm/action-setup@v3

# - name: Install node
# uses: actions/[email protected]
# with:
# node-version: lts/*

# - name: Clean install
# run: pnpm i --frozen-lockfile

# - name: Run playwright tests
# run: pnpm test
# env:
# HOME: /root
11 changes: 7 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
# testing
/test-results/
/playwright-test-results/
/playwright-report/
/playwright/
/blob-report/
/playwright/.cache/
/coverage
/html
stats.html

# MSW browser mocking init file
public/mockServiceWorker.js

# misc
.env.*
!.env.*.example
Expand Down Expand Up @@ -38,4 +41,4 @@ yarn-error.log*
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.yarn/install-state.gz
.yarn/install-state.gz
32 changes: 16 additions & 16 deletions docs/application-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@ The application built with:

- `vite` + `typescript` -> development productivity
- `biome` -> fast linter, formatter
- `vitest` + `@testing-library/react` -> unit test, integration test, coverage
- `msw` -> API response mocking for tests
- `@playwright/test` -> e2e test
- `tailwindcss` + `tailwindcss-animate` + `tailwind-merge` + `daisyui` -> easy styling
- `@formkit/auto-animate` -> automate transition animation when component mount/unmount
- `axios` + `@tanstack/react-query` -> server state management + data fetching
- `zod` -> runtime schema validation
- `@iconify/react` -> SVG icon on demand
- `react-aria` + `react-aria-components` + `react-stately` -> adaptive, accessible and robust unstyled UI components like radix-ui
- `react-aria` + `react-aria-components` + `react-stately` -> adaptive, accessible and robust unstyled UI components
- `react-hook-form` -> form management
- `zustand` -> performant global state
- `zustand` -> performant global state management
- `react-toastify` -> toast outside of react components
- `type-fest` -> collection of useful type helpers
- `@rifandani/nxact-yutiriti` -> collection of useful utils
- `@internationalized/date` -> collection of useful date utils
- `type-fest` -> type helpers
- `@rifandani/nxact-yutiriti` -> object/array/string utils
- `@internationalized/date` -> date utils
- `vite-plugin-pwa` + `@vite-pwa/assets-generator` + `@rollup/plugin-replace` + `https-localhost` + `workbox-core` + `workbox-precaching` + `workbox-routing` + `workbox-window` -> Progressive Web App (PWA)

[Demo App](https://react-app-rifandani.vercel.app)
Expand Down Expand Up @@ -55,14 +54,15 @@ $ pnpm dev

## Testing

We are using MSW v2 which utilize Node v18+. Make sure you install Node v18+, because it has a built-in fetch.

```bash
# run test
# run test headless
$ pnpm test

# coverage with instanbul
$ pnpm test:coverage
# run test in UI mode
$ pnpm test:ui

# open the test report
$ pnpm test:report
```

## Build
Expand All @@ -77,16 +77,16 @@ $ pnpm build

## Start

PWA relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/.
Please refer to it's docs for the steps to setup your local environment.
PWA relies on [https-localhost](https://github.com/daquinoaldo/https-localhost) to serve the dist files on https://localhost/. Please refer to it's docs for the steps to setup your local environment.

```bash
pnpm start
# build app in "production" mode & start a server
$ pnpm start
```

Open up https://localhost/, then restart the server, you will see a notification ask you to restart reload the offline content.

## Deployment

For now only supports deployment to Vercel.
Check out `vercel.json` file fo further details.
Check out `vercel.json` file for further details.
7 changes: 7 additions & 0 deletions docs/linting-and-formatting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Linting and Formatting

## Biome

Biome is a all-in-one javascript tooling. It's built on Rust, so it's really fast. This repo actually implements eslint and prettier before migrating it to biome, and I saw a significant upgrade to the speed.

In vscode, make sure you disable your ESLint and Prettier extensions, and install/enable Biome extension instead. You can override the linting and formatting rules in `biome.json` file in the root folder.
19 changes: 19 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Testing

## Playwright

We use Playwright for our End-to-End tests in this project. You'll find those in
the `e2e` directory. As you make changes, add to an existing file or create a
new file in the `e2e` directory to test your changes.

To run these tests in development, run `pnpm run test` which will start
the dev server for the app and run Playwright on it.

We have a setup test to automate authentication/login flow in `auth.setup.ts` file by default. If you want to opt out of it, you can specify it in the test file, like so:

```ts
test.describe('unauthorized', () => {
// reset storage state in a test file to avoid authentication that was set up for the whole project
test.use({ storageState: { cookies: [], origins: [] } });
});
```
4 changes: 0 additions & 4 deletions docs/todo.md

This file was deleted.

10 changes: 10 additions & 0 deletions e2e/.auth/user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": 15,
"username": "kminchelle",
"email": "[email protected]",
"firstName": "Jeanne",
"lastName": "Halvorson",
"gender": "female",
"image": "https://robohash.org/Jeanne.png?set=set4",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTUsInVzZXJuYW1lIjoia21pbmNoZWxsZSIsImVtYWlsIjoia21pbmNoZWxsZUBxcS5jb20iLCJmaXJzdE5hbWUiOiJKZWFubmUiLCJsYXN0TmFtZSI6IkhhbHZvcnNvbiIsImdlbmRlciI6ImZlbWFsZSIsImltYWdlIjoiaHR0cHM6Ly9yb2JvaGFzaC5vcmcvSmVhbm5lLnBuZz9zZXQ9c2V0NCIsImlhdCI6MTcwOTEyMDM5NywiZXhwIjoxNzA5MTIzOTk3fQ.P-ZpBhrCMltUGv5SgxXzpkVOqSeJjJDnzldUn9VMxoo"
}
31 changes: 31 additions & 0 deletions e2e/_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { LoginApiResponseSchema } from '#auth/apis/auth.api';
import { UserStoreState } from '#auth/hooks/use-user-store.hook';
import { faker } from '@faker-js/faker';

export function seedUser(): LoginApiResponseSchema {
return {
id: faker.number.int(),
username: faker.person.middleName(),
email: faker.internet.email(),
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
gender: faker.helpers.arrayElement(['male', 'female']),
image: faker.image.avatar(),
token: faker.string.uuid(),
};
}

export function getLocalStorageUser(): {
version: number;
state: UserStoreState;
} | null {
if (!localStorage) throw new Error('You are not in the browser env!');

return JSON.parse(localStorage.getItem('app-user') ?? 'null');
}

export function setLocalStorageUser(user: LoginApiResponseSchema) {
if (!localStorage) throw new Error('You are not in the browser env!');

localStorage.setItem('app-user', JSON.stringify(user));
}
21 changes: 21 additions & 0 deletions e2e/auth.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from '@playwright/test';

test('auth setup', async ({ page }) => {
// when we're not authenticated, the app redirects to the login page
await page.goto('');

const usernameInput = page.getByRole('textbox', { name: /username/i });
const passwordInput = page.getByRole('textbox', { name: /password/i });
const submitBtn = page.getByRole('button', { name: /login/i });

await usernameInput.fill('kminchelle');
await passwordInput.fill('0lelplR');
await submitBtn.click();

await page.waitForURL('');
await expect(usernameInput).not.toBeVisible();
await expect(passwordInput).not.toBeVisible();
await expect(submitBtn).not.toBeVisible();

await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
53 changes: 53 additions & 0 deletions e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { expect, test } from '@playwright/test';

test.beforeEach(async ({ page }) => {
await page.goto('/');
});

test.describe('authorized', () => {
test('should have title', async ({ page }) => {
const title = page.getByRole('heading', { level: 1 });

await expect(title).toBeVisible();
});

test('should be able to sort, toggle clock, and navigate to /todos', async ({
page,
}) => {
const clockTimer = page.getByRole('img', { name: /clock timer/i });
const sortBtn = page.getByRole('button', { name: /sort/i });
const toggleClockBtn = page.getByRole('button', { name: /clock/i });
const getStartedBtn = page.getByRole('button', { name: /start/i });

await expect(clockTimer).toBeVisible();
await expect(sortBtn).toBeVisible();
await expect(toggleClockBtn).toBeVisible();
await expect(getStartedBtn).toBeVisible();

// we don't assert sort button, because there's a possibility the position would be the same as before
await toggleClockBtn.click();
await expect(clockTimer).not.toBeVisible();
await getStartedBtn.click();

await page.waitForURL('/todos');
await expect(sortBtn).not.toBeVisible();
await expect(toggleClockBtn).not.toBeVisible();
await expect(getStartedBtn).not.toBeVisible();
});
});

test.describe('unauthorized', () => {
// reset storage state in a test file to avoid authentication that was set up for the whole project
test.use({ storageState: { cookies: [], origins: [] } });

test('should redirect back to /login', async ({ page }) => {
const usernameInput = page.getByRole('textbox', { name: /username/i });
const passwordInput = page.getByRole('textbox', { name: /password/i });
const submitBtn = page.getByRole('button', { name: /login/i });

await page.waitForURL('/login');
await expect(usernameInput).toBeVisible();
await expect(passwordInput).toBeVisible();
await expect(submitBtn).toBeVisible();
});
});
Loading

0 comments on commit a647116

Please sign in to comment.