From e7ebfd76c307b672abbff0e00bc28792745bf069 Mon Sep 17 00:00:00 2001 From: ShohanMir Date: Thu, 2 Apr 2026 13:05:40 +0600 Subject: [PATCH 1/2] question update flow --- .gitignore | 8 ++ package-lock.json | 128 +++++++++++++++++++++++-- package.json | 7 +- playwright.config.ts | 53 ++++++++++ storage/user_chrome.json | 35 +++++++ tests/config/db-e2e.ts | 69 +++++++++++++ tests/e2e/fixtures/question.fixture.ts | 45 +++++++++ tests/e2e/setup/auth.chromium.setup.ts | 35 +++++++ tests/e2e/setup/global-setup.ts | 39 ++++++++ tests/e2e/setup/global-teardown.ts | 21 ++++ tests/e2e/specs/question.spec.ts | 32 +++++++ tests/fixtures/questions.ts | 86 +++++++++++++++++ tests/fixtures/users.ts | 68 +++++++++++++ tests/seeders/e2e.seeder.ts | 33 +++++++ tests/seeders/question.seeder.ts | 28 ++++++ tests/seeders/user.seeder.ts | 24 +++++ 16 files changed, 701 insertions(+), 10 deletions(-) create mode 100644 playwright.config.ts create mode 100644 storage/user_chrome.json create mode 100644 tests/config/db-e2e.ts create mode 100644 tests/e2e/fixtures/question.fixture.ts create mode 100644 tests/e2e/setup/auth.chromium.setup.ts create mode 100644 tests/e2e/setup/global-setup.ts create mode 100644 tests/e2e/setup/global-teardown.ts create mode 100644 tests/e2e/specs/question.spec.ts create mode 100644 tests/fixtures/questions.ts create mode 100644 tests/fixtures/users.ts create mode 100644 tests/seeders/e2e.seeder.ts create mode 100644 tests/seeders/question.seeder.ts create mode 100644 tests/seeders/user.seeder.ts diff --git a/.gitignore b/.gitignore index 2dbc56f..c8d2cdb 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,11 @@ next-env.d.ts # Sentry Config File .env.sentry-build-plugin + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/package-lock.json b/package-lock.json index cbd205f..bb8335f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "clsx": "^2.1.1", "cm6-theme-basic-dark": "^0.2.0", "dayjs": "^1.11.18", + "dotenv-cli": "^7.4.0", "lucide-react": "^0.545.0", "mongoose": "^8.19.1", "next": "15.5.7", @@ -48,6 +49,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@playwright/test": "^1.59.1", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", "@testing-library/dom": "^10.4.1", @@ -234,6 +236,7 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1046,6 +1049,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -1134,6 +1138,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", + "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -1143,6 +1148,7 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.5.tgz", "integrity": "sha512-SFVsNAgsAoou+BjRewMqN+m9jaztB9wCWN9RSRgePqUbq8UVlvJfku5zB2KVhLPgH/h0RLk38tvd4tGeAhygnw==", "license": "MIT", + "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1317,6 +1323,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" }, @@ -1340,6 +1347,7 @@ } ], "license": "MIT", + "peer": true, "engines": { "node": ">=18" } @@ -2968,6 +2976,7 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", "license": "MIT", + "peer": true, "dependencies": { "@lezer/common": "^1.0.0" } @@ -3507,6 +3516,23 @@ "url": "https://opencollective.com/pkgr" } }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "devOptional": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@radix-ui/colors": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", @@ -4900,6 +4926,7 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5243,6 +5270,7 @@ "integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.14.0" } @@ -5252,6 +5280,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5262,6 +5291,7 @@ "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5364,6 +5394,7 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -5895,6 +5926,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6518,6 +6550,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -7029,7 +7062,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -7386,6 +7418,30 @@ "url": "https://dotenvx.com" } }, + "node_modules/dotenv-cli": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.4.0.tgz", + "integrity": "sha512-fZGFOGCC5rEz1OJ0Pp+1LN8y78ClMcyXDmBEmjvJwqCqZVsPHcQ85bLCh5hZ4Bqotw4dptXOLvvw0vxm2MD30g==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "dotenv": "^16.3.0", + "dotenv-expand": "^10.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, + "node_modules/dotenv-expand": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/downshift": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/downshift/-/downshift-7.6.2.tgz", @@ -7781,6 +7837,7 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -7870,6 +7927,7 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -7971,6 +8029,7 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -9881,7 +9940,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isomorphic.js": { @@ -9889,7 +9947,6 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", - "peer": true, "funding": { "type": "GitHub Sponsors โค", "url": "https://github.com/sponsors/dmonad" @@ -10995,6 +11052,7 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -11188,7 +11246,6 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "license": "MIT", - "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -13385,7 +13442,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13593,6 +13649,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -13651,6 +13754,7 @@ "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/preact" @@ -13681,6 +13785,7 @@ "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13956,6 +14061,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -13974,6 +14080,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -14002,6 +14109,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.64.0.tgz", "integrity": "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -14633,7 +14741,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -14646,7 +14753,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15342,7 +15448,8 @@ "version": "4.1.14", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -15471,6 +15578,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -15591,6 +15699,7 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15794,6 +15903,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16280,7 +16390,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -16677,6 +16786,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f6b6a9f..eebd676 100644 --- a/package.json +++ b/package.json @@ -6,10 +6,13 @@ "dev": "next dev --turbo", "build": "next build", "start": "next start", + "start:test": "dotenv -e .env.test -- next start", "lint": "next lint", "test": "jest", "test:watch": "jest --watch", - "test:coverage": "jest --coverage" + "test:coverage": "jest --coverage", + "test:e2e": "playwright test", + "test:report": "playwright show-report" }, "dependencies": { "@ai-sdk/openai": "^2.0.47", @@ -33,6 +36,7 @@ "clsx": "^2.1.1", "cm6-theme-basic-dark": "^0.2.0", "dayjs": "^1.11.18", + "dotenv-cli": "^7.4.0", "lucide-react": "^0.545.0", "mongoose": "^8.19.1", "next": "15.5.7", @@ -52,6 +56,7 @@ }, "devDependencies": { "@eslint/eslintrc": "^3", + "@playwright/test": "^1.59.1", "@tailwindcss/postcss": "^4", "@tailwindcss/typography": "^0.5.19", "@testing-library/dom": "^10.4.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2f782a0 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,53 @@ +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.test") }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./tests/e2e", + globalSetup: "./tests/e2e/setup/global-setup.ts", + globalTeardown: "./tests/e2e/setup/global-teardown.ts", + /* 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", + outputDir: "test-results", + /* 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('')`. */ + trace: "on-first-retry", + baseURL: process.env.BASE_URL, + screenshot: "only-on-failure", + }, + + /* Configure projects for major browsers */ + projects: [ + { name: "setup-chromium", testMatch: /tests\/e2e\/setup\/auth\.chromium\.setup\.ts/ }, + { + name: "chromium", + use: { ...devices["Desktop Chrome"], storageState: "storage/user_chrome.json" }, + dependencies: ["setup-chromium"], + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: "npm run start:test", + url: process.env.BASE_URL, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/storage/user_chrome.json b/storage/user_chrome.json new file mode 100644 index 0000000..7a795b0 --- /dev/null +++ b/storage/user_chrome.json @@ -0,0 +1,35 @@ +{ + "cookies": [ + { + "name": "authjs.csrf-token", + "value": "9fa27bb81c1fb4c73cfe1ae59d7529732ccd960391a18ea711780101357f33c3%7Cbcbe9622a4d3ed067f5473a00e4274af0c9522d727ed2847997b979e06552a71", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "authjs.callback-url", + "value": "http%3A%2F%2Flocalhost%3A3000%2Fsign-in", + "domain": "localhost", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + }, + { + "name": "authjs.session-token", + "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiek9HSG9RZ19Jdm9menJHWjNRNklWMmVteEpXQm50MTNoV09Rdnc2NkU2OVBOcHRRa0lxTGZDamswclpRb00yUUNzcHcwX1lmUFdfM0owakZXTEFVcEEifQ..X7VJmHq-Fq1MAnVNvaTc-g.mVlE6vsPU-RMhhzZk6MxK-riJjP7MZUl_HAZMnU8_45hqC31nlcvOKcxm7OCJB7j6dmTMB1wJ9bnwWhLcZKksVPhXSM_7ZG0pBOjDHVO5MSl6tTwXD43UPPY-YlyEt9SRcLFqxZYSDgrmg7_43IhqSAF4zc42WqD-qhJ6U8EMVS19t2t9L9JzDIo0Hh86rRd01O7acAs5SZVB2QlOZVRtjcJ1BoP7Ahpr31U40EdE0A.gnOqOPB2x2AlKlJSg4RtYJ_CfWimDFaCTJTLwolhLhI", + "domain": "localhost", + "path": "/", + "expires": 1777705472.867035, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [] +} \ No newline at end of file diff --git a/tests/config/db-e2e.ts b/tests/config/db-e2e.ts new file mode 100644 index 0000000..812939c --- /dev/null +++ b/tests/config/db-e2e.ts @@ -0,0 +1,69 @@ +import { config } from "dotenv"; +import mongoose from "mongoose"; + +config({ path: ".env.test" }); + +const MONGODB_URI = process.env.MONGODB_URI as string; + +export function isE2EDBConnected() { + return mongoose.connection.readyState === 1; +} + +export async function connectE2EDB() { + if (!MONGODB_URI) { + throw new Error("๐Ÿ”‘ Please define the MONGODB_URI environment variable "); + } + + if (isE2EDBConnected()) { + console.log("๐Ÿ—„๏ธ Already connected to E2E test database"); + return; + } + + try { + await mongoose.connect(MONGODB_URI, { dbName: "devflow" }); + console.log("๐Ÿ—„๏ธ Connected to E2E test database"); + } catch (error) { + console.error("๐Ÿ”ด Connection to E2E test database failed:", error); + throw error; + } +} + +export async function disconnectE2EDB() { + if (!isE2EDBConnected()) { + console.log("๐Ÿ—„๏ธ Already disconnected from E2E test database"); + return; + } + + try { + await mongoose.disconnect(); + console.log("๐Ÿ”Œ Disconnected from E2E test database"); + } catch (error) { + console.error("๐Ÿ”ด Disconnection from E2E test database failed:", error); + throw error; + } +} + +export async function cleanupE2EData() { + if (!isE2EDBConnected()) { + console.log("๐Ÿ—„๏ธ E2E test database is not connected"); + return; + } + + try { + await mongoose.connection.dropDatabase(); + console.log("๐ŸŸข Dropped entire E2E test database"); + } catch (err) { + console.error("๐Ÿ”ด Cleanup E2E test data failed:", err); + } +} + +export function getE2EConnectionInfo() { + const { connection } = mongoose; + + return { + dbName: connection.db?.databaseName || null, + host: connection.host || null, + port: connection.port || null, + isConnected: isE2EDBConnected(), + }; +} diff --git a/tests/e2e/fixtures/question.fixture.ts b/tests/e2e/fixtures/question.fixture.ts new file mode 100644 index 0000000..679afb4 --- /dev/null +++ b/tests/e2e/fixtures/question.fixture.ts @@ -0,0 +1,45 @@ +import { test as base, expect, BrowserContext, Page } from "@playwright/test"; + +type QuestionFixture = { + createQuestion: { questionId: string; title: string; page: Page }; +}; + +export const test = base.extend({ + createQuestion: [ + async ({ browser }, use) => { + const context: BrowserContext = await browser.newContext(); + const page: Page = await context.newPage(); + + await page.goto("/ask-question"); + await expect(page).toHaveURL("/ask-question"); + + const questionTitle = `E2E Test Question ${Date.now()}`; + + await page.getByRole("textbox", { name: "Question Title *" }).fill(questionTitle); + await page + .getByRole("textbox", { name: "editable markdown" }) + .fill( + "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s," + ); + + await page.getByRole("textbox", { name: "Add tags..." }).fill("playwright"); + await page.getByRole("textbox", { name: "Add tags..." }).press("Enter"); + await page.getByRole("button", { name: "Ask a question" }).click(); + + await expect(page).toHaveURL(/\/questions\/[a-f0-9]+$/); + + const url = page.url(); + const questionId = url.split("/").pop(); + if (!questionId) { + throw new Error("Failed to extract questionId from URL"); + } + + await expect(page.getByRole("heading", { name: questionTitle, exact: true })).toBeVisible(); + + await use({ questionId, title: questionTitle, page }); + + await context.close(); + }, + { scope: "test" }, + ], +}); diff --git a/tests/e2e/setup/auth.chromium.setup.ts b/tests/e2e/setup/auth.chromium.setup.ts new file mode 100644 index 0000000..c82df85 --- /dev/null +++ b/tests/e2e/setup/auth.chromium.setup.ts @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; + +import { BROWSER_USERS } from "@/tests/fixtures/users"; + +test.describe("Authentication Setup", () => { + test("authenticate and save storage state", async ({ page }) => { + // Go directly to sign-in + await page.goto("/sign-in"); + + // Sign in using real UI + await page.getByLabel("Email Address").fill(BROWSER_USERS.chrome.email); + await page.getByLabel("Password").fill(BROWSER_USERS.chrome.password); + await page.getByRole("button", { name: /sign in/i }).click(); + + // Wait for session cookie to be set, then go home + await expect + .poll(async () => { + const cookies = await page.context().cookies(); + return cookies.some((c) => + [ + "next-auth.session-token", + "__Secure-next-auth.session-token", + "authjs.session-token", + "__Secure-authjs.session-token", + ].includes(c.name) + ); + }) + .toBe(true); + + await page.goto("/"); + + // Persist authenticated state for dependent tests + await page.context().storageState({ path: "storage/user_chrome.json" }); + }); +}); diff --git a/tests/e2e/setup/global-setup.ts b/tests/e2e/setup/global-setup.ts new file mode 100644 index 0000000..35e7e86 --- /dev/null +++ b/tests/e2e/setup/global-setup.ts @@ -0,0 +1,39 @@ +import { connectE2EDB, cleanupE2EData, getE2EConnectionInfo, isE2EDBConnected } from "@/tests/config/db-e2e"; +import { seed } from "@/tests/seeders/e2e.seeder"; + +async function globalSetup() { + console.log("๐Ÿš€ Starting E2E test global setup..."); + + try { + // Connect to the E2E test database + await connectE2EDB(); + + // Verify connection is established + const connectionInfo = getE2EConnectionInfo(); + console.log("๐Ÿ” E2E Database connection info:", { + database: connectionInfo.dbName, + host: connectionInfo.host, + port: connectionInfo.port, + isConnected: connectionInfo.isConnected, + }); + + if (!isE2EDBConnected) { + throw new Error("Failed to establish stable E2E database connection"); + } + + // Clean up any existing test data to start fresh + await cleanupE2EData(); + + // Seed the database with test data + const seedData = await seed(); + + console.log("โœ… E2E global setup completed successfully"); + console.log(`๐Ÿ‘ฅ Created ${Object.keys(seedData.users).length} test users`); + console.log(`โ“ Created ${Object.keys(seedData.questions).length} test questions`); + } catch (error) { + console.error("โŒ E2E global setup failed:", error); + process.exit(1); // Exit with error code to stop test execution + } +} + +export default globalSetup; diff --git a/tests/e2e/setup/global-teardown.ts b/tests/e2e/setup/global-teardown.ts new file mode 100644 index 0000000..5ee691e --- /dev/null +++ b/tests/e2e/setup/global-teardown.ts @@ -0,0 +1,21 @@ +import { disconnectE2EDB, cleanupE2EData } from "@/tests/config/db-e2e"; + +async function globalTeardown() { + console.log("๐Ÿงน Starting E2E test global teardown..."); + + try { + // Clean up all test data from the database + await cleanupE2EData(); + + // Disconnect from the E2E test database + await disconnectE2EDB(); + + console.log("โœ… E2E global teardown completed successfully"); + console.log("๐Ÿ—‘๏ธ Test data cleaned up"); + console.log("๐Ÿ”Œ Database connection closed"); + } catch (error) { + console.error("โŒ E2E global teardown failed:", error); + } +} + +export default globalTeardown; diff --git a/tests/e2e/specs/question.spec.ts b/tests/e2e/specs/question.spec.ts new file mode 100644 index 0000000..c7e0ab3 --- /dev/null +++ b/tests/e2e/specs/question.spec.ts @@ -0,0 +1,32 @@ +import { test, expect } from "@playwright/test"; + +import { SAMPLE_QUESTIONS } from "@/tests/fixtures/questions"; + +const question = SAMPLE_QUESTIONS[0]; + +test.describe("Ask a Question Flow", () => { + test("should allow a user to submit a new question and view it on the question page", async ({ page }) => { + await page.goto("/ask-question"); + await expect(page).toHaveURL("/ask-question"); + + await page.getByRole("textbox", { name: "Question Title *" }).dblclick(); + await page.getByRole("textbox", { name: "Question Title *" }).fill(question.title); + + await page.getByRole("textbox", { name: "editable markdown" }).click(); + await page.getByRole("textbox", { name: "editable markdown" }).fill(question.content); + + await page.getByRole("textbox", { name: "Add tags..." }).click(); + await page.getByRole("textbox", { name: "Add tags..." }).fill(question.tags[0]); + await page.getByRole("textbox", { name: "Add tags..." }).press("Enter"); + + await page.getByRole("textbox", { name: "Add tags..." }).click(); + await page.getByRole("textbox", { name: "Add tags..." }).fill(question.tags[1]); + await page.getByRole("textbox", { name: "Add tags..." }).press("Enter"); + + await page.getByRole("button", { name: "Ask a question" }).click(); + + // after submission, check the redirected correct question page: + await expect(page).toHaveURL(/\/questions\/[a-f0-9]+$/); + await expect(page.getByRole("heading", { name: question.title, exact: true })).toBeVisible(); + }); +}); diff --git a/tests/fixtures/questions.ts b/tests/fixtures/questions.ts new file mode 100644 index 0000000..832667b --- /dev/null +++ b/tests/fixtures/questions.ts @@ -0,0 +1,86 @@ +export interface TestQuestion { + title: string; + content: string; + tags: string[]; +} + +export const SAMPLE_QUESTIONS: TestQuestion[] = [ + { + title: "How to use React hooks effectively?", + content: + "I'm learning React and want to understand how to use hooks properly. What are the best practices?. What are the gotchas? What are the tradeoffs? Explain with examples.", + tags: ["react", "javascript", "hooks"], + }, + { + title: "React state management patterns", + content: + "What are the different state management patterns in React? When should I use Context vs Redux? What are the tradeoffs? Explain with examples.", + tags: ["react", "state-management", "context", "redux"], + }, + { + title: "Optimizing React performance with useMemo", + content: + "How can I optimize my React app performance using useMemo and useCallback? What are the gotchas? What are the tradeoffs? Explain with examples.", + tags: ["react", "performance", "usememo", "optimization"], + }, + { + title: "Next.js routing best practices", + content: + "What are the best practices for routing in Next.js applications? How do I handle dynamic routes? What are the tradeoffs? Explain with examples.", + tags: ["nextjs", "routing", "typescript"], + }, + { + title: "Next.js App Router vs Pages Router", + content: + "What are the differences between App Router and Pages Router in Next.js? When should I use each? What are the tradeoffs? Explain with examples.", + tags: ["nextjs", "app-router", "pages-router", "migration"], + }, + { + title: "Server-side rendering optimization in Next.js", + content: + "How can I optimize SSR performance in Next.js? What are the trade-offs between SSR, SSG, and ISR? Explain with examples.", + tags: ["nextjs", "ssr", "ssg", "performance"], + }, + { + title: "Understanding JavaScript closures", + content: + "Can someone explain JavaScript closures with practical examples? When are they useful? What are the tradeoffs? Explain with examples.", + tags: ["javascript", "closures", "fundamentals"], + }, + { + title: "Async/await vs Promises", + content: + "What's the difference between using async/await and .then()/.catch()? Which approach is better? What are the tradeoffs? Explain with examples.", + tags: ["javascript", "async", "promises", "es6"], + }, + { + title: "Best practices for E2E testing", + content: + "What are the best practices for implementing end-to-end testing in web applications? How can I handle asynchronous operations? What are the tradeoffs? Explain with examples.", + tags: ["testing", "e2e", "automation"], + }, + { + title: "Unit testing React components", + content: + "How do I write effective unit tests for React components? What should I test and what should I avoid? What are the tradeoffs? Explain with examples.", + tags: ["testing", "react", "jest", "unit-tests"], + }, + { + title: "Testing strategies for microservices", + content: + "What are the best testing strategies for microservices architecture? How do I handle integration testing? What are the tradeoffs? Explain with examples.", + tags: ["testing", "microservices", "integration", "architecture"], + }, + { + title: "Getting started with web development", + content: + "I'm new to web development. What technologies should I learn first? What's the recommended learning path? What are the tradeoffs? Explain with examples.", + tags: ["beginners", "web-development", "learning-path"], + }, + { + title: "Code review best practices", + content: + "What are the best practices for conducting code reviews? How can I give constructive feedback? What are the tradeoffs? Explain with examples.", + tags: ["code-review", "best-practices", "teamwork"], + }, +]; diff --git a/tests/fixtures/users.ts b/tests/fixtures/users.ts new file mode 100644 index 0000000..c070307 --- /dev/null +++ b/tests/fixtures/users.ts @@ -0,0 +1,68 @@ +export interface TestUser { + name: string; + username: string; + email: string; + password: string; +} + +// Browser-specific users for E2E authentication +export const BROWSER_USERS: Record = { + chrome: { + name: "Chrome Test User", + username: "chromeuser", + email: "e2e-chrome@test.com", + password: "password123", + }, + firefox: { + name: "Firefox Test User", + username: "firefoxuser", + email: "e2e-firefox@test.com", + password: "password123", + }, + safari: { + name: "Safari Test User", + username: "safariuser", + email: "e2e-safari@test.com", + password: "password123", + }, + edge: { + name: "Edge Test User", + username: "edgeuser", + email: "e2e-edge@test.com", + password: "password123", + }, +}; + +// Common test users for various scenarios +export const COMMON_USERS: TestUser[] = [ + { + name: "Regular Test User", + username: "testuser", + email: "test@example.com", + password: "password123", + }, + { + name: "Admin User", + username: "admin", + email: "admin@example.com", + password: "admin123", + }, + { + name: "Moderator User", + username: "moderator", + email: "mod@example.com", + password: "mod123", + }, + { + name: "John Developer", + username: "johndev", + email: "john@dev.com", + password: "dev123", + }, + { + name: "Sarah Designer", + username: "sarahdesign", + email: "sarah@design.com", + password: "design123", + }, +]; diff --git a/tests/seeders/e2e.seeder.ts b/tests/seeders/e2e.seeder.ts new file mode 100644 index 0000000..1c5c3ff --- /dev/null +++ b/tests/seeders/e2e.seeder.ts @@ -0,0 +1,33 @@ +import { createTestQuestion } from "./question.seeder"; +import { createTestUser } from "./user.seeder"; +import { SAMPLE_QUESTIONS } from "../fixtures/questions"; +import { BROWSER_USERS, COMMON_USERS } from "../fixtures/users"; + +export async function seed() { + try { + // Create browser users + const chromeUser = await createTestUser(BROWSER_USERS.chrome); + + const userPromises = COMMON_USERS.map(async (user) => createTestUser(user)); + const allUsers = await Promise.all(userPromises); + + // Create test questions + const questionPromises = SAMPLE_QUESTIONS.slice(0, 3).map(async (question) => + createTestQuestion({ + ...question, + author: chromeUser._id.toString(), + }) + ); + const allQuestions = await Promise.all(questionPromises); + + console.log("๐ŸŒฑ E2E database seeded with test data"); + + return { + users: { chromeUser, ...allUsers }, + questions: { ...allQuestions }, + }; + } catch (error) { + console.error("๐Ÿ”ด Failed to seed E2E data:", error); + throw error; + } +} diff --git a/tests/seeders/question.seeder.ts b/tests/seeders/question.seeder.ts new file mode 100644 index 0000000..b3fb514 --- /dev/null +++ b/tests/seeders/question.seeder.ts @@ -0,0 +1,28 @@ +import { Question, Tag } from "@/database"; + +import { TestQuestion } from "../fixtures/questions"; + +export async function createTestQuestion(questionDetails: TestQuestion & { author: string }) { + // Create or get all tags in parallel using findOneAndUpdate + const tagIds = await Promise.all( + questionDetails.tags.map(async (tagName: string) => { + const tag = await Tag.findOneAndUpdate( + { name: tagName }, + { $setOnInsert: { name: tagName } }, + { upsert: true, new: true, setDefaultsOnInsert: true } + ); + + return tag._id; + }) + ); + + const question = await Question.create({ + title: questionDetails.title, + content: questionDetails.content, + tags: tagIds, + author: questionDetails.author, + }); + + console.log(`โ“ Created test question: ${questionDetails.title}`); + return question; +} diff --git a/tests/seeders/user.seeder.ts b/tests/seeders/user.seeder.ts new file mode 100644 index 0000000..8038555 --- /dev/null +++ b/tests/seeders/user.seeder.ts @@ -0,0 +1,24 @@ +import bcrypt from "bcryptjs"; + +import { Account, User } from "@/database"; + +import { TestUser } from "../fixtures/users"; + +export async function createTestUser(userDetails: TestUser) { + const user = await User.create({ + name: userDetails.name, + username: userDetails.username, + email: userDetails.email, + }); + + await Account.create({ + userId: user._id, + name: userDetails.name, + provider: "credentials", + providerAccountId: userDetails.email, + password: await bcrypt.hash(userDetails.password || "password123", 12), + }); + + console.log(`๐Ÿ‘ค Created test user: ${userDetails.username}`); + return user; +} From a324efe35fb9ba84ca947519e1e44fee476f0bcc Mon Sep 17 00:00:00 2001 From: ShohanMir Date: Thu, 2 Apr 2026 13:14:37 +0600 Subject: [PATCH 2/2] complete profile flow update --- playwright.config.ts | 3 ++ storage/user_chrome.json | 6 ++-- tests/e2e/fixtures/question.fixture.ts | 32 ++++++++--------- tests/e2e/specs/answer.spec.ts | 23 ++++++++++++ tests/e2e/specs/profile.spec.ts | 35 ++++++++++++++++++ tests/e2e/specs/question.spec.ts | 50 +++++++++++++++----------- tests/fixtures/questions.ts | 39 +++++++++----------- 7 files changed, 126 insertions(+), 62 deletions(-) create mode 100644 tests/e2e/specs/answer.spec.ts create mode 100644 tests/e2e/specs/profile.spec.ts diff --git a/playwright.config.ts b/playwright.config.ts index 2f782a0..0ba92c0 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -17,6 +17,8 @@ export default defineConfig({ globalTeardown: "./tests/e2e/setup/global-teardown.ts", /* Run tests in files in parallel */ fullyParallel: true, + timeout: 60 * 1000, // Maximum time a test can run + expect: { timeout: 30 * 1000 }, // Maximum time for an assertion /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, /* Retry on CI only */ @@ -48,6 +50,7 @@ export default defineConfig({ webServer: { command: "npm run start:test", url: process.env.BASE_URL, + timeout: 120 * 1000, reuseExistingServer: !process.env.CI, }, }); diff --git a/storage/user_chrome.json b/storage/user_chrome.json index 7a795b0..31e0883 100644 --- a/storage/user_chrome.json +++ b/storage/user_chrome.json @@ -2,7 +2,7 @@ "cookies": [ { "name": "authjs.csrf-token", - "value": "9fa27bb81c1fb4c73cfe1ae59d7529732ccd960391a18ea711780101357f33c3%7Cbcbe9622a4d3ed067f5473a00e4274af0c9522d727ed2847997b979e06552a71", + "value": "9f85f62fce79f5b51c922d9988c4b3134bc8573145a99f80562350a34e2e8884%7C22294db7450c039e3a1322e5436bd8956df0794098160045c72cb5dcce0fb06c", "domain": "localhost", "path": "/", "expires": -1, @@ -22,10 +22,10 @@ }, { "name": "authjs.session-token", - "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiek9HSG9RZ19Jdm9menJHWjNRNklWMmVteEpXQm50MTNoV09Rdnc2NkU2OVBOcHRRa0lxTGZDamswclpRb00yUUNzcHcwX1lmUFdfM0owakZXTEFVcEEifQ..X7VJmHq-Fq1MAnVNvaTc-g.mVlE6vsPU-RMhhzZk6MxK-riJjP7MZUl_HAZMnU8_45hqC31nlcvOKcxm7OCJB7j6dmTMB1wJ9bnwWhLcZKksVPhXSM_7ZG0pBOjDHVO5MSl6tTwXD43UPPY-YlyEt9SRcLFqxZYSDgrmg7_43IhqSAF4zc42WqD-qhJ6U8EMVS19t2t9L9JzDIo0Hh86rRd01O7acAs5SZVB2QlOZVRtjcJ1BoP7Ahpr31U40EdE0A.gnOqOPB2x2AlKlJSg4RtYJ_CfWimDFaCTJTLwolhLhI", + "value": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiek9HSG9RZ19Jdm9menJHWjNRNklWMmVteEpXQm50MTNoV09Rdnc2NkU2OVBOcHRRa0lxTGZDamswclpRb00yUUNzcHcwX1lmUFdfM0owakZXTEFVcEEifQ..YLNpE_VujsxhJeqalXx91Q.2HSPir7Ttfd2-BUZUjHJ0bHREaraB9wyZCNhMMOI8uPUqL_4JVtqdoIgV2pSjs13SJXWdQwN0KQfw3upOt79mMY1zDGj0Cbpnt638Lz_neoqDtEvmJWxPc8akCvUqg8BAhJudAxEXCkO20zbLfOenS4g_nk-_ZsHDeK9q3ZSwQZXLzaY8mNNqYZYLl9d3RxPE5L4i7ek-CoZlUBvKUuOuiPcROndQssymvDkcvl7c4c.0uKwsBGDZEn-bgDRzEQEQa1bwLjBRMQ992618lmKDDc", "domain": "localhost", "path": "/", - "expires": 1777705472.867035, + "expires": 1777705992.688505, "httpOnly": true, "secure": false, "sameSite": "Lax" diff --git a/tests/e2e/fixtures/question.fixture.ts b/tests/e2e/fixtures/question.fixture.ts index 679afb4..5ec13ab 100644 --- a/tests/e2e/fixtures/question.fixture.ts +++ b/tests/e2e/fixtures/question.fixture.ts @@ -1,43 +1,41 @@ import { test as base, expect, BrowserContext, Page } from "@playwright/test"; +import { SAMPLE_QUESTIONS, TestQuestion } from "@/tests/fixtures/questions"; type QuestionFixture = { + question: TestQuestion; createQuestion: { questionId: string; title: string; page: Page }; }; +const FALLBACK_QUESTION = SAMPLE_QUESTIONS[0]; + export const test = base.extend({ + question: [FALLBACK_QUESTION, { scope: "test" }], createQuestion: [ - async ({ browser }, use) => { + async ({ browser, question }, use) => { const context: BrowserContext = await browser.newContext(); const page: Page = await context.newPage(); await page.goto("/ask-question"); await expect(page).toHaveURL("/ask-question"); - const questionTitle = `E2E Test Question ${Date.now()}`; + await page.getByRole("textbox", { name: "Question Title *" }).fill(question.title); + await page.getByRole("textbox", { name: "editable markdown" }).fill(question.content); - await page.getByRole("textbox", { name: "Question Title *" }).fill(questionTitle); - await page - .getByRole("textbox", { name: "editable markdown" }) - .fill( - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s," - ); + for (const tag of question.tags) { + await page.getByRole("textbox", { name: "Add tags..." }).fill(tag); + await page.getByRole("textbox", { name: "Add tags..." }).press("Enter"); + } - await page.getByRole("textbox", { name: "Add tags..." }).fill("playwright"); - await page.getByRole("textbox", { name: "Add tags..." }).press("Enter"); await page.getByRole("button", { name: "Ask a question" }).click(); - await expect(page).toHaveURL(/\/questions\/[a-f0-9]+$/); const url = page.url(); const questionId = url.split("/").pop(); - if (!questionId) { - throw new Error("Failed to extract questionId from URL"); - } - - await expect(page.getByRole("heading", { name: questionTitle, exact: true })).toBeVisible(); + if (!questionId) throw new Error("Failed to extract questionId from URL"); - await use({ questionId, title: questionTitle, page }); + await expect(page.getByRole("heading", { name: question.title, exact: true })).toBeVisible(); + await use({ questionId, title: question.title, page }); await context.close(); }, { scope: "test" }, diff --git a/tests/e2e/specs/answer.spec.ts b/tests/e2e/specs/answer.spec.ts new file mode 100644 index 0000000..938b974 --- /dev/null +++ b/tests/e2e/specs/answer.spec.ts @@ -0,0 +1,23 @@ +import { expect } from "@playwright/test"; +import { SAMPLE_QUESTIONS } from "@/tests/fixtures/questions"; +import { test } from "../fixtures/question.fixture"; + +const answer = + "This is my answer to the question. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. It should be at least 100 characters long. Should be enough to test the answer."; + +test.describe("Answer Flow", () => { + test.use({ question: SAMPLE_QUESTIONS[1] }); + + test("should submit answer to a question successfully", async ({ page, createQuestion }) => { + const { questionId } = createQuestion; + + await page.goto(`/questions/${questionId}`); + await expect(page).toHaveURL(`/questions/${questionId}`); + + await page.getByRole("img", { name: "Upvote" }).click(); + await page.getByRole("textbox", { name: "editable markdown" }).fill(answer); + await page.getByRole("button", { name: "Post Answer" }).click(); + + await expect(page.getByText(answer, { exact: false })).toBeVisible(); + }); +}); diff --git a/tests/e2e/specs/profile.spec.ts b/tests/e2e/specs/profile.spec.ts new file mode 100644 index 0000000..7a58348 --- /dev/null +++ b/tests/e2e/specs/profile.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; + +test.describe("Signup and Profile Update", () => { + test("should allow user to update profile", async ({ page }) => { + // No fixture needed, just navigate and test + await page.goto("/profile/edit"); + await expect(page).toHaveURL("/profile/edit"); + + // Verify we're on the right page + await expect(page.getByRole("heading", { name: "Edit Profile" })).toBeVisible(); + + // Fill out the profile form with updated values + await page.getByPlaceholder("Your Name").fill("Updated Test User"); + await page.getByPlaceholder("Your username").fill("updatedusername"); + await page.getByPlaceholder("Your Portfolio link").fill("https://updated-portfolio.com"); + await page.getByPlaceholder("Where do you live?").fill("Updated City, Country"); + await page + .getByPlaceholder("What's special about you?") + .fill("This is my updated bio with more information about myself."); + + // Submit the form + await page.getByRole("button", { name: "Submit" }).click(); + + // After submission, should redirect to profile page + await expect(page).toHaveURL(/\/profile\/[a-f0-9]+$/); + + // Verify updated profile information is visible + await expect(page.getByRole("heading", { name: "Updated Test User", exact: true })).toBeVisible(); + await expect(page.getByText("@updatedusername")).toBeVisible(); + await expect( + page.getByText("This is my updated bio with more information about myself.", { exact: false }) + ).toBeVisible(); + await expect(page.getByText("Updated City, Country", { exact: false })).toBeVisible(); + }); +}); diff --git a/tests/e2e/specs/question.spec.ts b/tests/e2e/specs/question.spec.ts index c7e0ab3..ba5cac0 100644 --- a/tests/e2e/specs/question.spec.ts +++ b/tests/e2e/specs/question.spec.ts @@ -1,32 +1,42 @@ -import { test, expect } from "@playwright/test"; +import { expect } from "@playwright/test"; -import { SAMPLE_QUESTIONS } from "@/tests/fixtures/questions"; +import { test } from "../fixtures/question.fixture"; +// import { SAMPLE_QUESTIONS } from "@/tests/fixtures/questions"; -const question = SAMPLE_QUESTIONS[0]; +// const question = SAMPLE_QUESTIONS[0]; -test.describe("Ask a Question Flow", () => { - test("should allow a user to submit a new question and view it on the question page", async ({ page }) => { - await page.goto("/ask-question"); - await expect(page).toHaveURL("/ask-question"); +test.describe("Question Flow", () => { + test("should allow user to update question", async ({ page, createQuestion, question }) => { + const { questionId } = createQuestion; - await page.getByRole("textbox", { name: "Question Title *" }).dblclick(); - await page.getByRole("textbox", { name: "Question Title *" }).fill(question.title); + await page.goto(`/questions/${questionId}/edit`); + await expect(page).toHaveURL(/\/questions\/[a-f0-9]+\/edit$/); - await page.getByRole("textbox", { name: "editable markdown" }).click(); - await page.getByRole("textbox", { name: "editable markdown" }).fill(question.content); + // Fill out the question form + await page.getByRole("textbox", { name: "Question Title *" }).fill(`${question.title} - E2E Test`); + await page.getByRole("textbox", { name: "editable markdown" }).fill(`${question.content} - E2E Test`); - await page.getByRole("textbox", { name: "Add tags..." }).click(); - await page.getByRole("textbox", { name: "Add tags..." }).fill(question.tags[0]); - await page.getByRole("textbox", { name: "Add tags..." }).press("Enter"); + // remove first tag and add a new one + await page.getByRole("button", { name: question.tags[0] }).getByRole("img", { name: "close icon" }).click(); - await page.getByRole("textbox", { name: "Add tags..." }).click(); - await page.getByRole("textbox", { name: "Add tags..." }).fill(question.tags[1]); + await page.getByRole("textbox", { name: "Add tags..." }).fill("test"); await page.getByRole("textbox", { name: "Add tags..." }).press("Enter"); - await page.getByRole("button", { name: "Ask a question" }).click(); + await page.getByRole("button", { name: "Save edits" }).click(); + + // after submission, check udpated question details + await expect(page).toHaveURL(`/questions/${questionId}`); + // await expect(page).toHaveURL(/\/questions\/[a-f0-9]+$/); + + await expect( + page.getByRole("heading", { + name: `${question.title} - E2E Test`, + exact: true, + }) + ).toBeVisible(); + await expect(page.getByText(`${question.content} - E2E Test`, { exact: false })).toBeVisible(); - // after submission, check the redirected correct question page: - await expect(page).toHaveURL(/\/questions\/[a-f0-9]+$/); - await expect(page.getByRole("heading", { name: question.title, exact: true })).toBeVisible(); + await expect(page.getByRole("link", { name: question.tags[0], exact: true })).not.toBeVisible(); + await expect(page.getByRole("link", { name: "test", exact: true })).toBeVisible(); }); }); diff --git a/tests/fixtures/questions.ts b/tests/fixtures/questions.ts index 832667b..874edd2 100644 --- a/tests/fixtures/questions.ts +++ b/tests/fixtures/questions.ts @@ -14,73 +14,68 @@ export const SAMPLE_QUESTIONS: TestQuestion[] = [ { title: "React state management patterns", content: - "What are the different state management patterns in React? When should I use Context vs Redux? What are the tradeoffs? Explain with examples.", - tags: ["react", "state-management", "context", "redux"], + "What are the different state management patterns available in modern React applications? I am particularly interested in understanding the trade-offs and knowing exactly when I should use the native Context API versus a library like Redux.", + tags: ["state-management", "context", "redux"], }, { title: "Optimizing React performance with useMemo", - content: - "How can I optimize my React app performance using useMemo and useCallback? What are the gotchas? What are the tradeoffs? Explain with examples.", + content: "How can I optimize my React app performance using useMemo and useCallback? What are the gotchas?", tags: ["react", "performance", "usememo", "optimization"], }, + { title: "Next.js routing best practices", - content: - "What are the best practices for routing in Next.js applications? How do I handle dynamic routes? What are the tradeoffs? Explain with examples.", + content: "What are the best practices for routing in Next.js applications? How do I handle dynamic routes?", tags: ["nextjs", "routing", "typescript"], }, { title: "Next.js App Router vs Pages Router", - content: - "What are the differences between App Router and Pages Router in Next.js? When should I use each? What are the tradeoffs? Explain with examples.", + content: "What are the differences between App Router and Pages Router in Next.js? When should I use each?", tags: ["nextjs", "app-router", "pages-router", "migration"], }, { title: "Server-side rendering optimization in Next.js", - content: - "How can I optimize SSR performance in Next.js? What are the trade-offs between SSR, SSG, and ISR? Explain with examples.", + content: "How can I optimize SSR performance in Next.js? What are the trade-offs between SSR, SSG, and ISR?", tags: ["nextjs", "ssr", "ssg", "performance"], }, + { title: "Understanding JavaScript closures", - content: - "Can someone explain JavaScript closures with practical examples? When are they useful? What are the tradeoffs? Explain with examples.", + content: "Can someone explain JavaScript closures with practical examples? When are they useful?", tags: ["javascript", "closures", "fundamentals"], }, { title: "Async/await vs Promises", - content: - "What's the difference between using async/await and .then()/.catch()? Which approach is better? What are the tradeoffs? Explain with examples.", + content: "What's the difference between using async/await and .then()/.catch()? Which approach is better?", tags: ["javascript", "async", "promises", "es6"], }, + { title: "Best practices for E2E testing", - content: - "What are the best practices for implementing end-to-end testing in web applications? How can I handle asynchronous operations? What are the tradeoffs? Explain with examples.", + content: "What are the best practices for implementing end-to-end testing in web applications?", tags: ["testing", "e2e", "automation"], }, { title: "Unit testing React components", - content: - "How do I write effective unit tests for React components? What should I test and what should I avoid? What are the tradeoffs? Explain with examples.", + content: "How do I write effective unit tests for React components? What should I test and what should I avoid?", tags: ["testing", "react", "jest", "unit-tests"], }, { title: "Testing strategies for microservices", content: - "What are the best testing strategies for microservices architecture? How do I handle integration testing? What are the tradeoffs? Explain with examples.", + "What are the best testing strategies for microservices architecture? How do I handle integration testing?", tags: ["testing", "microservices", "integration", "architecture"], }, + { title: "Getting started with web development", content: - "I'm new to web development. What technologies should I learn first? What's the recommended learning path? What are the tradeoffs? Explain with examples.", + "I'm new to web development. What technologies should I learn first? What's the recommended learning path?", tags: ["beginners", "web-development", "learning-path"], }, { title: "Code review best practices", - content: - "What are the best practices for conducting code reviews? How can I give constructive feedback? What are the tradeoffs? Explain with examples.", + content: "What are the best practices for conducting code reviews? How can I give constructive feedback?", tags: ["code-review", "best-practices", "teamwork"], }, ];