diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..5ded09e4b --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,87 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master, v2-going-full-stack] + +jobs: + E2E: + timeout-minutes: 60 + runs-on: ubuntu-latest + env: + NEXT_PUBLIC_BASE_URL: http://localhost:3000 + NEXT_PUBLIC_DEV_ENV_NAME: tests + DATABASE_URL: postgres://startui:startui@localhost:5432/startui + NEXT_PUBLIC_IS_DEMO: false + AUTH_SECRET: Replace me with `openssl rand -base64 32` generated secret + EMAIL_SERVER: smtp://username:password@localhost:1025 + EMAIL_FROM: Start UI + services: + postgres: + image: postgres + + env: + POSTGRES_PASSWORD: startui + POSTGRES_USER: startui + POSTGRES_DB: startui + + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: pnpm/action-setup@v2 + name: Install pnpm + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Cache node modules + uses: actions/cache@v2 + env: + cache-name: cache-node-modules + with: + path: ${{ env.STORE_PATH }} + key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store-${{ env.cache-name }}- + ${{ runner.os }}-pnpm-store- + ${{ runner.os }}- + + - name: Install dependencies + run: pnpm install + + - name: Install Playwright Browsers + run: pnpm playwright install --with-deps + + - name: Migrate database + run: pnpm db:push + + - name: Add default data into database + run: pnpm db:seed + + - name: Run Playwright tests + run: pnpm e2e + + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 84ff50817..0d940d5d4 100644 --- a/.gitignore +++ b/.gitignore @@ -43,9 +43,6 @@ yarn-error.log* # vercel .vercel -# cypress -/cypress/videos - # Build info .build-info.json openapi.json @@ -56,3 +53,8 @@ prisma/dev.db-journal # Emails .react-email + +# playwright +/test-results/ +/playwright-report/ +/playwright/.cache/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index cd780656d..921a8c10e 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,6 +2,8 @@ "recommendations": [ "lokalise.i18n-ally", "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" + "esbenp.prettier-vscode", + "ms-playwright.playwright", + "Prisma.prisma" ] } diff --git a/README.md b/README.md index 4103d03f4..034369b7f 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,17 @@ You can update the storage key used to detect the color mode by updating this co export const COLOR_MODE_STORAGE_KEY = 'start-ui-color-mode'; // Update the key according to your needs ``` +### E2E Tests + +E2E tests are setup with Playwright. + +```sh +pnpm e2e # Run tests in headless mode, this is the command executed in CI +pnpm e2e:ui # Open a UI which allow you to run specific tests and see test execution +``` + +Tests are written in the `e2e` folder; there is also a `e2e/utils` folder which contains some utils to help writing tests. + ## Show hint on development environments Setup the `NEXT_PUBLIC_DEV_ENV_NAME` env variable with the name of the environment. diff --git a/e2e/login.spec.ts b/e2e/login.spec.ts new file mode 100644 index 000000000..4d85ad03e --- /dev/null +++ b/e2e/login.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from '@playwright/test'; +import { getUtils } from 'e2e/utils'; + +test.describe('Authentication flow', () => { + test('Login as admin', async ({ page }) => { + const utils = getUtils(page); + + await utils.loginAsAdmin(); + + await expect( + page.getByRole('heading', { name: 'Dashboard', level: 2 }) + ).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Admin' })).toBeVisible(); + }); + + test('Login as user', async ({ page }) => { + const utils = getUtils(page); + + await utils.loginAsUser(); + + await expect( + page.getByRole('heading', { name: 'Dashboard', level: 2 }) + ).toBeVisible(); + + await expect(page.getByRole('link', { name: 'Admin' })).not.toBeVisible(); + }); +}); diff --git a/e2e/utils/index.ts b/e2e/utils/index.ts new file mode 100644 index 000000000..e867409c1 --- /dev/null +++ b/e2e/utils/index.ts @@ -0,0 +1,45 @@ +import { Page } from '@playwright/test'; + +/** + * Utilities constructor + * + * @example + * ```ts + * test.describe('My scope', () => { + * test('My test', async ({ page }) => { + * const utils = getUtils(page); + * + * // No need too pass page on each util + * await utils.login(...) + * }) + * }) + * ``` + */ +export const getUtils = (page: Page) => { + return { + /** + * Utility used to authenticate the user on the app + */ + async login(userDetails: { email: string; password: string }) { + await page.goto('/login'); + await page.waitForURL('**/login'); + + await page.getByLabel('Email').fill(userDetails.email); + await page.getByLabel(/^Password/).fill(userDetails.password); + + await page.getByRole('button', { name: 'Log In' }).click(); + }, + async loginAsAdmin() { + return this.login({ + email: 'admin@admin.com', + password: 'admin', + }); + }, + async loginAsUser() { + return this.login({ + email: 'user@user.com', + password: 'user', + }); + }, + } as const; +}; diff --git a/package.json b/package.json index 9ef9916ce..bd37b5315 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "storybook:build": "pnpm build:info && storybook build && mv ./storybook-static ./public/storybook", "theme:generate-typing": "chakra-cli tokens ./src/theme/theme.ts", "theme:generate-icons": "svgr --config-file src/components/Icons/svgr.config.js src/components/Icons/svg-sources", + "e2e": "playwright test", + "e2e:ui": "playwright test --ui", "db:init": "docker run --name startui -e POSTGRES_PASSWORD=startui -e POSTGRES_USER=startui -e POSTGRES_DB=startui -p 5432:5432 -d postgres && sleep 10 && pnpm db:push && pnpm db:seed", "db:start": "docker start startui", "db:stop": "docker stop startui", @@ -87,6 +89,7 @@ "@babel/parser": "7.22.16", "@chakra-ui/cli": "2.4.1", "@next/eslint-plugin-next": "13.5.2", + "@playwright/test": "1.38.1", "@storybook/addon-actions": "7.4.5", "@storybook/addon-essentials": "7.4.5", "@storybook/addon-links": "7.4.5", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..795d0c159 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,49 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* 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: process.env.CI ? 'github' : 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + baseURL: 'http://localhost: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'] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'yarn dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e63fc38b..6208cdfc6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,6 +160,9 @@ devDependencies: '@next/eslint-plugin-next': specifier: 13.5.2 version: 13.5.2 + '@playwright/test': + specifier: 1.38.1 + version: 1.38.1 '@storybook/addon-actions': specifier: 7.4.5 version: 7.4.5(@types/react-dom@18.2.7)(@types/react@18.2.22)(react-dom@18.2.0)(react@18.2.0) @@ -3771,6 +3774,14 @@ packages: dev: true optional: true + /@playwright/test@1.38.1: + resolution: {integrity: sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.38.1 + dev: true + /@pmmmwh/react-refresh-webpack-plugin@0.5.11(react-refresh@0.11.0)(webpack@5.88.2): resolution: {integrity: sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==} engines: {node: '>= 10.13'} @@ -10047,6 +10058,14 @@ 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] + requiresBuild: true + dev: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -12692,6 +12711,22 @@ packages: find-up: 6.3.0 dev: true + /playwright-core@1.38.1: + resolution: {integrity: sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==} + engines: {node: '>=16'} + hasBin: true + dev: true + + /playwright@1.38.1: + resolution: {integrity: sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.38.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /pnp-webpack-plugin@1.7.0(typescript@5.1.3): resolution: {integrity: sha512-2Rb3vm+EXble/sMXNSu6eoBx8e79gKqhNq9F5ZWW6ERNCTE/Q0wQNne5541tE5vKjfM8hpNCYL+LGc1YTfI0dg==} engines: {node: '>=6'}