diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 66fd24d8..fd6acc2f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,15 @@ You will also see any lint errors in the console. Launches the test runner in the interactive watch mode. -6. Update package-lock.json when installing a new lib: +6. Run end to end tests + + ```sh + pnpm e2e:ui + ``` + +Launches the e2e test runner in the interactive mode. + +7. Update package-lock.json when installing a new lib: ```sh npm i --package-lock-only diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts deleted file mode 100644 index e05ec8d5..00000000 --- a/e2e/example.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { expect, test } from "@playwright/test"; - -test("has title", async ({ page }) => { - await page.goto("https://playwright.dev/"); - - // Expect a title "to contain" a substring. - await expect(page).toHaveTitle(/Playwright/); -}); - -test("get started link", async ({ page }) => { - await page.goto("https://playwright.dev/"); - - // Click the get started link. - await page.getByRole("link", { name: "Get started" }).click(); - - // Expects page to have a heading with the name of Installation. - await expect( - page.getByRole("heading", { name: "Installation" }), - ).toBeVisible(); -}); diff --git a/e2e/happyPath.spec.ts b/e2e/happyPath.spec.ts new file mode 100644 index 00000000..19e74076 --- /dev/null +++ b/e2e/happyPath.spec.ts @@ -0,0 +1,164 @@ +import { expect, test } from "@playwright/test"; + +test("should complete the happy path", async ({ page, isMobile }) => { + await page.goto("/"); + + // "should show landing page + await expect(page.getByText("new")).toBeVisible(); + await expect(page.getByText("help")).toBeVisible(); + + // should create new budget + await page.getByText("new").click(); + + await expect(page.getByText("Statistics")).toBeVisible(); + await expect(page.getByText("Revenue")).toBeVisible(); + await expect(page.getByText("Expenses")).toBeVisible(); + + // should create new incomes + await page.locator("#Revenue-1-name").click(); + await page.locator("#Revenue-1-name").fill("salary"); + + await expect(page.locator("#Revenue-1-name")).toHaveValue("salary"); + + await page.locator("#Revenue-1-value").click(); + await page.locator("#Revenue-1-value").fill("1000"); + await page.locator("#Revenue-1-name").click(); + + await expect(page.locator("#Revenue-1-value")).toHaveValue("$1,000"); + + await page.getByLabel("add item to Revenue").click(); + await page.getByLabel("item 2 name").fill("sale"); + await page.getByLabel("item 2 name").press("Tab"); + await page.getByLabel("item 2 value").fill("50"); + await page.getByLabel("item 2 value").press("Tab"); + + await expect(page.getByLabel("item 2 name")).toHaveValue("sale"); + await expect(page.getByLabel("item 2 value")).toHaveValue("$50"); + + await page.locator("#Revenue-2-operate-button").press("Tab"); + await page.getByLabel("delete item 2").press("Tab"); + await page.getByLabel("add item to Revenue").press("Enter"); + await page.getByLabel("item 3 name").fill("bonus"); + await page.getByLabel("item 3 name").press("Tab"); + await page.getByLabel("item 3 value").fill("25"); + await page.getByLabel("item 3 value").press("Tab"); + await page.locator("#Revenue-3-operate-button").press("Enter"); + await page.getByLabel("add", { exact: true }).fill("10"); + await page.getByLabel("add", { exact: true }).press("Enter"); + + await expect(page.getByLabel("item 3 name")).toHaveValue("bonus"); + await expect(page.getByLabel("item 3 value")).toHaveValue("$35"); + + // should create new expenses + await page.locator("#Expenses-1-name").click(); + await page.locator("#Expenses-1-name").fill("rent"); + await page.locator("#Expenses-1-name").press("Tab"); + await page.locator("#Expenses-1-value").fill("500.75"); + await page.locator("#Expenses-1-value").press("Tab"); + await page.locator("#Expenses-1-operate-button").press("Tab"); + await page.locator("#delete-Expenses-1-button").press("Tab"); + + await expect(page.locator("#Expenses-1-name")).toHaveValue("rent"); + await expect(page.locator("#Expenses-1-value")).toHaveValue("$500.75"); + + await page.getByLabel("add item to Expenses").press("Enter"); + await page.locator("#Expenses-2-name").fill("groceries"); + await page.locator("#Expenses-2-name").press("Tab"); + await page.locator("#Expenses-2-value").fill("100"); + await page.locator("#Expenses-2-value").press("Tab"); + await page.locator("#Expenses-2-operate-button").press("Tab"); + await page.locator("#delete-Expenses-2-button").press("Tab"); + + await expect(page.locator("#Expenses-2-name")).toHaveValue("groceries"); + await expect(page.locator("#Expenses-2-value")).toHaveValue("$100"); + + await page.getByLabel("add item to Expenses").press("Enter"); + await page.locator("#Expenses-3-name").fill("item"); + await page.locator("#Expenses-3-name").press("Tab"); + await page.locator("#Expenses-3-value").fill("50"); + + await expect(page.locator("#Expenses-3-name")).toHaveValue("item"); + await expect(page.locator("#Expenses-3-value")).toHaveValue("$50"); + + await page.locator("#Expenses-3-operate-button").click(); + await page.getByLabel("select type of operation on").click(); + await page.getByLabel("subtraction").click(); + await page.getByLabel("subtract", { exact: true }).click(); + await page.getByLabel("subtract", { exact: true }).fill("50"); + await page.getByLabel("apply change to item value").click(); + + await expect(page.locator("#Expenses-3-value")).toHaveValue("$0"); + + await page.getByLabel("add item to Expenses").click(); + await page.getByLabel("item 4 name").fill("mistake"); + await page.getByLabel("item 4 value").click(); + await page.getByLabel("item 4 value").fill("20"); + await expect(page.locator("#Expenses-4-name")).toHaveValue("mistake"); + await expect(page.locator("#Expenses-4-value")).toHaveValue("$20"); + + await page.getByLabel("delete item 4").click(); + await page.getByLabel("confirm item 4 deletion").click(); + + await expect(page.locator("#Expenses-4-name")).not.toBeVisible(); + await expect(page.locator("#Expenses-4-value")).not.toBeVisible(); + + // should undo changes + if (isMobile) { + await page.getByLabel("Toggle navigation").click(); + await expect(page.locator("#offcanvasNavbarLabel-expand-md")).toBeVisible(); + } + + await page.getByLabel("undo change").click(); + + await expect(page.locator("#Expenses-4-name")).toHaveValue("mistake"); + await expect(page.locator("#Expenses-4-value")).toHaveValue("$20"); + + // should handle statistics changes + await page.getByLabel("reserves").click(); + await page.getByLabel("reserves").fill("2000"); + + await expect(page.locator("#reserves")).toHaveValue("$2,000"); + await expect(page.getByLabel("available", { exact: true })).toHaveValue( + "$464.25", + ); + await expect(page.locator("#with-goal")).toHaveValue("$355.75"); + await expect(page.locator("#saved")).toHaveValue("$108.5"); + + await page.getByLabel("calculate savings goal").click(); + + await expect(page.getByLabel("available", { exact: true })).toHaveValue( + "$464.25", + ); + await expect(page.locator("#with-goal")).toHaveValue("$0"); + await expect(page.locator("#saved")).toHaveValue("$464.25"); + + // should handle budget changes + await page.getByLabel("budget name").fill("2024-01"); + await expect(page.getByLabel("budget name")).toHaveValue("2024-01"); + + if (isMobile) { + await page.getByLabel("Toggle navigation").click(); + } + + await page.getByLabel("new budget").click(); + + await expect(page.getByLabel("go to older budget")).toBeVisible(); + await expect(page.getByLabel("budget name")).not.toHaveValue("2024-01"); + + if (isMobile) { + await page.getByLabel("Toggle navigation").click(); + } + await page.getByLabel("delete budget").click(); + await page.getByLabel("confirm budget deletion").click(); + + await expect(page.getByLabel("budget name")).toHaveValue("2024-01"); + + if (isMobile) { + await page.getByLabel("Toggle navigation").click(); + } + await page.getByLabel("clone budget").click(); + + await expect(page.getByLabel("budget name")).toHaveValue("2024-01-clone"); + + await page.close(); +}); diff --git a/e2e/settingsHappyPath.spec.ts b/e2e/settingsHappyPath.spec.ts new file mode 100644 index 00000000..caaf2d70 --- /dev/null +++ b/e2e/settingsHappyPath.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from "@playwright/test"; +import fs from "fs"; + +test("should complete the settings happy path", async ({ page, isMobile }) => { + await page.goto("/"); + + // should show charts page + await page.getByText("new").click(); + await page.locator("#Expenses-1-name").click(); + await page.locator("#Expenses-1-name").fill("rent"); + await page.locator("#Expenses-1-name").press("Tab"); + await page.locator("#Expenses-1-value").fill("500.75"); + + await expect(page.locator("#Expenses-1-name")).toHaveValue("rent"); + await expect(page.locator("#Expenses-1-value")).toHaveValue("$500.75"); + + await page.getByLabel("open charts view").click(); + + await expect(page.getByText("Revenue vs expenses")).toBeVisible(); + await expect(page.getByText("Savings", { exact: true })).toBeVisible(); + await expect(page.getByText("Reserves", { exact: true })).toBeVisible(); + await expect(page.getByText("Available vs with goal")).toBeVisible(); + await expect(page.getByText("Savings goal")).toBeVisible(); + + await page.getByPlaceholder("Filter...").click(); + await page.getByPlaceholder("Filter...").fill("rent"); + await page.getByLabel(/rent/).click(); + + await page.getByText("strict match").click(); + await expect(page.getByText("Expenses filtered by: rent")).toBeVisible(); + + await page.getByLabel("go back to budgets").click(); + + await expect(page.getByText("Statistics")).toBeVisible(); + + // should handle settings changes + if (isMobile) { + await page.getByLabel("Toggle navigation").click(); + } + await page.getByLabel("budget settings").click(); + await page.getByLabel("select display currency").click(); + await page.getByLabel("select display currency").fill("eur"); + await page.getByLabel("EUR").click(); + + await expect(page.getByText("€0.00")).toBeVisible(); + await expect(page.getByText("CSV")).toBeVisible(); + + // should handle downloads + const csvDownloadPromise = page.waitForEvent("download"); + await page.getByLabel("export budget as csv").click(); + const csvDownload = await csvDownloadPromise; + expect(csvDownload.suggestedFilename()).toMatch(/.csv/); + expect( + (await fs.promises.stat(await csvDownload.path())).size, + ).toBeGreaterThan(0); + + await page.getByLabel("budget settings").click(); + await expect(page.getByText("JSON")).toBeVisible(); + + const jsonDownloadPromise = page.waitForEvent("download"); + await page.getByLabel("export budget as json").click(); + const jsonDownload = await jsonDownloadPromise; + expect(jsonDownload.suggestedFilename()).toMatch(/.json/); + expect( + (await fs.promises.stat(await jsonDownload.path())).size, + ).toBeGreaterThan(0); + + // should handle import + await page.locator("#import").setInputFiles("./guitos-sample.json"); + + await expect(page.getByLabel("go to older budget")).toBeVisible(); + await page.getByLabel("go to older budget").click(); + + if (isMobile) { + await page.getByLabel("Toggle navigation").click(); + } + + await expect( + page.getByRole("combobox", { name: "search in budgets" }), + ).toBeVisible(); + + await expect(page.getByLabel("budget name")).toHaveValue("2023-08"); + + await page.close(); +}); diff --git a/package.json b/package.json index 29e27640..c13fb305 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,10 @@ "build": "vite build", "serve": "vite preview", "test": "vitest", + "e2e": "playwright test --reporter=list --project chromium", + "e2e:ui": "playwright test --ui --project chromium", + "e2e:mobile": "playwright test --reporter=list --project 'Mobile Chrome'", + "e2e:mobile:ui": "playwright test --ui --project 'Mobile Chrome'", "coverage": "vitest run --coverage --silent", "coverage:ui": "vitest --ui --open --coverage --silent", "lint": "pnpm dlx eslint src e2e --fix && pnpm dlx stylelint src/**/*.css --fix && pnpm dlx prettier --write src e2e", diff --git a/playwright.config.ts b/playwright.config.ts index 5a99d134..51aa8653 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -24,7 +24,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - // baseURL: 'http://127.0.0.1:3000', + baseURL: "http://localhost:5173", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -56,22 +56,12 @@ export default defineConfig({ name: "Mobile Safari", use: { ...devices["iPhone 12"] }, }, - - /* Test against branded browsers. */ - // { - // name: "Microsoft Edge", - // use: { ...devices["Desktop Edge"], channel: "msedge" }, - // }, - // { - // name: "Google Chrome", - // use: { ...devices["Desktop Chrome"], channel: "chrome" }, - // }, ], /* Run your local dev server before starting the tests */ - // webServer: { - // command: 'npm run start', - // url: 'http://127.0.0.1:3000', - // reuseExistingServer: !process.env.CI, - // }, + webServer: { + command: "pnpm run start", + url: "http://localhost:5173", + reuseExistingServer: !process.env.CI, + }, });