From 98738da565e3641aad2972dba1d02c1f0c1c8769 Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 13:05:00 +0200 Subject: [PATCH 01/10] init playwright --- pnpm-lock.yaml | 44 ++ .../.github/workflows/playwright.yml | 27 ++ projects/project-generator-e2e/.gitignore | 7 + projects/project-generator-e2e/package.json | 15 + .../playwright.config.ts | 79 ++++ .../tests-examples/demo-todo-app.spec.ts | 437 ++++++++++++++++++ .../tests/example.spec.ts | 18 + 7 files changed, 627 insertions(+) create mode 100644 projects/project-generator-e2e/.github/workflows/playwright.yml create mode 100644 projects/project-generator-e2e/.gitignore create mode 100644 projects/project-generator-e2e/package.json create mode 100644 projects/project-generator-e2e/playwright.config.ts create mode 100644 projects/project-generator-e2e/tests-examples/demo-todo-app.spec.ts create mode 100644 projects/project-generator-e2e/tests/example.spec.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c763054..0a53c19 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,6 +100,15 @@ importers: specifier: ~5.8.3 version: 5.8.3 + projects/project-generator-e2e: + devDependencies: + '@playwright/test': + specifier: ^1.54.1 + version: 1.54.1 + '@types/node': + specifier: ^24.1.0 + version: 24.1.0 + projects/web-ui: dependencies: '@apollo/client': @@ -960,6 +969,11 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@playwright/test@1.54.1': + resolution: {integrity: sha512-FS8hQ12acieG2dYSksmLOF7BNxnVf2afRJdCuM1eMSxj6QTSE6G4InGF7oApGgDb65MX7AwMVlIkpru0yZA4Xw==} + engines: {node: '>=18'} + hasBin: true + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -1951,6 +1965,11 @@ packages: resolution: {integrity: sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==} engines: {node: '>=14.14'} + 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} @@ -2628,6 +2647,16 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + playwright-core@1.54.1: + resolution: {integrity: sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.54.1: + resolution: {integrity: sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g==} + engines: {node: '>=18'} + hasBin: true + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -4213,6 +4242,10 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 + '@playwright/test@1.54.1': + dependencies: + playwright: 1.54.1 + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -5257,6 +5290,9 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -5880,6 +5916,14 @@ snapshots: picomatch@4.0.3: {} + playwright-core@1.54.1: {} + + playwright@1.54.1: + dependencies: + playwright-core: 1.54.1 + optionalDependencies: + fsevents: 2.3.2 + possible-typed-array-names@1.1.0: {} postcss@8.5.6: diff --git a/projects/project-generator-e2e/.github/workflows/playwright.yml b/projects/project-generator-e2e/.github/workflows/playwright.yml new file mode 100644 index 0000000..8116248 --- /dev/null +++ b/projects/project-generator-e2e/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - 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 + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/projects/project-generator-e2e/.gitignore b/projects/project-generator-e2e/.gitignore new file mode 100644 index 0000000..58786aa --- /dev/null +++ b/projects/project-generator-e2e/.gitignore @@ -0,0 +1,7 @@ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/projects/project-generator-e2e/package.json b/projects/project-generator-e2e/package.json new file mode 100644 index 0000000..cf32dfc --- /dev/null +++ b/projects/project-generator-e2e/package.json @@ -0,0 +1,15 @@ +{ + "name": "project-generator-e2e", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.13.1", + "devDependencies": { + "@playwright/test": "^1.54.1", + "@types/node": "^24.1.0" + } +} diff --git a/projects/project-generator-e2e/playwright.config.ts b/projects/project-generator-e2e/playwright.config.ts new file mode 100644 index 0000000..390646d --- /dev/null +++ b/projects/project-generator-e2e/playwright.config.ts @@ -0,0 +1,79 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './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 ? 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: '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'] }, + }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://localhost:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/projects/project-generator-e2e/tests-examples/demo-todo-app.spec.ts b/projects/project-generator-e2e/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000..8641cb5 --- /dev/null +++ b/projects/project-generator-e2e/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,437 @@ +import { test, expect, type Page } from '@playwright/test'; + +test.beforeEach(async ({ page }) => { + await page.goto('https://demo.playwright.dev/todomvc'); +}); + +const TODO_ITEMS = [ + 'buy some cheese', + 'feed the cat', + 'book a doctors appointment' +] as const; + +test.describe('New Todo', () => { + test('should allow me to add todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Make sure the list only has one todo item. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0] + ]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + + // Make sure the list now has two todo items. + await expect(page.getByTestId('todo-title')).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[1] + ]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test('should clear text input field when an item is added', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test('should append new items to the bottom of the list', async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + // Check test using different methods. + await expect(page.getByText('3 items left')).toBeVisible(); + await expect(todoCount).toHaveText('3 items left'); + await expect(todoCount).toContainText('3'); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe('Mark all as completed', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should allow me to mark all items as completed', async ({ page }) => { + // Complete all todos. + await page.getByLabel('Mark all as complete').check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test('should allow me to clear the complete state of all items', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); + }); + + test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { + const toggleAll = page.getByLabel('Mark all as complete'); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe('Item', () => { + + test('should allow me to mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + // Check first item. + const firstTodo = page.getByTestId('todo-item').nth(0); + await firstTodo.getByRole('checkbox').check(); + await expect(firstTodo).toHaveClass('completed'); + + // Check second item. + const secondTodo = page.getByTestId('todo-item').nth(1); + await expect(secondTodo).not.toHaveClass('completed'); + await secondTodo.getByRole('checkbox').check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).toHaveClass('completed'); + }); + + test('should allow me to un-mark items as complete', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const firstTodo = page.getByTestId('todo-item').nth(0); + const secondTodo = page.getByTestId('todo-item').nth(1); + const firstTodoCheckbox = firstTodo.getByRole('checkbox'); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass('completed'); + await expect(secondTodo).not.toHaveClass('completed'); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test('should allow me to edit an item', async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId('todo-item'); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2] + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); +}); + +test.describe('Editing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should hide other controls when editing', async ({ page }) => { + const todoItem = page.getByTestId('todo-item').nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); + await expect(todoItem.locator('label', { + hasText: TODO_ITEMS[1], + })).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test('should save edits on blur', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should trim entered text', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + 'buy some sausages', + TODO_ITEMS[2], + ]); + await checkTodosInLocalStorage(page, 'buy some sausages'); + }); + + test('should remove the item if an empty text string was entered', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); + + await expect(todoItems).toHaveText([ + TODO_ITEMS[0], + TODO_ITEMS[2], + ]); + }); + + test('should cancel edits on escape', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); + await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe('Counter', () => { + test('should display the current number of todo items', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + // create a todo count locator + const todoCount = page.getByTestId('todo-count') + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press('Enter'); + + await expect(todoCount).toContainText('1'); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press('Enter'); + await expect(todoCount).toContainText('2'); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe('Clear completed button', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test('should display the correct text', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); + }); + + test('should remove completed items when clicked', async ({ page }) => { + const todoItems = page.getByTestId('todo-item'); + await todoItems.nth(1).getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should be hidden when there are no items that are completed', async ({ page }) => { + await page.locator('.todo-list li .toggle').first().check(); + await page.getByRole('button', { name: 'Clear completed' }).click(); + await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); + }); +}); + +test.describe('Persistence', () => { + test('should persist its data', async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } + + const todoItems = page.getByTestId('todo-item'); + const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(['completed', '']); + }); +}); + +test.describe('Routing', () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test('should allow me to display active items', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test('should respect the back button', async ({ page }) => { + const todoItem = page.getByTestId('todo-item'); + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step('Showing all items', async () => { + await page.getByRole('link', { name: 'All' }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step('Showing active items', async () => { + await page.getByRole('link', { name: 'Active' }).click(); + }); + + await test.step('Showing completed items', async () => { + await page.getByRole('link', { name: 'Completed' }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test('should allow me to display completed items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Completed' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(1); + }); + + test('should allow me to display all items', async ({ page }) => { + await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole('link', { name: 'Active' }).click(); + await page.getByRole('link', { name: 'Completed' }).click(); + await page.getByRole('link', { name: 'All' }).click(); + await expect(page.getByTestId('todo-item')).toHaveCount(3); + }); + + test('should highlight the currently applied filter', async ({ page }) => { + await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); + + //create locators for active and completed links + const activeLink = page.getByRole('link', { name: 'Active' }); + const completedLink = page.getByRole('link', { name: 'Completed' }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass('selected'); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass('selected'); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder('What needs to be done?'); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press('Enter'); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction(e => { + return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction(t => { + return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); + }, title); +} diff --git a/projects/project-generator-e2e/tests/example.spec.ts b/projects/project-generator-e2e/tests/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/projects/project-generator-e2e/tests/example.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from '@playwright/test'; + +test('has title', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Playwright/); +}); + +test('get started link', async ({ page }) => { + await page.goto('https://playwright.dev/'); + + // Click the get started link. + await page.getByRole('link', { name: 'Get started' }).click(); + + // Expects page to have a heading with the name of Installation. + await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); +}); From 1ab25229528383cfd5471a41d6c099d6c2e809a6 Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 13:12:14 +0200 Subject: [PATCH 02/10] refactor tsconfig --- projects/api/src/utils/logger.ts | 0 projects/api/tsconfig.json | 2 +- projects/project-generator-e2e/tsconfig.json | 17 +++++++++++++++++ projects/project-generator/tsconfig.json | 1 + .../{tsconfig-base.json => tsconfig.base.json} | 0 projects/web-ui/tsconfig.json | 2 +- 6 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 projects/api/src/utils/logger.ts create mode 100644 projects/project-generator-e2e/tsconfig.json rename projects/{tsconfig-base.json => tsconfig.base.json} (100%) diff --git a/projects/api/src/utils/logger.ts b/projects/api/src/utils/logger.ts new file mode 100644 index 0000000..e69de29 diff --git a/projects/api/tsconfig.json b/projects/api/tsconfig.json index f54c2c4..4bd690d 100644 --- a/projects/api/tsconfig.json +++ b/projects/api/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig-base.json", + "extends": "../tsconfig.base.json", "compilerOptions": { "target": "ES2022", "lib": ["ES2022"], diff --git a/projects/project-generator-e2e/tsconfig.json b/projects/project-generator-e2e/tsconfig.json new file mode 100644 index 0000000..4cc1db9 --- /dev/null +++ b/projects/project-generator-e2e/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["ES2022"], + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["node", "@playwright/test"] + }, + "include": ["tests/**/*", "playwright.config.ts"], + "exclude": ["node_modules"] +} diff --git a/projects/project-generator/tsconfig.json b/projects/project-generator/tsconfig.json index 62e9a9d..b590eed 100644 --- a/projects/project-generator/tsconfig.json +++ b/projects/project-generator/tsconfig.json @@ -1,4 +1,5 @@ { + "extends": "../tsconfig.base.json", "compilerOptions": { "target": "ES2020", "lib": ["ES2020"], diff --git a/projects/tsconfig-base.json b/projects/tsconfig.base.json similarity index 100% rename from projects/tsconfig-base.json rename to projects/tsconfig.base.json diff --git a/projects/web-ui/tsconfig.json b/projects/web-ui/tsconfig.json index 5f41279..1bb4b0d 100644 --- a/projects/web-ui/tsconfig.json +++ b/projects/web-ui/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig-base.json", + "extends": "../tsconfig.base.json", "files": [], "references": [ { "path": "./tsconfig.app.json" }, From bc929b4e18ac095d559c53f450e318c222c7d39e Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 14:56:22 +0200 Subject: [PATCH 03/10] add e2e tests --- .../{tests => _old}/example.spec.ts | 0 .../tests-examples/demo-todo-app.spec.ts | 0 projects/project-generator-e2e/package.json | 14 ++++----- .../tests/can-add-user.test.ts | 31 +++++++++++++++++++ .../tests/has-greetings.test.ts | 13 ++++++++ .../tests/has-title.test.ts | 7 +++++ .../tests/has-users.test.ts | 19 ++++++++++++ projects/project-generator-e2e/tests/url.ts | 1 + 8 files changed, 77 insertions(+), 8 deletions(-) rename projects/project-generator-e2e/{tests => _old}/example.spec.ts (100%) rename projects/project-generator-e2e/{ => _old}/tests-examples/demo-todo-app.spec.ts (100%) create mode 100644 projects/project-generator-e2e/tests/can-add-user.test.ts create mode 100644 projects/project-generator-e2e/tests/has-greetings.test.ts create mode 100644 projects/project-generator-e2e/tests/has-title.test.ts create mode 100644 projects/project-generator-e2e/tests/has-users.test.ts create mode 100644 projects/project-generator-e2e/tests/url.ts diff --git a/projects/project-generator-e2e/tests/example.spec.ts b/projects/project-generator-e2e/_old/example.spec.ts similarity index 100% rename from projects/project-generator-e2e/tests/example.spec.ts rename to projects/project-generator-e2e/_old/example.spec.ts diff --git a/projects/project-generator-e2e/tests-examples/demo-todo-app.spec.ts b/projects/project-generator-e2e/_old/tests-examples/demo-todo-app.spec.ts similarity index 100% rename from projects/project-generator-e2e/tests-examples/demo-todo-app.spec.ts rename to projects/project-generator-e2e/_old/tests-examples/demo-todo-app.spec.ts diff --git a/projects/project-generator-e2e/package.json b/projects/project-generator-e2e/package.json index cf32dfc..e01c36d 100644 --- a/projects/project-generator-e2e/package.json +++ b/projects/project-generator-e2e/package.json @@ -1,15 +1,13 @@ { "name": "project-generator-e2e", "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": {}, - "keywords": [], - "author": "", - "license": "ISC", - "packageManager": "pnpm@10.13.1", + "scripts": { + "test": "playwright test", + "show-report": "playwright show-report" + }, "devDependencies": { "@playwright/test": "^1.54.1", "@types/node": "^24.1.0" - } + }, + "packageManager": "pnpm@10.13.1" } diff --git a/projects/project-generator-e2e/tests/can-add-user.test.ts b/projects/project-generator-e2e/tests/can-add-user.test.ts new file mode 100644 index 0000000..d92ea1a --- /dev/null +++ b/projects/project-generator-e2e/tests/can-add-user.test.ts @@ -0,0 +1,31 @@ +// data-testid="add-user-input" +// data-testid="add-user-button" + +import { expect, test } from "@playwright/test"; +import { url } from "./url"; + +test("can add user", async ({ page }) => { + await page.goto(url); + + // Check if the add user input and button are visible + const addUserInput = page.locator('input[data-testid="add-user-input"]'); + const addUserButton = page.locator('button[data-testid="add-user-button"]'); + + await expect(addUserInput).toBeVisible(); + await expect(addUserButton).toBeVisible(); + + // Add a new user + const newUserName = "TestUser"; + await addUserInput.fill(newUserName); + await addUserButton.click(); + + await page.waitForTimeout(3000); + + // Verify the new user is added to the list + const usersList = page.locator('ul[data-testid="users-list"]'); + const newUserLocator = usersList + .locator(`li:has-text("${newUserName}")`) + .first(); + + await expect(newUserLocator).toBeVisible(); +}); diff --git a/projects/project-generator-e2e/tests/has-greetings.test.ts b/projects/project-generator-e2e/tests/has-greetings.test.ts new file mode 100644 index 0000000..b8b413f --- /dev/null +++ b/projects/project-generator-e2e/tests/has-greetings.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from "@playwright/test"; +import { url } from "./url"; + +test("hello query has one of known greetings", async ({ page }) => { + const knownGreetings = ["Hello!", "Hi there!", "Welcome!", "Good day!"]; + await page.goto(url); + const helloQuery = page.locator('span[data-testid="greetings-subscription"]'); + await expect(helloQuery).toBeVisible(); + await page.waitForTimeout(3000); + const helloText = await helloQuery.textContent(); + expect(helloText).toBeDefined(); + expect(knownGreetings).toContain(helloText); +}); diff --git a/projects/project-generator-e2e/tests/has-title.test.ts b/projects/project-generator-e2e/tests/has-title.test.ts new file mode 100644 index 0000000..a6acd1d --- /dev/null +++ b/projects/project-generator-e2e/tests/has-title.test.ts @@ -0,0 +1,7 @@ +import { expect, test } from "@playwright/test"; +import { url } from "./url"; + +test("has title", async ({ page }) => { + await page.goto(url); + await expect(page).toHaveTitle("Vite + React + TS"); +}); diff --git a/projects/project-generator-e2e/tests/has-users.test.ts b/projects/project-generator-e2e/tests/has-users.test.ts new file mode 100644 index 0000000..295a790 --- /dev/null +++ b/projects/project-generator-e2e/tests/has-users.test.ts @@ -0,0 +1,19 @@ +import test, { expect } from "@playwright/test"; +import { url } from "./url"; + +test("has users", async ({ page }) => { + await page.goto(url); + const usersList = page.locator('ul[data-testid="users-list"]'); + await expect(usersList).toBeVisible(); + + // Check if the list has at least one user + const usersCount = await usersList.locator("li").count(); + expect(usersCount).toBeGreaterThan(0); + + // Optionally, check for specific users + const knownUsers = ["John Doe", "Jane Smith"]; + for (const user of knownUsers) { + const userLocator = usersList.locator(`li:has-text("${user}")`); + await expect(userLocator).toBeVisible(); + } +}); diff --git a/projects/project-generator-e2e/tests/url.ts b/projects/project-generator-e2e/tests/url.ts new file mode 100644 index 0000000..417d55f --- /dev/null +++ b/projects/project-generator-e2e/tests/url.ts @@ -0,0 +1 @@ +export const url = "http://localhost:5173/movies-with-actors"; From 9a0ef7646b8981889eb90ab252783964367d894b Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 14:56:38 +0200 Subject: [PATCH 04/10] remove old stuff --- .../_old/example.spec.ts | 18 - .../_old/tests-examples/demo-todo-app.spec.ts | 437 ------------------ 2 files changed, 455 deletions(-) delete mode 100644 projects/project-generator-e2e/_old/example.spec.ts delete mode 100644 projects/project-generator-e2e/_old/tests-examples/demo-todo-app.spec.ts diff --git a/projects/project-generator-e2e/_old/example.spec.ts b/projects/project-generator-e2e/_old/example.spec.ts deleted file mode 100644 index 54a906a..0000000 --- a/projects/project-generator-e2e/_old/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test('get started link', async ({ page }) => { - await page.goto('https://playwright.dev/'); - - // Click the get started link. - await page.getByRole('link', { name: 'Get started' }).click(); - - // Expects page to have a heading with the name of Installation. - await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible(); -}); diff --git a/projects/project-generator-e2e/_old/tests-examples/demo-todo-app.spec.ts b/projects/project-generator-e2e/_old/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 8641cb5..0000000 --- a/projects/project-generator-e2e/_old/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { test, expect, type Page } from '@playwright/test'; - -test.beforeEach(async ({ page }) => { - await page.goto('https://demo.playwright.dev/todomvc'); -}); - -const TODO_ITEMS = [ - 'buy some cheese', - 'feed the cat', - 'book a doctors appointment' -] as const; - -test.describe('New Todo', () => { - test('should allow me to add todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Make sure the list only has one todo item. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0] - ]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - - // Make sure the list now has two todo items. - await expect(page.getByTestId('todo-title')).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[1] - ]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test('should clear text input field when an item is added', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test('should append new items to the bottom of the list', async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - // Check test using different methods. - await expect(page.getByText('3 items left')).toBeVisible(); - await expect(todoCount).toHaveText('3 items left'); - await expect(todoCount).toContainText('3'); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe('Mark all as completed', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should allow me to mark all items as completed', async ({ page }) => { - // Complete all todos. - await page.getByLabel('Mark all as complete').check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test('should allow me to clear the complete state of all items', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']); - }); - - test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => { - const toggleAll = page.getByLabel('Mark all as complete'); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe('Item', () => { - - test('should allow me to mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - // Check first item. - const firstTodo = page.getByTestId('todo-item').nth(0); - await firstTodo.getByRole('checkbox').check(); - await expect(firstTodo).toHaveClass('completed'); - - // Check second item. - const secondTodo = page.getByTestId('todo-item').nth(1); - await expect(secondTodo).not.toHaveClass('completed'); - await secondTodo.getByRole('checkbox').check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).toHaveClass('completed'); - }); - - test('should allow me to un-mark items as complete', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const firstTodo = page.getByTestId('todo-item').nth(0); - const secondTodo = page.getByTestId('todo-item').nth(1); - const firstTodoCheckbox = firstTodo.getByRole('checkbox'); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass('completed'); - await expect(secondTodo).not.toHaveClass('completed'); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test('should allow me to edit an item', async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId('todo-item'); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter'); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2] - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); -}); - -test.describe('Editing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should hide other controls when editing', async ({ page }) => { - const todoItem = page.getByTestId('todo-item').nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole('checkbox')).not.toBeVisible(); - await expect(todoItem.locator('label', { - hasText: TODO_ITEMS[1], - })).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test('should save edits on blur', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should trim entered text', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages '); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - 'buy some sausages', - TODO_ITEMS[2], - ]); - await checkTodosInLocalStorage(page, 'buy some sausages'); - }); - - test('should remove the item if an empty text string was entered', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(''); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter'); - - await expect(todoItems).toHaveText([ - TODO_ITEMS[0], - TODO_ITEMS[2], - ]); - }); - - test('should cancel edits on escape', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages'); - await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape'); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe('Counter', () => { - test('should display the current number of todo items', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - // create a todo count locator - const todoCount = page.getByTestId('todo-count') - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press('Enter'); - - await expect(todoCount).toContainText('1'); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press('Enter'); - await expect(todoCount).toContainText('2'); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe('Clear completed button', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test('should display the correct text', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible(); - }); - - test('should remove completed items when clicked', async ({ page }) => { - const todoItems = page.getByTestId('todo-item'); - await todoItems.nth(1).getByRole('checkbox').check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should be hidden when there are no items that are completed', async ({ page }) => { - await page.locator('.todo-list li .toggle').first().check(); - await page.getByRole('button', { name: 'Clear completed' }).click(); - await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden(); - }); -}); - -test.describe('Persistence', () => { - test('should persist its data', async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } - - const todoItems = page.getByTestId('todo-item'); - const firstTodoCheck = todoItems.nth(0).getByRole('checkbox'); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(['completed', '']); - }); -}); - -test.describe('Routing', () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test('should allow me to display active items', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test('should respect the back button', async ({ page }) => { - const todoItem = page.getByTestId('todo-item'); - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step('Showing all items', async () => { - await page.getByRole('link', { name: 'All' }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step('Showing active items', async () => { - await page.getByRole('link', { name: 'Active' }).click(); - }); - - await test.step('Showing completed items', async () => { - await page.getByRole('link', { name: 'Completed' }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test('should allow me to display completed items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Completed' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(1); - }); - - test('should allow me to display all items', async ({ page }) => { - await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole('link', { name: 'Active' }).click(); - await page.getByRole('link', { name: 'Completed' }).click(); - await page.getByRole('link', { name: 'All' }).click(); - await expect(page.getByTestId('todo-item')).toHaveCount(3); - }); - - test('should highlight the currently applied filter', async ({ page }) => { - await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected'); - - //create locators for active and completed links - const activeLink = page.getByRole('link', { name: 'Active' }); - const completedLink = page.getByRole('link', { name: 'Completed' }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass('selected'); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass('selected'); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press('Enter'); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction(e => { - return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e; - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction(t => { - return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t); - }, title); -} From 58774fb80683948dd8b1e33eec790c82db44cf04 Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 15:09:56 +0200 Subject: [PATCH 05/10] add github action --- .github/workflows/e2e-tests.yml | 77 +++++++++++++++++++ .../.github/workflows/playwright.yml | 27 ------- .../tests/can-add-user.test.ts | 3 - 3 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/e2e-tests.yml delete mode 100644 projects/project-generator-e2e/.github/workflows/playwright.yml diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..fea1c04 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,77 @@ +name: E2E Tests + +on: + pull_request: + paths: + - "projects/project-generator/**" + push: + branches: + - main + paths: + - "projects/project-generator/**" + +jobs: + e2e-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "18" + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install dependencies + run: pnpm install + + - name: Build project generator + run: pnpm --filter create-vite-apollo-fs build + + - name: Generate test project + run: ./projects/project-generator/dist/generator.js --destinationPath=myProject --apiName=my-api --webUiName=my-web-ui + + - name: Install dependencies in generated project + run: | + cd myProject + pnpm install + + - name: Start development server in background + run: | + cd myProject + pnpm dev & + echo $! > dev_server.pid + + - name: Wait for server to be ready + run: | + timeout 60 bash -c 'until curl -f http://localhost:5173; do sleep 2; done' + + - name: Install Playwright browsers + run: | + cd projects/project-generator-e2e + npx playwright install + + - name: Run E2E tests + run: | + cd projects/project-generator-e2e + npm run test + + - name: Stop development server + if: always() + run: | + if [ -f myProject/dev_server.pid ]; then + kill $(cat myProject/dev_server.pid) || true + fi + + - name: Upload test results + if: failure() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: projects/project-generator-e2e/playwright-report/ diff --git a/projects/project-generator-e2e/.github/workflows/playwright.yml b/projects/project-generator-e2e/.github/workflows/playwright.yml deleted file mode 100644 index 8116248..0000000 --- a/projects/project-generator-e2e/.github/workflows/playwright.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Playwright Tests -on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] -jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - 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 - run: pnpm exec playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 diff --git a/projects/project-generator-e2e/tests/can-add-user.test.ts b/projects/project-generator-e2e/tests/can-add-user.test.ts index d92ea1a..7c37b82 100644 --- a/projects/project-generator-e2e/tests/can-add-user.test.ts +++ b/projects/project-generator-e2e/tests/can-add-user.test.ts @@ -1,6 +1,3 @@ -// data-testid="add-user-input" -// data-testid="add-user-button" - import { expect, test } from "@playwright/test"; import { url } from "./url"; From 9f7ae53518cd798b4d857707916ff90e0b9b741b Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 15:12:48 +0200 Subject: [PATCH 06/10] fix ts issue --- projects/project-generator/src/generator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/project-generator/src/generator.ts b/projects/project-generator/src/generator.ts index 28962ed..9b78f76 100644 --- a/projects/project-generator/src/generator.ts +++ b/projects/project-generator/src/generator.ts @@ -234,7 +234,7 @@ async function generateWebUiProject(config: ProjectConfig) { // Add graphqlLoader() to plugins array if not present viteConfig = viteConfig.replace( /(plugins:\s*\[)([^\]]*)\]/, - (match: string, p1: string, p2: string) => { + (_: string, p1: string, p2: string) => { let plugins = p2.trim().replace(/,$/, ""); if (!plugins.includes("graphqlLoader()")) plugins += ", graphqlLoader()"; From 3e05732065384385e1d163b3078e189339f04ea1 Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 15:14:55 +0200 Subject: [PATCH 07/10] try to fix action --- .github/workflows/e2e-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index fea1c04..3caf272 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -35,7 +35,7 @@ jobs: run: pnpm --filter create-vite-apollo-fs build - name: Generate test project - run: ./projects/project-generator/dist/generator.js --destinationPath=myProject --apiName=my-api --webUiName=my-web-ui + run: node ./projects/project-generator/dist/generator.js --destinationPath=myProject --apiName=my-api --webUiName=my-web-ui - name: Install dependencies in generated project run: | From b96a6057e7266eb6348e4031ece0c76ab55ca34b Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 15:18:04 +0200 Subject: [PATCH 08/10] try to fix action --- .github/workflows/e2e-tests.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 3caf272..1c91971 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -21,12 +21,13 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "18" + node-version: "20" + registry-url: "https://registry.npmjs.org" - name: Install pnpm uses: pnpm/action-setup@v4 with: - version: 10 + version: 10.13.1 - name: Install dependencies run: pnpm install @@ -35,7 +36,9 @@ jobs: run: pnpm --filter create-vite-apollo-fs build - name: Generate test project - run: node ./projects/project-generator/dist/generator.js --destinationPath=myProject --apiName=my-api --webUiName=my-web-ui + run: | + chmod +x ./projects/project-generator/dist/generator.js + ./projects/project-generator/dist/generator.js --destinationPath=myProject --apiName=my-api --webUiName=my-web-ui - name: Install dependencies in generated project run: | From 66b90f8d1f9a9705bee58421eb57ec544452377f Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 15:24:11 +0200 Subject: [PATCH 09/10] try to fix action --- .../templates/web-ui/Dashboard.tsx | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/projects/project-generator/templates/web-ui/Dashboard.tsx b/projects/project-generator/templates/web-ui/Dashboard.tsx index 7159a1c..4d11b35 100644 --- a/projects/project-generator/templates/web-ui/Dashboard.tsx +++ b/projects/project-generator/templates/web-ui/Dashboard.tsx @@ -61,7 +61,8 @@ function Dashboard() {

Loading...

) : (

- Hello Query: {helloData?.hello} + Hello Query:{" "} + {helloData?.hello}

)} @@ -79,7 +80,10 @@ function Dashboard() { {usersLoading ? (

Loading users...

) : ( -
    +
      {usersData?.users?.map((user) => (
    • {user.name} (ID: {user.id}) @@ -112,6 +116,7 @@ function Dashboard() { backgroundColor: "#fff", border: "1px solid #ccc", }} + data-testid="add-user-input" /> @@ -139,7 +145,9 @@ function Dashboard() { >

      Latest Greeting:{" "} - {greetingData?.greetings || "Waiting for messages..."} + + {greetingData?.greetings || "Waiting for messages..."} +

      From 3e68c6c81860df7cdc38028ce229995a68d45269 Mon Sep 17 00:00:00 2001 From: Sascha Rose Date: Tue, 29 Jul 2025 15:27:08 +0200 Subject: [PATCH 10/10] try to fix action --- .github/workflows/e2e-tests.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 1c91971..f840703 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -55,10 +55,10 @@ jobs: run: | timeout 60 bash -c 'until curl -f http://localhost:5173; do sleep 2; done' - - name: Install Playwright browsers + - name: Install Playwright browsers and dependencies run: | cd projects/project-generator-e2e - npx playwright install + npx playwright install --with-deps - name: Run E2E tests run: |