diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml deleted file mode 100644 index c6103eafb..000000000 --- a/.github/workflows/cypress.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Cypress Tests -on: [deployment_status] - -jobs: - Setup: - if: github.event.deployment_status.state == 'success' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: pipx install pipenv - - uses: actions/setup-python@v5 - with: - python-version: '3.10' - # Can't use cache because of https://github.com/actions/cache/issues/319 - # cache: 'pipenv' - - name: Install bootstrapper dependencies - run: pipenv install --dev --deploy - - run: | - config_file=$(./scripts/vue_or_react.sh) - pipenv run cookiecutter . --config-file $config_file --no-input -f - cat $config_file - - uses: actions/upload-artifact@v4 - with: - name: my_project - path: my_project/ - retention-days: 1 - Chrome: - needs: Setup - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: my_project - path: my_project/ - - uses: actions/setup-node@v4 - with: - node-version: 16 - - name: Install frontend dependencies - env: - NPM_CONFIG_PRODUCTION: false - working-directory: ./my_project/client - run: npm install - - name: Run against ${{ github.event.deployment_status.environment_url }} - uses: cypress-io/github-action@v6 - with: - working-directory: my_project/client - browser: chrome - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: ${{ secrets.CYPRESS_TEST_USER_PASS }} - CYPRESS_baseUrl: ${{ github.event.deployment_status.environment_url }} - Firefox: - needs: Setup - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/checkout@v4 - - uses: actions/download-artifact@v4 - with: - name: my_project - path: my_project/ - - uses: actions/setup-node@v4 - with: - node-version: 16 - - name: Install frontend dependencies - env: - NPM_CONFIG_PRODUCTION: false - working-directory: ./my_project/client - run: npm install - - name: Run against ${{ github.event.deployment_status.environment_url }} - uses: cypress-io/github-action@v6 - with: - working-directory: my_project/client - browser: firefox - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: ${{ secrets.CYPRESS_TEST_USER_PASS }} - CYPRESS_baseUrl: ${{ github.event.deployment_status.environment_url }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 000000000..07d5a363d --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,59 @@ +name: E2E Tests +on: [deployment_status] + +jobs: + Setup: + if: github.event.deployment_status.state == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: pipx install pipenv + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + # Can't use cache because of https://github.com/actions/cache/issues/319 + # cache: 'pipenv' + - name: Install bootstrapper dependencies + run: pipenv install --dev --deploy + - run: | + config_file=$(./scripts/vue_or_react.sh) + pipenv run cookiecutter . --config-file $config_file --no-input -f + cat $config_file + - uses: actions/upload-artifact@v4 + with: + name: my_project + path: my_project/ + retention-days: 1 + Playwright: + needs: Setup + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + name: my_project + path: my_project/ + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: 📦 Install frontend dependencies + working-directory: ./my_project/client + run: npm install + - name: 🎭 Install Playwright + working-directory: ./my_project/client + run: npx playwright install --with-deps + - name: Run Playwright tests against ${{ github.event.deployment_status.environment_url }} + working-directory: ./my_project/client + run: npx playwright test --reporter=html + env: + NPM_CONFIG_PRODUCTION: false + PLAYWRIGHT_TEST_BASE_URL: ${{ github.event.deployment_status.environment_url }} + CYPRESS_TEST_USER_PASS: ${{ secrets.CYPRESS_TEST_USER_PASS }} + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: my_project/client/playwright-report/ + retention-days: 30 diff --git a/hooks/post_gen_project.py b/hooks/post_gen_project.py index ab6d66362..125d4242a 100644 --- a/hooks/post_gen_project.py +++ b/hooks/post_gen_project.py @@ -118,15 +118,12 @@ def set_keys_in_envs(django_secret, postgres_secret): cookie_cutter_settings_path = join("app.json") postgres_init_file = join("scripts/init-db.sh") set_flag(env_file_path, "!!!DJANGO_SECRET_KEY!!!", django_secret) + set_flag(env_file_path, "!!!PLAYWRIGHT_SECRET_KEY!!!", django_secret) set_flag(pull_request_template_path, "!!!DJANGO_SECRET_KEY!!!", django_secret) set_flag(cookie_cutter_settings_path, "!!!DJANGO_SECRET_KEY!!!", django_secret) set_flag(env_file_path, "!!!POSTGRES_PASSWORD!!!", postgres_secret) set_flag(postgres_init_file, "!!!POSTGRES_PASSWORD!!!", postgres_secret) copy2(env_file_path, join(".env")) - cypress_example_file_dir = join(web_clients_path, "react") - cypress_example_file = join(cypress_example_file_dir, "cypress.example.env.json") - set_flag(cypress_example_file, "!!!POSTGRES_PASSWORD!!!", postgres_secret) - copy2(cypress_example_file, join(cypress_example_file_dir, "cypress.env.json")) def get_secrets(): diff --git a/{{cookiecutter.project_slug}}/.github/pull_request_template.md b/{{cookiecutter.project_slug}}/.github/pull_request_template.md index 1b49d4c79..53606cdae 100644 --- a/{{cookiecutter.project_slug}}/.github/pull_request_template.md +++ b/{{cookiecutter.project_slug}}/.github/pull_request_template.md @@ -22,4 +22,4 @@ Add user steps to achieve desired functionality for this feature. | user | password | has admin | notes | | --- | --- | --- | --- | | `admin@thinknimble.com` | !!!DJANGO_SECRET_KEY!!! | :white_check_mark: | | -| `cypress@example.com` | !!!DJANGO_SECRET_KEY!!! | :x: | Only use for automated E2E testing | +| `playwright@thinknimble.com` | !!!DJANGO_SECRET_KEY!!! | :x: | Only use for automated E2E testing | diff --git a/{{cookiecutter.project_slug}}/.github/workflows/cypress.yml b/{{cookiecutter.project_slug}}/.github/workflows/cypress.yml deleted file mode 100644 index ec927b2bb..000000000 --- a/{{cookiecutter.project_slug}}/.github/workflows/cypress.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Cypress Tests -on: [deployment_status] - -jobs: - Chrome: - if: github.event.deployment_status.state == 'success' - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/setup-node@v4 - with: - node-version: 16 - - uses: actions/checkout@v4 - - name: Run against {{ "${{ github.event.deployment_status.environment_url }}" }} - uses: cypress-io/github-action@v6 - with: - working-directory: client - browser: chrome - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: {{ "${{ secrets.CYPRESS_TEST_USER_PASS }}" }} - CYPRESS_baseUrl: {{ "${{ github.event.deployment_status.environment_url }}" }} - Firefox: - if: github.event.deployment_status.state == 'success' - runs-on: ubuntu-latest - container: - image: cypress/browsers:node16.14.2-slim-chrome103-ff102 # https://github.com/cypress-io/cypress-docker-images/tree/master/browsers - options: --user 1001 - steps: - - uses: actions/checkout@v4 - - name: Run against {{ "${{ github.event.deployment_status.environment_url }}" }} - uses: cypress-io/github-action@v6 - with: - working-directory: client - browser: firefox - env: - NPM_CONFIG_PRODUCTION: false - CYPRESS_TEST_USER_EMAIL: "cypress@example.com" - CYPRESS_TEST_USER_PASS: {{ "${{ secrets.CYPRESS_TEST_USER_PASS }}" }} - CYPRESS_baseUrl: {{ "${{ github.event.deployment_status.environment_url }}" }} diff --git a/{{cookiecutter.project_slug}}/.github/workflows/playwright.yml b/{{cookiecutter.project_slug}}/.github/workflows/playwright.yml new file mode 100644 index 000000000..d5d94976a --- /dev/null +++ b/{{cookiecutter.project_slug}}/.github/workflows/playwright.yml @@ -0,0 +1,24 @@ +name: Playwright Tests +on: + deployment_status: +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + if: github.event.deployment_status.state == 'success' + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: npm --prefix ./client i + - name: Install Playwright + run: npx --prefix ./client playwright install --with-deps + - name: Run Playwright tests + run: npx --prefix ./client playwright test --reporter=html + env: + PLAYWRIGHT_TEST_BASE_URL: + {{ "${{ github.event.deployment_status.environment_url }}" }} + CYPRESS_TEST_USER_PASS: + {{ "${{ secrets.CYPRESS_TEST_USER_PASS }}" }} diff --git a/{{cookiecutter.project_slug}}/.gitignore b/{{cookiecutter.project_slug}}/.gitignore index 614bd54fc..367110f06 100644 --- a/{{cookiecutter.project_slug}}/.gitignore +++ b/{{cookiecutter.project_slug}}/.gitignore @@ -31,11 +31,6 @@ wheels/ .installed.cfg *.egg -# Ignore Cypress environment variables & media -client/cypress.env.json -client/tests/e2e/screenshots/* -client/tests/e2e/videos/* - # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 275dbc7fc..22bf640b1 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -5,34 +5,38 @@ ## Setup ### Docker + If this is your first time... + 1. [Install Docker](https://www.docker.com/) 1. Run `pipenv lock` to generate a Pipfile.lock 1. Run `cd client && npm install` so you have node_modules available outside of Docker 1. Back in the root directory, run `make build` 1. `make run` to start the app 1. If the DB is new, run `make create-test-data` - 1. SuperUser `admin@thinknimble.com` with credentials from your `.env` - 1. User `cypress@thinknimble.com` with credentials from your `.env` is used by the Cypress - tests + 1. SuperUser `admin@thinknimble.com` with credentials from your `.env` + 1. User `playwright@thinknimble.com` with credentials from your `.env` is used by the Playwright + tests 1. View other available scripts/commands with `make commands` 1. `localhost:8080` to view the app. 1. `localhost:8000/staff/` to log into the Django admin 1. `localhost:8000/api/docs/` to view backend API endpoints available for frontend development - ### Backend + If not using Docker... See the [backend README](server/README.md) ### Frontend + If not using Docker... See the [frontend README](client/README.md) - ## Testing & Linting Locally + 1. `pipenv install --dev` 1. `pipenv run pytest server` 1. `pipenv run black server` 1. `pipenv run isort server --diff` (shows you what isort is expecting) -1. `npm run cypress` +1. `npx playwright test` +1. `npx playwright codegen localhost:8080` (generate your tests through manual testing) diff --git a/{{cookiecutter.project_slug}}/app.json b/{{cookiecutter.project_slug}}/app.json index 9a8281135..85e1b66b8 100644 --- a/{{cookiecutter.project_slug}}/app.json +++ b/{{cookiecutter.project_slug}}/app.json @@ -29,16 +29,10 @@ "generator": "secret" } }, - "addons": [ - "heroku-postgresql:standard-0", - "papertrail:choklad" - ], + "addons": ["heroku-postgresql:standard-0", "papertrail:choklad"], "environments": { "review": { - "addons": [ - "heroku-postgresql:essential-0", - "papertrail:choklad" - ] + "addons": ["heroku-postgresql:essential-0", "papertrail:choklad"] } }, "buildpacks": [ diff --git a/{{cookiecutter.project_slug}}/clients/web/react/.gitignore b/{{cookiecutter.project_slug}}/clients/web/react/.gitignore index 4d29575de..2b8d731ad 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/.gitignore +++ b/{{cookiecutter.project_slug}}/clients/web/react/.gitignore @@ -21,3 +21,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* + +# Playwright files +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/README.md b/{{cookiecutter.project_slug}}/clients/web/react/README.md index 1a82fd924..ec9d7fd75 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/README.md +++ b/{{cookiecutter.project_slug}}/clients/web/react/README.md @@ -17,7 +17,7 @@ This app includes basic configurations for developers to have a starting point o - TN Forms - Vitest - React testing library -- Cypress +- Playwright ## Getting started @@ -49,6 +49,7 @@ npm i First, create .env.local at the top-level of the client directory, and copy the contents of .env.local.example into it. Update the value of VITE_DEV_BACKEND_URL to point to your desired backend. Then run the project with: + ``` npm run serve ``` @@ -73,10 +74,16 @@ If you want to watch a single test you can specify its path as an argument to: npm run test:single path/to/test/file ``` -### Run e2e tests with Cypress +### Run e2e tests with Playwright ``` -npm run cypress +npm run test:e2e ``` -Will open cypress wizard. Make sure you run your app locally with `npm run start` and them choose the test you want to run from the wizard. +Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. + +To open last HTML report run: + +``` +npx playwright show-report +``` diff --git a/{{cookiecutter.project_slug}}/clients/web/react/cypress.config.ts b/{{cookiecutter.project_slug}}/clients/web/react/cypress.config.ts deleted file mode 100644 index 9d63d2ecb..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/cypress.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'cypress' -import pluginsFile from './tests/e2e/plugins' - -export default defineConfig({ - e2e: { - baseUrl: 'http://localhost:8080', - setupNodeEvents: pluginsFile, - supportFile: 'tests/e2e/support/e2e.js', - specPattern: 'tests/e2e/**/*.cy.{js,jsx,ts,tsx}', - }, -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/cypress.example.env.json b/{{cookiecutter.project_slug}}/clients/web/react/cypress.example.env.json deleted file mode 100644 index 939ade11d..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/cypress.example.env.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST_USER_PASS": "!!!POSTGRES_PASSWORD!!!", - "TEST_USER_EMAIL": "cypress@example.com" -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/react/package.json b/{{cookiecutter.project_slug}}/clients/web/react/package.json index 60257c716..532b4fad6 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/react/package.json @@ -5,8 +5,8 @@ "scripts": { "serve": "vite --host 0.0.0.0", "build": "vite build --base=/static/", - "cypress": "source ../.env && cypress open", "test": "vitest run", + "test:e2e": "npx playwright test --reporter=html", "test:dev": "vitest", "test:watch": "vitest run", "test:single": "vitest $0", @@ -30,10 +30,12 @@ "zustand": "^4.4.0" }, "devDependencies": { + "@playwright/test": "^1.46.0", "@tanstack/eslint-plugin-query": "5.35.6", "@testing-library/jest-dom": "^6.2.0", "@testing-library/react": "^14.1.2", "@testing-library/user-event": "^14.4.3", + "@types/node": "^22.1.0", "@types/qs": "^6.9.15", "@types/react": "^18.2.21", "@types/react-dom": "^18.2.7", @@ -42,7 +44,7 @@ "@typescript-eslint/parser": "^6.19.0", "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.15", - "cypress": "^13.5.1", + "dotenv": "^16.4.5", "eslint": "^8.48.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", diff --git a/{{cookiecutter.project_slug}}/clients/web/react/playwright.config.ts b/{{cookiecutter.project_slug}}/clients/web/react/playwright.config.ts new file mode 100644 index 000000000..39186a038 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/playwright.config.ts @@ -0,0 +1,55 @@ +import { defineConfig, devices } from '@playwright/test' +import dotenv from 'dotenv' +import path from 'path' + +dotenv.config({ path: path.resolve(__dirname, '.env') }) + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e/specs', + /* 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: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx index dd77bd035..3e8d4f53e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx +++ b/{{cookiecutter.project_slug}}/clients/web/react/src/pages/log-in.tsx @@ -51,7 +51,7 @@ function LogInInner() { return (
-
{ e.preventDefault() }} @@ -62,7 +62,7 @@ function LogInInner() { placeholder="Enter email..." onChange={(e) => createFormFieldChangeHandler(form.email)(e.target.value)} value={form.email.value ?? ''} - data-cy="email" + data-testid="email" id="id" label="Email address" /> @@ -84,7 +84,7 @@ function LogInInner() { createFormFieldChangeHandler(form.password)(e.target.value) }} value={form.password.value ?? ''} - data-cy="password" + data-testid="password" id="password" /> @@ -96,7 +96,7 @@ function LogInInner() { {errorMessage} - {errors.length + {errors.length ? errors.map((e, idx) => {e}) : null} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/.eslintrc.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/.eslintrc.js deleted file mode 100644 index a3e436bc3..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - plugins: ['cypress'], - env: { - mocha: true, - 'cypress/globals': true, - }, - rules: { - strict: 'off', - }, -} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/plugins/index.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/plugins/index.js deleted file mode 100644 index b150c40be..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/plugins/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable arrow-body-style */ -// https://docs.cypress.io/guides/guides/plugins-guide.html - -// if you need a custom webpack configuration you can uncomment the following import -// and then use the `file:preprocessor` event -// as explained in the cypress docs -// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples - -// /* eslint-disable import/no-extraneous-dependencies, global-require */ -// const webpack = require('@cypress/webpack-preprocessor') - -export default (on, config) => { - // on('file:preprocessor', webpack({ - // webpackOptions: require('@vue/cli-service/webpack.config'), - // watchOptions: {} - // })) - - return Object.assign({}, config, { - fixturesFolder: 'tests/e2e/fixtures', - screenshotsFolder: 'tests/e2e/screenshots', - videosFolder: 'tests/e2e/videos', - }) -} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/home.spec.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/home.spec.ts new file mode 100644 index 000000000..9b551d4f7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/home.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Has Welcome text', async ({ page }) => { + await expect(page.getByText('Welcome')).toBeVisible() +}) + +test('Login and signup buttons are visible', async ({ page }) => { + await expect(page.getByText('Login')).toBeVisible() + await expect(page.getByText('Signup')).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/login.spec.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/login.spec.ts new file mode 100644 index 000000000..af34bae87 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/login.spec.ts @@ -0,0 +1,14 @@ +// @ts-check +import { test, expect } from '@playwright/test' +import dotenv from 'dotenv' + +test('Login workflow', async ({ page }) => { + expect(process.env.CYPRESS_TEST_USER_PASS).toBeTruthy() + + await page.goto('/log-in') + await page.getByTestId('email').fill('playwright@thinknimble.com') + await page.getByTestId('password').fill(process.env.CYPRESS_TEST_USER_PASS ?? '') + await page.getByTestId('submit').click() + + await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/sign-up.spec.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/sign-up.spec.ts new file mode 100644 index 000000000..8c6316db7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/sign-up.spec.ts @@ -0,0 +1,23 @@ +import { test, expect } from '@playwright/test' + +const PASSWORD = 'PASSWORD' + +function generateUniqueEmail() { + const timestamp = Date.now().toString(); + return `playwright-${timestamp}@thinknimble.com`; +} + +test('Login workflow', async ({ page }) => { + const uniqueEmail = generateUniqueEmail() + + await page.goto('/sign-up') + await page.getByTestId('first-name').fill('playwright') + await page.getByTestId('last-name').fill('e2e test') + await page + .getByTestId('email') + .fill(uniqueEmail) + await page.getByTestId('password').fill(PASSWORD) + await page.getByTestId('confirm-password').fill(PASSWORD) + await page.getByTestId('submit').click() + await expect(page.getByText('Welcome to')).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts deleted file mode 100644 index dd9e8fcee..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/specs/test-login.cy.ts +++ /dev/null @@ -1,14 +0,0 @@ -describe('Tests login workflow', () => { - it('Home page has link to login', () => { - cy.visit('/') - cy.get('[data-cy=login]').click() - cy.url().should('include', '/log-in') - }), - it('Home page auto redirects to login', () => { - cy.visit('/log-in') - cy.get('[data-cy=email]').type(Cypress.env('TEST_USER_EMAIL')) - cy.get('[data-cy=password]').type(Cypress.env('TEST_USER_PASS')) - cy.get('[data-cy=submit]').click() - cy.url().should('include', '/dashboard') - }) -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/commands.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/commands.js deleted file mode 100644 index c1f5a772e..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/commands.js +++ /dev/null @@ -1,25 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This is will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/e2e.js b/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/e2e.js deleted file mode 100644 index d68db96df..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/e2e/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tests/tsconfig.json b/{{cookiecutter.project_slug}}/clients/web/react/tests/tsconfig.json deleted file mode 100644 index aa9df84f7..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/react/tests/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "compilerOptions": { - "isolatedModules": false, - "types": ["cypress", "node"] - } -} diff --git a/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json b/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json index 6b33553df..76a139e18 100644 --- a/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json +++ b/{{cookiecutter.project_slug}}/clients/web/react/tsconfig.json @@ -20,7 +20,7 @@ "noEmit": true, "jsx": "react-jsx", "baseUrl": ".", - "types": ["cypress","node"] + "types": ["node"] }, "include": [ "src", diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore b/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore index 4ec828127..291b779aa 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/.gitignore @@ -24,3 +24,7 @@ pnpm-debug.log* *.njsproj *.sln *.sw? +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/README.md b/{{cookiecutter.project_slug}}/clients/web/vue3/README.md index 6516bd004..4ce3fba4d 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/README.md +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/README.md @@ -20,7 +20,7 @@ Swap out the logo files in these locations: ## Initial Setup for non-Docker local First, create `.env.local` at the top-level of the **client** directory, and copy the contents of `.env.local.example` into it. -Un-comment the value of `VUE_APP_DEV_SERVER_BACKEND` that is appropriate for your situation. +Un-comment the value of `VITE_DEV_BACKEND_URL` that is appropriate for your situation. ``` npm install @@ -44,12 +44,19 @@ npm run build npm run test:unit ``` -### Run your end-to-end tests +### Run e2e tests with Playwright ``` npm run test:e2e ``` +Tests are run in headless mode meaning no browser will open up when running the tests. Results of the tests and test logs will be shown in the terminal. + +To open last HTML report run: + +``` +npx playwright show-report + ### Lints and fixes files ``` diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.config.js b/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.config.js deleted file mode 100644 index 9d63d2ecb..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.config.js +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from 'cypress' -import pluginsFile from './tests/e2e/plugins' - -export default defineConfig({ - e2e: { - baseUrl: 'http://localhost:8080', - setupNodeEvents: pluginsFile, - supportFile: 'tests/e2e/support/e2e.js', - specPattern: 'tests/e2e/**/*.cy.{js,jsx,ts,tsx}', - }, -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.env.json.example b/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.env.json.example deleted file mode 100644 index 3422f0062..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/cypress.env.json.example +++ /dev/null @@ -1,4 +0,0 @@ -{ - "TEST_USER_EMAIL": "cypress@example.com", - "TEST_USER_PASS": "" -} \ No newline at end of file diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json index dbc9ca376..654ef18fc 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/package.json +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/package.json @@ -9,8 +9,8 @@ "build": "vite build", "preview": "vite preview", "test": "vitest run", + "test:e2e": "npx playwright test --reporter=html", "test:dev": "vitest", - "cypress:dev": "cypress open", "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src", "format:write": "prettier --write ./src", "format:check": "prettier --check ./src", @@ -36,13 +36,15 @@ "zod": "3.21.4" }, "devDependencies": { + "@playwright/test": "^1.46.0", "@testing-library/vue": "^8.0.0", + "@types/node": "^22.2.0", "@typescript-eslint/eslint-plugin": "^6.7.0", "@typescript-eslint/parser": "^6.7.0", "@vitejs/plugin-vue": "^5.0.0", "@types/qs": "^6.9.15", "autoprefixer": "^10.4.15", - "cypress": "^13.5.1", + "dotenv": "^16.4.5", "eslint": "^8.49.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.0", diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/playwright.config.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/playwright.config.ts new file mode 100644 index 000000000..5595fef3e --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/playwright.config.ts @@ -0,0 +1,53 @@ +import { defineConfig, devices } from '@playwright/test' +import dotenv from 'dotenv' +dotenv.config({ path: '.env' }) + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests/e2e/specs', + /* 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: process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue index ae6789498..dec049020 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/components/HelloWorld.vue @@ -48,14 +48,6 @@ >unit-mocha -
  • - e2e-cypress -
  • Essential Links

      diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Login.vue b/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Login.vue index cda83b7d6..80d1adc7e 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Login.vue +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Login.vue @@ -14,7 +14,6 @@ :errors="form.email.errors" @blur="form.email.validate()" type="email" - data-cy="email" label="Email address" placeholder="Enter email..." :id="form.email.id" @@ -26,7 +25,6 @@ :errors="form.password.errors" @blur="form.password.validate()" type="password" - data-cy="password" placeholder="Enter password..." label="Password" autocomplete="current-password" @@ -56,7 +54,7 @@
      - +
      diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Signup.vue b/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Signup.vue index ac5d7a008..32ae6e112 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Signup.vue +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/src/views/Signup.vue @@ -72,13 +72,7 @@
      -
      diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/.eslintrc.js b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/.eslintrc.js deleted file mode 100644 index a3e436bc3..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/.eslintrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - plugins: ['cypress'], - env: { - mocha: true, - 'cypress/globals': true, - }, - rules: { - strict: 'off', - }, -} diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/plugins/index.js b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/plugins/index.js deleted file mode 100644 index b150c40be..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/plugins/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable arrow-body-style */ -// https://docs.cypress.io/guides/guides/plugins-guide.html - -// if you need a custom webpack configuration you can uncomment the following import -// and then use the `file:preprocessor` event -// as explained in the cypress docs -// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples - -// /* eslint-disable import/no-extraneous-dependencies, global-require */ -// const webpack = require('@cypress/webpack-preprocessor') - -export default (on, config) => { - // on('file:preprocessor', webpack({ - // webpackOptions: require('@vue/cli-service/webpack.config'), - // watchOptions: {} - // })) - - return Object.assign({}, config, { - fixturesFolder: 'tests/e2e/fixtures', - screenshotsFolder: 'tests/e2e/screenshots', - videosFolder: 'tests/e2e/videos', - }) -} diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/home.spec.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/home.spec.ts new file mode 100644 index 000000000..9b551d4f7 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/home.spec.ts @@ -0,0 +1,14 @@ +import { test, expect } from '@playwright/test' + +test.beforeEach(async ({ page }) => { + await page.goto('/') +}) + +test('Has Welcome text', async ({ page }) => { + await expect(page.getByText('Welcome')).toBeVisible() +}) + +test('Login and signup buttons are visible', async ({ page }) => { + await expect(page.getByText('Login')).toBeVisible() + await expect(page.getByText('Signup')).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/login.spec.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/login.spec.ts new file mode 100644 index 000000000..56a73d257 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/login.spec.ts @@ -0,0 +1,12 @@ +import { test, expect } from '@playwright/test' + +test('Login workflow', async ({ page }) => { + expect(process.env.CYPRESS_TEST_USER_PASS).toBeTruthy() + + await page.goto('/login') + await page.getByPlaceholder('Enter email...').fill('playwright@thinknimble.com') + await page.getByPlaceholder('Enter password...').fill(process.env.CYPRESS_TEST_USER_PASS ?? '') + await page.getByRole('button', { name: 'Log in' }).click() + + await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/sign-up.spec.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/sign-up.spec.ts new file mode 100644 index 000000000..d230bd5c6 --- /dev/null +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/sign-up.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test' + +const PASSWORD = 'PASSWORD' + +function generateUniqueEmail() { + const timestamp = Date.now().toString(); + return `playwright-${timestamp}@thinknimble.com`; +} + +test('Sign up workflow', async ({ page }) => { + const uniqueEmail = generateUniqueEmail() + + await page.goto('/signup') + await page.getByPlaceholder('Enter first name...').fill('playwright') + await page.getByPlaceholder('Enter last name...').fill('e2e test') + await page + .getByPlaceholder('Enter email...') + .fill(uniqueEmail) + await page.getByPlaceholder('Enter password...').fill(PASSWORD) + await page.getByPlaceholder('Confirm Password').fill(PASSWORD) + await page.getByRole('button', { name: 'Sign up' }).click() + + await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible() +}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-login.cy.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-login.cy.ts deleted file mode 100644 index 95dd4410c..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-login.cy.ts +++ /dev/null @@ -1,15 +0,0 @@ -describe('Tests login workflow', () => { - it('Home page has link to login', () => { - cy.visit('/') - cy.get('[data-cy=login]').click() - cy.url().should('include', '/login') - }) - - it('Filling in email and passwords goes to', () => { - cy.visit('/login') - cy.get('[data-cy=email]').type(Cypress.env('TEST_USER_EMAIL')) - cy.get('[data-cy=password]').type(Cypress.env('TEST_USER_PASS')) - cy.contains('[data-cy=submit]', 'Log in').click() - cy.url().should('include', '/dashboard') - }) -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-password-reset.cy.ts b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-password-reset.cy.ts deleted file mode 100644 index b0ae26b6f..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/specs/test-password-reset.cy.ts +++ /dev/null @@ -1,15 +0,0 @@ -describe('Tests password reset workflow', () => { - it('Login page has link to reset password', () => { - cy.visit('/login') - cy.contains('[data-cy=password-reset]', 'Forgot password?') - }) - - it('Submitting email doesnt throw error', () => { - cy.visit('/login') - cy.contains('[data-cy=password-reset]', 'Forgot password?').click() - cy.url().should('include', '/password/request-reset/') - cy.get('[data-cy=email]').type(Cypress.env('TEST_USER_EMAIL')) - cy.contains('[data-cy=submit]', 'Request Password Reset').click() - cy.get('[data-cy=submit-success]').should('contain.text', 'Your request has been submitted') - }) -}) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/commands.js b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/commands.js deleted file mode 100644 index c1f5a772e..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/commands.js +++ /dev/null @@ -1,25 +0,0 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add("login", (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This is will overwrite an existing command -- -// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/e2e.js b/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/e2e.js deleted file mode 100644 index d68db96df..000000000 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/tests/e2e/support/e2e.js +++ /dev/null @@ -1,20 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import commands.js using ES2015 syntax: -import './commands' - -// Alternatively you can use CommonJS syntax: -// require('./commands') diff --git a/{{cookiecutter.project_slug}}/clients/web/vue3/tsconfig.json b/{{cookiecutter.project_slug}}/clients/web/vue3/tsconfig.json index be6bcf6c3..69d94d90f 100644 --- a/{{cookiecutter.project_slug}}/clients/web/vue3/tsconfig.json +++ b/{{cookiecutter.project_slug}}/clients/web/vue3/tsconfig.json @@ -24,7 +24,6 @@ "noFallthroughCasesInSwitch": true, "types": [ "vitest/globals", - "cypress" ], "paths": { "@/*": [ diff --git a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/management/commands/create_test_data.py b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/management/commands/create_test_data.py index cb37c5541..60e573de0 100644 --- a/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/management/commands/create_test_data.py +++ b/{{cookiecutter.project_slug}}/server/{{cookiecutter.project_slug}}/core/management/commands/create_test_data.py @@ -13,11 +13,11 @@ class Command(BaseCommand): def handle(self, *args, **kwargs): logger.info(f"Starting management command {__name__}") superuser_password = config("DJANGO_SUPERUSER_PASSWORD") - cypress_password = config("CYPRESS_TEST_USER_PASS") + playwright_password = config("CYPRESS_TEST_USER_PASS") get_user_model().objects.create_superuser( email="admin@thinknimble.com", password=superuser_password, first_name="Admin", last_name="ThinkNimble" ) get_user_model().objects.create_user( - email="cypress@example.com", password=cypress_password, first_name="Cypress", last_name="E2E_test" + email="playwright@thinknimble.com", password=playwright_password, first_name="Playwright", last_name="E2E_test" ) logger.info(f"Finished management command {__name__}")