mirror of
https://github.com/oxynote/oxynote.git
synced 2026-10-17 10:59:20 +02:00
212 lines
6.2 KiB
TypeScript
212 lines
6.2 KiB
TypeScript
import { randomUUID } from "node:crypto"
|
|
import {
|
|
expect,
|
|
type APIRequestContext,
|
|
type Locator,
|
|
type Page,
|
|
} from "@playwright/test"
|
|
import { BASE_URL } from "./config"
|
|
import { fetchPasswordResetLink, fetchVerificationLink } from "./mailpit"
|
|
import { t } from "./i18n"
|
|
import { visit } from "./page"
|
|
|
|
export interface Credentials {
|
|
email: string
|
|
password: string
|
|
}
|
|
|
|
// newCredentials mints an account nobody else uses, which is what keeps
|
|
// the tests independent of each other on a shared stack. The password
|
|
// satisfies the signup rules: at least 16 characters, one digit and one
|
|
// symbol.
|
|
export function newCredentials(): Credentials {
|
|
return {
|
|
email: `e2e-${randomUUID()}@oxynote.test`,
|
|
password: "e2e-Passw0rd!-secure",
|
|
}
|
|
}
|
|
|
|
// revealForm clicks the method button until the form it reveals is on
|
|
// the page. The button is server-rendered and visible before vue has
|
|
// bound its handler; visit() waits for the app instance, but under load
|
|
// the handler can still land a beat later, and a click in that window
|
|
// does nothing at all — no request, no error, just the same page.
|
|
export async function revealForm(
|
|
button: Locator,
|
|
field: Locator,
|
|
): Promise<void> {
|
|
await expect(async () => {
|
|
await button.click()
|
|
await expect(field).toBeVisible({ timeout: 1_000 })
|
|
}).toPass()
|
|
}
|
|
|
|
// submitSignupForm walks the email-password branch of the signup page and
|
|
// leaves the browser wherever the app sends it. The origin is the stack's
|
|
// front door unless a test drives another instance.
|
|
export async function submitSignupForm(
|
|
page: Page,
|
|
credentials: Credentials,
|
|
origin = BASE_URL,
|
|
): Promise<void> {
|
|
await visit(page, `${origin}/signup`)
|
|
await fillSignupForm(page, credentials)
|
|
}
|
|
|
|
// fillSignupForm is submitSignupForm for a signup page the browser is
|
|
// already on, such as one opened from an invitation.
|
|
export async function fillSignupForm(
|
|
page: Page,
|
|
credentials: Credentials,
|
|
): Promise<void> {
|
|
const email = page.getByPlaceholder(
|
|
t("onboarding.signup.email-password-form.email-placeholder"),
|
|
)
|
|
await revealForm(
|
|
page.getByRole("button", {
|
|
name: t("onboarding.signup.signup-email-password"),
|
|
}),
|
|
email,
|
|
)
|
|
|
|
await email.fill(credentials.email)
|
|
await page
|
|
.getByPlaceholder(
|
|
t("onboarding.signup.email-password-form.password-placeholder"),
|
|
)
|
|
.fill(credentials.password)
|
|
|
|
await page
|
|
.getByRole("button", {
|
|
name: t("onboarding.signup.email-password-form.continue"),
|
|
})
|
|
.click()
|
|
}
|
|
|
|
// submitLoginForm walks the email-password branch of the login page and
|
|
// leaves the browser wherever the app sends it. The origin is the stack's
|
|
// front door unless a test drives another instance.
|
|
export async function submitLoginForm(
|
|
page: Page,
|
|
credentials: Credentials,
|
|
origin = BASE_URL,
|
|
): Promise<void> {
|
|
await visit(page, `${origin}/login`)
|
|
await fillLoginForm(page, credentials)
|
|
}
|
|
|
|
// fillLoginForm signs in through the login page the browser is already
|
|
// on, so whatever its url carries reaches the sign-in.
|
|
export async function fillLoginForm(
|
|
page: Page,
|
|
credentials: Credentials,
|
|
): Promise<void> {
|
|
const email = page.getByPlaceholder(
|
|
t("onboarding.login.email-password-form.email-placeholder"),
|
|
)
|
|
await revealForm(
|
|
page.getByRole("button", {
|
|
name: t("onboarding.login.login-email-password"),
|
|
}),
|
|
email,
|
|
)
|
|
|
|
await email.fill(credentials.email)
|
|
await page
|
|
.getByPlaceholder(
|
|
t("onboarding.login.email-password-form.password-placeholder"),
|
|
)
|
|
.fill(credentials.password)
|
|
|
|
await page
|
|
.getByRole("button", {
|
|
name: t("onboarding.login.email-password-form.continue"),
|
|
})
|
|
.click()
|
|
}
|
|
|
|
// resetPassword walks the forgot-password flow end to end: it requests
|
|
// a reset link from the login page, follows the emailed link and
|
|
// chooses the new password. Asserted here rather than left to the
|
|
// caller: an expired or rejected token also lands back on the login
|
|
// page, and only the reset flag separates the two outcomes.
|
|
export async function resetPassword(
|
|
page: Page,
|
|
request: APIRequestContext,
|
|
email: string,
|
|
newPassword: string,
|
|
): Promise<void> {
|
|
await visit(page, "/login")
|
|
|
|
const emailField = page.getByPlaceholder(
|
|
t("onboarding.login.password-reset-form.email-placeholder"),
|
|
)
|
|
await revealForm(
|
|
page.getByRole("button", {
|
|
name: t("onboarding.login.login-email-password"),
|
|
}),
|
|
page.getByPlaceholder(
|
|
t("onboarding.login.email-password-form.email-placeholder"),
|
|
),
|
|
)
|
|
await page
|
|
.getByRole("button", {
|
|
name: t("onboarding.login.forgot-password.placeholders.reset"),
|
|
})
|
|
.click()
|
|
|
|
await emailField.fill(email)
|
|
await page
|
|
.getByRole("button", {
|
|
name: t("onboarding.login.password-reset-form.continue"),
|
|
})
|
|
.click()
|
|
await expect(
|
|
page.getByText(t("onboarding.login.password-reset-sent", { email })),
|
|
).toBeVisible()
|
|
|
|
await visit(page, await fetchPasswordResetLink(request, email))
|
|
|
|
// both placeholders contain the other's text, so they are matched
|
|
// exactly
|
|
await page
|
|
.getByPlaceholder(t("onboarding.reset-password.password-placeholder"), {
|
|
exact: true,
|
|
})
|
|
.fill(newPassword)
|
|
await page
|
|
.getByPlaceholder(t("onboarding.reset-password.confirm-placeholder"), {
|
|
exact: true,
|
|
})
|
|
.fill(newPassword)
|
|
await page
|
|
.getByRole("button", { name: t("onboarding.reset-password.continue") })
|
|
.click()
|
|
|
|
await expect(page).toHaveURL(`${BASE_URL}/login?reset=true`)
|
|
}
|
|
|
|
// signUpAndVerify creates an account that can log in. Verification is not
|
|
// optional: the server refuses to sign in an unverified user, so this is
|
|
// the setup any test needing an existing account starts from.
|
|
export async function signUpAndVerify(
|
|
page: Page,
|
|
request: APIRequestContext,
|
|
credentials: Credentials = newCredentials(),
|
|
): Promise<Credentials> {
|
|
await submitSignupForm(page, credentials)
|
|
// a signup is a round trip through auth-realtime and core, which
|
|
// renders and queues two emails before answering; with several
|
|
// workers signing up at once that can exceed the default wait.
|
|
await expect(page).toHaveURL(/\/verify-email\?/, { timeout: 15_000 })
|
|
|
|
await page.goto(await fetchVerificationLink(request, credentials.email))
|
|
|
|
// asserted here rather than left to the caller: a rejected token still
|
|
// lands on the login page, and the sign-in that follows would then fail
|
|
// somewhere far away from the reason it failed.
|
|
await expect(page).toHaveURL(`${BASE_URL}/login?verified=true`)
|
|
|
|
return credentials
|
|
}
|