Give the tests their own database and drop the template specs

pytest was deleting every user on teardown against the development
database. The engine is built at import time, so tests/__init__.py pins
POSTGRES_DB before app.core.config loads; the fixture creates the schema
and drops the database again, guarded against a name that is not _test.

Playwright now defaults at the integrated stack, which is the origin the
API allows, so a bare `bunx playwright test` works without a dev server.
The four template specs that asserted copy we no longer ship are gone,
along with the helpers they were the last callers of. The admin edit
assertion was page-wide and only ever passed on a fresh database.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KkmeRiyeYmVZqJVwuyHq9o
This commit is contained in:
Melvin Strobl
2026-08-15 21:19:12 +02:00
co-authored by Claude Opus 5
parent aa0e760615
commit 82356810ce
11 changed files with 40 additions and 858 deletions
+11
View File
@@ -0,0 +1,11 @@
"""Redirect the suite at its own database.
`app.core.db` builds the engine at import time and several modules bind that
object, so the name has to be in the environment before anything imports the
settings. Overriding it here — the first module pytest imports for the
package — keeps a test run from touching the development data.
"""
import os
os.environ["POSTGRES_DB"] = "app_test"
+23 -5
View File
@@ -2,12 +2,13 @@ from collections.abc import Generator
import pytest import pytest
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from sqlmodel import Session, delete from sqlalchemy import create_engine, text
from sqlalchemy.engine import make_url
from sqlmodel import Session, SQLModel
from app.core.config import settings from app.core.config import settings
from app.core.db import engine, init_db from app.core.db import engine, init_db
from app.main import app from app.main import app
from app.models import User
from tests.utils.user import authentication_token_from_email from tests.utils.user import authentication_token_from_email
from tests.utils.utils import get_superuser_token_headers from tests.utils.utils import get_superuser_token_headers
@@ -23,12 +24,29 @@ def flow_data(tmp_path_factory: pytest.TempPathFactory) -> Generator[None, None,
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def db() -> Generator[Session, None, None]: def db() -> Generator[Session, None, None]:
"""Create the throwaway database `tests/__init__.py` points at, drop it after."""
url = make_url(str(settings.SQLALCHEMY_DATABASE_URI))
# The teardown drops this database, so refuse to run against anything but
# the dedicated test one.
assert url.database and url.database.endswith("_test"), url.database
maintenance = create_engine(
url.set(database="postgres"), isolation_level="AUTOCOMMIT"
)
drop = text(f'DROP DATABASE IF EXISTS "{url.database}" WITH (FORCE)')
with maintenance.connect() as connection:
connection.execute(drop)
connection.execute(text(f'CREATE DATABASE "{url.database}"'))
SQLModel.metadata.create_all(engine)
with Session(engine) as session: with Session(engine) as session:
init_db(session) init_db(session)
yield session yield session
statement = delete(User)
session.execute(statement) engine.dispose()
session.commit() with maintenance.connect() as connection:
connection.execute(drop)
maintenance.dispose()
@pytest.fixture(scope="module") @pytest.fixture(scope="module")
+4 -14
View File
@@ -23,10 +23,10 @@ export default defineConfig({
reporter: process.env.CI ? 'blob' : 'html', reporter: process.env.CI ? 'blob' : 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: { use: {
/* Base URL to use in actions like `await page.goto('/')`. /* Base URL to use in actions like `await page.goto('/')`. Defaults to the
Point PLAYWRIGHT_BASE_URL at the integrated stack (http://app.localhost) integrated stack (`make dev`), the only origin the API allows CORS from.
to test what `make dev` is serving; the API only allows CORS from there. */ Point PLAYWRIGHT_BASE_URL elsewhere to test another running server. */
baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:5173', baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://app.localhost',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry', trace: 'on-first-retry',
@@ -83,14 +83,4 @@ export default defineConfig({
// use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// }, // },
], ],
/* Run your local dev server before starting the tests, unless we were
pointed at an already-running one. */
webServer: process.env.PLAYWRIGHT_BASE_URL
? undefined
: {
command: 'bun run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
}); });
-97
View File
@@ -1,97 +0,0 @@
import { Link as RouterLink } from "@tanstack/react-router"
import { ChevronsUpDown, LogOut, Settings } from "lucide-react"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import {
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from "@/components/ui/sidebar"
import useAuth from "@/hooks/useAuth"
import { getInitials } from "@/utils"
interface UserInfoProps {
fullName?: string
email?: string
}
function UserInfo({ fullName, email }: UserInfoProps) {
return (
<div className="flex items-center gap-2.5 w-full min-w-0">
<Avatar className="size-8">
<AvatarFallback className="bg-zinc-600 text-white">
{getInitials(fullName || "User")}
</AvatarFallback>
</Avatar>
<div className="flex flex-col items-start min-w-0">
<p className="text-sm font-medium truncate w-full">{fullName}</p>
<p className="text-xs text-muted-foreground truncate w-full">{email}</p>
</div>
</div>
)
}
export function User({ user }: { user: any }) {
const { logout } = useAuth()
const { isMobile, setOpenMobile } = useSidebar()
if (!user) return null
const handleMenuClick = () => {
if (isMobile) {
setOpenMobile(false)
}
}
const handleLogout = async () => {
logout()
}
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
data-testid="user-menu"
>
<UserInfo fullName={user?.full_name} email={user?.email} />
<ChevronsUpDown className="ml-auto size-4 text-muted-foreground" />
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
side={isMobile ? "bottom" : "right"}
align="end"
sideOffset={4}
>
<DropdownMenuLabel className="p-0 font-normal">
<UserInfo fullName={user?.full_name} email={user?.email} />
</DropdownMenuLabel>
<DropdownMenuSeparator />
<RouterLink to="/settings" onClick={handleMenuClick}>
<DropdownMenuItem>
<Settings />
User Settings
</DropdownMenuItem>
</RouterLink>
<DropdownMenuItem onClick={handleLogout}>
<LogOut />
Log Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}
+2 -1
View File
@@ -93,7 +93,8 @@ test.describe("Admin user management", () => {
await page.getByRole("button", { name: "Save" }).click() await page.getByRole("button", { name: "Save" }).click()
await expect(page.getByText("User updated successfully")).toBeVisible() await expect(page.getByText("User updated successfully")).toBeVisible()
await expect(page.getByText(updatedName)).toBeVisible() // Scoped to this run's row: earlier runs leave their own "Updated Name".
await expect(userRow.getByText(updatedName)).toBeVisible()
}) })
test("Delete a user successfully", async ({ page }) => { test("Delete a user successfully", async ({ page }) => {
-117
View File
@@ -1,117 +0,0 @@
import { expect, type Page, test } from "@playwright/test"
import { firstSuperuser, firstSuperuserPassword } from "./config.ts"
import { randomPassword } from "./utils/random.ts"
test.use({ storageState: { cookies: [], origins: [] } })
const fillForm = async (page: Page, email: string, password: string) => {
await page.getByTestId("email-input").fill(email)
await page.getByTestId("password-input").fill(password)
}
const verifyInput = async (page: Page, testId: string) => {
const input = page.getByTestId(testId)
await expect(input).toBeVisible()
await expect(input).toHaveText("")
await expect(input).toBeEditable()
}
test("Inputs are visible, empty and editable", async ({ page }) => {
await page.goto("/login")
await verifyInput(page, "email-input")
await verifyInput(page, "password-input")
})
test("Log In button is visible", async ({ page }) => {
await page.goto("/login")
await expect(page.getByRole("button", { name: "Log In" })).toBeVisible()
})
test("Forgot Password link is visible", async ({ page }) => {
await page.goto("/login")
await expect(
page.getByRole("link", { name: "Forgot your password?" }),
).toBeVisible()
})
test("Log in with valid email and password ", async ({ page }) => {
await page.goto("/login")
await fillForm(page, firstSuperuser, firstSuperuserPassword)
await page.getByRole("button", { name: "Log In" }).click()
await page.waitForURL("/")
await expect(
page.getByText("Welcome back, nice to see you again!"),
).toBeVisible()
})
test("Log in with invalid email", async ({ page }) => {
await page.goto("/login")
await fillForm(page, "invalidemail", firstSuperuserPassword)
await page.getByRole("button", { name: "Log In" }).click()
await expect(page.getByText("Invalid email address")).toBeVisible()
})
test("Log in with invalid password", async ({ page }) => {
const password = randomPassword()
await page.goto("/login")
await fillForm(page, firstSuperuser, password)
await page.getByRole("button", { name: "Log In" }).click()
await expect(page.getByText("Incorrect email or password")).toBeVisible()
})
test("Successful log out", async ({ page }) => {
await page.goto("/login")
await fillForm(page, firstSuperuser, firstSuperuserPassword)
await page.getByRole("button", { name: "Log In" }).click()
await page.waitForURL("/")
await expect(
page.getByText("Welcome back, nice to see you again!"),
).toBeVisible()
await page.getByTestId("user-menu").click()
await page.getByRole("menuitem", { name: "Log out" }).click()
await page.waitForURL("/login")
})
test("Logged-out user cannot access protected routes", async ({ page }) => {
await page.goto("/login")
await fillForm(page, firstSuperuser, firstSuperuserPassword)
await page.getByRole("button", { name: "Log In" }).click()
await page.waitForURL("/")
await expect(
page.getByText("Welcome back, nice to see you again!"),
).toBeVisible()
await page.getByTestId("user-menu").click()
await page.getByRole("menuitem", { name: "Log out" }).click()
await page.waitForURL("/login")
await page.goto("/settings")
await page.waitForURL("/login")
})
test("Redirects to /login when token is wrong", async ({ page }) => {
await page.goto("/settings")
await page.evaluate(() => {
localStorage.setItem("access_token", "invalid_token")
})
await page.goto("/settings")
await page.waitForURL("/login")
await expect(page).toHaveURL("/login")
})
-125
View File
@@ -1,125 +0,0 @@
import { expect, test } from "@playwright/test"
import { findLastEmail } from "./utils/mailcatcher"
import { randomEmail, randomPassword } from "./utils/random"
import { logInUser, signUpNewUser } from "./utils/user"
test.use({ storageState: { cookies: [], origins: [] } })
test("Password Recovery title is visible", async ({ page }) => {
await page.goto("/recover-password")
await expect(
page.getByRole("heading", { name: "Password Recovery" }),
).toBeVisible()
})
test("Input is visible, empty and editable", async ({ page }) => {
await page.goto("/recover-password")
await expect(page.getByTestId("email-input")).toBeVisible()
await expect(page.getByTestId("email-input")).toHaveText("")
await expect(page.getByTestId("email-input")).toBeEditable()
})
test("Continue button is visible", async ({ page }) => {
await page.goto("/recover-password")
await expect(page.getByRole("button", { name: "Continue" })).toBeVisible()
})
test("User can reset password successfully using the link", async ({
page,
request,
}) => {
const fullName = "Test User"
const email = randomEmail()
const password = randomPassword()
const newPassword = randomPassword()
// Sign up a new user
await signUpNewUser(page, fullName, email, password)
await page.goto("/recover-password")
await page.getByTestId("email-input").fill(email)
await page.getByRole("button", { name: "Continue" }).click()
const emailData = await findLastEmail({
request,
filter: (e) => e.recipients.includes(`<${email}>`),
timeout: 5000,
})
await page.goto(
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`,
)
const selector = 'a[href*="/reset-password?token="]'
let url = await page.getAttribute(selector, "href")
// TODO: update var instead of doing a replace
url = url!.replace("http://localhost/", "http://localhost:5173/")
// Set the new password and confirm it
await page.goto(url)
await page.getByTestId("new-password-input").fill(newPassword)
await page.getByTestId("confirm-password-input").fill(newPassword)
await page.getByRole("button", { name: "Reset Password" }).click()
await expect(page.getByText("Password updated successfully")).toBeVisible()
// Check if the user is able to login with the new password
await logInUser(page, email, newPassword)
})
test("Expired or invalid reset link", async ({ page }) => {
const password = randomPassword()
const invalidUrl = "/reset-password?token=invalidtoken"
await page.goto(invalidUrl)
await page.getByTestId("new-password-input").fill(password)
await page.getByTestId("confirm-password-input").fill(password)
await page.getByRole("button", { name: "Reset Password" }).click()
await expect(page.getByText("Invalid token")).toBeVisible()
})
test("Weak new password validation", async ({ page, request }) => {
const fullName = "Test User"
const email = randomEmail()
const password = randomPassword()
const weakPassword = "123"
// Sign up a new user
await signUpNewUser(page, fullName, email, password)
await page.goto("/recover-password")
await page.getByTestId("email-input").fill(email)
await page.getByRole("button", { name: "Continue" }).click()
const emailData = await findLastEmail({
request,
filter: (e) => e.recipients.includes(`<${email}>`),
timeout: 5000,
})
await page.goto(
`${process.env.MAILCATCHER_HOST}/messages/${emailData.id}.html`,
)
const selector = 'a[href*="/reset-password?token="]'
let url = await page.getAttribute(selector, "href")
url = url!.replace("http://localhost/", "http://localhost:5173/")
// Set a weak new password
await page.goto(url)
await page.getByTestId("new-password-input").fill(weakPassword)
await page.getByTestId("confirm-password-input").fill(weakPassword)
await page.getByRole("button", { name: "Reset Password" }).click()
await expect(
page.getByText("Password must be at least 8 characters"),
).toBeVisible()
})
-159
View File
@@ -1,159 +0,0 @@
import { expect, type Page, test } from "@playwright/test"
import { randomEmail, randomPassword } from "./utils/random"
test.use({ storageState: { cookies: [], origins: [] } })
const fillForm = async (
page: Page,
full_name: string,
email: string,
password: string,
confirm_password: string,
) => {
await page.getByTestId("full-name-input").fill(full_name)
await page.getByTestId("email-input").fill(email)
await page.getByTestId("password-input").fill(password)
await page.getByTestId("confirm-password-input").fill(confirm_password)
}
const verifyInput = async (page: Page, testId: string) => {
const input = page.getByTestId(testId)
await expect(input).toBeVisible()
await expect(input).toHaveText("")
await expect(input).toBeEditable()
}
test("Inputs are visible, empty and editable", async ({ page }) => {
await page.goto("/signup")
await verifyInput(page, "full-name-input")
await verifyInput(page, "email-input")
await verifyInput(page, "password-input")
await verifyInput(page, "confirm-password-input")
})
test("Sign Up button is visible", async ({ page }) => {
await page.goto("/signup")
await expect(page.getByRole("button", { name: "Sign Up" })).toBeVisible()
})
test("Log In link is visible", async ({ page }) => {
await page.goto("/signup")
await expect(page.getByRole("link", { name: "Log In" })).toBeVisible()
})
test("Sign up with valid name, email, and password", async ({ page }) => {
const full_name = "Test User"
const email = randomEmail()
const password = randomPassword()
await page.goto("/signup")
await fillForm(page, full_name, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()
})
test("Sign up with invalid email", async ({ page }) => {
await page.goto("/signup")
await fillForm(
page,
"Playwright Test",
"invalid-email",
"changethis",
"changethis",
)
await page.getByRole("button", { name: "Sign Up" }).click()
await expect(page.getByText("Invalid email address")).toBeVisible()
})
test("Sign up with existing email", async ({ page }) => {
const fullName = "Test User"
const email = randomEmail()
const password = randomPassword()
await page.goto("/signup")
await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()
await page.goto("/signup")
await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()
await page
.getByText("The user with this email already exists in the system")
.click()
})
test("Sign up with weak password", async ({ page }) => {
const fullName = "Test User"
const email = randomEmail()
const password = "weak"
await page.goto("/signup")
await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()
await expect(
page.getByText("Password must be at least 8 characters"),
).toBeVisible()
})
test("Sign up with mismatched passwords", async ({ page }) => {
const fullName = "Test User"
const email = randomEmail()
const password = randomPassword()
const password2 = randomPassword()
await page.goto("/signup")
await fillForm(page, fullName, email, password, password2)
await page.getByRole("button", { name: "Sign Up" }).click()
await expect(page.getByText("The passwords don't match")).toBeVisible()
})
test("Sign up with missing full name", async ({ page }) => {
const fullName = ""
const email = randomEmail()
const password = randomPassword()
await page.goto("/signup")
await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()
await expect(page.getByText("Full Name is required")).toBeVisible()
})
test("Sign up with missing email", async ({ page }) => {
const fullName = "Test User"
const email = ""
const password = randomPassword()
await page.goto("/signup")
await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()
await expect(page.getByText("Invalid email address")).toBeVisible()
})
test("Sign up with missing password", async ({ page }) => {
const fullName = ""
const email = randomEmail()
const password = ""
await page.goto("/signup")
await fillForm(page, fullName, email, password, password)
await page.getByRole("button", { name: "Sign Up" }).click()
await expect(page.getByText("Password is required")).toBeVisible()
})
-256
View File
@@ -1,256 +0,0 @@
import { expect, test } from "@playwright/test"
import { firstSuperuser, firstSuperuserPassword } from "./config.ts"
import { createUser } from "./utils/privateApi.ts"
import { randomEmail, randomPassword } from "./utils/random"
import { logInUser, logOutUser } from "./utils/user"
const tabs = ["My profile", "Password", "Danger zone"]
test("My profile tab is active by default", async ({ page }) => {
await page.goto("/settings")
await expect(page.getByRole("tab", { name: "My profile" })).toHaveAttribute(
"aria-selected",
"true",
)
})
test("All tabs are visible", async ({ page }) => {
await page.goto("/settings")
for (const tab of tabs) {
await expect(page.getByRole("tab", { name: tab })).toBeVisible()
}
})
test.describe("Edit user profile", () => {
test.use({ storageState: { cookies: [], origins: [] } })
let email: string
let password: string
test.beforeAll(async () => {
email = randomEmail()
password = randomPassword()
await createUser({ email, password })
})
test.beforeEach(async ({ page }) => {
await logInUser(page, email, password)
await page.goto("/settings")
await page.getByRole("tab", { name: "My profile" }).click()
})
test("Edit user name with a valid name", async ({ page }) => {
const updatedName = "Test User 2"
await page.getByRole("button", { name: "Edit" }).click()
await page.getByLabel("Full name").fill(updatedName)
await page.getByRole("button", { name: "Save" }).click()
await expect(page.getByText("User updated successfully")).toBeVisible()
await expect(
page.locator("form").getByText(updatedName, { exact: true }),
).toBeVisible()
})
test("Edit user email with an invalid email shows error", async ({
page,
}) => {
await page.getByRole("button", { name: "Edit" }).click()
await page.getByLabel("Email").fill("")
await page.locator("body").click()
await expect(page.getByText("Invalid email address")).toBeVisible()
})
})
test.describe("Edit user email", () => {
test.use({ storageState: { cookies: [], origins: [] } })
test("Edit user email with a valid email", async ({ page }) => {
const email = randomEmail()
const password = randomPassword()
const updatedEmail = randomEmail()
await createUser({ email, password })
await logInUser(page, email, password)
await page.goto("/settings")
await page.getByRole("tab", { name: "My profile" }).click()
await page.getByRole("button", { name: "Edit" }).click()
await page.getByLabel("Email").fill(updatedEmail)
await page.getByRole("button", { name: "Save" }).click()
await expect(page.getByText("User updated successfully")).toBeVisible()
await expect(
page.locator("form").getByText(updatedEmail, { exact: true }),
).toBeVisible()
})
})
test.describe("Cancel edit actions", () => {
test.use({ storageState: { cookies: [], origins: [] } })
test("Cancel edit action restores original name", async ({ page }) => {
const email = randomEmail()
const password = randomPassword()
const user = await createUser({ email, password })
await logInUser(page, email, password)
await page.goto("/settings")
await page.getByRole("tab", { name: "My profile" }).click()
await page.getByRole("button", { name: "Edit" }).click()
await page.getByLabel("Full name").fill("Test User")
await page.getByRole("button", { name: "Cancel" }).first().click()
await expect(
page.locator("form").getByText(user.full_name as string, { exact: true }),
).toBeVisible()
})
test("Cancel edit action restores original email", async ({ page }) => {
const email = randomEmail()
const password = randomPassword()
await createUser({ email, password })
await logInUser(page, email, password)
await page.goto("/settings")
await page.getByRole("tab", { name: "My profile" }).click()
await page.getByRole("button", { name: "Edit" }).click()
await page.getByLabel("Email").fill(randomEmail())
await page.getByRole("button", { name: "Cancel" }).first().click()
await expect(
page.locator("form").getByText(email, { exact: true }),
).toBeVisible()
})
})
test.describe("Change password", () => {
test.use({ storageState: { cookies: [], origins: [] } })
test("Update password successfully", async ({ page }) => {
const email = randomEmail()
const password = randomPassword()
const newPassword = randomPassword()
await createUser({ email, password })
await logInUser(page, email, password)
await page.goto("/settings")
await page.getByRole("tab", { name: "Password" }).click()
await page.getByTestId("current-password-input").fill(password)
await page.getByTestId("new-password-input").fill(newPassword)
await page.getByTestId("confirm-password-input").fill(newPassword)
await page.getByRole("button", { name: "Update Password" }).click()
await expect(page.getByText("Password updated successfully")).toBeVisible()
await logOutUser(page)
await logInUser(page, email, newPassword)
})
})
test.describe("Change password validation", () => {
test.use({ storageState: { cookies: [], origins: [] } })
let email: string
let password: string
test.beforeAll(async () => {
email = randomEmail()
password = randomPassword()
await createUser({ email, password })
})
test.beforeEach(async ({ page }) => {
await logInUser(page, email, password)
await page.goto("/settings")
await page.getByRole("tab", { name: "Password" }).click()
})
test("Update password with weak passwords", async ({ page }) => {
const weakPassword = "weak"
await page.getByTestId("current-password-input").fill(password)
await page.getByTestId("new-password-input").fill(weakPassword)
await page.getByTestId("confirm-password-input").fill(weakPassword)
await page.getByRole("button", { name: "Update Password" }).click()
await expect(
page.getByText("Password must be at least 8 characters"),
).toBeVisible()
})
test("New password and confirmation password do not match", async ({
page,
}) => {
await page.getByTestId("current-password-input").fill(password)
await page.getByTestId("new-password-input").fill(randomPassword())
await page.getByTestId("confirm-password-input").fill(randomPassword())
await page.getByRole("button", { name: "Update Password" }).click()
await expect(page.getByText("The passwords don't match")).toBeVisible()
})
test("Current password and new password are the same", async ({ page }) => {
await page.getByTestId("current-password-input").fill(password)
await page.getByTestId("new-password-input").fill(password)
await page.getByTestId("confirm-password-input").fill(password)
await page.getByRole("button", { name: "Update Password" }).click()
await expect(
page.getByText("New password cannot be the same as the current one"),
).toBeVisible()
})
})
test("Appearance button is visible in sidebar", async ({ page }) => {
await page.goto("/settings")
await expect(page.getByTestId("theme-button")).toBeVisible()
})
test("User can switch between theme modes", async ({ page }) => {
await page.goto("/settings")
await page.getByTestId("theme-button").click()
await page.getByTestId("dark-mode").click()
await expect(page.locator("html")).toHaveClass(/dark/)
await expect(page.getByTestId("dark-mode")).not.toBeVisible()
await page.getByTestId("theme-button").click()
await page.getByTestId("light-mode").click()
await expect(page.locator("html")).toHaveClass(/light/)
})
test("Selected mode is preserved across sessions", async ({ page }) => {
await page.goto("/settings")
await page.getByTestId("theme-button").click()
if (
await page.evaluate(() =>
document.documentElement.classList.contains("dark"),
)
) {
await page.getByTestId("light-mode").click()
await page.getByTestId("theme-button").click()
}
const isLightMode = await page.evaluate(() =>
document.documentElement.classList.contains("light"),
)
expect(isLightMode).toBe(true)
await page.getByTestId("theme-button").click()
await page.getByTestId("dark-mode").click()
let isDarkMode = await page.evaluate(() =>
document.documentElement.classList.contains("dark"),
)
expect(isDarkMode).toBe(true)
await logOutUser(page)
await logInUser(page, firstSuperuser, firstSuperuserPassword)
isDarkMode = await page.evaluate(() =>
document.documentElement.classList.contains("dark"),
)
expect(isDarkMode).toBe(true)
})
-62
View File
@@ -1,62 +0,0 @@
import type { APIRequestContext } from "@playwright/test"
type Email = {
id: number
recipients: string[]
subject: string
}
async function findEmail({
request,
filter,
}: {
request: APIRequestContext
filter?: (email: Email) => boolean
}) {
const response = await request.get(`${process.env.MAILCATCHER_HOST}/messages`)
let emails = await response.json()
if (filter) {
emails = emails.filter(filter)
}
const email = emails[emails.length - 1]
if (email) {
return email as Email
}
return null
}
export function findLastEmail({
request,
filter,
timeout = 5000,
}: {
request: APIRequestContext
filter?: (email: Email) => boolean
timeout?: number
}) {
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error("Timeout while trying to get latest email")),
timeout,
),
)
const checkEmails = async () => {
while (true) {
const emailData = await findEmail({ request, filter })
if (emailData) {
return emailData
}
// Wait for 100ms before checking again
await new Promise((resolve) => setTimeout(resolve, 100))
}
}
return Promise.race([timeoutPromise, checkEmails()])
}
-22
View File
@@ -1,21 +1,5 @@
import { expect, type Page } from "@playwright/test" import { expect, type Page } from "@playwright/test"
export async function signUpNewUser(
page: Page,
name: string,
email: string,
password: string,
) {
await page.goto("/signup")
await page.getByTestId("full-name-input").fill(name)
await page.getByTestId("email-input").fill(email)
await page.getByTestId("password-input").fill(password)
await page.getByTestId("confirm-password-input").fill(password)
await page.getByRole("button", { name: "Sign Up" }).click()
await page.goto("/login")
}
export async function logInUser(page: Page, email: string, password: string) { export async function logInUser(page: Page, email: string, password: string) {
await page.goto("/login") await page.goto("/login")
@@ -27,9 +11,3 @@ export async function logInUser(page: Page, email: string, password: string) {
page.getByText("Welcome back, nice to see you again!"), page.getByText("Welcome back, nice to see you again!"),
).toBeVisible() ).toBeVisible()
} }
export async function logOutUser(page: Page) {
await page.getByTestId("user-menu").click()
await page.getByRole("menuitem", { name: "Log out" }).click()
await page.goto("/login")
}