From 8c832c78b350664617210b5a5d9ddb44cf5a9bb7 Mon Sep 17 00:00:00 2001 From: Hasan <57855533+hasan-py@users.noreply.github.com> Date: Sat, 12 Oct 2024 14:31:15 +0600 Subject: [PATCH] Revert "First test case" (#2) --- client/e2e/example.spec.ts | 18 + client/e2e/fixtures/createModerator.json | 4 - client/e2e/fixtures/moderatorExists.json | 3 - client/e2e/fixtures/moderatorNotExists.json | 3 - client/e2e/support/constants.ts | 9 - client/e2e/tests/home.spec.ts | 45 -- client/e2e/tests/login.spec.ts | 140 ------- client/package.json | 4 +- client/playwright.config.ts | 27 +- client/src/pages/gameList/gameCard.tsx | 4 +- client/src/pages/gameList/index.tsx | 4 +- client/src/pages/gameReview/index.tsx | 12 +- client/src/pages/moderator/mapView.tsx | 6 +- client/tests-examples/demo-todo-app.spec.ts | 437 ++++++++++++++++++++ client/tsconfig.json | 5 +- 15 files changed, 484 insertions(+), 237 deletions(-) create mode 100644 client/e2e/example.spec.ts delete mode 100644 client/e2e/fixtures/createModerator.json delete mode 100644 client/e2e/fixtures/moderatorExists.json delete mode 100644 client/e2e/fixtures/moderatorNotExists.json delete mode 100644 client/e2e/support/constants.ts delete mode 100644 client/e2e/tests/home.spec.ts delete mode 100644 client/e2e/tests/login.spec.ts create mode 100644 client/tests-examples/demo-todo-app.spec.ts diff --git a/client/e2e/example.spec.ts b/client/e2e/example.spec.ts new file mode 100644 index 0000000..54a906a --- /dev/null +++ b/client/e2e/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/client/e2e/fixtures/createModerator.json b/client/e2e/fixtures/createModerator.json deleted file mode 100644 index b0f6f8a..0000000 --- a/client/e2e/fixtures/createModerator.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "success": true, - "message": "Moderator created successfully" -} diff --git a/client/e2e/fixtures/moderatorExists.json b/client/e2e/fixtures/moderatorExists.json deleted file mode 100644 index ed421bd..0000000 --- a/client/e2e/fixtures/moderatorExists.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "userExists": true -} diff --git a/client/e2e/fixtures/moderatorNotExists.json b/client/e2e/fixtures/moderatorNotExists.json deleted file mode 100644 index 76250b3..0000000 --- a/client/e2e/fixtures/moderatorNotExists.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "userExists": false -} diff --git a/client/e2e/support/constants.ts b/client/e2e/support/constants.ts deleted file mode 100644 index 1d72ab8..0000000 --- a/client/e2e/support/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -const serverURL = (path: string) => { - return "http://localhost:8000" + path; -}; - -export const API_ROUTES = { - LOGIN: serverURL("/api/auth/login"), - CHECK_MODERATOR: serverURL("/api/auth/check-moderator"), - CREATE_MODERATOR: serverURL("/api/auth/create-moderator"), -}; diff --git a/client/e2e/tests/home.spec.ts b/client/e2e/tests/home.spec.ts deleted file mode 100644 index 74949fa..0000000 --- a/client/e2e/tests/home.spec.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test.describe("Home Page", () => { - test("page rendered successfully", async ({ page }) => { - await page.goto("/"); - await expect( - page.locator("text=Find your next captivating gaming moment") - ).toBeVisible(); - }); - - test("Game List: should display game cards with images, titles, and review counts", async ({ - page, - }) => { - await page.goto("/"); - - // Check that the grid exists - await expect(page.locator(".grid")).toBeVisible(); - - const gameCards = page.locator(".grid .col-span-1"); - const cardCount = await gameCards.count(); // Get the number of game cards - - // Check if the count is greater than 1 - expect(cardCount).toBeGreaterThan(1); - - // Check the first game card - const firstGameCard = gameCards.first(); - await expect(firstGameCard.locator("img")).toHaveAttribute( - "src", - /thumbnail\.jpg/ - ); - await expect(firstGameCard.locator("img")).toHaveAttribute( - "alt", - /Game Image/ - ); - - // Check game card details - await expect(firstGameCard.locator("p.text-lg")).toBeVisible(); - await expect(firstGameCard.locator("p.text-lg")).not.toBeEmpty(); - - await expect(firstGameCard.locator("p.text-sm")).toBeVisible(); - await expect(firstGameCard.locator("p.text-sm")).not.toBeEmpty(); - - await expect(firstGameCard.locator("p.text-xs")).toContainText("Reviews"); - }); -}); diff --git a/client/e2e/tests/login.spec.ts b/client/e2e/tests/login.spec.ts deleted file mode 100644 index 3d84673..0000000 --- a/client/e2e/tests/login.spec.ts +++ /dev/null @@ -1,140 +0,0 @@ -// import { test, expect, Page } from "@playwright/test"; -// import { API_ROUTES } from "../support/constants"; - -// test.describe("Moderator Authentication", () => { -// test.beforeEach(async ({ page }) => { -// await page.goto("/login"); -// }); - -// const interceptCheckModerator = async (page: Page, fixtureName: string) => { -// await page.route(API_ROUTES.CHECK_MODERATOR, async (route) => { -// const jsonResponse = await route.fetch(); -// await jsonResponse.json(); -// route.fulfill({ -// status: 200, -// }); -// }); -// }; - -// test("should display login or create account based on user existence", async ({ -// page, -// }) => { -// await interceptCheckModerator(page, "moderatorExists.json"); - -// // Visit the login page or trigger the action that sends the request -// await page.goto("/login"); // Ensure the page that triggers the API call is visited - -// // Now wait for the response -// const response = await page.waitForResponse(API_ROUTES.CHECK_MODERATOR); -// const data = response.json(); -// const userExists = data; - -// // Expectation based on the response -// if (!!userExists) { -// await expect(page.locator("text=Login")).toBeVisible(); -// } else { -// await expect(page.locator("text=Create")).toBeVisible(); -// } -// }); - -// test("should show error on login validations", async ({ page }) => { -// await interceptCheckModerator(page, "moderatorExists.json"); - -// await page.click("button"); -// await expect(page.locator("text=Invalid email address")).toBeVisible(); -// await expect( -// page.locator("text=Password must be at least 6 characters") -// ).toBeVisible(); -// }); - -// test("should show error on create moderator validations", async ({ -// page, -// }) => { -// await interceptCheckModerator(page, "moderatorNotExists.json"); - -// await page.click("button"); -// await expect(page.locator("text=Invalid email address")).toBeVisible(); -// await expect( -// page.locator("text=Password must be at least 6 characters") -// ).toBeVisible(); -// await expect(page.locator("text=Name is required")).toBeVisible(); -// }); - -// test("should toggle the password visibility", async ({ page }) => { -// await interceptCheckModerator(page, "moderatorExists.json"); - -// await page.fill('input[placeholder="Password"]', "password123"); -// await page.click("#toggle-password"); -// await expect(page.locator('input[placeholder="Password"]')).toHaveAttribute( -// "type", -// "text" -// ); -// await expect(page.locator('input[placeholder="Password"]')).toHaveValue( -// "password123" -// ); -// await page.click("#toggle-password"); -// await expect(page.locator('input[placeholder="Password"]')).toHaveAttribute( -// "type", -// "password" -// ); -// }); - -// test("should create an account for new users", async ({ page }) => { -// await interceptCheckModerator(page, "moderatorNotExists.json"); - -// await page.route(API_ROUTES.CREATE_MODERATOR, (route) => -// route.fulfill({ -// status: 200, -// body: JSON.stringify({ -// success: true, -// message: "Moderator created! Please login!", -// }), -// }) -// ); - -// const response = await page.waitForResponse(API_ROUTES.CHECK_MODERATOR); -// const data = await response.json(); -// const userExists = data.userExists; - -// if (!userExists) { -// await expect(page.locator("text=Create Account")).toBeVisible(); -// await page.fill('input[placeholder="Name"]', "John Doe"); -// await page.fill('input[placeholder="Email"]', "john@example.com"); -// await page.fill('input[placeholder="Password"]', "password123"); -// await page.click("button"); - -// const createModeratorResponse = await page.waitForResponse( -// API_ROUTES.CREATE_MODERATOR -// ); -// const createModeratorData = await createModeratorResponse.json(); - -// expect(createModeratorData.success).toBeTruthy(); -// await expect( -// page.locator("text=Moderator created! Please login!") -// ).toBeVisible(); -// } -// }); - -// test("should login an existing user", async ({ page }) => { -// await interceptCheckModerator(page, "moderatorExists.json"); - -// await page.route(API_ROUTES.LOGIN, (route) => -// route.fulfill({ -// status: 200, -// body: JSON.stringify({ token: "mockToken" }), -// }) -// ); - -// await page.fill('input[placeholder="Email"]', "john@example.com"); -// await page.fill('input[placeholder="Password"]', "password123"); -// await page.click("button"); - -// const loginResponse = await page.waitForResponse(API_ROUTES.LOGIN); -// const loginData = await loginResponse.json(); - -// expect(loginData.token).toBe("mockToken"); -// await expect(page.locator("text=Login successfully")).toBeVisible(); - -// await expect(page).toHaveURL("/admin"); -// }); -// }); diff --git a/client/package.json b/client/package.json index 3554c42..009d311 100644 --- a/client/package.json +++ b/client/package.json @@ -6,9 +6,7 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview", - "e2e": "npx playwright test", - "e2e:ui": "npx playwright test --ui" + "preview": "vite preview" }, "dependencies": { "@chakra-ui/icons": "^2.0.18", diff --git a/client/playwright.config.ts b/client/playwright.config.ts index 0582a58..e3309cf 100644 --- a/client/playwright.config.ts +++ b/client/playwright.config.ts @@ -1,4 +1,4 @@ -import { defineConfig, devices } from "@playwright/test"; +import { defineConfig, devices } from '@playwright/test'; /** * Read environment variables from file. @@ -12,7 +12,7 @@ import { defineConfig, devices } from "@playwright/test"; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: "./e2e", + testDir: './e2e', /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -22,25 +22,26 @@ export default defineConfig({ /* 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", + 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:5173", + // 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", + trace: 'on-first-retry', }, /* Configure projects for major browsers */ projects: [ { - name: "chromium", - use: { ...devices["Desktop Chrome"] }, + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, }, { - name: "firefox", - use: { ...devices["Desktop Firefox"] }, + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, }, // { @@ -50,8 +51,8 @@ export default defineConfig({ /* Test against mobile viewports. */ { - name: "Mobile Chrome", - use: { ...devices["Pixel 5"] }, + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, }, // { // name: 'Mobile Safari', @@ -64,8 +65,8 @@ export default defineConfig({ // use: { ...devices['Desktop Edge'], channel: 'msedge' }, // }, { - name: "Google Chrome", - use: { ...devices["Desktop Chrome"], channel: "chrome" }, + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' }, }, ], diff --git a/client/src/pages/gameList/gameCard.tsx b/client/src/pages/gameList/gameCard.tsx index b280aee..6aaac43 100644 --- a/client/src/pages/gameList/gameCard.tsx +++ b/client/src/pages/gameList/gameCard.tsx @@ -27,8 +27,8 @@ export default function GameCard({ item }: { item: any }) { /> {item?.reviews?.length > 0 ? (
- {[...Array(Math.floor(+item?.avgRating))]?.map((_, key) => ( - + {[...Array(Math.floor(+item?.avgRating))]?.map((_) => ( + ))} {+item?.avgRating?.toFixed(2)} diff --git a/client/src/pages/gameList/index.tsx b/client/src/pages/gameList/index.tsx index 58c1299..7fbbc3b 100644 --- a/client/src/pages/gameList/index.tsx +++ b/client/src/pages/gameList/index.tsx @@ -38,8 +38,8 @@ export default function Home() { {!isLoading && data?.length > 0 ? (
- {data?.map((item: any, key: number) => ( -
+ {data?.map((item: any) => ( +
))} diff --git a/client/src/pages/gameReview/index.tsx b/client/src/pages/gameReview/index.tsx index 81b5cba..22cd12a 100644 --- a/client/src/pages/gameReview/index.tsx +++ b/client/src/pages/gameReview/index.tsx @@ -81,8 +81,8 @@ export default function Review() { {avgRating() > 0 ? ( - {[...Array(Math.floor(+avgRating()))]?.map((_, key) => ( - + {[...Array(Math.floor(+avgRating()))]?.map((_) => ( + ))} {avgRating()?.toFixed(2)} @@ -103,8 +103,8 @@ export default function Review() {
- {data?.reviews?.map((item: any, key:number) => ( - + {data?.reviews?.map((item: any) => ( + @@ -124,8 +124,8 @@ export default function Review() { {+item?.rating > 0 ? ( - {[...Array(Math.floor(+item?.rating))]?.map((_, key) => ( - + {[...Array(Math.floor(+item?.rating))]?.map((_) => ( + ))} {item?.rating} diff --git a/client/src/pages/moderator/mapView.tsx b/client/src/pages/moderator/mapView.tsx index 144d0e8..2d64915 100644 --- a/client/src/pages/moderator/mapView.tsx +++ b/client/src/pages/moderator/mapView.tsx @@ -85,9 +85,8 @@ export default function MapView(props: any) { url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - {data?.map((item: any, key: number) => ( + {data?.map((item: any) => ( @@ -101,9 +100,8 @@ export default function MapView(props: any) { {item?.reviews ? ( - {[...Array(+item?.reviews?.rating)]?.map((star, key) => ( + {[...Array(+item?.reviews?.rating)]?.map((star) => ( { + 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/client/tsconfig.json b/client/tsconfig.json index 04c5825..3d0a51a 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -14,9 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx", - "types": ["node"] + "jsx": "react-jsx" }, - "include": ["src", "e2e", "./**/*.ts"], + "include": ["src"], "references": [{ "path": "./tsconfig.node.json" }] }