diff --git a/.gitignore b/.gitignore index 5964ca9..15ad555 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ .sass-cache dist/ node_modules/ -out/ \ No newline at end of file +out/ +/test-results/ +/playwright-report/ +/playwright/.cache/ +screenshots \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..521cb9d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": false, + "trailingComma": "none", + "bracketSpacing": true, + "jsxBracketSameLine": false, + "arrowParens": "avoid" +} \ No newline at end of file diff --git a/e2e/tasks.spec.js b/e2e/tasks.spec.js new file mode 100644 index 0000000..80a7bba --- /dev/null +++ b/e2e/tasks.spec.js @@ -0,0 +1,144 @@ +import axios from "axios"; +// @ts-check +const { test, expect } = require("@playwright/test"); + +const url = "http://localhost:3000/"; +const api = "http://localhost:4000"; +const screenshotsFolder = "screenshots"; +const newColleague = "Trinity"; + +const deleteUserById = async id => { + const response = await axios.delete(`${api}/colleagues/${id}`); + return response.data; +}; + +const deleteUserByName = async name => { + const response = await axios.get(`${api}/colleagues`); + const colleagues = response.data; + const colleague = colleagues.find(c => c.name === name); + if (colleague) { + await deleteUserById(colleague.id); + } +}; + +test("It is ALIVE 💓", async ({ page }) => { + await page.goto(url); + // Expect a title "to contain" a substring. + await expect(page).toHaveTitle(/React App/); +}); + +test("There is a header! 🥳", async ({ page }) => { + await page.goto(url); + // Expect the page to contain a header with the text "My Colleagues". + const header = page.getByText("My Colleagues"); + await expect(header).toBeVisible(); + await header.screenshot({ path: `${screenshotsFolder}/header.png` }); +}); + +test("We got some colleagues! 🤝", async ({ page }) => { + await page.goto(url); + // Expect the page to contain a list of colleagues. + const list = page.getByRole("list"); + await expect(list).toBeVisible(); + await list.screenshot({ path: `${screenshotsFolder}/list.png` }); +}); + +test("We got style! 💄", async ({ page }) => { + await page.goto(url); + // Expect that we have a style tag with the text "main", "h1", "ul" and "li". + const styleTag = await page.innerHTML("head > style:last-of-type"); + await expect(styleTag).toContain("main"); + await expect(styleTag).toContain("h1"); + await expect(styleTag).toContain("ul"); + await expect(styleTag).toContain("li"); +}); + +test("We got a form! 📝", async ({ page }) => { + await page.goto(url); + // Expect the page to contain a form. + const form = page.getByTestId("add-colleague-form"); + await expect(form).toBeVisible(); + await form.screenshot({ path: `${screenshotsFolder}/form.png` }); +}); + +test("We can add a colleague! ➕", async ({ page }) => { + await page.goto(url); + // Get input field and button. + const input = page.getByPlaceholder("Neo"); + const button = page.getByText("Add Colleague"); + // Type into input field. + await input.fill(newColleague); + // Click button. + await button.click(); + // Expect the page to contain a list of colleagues. + + const list = await page.getByRole("list"); + await expect(list).toBeVisible(); + const colleagues = await list.innerText(); + await expect(colleagues).toContain(newColleague); + + await list.screenshot({ path: `${screenshotsFolder}/list-with-trinity.png` }); + await deleteUserByName(newColleague); +}); + +test("We can remove a colleague! ➖", async ({ page }) => { + await page.goto(url); + // Get input field and button. + const input = page.getByPlaceholder("Neo"); + const button = page.getByText("Add Colleague"); + // Type into input field. + await input.fill(newColleague); + // Click button. + await button.click(); + // Expect the page to contain a list of colleagues. + const list = page.getByRole("list"); + await expect(list).toBeVisible(); + const colleagues = await list.innerText(); + await expect(colleagues).toContain(newColleague); + // Get list item with the new colleague. + const removeButton = list.locator(`li:last-child > button.button-secondary`); + // Click remove button. + await removeButton.click(); + // Expect the page to contain a list of colleagues. + const newColleagues = await list.innerText(); + await expect(newColleagues).not.toContain(newColleague); + await list.screenshot({ + path: `${screenshotsFolder}/list-without-trinity.png` + }); +}); + +test("We can edit a colleague! ✏️", async ({ page }) => { + await page.goto(url); + // Get input field and button. + const input = page.getByPlaceholder("Neo"); + const button = page.getByText("Add Colleague"); + // Type into input field. + await input.fill(newColleague); + // Click button. + await button.click(); + // Expect the page to contain a list of colleagues. + const list = page.getByRole("list"); + await expect(list).toBeVisible(); + const colleagues = await list.innerText(); + await expect(colleagues).toContain(newColleague); + // Get list item with the new colleague. + const editButton = list.locator(`li:last-child > button.button-primary`); + // Click edit button. + await editButton.click(); + // Expect the list to contain an input field. + const inputField = list.locator(`li:last-child > input`); + await expect(inputField).toBeVisible(); + // Type into input field. + await inputField.fill("Morpheus"); + // Clicks the save button. + const saveButton = list.locator(`li:last-child > button.button-primary`); + await saveButton.click(); + // Expect the page to contain a list of colleagues. + const newColleagues = await list.innerText(); + await expect(newColleagues).toContain("Morpheus"); + await expect(newColleagues).not.toContain(newColleague); + await list.screenshot({ + path: `${screenshotsFolder}/list-with-morpheus.png` + }); + await deleteUserByName("Morpheus"); +}); diff --git a/package-lock.json b/package-lock.json index 1c34ae4..5d7ce8b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ }, "devDependencies": { "@parcel/transformer-sass": "^2.7.0", + "@playwright/test": "^1.30.0", + "axios": "^1.3.2", "nodemon": "^2.0.15", "parcel": "^2.3.2", "parcel-reporter-static-files-copy": "^1.3.4" @@ -1718,6 +1720,22 @@ "@parcel/core": "^2.7.0" } }, + "node_modules/@playwright/test": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", + "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "playwright-core": "1.30.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/@swc/helpers": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", @@ -1736,6 +1754,12 @@ "node": ">=10.13.0" } }, + "node_modules/@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "dev": true + }, "node_modules/@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -1846,6 +1870,12 @@ "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -1885,6 +1915,17 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/axios": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.2.tgz", + "integrity": "sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2147,6 +2188,18 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2245,6 +2298,15 @@ "ms": "^2.1.1" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2506,6 +2568,40 @@ "node": ">=14" } }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2838,6 +2934,27 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -3170,6 +3287,18 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz", "integrity": "sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==" }, + "node_modules/playwright-core": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", + "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", + "dev": true, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -3250,6 +3379,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -4814,6 +4949,16 @@ "nullthrows": "^1.1.1" } }, + "@playwright/test": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.30.0.tgz", + "integrity": "sha512-SVxkQw1xvn/Wk/EvBnqWIq6NLo1AppwbYOjNLmyU0R1RoQ3rLEBtmjTnElcnz8VEtn11fptj1ECxK0tgURhajw==", + "dev": true, + "requires": { + "@types/node": "*", + "playwright-core": "1.30.0" + } + }, "@swc/helpers": { "version": "0.4.11", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.4.11.tgz", @@ -4829,6 +4974,12 @@ "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true }, + "@types/node": { + "version": "18.11.18", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", + "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==", + "dev": true + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -4909,6 +5060,12 @@ "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -4939,6 +5096,17 @@ } } }, + "axios": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.3.2.tgz", + "integrity": "sha512-1M3O703bYqYuPhbHeya5bnhpYVsDDRyQSabNja04mZtboLNSuZ4YrltestrLXfHgmzua4TpUqRiVKbiQuo2epw==", + "dev": true, + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5104,6 +5272,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -5181,6 +5358,12 @@ "ms": "^2.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -5387,6 +5570,23 @@ "safe-regex2": "^2.0.0" } }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5606,6 +5806,21 @@ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", "dev": true }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5860,6 +6075,12 @@ "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.1.0.tgz", "integrity": "sha512-KO0m2f1HkrPe9S0ldjx7za9BJjeHqBku5Ch8JyxETxT8dEFGz1PwgrHaOQupVYitpzbFSYm7nnljxD8dik2c+g==" }, + "playwright-core": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.30.0.tgz", + "integrity": "sha512-7AnRmTCf+GVYhHbLJsGUtskWTE33SwMZkybJ0v6rqR1boxq2x36U7p1vDRV7HO2IwTZgmycracLxPEJI49wu4g==", + "dev": true + }, "postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", @@ -5924,6 +6145,12 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, "pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/package.json b/package.json index 9475c59..c226894 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,9 @@ "start:nowatch": "parcel ./src/index.html", "gen:diagrams": "rm -rf ./static/img && plantuml ./src/diagrams/*.puml -tsvg -o ../../static/img", "build": "parcel build ./src/index.html --public-url .", - "server": "node ./src/local-server.js" + "server": "node ./src/local-server.js", + "test": "playwright test", + "test:report": "playwright show-report" }, "contributors": [ { @@ -40,6 +42,8 @@ }, "devDependencies": { "@parcel/transformer-sass": "^2.7.0", + "@playwright/test": "^1.30.0", + "axios": "^1.3.2", "nodemon": "^2.0.15", "parcel": "^2.3.2", "parcel-reporter-static-files-copy": "^1.3.4" diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..7dd3972 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,108 @@ +// @ts-check +const { devices } = require("@playwright/test"); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * @see https://playwright.dev/docs/test-configuration + * @type {import('@playwright/test').PlaywrightTestConfig} + */ +const config = { + testDir: "./e2e", + /* Maximum time one test can run for. */ + timeout: 30 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000 + }, + /* Run tests in files in parallel */ + fullyParallel: false, + /* 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 : 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry" + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"] + } + } + + // { + // name: "firefox", + // use: { + // ...devices["Desktop Firefox"] + // } + // }, + + // { + // name: "webkit", + // use: { + // ...devices["Desktop Safari"] + // } + // } + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { + // ...devices['Pixel 5'], + // }, + // }, + // { + // name: 'Mobile Safari', + // use: { + // ...devices['iPhone 12'], + // }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { + // channel: 'msedge', + // }, + // }, + // { + // name: 'Google Chrome', + // use: { + // channel: 'chrome', + // }, + // }, + ] + + /* Folder for test artifacts such as screenshots, videos, traces, etc. */ + // outputDir: 'test-results/', + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // port: 3000, + // }, +}; + +module.exports = config; diff --git a/src/local-server.js b/src/local-server.js index 1fcac34..a3287f2 100644 --- a/src/local-server.js +++ b/src/local-server.js @@ -1,5 +1,5 @@ const fastify = require("fastify")({ - logger: true, + logger: true }); fastify.register(require("@fastify/cors"), { origin: true }); @@ -7,16 +7,16 @@ fastify.register(require("@fastify/cors"), { origin: true }); const colleagueList = [ { id: 1, - name: "Tin Anh Nguyen", + name: "Tin Anh Nguyen" }, { id: 2, - name: "Thanh Son Vo", + name: "Thanh Son Vo" }, { id: 3, - name: "Didrik Fleischer", - }, + name: "Didrik Fleischer" + } ]; let runningId = colleagueList.length + 1; @@ -26,7 +26,7 @@ fastify.get("/colleagues", (req, res) => { fastify.get("/colleagues/:id", (req, res) => { const id = parseInt(req.params.id, 10); - const colleague = colleagueList.find((c) => c.id === id); + const colleague = colleagueList.find(c => c.id === id); if (!colleague) { res.status(404).send(); @@ -37,34 +37,35 @@ fastify.get("/colleagues/:id", (req, res) => { }); fastify.post("/colleagues", (req, res) => { - const colleague = JSON.parse(req.body); + const colleague = req.body; if (!colleague.name) { res.status(400).send("Missing required 'name' property"); return; } - colleagueList.push({ ...colleague, id: runningId++ }); - res.send(colleague); + const id = runningId++; + const newColleague = { ...colleague, id }; + colleagueList.push(newColleague); + res.send(newColleague); }); fastify.put("/colleagues/:id", (req, res) => { const id = parseInt(req.params.id, 10); - const colleagueIndex = colleagueList.findIndex((c) => c.id === id); + const colleagueIndex = colleagueList.findIndex(c => c.id === id); if (colleagueIndex < 0) { res.status(404).send(); return; } - const colleague = JSON.parse(req.body); - colleagueList[colleagueIndex] = { ...colleague, id }; - res.send(colleague); + colleagueList[colleagueIndex] = { ...req.body, id }; + res.send(colleagueList[colleagueIndex]); }); fastify.delete("/colleagues/:id", (req, res) => { const id = parseInt(req.params.id, 10); - const colleagueIndex = colleagueList.findIndex((c) => c.id === id); + const colleagueIndex = colleagueList.findIndex(c => c.id === id); if (colleagueIndex < 0) { res.status(404).send(); @@ -75,7 +76,7 @@ fastify.delete("/colleagues/:id", (req, res) => { res.send(colleague); }); -fastify.listen({port: 4000}, (err, address) => { +fastify.listen({ port: 4000 }, (err, address) => { if (err) { throw err; }