From 429f89510898b96c05b7f96dd447afb088b1fe38 Mon Sep 17 00:00:00 2001 From: Mihovil Ilakovac Date: Fri, 31 Jan 2025 19:08:50 +0100 Subject: [PATCH] Headless tests for tutorial apps --- .../headless-tests/playwright.config.ts | 2 +- examples/tutorials/TodoApp/.gitignore | 3 + .../headless-tests/playwright.config.ts | 56 ++++++++++++++++++ .../TodoApp/headless-tests/tests/helpers.ts | 43 ++++++++++++++ .../headless-tests/tests/simple.spec.ts | 55 ++++++++++++++++++ examples/tutorials/TodoApp/package-lock.json | 57 ++++++++++++++++++- examples/tutorials/TodoApp/package.json | 1 + examples/tutorials/TodoAppTs/.gitignore | 3 + .../headless-tests/playwright.config.ts | 56 ++++++++++++++++++ .../TodoAppTs/headless-tests/tests/helpers.ts | 43 ++++++++++++++ .../headless-tests/tests/simple.spec.ts | 55 ++++++++++++++++++ .../tutorials/TodoAppTs/package-lock.json | 57 ++++++++++++++++++- examples/tutorials/TodoAppTs/package.json | 1 + 13 files changed, 429 insertions(+), 3 deletions(-) create mode 100644 examples/tutorials/TodoApp/headless-tests/playwright.config.ts create mode 100644 examples/tutorials/TodoApp/headless-tests/tests/helpers.ts create mode 100644 examples/tutorials/TodoApp/headless-tests/tests/simple.spec.ts create mode 100644 examples/tutorials/TodoAppTs/headless-tests/playwright.config.ts create mode 100644 examples/tutorials/TodoAppTs/headless-tests/tests/helpers.ts create mode 100644 examples/tutorials/TodoAppTs/headless-tests/tests/simple.spec.ts diff --git a/examples/streaming/headless-tests/playwright.config.ts b/examples/streaming/headless-tests/playwright.config.ts index 776e0912a0..13825aff49 100644 --- a/examples/streaming/headless-tests/playwright.config.ts +++ b/examples/streaming/headless-tests/playwright.config.ts @@ -45,7 +45,7 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { command: - "wasp-app-runner --app-path=../ --app-name=examples-waspleau --db-type sqlite", + "wasp-app-runner --app-path=../ --app-name=examples-streaming --db-type sqlite", // Wait for the backend to start url: "http://localhost:3001", diff --git a/examples/tutorials/TodoApp/.gitignore b/examples/tutorials/TodoApp/.gitignore index fd453426d8..08bda14889 100644 --- a/examples/tutorials/TodoApp/.gitignore +++ b/examples/tutorials/TodoApp/.gitignore @@ -9,3 +9,6 @@ node_modules/ # Don't ignore example dotenv files. !.env.example !.env.*.example + +# Headless tests +test-results/ diff --git a/examples/tutorials/TodoApp/headless-tests/playwright.config.ts b/examples/tutorials/TodoApp/headless-tests/playwright.config.ts new file mode 100644 index 0000000000..8fdd0116ee --- /dev/null +++ b/examples/tutorials/TodoApp/headless-tests/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * 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: process.env.CI ? "dot" : "list", + /* 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://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + /* Test against mobile viewports. */ + { + name: "Mobile Chrome", + use: { ...devices["Pixel 5"] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: + "wasp-app-runner --app-path=../ --app-name=examples-tutorials-TodoApp --db-type sqlite", + + // Wait for the backend to start + url: "http://localhost:3001", + reuseExistingServer: !process.env.CI, + timeout: 180 * 1000, + gracefulShutdown: { signal: "SIGTERM", timeout: 500 }, + }, +}); diff --git a/examples/tutorials/TodoApp/headless-tests/tests/helpers.ts b/examples/tutorials/TodoApp/headless-tests/tests/helpers.ts new file mode 100644 index 0000000000..005cbac02e --- /dev/null +++ b/examples/tutorials/TodoApp/headless-tests/tests/helpers.ts @@ -0,0 +1,43 @@ +import type { Page } from "@playwright/test"; + +export async function performSignup( + page: Page, + { username, password }: { username: string; password: string } +) { + await page.goto("/signup"); + + await page.waitForSelector("text=Create a new account"); + + await page.locator("input[name='username']").fill(username); + await page.locator("input[type='password']").fill(password); + await page.locator("button").click(); +} + +export async function performLogin( + page: Page, + { + username, + password, + }: { + username: string; + password: string; + } +) { + await page.goto("/login"); + + await page.waitForSelector("text=Log in to your account"); + + await page.locator("input[name='username']").fill(username); + await page.locator("input[type='password']").fill(password); + await page.getByRole("button", { name: "Log in" }).click(); +} + +export function generateRandomCredentials(): { + username: string; + password: string; +} { + return { + username: `test${Math.random().toString(36).substring(7)}`, + password: "12345678", + }; +} diff --git a/examples/tutorials/TodoApp/headless-tests/tests/simple.spec.ts b/examples/tutorials/TodoApp/headless-tests/tests/simple.spec.ts new file mode 100644 index 0000000000..3aeb118034 --- /dev/null +++ b/examples/tutorials/TodoApp/headless-tests/tests/simple.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from "@playwright/test"; +import { + generateRandomCredentials, + performLogin, + performSignup, +} from "./helpers"; + +test.describe("auth and work with tasks", () => { + const { username, password } = generateRandomCredentials(); + + test.describe.configure({ mode: "serial" }); + + test("can sign up", async ({ page }) => { + await performSignup(page, { + username, + password, + }); + + await expect(page).toHaveURL("/"); + + await page.getByText("Logout").click(); + + await expect(page).toHaveURL("/login"); + }); + + test("can log in and cast a vote", async ({ page }) => { + await performLogin(page, { + username, + password: "12345678xxx", + }); + + await expect(page.locator("body")).toContainText("Invalid credentials"); + + await performLogin(page, { + username, + password, + }); + + await expect(page).toHaveURL("/"); + + const randomTask = `New Task ${Math.random().toString(36).substring(7)}`; + // Fill input[name="description"] with random task + await page.locator("input[name='description']").fill(randomTask); + // Click input[type="submit"] to submit the form + await page.locator("input[type='submit']").click(); + // Expect to see the task on the page + await expect(page.locator("body")).toContainText(randomTask); + // Check the task as done input[type="checkbox"] + await page.locator("input[type='checkbox']").click(); + // Reload the page + await page.reload(); + // Expect the task to be checked + await expect(page.locator("input[type='checkbox']")).toBeChecked(); + }); +}); diff --git a/examples/tutorials/TodoApp/package-lock.json b/examples/tutorials/TodoApp/package-lock.json index f0b90bad8f..5b0a93bee5 100644 --- a/examples/tutorials/TodoApp/package-lock.json +++ b/examples/tutorials/TodoApp/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "TodoApp", "dependencies": { + "@playwright/test": "^1.50.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.2", @@ -1299,6 +1300,20 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", + "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", + "dependencies": { + "playwright": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -5172,6 +5187,47 @@ "pathe": "^1.1.2" } }, + "node_modules/playwright": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", + "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", + "dependencies": { + "playwright-core": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", + "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==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -7644,7 +7700,6 @@ "version": "3.24.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/examples/tutorials/TodoApp/package.json b/examples/tutorials/TodoApp/package.json index e9e104d72c..9c31ee1d77 100644 --- a/examples/tutorials/TodoApp/package.json +++ b/examples/tutorials/TodoApp/package.json @@ -2,6 +2,7 @@ "name": "TodoApp", "type": "module", "dependencies": { + "@playwright/test": "^1.50.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.2", diff --git a/examples/tutorials/TodoAppTs/.gitignore b/examples/tutorials/TodoAppTs/.gitignore index fd453426d8..08bda14889 100644 --- a/examples/tutorials/TodoAppTs/.gitignore +++ b/examples/tutorials/TodoAppTs/.gitignore @@ -9,3 +9,6 @@ node_modules/ # Don't ignore example dotenv files. !.env.example !.env.*.example + +# Headless tests +test-results/ diff --git a/examples/tutorials/TodoAppTs/headless-tests/playwright.config.ts b/examples/tutorials/TodoAppTs/headless-tests/playwright.config.ts new file mode 100644 index 0000000000..79ef296fe5 --- /dev/null +++ b/examples/tutorials/TodoAppTs/headless-tests/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * 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: process.env.CI ? "dot" : "list", + /* 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://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + /* Test against mobile viewports. */ + { + name: "Mobile Chrome", + use: { ...devices["Pixel 5"] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: + "wasp-app-runner --app-path=../ --app-name=examples-tutorials-TodoAppTs --db-type sqlite", + + // Wait for the backend to start + url: "http://localhost:3001", + reuseExistingServer: !process.env.CI, + timeout: 180 * 1000, + gracefulShutdown: { signal: "SIGTERM", timeout: 500 }, + }, +}); diff --git a/examples/tutorials/TodoAppTs/headless-tests/tests/helpers.ts b/examples/tutorials/TodoAppTs/headless-tests/tests/helpers.ts new file mode 100644 index 0000000000..005cbac02e --- /dev/null +++ b/examples/tutorials/TodoAppTs/headless-tests/tests/helpers.ts @@ -0,0 +1,43 @@ +import type { Page } from "@playwright/test"; + +export async function performSignup( + page: Page, + { username, password }: { username: string; password: string } +) { + await page.goto("/signup"); + + await page.waitForSelector("text=Create a new account"); + + await page.locator("input[name='username']").fill(username); + await page.locator("input[type='password']").fill(password); + await page.locator("button").click(); +} + +export async function performLogin( + page: Page, + { + username, + password, + }: { + username: string; + password: string; + } +) { + await page.goto("/login"); + + await page.waitForSelector("text=Log in to your account"); + + await page.locator("input[name='username']").fill(username); + await page.locator("input[type='password']").fill(password); + await page.getByRole("button", { name: "Log in" }).click(); +} + +export function generateRandomCredentials(): { + username: string; + password: string; +} { + return { + username: `test${Math.random().toString(36).substring(7)}`, + password: "12345678", + }; +} diff --git a/examples/tutorials/TodoAppTs/headless-tests/tests/simple.spec.ts b/examples/tutorials/TodoAppTs/headless-tests/tests/simple.spec.ts new file mode 100644 index 0000000000..3aeb118034 --- /dev/null +++ b/examples/tutorials/TodoAppTs/headless-tests/tests/simple.spec.ts @@ -0,0 +1,55 @@ +import { test, expect } from "@playwright/test"; +import { + generateRandomCredentials, + performLogin, + performSignup, +} from "./helpers"; + +test.describe("auth and work with tasks", () => { + const { username, password } = generateRandomCredentials(); + + test.describe.configure({ mode: "serial" }); + + test("can sign up", async ({ page }) => { + await performSignup(page, { + username, + password, + }); + + await expect(page).toHaveURL("/"); + + await page.getByText("Logout").click(); + + await expect(page).toHaveURL("/login"); + }); + + test("can log in and cast a vote", async ({ page }) => { + await performLogin(page, { + username, + password: "12345678xxx", + }); + + await expect(page.locator("body")).toContainText("Invalid credentials"); + + await performLogin(page, { + username, + password, + }); + + await expect(page).toHaveURL("/"); + + const randomTask = `New Task ${Math.random().toString(36).substring(7)}`; + // Fill input[name="description"] with random task + await page.locator("input[name='description']").fill(randomTask); + // Click input[type="submit"] to submit the form + await page.locator("input[type='submit']").click(); + // Expect to see the task on the page + await expect(page.locator("body")).toContainText(randomTask); + // Check the task as done input[type="checkbox"] + await page.locator("input[type='checkbox']").click(); + // Reload the page + await page.reload(); + // Expect the task to be checked + await expect(page.locator("input[type='checkbox']")).toBeChecked(); + }); +}); diff --git a/examples/tutorials/TodoAppTs/package-lock.json b/examples/tutorials/TodoAppTs/package-lock.json index 369536a0c2..dc19c8b807 100644 --- a/examples/tutorials/TodoAppTs/package-lock.json +++ b/examples/tutorials/TodoAppTs/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "TodoAppTs", "dependencies": { + "@playwright/test": "^1.50.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.2", @@ -1299,6 +1300,20 @@ "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", + "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", + "dependencies": { + "playwright": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.28", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", @@ -5172,6 +5187,47 @@ "pathe": "^1.1.2" } }, + "node_modules/playwright": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", + "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", + "dependencies": { + "playwright-core": "1.50.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", + "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", + "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==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -7644,7 +7700,6 @@ "version": "3.24.1", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", - "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/examples/tutorials/TodoAppTs/package.json b/examples/tutorials/TodoAppTs/package.json index 0ca91c05ac..e38852bf8c 100644 --- a/examples/tutorials/TodoAppTs/package.json +++ b/examples/tutorials/TodoAppTs/package.json @@ -2,6 +2,7 @@ "name": "TodoAppTs", "type": "module", "dependencies": { + "@playwright/test": "^1.50.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.26.2",