From f1fdb5adb15e3654ad09b13ebb3492f0d20028e8 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 30 Apr 2024 11:59:32 -0400 Subject: [PATCH 01/26] start playwright test --- .nvmrc | 2 +- core/.github/workflows/playwright.yml | 27 ++ core/.gitignore | 5 + core/package.json | 2 + core/playwright.config.ts | 77 ++++ core/playwright/example.spec.ts | 18 + core/tests-examples/demo-todo-app.spec.ts | 437 ++++++++++++++++++++++ pnpm-lock.yaml | 42 ++- 8 files changed, 604 insertions(+), 6 deletions(-) create mode 100644 core/.github/workflows/playwright.yml create mode 100644 core/.gitignore create mode 100644 core/playwright.config.ts create mode 100644 core/playwright/example.spec.ts create mode 100644 core/tests-examples/demo-todo-app.spec.ts diff --git a/.nvmrc b/.nvmrc index b1855cbb8..322fa5fa4 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.6.0 \ No newline at end of file +v21.7.1 \ No newline at end of file diff --git a/core/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml new file mode 100644 index 000000000..9662b54e9 --- /dev/null +++ b/core/.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: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 000000000..68c5d18f0 --- /dev/null +++ b/core/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/core/package.json b/core/package.json index c68170ced..fa5b07a54 100644 --- a/core/package.json +++ b/core/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { + "playright": "npx playwright test", "dev": "next dev -p 3000 --turbo | pino-pretty", "build": "SKIP_VALIDATION=true next build", "invite-users": "dotenv -e .env.local -e .env.development tsx scripts/invite.ts", @@ -81,6 +82,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@playwright/test": "^1.43.1", "@preconstruct/next": "^4.0.0", "@pubpub/eslint-config": "workspace:*", "@pubpub/prettier-config": "workspace:*", diff --git a/core/playwright.config.ts b/core/playwright.config.ts new file mode 100644 index 000000000..b1e786033 --- /dev/null +++ b/core/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './playwright', + /* 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://127.0.0.1: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://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/core/playwright/example.spec.ts b/core/playwright/example.spec.ts new file mode 100644 index 000000000..54a906a4e --- /dev/null +++ b/core/playwright/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(); +}); diff --git a/core/tests-examples/demo-todo-app.spec.ts b/core/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 000000000..2fd6016fe --- /dev/null +++ b/core/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' +]; + +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/pnpm-lock.yaml b/pnpm-lock.yaml index d01edf743..e6166529c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,7 +212,7 @@ importers: version: 0.356.0(react@18.2.0) next: specifier: 14.2.1 - version: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) next-connect: specifier: ^1.0.0 version: 1.0.0 @@ -268,6 +268,9 @@ importers: specifier: ^3.22.4 version: 3.22.4 devDependencies: + '@playwright/test': + specifier: ^1.43.1 + version: 1.43.1 '@preconstruct/next': specifier: ^4.0.0 version: 4.0.0 @@ -5221,6 +5224,13 @@ packages: dev: false optional: true + /@playwright/test@1.43.1: + resolution: {integrity: sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.43.1 + /@preconstruct/cli@2.8.3: resolution: {integrity: sha512-4PNEPcp8REUdqZIjtpXF1fqECuHt+pIS6k0PluSRcgX0KwPtfSw407Y2B/ItndgtRD3rKHXI6cKkwh/6Mc4TXg==} hasBin: true @@ -7299,7 +7309,7 @@ packages: '@sentry/vercel-edge': 7.109.0 '@sentry/webpack-plugin': 1.21.0 chalk: 3.0.0 - next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 resolve: 1.22.8 rollup: 2.78.0 @@ -8503,7 +8513,7 @@ packages: zod: optional: true dependencies: - next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) zod: 3.22.4 dev: false @@ -12201,6 +12211,13 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -14617,7 +14634,7 @@ packages: next: ^14 react: ^18 dependencies: - next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -14668,7 +14685,7 @@ packages: - babel-plugin-macros dev: false - /next@14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0): + /next@14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SF3TJnKdH43PMkCcErLPv+x/DY1YCklslk3ZmwaVoyUfDgHKexuKlf9sEfBQ69w+ue8jQ3msLb+hSj1T19hGag==} engines: {node: '>=18.17.0'} hasBin: true @@ -14688,6 +14705,7 @@ packages: dependencies: '@next/env': 14.2.1 '@opentelemetry/api': 1.7.0 + '@playwright/test': 1.43.1 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001605 @@ -15476,6 +15494,20 @@ packages: pathe: 1.1.2 dev: true + /playwright-core@1.43.1: + resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==} + engines: {node: '>=16'} + hasBin: true + + /playwright@1.43.1: + resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.43.1 + optionalDependencies: + fsevents: 2.3.2 + /polished@4.2.2: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} engines: {node: '>=10'} From 43772844c6e1e0e3ae111aba6b332caa0b7388b6 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 30 Apr 2024 20:21:35 -0400 Subject: [PATCH 02/26] begin testing lib --- core/playwright.config.ts | 116 +++++++++++++++++----------------- core/playwright/login.spec.ts | 0 2 files changed, 58 insertions(+), 58 deletions(-) create mode 100644 core/playwright/login.spec.ts diff --git a/core/playwright.config.ts b/core/playwright.config.ts index b1e786033..b41cd1c7c 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from '@playwright/test'; +import { defineConfig, devices } from "@playwright/test"; /** * Read environment variables from file. @@ -10,68 +10,68 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './playwright', - /* 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://127.0.0.1:3000', + testDir: "./playwright", + /* 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', - }, + /* 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'] }, - }, + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, + { + 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 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' }, - // }, - ], + /* 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://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, }); diff --git a/core/playwright/login.spec.ts b/core/playwright/login.spec.ts new file mode 100644 index 000000000..e69de29bb From 087409df92d2947ecabc16fcae727c6b5ddf56f0 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 30 Apr 2024 23:07:28 -0400 Subject: [PATCH 03/26] create login spec and logout spec --- core/.gitignore | 2 ++ .../demo-todo-app.spec.ts | 0 core/playwright/example.spec.ts | 2 +- core/playwright/login.spec.ts | 35 +++++++++++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) rename core/{tests-examples => playwright}/demo-todo-app.spec.ts (100%) diff --git a/core/.gitignore b/core/.gitignore index 68c5d18f0..cfaa9a41b 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -3,3 +3,5 @@ node_modules/ /playwright-report/ /blob-report/ /playwright/.cache/ + +playwright/.auth diff --git a/core/tests-examples/demo-todo-app.spec.ts b/core/playwright/demo-todo-app.spec.ts similarity index 100% rename from core/tests-examples/demo-todo-app.spec.ts rename to core/playwright/demo-todo-app.spec.ts diff --git a/core/playwright/example.spec.ts b/core/playwright/example.spec.ts index 54a906a4e..d40a13c2d 100644 --- a/core/playwright/example.spec.ts +++ b/core/playwright/example.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test'; test('has title', async ({ page }) => { - await page.goto('https://playwright.dev/'); + await page.goto('http:/localhost:3000/login'); // Expect a title "to contain" a substring. await expect(page).toHaveTitle(/Playwright/); diff --git a/core/playwright/login.spec.ts b/core/playwright/login.spec.ts index e69de29bb..1b200343d 100644 --- a/core/playwright/login.spec.ts +++ b/core/playwright/login.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from "@playwright/test"; + +const authFile = "playwright/.auth/user.json"; + +test("Login", async ({ page }) => { + // Perform authentication steps. Replace these actions with your own. + await page.goto("/login"); + await page.getByLabel("email").fill("all@pubpub.org"); + await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); + await page.getByRole("button", { name: "Sign in" }).click(); + // Wait until the page receives the cookies. + // + // Sometimes login flow sets cookies in the process of several redirects. + // Wait for the final URL to ensure that the cookies are actually set. + await page.waitForURL("/c/unjournal/stages"); + + // // End of authentication steps. + + // await page.context().storageState({ path: authFile }); +}); + +test("Logout", async ({ page }) => { + // Perform authentication steps. Replace these actions with your own. + await page.goto("/login"); + await page.getByLabel("email").fill("all@pubpub.org"); + await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); + await page.getByRole("button", { name: "Sign in" }).click(); + // Wait until the page receives the cookies. + // + // Sometimes login flow sets cookies in the process of several redirects. + // Wait for the final URL to ensure that the cookies are actually set. + await page.waitForURL("/c/unjournal/stages"); + await page.getByRole("button", { name: "Log out" }).click(); + await page.waitForURL("/login"); +}); From 54af6d5c8e55786577ed23abe7306dcc3970a529 Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 1 May 2024 10:26:21 -0400 Subject: [PATCH 04/26] working login test --- core/playwright.config.ts | 8 +- core/playwright/demo-todo-app.spec.ts | 437 -------------------------- core/playwright/example.spec.ts | 18 -- core/playwright/login.spec.ts | 12 +- 4 files changed, 10 insertions(+), 465 deletions(-) delete mode 100644 core/playwright/demo-todo-app.spec.ts delete mode 100644 core/playwright/example.spec.ts diff --git a/core/playwright.config.ts b/core/playwright.config.ts index b41cd1c7c..43fc47f74 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -42,10 +42,10 @@ export default defineConfig({ use: { ...devices["Desktop Firefox"] }, }, - { - name: "webkit", - use: { ...devices["Desktop Safari"] }, - }, + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"] }, + // }, /* Test against mobile viewports. */ // { diff --git a/core/playwright/demo-todo-app.spec.ts b/core/playwright/demo-todo-app.spec.ts deleted file mode 100644 index 2fd6016fe..000000000 --- a/core/playwright/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' -]; - -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/core/playwright/example.spec.ts b/core/playwright/example.spec.ts deleted file mode 100644 index d40a13c2d..000000000 --- a/core/playwright/example.spec.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('http:/localhost:3000/login'); - - // 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/core/playwright/login.spec.ts b/core/playwright/login.spec.ts index 1b200343d..b13ef4361 100644 --- a/core/playwright/login.spec.ts +++ b/core/playwright/login.spec.ts @@ -3,20 +3,19 @@ import { expect, test } from "@playwright/test"; const authFile = "playwright/.auth/user.json"; test("Login", async ({ page }) => { - // Perform authentication steps. Replace these actions with your own. await page.goto("/login"); await page.getByLabel("email").fill("all@pubpub.org"); await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); await page.getByRole("button", { name: "Sign in" }).click(); + // Wait until the page receives the cookies. // // Sometimes login flow sets cookies in the process of several redirects. // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL("/c/unjournal/stages"); - - // // End of authentication steps. - - // await page.context().storageState({ path: authFile }); + await expect(page.getByRole("link", { name: "Stages" })).toBeVisible(); + // End of authentication steps. + await page.context().storageState({ path: authFile }); }); test("Logout", async ({ page }) => { @@ -30,6 +29,7 @@ test("Logout", async ({ page }) => { // Sometimes login flow sets cookies in the process of several redirects. // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL("/c/unjournal/stages"); - await page.getByRole("button", { name: "Log out" }).click(); + await page.getByRole("button", { name: "Logout" }).click(); await page.waitForURL("/login"); + await page.context().storageState({ path: authFile }); }); From 8fe40cceda2d361584476f9fded61d1f124c4048 Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 1 May 2024 10:28:56 -0400 Subject: [PATCH 05/26] expect a logout button --- core/playwright/login.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/core/playwright/login.spec.ts b/core/playwright/login.spec.ts index b13ef4361..ffb513c2e 100644 --- a/core/playwright/login.spec.ts +++ b/core/playwright/login.spec.ts @@ -29,6 +29,7 @@ test("Logout", async ({ page }) => { // Sometimes login flow sets cookies in the process of several redirects. // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL("/c/unjournal/stages"); + await expect(page.getByRole("button", { name: "Logout" })).toBeVisible(); await page.getByRole("button", { name: "Logout" }).click(); await page.waitForURL("/login"); await page.context().storageState({ path: authFile }); From 093d34281db40e135617e2d64112149601fce3d8 Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 1 May 2024 10:42:23 -0400 Subject: [PATCH 06/26] expect a logout button, create assignment test --- core/playwright.config.ts | 8 ++++---- core/playwright/assign.spec.ts | 10 ++++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 core/playwright/assign.spec.ts diff --git a/core/playwright.config.ts b/core/playwright.config.ts index 43fc47f74..b41cd1c7c 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -42,10 +42,10 @@ export default defineConfig({ use: { ...devices["Desktop Firefox"] }, }, - // { - // name: "webkit", - // use: { ...devices["Desktop Safari"] }, - // }, + { + name: "webkit", + use: { ...devices["Desktop Safari"] }, + }, /* Test against mobile viewports. */ // { diff --git a/core/playwright/assign.spec.ts b/core/playwright/assign.spec.ts new file mode 100644 index 000000000..4171e043a --- /dev/null +++ b/core/playwright/assign.spec.ts @@ -0,0 +1,10 @@ +import { expect, test } from "@playwright/test"; + + +test("Assigning members to a stage", async ({ page }) => { + await page.goto("/c/unjournal/stages"); + await page.getByRole("button", { name: "Create stage" }).click(); + await page.getByRole("textbox", { name: "title" }).fill("Test stage"); + await page.getByRole("button", { name: "Create" }).click(); + await page.waitForURL("/c/unjournal/stages/test-stage"); +}); From c64990388b4529db20050a2a9513966faff26935 Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 1 May 2024 10:56:43 -0400 Subject: [PATCH 07/26] run lint --- core/.github/workflows/playwright.yml | 48 +++++++++++++-------------- core/package.json | 3 +- core/playwright/assign.spec.ts | 5 ++- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/core/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml index 9662b54e9..f314305dc 100644 --- a/core/.github/workflows/playwright.yml +++ b/core/.github/workflows/playwright.yml @@ -1,27 +1,27 @@ name: Playwright Tests on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] + 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: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + 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: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/core/package.json b/core/package.json index fa5b07a54..65b742866 100644 --- a/core/package.json +++ b/core/package.json @@ -3,7 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { - "playright": "npx playwright test", + "playright:test": "npx playwright test", + "playright:ui": "npx playwright test --ui", "dev": "next dev -p 3000 --turbo | pino-pretty", "build": "SKIP_VALIDATION=true next build", "invite-users": "dotenv -e .env.local -e .env.development tsx scripts/invite.ts", diff --git a/core/playwright/assign.spec.ts b/core/playwright/assign.spec.ts index 4171e043a..6cf6899a8 100644 --- a/core/playwright/assign.spec.ts +++ b/core/playwright/assign.spec.ts @@ -1,10 +1,13 @@ import { expect, test } from "@playwright/test"; +const authFile = "playwright/.auth/user.json"; -test("Assigning members to a stage", async ({ page }) => { +test("Assigning members to a pub", async ({ page }) => { + // move a pub to to evaluate or just add it there in seed??? await page.goto("/c/unjournal/stages"); await page.getByRole("button", { name: "Create stage" }).click(); await page.getByRole("textbox", { name: "title" }).fill("Test stage"); await page.getByRole("button", { name: "Create" }).click(); await page.waitForURL("/c/unjournal/stages/test-stage"); + await page.context().storageState({ path: authFile }); }); From 8337bc42c74eb7c3f4698938c5da8b7d83b4682b Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 1 May 2024 14:05:16 -0400 Subject: [PATCH 08/26] im a fifth rate coder with a 4th rate dev setup --- core/playwright.config.ts | 10 +-- core/playwright/assign.spec.ts | 18 +++-- .../prisma/exampleCommunitySeeds/unjournal.ts | 81 ++++++++++++------- core/prisma/seed.ts | 2 +- 4 files changed, 68 insertions(+), 43 deletions(-) diff --git a/core/playwright.config.ts b/core/playwright.config.ts index b41cd1c7c..42cd55466 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -69,9 +69,9 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + webServer: { + command: 'pnpm dev', + url: 'http://127.0.0.1:3000', + reuseExistingServer: !process.env.CI, + }, }); diff --git a/core/playwright/assign.spec.ts b/core/playwright/assign.spec.ts index 6cf6899a8..565c916ff 100644 --- a/core/playwright/assign.spec.ts +++ b/core/playwright/assign.spec.ts @@ -3,11 +3,17 @@ import { expect, test } from "@playwright/test"; const authFile = "playwright/.auth/user.json"; test("Assigning members to a pub", async ({ page }) => { - // move a pub to to evaluate or just add it there in seed??? - await page.goto("/c/unjournal/stages"); - await page.getByRole("button", { name: "Create stage" }).click(); - await page.getByRole("textbox", { name: "title" }).fill("Test stage"); - await page.getByRole("button", { name: "Create" }).click(); - await page.waitForURL("/c/unjournal/stages/test-stage"); + await page.goto("/login"); + await page.getByLabel("email").fill("all@pubpub.org"); + await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); + await page.getByRole("button", { name: "Sign in" }).click(); + // Wait until the page receives the cookies. + // + // Sometimes login flow sets cookies in the process of several redirects. + // Wait for the final URL to ensure that the cookies are actually set. + await page.waitForURL("/c/unjournal/stages"); + await page.getByRole("button", { name: "Assign" }).click(); + await page.getByRole("option", { name: "Jill Admin" }).click(); + await page.getByText("Succes").waitFor({ state: "hidden" }); await page.context().storageState({ path: authFile }); }); diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 99f31652f..f00845e9f 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -1,13 +1,22 @@ import { faker } from "@faker-js/faker"; -import { PrismaClient } from "@prisma/client"; +import { PrismaClient, PubType } from "@prisma/client"; import { v4 as uuidv4 } from "uuid"; +import type { db as kyselyDb } from "~/kysely/database"; +import type { CommunitiesId } from "~/kysely/types/public/Communities"; +import type { PubTypesId } from "~/kysely/types/public/PubTypes"; +import { corePubFields } from "~/actions/corePubFields"; +import { StagesId } from "~/kysely/types/public/Stages"; import { env } from "../../lib/env/env.mjs"; import { FileUpload } from "../../lib/fields/fileUpload"; export const unJournalId = "03e7a5fd-bdca-4682-9221-3a69992c1f3b"; -export default async function main(prisma: PrismaClient, communityUUID: string) { +export default async function main( + db: typeof kyselyDb, + prisma: PrismaClient, + communityUUID: string +) { await prisma.community.create({ data: { id: communityUUID, @@ -627,7 +636,45 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }, }, }); - + const corePubSlugs = corePubFields.map((field) => field.slug); + const persistedCorePubFields = await db + .selectFrom("pub_fields") + .selectAll() + .where("pub_fields.slug", "in", corePubSlugs) + .execute(); + await db + .with("new_pubs", (db) => + db + .insertInto("pubs") + .values({ + community_id: communityUUID as CommunitiesId, + pub_type_id: submissionTypeId as PubTypesId, + }) + .returning("id") + ) + .with("pubs_in_stages", (db) => + db.insertInto("PubsInStages").values((eb) => [ + { + pubId: eb.selectFrom("new_pubs").select("id"), + stageId: stageIds[3] as StagesId, + }, + ]) + ) + .insertInto("pub_values") + .values((eb) => [ + { + pub_id: eb.selectFrom("new_pubs").select("new_pubs.id"), + field_id: persistedCorePubFields.find((field) => field.slug === "pubpub:title")!.id, + value: '"It Aint Ease Bein Cheese"', + }, + { + pub_id: eb.selectFrom("new_pubs").select("new_pubs.id"), + field_id: persistedCorePubFields.find((field) => field.slug === "pubpub:content")! + .id, + value: '"# Abstract"', + }, + ]) + .execute(); // await prisma.pub.update({ // where: { id: submission.id }, // data: { @@ -776,32 +823,4 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }); }) ); - - // const pubIds = [...Array(7)].map((x) => uuidv4()); - // const submissionToEvaluate = await prisma.pub.create({ - // data: { - // pubTypeId: submissionTypeId, - // communityId: communityUUID, - // stages: { connect: { id: stageIds[3] } }, - - // values: { - // createMany: { - // data: [ - // { - // fieldId: fieldIds[0], - // value: "When Celebrities Speak: A Nationwide Twitter Experiment Promoting Vaccination In Indonesia", - // }, - // { - // fieldId: fieldIds[1], - // value: "Celebrity endorsements are often sought to influence public opinion. We ask whether celebrity endorsement per se has an effect beyond the fact that their statements are seen by many, and whether on net their statements actually lead people to change their beliefs. To do so, we conducted a nationwide Twitter experiment in Indonesia with 46 high-profile celebrities and organizations, with a total of 7.8 million followers, who agreed to let us randomly tweet or retweet content promoting immunization from their accounts. Our design exploits the structure of what information is passed on along a retweet chain on Twitter to parse reach versus endorsement effects. Endorsements matter: tweets that users can identify as being originated by a celebrity are far more likely to be liked or retweeted by users than similar tweets seen by the same users but without the celebrities' imprimatur. By contrast, explicitly citing sources in the tweets actually reduces diffusion. By randomizing which celebrities tweeted when, we find suggestive evidence that overall exposure to the campaign may influence beliefs about vaccination and knowledge of immunization-seeking behavior by one's network. Taken together, the findings suggest an important role for celebrity endorsement.", - // }, - // { - // fieldId: fieldIds[8], - // value: "10.3386/w25589", - // }, - // ], - // }, - // }, - // }, - // }); } diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index a1f1c11a0..8a6f5e0d4 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -67,7 +67,7 @@ async function main() { logger.info("build crocroc"); await buildCrocCroc(db, crocCrocId); logger.info("build unjournal"); - await buildUnjournal(prisma, unJournalId); + await buildUnjournal(db, prisma, unJournalId); try { await createUserMembers( From c05942def9c66cbf186ef283d4ac764e916e0b55 Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 1 May 2024 15:04:46 -0400 Subject: [PATCH 09/26] Test assign: todo add reassign --- core/app/c/[communitySlug]/stages/components/Assign.tsx | 1 + core/playwright/assign.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/core/app/c/[communitySlug]/stages/components/Assign.tsx b/core/app/c/[communitySlug]/stages/components/Assign.tsx index 14603dc38..0dd78eeff 100644 --- a/core/app/c/[communitySlug]/stages/components/Assign.tsx +++ b/core/app/c/[communitySlug]/stages/components/Assign.tsx @@ -107,6 +107,7 @@ export default function Assign(props: Props) { size="sm" variant="outline" role="combobox" + name="Assign" aria-expanded={open} className="w-[150px] justify-between" > diff --git a/core/playwright/assign.spec.ts b/core/playwright/assign.spec.ts index 565c916ff..a3694386d 100644 --- a/core/playwright/assign.spec.ts +++ b/core/playwright/assign.spec.ts @@ -12,8 +12,8 @@ test("Assigning members to a pub", async ({ page }) => { // Sometimes login flow sets cookies in the process of several redirects. // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL("/c/unjournal/stages"); - await page.getByRole("button", { name: "Assign" }).click(); + await page.getByRole("combobox").click(); await page.getByRole("option", { name: "Jill Admin" }).click(); - await page.getByText("Succes").waitFor({ state: "hidden" }); + await page.getByText("Success", { exact: true }); await page.context().storageState({ path: authFile }); }); From 231ef68c6c6439dd61a7c03a6b7a8390c40a085a Mon Sep 17 00:00:00 2001 From: qweliant Date: Wed, 1 May 2024 15:55:27 -0400 Subject: [PATCH 10/26] format --- core/package.json | 4 ++-- core/playwright.config.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/core/package.json b/core/package.json index 65b742866..c0aec10fa 100644 --- a/core/package.json +++ b/core/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { - "playright:test": "npx playwright test", - "playright:ui": "npx playwright test --ui", + "playwright:test": "npx playwright test", + "playwright:ui": "npx playwright test --ui", "dev": "next dev -p 3000 --turbo | pino-pretty", "build": "SKIP_VALIDATION=true next build", "invite-users": "dotenv -e .env.local -e .env.development tsx scripts/invite.ts", diff --git a/core/playwright.config.ts b/core/playwright.config.ts index 42cd55466..4cf8cdf40 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -70,8 +70,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: 'pnpm dev', - url: 'http://127.0.0.1:3000', - reuseExistingServer: !process.env.CI, + command: "pnpm start", + url: "http://127.0.0.1:3000", + reuseExistingServer: !process.env.CI, }, }); From 70d0897ae03d8e25f475c3f7f285788bd925ce10 Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 09:49:32 -0400 Subject: [PATCH 11/26] add to ci --- .github/workflows/changesets.yml | 2 +- .github/workflows/ci.yml | 2 +- .github/workflows/playwright.yml | 42 +++++++++++++++++++++++++++ Dockerfile | 2 +- core/.github/workflows/playwright.yml | 27 ----------------- core/playwright.config.ts | 10 +++---- 6 files changed, 50 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/playwright.yml delete mode 100644 core/.github/workflows/playwright.yml diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 3b071e66a..52f5670d1 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -18,7 +18,7 @@ jobs: - name: setup node.js uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 21.7.1 - name: install pnpm run: npm i pnpm@latest -g - name: setup npmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6df4cb07..9f42d3d7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 21.7.1 - name: Install pnpm uses: pnpm/action-setup@v3 diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 000000000..78a7b752a --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,42 @@ +name: Playwright Tests +on: + push: + branches: [main, master, qdt/it-playwright] + pull_request: + branches: [main, master] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Node.js + uses: actions/setup-node@v4 + with: + node-version: 21.7.1 + + - name: Install pnpm + uses: pnpm/action-setup@v3 + with: + version: 8 + run_install: false + + - name: Get pnpm store directory + shell: bash + run: | + echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV + + - name: Install dependencies + run: pnpm install + + - name: playwright-tests + run: pnpm --filter core run playwright:test + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/Dockerfile b/Dockerfile index 99b27114b..c126e805b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ -ARG NODE_VERSION=20.6.0 +ARG NODE_VERSION=21.7.1 ARG PACKAGE ARG PORT=3000 diff --git a/core/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml deleted file mode 100644 index f314305dc..000000000 --- a/core/.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: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 diff --git a/core/playwright.config.ts b/core/playwright.config.ts index 4cf8cdf40..ddea31ae9 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -69,9 +69,9 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - webServer: { - command: "pnpm start", - url: "http://127.0.0.1:3000", - reuseExistingServer: !process.env.CI, - }, + // webServer: { + // command: "pnpm dev", + // url: "http://127.0.0.1:3000", + // reuseExistingServer: !process.env.CI, + // }, }); From 83d13fd26c027cec4b1dd3aef88950deb7508be9 Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 09:54:12 -0400 Subject: [PATCH 12/26] install browsers --- .github/workflows/playwright.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 78a7b752a..15c02dc3c 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,7 +1,7 @@ name: Playwright Tests on: push: - branches: [main, master, qdt/it-playwright] + branches: [main, master] pull_request: branches: [main, master] jobs: @@ -27,13 +27,13 @@ jobs: shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - + - name: Install dependencies run: pnpm install - + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps - name: playwright-tests run: pnpm --filter core run playwright:test - - uses: actions/upload-artifact@v4 if: always() with: From 4dd42d62b42294210b1c09b6d8ee91de0d55d5dd Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 09:57:04 -0400 Subject: [PATCH 13/26] install browsers and pnpm globally --- {.github => core/.github}/workflows/playwright.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename {.github => core/.github}/workflows/playwright.yml (92%) diff --git a/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml similarity index 92% rename from .github/workflows/playwright.yml rename to core/.github/workflows/playwright.yml index 15c02dc3c..ddd2a6eda 100644 --- a/.github/workflows/playwright.yml +++ b/core/.github/workflows/playwright.yml @@ -27,12 +27,12 @@ jobs: shell: bash run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - + - name: Install dependencies - run: pnpm install + run: npm install -g pnpm && pnpm install - name: Install Playwright Browsers run: pnpm exec playwright install --with-deps - - name: playwright-tests + - name: Run Playwright tests run: pnpm --filter core run playwright:test - uses: actions/upload-artifact@v4 if: always() From b4bf5f49f3d6efcf697e419fc0ee5a8303f3f050 Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 09:57:48 -0400 Subject: [PATCH 14/26] move to top level hook --- {core/.github => .github}/workflows/playwright.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {core/.github => .github}/workflows/playwright.yml (100%) diff --git a/core/.github/workflows/playwright.yml b/.github/workflows/playwright.yml similarity index 100% rename from core/.github/workflows/playwright.yml rename to .github/workflows/playwright.yml From bfb9f51fdba10ea00be861a0c9d7b45896c91077 Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 10:01:12 -0400 Subject: [PATCH 15/26] last try b4 food --- .github/workflows/playwright.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index ddd2a6eda..399a41208 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -16,18 +16,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: 21.7.1 - - - name: Install pnpm - uses: pnpm/action-setup@v3 - with: - version: 8 - run_install: false - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - name: Install dependencies run: npm install -g pnpm && pnpm install - name: Install Playwright Browsers From abb9fb53bf6e9fccaad5e68560bc67d9b3c615d5 Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 10:07:05 -0400 Subject: [PATCH 16/26] rmv from workflows since turbo calls playwright test automagically --- {.github => core/.github}/workflows/playwright.yml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) rename {.github => core/.github}/workflows/playwright.yml (73%) diff --git a/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml similarity index 73% rename from .github/workflows/playwright.yml rename to core/.github/workflows/playwright.yml index 399a41208..f314305dc 100644 --- a/.github/workflows/playwright.yml +++ b/core/.github/workflows/playwright.yml @@ -9,19 +9,16 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Node.js - uses: actions/setup-node@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 21.7.1 + 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 --filter core run playwright:test + run: pnpm exec playwright test - uses: actions/upload-artifact@v4 if: always() with: From 77aad5b6d0570cc5d95e4796530f826a87754dde Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 15:08:33 -0400 Subject: [PATCH 17/26] test turbo --- core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/package.json b/core/package.json index c0aec10fa..9ce5edf95 100644 --- a/core/package.json +++ b/core/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "playwright:test": "npx playwright test", + "playwright:👀": "npx playwright test", "playwright:ui": "npx playwright test --ui", "dev": "next dev -p 3000 --turbo | pino-pretty", "build": "SKIP_VALIDATION=true next build", From 2042e769b59d1f01c79e18cbdcabf87bdab36b6b Mon Sep 17 00:00:00 2001 From: qweliant Date: Thu, 2 May 2024 15:54:21 -0400 Subject: [PATCH 18/26] test vitest --- core/vitest.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/vitest.config.ts b/core/vitest.config.ts index f2561f83b..9e6ccf103 100644 --- a/core/vitest.config.ts +++ b/core/vitest.config.ts @@ -6,5 +6,7 @@ export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { environment: "jsdom", + // exclude playwrigth tests + exclude: ["**/playwright/**"], }, }); From b8e0d0b456439fb206578085f7afc333d877f787 Mon Sep 17 00:00:00 2001 From: qweliant Date: Mon, 6 May 2024 18:13:56 -0400 Subject: [PATCH 19/26] passes test --- core/vitest.config.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/vitest.config.ts b/core/vitest.config.ts index 9e6ccf103..26414b1e9 100644 --- a/core/vitest.config.ts +++ b/core/vitest.config.ts @@ -6,7 +6,13 @@ export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { environment: "jsdom", - // exclude playwrigth tests - exclude: ["**/playwright/**"], + exclude: [ + "**/playwright/**", + "**/node_modules/**", + "**/dist/**", + "**/cypress/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*", + ], }, }); From 865a9d7020f0b41b3d699a09a074348de822a7ba Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 7 May 2024 08:41:37 -0400 Subject: [PATCH 20/26] webkit test do not run --- core/playwright.config.ts | 10 +++++----- core/playwright/assign.spec.ts | 5 +---- core/playwright/login.spec.ts | 10 +--------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/core/playwright.config.ts b/core/playwright.config.ts index ddea31ae9..676f0bbc2 100644 --- a/core/playwright.config.ts +++ b/core/playwright.config.ts @@ -41,11 +41,11 @@ export default defineConfig({ name: "firefox", use: { ...devices["Desktop Firefox"] }, }, - - { - name: "webkit", - use: { ...devices["Desktop Safari"] }, - }, + /* Test against WebKit on macOS is failing for me */ + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"] }, + // }, /* Test against mobile viewports. */ // { diff --git a/core/playwright/assign.spec.ts b/core/playwright/assign.spec.ts index a3694386d..bb1103eb5 100644 --- a/core/playwright/assign.spec.ts +++ b/core/playwright/assign.spec.ts @@ -7,10 +7,7 @@ test("Assigning members to a pub", async ({ page }) => { await page.getByLabel("email").fill("all@pubpub.org"); await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); await page.getByRole("button", { name: "Sign in" }).click(); - // Wait until the page receives the cookies. - // - // Sometimes login flow sets cookies in the process of several redirects. - // Wait for the final URL to ensure that the cookies are actually set. + await page.waitForURL("/c/unjournal/stages"); await page.getByRole("combobox").click(); await page.getByRole("option", { name: "Jill Admin" }).click(); diff --git a/core/playwright/login.spec.ts b/core/playwright/login.spec.ts index ffb513c2e..56835cc1f 100644 --- a/core/playwright/login.spec.ts +++ b/core/playwright/login.spec.ts @@ -8,10 +8,6 @@ test("Login", async ({ page }) => { await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); await page.getByRole("button", { name: "Sign in" }).click(); - // Wait until the page receives the cookies. - // - // Sometimes login flow sets cookies in the process of several redirects. - // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL("/c/unjournal/stages"); await expect(page.getByRole("link", { name: "Stages" })).toBeVisible(); // End of authentication steps. @@ -19,15 +15,11 @@ test("Login", async ({ page }) => { }); test("Logout", async ({ page }) => { - // Perform authentication steps. Replace these actions with your own. + // should replace login with startup and and eventual db actions with teardown steps await page.goto("/login"); await page.getByLabel("email").fill("all@pubpub.org"); await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); await page.getByRole("button", { name: "Sign in" }).click(); - // Wait until the page receives the cookies. - // - // Sometimes login flow sets cookies in the process of several redirects. - // Wait for the final URL to ensure that the cookies are actually set. await page.waitForURL("/c/unjournal/stages"); await expect(page.getByRole("button", { name: "Logout" })).toBeVisible(); await page.getByRole("button", { name: "Logout" }).click(); From 73173bb4ea193434ff15d14ae87403cc50136750 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 7 May 2024 08:46:34 -0400 Subject: [PATCH 21/26] lint and clean code --- core/playwright/assign.spec.ts | 2 +- core/playwright/login.spec.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core/playwright/assign.spec.ts b/core/playwright/assign.spec.ts index bb1103eb5..3257fce05 100644 --- a/core/playwright/assign.spec.ts +++ b/core/playwright/assign.spec.ts @@ -11,6 +11,6 @@ test("Assigning members to a pub", async ({ page }) => { await page.waitForURL("/c/unjournal/stages"); await page.getByRole("combobox").click(); await page.getByRole("option", { name: "Jill Admin" }).click(); - await page.getByText("Success", { exact: true }); + await expect(page.getByText("Success", { exact: true })).toBeVisible(); await page.context().storageState({ path: authFile }); }); diff --git a/core/playwright/login.spec.ts b/core/playwright/login.spec.ts index 56835cc1f..d9ee52526 100644 --- a/core/playwright/login.spec.ts +++ b/core/playwright/login.spec.ts @@ -7,10 +7,8 @@ test("Login", async ({ page }) => { await page.getByLabel("email").fill("all@pubpub.org"); await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); await page.getByRole("button", { name: "Sign in" }).click(); - await page.waitForURL("/c/unjournal/stages"); await expect(page.getByRole("link", { name: "Stages" })).toBeVisible(); - // End of authentication steps. await page.context().storageState({ path: authFile }); }); From d2213c23f09e5a137e4d025fde2992a63f0d90c0 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 7 May 2024 08:49:28 -0400 Subject: [PATCH 22/26] rmv cooler way of calling this --- core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/package.json b/core/package.json index 9ce5edf95..c0aec10fa 100644 --- a/core/package.json +++ b/core/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "private": true, "scripts": { - "playwright:👀": "npx playwright test", + "playwright:test": "npx playwright test", "playwright:ui": "npx playwright test --ui", "dev": "next dev -p 3000 --turbo | pino-pretty", "build": "SKIP_VALIDATION=true next build", From 1e6defc76729dfbdcfb074d549bc3598435b95bc Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 7 May 2024 12:14:29 -0400 Subject: [PATCH 23/26] rmv npx --- core/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/package.json b/core/package.json index c0aec10fa..b174c6c84 100644 --- a/core/package.json +++ b/core/package.json @@ -3,8 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { - "playwright:test": "npx playwright test", - "playwright:ui": "npx playwright test --ui", + "playwright:test": "playwright test", + "playwright:ui": "playwright test --ui", "dev": "next dev -p 3000 --turbo | pino-pretty", "build": "SKIP_VALIDATION=true next build", "invite-users": "dotenv -e .env.local -e .env.development tsx scripts/invite.ts", From 496b21099e82d2a9496c10384438ba4e960c9f74 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 7 May 2024 12:45:36 -0400 Subject: [PATCH 24/26] fix: rmv expression from argument expressions --- core/prisma/exampleCommunitySeeds/unjournal.ts | 3 +-- core/prisma/seed.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index f00845e9f..692b0c9f7 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -2,10 +2,10 @@ import { faker } from "@faker-js/faker"; import { PrismaClient, PubType } from "@prisma/client"; import { v4 as uuidv4 } from "uuid"; -import type { db as kyselyDb } from "~/kysely/database"; import type { CommunitiesId } from "~/kysely/types/public/Communities"; import type { PubTypesId } from "~/kysely/types/public/PubTypes"; import { corePubFields } from "~/actions/corePubFields"; +import { db } from "~/kysely/database"; import { StagesId } from "~/kysely/types/public/Stages"; import { env } from "../../lib/env/env.mjs"; import { FileUpload } from "../../lib/fields/fileUpload"; @@ -13,7 +13,6 @@ import { FileUpload } from "../../lib/fields/fileUpload"; export const unJournalId = "03e7a5fd-bdca-4682-9221-3a69992c1f3b"; export default async function main( - db: typeof kyselyDb, prisma: PrismaClient, communityUUID: string ) { diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index 8a6f5e0d4..a1f1c11a0 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -67,7 +67,7 @@ async function main() { logger.info("build crocroc"); await buildCrocCroc(db, crocCrocId); logger.info("build unjournal"); - await buildUnjournal(db, prisma, unJournalId); + await buildUnjournal(prisma, unJournalId); try { await createUserMembers( From 87175aa849f4087cffd32a916f03c3c2c3e71fc5 Mon Sep 17 00:00:00 2001 From: qweliant Date: Tue, 7 May 2024 12:51:53 -0400 Subject: [PATCH 25/26] change db import --- core/prisma/exampleCommunitySeeds/croccroc.ts | 10 ++-------- core/prisma/exampleCommunitySeeds/unjournal.ts | 7 ++----- core/prisma/seed.ts | 2 +- 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/core/prisma/exampleCommunitySeeds/croccroc.ts b/core/prisma/exampleCommunitySeeds/croccroc.ts index 16cd683fc..88d5cc46a 100644 --- a/core/prisma/exampleCommunitySeeds/croccroc.ts +++ b/core/prisma/exampleCommunitySeeds/croccroc.ts @@ -1,6 +1,4 @@ import { faker } from "@faker-js/faker"; -import { PubType } from "@prisma/client"; -import { v4 as uuidv4, v4 } from "uuid"; import { logger } from "logger"; @@ -8,15 +6,11 @@ import type { CommunitiesId } from "~/kysely/types/public/Communities"; import type { PubTypesId } from "~/kysely/types/public/PubTypes"; import { registerCorePubField } from "~/actions/_lib/init"; import { corePubFields } from "~/actions/corePubFields"; -// import { PrismaClient } from "@prisma/client"; -import { type db as kyselyDb } from "~/kysely/database"; -import { env } from "../../lib/env/env.mjs"; - -// import { FileUpload } from "../../lib/fields/fileUpload"; +import { db } from "~/kysely/database"; export const crocCrocId = "758ba348-92c7-46ec-9612-7afda81e2d70" as CommunitiesId; -export default async function main(db: typeof kyselyDb, communityUUID: CommunitiesId) { +export default async function main(communityUUID: CommunitiesId) { logger.info("Registering core fields"); for (const corePubField of corePubFields) { logger.info(`Registering core field ${corePubField.slug}`); diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 692b0c9f7..98770f1a1 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -1,5 +1,5 @@ import { faker } from "@faker-js/faker"; -import { PrismaClient, PubType } from "@prisma/client"; +import { PrismaClient } from "@prisma/client"; import { v4 as uuidv4 } from "uuid"; import type { CommunitiesId } from "~/kysely/types/public/Communities"; @@ -12,10 +12,7 @@ import { FileUpload } from "../../lib/fields/fileUpload"; export const unJournalId = "03e7a5fd-bdca-4682-9221-3a69992c1f3b"; -export default async function main( - prisma: PrismaClient, - communityUUID: string -) { +export default async function main(prisma: PrismaClient, communityUUID: string) { await prisma.community.create({ data: { id: communityUUID, diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index a1f1c11a0..33151aad4 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -65,7 +65,7 @@ async function main() { ]; logger.info("build crocroc"); - await buildCrocCroc(db, crocCrocId); + await buildCrocCroc(crocCrocId); logger.info("build unjournal"); await buildUnjournal(prisma, unJournalId); From 9390480aa04a8bfd836b2645222809f8c53a55cd Mon Sep 17 00:00:00 2001 From: qweliant Date: Mon, 13 May 2024 14:20:56 -0400 Subject: [PATCH 26/26] use lts --- .github/workflows/changesets.yml | 2 +- .github/workflows/ci.yml | 2 +- .nvmrc | 2 +- Dockerfile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 52f5670d1..b52230e20 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -18,7 +18,7 @@ jobs: - name: setup node.js uses: actions/setup-node@v3 with: - node-version: 21.7.1 + node-version: 20.13.1 - name: install pnpm run: npm i pnpm@latest -g - name: setup npmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f42d3d7e..371eaea18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 21.7.1 + node-version: 20.13.1 - name: Install pnpm uses: pnpm/action-setup@v3 diff --git a/.nvmrc b/.nvmrc index 322fa5fa4..67d2ffed5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v21.7.1 \ No newline at end of file +v20.13.1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c126e805b..07ce69ef6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ -ARG NODE_VERSION=21.7.1 +ARG NODE_VERSION=20.13.1 ARG PACKAGE ARG PORT=3000