From 1c560574908cd732a959864aecb0e36d9cc41607 Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Wed, 8 Jan 2025 14:34:32 +0900 Subject: [PATCH 01/15] =?UTF-8?q?build:=20playwright=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +- package.json | 17 +- playwright.config.ts | 85 ++++++ pnpm-lock.yaml | 83 +++--- tests-examples/demo-todo-app.spec.ts | 424 +++++++++++++++++++++++++++ tests/example.spec.ts | 8 + 6 files changed, 577 insertions(+), 47 deletions(-) create mode 100644 playwright.config.ts create mode 100644 tests-examples/demo-todo-app.spec.ts create mode 100644 tests/example.spec.ts diff --git a/.gitignore b/.gitignore index 6bc492a..fdebf7e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,9 @@ tsconfig.tsbuildinfo # Jest globalConfig file -../globalConfig.json \ No newline at end of file +../globalConfig.json +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/package.json b/package.json index 3affd83..3981b4f 100644 --- a/package.json +++ b/package.json @@ -14,26 +14,31 @@ "build:lib": "cd @noctaCrdt && pnpm build", "build:client": "cd client && pnpm build", "build:server": "cd server && pnpm build", - "dev": "pnpm -r --parallel dev" + "dev": "pnpm -r --parallel dev", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:report": "playwright show-report" }, "keywords": [], "author": "", "license": "ISC", "devDependencies": { - "@noctaCrdt": "workspace:*", "@eslint/js": "^9.14.0", + "@noctaCrdt": "workspace:*", + "@playwright/test": "^1.49.1", + "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "eslint": "^8.57.1", - "eslint-plugin-jsx-a11y": "^6.8.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-prettier": "^9.0.0", + "eslint-import-resolver-typescript": "^3.6.3", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prettier": "^5.0.0", - "eslint-import-resolver-typescript": "^3.6.3", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", "prettier": "^3.0.0", "typescript": "~5.3.3" } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..cd70500 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,85 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + + timeout: 60000, + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:5173", + + viewport: { width: 1280, height: 720 }, + + screenshot: "only-on-failure", + video: "retain-on-failure", + /* 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: "pnpm dev", // 개발 서버 시작 명령어 + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e743c8..8b1e0a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@noctaCrdt': specifier: workspace:* version: link:@noctaCrdt + '@playwright/test': + specifier: ^1.49.1 + version: 1.49.1 + '@types/node': + specifier: ^20.3.1 + version: 20.17.6 '@typescript-eslint/eslint-plugin': specifier: ^7.18.0 version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint@8.57.1)(typescript@5.3.3) @@ -130,7 +136,7 @@ importers: version: 3.2.1 eslint-plugin-import: specifier: ^2.29.1 - version: 2.31.0(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1) eslint-plugin-jsx-a11y: specifier: ^6.8.0 version: 6.10.2(eslint@8.57.1) @@ -1248,6 +1254,11 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.49.1': + resolution: {integrity: sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==} + engines: {node: '>=18'} + hasBin: true + '@rollup/pluginutils@5.1.3': resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} engines: {node: '>=14.0.0'} @@ -2885,6 +2896,11 @@ 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] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4191,6 +4207,16 @@ packages: pkg-types@1.2.1: resolution: {integrity: sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==} + playwright-core@1.49.1: + resolution: {integrity: sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.49.1: + resolution: {integrity: sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -6437,6 +6463,10 @@ snapshots: '@pkgr/core@0.1.1': {} + '@playwright/test@1.49.1': + dependencies: + playwright: 1.49.1 + '@rollup/pluginutils@5.1.3(rollup@4.24.3)': dependencies: '@types/estree': 1.0.6 @@ -7219,7 +7249,7 @@ snapshots: axios@1.7.7: dependencies: - follow-redirects: 1.15.9 + follow-redirects: 1.15.9(debug@4.3.7) form-data: 4.0.1 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -8002,15 +8032,6 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): - dependencies: - debug: 3.2.7 - optionalDependencies: - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - transitivePeerDependencies: - - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.3.3))(eslint-plugin-import@2.31.0)(eslint@8.57.1))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 @@ -8040,33 +8061,6 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(eslint@8.57.1): - dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 - array.prototype.flat: 1.3.2 - array.prototype.flatmap: 1.3.2 - debug: 3.2.7 - doctrine: 2.1.0 - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.15.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.0 - semver: 6.3.1 - string.prototype.trimend: 1.0.8 - tsconfig-paths: 3.15.0 - transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - - supports-color - eslint-plugin-jsx-a11y@6.10.2(eslint@8.57.1): dependencies: aria-query: 5.3.2 @@ -8363,8 +8357,6 @@ snapshots: flatted@3.3.1: {} - follow-redirects@1.15.9: {} - follow-redirects@1.15.9(debug@4.3.7): optionalDependencies: debug: 4.3.7 @@ -8438,6 +8430,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -9916,6 +9911,14 @@ snapshots: mlly: 1.7.2 pathe: 1.1.2 + playwright-core@1.49.1: {} + + playwright@1.49.1: + dependencies: + playwright-core: 1.49.1 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.0.0: {} diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts new file mode 100644 index 0000000..5da1ade --- /dev/null +++ b/tests-examples/demo-todo-app.spec.ts @@ -0,0 +1,424 @@ +import { test, expect, type Page } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("https://demo.playwright.dev/todomvc"); +}); + +const TODO_ITEMS = ["buy some cheese", "feed the cat", "book a doctors appointment"] as const; + +test.describe("New Todo", () => { + test("should allow me to add todo items", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create 1st todo. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + // Make sure the list only has one todo item. + await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]); + + // Create 2nd todo. + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press("Enter"); + + // Make sure the list now has two todo items. + await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); + + test("should clear text input field when an item is added", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create one todo item. + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + // Check that input is empty. + await expect(newTodo).toBeEmpty(); + await checkNumberOfTodosInLocalStorage(page, 1); + }); + + test("should append new items to the bottom of the list", async ({ page }) => { + // Create 3 items. + await createDefaultTodos(page); + + // create a todo count locator + const todoCount = page.getByTestId("todo-count"); + + // Check test using different methods. + await expect(page.getByText("3 items left")).toBeVisible(); + await expect(todoCount).toHaveText("3 items left"); + await expect(todoCount).toContainText("3"); + await expect(todoCount).toHaveText(/3/); + + // Check all items in one call. + await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS); + await checkNumberOfTodosInLocalStorage(page, 3); + }); +}); + +test.describe("Mark all as completed", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test.afterEach(async ({ page }) => { + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should allow me to mark all items as completed", async ({ page }) => { + // Complete all todos. + await page.getByLabel("Mark all as complete").check(); + + // Ensure all todos have 'completed' class. + await expect(page.getByTestId("todo-item")).toHaveClass([ + "completed", + "completed", + "completed", + ]); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + }); + + test("should allow me to clear the complete state of all items", async ({ page }) => { + const toggleAll = page.getByLabel("Mark all as complete"); + // Check and then immediately uncheck. + await toggleAll.check(); + await toggleAll.uncheck(); + + // Should be no completed classes. + await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]); + }); + + test("complete all checkbox should update state when items are completed / cleared", async ({ + page, + }) => { + const toggleAll = page.getByLabel("Mark all as complete"); + await toggleAll.check(); + await expect(toggleAll).toBeChecked(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Uncheck first todo. + const firstTodo = page.getByTestId("todo-item").nth(0); + await firstTodo.getByRole("checkbox").uncheck(); + + // Reuse toggleAll locator and make sure its not checked. + await expect(toggleAll).not.toBeChecked(); + + await firstTodo.getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 3); + + // Assert the toggle all is checked again. + await expect(toggleAll).toBeChecked(); + }); +}); + +test.describe("Item", () => { + test("should allow me to mark items as complete", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + // Check first item. + const firstTodo = page.getByTestId("todo-item").nth(0); + await firstTodo.getByRole("checkbox").check(); + await expect(firstTodo).toHaveClass("completed"); + + // Check second item. + const secondTodo = page.getByTestId("todo-item").nth(1); + await expect(secondTodo).not.toHaveClass("completed"); + await secondTodo.getByRole("checkbox").check(); + + // Assert completed class. + await expect(firstTodo).toHaveClass("completed"); + await expect(secondTodo).toHaveClass("completed"); + }); + + test("should allow me to un-mark items as complete", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // Create two items. + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + const firstTodo = page.getByTestId("todo-item").nth(0); + const secondTodo = page.getByTestId("todo-item").nth(1); + const firstTodoCheckbox = firstTodo.getByRole("checkbox"); + + await firstTodoCheckbox.check(); + await expect(firstTodo).toHaveClass("completed"); + await expect(secondTodo).not.toHaveClass("completed"); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await firstTodoCheckbox.uncheck(); + await expect(firstTodo).not.toHaveClass("completed"); + await expect(secondTodo).not.toHaveClass("completed"); + await checkNumberOfCompletedTodosInLocalStorage(page, 0); + }); + + test("should allow me to edit an item", async ({ page }) => { + await createDefaultTodos(page); + + const todoItems = page.getByTestId("todo-item"); + const secondTodo = todoItems.nth(1); + await secondTodo.dblclick(); + await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue(TODO_ITEMS[1]); + await secondTodo.getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); + await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter"); + + // Explicitly assert the new text value. + await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); +}); + +test.describe("Editing", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should hide other controls when editing", async ({ page }) => { + const todoItem = page.getByTestId("todo-item").nth(1); + await todoItem.dblclick(); + await expect(todoItem.getByRole("checkbox")).not.toBeVisible(); + await expect( + todoItem.locator("label", { + hasText: TODO_ITEMS[1], + }), + ).not.toBeVisible(); + await checkNumberOfTodosInLocalStorage(page, 3); + }); + + test("should save edits on blur", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).dispatchEvent("blur"); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); + + test("should trim entered text", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(" buy some sausages "); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); + await checkTodosInLocalStorage(page, "buy some sausages"); + }); + + test("should remove the item if an empty text string was entered", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(""); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); + + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should cancel edits on escape", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).dblclick(); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); + await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Escape"); + await expect(todoItems).toHaveText(TODO_ITEMS); + }); +}); + +test.describe("Counter", () => { + test("should display the current number of todo items", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + // create a todo count locator + const todoCount = page.getByTestId("todo-count"); + + await newTodo.fill(TODO_ITEMS[0]); + await newTodo.press("Enter"); + + await expect(todoCount).toContainText("1"); + + await newTodo.fill(TODO_ITEMS[1]); + await newTodo.press("Enter"); + await expect(todoCount).toContainText("2"); + + await checkNumberOfTodosInLocalStorage(page, 2); + }); +}); + +test.describe("Clear completed button", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + }); + + test("should display the correct text", async ({ page }) => { + await page.locator(".todo-list li .toggle").first().check(); + await expect(page.getByRole("button", { name: "Clear completed" })).toBeVisible(); + }); + + test("should remove completed items when clicked", async ({ page }) => { + const todoItems = page.getByTestId("todo-item"); + await todoItems.nth(1).getByRole("checkbox").check(); + await page.getByRole("button", { name: "Clear completed" }).click(); + await expect(todoItems).toHaveCount(2); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should be hidden when there are no items that are completed", async ({ page }) => { + await page.locator(".todo-list li .toggle").first().check(); + await page.getByRole("button", { name: "Clear completed" }).click(); + await expect(page.getByRole("button", { name: "Clear completed" })).toBeHidden(); + }); +}); + +test.describe("Persistence", () => { + test("should persist its data", async ({ page }) => { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + for (const item of TODO_ITEMS.slice(0, 2)) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } + + const todoItems = page.getByTestId("todo-item"); + const firstTodoCheck = todoItems.nth(0).getByRole("checkbox"); + await firstTodoCheck.check(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(["completed", ""]); + + // Ensure there is 1 completed item. + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + // Now reload. + await page.reload(); + await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); + await expect(firstTodoCheck).toBeChecked(); + await expect(todoItems).toHaveClass(["completed", ""]); + }); +}); + +test.describe("Routing", () => { + test.beforeEach(async ({ page }) => { + await createDefaultTodos(page); + // make sure the app had a chance to save updated todos in storage + // before navigating to a new view, otherwise the items can get lost :( + // in some frameworks like Durandal + await checkTodosInLocalStorage(page, TODO_ITEMS[0]); + }); + + test("should allow me to display active items", async ({ page }) => { + const todoItem = page.getByTestId("todo-item"); + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Active" }).click(); + await expect(todoItem).toHaveCount(2); + await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); + }); + + test("should respect the back button", async ({ page }) => { + const todoItem = page.getByTestId("todo-item"); + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + + await test.step("Showing all items", async () => { + await page.getByRole("link", { name: "All" }).click(); + await expect(todoItem).toHaveCount(3); + }); + + await test.step("Showing active items", async () => { + await page.getByRole("link", { name: "Active" }).click(); + }); + + await test.step("Showing completed items", async () => { + await page.getByRole("link", { name: "Completed" }).click(); + }); + + await expect(todoItem).toHaveCount(1); + await page.goBack(); + await expect(todoItem).toHaveCount(2); + await page.goBack(); + await expect(todoItem).toHaveCount(3); + }); + + test("should allow me to display completed items", async ({ page }) => { + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Completed" }).click(); + await expect(page.getByTestId("todo-item")).toHaveCount(1); + }); + + test("should allow me to display all items", async ({ page }) => { + await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); + await checkNumberOfCompletedTodosInLocalStorage(page, 1); + await page.getByRole("link", { name: "Active" }).click(); + await page.getByRole("link", { name: "Completed" }).click(); + await page.getByRole("link", { name: "All" }).click(); + await expect(page.getByTestId("todo-item")).toHaveCount(3); + }); + + test("should highlight the currently applied filter", async ({ page }) => { + await expect(page.getByRole("link", { name: "All" })).toHaveClass("selected"); + + // create locators for active and completed links + const activeLink = page.getByRole("link", { name: "Active" }); + const completedLink = page.getByRole("link", { name: "Completed" }); + await activeLink.click(); + + // Page change - active items. + await expect(activeLink).toHaveClass("selected"); + await completedLink.click(); + + // Page change - completed items. + await expect(completedLink).toHaveClass("selected"); + }); +}); + +async function createDefaultTodos(page: Page) { + // create a new todo locator + const newTodo = page.getByPlaceholder("What needs to be done?"); + + for (const item of TODO_ITEMS) { + await newTodo.fill(item); + await newTodo.press("Enter"); + } +} + +async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return JSON.parse(localStorage["react-todos"]).length === e; + }, expected); +} + +async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { + return await page.waitForFunction((e) => { + return ( + JSON.parse(localStorage["react-todos"]).filter((todo: any) => todo.completed).length === e + ); + }, expected); +} + +async function checkTodosInLocalStorage(page: Page, title: string) { + return await page.waitForFunction((t) => { + return JSON.parse(localStorage["react-todos"]) + .map((todo: any) => todo.title) + .includes(t); + }, title); +} diff --git a/tests/example.spec.ts b/tests/example.spec.ts new file mode 100644 index 0000000..9cd83cf --- /dev/null +++ b/tests/example.spec.ts @@ -0,0 +1,8 @@ +import { test, expect } from "@playwright/test"; + +test("has title", async ({ page }) => { + await page.goto("http://localhost:5173"); + + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/Nocta/); +}); From 182e0724d973c72fe1f617bb8753018c6a8a5155 Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Sun, 12 Jan 2025 17:38:12 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20playwright=20=EC=8B=9C=EB=82=98?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=EC=9E=91=EC=84=B1=20-=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=EB=82=98=EB=A6=AC=EC=98=A4=20-?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94=EA=B0=80=20-=20?= =?UTF-8?q?=EB=B8=94=EB=A1=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/features/auth/AuthButton.tsx | 4 +- playwright.config.ts | 35 +++--- tests/auth.spec.ts | 146 ++++++++++++++++++++++++ tests/example.spec.ts | 8 -- tests/markdown.spec.ts | 88 ++++++++++++++ 5 files changed, 254 insertions(+), 27 deletions(-) create mode 100644 tests/auth.spec.ts delete mode 100644 tests/example.spec.ts create mode 100644 tests/markdown.spec.ts diff --git a/client/src/features/auth/AuthButton.tsx b/client/src/features/auth/AuthButton.tsx index 3f500ed..51e3ea2 100644 --- a/client/src/features/auth/AuthButton.tsx +++ b/client/src/features/auth/AuthButton.tsx @@ -26,11 +26,11 @@ export const AuthButton = () => { return (
{isLogin ? ( - + 로그아웃 ) : ( - + 로그인 )} diff --git a/playwright.config.ts b/playwright.config.ts index cd70500..39ef032 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -25,34 +25,38 @@ export default defineConfig({ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - timeout: 60000, + timeout: 10000, use: { /* Base URL to use in actions like `await page.goto('/')`. */ baseURL: "http://localhost:5173", viewport: { width: 1280, height: 720 }, - screenshot: "only-on-failure", - video: "retain-on-failure", + screenshot: "on", + video: "on", /* 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: "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"] }, + name: "Google Chrome", + use: { ...devices["Desktop Chrome"], channel: "chrome" }, }, /* Test against mobile viewports. */ @@ -70,10 +74,6 @@ export default defineConfig({ // 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 */ @@ -81,5 +81,6 @@ export default defineConfig({ command: "pnpm dev", // 개발 서버 시작 명령어 url: "http://localhost:5173", reuseExistingServer: !process.env.CI, + timeout: 15000, }, }); diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts new file mode 100644 index 0000000..80b935a --- /dev/null +++ b/tests/auth.spec.ts @@ -0,0 +1,146 @@ +// import { test, expect } from "@playwright/test"; + +// test.describe("유저 로그인 및 회원가입", () => { +// test.beforeEach(async ({ page }) => { +// await page.goto("/"); +// await onBoarding(page); +// }); +// }); + +import { test, expect } from "@playwright/test"; + +// 테스트에 필요한 사용자 데이터 +const TEST_USER = { + name: "테스트 사용자", + email: `test${Date.now()}@example.com`, + password: "test1234", +}; + +const onBoarding = async (page) => { + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); +}; + +test.describe("인증 테스트", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await onBoarding(page); + }); + + test("회원가입 성공", async ({ page }) => { + // 로그인 버튼 클릭 + await page.getByRole("button", { name: "로그인" }).click(); + + // 회원가입 모드로 전환 + await page.getByText("계정이 없으신가요? 회원가입하기").click(); + + // 폼 입력 + await page.getByPlaceholder("이름").fill(TEST_USER.name); + await page.getByPlaceholder("이메일").fill(TEST_USER.email); + await page.getByPlaceholder("비밀번호").fill(TEST_USER.password); + + // 회원가입 버튼 클릭 + await page.getByRole("button", { name: "회원가입" }).click(); + + // 로그아웃 확인 모달에서 로그아웃 버튼 클릭 + await page.getByRole("button", { name: "로그아웃" }).click(); + + // 로그아웃 상태 확인 + await page.waitForSelector('button:text("로그인")'); + await expect(page.getByRole("button", { name: "로그인" })).toBeVisible(); + }); + + test("로그인 성공", async ({ page }) => { + // 로그인 버튼 클릭 + // await page.getByRole("button", { name: "로그인" }).click(); + await page.click('[data-onboarding="login-button"]'); + + // 폼 입력 + await page.getByPlaceholder("이메일").fill(TEST_USER.email); + await page.getByPlaceholder("비밀번호").fill(TEST_USER.password); + + // 로그인 버튼 클릭 + // await page.getByRole("button", { name: "로그인" }).click(); + await page.click( + ".glassContainer.glassContainer--border_lg.bdr_md.py_4px.cursor_pointer.bg_transparent", + ); + + // 로그인 성공 확인 + // await expect(page.getByRole("button", { name: "로그아웃" })).toBeVisible(); + await page.waitForSelector('[data-onboarding="login-button"]'); + await expect(page.locator('[data-onboarding="login-button"]')); + }); + + test("로그인 실패 - 잘못된 비밀번호", async ({ page }) => { + // 로그인 버튼 클릭 + await page.getByRole("button", { name: "로그인" }).click(); + + // 잘못된 정보 입력 + await page.getByPlaceholder("이메일").fill(TEST_USER.email); + await page.getByPlaceholder("비밀번호").fill("wrong_password"); + + // 로그인 버튼 클릭 + await page.getByRole("button", { name: "로그인" }).click(); + + // 에러 메시지 확인 + await expect(page.getByText("이메일 또는 비밀번호가 올바르지 않습니다.")).toBeVisible(); + + // 취소 버튼으로 모달 닫기 + await page.getByRole("button", { name: "취소" }).click(); + + // 모달이 닫혔는지 확인 + await expect(page.getByText("Login")).not.toBeVisible(); + }); + + test("이메일 형식 검증", async ({ page }) => { + // 로그인 버튼 클릭 + await page.getByRole("button", { name: "로그인" }).click(); + + // 잘못된 이메일 형식 입력 + await page.getByPlaceholder("이메일").fill("invalid-email"); + await page.getByPlaceholder("비밀번호").fill(TEST_USER.password); + + // 로그인 버튼 클릭 + await page.getByRole("button", { name: "로그인" }).click(); + + // 에러 메시지 확인 + await expect(page.getByText("올바른 이메일 형식이 아닙니다.")).toBeVisible(); + + // 취소 버튼으로 모달 닫기 + await page.getByRole("button", { name: "취소" }).click(); + + // 모달이 닫혔는지 확인 + await expect(page.getByText("Login")).not.toBeVisible(); + }); + + test("로그아웃", async ({ page }) => { + // 먼저 로그인 + await page.getByRole("button", { name: "로그인" }).click(); + await page.getByPlaceholder("이메일").fill(TEST_USER.email); + await page.getByPlaceholder("비밀번호").fill(TEST_USER.password); + await page.getByRole("button", { name: "로그인" }).click(); + + // 로그아웃 버튼 클릭 + await page.getByRole("button", { name: "로그아웃" }).click(); + + // 로그아웃 확인 모달에서 로그아웃 버튼 클릭 + await page.getByRole("button", { name: "로그아웃" }).click(); + + // 로그아웃 상태 확인 + await expect(page.getByRole("button", { name: "로그인" })).toBeVisible(); + }); + + test("모달 닫기", async ({ page }) => { + // 로그인 모달 열기 + await page.getByRole("button", { name: "로그인" }).click(); + + // 취소 버튼으로 모달 닫기 + await page.getByRole("button", { name: "취소" }).click(); + + // 모달이 닫혔는지 확인 + await expect(page.getByText("Login")).not.toBeVisible(); + }); +}); diff --git a/tests/example.spec.ts b/tests/example.spec.ts deleted file mode 100644 index 9cd83cf..0000000 --- a/tests/example.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("has title", async ({ page }) => { - await page.goto("http://localhost:5173"); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Nocta/); -}); diff --git a/tests/markdown.spec.ts b/tests/markdown.spec.ts new file mode 100644 index 0000000..b2035db --- /dev/null +++ b/tests/markdown.spec.ts @@ -0,0 +1,88 @@ +import { test, expect } from "@playwright/test"; + +const onBoarding = async (page) => { + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); + await page.click(".hover\\:bg-c_purple\\.600"); +}; + +const createNewPage = async (page) => { + const pageListSelector = ".d_flex.pos_relative.gap_lg.ai_center"; + const pageCount = await page.locator(pageListSelector).count(); + if (pageCount === 0) { + await page.click('[data-onboarding="page-add-button"]'); + } + await page.waitForSelector(pageListSelector); +}; + +const openPage = async (page) => { + await page.click(".d_flex.pos_relative.gap_lg.ai_center"); +}; + +const addNewBlock = async (page) => { + const blockSelector = + ".d_flex.gap_spacing\\.sm.bdr_4px.p_spacing\\.sm.c_gray\\.900.op_0\\.8.cursor_pointer"; + await page.click(blockSelector); + await page.locator(".textStyle_display-medium16"); +}; + +test.describe("마크다운 에디터 테스트", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await onBoarding(page); + }); + + test("페이지 추가", async ({ page }) => { + await createNewPage(page); + page.locator(".d_flex.pos_relative.gap_lg.ai_center.w_100%.h_56px.px_md"); + }); + + test("마크다운 블록 추가", async ({ page }) => { + await openPage(page); + await addNewBlock(page); + page.locator(".textStyle_display-medium16"); + // // 헤더 테스트 + // await page.keyboard.type("# 헤더1"); + // await page.keyboard.press("Space"); + // await expect(page.locator(".textStyle_display-medium24")).toHaveText("헤더1"); + // await page.keyboard.type("## 헤더2"); + // await page.keyboard.press("Enter"); + // await expect(page.locator(".textStyle_display-medium20")).toHaveText("헤더2"); + // 리스트 테스트 + // 순서 있는 리스트 + // await page.keyboard.type("1. 첫번째"); + // await page.keyboard.press("Enter"); + // await page.keyboard.type("2. 두번째"); + // await expect(page.locator("ol > li")).toHaveCount(2); + // // 순서 없는 리스트 + // await page.keyboard.type("- 항목1"); + // await page.keyboard.press("Enter"); + // await page.keyboard.type("- 항목2"); + // await expect(page.locator("ul > li")).toHaveCount(2); + // // 체크박스 + // await page.keyboard.type("- [ ] 할일1"); + // await page.keyboard.press("Enter"); + // await page.keyboard.type("- [x] 완료된 일"); + // const checkboxes = page.locator('input[type="checkbox"]'); + // await expect(checkboxes).toHaveCount(2); + // // 인용구 + // await page.keyboard.type("> 인용문"); + // await page.keyboard.press("Enter"); + // await expect(page.locator("blockquote")).toHaveText("인용문"); + }); + + // test("마크다운 문법 변환 테스트", async ({ page }) => { + // await createNewPage(page); + // await openPage(page); + // await addNewBlock(page); + // // 헤더 테스트 + // await page.keyboard.type("# 헤더1"); + // await page.keyboard.press("Space"); + // await expect(page.locator(".textStyle_display-medium24.flex_1_1_auto")).toHaveText("헤더1"); + // await page.keyboard.type("## 헤더2"); + // await page.keyboard.press("Enter"); + // await expect(page.locator(".textStyle_display-medium20.flex_1_1_auto")).toHaveText("헤더2"); + // }); +}); From c26d0cae43149ae9510ffb61035f691ce09ad23d Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Sun, 12 Jan 2025 18:33:16 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=EB=B3=84=20testId=20=EC=A0=80=EC=9E=A5=20-=20BottomNa?= =?UTF-8?q?vigator=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=B2=84=ED=8A=BC(icon?= =?UTF-8?q?Button-{idx})=20-=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B2=84=ED=8A=BC(addPageButton)=20-=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EB=AA=A8=EB=8B=AC=20=EB=B2=84=ED=8A=BC(mo?= =?UTF-8?q?dalPrimaryButton,=20modalSecondaryButton)=20-=20=EC=82=AC?= =?UTF-8?q?=EC=9D=B4=EB=93=9C=EB=B0=94=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=97=B4=EA=B8=B0=20=EB=B2=84=ED=8A=BC(pageItem-{idx})=20-=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=95=84=EC=9D=B4=EC=BD=98=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC(pageItem-{idx}-pageIconButton)=20-=20?= =?UTF-8?q?=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B2=84=ED=8A=BC(sidebarLoginButton,=20sidebarLog?= =?UTF-8?q?outButton)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/bottomNavigator/BottomNavigator.tsx | 3 ++- client/src/components/button/IconButton.tsx | 4 +++- client/src/components/button/textButton.tsx | 9 ++++++++- client/src/components/modal/modal.tsx | 10 ++++++++-- client/src/components/sidebar/Sidebar.tsx | 10 ++++++++-- .../components/pageIconButton/PageIconButton.tsx | 5 +++-- .../components/pageIconButton/PageIconModal.tsx | 3 ++- .../sidebar/components/pageItem/PageItem.tsx | 10 ++++++++-- client/src/features/auth/AuthButton.tsx | 4 ++-- 9 files changed, 44 insertions(+), 14 deletions(-) diff --git a/client/src/components/bottomNavigator/BottomNavigator.tsx b/client/src/components/bottomNavigator/BottomNavigator.tsx index 8e22e7b..618713d 100644 --- a/client/src/components/bottomNavigator/BottomNavigator.tsx +++ b/client/src/components/bottomNavigator/BottomNavigator.tsx @@ -17,7 +17,7 @@ export const BottomNavigator = ({ }: BottomNavigatorProps) => { return (
- {pages.map((page) => ( + {pages.map((page, idx) => ( { handlePageSelect({ pageId: page.id, diff --git a/client/src/components/button/IconButton.tsx b/client/src/components/button/IconButton.tsx index ddd1b90..47ca2a5 100644 --- a/client/src/components/button/IconButton.tsx +++ b/client/src/components/button/IconButton.tsx @@ -5,14 +5,16 @@ import { iconButtonContainer } from "./IconButton.style"; interface IconButtonProps { icon: PageIconType | "plus"; size: "sm" | "md"; + testKey: string; onClick?: () => void; } -export const IconButton = ({ icon, size, onClick }: IconButtonProps) => { +export const IconButton = ({ icon, size, testKey, onClick }: IconButtonProps) => { const { icon: IconComponent, color: defaultColor }: IconConfig = iconComponents[icon]; return (
{secondaryButtonLabel && ( - + {secondaryButtonLabel} )} - {primaryButtonLabel} + + {primaryButtonLabel} +
diff --git a/client/src/components/sidebar/Sidebar.tsx b/client/src/components/sidebar/Sidebar.tsx index 0dc04dc..104157a 100644 --- a/client/src/components/sidebar/Sidebar.tsx +++ b/client/src/components/sidebar/Sidebar.tsx @@ -100,7 +100,7 @@ export const Sidebar = ({ {pages.length === 0 ? (
+ 버튼을 눌러 페이지를 추가하세요
) : ( - pages?.map((item) => ( + pages?.map((item, idx) => ( handlePageItemClick(item.id)} onDelete={() => confirmPageDelete(item)} handleIconUpdate={handlePageUpdate} @@ -118,7 +119,12 @@ export const Sidebar = ({ )} - + diff --git a/client/src/components/sidebar/components/pageIconButton/PageIconButton.tsx b/client/src/components/sidebar/components/pageIconButton/PageIconButton.tsx index 52d8e22..9a7ebb0 100644 --- a/client/src/components/sidebar/components/pageIconButton/PageIconButton.tsx +++ b/client/src/components/sidebar/components/pageIconButton/PageIconButton.tsx @@ -4,14 +4,15 @@ import { IconBox } from "./PageIconButton.style"; interface PageIconButtonProps { type: PageIconType; + testKey: string; onClick: (e: React.MouseEvent) => void; } -export const PageIconButton = ({ type, onClick }: PageIconButtonProps) => { +export const PageIconButton = ({ type, testKey, onClick }: PageIconButtonProps) => { const { icon: IconComponent, color: defaultColor }: IconConfig = iconComponents[type]; return ( -
+
onClick(e)}>
diff --git a/client/src/components/sidebar/components/pageIconButton/PageIconModal.tsx b/client/src/components/sidebar/components/pageIconButton/PageIconModal.tsx index d36c0f0..00238c4 100644 --- a/client/src/components/sidebar/components/pageIconButton/PageIconModal.tsx +++ b/client/src/components/sidebar/components/pageIconButton/PageIconModal.tsx @@ -15,7 +15,7 @@ export const PageIconModal = ({ onClose, onSelect, currentType }: PageIconModalP return (
-
@@ -39,6 +39,7 @@ export const PageIconModal = ({ onClose, onSelect, currentType }: PageIconModalP return (
diff --git a/client/src/components/sidebar/components/pageItem/PageItem.tsx b/client/src/components/sidebar/components/pageItem/PageItem.tsx index 9bd4e0d..fb0f5f7 100644 --- a/client/src/components/sidebar/components/pageItem/PageItem.tsx +++ b/client/src/components/sidebar/components/pageItem/PageItem.tsx @@ -63,14 +63,18 @@ export const PageItem = ({ }; return ( -
+
{title || "새로운 페이지"} - + {isOpen && ( diff --git a/client/src/features/auth/AuthModal.tsx b/client/src/features/auth/AuthModal.tsx index f04e157..02a02b7 100644 --- a/client/src/features/auth/AuthModal.tsx +++ b/client/src/features/auth/AuthModal.tsx @@ -187,7 +187,7 @@ export const AuthModal = ({ isOpen, onClose }: AuthModalProps) => {

)}
- - -
diff --git a/tests/page.spec.ts b/tests/page.spec.ts index fea8d7b..5d9b055 100644 --- a/tests/page.spec.ts +++ b/tests/page.spec.ts @@ -10,7 +10,25 @@ const onBoarding = async (page: Page) => { test.describe.configure({ mode: "serial" }); -test.describe("페이지 테스트", () => { +test.describe("사이드바 테스트", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await onBoarding(page); + }); + + test("사이드바 토글", async ({ page }) => { + await expect(page.getByTestId("sidebarToggle")).toBeVisible(); + await expect(page.getByTestId("sidebarToggle")).toBeEnabled(); + + await page.getByTestId("sidebarToggle").click(); + await expect(page.getByTestId("sidebar")).toHaveCSS("width", "40px"); + + await page.getByTestId("sidebarToggle").click(); + await expect(page.getByTestId("sidebar")).toHaveCSS("width", "300px"); + }); +}); + +test.describe("사이드바 페이지 테스트", () => { test.beforeEach(async ({ page }) => { await page.goto("/"); await onBoarding(page); @@ -86,38 +104,8 @@ test.describe("페이지 테스트", () => { // 개수가 1 감소했는지 확인 expect(newPages).toBe(initialPages - 1); }); -}); - -test.describe("페이지 아이콘 테스트", () => { - test.beforeEach(async ({ page }) => { - await page.goto("/"); - await onBoarding(page); - }); - - test.afterEach(async ({ page }) => { - const pageCount = await page.locator('[data-testid^="pageItem-"]').count(); - - if (pageCount === 0) { - return; - } - - for (let i = pageCount - 1; i >= 0; i--) { - // 먼저 페이지 아이템에 hover - await page.getByTestId(`pageItem-${i}`).hover(); - - // 삭제 버튼이 보일 때까지 대기 - await page.getByTestId(`pageDeleteButton-${i}`).waitFor({ state: "visible" }); - - // 이제 삭제 버튼 클릭 - await page.getByTestId(`pageDeleteButton-${i}`).click(); - // 확인 모달의 확인 버튼 클릭 - await page.getByTestId("modalPrimaryButton").click(); - await expect(page.getByTestId(`pageItem-${i}`)).not.toBeVisible(); - } - }); - - test("아이콘 변경", async ({ page }) => { + test("페이지 아이콘 변경", async ({ page }) => { // 페이지 추가 await page.getByTestId("addPageButton").click(); await page.waitForLoadState("networkidle"); @@ -145,20 +133,89 @@ test.describe("페이지 아이콘 테스트", () => { }); }); -test.describe("사이드바 토글 테스트", () => { +test.describe("페이지 테스트", () => { test.beforeEach(async ({ page }) => { await page.goto("/"); await onBoarding(page); + await page.getByTestId("addPageButton").click(); + await page.waitForLoadState("networkidle"); + // 페이지 열기 + const targetIndex = (await page.locator('[data-testid^="pageItem-"]').count()) - 1; + await page.getByTestId(`pageItem-${targetIndex}`).click(); }); - test("사이드바 토글", async ({ page }) => { - await expect(page.getByTestId("sidebarToggle")).toBeVisible(); - await expect(page.getByTestId("sidebarToggle")).toBeEnabled(); + test.afterEach(async ({ page }) => { + const pageCount = await page.locator('[data-testid^="pageItem-"]').count(); - await page.getByTestId("sidebarToggle").click(); - await expect(page.getByTestId("sidebar")).toHaveCSS("width", "40px"); + if (pageCount === 0) { + return; + } - await page.getByTestId("sidebarToggle").click(); - await expect(page.getByTestId("sidebar")).toHaveCSS("width", "300px"); + for (let i = pageCount - 1; i >= 0; i--) { + // 먼저 페이지 아이템에 hover + await page.getByTestId(`pageItem-${i}`).hover(); + await page.getByTestId(`pageDeleteButton-${i}`).waitFor({ state: "visible" }); + // 이제 삭제 버튼 클릭 + await page.getByTestId(`pageDeleteButton-${i}`).click(); + // 확인 모달의 확인 버튼 클릭 + await page.getByTestId("modalPrimaryButton").click(); + await expect(page.getByTestId(`pageItem-${i}`)).not.toBeVisible(); + } + }); + + test("페이지 최소화", async ({ page }) => { + const targetIndex = (await page.locator('[data-testid^="page-"]').count()) - 1; + await page.getByTestId(`pageMinimizeButton-${targetIndex}`).click(); + await expect(page.getByTestId(`page-${targetIndex}`)).toHaveCSS("width", "300px"); + await expect(page.getByTestId(`page-${targetIndex}`)).toHaveCSS("height", "200px"); + }); + + test("페이지 최대화", async ({ page }) => { + const targetIndex = (await page.locator('[data-testid^="page-"]').count()) - 1; + + // 초기 위치와 크기 저장 + const initialSize = await page.getByTestId(`page-${targetIndex}`).evaluate((el) => { + const style = window.getComputedStyle(el); + return { + width: style.width, + height: style.height, + }; + }); + + // 최대화 버튼 클릭 + await page.getByTestId(`pageMaximizeButton-${targetIndex}`).click(); + + // 창 크기에 맞게 최대화되었는지 확인 + const maximizedSize = await page.getByTestId(`page-${targetIndex}`).evaluate(() => { + const sidebarWidth = document.querySelector('[data-testid="sidebar"]')?.clientWidth || 0; + const padding = 40; // PADDING 상수값 + console.log({ + windowWidth: window.innerWidth, + sidebarWidth, + padding, + calculatedWidth: window.innerWidth - sidebarWidth - padding, + }); + // 2를 빼야함 + // glassContainer에 border가 있어서 1px + 1px 총 2px를 빼야함 + return { + width: `${window.innerWidth - sidebarWidth - padding - 2}px`, + height: `${window.innerHeight - padding}px`, + }; + }); + + await expect(page.getByTestId(`page-${targetIndex}`)).toHaveCSS("width", maximizedSize.width); + await expect(page.getByTestId(`page-${targetIndex}`)).toHaveCSS("height", maximizedSize.height); + + // 다시 최대화 버튼 클릭 (원래 크기로 복귀) + await page.getByTestId(`pageMaximizeButton-${targetIndex}`).click(); + + await expect(page.getByTestId(`page-${targetIndex}`)).toHaveCSS("width", initialSize.width); + await expect(page.getByTestId(`page-${targetIndex}`)).toHaveCSS("height", initialSize.height); + }); + + test("페이지 닫기", async ({ page }) => { + const targetIndex = (await page.locator('[data-testid^="pageItem-"]').count()) - 1; + await page.getByTestId(`pageCloseButton-${targetIndex}`).click(); + await expect(page.getByTestId(`page-${targetIndex}`)).not.toBeVisible(); }); }); From 51c342d188cf2b080eb809754192cd5e70778675 Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Mon, 13 Jan 2025 00:14:15 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20=EC=97=90=EB=94=94=ED=84=B0=20pla?= =?UTF-8?q?ywright=20=EC=84=A0=ED=83=9D=EC=9E=90=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=20=20-=20=EC=82=AC=EC=9D=B4=EB=93=9C=EB=B0=94=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=9C=EB=AA=A9(sidebarTitle-{idx})=20?= =?UTF-8?q?=20=20-=20=EC=97=90=EB=94=94=ED=84=B0(editor-{idx})=20=20=20-?= =?UTF-8?q?=20=EB=B8=94=EB=A1=9D(block-{idx})=20=20=20-=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EA=B0=80=20=EB=B2=84=ED=8A=BC(addNewBlock?= =?UTF-8?q?Button)=20=20=20-=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D(iconBlock)=20=20=20-=20=ED=85=8D=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=98=81=EC=97=AD(contentEditable)=20=20=20-=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=A0=9C=EB=AA=A9(pageTitle-{idx})?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sidebar/components/pageItem/PageItem.tsx | 4 +++- client/src/features/editor/Editor.tsx | 17 +++++++++++++---- .../editor/components/IconBlock/IconBlock.tsx | 8 +++++++- .../features/editor/components/block/Block.tsx | 6 +++++- client/src/features/page/Page.tsx | 3 ++- .../page/components/PageTitle/PageTitle.tsx | 7 +++++-- 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/client/src/components/sidebar/components/pageItem/PageItem.tsx b/client/src/components/sidebar/components/pageItem/PageItem.tsx index fb0f5f7..aea6eb6 100644 --- a/client/src/components/sidebar/components/pageItem/PageItem.tsx +++ b/client/src/components/sidebar/components/pageItem/PageItem.tsx @@ -69,7 +69,9 @@ export const PageItem = ({ type={pageIcon ?? "Docs"} onClick={handleToggleModal} /> - {title || "새로운 페이지"} + + {title || "새로운 페이지"} + void; pageId: string; serializedEditorData: serializedEditorDataProps; pageTitle: string; } -export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData }: EditorProps) => { +export const Editor = ({ + testKey, + onTitleChange, + pageId, + pageTitle, + serializedEditorData, +}: EditorProps) => { const { sendCharInsertOperation, sendCharDeleteOperation, @@ -343,9 +350,10 @@ export const Editor = ({ onTitleChange, pageId, pageTitle, serializedEditorData return
Loading editor data...
; } return ( -
+
`${block.id.client}-${block.id.clock}`)} strategy={verticalListSortingStrategy} > - {editorState.linkedList.spread().map((block) => ( + {editorState.linkedList.spread().map((block, idx) => ( {editorState.linkedList.spread().length === 0 && ( -
+
클릭해서 새로운 블록을 추가하세요
)} diff --git a/client/src/features/editor/components/IconBlock/IconBlock.tsx b/client/src/features/editor/components/IconBlock/IconBlock.tsx index 61c0632..167fa7b 100644 --- a/client/src/features/editor/components/IconBlock/IconBlock.tsx +++ b/client/src/features/editor/components/IconBlock/IconBlock.tsx @@ -2,6 +2,7 @@ import { ElementType } from "@noctaCrdt/Interfaces"; import { iconContainerStyle, iconStyle } from "./IconBlock.style"; interface IconBlockProps { + testKey: string; type: ElementType; index: number | undefined; indent?: number; @@ -10,6 +11,7 @@ interface IconBlockProps { } export const IconBlock = ({ + testKey, type, index = 1, indent = 0, @@ -48,5 +50,9 @@ export const IconBlock = ({ const icon = getIcon(); if (!icon) return null; - return
{icon}
; + return ( +
+ {icon} +
+ ); }; diff --git a/client/src/features/editor/components/block/Block.tsx b/client/src/features/editor/components/block/Block.tsx index 635181f..e861367 100644 --- a/client/src/features/editor/components/block/Block.tsx +++ b/client/src/features/editor/components/block/Block.tsx @@ -27,6 +27,7 @@ import { } from "./Block.style"; interface BlockProps { + testKey: string; id: string; block: CRDTBlock; dragBlockList: string[]; @@ -70,6 +71,7 @@ interface BlockProps { } export const Block: React.FC = memo( ({ + testKey, id, block, dragBlockList, @@ -273,7 +275,7 @@ export const Block: React.FC = memo( return ( // TODO: eslint 규칙을 수정해야 할까? // TODO: ol일때 index 순서 처리 -
+
{showTopIndicator && } = memo( /> = memo( onCheckboxClick={handleCheckboxClick} />
handleKeyDown(e, blockRef.current, block)} onInput={handleInput} diff --git a/client/src/features/page/Page.tsx b/client/src/features/page/Page.tsx index 75496e0..03b3435 100644 --- a/client/src/features/page/Page.tsx +++ b/client/src/features/page/Page.tsx @@ -77,7 +77,7 @@ export const Page = ({ onPointerDown={handlePageClick} >
- +
{ +export const PageTitle = ({ testKey, title, icon }: PageTitleProps) => { const { icon: IconComponent, color } = iconComponents[icon]; return (
-

{title || "Title"}

+

+ {title || "Title"} +

); }; From 19a38598513735bc9e811d023f2b72d6035aed47 Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Mon, 13 Jan 2025 00:14:58 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:=20=EB=A7=88=ED=81=AC=EB=8B=A4?= =?UTF-8?q?=EC=9A=B4=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8B=9C=EB=82=98?= =?UTF-8?q?=EB=A6=AC=EC=98=A4=20=EC=9E=91=EC=84=B1=20=20=20-=20=EB=B8=94?= =?UTF-8?q?=EB=A1=9D=20=EC=B6=94=EA=B0=80=20=20=20-=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EC=A0=9C=EB=AA=A9=20=EB=B3=80=EA=B2=BD=20=20=20-?= =?UTF-8?q?=20=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4=20=EB=AC=B8=EB=B2=95=20?= =?UTF-8?q?=EB=B3=80=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/markdown.spec.ts | 212 ++++++++++++++++++++++++++++------------- 1 file changed, 147 insertions(+), 65 deletions(-) diff --git a/tests/markdown.spec.ts b/tests/markdown.spec.ts index b2035db..90d37bf 100644 --- a/tests/markdown.spec.ts +++ b/tests/markdown.spec.ts @@ -1,4 +1,5 @@ import { test, expect } from "@playwright/test"; +import { escape } from "querystring"; const onBoarding = async (page) => { await page.click(".hover\\:bg-c_purple\\.600"); @@ -8,81 +9,162 @@ const onBoarding = async (page) => { await page.click(".hover\\:bg-c_purple\\.600"); }; -const createNewPage = async (page) => { - const pageListSelector = ".d_flex.pos_relative.gap_lg.ai_center"; - const pageCount = await page.locator(pageListSelector).count(); - if (pageCount === 0) { - await page.click('[data-onboarding="page-add-button"]'); - } - await page.waitForSelector(pageListSelector); -}; - -const openPage = async (page) => { - await page.click(".d_flex.pos_relative.gap_lg.ai_center"); -}; - -const addNewBlock = async (page) => { - const blockSelector = - ".d_flex.gap_spacing\\.sm.bdr_4px.p_spacing\\.sm.c_gray\\.900.op_0\\.8.cursor_pointer"; - await page.click(blockSelector); - await page.locator(".textStyle_display-medium16"); -}; +test.describe.configure({ mode: "serial" }); test.describe("마크다운 에디터 테스트", () => { test.beforeEach(async ({ page }) => { await page.goto("/"); await onBoarding(page); + await page.getByTestId("addPageButton").click(); + await page.waitForLoadState("networkidle"); + await page.getByTestId("pageItem-0").click(); + expect(page.getByTestId("page-0")).toBeVisible(); }); - test("페이지 추가", async ({ page }) => { - await createNewPage(page); - page.locator(".d_flex.pos_relative.gap_lg.ai_center.w_100%.h_56px.px_md"); + test.afterEach(async ({ page }) => { + const pageCount = await page.locator('[data-testid^="pageItem-"]').count(); + + if (pageCount === 0) { + return; + } + + for (let i = pageCount - 1; i >= 0; i--) { + // 먼저 페이지 아이템에 hover + await page.getByTestId(`pageItem-${i}`).hover(); + await page.getByTestId(`pageDeleteButton-${i}`).waitFor({ state: "visible" }); + // 이제 삭제 버튼 클릭 + await page.getByTestId(`pageDeleteButton-${i}`).click(); + // 확인 모달의 확인 버튼 클릭 + await page.getByTestId("modalPrimaryButton").click(); + await expect(page.getByTestId(`pageItem-${i}`)).not.toBeVisible(); + } }); test("마크다운 블록 추가", async ({ page }) => { - await openPage(page); - await addNewBlock(page); - page.locator(".textStyle_display-medium16"); - // // 헤더 테스트 - // await page.keyboard.type("# 헤더1"); - // await page.keyboard.press("Space"); - // await expect(page.locator(".textStyle_display-medium24")).toHaveText("헤더1"); - // await page.keyboard.type("## 헤더2"); - // await page.keyboard.press("Enter"); - // await expect(page.locator(".textStyle_display-medium20")).toHaveText("헤더2"); - // 리스트 테스트 - // 순서 있는 리스트 - // await page.keyboard.type("1. 첫번째"); - // await page.keyboard.press("Enter"); - // await page.keyboard.type("2. 두번째"); - // await expect(page.locator("ol > li")).toHaveCount(2); - // // 순서 없는 리스트 - // await page.keyboard.type("- 항목1"); - // await page.keyboard.press("Enter"); - // await page.keyboard.type("- 항목2"); - // await expect(page.locator("ul > li")).toHaveCount(2); - // // 체크박스 - // await page.keyboard.type("- [ ] 할일1"); - // await page.keyboard.press("Enter"); - // await page.keyboard.type("- [x] 완료된 일"); - // const checkboxes = page.locator('input[type="checkbox"]'); - // await expect(checkboxes).toHaveCount(2); - // // 인용구 - // await page.keyboard.type("> 인용문"); - // await page.keyboard.press("Enter"); - // await expect(page.locator("blockquote")).toHaveText("인용문"); + // 현재 열린 페이지 + const currentEditor = page.getByTestId("editor-0"); + // 블록 추가 버튼 클릭 + const addNewBlockButton = currentEditor.getByTestId("addNewBlockButton"); + await addNewBlockButton.click(); + + // 블록 추가 확인 + await expect(currentEditor.getByTestId("block-0")).toBeVisible(); + // 블록에 포커스 되었는지 확인 + const contentEditable = currentEditor.getByTestId("block-0").getByTestId("contentEditable"); + await contentEditable.click(); + expect(contentEditable).toBeFocused(); }); - // test("마크다운 문법 변환 테스트", async ({ page }) => { - // await createNewPage(page); - // await openPage(page); - // await addNewBlock(page); - // // 헤더 테스트 - // await page.keyboard.type("# 헤더1"); - // await page.keyboard.press("Space"); - // await expect(page.locator(".textStyle_display-medium24.flex_1_1_auto")).toHaveText("헤더1"); - // await page.keyboard.type("## 헤더2"); - // await page.keyboard.press("Enter"); - // await expect(page.locator(".textStyle_display-medium20.flex_1_1_auto")).toHaveText("헤더2"); - // }); + test("페이지 제목 변경", async ({ page }) => { + const editorTitle = page.getByTestId("editorTitle-0"); + const pageTitle = page.getByTestId("pageTitle-0"); + const sidebarTitle = page.getByTestId("sidebarTitle-0"); + await editorTitle.click(); + await page.keyboard.type("페이지 제목"); + expect(pageTitle).toHaveText("페이지 제목"); + expect(sidebarTitle).toHaveText("페이지 제목"); + }); +}); + +test.describe("마크다운 문법 변환 테스트", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await onBoarding(page); + await page.getByTestId("addPageButton").click(); + await page.waitForLoadState("networkidle"); + await page.getByTestId("pageItem-0").click(); + expect(page.getByTestId("page-0")).toBeVisible(); + const currentEditor = page.getByTestId("editor-0"); + const addNewBlockButton = currentEditor.getByTestId("addNewBlockButton"); + await addNewBlockButton.click(); + const contentEditable = page.getByTestId("block-0").getByTestId("contentEditable"); + await contentEditable.click(); + }); + + test.afterEach(async ({ page }) => { + const pageCount = await page.locator('[data-testid^="pageItem-"]').count(); + + if (pageCount === 0) { + return; + } + + for (let i = pageCount - 1; i >= 0; i--) { + // 먼저 페이지 아이템에 hover + await page.getByTestId(`pageItem-${i}`).hover(); + await page.getByTestId(`pageDeleteButton-${i}`).waitFor({ state: "visible" }); + // 이제 삭제 버튼 클릭 + await page.getByTestId(`pageDeleteButton-${i}`).click(); + // 확인 모달의 확인 버튼 클릭 + await page.getByTestId("modalPrimaryButton").click(); + await expect(page.getByTestId(`pageItem-${i}`)).not.toBeVisible(); + } + }); + + test("h1 헤더 변환", async ({ page }) => { + const contentEditable = page.getByTestId("block-0").getByTestId("contentEditable"); + await page.keyboard.type("# 헤더1"); + await expect(contentEditable).toHaveText("헤더1"); + await expect(contentEditable).toHaveClass(/textStyle_display-medium24/); + }); + + test("h2 헤더 변환", async ({ page }) => { + const contentEditable = page.getByTestId("block-0").getByTestId("contentEditable"); + await page.keyboard.type("## 헤더2"); + await expect(contentEditable).toHaveText("헤더2"); + await expect(contentEditable).toHaveClass(/textStyle_display-medium20/); + }); + + test("h3 헤더 변환", async ({ page }) => { + const contentEditable = page.getByTestId("block-0").getByTestId("contentEditable"); + await page.keyboard.type("### 헤더3"); + await expect(contentEditable).toHaveText("헤더3"); + await expect(contentEditable).toHaveClass(/textStyle_display-medium16/); + }); + + test("순서 있는 리스트 변환", async ({ page }) => { + await page.keyboard.type("1. 첫번째"); + await page.keyboard.press("Enter"); + await page.keyboard.type("2. 두번째"); + const blocks = await page.locator('[data-testid^="block-"]').all(); + expect(blocks.length).toBe(2); + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + const iconBlock = block.getByTestId("iconBlock").locator("span"); + await expect(iconBlock).toHaveText(`${i + 1}.`); + } + }); + + test("순서 없는 리스트 변환", async ({ page }) => { + await page.keyboard.type("- 첫번째"); + await page.keyboard.press("Enter"); + await page.keyboard.type("- 두번째"); + const blocks = await page.locator('[data-testid^="block-"]').all(); + expect(blocks.length).toBe(2); + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + const iconBlock = block.getByTestId("iconBlock").locator("span"); + await expect(iconBlock).toHaveText("●"); + } + }); + + test("체크박스 변환", async ({ page }) => { + const contentEditable = page.getByTestId("block-0").getByTestId("contentEditable"); + await page.keyboard.type("[ ] 체크박스"); + const iconBlock = page.getByTestId("iconBlock").locator("span"); + await expect(iconBlock).toHaveText(""); + await expect(contentEditable).toHaveText("체크박스"); + await iconBlock.click(); + await expect(iconBlock).toHaveText("✓"); + }); + + // iconbutton이 없다? + test("인용구 변환", async ({ page }) => { + await page.keyboard.type("> 인용구"); + const contentEditable = page.getByTestId("block-0").getByTestId("contentEditable"); + const hasClass = await contentEditable.evaluate((el) => { + return el.classList.contains("c_gray.500") && el.classList.contains("font-style_italic"); + }); + await expect(contentEditable).toHaveText("인용구"); + expect(hasClass).toBe(true); + }); }); From 3cb22c036eb422e318dcc80695a177c7c851409d Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Mon, 13 Jan 2025 01:07:56 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EC=A7=84=EC=9E=85=EC=8B=9C=20=EB=A1=9C=ED=8B=B0=20=EC=95=A0?= =?UTF-8?q?=EB=8B=88=EB=A9=94=EC=9D=B4=EC=85=98=20=EC=83=9D=EB=9E=B5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/auth.spec.ts | 3 +++ tests/markdown.spec.ts | 10 +++++++--- tests/page.spec.ts | 9 +++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/auth.spec.ts b/tests/auth.spec.ts index eb5e8e7..aa88c55 100644 --- a/tests/auth.spec.ts +++ b/tests/auth.spec.ts @@ -17,6 +17,9 @@ const onBoarding = async (page: Page) => { test.describe("인증 테스트", () => { test.beforeEach(async ({ page }) => { + await page.context().addInitScript(() => { + window.sessionStorage.setItem("hasVisitedBefore", "true"); + }); await page.goto("/"); await onBoarding(page); }); diff --git a/tests/markdown.spec.ts b/tests/markdown.spec.ts index 90d37bf..888292b 100644 --- a/tests/markdown.spec.ts +++ b/tests/markdown.spec.ts @@ -1,5 +1,4 @@ -import { test, expect } from "@playwright/test"; -import { escape } from "querystring"; +import { test, expect, Page } from "@playwright/test"; const onBoarding = async (page) => { await page.click(".hover\\:bg-c_purple\\.600"); @@ -13,6 +12,9 @@ test.describe.configure({ mode: "serial" }); test.describe("마크다운 에디터 테스트", () => { test.beforeEach(async ({ page }) => { + await page.context().addInitScript(() => { + window.sessionStorage.setItem("hasVisitedBefore", "true"); + }); await page.goto("/"); await onBoarding(page); await page.getByTestId("addPageButton").click(); @@ -68,10 +70,12 @@ test.describe("마크다운 에디터 테스트", () => { test.describe("마크다운 문법 변환 테스트", () => { test.beforeEach(async ({ page }) => { + await page.context().addInitScript(() => { + window.sessionStorage.setItem("hasVisitedBefore", "true"); + }); await page.goto("/"); await onBoarding(page); await page.getByTestId("addPageButton").click(); - await page.waitForLoadState("networkidle"); await page.getByTestId("pageItem-0").click(); expect(page.getByTestId("page-0")).toBeVisible(); const currentEditor = page.getByTestId("editor-0"); diff --git a/tests/page.spec.ts b/tests/page.spec.ts index 5d9b055..a6f6126 100644 --- a/tests/page.spec.ts +++ b/tests/page.spec.ts @@ -12,6 +12,9 @@ test.describe.configure({ mode: "serial" }); test.describe("사이드바 테스트", () => { test.beforeEach(async ({ page }) => { + await page.context().addInitScript(() => { + window.sessionStorage.setItem("hasVisitedBefore", "true"); + }); await page.goto("/"); await onBoarding(page); }); @@ -30,6 +33,9 @@ test.describe("사이드바 테스트", () => { test.describe("사이드바 페이지 테스트", () => { test.beforeEach(async ({ page }) => { + await page.context().addInitScript(() => { + window.sessionStorage.setItem("hasVisitedBefore", "true"); + }); await page.goto("/"); await onBoarding(page); }); @@ -135,6 +141,9 @@ test.describe("사이드바 페이지 테스트", () => { test.describe("페이지 테스트", () => { test.beforeEach(async ({ page }) => { + await page.context().addInitScript(() => { + window.sessionStorage.setItem("hasVisitedBefore", "true"); + }); await page.goto("/"); await onBoarding(page); await page.getByTestId("addPageButton").click(); From 33730c32a0b2636d7c6ae0be5dd9c9a82c7c5fae Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Mon, 13 Jan 2025 01:09:50 +0900 Subject: [PATCH 14/15] =?UTF-8?q?feat:=20=EB=8F=99=EA=B8=B0=EC=A0=81?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=ED=95=9C=EB=B2=88=EC=97=90=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EC=9D=98=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A7=8C=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3981b4f..984124c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build:server": "cd server && pnpm build", "dev": "pnpm -r --parallel dev", "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", + "test:e2e:ui": "playwright test --workers=1 --ui", "test:e2e:report": "playwright show-report" }, "keywords": [], From f16add78c0d7afc26a2e21cc93119e60eda560bb Mon Sep 17 00:00:00 2001 From: Ludovico7 Date: Tue, 14 Jan 2025 13:26:56 +0900 Subject: [PATCH 15/15] =?UTF-8?q?chore:=20lint=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- tests-examples/demo-todo-app.spec.ts | 424 --------------------------- 2 files changed, 1 insertion(+), 425 deletions(-) delete mode 100644 tests-examples/demo-todo-app.spec.ts diff --git a/package.json b/package.json index 984124c..3981b4f 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build:server": "cd server && pnpm build", "dev": "pnpm -r --parallel dev", "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --workers=1 --ui", + "test:e2e:ui": "playwright test --ui", "test:e2e:report": "playwright show-report" }, "keywords": [], diff --git a/tests-examples/demo-todo-app.spec.ts b/tests-examples/demo-todo-app.spec.ts deleted file mode 100644 index 5da1ade..0000000 --- a/tests-examples/demo-todo-app.spec.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { test, expect, type Page } from "@playwright/test"; - -test.beforeEach(async ({ page }) => { - await page.goto("https://demo.playwright.dev/todomvc"); -}); - -const TODO_ITEMS = ["buy some cheese", "feed the cat", "book a doctors appointment"] as const; - -test.describe("New Todo", () => { - test("should allow me to add todo items", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create 1st todo. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press("Enter"); - - // Make sure the list only has one todo item. - await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0]]); - - // Create 2nd todo. - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press("Enter"); - - // Make sure the list now has two todo items. - await expect(page.getByTestId("todo-title")).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); - - test("should clear text input field when an item is added", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create one todo item. - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press("Enter"); - - // Check that input is empty. - await expect(newTodo).toBeEmpty(); - await checkNumberOfTodosInLocalStorage(page, 1); - }); - - test("should append new items to the bottom of the list", async ({ page }) => { - // Create 3 items. - await createDefaultTodos(page); - - // create a todo count locator - const todoCount = page.getByTestId("todo-count"); - - // Check test using different methods. - await expect(page.getByText("3 items left")).toBeVisible(); - await expect(todoCount).toHaveText("3 items left"); - await expect(todoCount).toContainText("3"); - await expect(todoCount).toHaveText(/3/); - - // Check all items in one call. - await expect(page.getByTestId("todo-title")).toHaveText(TODO_ITEMS); - await checkNumberOfTodosInLocalStorage(page, 3); - }); -}); - -test.describe("Mark all as completed", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test.afterEach(async ({ page }) => { - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test("should allow me to mark all items as completed", async ({ page }) => { - // Complete all todos. - await page.getByLabel("Mark all as complete").check(); - - // Ensure all todos have 'completed' class. - await expect(page.getByTestId("todo-item")).toHaveClass([ - "completed", - "completed", - "completed", - ]); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - }); - - test("should allow me to clear the complete state of all items", async ({ page }) => { - const toggleAll = page.getByLabel("Mark all as complete"); - // Check and then immediately uncheck. - await toggleAll.check(); - await toggleAll.uncheck(); - - // Should be no completed classes. - await expect(page.getByTestId("todo-item")).toHaveClass(["", "", ""]); - }); - - test("complete all checkbox should update state when items are completed / cleared", async ({ - page, - }) => { - const toggleAll = page.getByLabel("Mark all as complete"); - await toggleAll.check(); - await expect(toggleAll).toBeChecked(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Uncheck first todo. - const firstTodo = page.getByTestId("todo-item").nth(0); - await firstTodo.getByRole("checkbox").uncheck(); - - // Reuse toggleAll locator and make sure its not checked. - await expect(toggleAll).not.toBeChecked(); - - await firstTodo.getByRole("checkbox").check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 3); - - // Assert the toggle all is checked again. - await expect(toggleAll).toBeChecked(); - }); -}); - -test.describe("Item", () => { - test("should allow me to mark items as complete", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } - - // Check first item. - const firstTodo = page.getByTestId("todo-item").nth(0); - await firstTodo.getByRole("checkbox").check(); - await expect(firstTodo).toHaveClass("completed"); - - // Check second item. - const secondTodo = page.getByTestId("todo-item").nth(1); - await expect(secondTodo).not.toHaveClass("completed"); - await secondTodo.getByRole("checkbox").check(); - - // Assert completed class. - await expect(firstTodo).toHaveClass("completed"); - await expect(secondTodo).toHaveClass("completed"); - }); - - test("should allow me to un-mark items as complete", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // Create two items. - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } - - const firstTodo = page.getByTestId("todo-item").nth(0); - const secondTodo = page.getByTestId("todo-item").nth(1); - const firstTodoCheckbox = firstTodo.getByRole("checkbox"); - - await firstTodoCheckbox.check(); - await expect(firstTodo).toHaveClass("completed"); - await expect(secondTodo).not.toHaveClass("completed"); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await firstTodoCheckbox.uncheck(); - await expect(firstTodo).not.toHaveClass("completed"); - await expect(secondTodo).not.toHaveClass("completed"); - await checkNumberOfCompletedTodosInLocalStorage(page, 0); - }); - - test("should allow me to edit an item", async ({ page }) => { - await createDefaultTodos(page); - - const todoItems = page.getByTestId("todo-item"); - const secondTodo = todoItems.nth(1); - await secondTodo.dblclick(); - await expect(secondTodo.getByRole("textbox", { name: "Edit" })).toHaveValue(TODO_ITEMS[1]); - await secondTodo.getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); - await secondTodo.getByRole("textbox", { name: "Edit" }).press("Enter"); - - // Explicitly assert the new text value. - await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, "buy some sausages"); - }); -}); - -test.describe("Editing", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test("should hide other controls when editing", async ({ page }) => { - const todoItem = page.getByTestId("todo-item").nth(1); - await todoItem.dblclick(); - await expect(todoItem.getByRole("checkbox")).not.toBeVisible(); - await expect( - todoItem.locator("label", { - hasText: TODO_ITEMS[1], - }), - ).not.toBeVisible(); - await checkNumberOfTodosInLocalStorage(page, 3); - }); - - test("should save edits on blur", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).dispatchEvent("blur"); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, "buy some sausages"); - }); - - test("should trim entered text", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(" buy some sausages "); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], "buy some sausages", TODO_ITEMS[2]]); - await checkTodosInLocalStorage(page, "buy some sausages"); - }); - - test("should remove the item if an empty text string was entered", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill(""); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Enter"); - - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test("should cancel edits on escape", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).dblclick(); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).fill("buy some sausages"); - await todoItems.nth(1).getByRole("textbox", { name: "Edit" }).press("Escape"); - await expect(todoItems).toHaveText(TODO_ITEMS); - }); -}); - -test.describe("Counter", () => { - test("should display the current number of todo items", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - // create a todo count locator - const todoCount = page.getByTestId("todo-count"); - - await newTodo.fill(TODO_ITEMS[0]); - await newTodo.press("Enter"); - - await expect(todoCount).toContainText("1"); - - await newTodo.fill(TODO_ITEMS[1]); - await newTodo.press("Enter"); - await expect(todoCount).toContainText("2"); - - await checkNumberOfTodosInLocalStorage(page, 2); - }); -}); - -test.describe("Clear completed button", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - }); - - test("should display the correct text", async ({ page }) => { - await page.locator(".todo-list li .toggle").first().check(); - await expect(page.getByRole("button", { name: "Clear completed" })).toBeVisible(); - }); - - test("should remove completed items when clicked", async ({ page }) => { - const todoItems = page.getByTestId("todo-item"); - await todoItems.nth(1).getByRole("checkbox").check(); - await page.getByRole("button", { name: "Clear completed" }).click(); - await expect(todoItems).toHaveCount(2); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test("should be hidden when there are no items that are completed", async ({ page }) => { - await page.locator(".todo-list li .toggle").first().check(); - await page.getByRole("button", { name: "Clear completed" }).click(); - await expect(page.getByRole("button", { name: "Clear completed" })).toBeHidden(); - }); -}); - -test.describe("Persistence", () => { - test("should persist its data", async ({ page }) => { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - for (const item of TODO_ITEMS.slice(0, 2)) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } - - const todoItems = page.getByTestId("todo-item"); - const firstTodoCheck = todoItems.nth(0).getByRole("checkbox"); - await firstTodoCheck.check(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(["completed", ""]); - - // Ensure there is 1 completed item. - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - // Now reload. - await page.reload(); - await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]); - await expect(firstTodoCheck).toBeChecked(); - await expect(todoItems).toHaveClass(["completed", ""]); - }); -}); - -test.describe("Routing", () => { - test.beforeEach(async ({ page }) => { - await createDefaultTodos(page); - // make sure the app had a chance to save updated todos in storage - // before navigating to a new view, otherwise the items can get lost :( - // in some frameworks like Durandal - await checkTodosInLocalStorage(page, TODO_ITEMS[0]); - }); - - test("should allow me to display active items", async ({ page }) => { - const todoItem = page.getByTestId("todo-item"); - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole("link", { name: "Active" }).click(); - await expect(todoItem).toHaveCount(2); - await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]); - }); - - test("should respect the back button", async ({ page }) => { - const todoItem = page.getByTestId("todo-item"); - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - - await test.step("Showing all items", async () => { - await page.getByRole("link", { name: "All" }).click(); - await expect(todoItem).toHaveCount(3); - }); - - await test.step("Showing active items", async () => { - await page.getByRole("link", { name: "Active" }).click(); - }); - - await test.step("Showing completed items", async () => { - await page.getByRole("link", { name: "Completed" }).click(); - }); - - await expect(todoItem).toHaveCount(1); - await page.goBack(); - await expect(todoItem).toHaveCount(2); - await page.goBack(); - await expect(todoItem).toHaveCount(3); - }); - - test("should allow me to display completed items", async ({ page }) => { - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole("link", { name: "Completed" }).click(); - await expect(page.getByTestId("todo-item")).toHaveCount(1); - }); - - test("should allow me to display all items", async ({ page }) => { - await page.getByTestId("todo-item").nth(1).getByRole("checkbox").check(); - await checkNumberOfCompletedTodosInLocalStorage(page, 1); - await page.getByRole("link", { name: "Active" }).click(); - await page.getByRole("link", { name: "Completed" }).click(); - await page.getByRole("link", { name: "All" }).click(); - await expect(page.getByTestId("todo-item")).toHaveCount(3); - }); - - test("should highlight the currently applied filter", async ({ page }) => { - await expect(page.getByRole("link", { name: "All" })).toHaveClass("selected"); - - // create locators for active and completed links - const activeLink = page.getByRole("link", { name: "Active" }); - const completedLink = page.getByRole("link", { name: "Completed" }); - await activeLink.click(); - - // Page change - active items. - await expect(activeLink).toHaveClass("selected"); - await completedLink.click(); - - // Page change - completed items. - await expect(completedLink).toHaveClass("selected"); - }); -}); - -async function createDefaultTodos(page: Page) { - // create a new todo locator - const newTodo = page.getByPlaceholder("What needs to be done?"); - - for (const item of TODO_ITEMS) { - await newTodo.fill(item); - await newTodo.press("Enter"); - } -} - -async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return JSON.parse(localStorage["react-todos"]).length === e; - }, expected); -} - -async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) { - return await page.waitForFunction((e) => { - return ( - JSON.parse(localStorage["react-todos"]).filter((todo: any) => todo.completed).length === e - ); - }, expected); -} - -async function checkTodosInLocalStorage(page: Page, title: string) { - return await page.waitForFunction((t) => { - return JSON.parse(localStorage["react-todos"]) - .map((todo: any) => todo.title) - .includes(t); - }, title); -}