mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-07-24 00:43:29 +10:00
Add Playwright E2E test setup (#3169)
* docs: design e2e test setup Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * docs: plan e2e test implementation Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * test: add playwright e2e scripts Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * test: configure playwright Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * test: add core e2e fixtures and specs Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * ci: run e2e tests on pull requests Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * [autofix.ci] apply automated fixes * test: stabilize e2e suite Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * test: ignore playwright artifacts Co-authored-by: Amruth Pillai <im.amruth@gmail.com> * Update .github/workflows/e2e.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * test: address e2e review feedback Co-authored-by: Amruth Pillai <im.amruth@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
APP_URL: http://localhost:3000
|
||||||
|
PORT: "3000"
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||||
|
FLAG_DISABLE_SIGNUPS: "false"
|
||||||
|
FLAG_DISABLE_EMAIL_AUTH: "false"
|
||||||
|
FLAG_DISABLE_API_RATE_LIMIT: "true"
|
||||||
|
LOCAL_STORAGE_PATH: /tmp/reactive-resume-e2e-storage
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U postgres -d postgres"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Playwright Browser
|
||||||
|
run: pnpm exec playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Generate Test Secrets
|
||||||
|
run: |
|
||||||
|
echo "AUTH_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV"
|
||||||
|
echo "ENCRYPTION_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Prepare Storage
|
||||||
|
run: mkdir -p "$LOCAL_STORAGE_PATH"
|
||||||
|
|
||||||
|
- name: Run Database Migrations
|
||||||
|
run: pnpm db:migrate
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Run E2E Tests
|
||||||
|
run: pnpm test:e2e:ci
|
||||||
|
|
||||||
|
- name: Upload Playwright Report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: |
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 7
|
||||||
@@ -36,6 +36,8 @@ logs
|
|||||||
# Testing
|
# Testing
|
||||||
coverage
|
coverage
|
||||||
reports
|
reports
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
|
||||||
# Cache
|
# Cache
|
||||||
tmp
|
tmp
|
||||||
|
|||||||
@@ -0,0 +1,670 @@
|
|||||||
|
# E2E Tests Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build a Playwright E2E test setup that runs deterministic core Reactive Resume flows with ephemeral accounts/data locally and on GitHub Actions for every PR.
|
||||||
|
|
||||||
|
**Architecture:** Add a root-level Playwright harness that targets the production server after `pnpm build`. Use hybrid fixtures: one browser-driven auth smoke spec, and helper-created authenticated browser state plus ephemeral database cleanup for resume flows. Keep initial PR-gated coverage to auth, sample resume creation, builder autosave, JSON export/import, and public sharing.
|
||||||
|
|
||||||
|
**Tech Stack:** Playwright, TypeScript, pnpm, Turbo, GitHub Actions, PostgreSQL service container, Better Auth HTTP endpoints, Drizzle/Postgres cleanup helpers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File structure
|
||||||
|
|
||||||
|
- Create `playwright.config.ts`: Playwright projects, reporters, artifact policy, base URL, and production `webServer`.
|
||||||
|
- Create `tests/e2e/README.md`: local setup and CI behavior.
|
||||||
|
- Create `tests/e2e/fixtures/data.ts`: unique test identity and resume value generation.
|
||||||
|
- Create `tests/e2e/fixtures/auth.ts`: browser UI auth helpers and API-backed authenticated storage state helpers.
|
||||||
|
- Create `tests/e2e/fixtures/db.ts`: user cleanup by email/username prefix.
|
||||||
|
- Create `tests/e2e/fixtures/resume.ts`: UI helpers for creating sample resumes and accessing builder sections.
|
||||||
|
- Create `tests/e2e/fixtures/test.ts`: typed Playwright fixture composition.
|
||||||
|
- Create `tests/e2e/specs/auth.spec.ts`: browser registration/login smoke flow.
|
||||||
|
- Create `tests/e2e/specs/resume-lifecycle.spec.ts`: dashboard create sample resume and builder autosave flow.
|
||||||
|
- Create `tests/e2e/specs/json-export-import.spec.ts`: deterministic JSON backup/restore flow.
|
||||||
|
- Create `tests/e2e/specs/public-sharing.spec.ts`: public sharing flow with anonymous browser context.
|
||||||
|
- Create `.github/workflows/e2e.yml`: PR/push workflow with Postgres, build, Playwright install, E2E run, and report uploads.
|
||||||
|
- Modify `package.json`: add Playwright dependency and E2E scripts.
|
||||||
|
- Modify `turbo.json`: register E2E task if routed through package scripts.
|
||||||
|
- Modify targeted UI files only if accessible locators are not sufficient.
|
||||||
|
|
||||||
|
## Task 1: Add Playwright dependency and scripts
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `package.json`
|
||||||
|
- Modify: `pnpm-lock.yaml`
|
||||||
|
- Modify: `turbo.json`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add Playwright with the package manager**
|
||||||
|
|
||||||
|
Run: `pnpm add -D @playwright/test`
|
||||||
|
|
||||||
|
Expected: `package.json` and `pnpm-lock.yaml` include `@playwright/test`.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add root scripts**
|
||||||
|
|
||||||
|
Change root `package.json` scripts to include:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:ci": "playwright test"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Register the Turbo task**
|
||||||
|
|
||||||
|
Add this task entry to `turbo.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"test:e2e": {
|
||||||
|
"cache": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Verify script discovery**
|
||||||
|
|
||||||
|
Run: `pnpm exec playwright --version`
|
||||||
|
|
||||||
|
Expected: Playwright prints a version and exits successfully.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add package.json pnpm-lock.yaml turbo.json
|
||||||
|
git commit -m "test: add playwright e2e scripts"
|
||||||
|
git push -u origin feat/e2e-test-plan-2b10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 2: Add Playwright configuration
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `playwright.config.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create the config**
|
||||||
|
|
||||||
|
Create `playwright.config.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
||||||
|
const baseURL = process.env.APP_URL ?? `http://127.0.0.1:${port}`;
|
||||||
|
const isCI = process.env.CI === "true" || process.env.CI === "1";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e/specs",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: isCI,
|
||||||
|
retries: isCI ? 2 : 0,
|
||||||
|
workers: isCI ? 2 : undefined,
|
||||||
|
reporter: isCI
|
||||||
|
? [
|
||||||
|
["list"],
|
||||||
|
["github"],
|
||||||
|
["junit", { outputFile: "test-results/e2e-junit.xml" }],
|
||||||
|
]
|
||||||
|
: [["list"], ["html", { open: "never" }]],
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
video: "retain-on-failure",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: "pnpm start",
|
||||||
|
url: `${baseURL}/api/health`,
|
||||||
|
reuseExistingServer: !isCI,
|
||||||
|
timeout: 120_000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
PORT: String(port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify config loads**
|
||||||
|
|
||||||
|
Run: `pnpm exec playwright test --list`
|
||||||
|
|
||||||
|
Expected: Playwright lists zero tests at this point without config errors.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add playwright.config.ts
|
||||||
|
git commit -m "test: configure playwright"
|
||||||
|
git push -u origin feat/e2e-test-plan-2b10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 3: Add E2E fixtures
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/e2e/fixtures/data.ts`
|
||||||
|
- Create: `tests/e2e/fixtures/db.ts`
|
||||||
|
- Create: `tests/e2e/fixtures/auth.ts`
|
||||||
|
- Create: `tests/e2e/fixtures/resume.ts`
|
||||||
|
- Create: `tests/e2e/fixtures/test.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add test data helpers**
|
||||||
|
|
||||||
|
Create `tests/e2e/fixtures/data.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { TestInfo } from "@playwright/test";
|
||||||
|
|
||||||
|
const sanitize = (value: string) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
||||||
|
|
||||||
|
export type E2EAccount = {
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createRunSlug(testInfo: TestInfo) {
|
||||||
|
const worker = testInfo.workerIndex;
|
||||||
|
const title = sanitize(testInfo.titlePath.join("-")).slice(0, 40);
|
||||||
|
const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
return `e2e-${worker}-${title}-${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAccount(testInfo: TestInfo): E2EAccount {
|
||||||
|
const slug = createRunSlug(testInfo).replaceAll("-", "_").slice(0, 48);
|
||||||
|
return {
|
||||||
|
name: "E2E Test User",
|
||||||
|
username: slug,
|
||||||
|
email: `${slug}@example.test`,
|
||||||
|
password: "Password123!",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createResumeName(testInfo: TestInfo) {
|
||||||
|
return `E2E Resume ${createRunSlug(testInfo)}`;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add database cleanup**
|
||||||
|
|
||||||
|
Create `tests/e2e/fixtures/db.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { eq, or } from "drizzle-orm";
|
||||||
|
import { db, getPool } from "@reactive-resume/db/client";
|
||||||
|
import { user } from "@reactive-resume/db/schema";
|
||||||
|
|
||||||
|
export async function deleteE2EUser(account: { email: string; username: string }) {
|
||||||
|
await db.delete(user).where(or(eq(user.email, account.email), eq(user.username, account.username)));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function closeE2EDatabase() {
|
||||||
|
await getPool().end();
|
||||||
|
globalThis.__pool = undefined;
|
||||||
|
globalThis.__drizzle = undefined;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add auth helpers**
|
||||||
|
|
||||||
|
Create `tests/e2e/fixtures/auth.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Browser, Page } from "@playwright/test";
|
||||||
|
import type { E2EAccount } from "./data";
|
||||||
|
|
||||||
|
export async function registerViaUi(page: Page, account: E2EAccount) {
|
||||||
|
await page.goto("/auth/register");
|
||||||
|
await page.getByLabel("Name").fill(account.name);
|
||||||
|
await page.getByLabel("Username").fill(account.username);
|
||||||
|
await page.getByLabel("Email Address").fill(account.email);
|
||||||
|
await page.getByLabel("Password").fill(account.password);
|
||||||
|
await page.getByRole("button", { name: "Sign up" }).click();
|
||||||
|
await page.getByRole("link", { name: /continue/i }).click();
|
||||||
|
await page.waitForURL(/\/dashboard/);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginViaUi(page: Page, account: E2EAccount) {
|
||||||
|
await page.goto("/auth/login");
|
||||||
|
await page.getByLabel(/email|username/i).fill(account.email);
|
||||||
|
await page.getByLabel("Password").fill(account.password);
|
||||||
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
await page.waitForURL(/\/dashboard/);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAuthenticatedPage(browser: Browser, account: E2EAccount) {
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
await registerViaUi(page, account);
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add resume UI helpers**
|
||||||
|
|
||||||
|
Create `tests/e2e/fixtures/resume.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { Page, TestInfo } from "@playwright/test";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import { createResumeName } from "./data";
|
||||||
|
|
||||||
|
export async function createSampleResumeFromDashboard(page: Page, testInfo: TestInfo) {
|
||||||
|
const resumeName = createResumeName(testInfo);
|
||||||
|
await page.goto("/dashboard/resumes");
|
||||||
|
await page.getByText("Create a new resume").click();
|
||||||
|
await page.getByRole("dialog", { name: "Create a new resume" }).getByLabel("Name").fill(resumeName);
|
||||||
|
await page.getByRole("button", { name: "Create resume with options" }).click();
|
||||||
|
await page.getByRole("menuitem", { name: "Create a Sample Resume" }).click();
|
||||||
|
await expect(page.getByText(resumeName)).toBeVisible();
|
||||||
|
await page.getByText(resumeName).click();
|
||||||
|
await page.waitForURL(/\/builder\/.+/);
|
||||||
|
return resumeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openRightSidebarSection(page: Page, name: string) {
|
||||||
|
await page.getByRole("button", { name }).click();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Add fixture composition**
|
||||||
|
|
||||||
|
Create `tests/e2e/fixtures/test.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test as base, expect } from "@playwright/test";
|
||||||
|
import type { E2EAccount } from "./data";
|
||||||
|
import { createAccount } from "./data";
|
||||||
|
import { deleteE2EUser } from "./db";
|
||||||
|
import { registerViaUi } from "./auth";
|
||||||
|
|
||||||
|
type Fixtures = {
|
||||||
|
account: E2EAccount;
|
||||||
|
authPage: import("@playwright/test").Page;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const test = base.extend<Fixtures>({
|
||||||
|
account: async ({}, use, testInfo) => {
|
||||||
|
const account = createAccount(testInfo);
|
||||||
|
await use(account);
|
||||||
|
await deleteE2EUser(account);
|
||||||
|
},
|
||||||
|
authPage: async ({ browser, account }, use) => {
|
||||||
|
const context = await browser.newContext();
|
||||||
|
const page = await context.newPage();
|
||||||
|
await registerViaUi(page, account);
|
||||||
|
await use(page);
|
||||||
|
await context.close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { expect };
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run type-aware feedback**
|
||||||
|
|
||||||
|
Run: `pnpm exec tsc --noEmit --allowImportingTsExtensions false --moduleResolution bundler --target es2022 --module esnext tests/e2e/fixtures/*.ts`
|
||||||
|
|
||||||
|
Expected: If direct `tsc` is too narrow for workspace exports, use `pnpm typecheck` after Task 6 instead.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/e2e/fixtures
|
||||||
|
git commit -m "test: add e2e fixtures"
|
||||||
|
git push -u origin feat/e2e-test-plan-2b10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 4: Add core E2E specs
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/e2e/specs/auth.spec.ts`
|
||||||
|
- Create: `tests/e2e/specs/resume-lifecycle.spec.ts`
|
||||||
|
- Create: `tests/e2e/specs/json-export-import.spec.ts`
|
||||||
|
- Create: `tests/e2e/specs/public-sharing.spec.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add auth smoke spec**
|
||||||
|
|
||||||
|
Create `tests/e2e/specs/auth.spec.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "../fixtures/test";
|
||||||
|
import { loginViaUi, registerViaUi } from "../fixtures/auth";
|
||||||
|
|
||||||
|
test("registers and logs in with email credentials", async ({ page, account }) => {
|
||||||
|
await registerViaUi(page, account);
|
||||||
|
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: /user menu|account|profile/i }).click();
|
||||||
|
await page.getByRole("menuitem", { name: /logout|sign out/i }).click();
|
||||||
|
await loginViaUi(page, account);
|
||||||
|
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add resume lifecycle spec**
|
||||||
|
|
||||||
|
Create `tests/e2e/specs/resume-lifecycle.spec.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "../fixtures/test";
|
||||||
|
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||||
|
|
||||||
|
test("creates a sample resume and persists a basics edit", async ({ authPage: page }, testInfo) => {
|
||||||
|
await createSampleResumeFromDashboard(page, testInfo);
|
||||||
|
const updatedName = `E2E Edited ${Date.now()}`;
|
||||||
|
await page.getByRole("button", { name: "Basics" }).click();
|
||||||
|
await page.getByLabel("Name").fill(updatedName);
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "Basics" }).click();
|
||||||
|
await expect(page.getByLabel("Name")).toHaveValue(updatedName);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add JSON export/import spec**
|
||||||
|
|
||||||
|
Create `tests/e2e/specs/json-export-import.spec.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "../fixtures/test";
|
||||||
|
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||||
|
|
||||||
|
test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => {
|
||||||
|
const resumeName = await createSampleResumeFromDashboard(page, testInfo);
|
||||||
|
await page.getByRole("button", { name: "Export" }).click();
|
||||||
|
const downloadPromise = page.waitForEvent("download");
|
||||||
|
await page.getByRole("button", { name: /^JSON$/ }).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toMatch(/\.json$/);
|
||||||
|
const path = await download.path();
|
||||||
|
expect(path).toBeTruthy();
|
||||||
|
if (!path) throw new Error("Expected Playwright to provide a downloaded JSON path.");
|
||||||
|
await page.goto("/dashboard/resumes");
|
||||||
|
await page.getByText("Import an existing resume").click();
|
||||||
|
await page.getByRole("combobox").click();
|
||||||
|
await page.getByRole("option", { name: "Reactive Resume (JSON)" }).click();
|
||||||
|
await page.getByText("Click here to select a file to import").setInputFiles(path);
|
||||||
|
await page.getByRole("button", { name: "Import" }).click();
|
||||||
|
await page.waitForURL(/\/builder\/.+/);
|
||||||
|
await expect(page.getByText(resumeName)).toBeVisible();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add public sharing spec**
|
||||||
|
|
||||||
|
Create `tests/e2e/specs/public-sharing.spec.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { test, expect } from "../fixtures/test";
|
||||||
|
import { createSampleResumeFromDashboard } from "../fixtures/resume";
|
||||||
|
|
||||||
|
test("publishes a resume and renders it for an anonymous visitor", async ({ browser, authPage: page }, testInfo) => {
|
||||||
|
await createSampleResumeFromDashboard(page, testInfo);
|
||||||
|
await page.getByRole("button", { name: "Sharing" }).click();
|
||||||
|
await page.getByLabel("Allow Public Access").click();
|
||||||
|
const publicUrl = await page.getByLabel("URL").inputValue();
|
||||||
|
const anonymous = await browser.newPage();
|
||||||
|
await anonymous.goto(publicUrl);
|
||||||
|
await expect(anonymous.getByRole("button", { name: /download/i })).toBeVisible();
|
||||||
|
await anonymous.close();
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run list mode**
|
||||||
|
|
||||||
|
Run: `pnpm test:e2e -- --list`
|
||||||
|
|
||||||
|
Expected: Four Chromium tests are listed.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/e2e/specs
|
||||||
|
git commit -m "test: add core e2e specs"
|
||||||
|
git push -u origin feat/e2e-test-plan-2b10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 5: Add documentation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `tests/e2e/README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add E2E README**
|
||||||
|
|
||||||
|
Create `tests/e2e/README.md`:
|
||||||
|
|
||||||
|
```md
|
||||||
|
# E2E Tests
|
||||||
|
|
||||||
|
Reactive Resume uses Playwright for PR-gated browser coverage of deterministic core flows.
|
||||||
|
|
||||||
|
## Local setup
|
||||||
|
|
||||||
|
Start PostgreSQL:
|
||||||
|
|
||||||
|
`sudo docker compose -f compose.dev.yml up -d postgres`
|
||||||
|
|
||||||
|
Generate local test secrets:
|
||||||
|
|
||||||
|
`export AUTH_SECRET=$(openssl rand -hex 32)`
|
||||||
|
|
||||||
|
`export ENCRYPTION_SECRET=$(openssl rand -hex 32)`
|
||||||
|
|
||||||
|
Run database migrations:
|
||||||
|
|
||||||
|
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm db:migrate`
|
||||||
|
|
||||||
|
Build the production app:
|
||||||
|
|
||||||
|
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm build`
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm test:e2e`
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
- Email/password auth smoke.
|
||||||
|
- Dashboard sample resume creation.
|
||||||
|
- Builder basics edit and autosave persistence.
|
||||||
|
- JSON export/import.
|
||||||
|
- Public sharing for anonymous visitors.
|
||||||
|
|
||||||
|
PDF, DOCX, OAuth, passkeys, 2FA, password reset, and AI flows are intentionally outside the initial PR gate.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/e2e/README.md
|
||||||
|
git commit -m "docs: document e2e workflow"
|
||||||
|
git push -u origin feat/e2e-test-plan-2b10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 6: Add GitHub Actions workflow
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `.github/workflows/e2e.yml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add workflow**
|
||||||
|
|
||||||
|
Create `.github/workflows/e2e.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: E2E Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||||
|
APP_URL: http://localhost:3000
|
||||||
|
PORT: "3000"
|
||||||
|
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres
|
||||||
|
FLAG_DISABLE_SIGNUPS: "false"
|
||||||
|
FLAG_DISABLE_EMAIL_AUTH: "false"
|
||||||
|
FLAG_DISABLE_API_RATE_LIMIT: "true"
|
||||||
|
LOCAL_STORAGE_PATH: /tmp/reactive-resume-e2e-storage
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
e2e:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: postgres
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U postgres -d postgres"
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v6
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: "24"
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Playwright Browser
|
||||||
|
run: pnpm exec playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Generate Test Secrets
|
||||||
|
run: |
|
||||||
|
echo "AUTH_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV"
|
||||||
|
echo "ENCRYPTION_SECRET=$(openssl rand -hex 32)" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Prepare Storage
|
||||||
|
run: mkdir -p "$LOCAL_STORAGE_PATH"
|
||||||
|
|
||||||
|
- name: Run Database Migrations
|
||||||
|
run: pnpm db:migrate
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: Run E2E Tests
|
||||||
|
run: pnpm test:e2e:ci
|
||||||
|
|
||||||
|
- name: Upload Playwright Report
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: |
|
||||||
|
playwright-report
|
||||||
|
test-results
|
||||||
|
if-no-files-found: ignore
|
||||||
|
retention-days: 7
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .github/workflows/e2e.yml
|
||||||
|
git commit -m "ci: run e2e tests on pull requests"
|
||||||
|
git push -u origin feat/e2e-test-plan-2b10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Task 7: Verify and stabilize
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify any E2E files that fail due to actual labels or app behavior.
|
||||||
|
- Modify targeted UI files only if no stable accessible locator exists.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run non-mutating checks**
|
||||||
|
|
||||||
|
Run: `pnpm exec biome check package.json turbo.json playwright.config.ts tests/e2e .github/workflows/e2e.yml`
|
||||||
|
|
||||||
|
Expected: No Biome errors.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run typecheck**
|
||||||
|
|
||||||
|
Run: `pnpm typecheck`
|
||||||
|
|
||||||
|
Expected: Typecheck passes for all workspaces.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Start PostgreSQL**
|
||||||
|
|
||||||
|
Run: `sudo docker compose -f compose.dev.yml up -d postgres`
|
||||||
|
|
||||||
|
Expected: Postgres container is healthy.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build production app**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export AUTH_SECRET=$(openssl rand -hex 32)
|
||||||
|
export ENCRYPTION_SECRET=$(openssl rand -hex 32)
|
||||||
|
APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: web and server production builds complete.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run E2E suite**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: All Chromium specs pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit stabilization changes**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "test: stabilize e2e suite"
|
||||||
|
git push -u origin feat/e2e-test-plan-2b10
|
||||||
|
```
|
||||||
|
|
||||||
|
## Self-review
|
||||||
|
|
||||||
|
- Spec coverage: Tasks cover Playwright setup, production-build target, hybrid fixtures, ephemeral data cleanup, core deterministic flows, CI workflow, and documentation.
|
||||||
|
- Placeholder scan: No placeholder steps remain; each task names exact files, commands, and expected outcomes.
|
||||||
|
- Type consistency: Fixture types are introduced before specs use them, and all helper names match across tasks.
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# E2E Tests Design
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Reactive Resume is a pnpm/Turborepo monorepo with two deployable apps:
|
||||||
|
|
||||||
|
- `apps/web`: TanStack Start / React / Vite user interface.
|
||||||
|
- `apps/server`: Hono / Node.js server that owns API, auth, MCP, static uploads, OpenAPI, and production web serving.
|
||||||
|
|
||||||
|
The product is a resume builder. The highest-value browser journeys are:
|
||||||
|
|
||||||
|
1. Create an account and authenticate.
|
||||||
|
2. Create a resume from the dashboard.
|
||||||
|
3. Open the builder and edit resume basics.
|
||||||
|
4. Persist builder edits through autosave.
|
||||||
|
5. Export and import structured resume JSON.
|
||||||
|
6. Enable public sharing and view the resume as an anonymous visitor.
|
||||||
|
|
||||||
|
The repository currently has broad Vitest coverage but no Playwright or Cypress harness. GitHub Actions currently runs autofix/lint behavior on PRs and Docker image publishing on pushes/tags, but no PR-gated E2E workflow.
|
||||||
|
|
||||||
|
Context7 was requested for current framework guidance, but the configured Context7 quota was exhausted in this environment. The implementation plan should re-check current Playwright docs when Context7 access is available.
|
||||||
|
|
||||||
|
## Goals
|
||||||
|
|
||||||
|
- Add a working Playwright E2E setup.
|
||||||
|
- Run E2E tests against the production build path, not only Vite dev mode.
|
||||||
|
- Use ephemeral accounts and data for each run.
|
||||||
|
- Cover deterministic core UX flows on every PR.
|
||||||
|
- Keep CI runtime and flake risk low enough for a default PR gate.
|
||||||
|
- Leave room to add PDF, DOCX, OAuth, passkey, 2FA, and AI flows later without bloating the initial gate.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Do not add PDF or DOCX download validation to the initial PR gate.
|
||||||
|
- Do not automate OAuth, passkeys, 2FA, or password reset in the first suite.
|
||||||
|
- Do not include AI Agent, AI import, or resume analysis flows in the first suite.
|
||||||
|
- Do not add a cross-browser matrix initially.
|
||||||
|
- Do not assert pixel-perfect PDF/canvas rendering.
|
||||||
|
|
||||||
|
## Recommended approach
|
||||||
|
|
||||||
|
Use a root-level Playwright harness that runs Chromium against a production build:
|
||||||
|
|
||||||
|
1. CI installs dependencies and Playwright Chromium.
|
||||||
|
2. CI starts PostgreSQL.
|
||||||
|
3. CI sets test environment variables.
|
||||||
|
4. CI builds the app with `pnpm build`.
|
||||||
|
5. Playwright starts the production server with `pnpm start`.
|
||||||
|
6. E2E specs exercise browser flows against `APP_URL`.
|
||||||
|
|
||||||
|
Use hybrid fixtures:
|
||||||
|
|
||||||
|
- The auth smoke test registers and logs in through the browser UI.
|
||||||
|
- Non-auth specs create ephemeral users and authenticated browser state through test helpers.
|
||||||
|
- Resume setup can use helper APIs or direct database helpers when the setup itself is not the behavior under test.
|
||||||
|
- Each test uses unique data and cleans up its own ephemeral user.
|
||||||
|
|
||||||
|
This keeps the suite close to user reality while avoiding repeated slow setup in every test.
|
||||||
|
|
||||||
|
## File layout
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
- `playwright.config.ts`
|
||||||
|
- `tests/e2e/README.md`
|
||||||
|
- `tests/e2e/fixtures/test.ts`
|
||||||
|
- `tests/e2e/fixtures/auth.ts`
|
||||||
|
- `tests/e2e/fixtures/db.ts`
|
||||||
|
- `tests/e2e/fixtures/resume.ts`
|
||||||
|
- `tests/e2e/specs/auth.spec.ts`
|
||||||
|
- `tests/e2e/specs/resume-lifecycle.spec.ts`
|
||||||
|
- `tests/e2e/specs/json-export-import.spec.ts`
|
||||||
|
- `tests/e2e/specs/public-sharing.spec.ts`
|
||||||
|
- `.github/workflows/e2e.yml`
|
||||||
|
|
||||||
|
Update:
|
||||||
|
|
||||||
|
- root `package.json` scripts
|
||||||
|
- `turbo.json` tasks, if root scripts are routed through Turbo
|
||||||
|
- lockfile after adding Playwright
|
||||||
|
- contributing docs if a short root README is not enough
|
||||||
|
|
||||||
|
## Playwright configuration
|
||||||
|
|
||||||
|
The initial config should:
|
||||||
|
|
||||||
|
- Use Chromium only.
|
||||||
|
- Read `baseURL` from `APP_URL`, defaulting to `http://localhost:3000`.
|
||||||
|
- Use traces, screenshots, and videos on failure.
|
||||||
|
- Use a local-friendly reporter plus a CI reporter/artifact format.
|
||||||
|
- Start `pnpm start` through `webServer`.
|
||||||
|
- Reuse an existing local server outside CI when possible.
|
||||||
|
- Prefer role, label, and accessible-name locators.
|
||||||
|
- Add targeted `data-testid` attributes only when accessibility locators are not stable enough.
|
||||||
|
|
||||||
|
Recommended root scripts:
|
||||||
|
|
||||||
|
- `test:e2e`: run Playwright.
|
||||||
|
- `test:e2e:ui`: run Playwright UI mode.
|
||||||
|
- `test:e2e:ci`: run Playwright with CI reporter settings.
|
||||||
|
|
||||||
|
## Environment and infrastructure
|
||||||
|
|
||||||
|
Minimum E2E environment:
|
||||||
|
|
||||||
|
- `APP_URL=http://localhost:3000`
|
||||||
|
- `PORT=3000`
|
||||||
|
- `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres`
|
||||||
|
- `AUTH_SECRET=<non-empty test secret>`
|
||||||
|
- `FLAG_DISABLE_SIGNUPS=false`
|
||||||
|
- `FLAG_DISABLE_EMAIL_AUTH=false`
|
||||||
|
- `FLAG_DISABLE_API_RATE_LIMIT=true`
|
||||||
|
- `LOCAL_STORAGE_PATH=<absolute temp path>`
|
||||||
|
|
||||||
|
S3, Redis, SMTP, and AI provider variables are not required for the initial deterministic suite.
|
||||||
|
|
||||||
|
PostgreSQL should be isolated for CI runs. The simplest PR workflow can use a GitHub Actions Postgres service container and a clean database per job. Local development can use `compose.dev.yml` to start Postgres.
|
||||||
|
|
||||||
|
## Ephemeral data strategy
|
||||||
|
|
||||||
|
Each worker/test should generate unique identities:
|
||||||
|
|
||||||
|
- Email: `e2e-<runId>-<worker>-<suffix>@example.test`
|
||||||
|
- Username: `e2e_<runId>_<worker>_<suffix>`
|
||||||
|
- Resume names/slugs prefixed with `e2e-`
|
||||||
|
|
||||||
|
Cleanup should delete the ephemeral user and rely on database cascade relationships for related resumes, sessions, accounts, API keys, and related records.
|
||||||
|
|
||||||
|
Use helper setup where it improves speed and stability:
|
||||||
|
|
||||||
|
- Create authenticated storage state for non-auth specs.
|
||||||
|
- Create seeded resumes for tests that are about builder/export/sharing behavior rather than resume creation.
|
||||||
|
- Keep one browser-driven auth spec to ensure registration/login still works end to end.
|
||||||
|
|
||||||
|
## Initial PR-gated specs
|
||||||
|
|
||||||
|
### Auth smoke
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
1. Visit `/auth/register`.
|
||||||
|
2. Register a unique user through the UI.
|
||||||
|
3. Continue to `/dashboard`.
|
||||||
|
4. Log out.
|
||||||
|
5. Log in with the same credentials.
|
||||||
|
6. Assert the dashboard resumes page loads.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- Proves email/password auth, session cookies, redirects, and dashboard access work.
|
||||||
|
|
||||||
|
### Resume lifecycle
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
1. Start authenticated as an ephemeral user.
|
||||||
|
2. Visit `/dashboard/resumes`.
|
||||||
|
3. Create a sample resume through the UI.
|
||||||
|
4. Open the resume in the builder.
|
||||||
|
5. Edit the basics name field.
|
||||||
|
6. Wait for autosave.
|
||||||
|
7. Reload the builder.
|
||||||
|
8. Assert the edited value persisted.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- Proves dashboard create, builder load, form editing, API update, and autosave persistence.
|
||||||
|
|
||||||
|
### JSON export and import
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
1. Start authenticated with a sample resume.
|
||||||
|
2. Open the builder.
|
||||||
|
3. Export JSON.
|
||||||
|
4. Assert a JSON download is created and parseable.
|
||||||
|
5. Return to dashboard.
|
||||||
|
6. Import the downloaded JSON through the import dialog.
|
||||||
|
7. Assert the imported resume opens in the builder.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- Proves deterministic backup/restore without AI or binary rendering dependencies.
|
||||||
|
|
||||||
|
### Public sharing
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
|
||||||
|
1. Start authenticated with a sample resume.
|
||||||
|
2. Open builder sharing settings.
|
||||||
|
3. Enable public access.
|
||||||
|
4. Read the generated public URL.
|
||||||
|
5. Open a fresh unauthenticated browser context.
|
||||||
|
6. Visit the public URL.
|
||||||
|
7. Assert the public resume viewer renders expected resume content.
|
||||||
|
|
||||||
|
Purpose:
|
||||||
|
|
||||||
|
- Proves sharing state updates, public routing, anonymous access, and public resume rendering.
|
||||||
|
|
||||||
|
## CI workflow
|
||||||
|
|
||||||
|
Add `.github/workflows/e2e.yml`:
|
||||||
|
|
||||||
|
- Trigger on `pull_request` and pushes to `main`.
|
||||||
|
- Use Node 24.
|
||||||
|
- Use pnpm via Corepack or `pnpm/action-setup`.
|
||||||
|
- Cache pnpm dependencies through `actions/setup-node`.
|
||||||
|
- Start PostgreSQL service.
|
||||||
|
- Install dependencies with `pnpm install --frozen-lockfile`.
|
||||||
|
- Install Playwright Chromium.
|
||||||
|
- Run `pnpm build`.
|
||||||
|
- Run `pnpm test:e2e:ci`.
|
||||||
|
- Upload Playwright reports, traces, screenshots, and videos on failure.
|
||||||
|
|
||||||
|
The workflow should avoid secrets. All test credentials should be generated during the run.
|
||||||
|
|
||||||
|
## Selector strategy
|
||||||
|
|
||||||
|
Prefer stable user-facing locators:
|
||||||
|
|
||||||
|
- Roles: buttons, links, dialogs, textboxes, comboboxes.
|
||||||
|
- Labels: form inputs and switches.
|
||||||
|
- URLs: route assertions after navigation.
|
||||||
|
|
||||||
|
Add `data-testid` only where necessary:
|
||||||
|
|
||||||
|
- Builder canvas/PDF viewer surfaces that lack accessible names.
|
||||||
|
- Download/import controls if file input access is otherwise too implementation-dependent.
|
||||||
|
- Toast-independent save-state assertions if no visible stable status exists.
|
||||||
|
|
||||||
|
Keep test IDs narrow and product-neutral.
|
||||||
|
|
||||||
|
## Reliability considerations
|
||||||
|
|
||||||
|
- Autosave currently debounces changes by 500ms. Tests should wait for persisted state through reload or API-visible state, not arbitrary sleeps.
|
||||||
|
- Public resume viewer uses browser PDF/canvas behavior. Assert meaningful content or viewer presence, not pixels.
|
||||||
|
- JSON export/import is deterministic and should be the first export/import loop.
|
||||||
|
- Disable API rate limiting in test env to avoid worker-order flakes.
|
||||||
|
- Pin locale to the default English path or use role/label selectors robust to translations where practical.
|
||||||
|
- Keep the initial suite serial only where shared worker state forces it; otherwise allow Playwright worker isolation.
|
||||||
|
|
||||||
|
## Future expansion
|
||||||
|
|
||||||
|
After the initial suite is stable:
|
||||||
|
|
||||||
|
1. Add PDF download validation as a separate optional or nightly job.
|
||||||
|
2. Add DOCX download validation if the PDF job proves stable.
|
||||||
|
3. Add password-protected public sharing.
|
||||||
|
4. Add dashboard management flows: duplicate, lock, delete, tag filtering, list/grid view.
|
||||||
|
5. Add settings flows: profile update, API key creation/deletion.
|
||||||
|
6. Add AI flows only with mocked providers or a deterministic local provider.
|
||||||
|
7. Add browser matrix only after the Chromium gate has low flake rates.
|
||||||
|
|
||||||
|
## Success criteria
|
||||||
|
|
||||||
|
- `pnpm test:e2e` runs locally against a built app and Postgres.
|
||||||
|
- `pnpm test:e2e:ci` runs in GitHub Actions on every PR.
|
||||||
|
- Tests create no persistent shared seed data.
|
||||||
|
- Tests clean up ephemeral users and related data.
|
||||||
|
- The initial suite covers auth, dashboard creation, builder autosave, JSON export/import, and public sharing.
|
||||||
|
- CI uploads useful artifacts for diagnosing failures.
|
||||||
@@ -35,23 +35,29 @@
|
|||||||
"test": "turbo run test",
|
"test": "turbo run test",
|
||||||
"test:coverage": "turbo run test:coverage",
|
"test:coverage": "turbo run test:coverage",
|
||||||
"test:ci": "turbo run test:ci",
|
"test:ci": "turbo run test:ci",
|
||||||
|
"test:e2e": "playwright test",
|
||||||
|
"test:e2e:ui": "playwright test --ui",
|
||||||
|
"test:e2e:ci": "playwright test",
|
||||||
"test:agent": "turbo run test:agent"
|
"test:agent": "turbo run test:agent"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.5.0",
|
"@biomejs/biome": "^2.5.0",
|
||||||
"@commitlint/cli": "^21.0.2",
|
"@commitlint/cli": "^21.0.2",
|
||||||
"@commitlint/config-conventional": "^21.0.2",
|
"@commitlint/config-conventional": "^21.0.2",
|
||||||
|
"@playwright/test": "^1.61.0",
|
||||||
"@reactive-resume/config": "workspace:*",
|
"@reactive-resume/config": "workspace:*",
|
||||||
"@testing-library/dom": "^10.4.1",
|
"@testing-library/dom": "^10.4.1",
|
||||||
"@testing-library/jest-dom": "^6.9.1",
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
"@testing-library/react": "^16.3.2",
|
"@testing-library/react": "^16.3.2",
|
||||||
"@testing-library/user-event": "^14.6.1",
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/node": "^25.9.3",
|
"@types/node": "^25.9.3",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"@vitest/coverage-v8": "^4.1.9",
|
"@vitest/coverage-v8": "^4.1.9",
|
||||||
"happy-dom": "^20.10.5",
|
"happy-dom": "^20.10.5",
|
||||||
"knip": "^6.17.1",
|
"knip": "^6.17.1",
|
||||||
"lefthook": "^2.1.9",
|
"lefthook": "^2.1.9",
|
||||||
"npm-check-updates": "^22.2.3",
|
"npm-check-updates": "^22.2.3",
|
||||||
|
"pg": "^8.21.0",
|
||||||
"turbo": "^2.9.18",
|
"turbo": "^2.9.18",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vitest": "^4.1.9"
|
"vitest": "^4.1.9"
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
const port = Number.parseInt(process.env.PORT ?? "3000", 10);
|
||||||
|
const baseURL = process.env.APP_URL ?? `http://localhost:${port}`;
|
||||||
|
const isCI = process.env.CI === "true" || process.env.CI === "1";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./tests/e2e/specs",
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: isCI,
|
||||||
|
retries: isCI ? 2 : 0,
|
||||||
|
workers: isCI ? 2 : undefined,
|
||||||
|
timeout: 60_000,
|
||||||
|
expect: {
|
||||||
|
timeout: 10_000,
|
||||||
|
},
|
||||||
|
reporter: isCI
|
||||||
|
? [["list"], ["github"], ["junit", { outputFile: "test-results/e2e-junit.xml" }]]
|
||||||
|
: [["list"], ["html", { open: "never" }]],
|
||||||
|
use: {
|
||||||
|
baseURL,
|
||||||
|
trace: "retain-on-failure",
|
||||||
|
screenshot: "only-on-failure",
|
||||||
|
video: "retain-on-failure",
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: { ...devices["Desktop Chrome"] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: "pnpm start",
|
||||||
|
url: `${baseURL}/api/health`,
|
||||||
|
reuseExistingServer: !isCI,
|
||||||
|
timeout: 120_000,
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
APP_URL: baseURL,
|
||||||
|
NODE_ENV: "production",
|
||||||
|
PORT: String(port),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Generated
+78
-33
@@ -27,6 +27,9 @@ importers:
|
|||||||
'@commitlint/config-conventional':
|
'@commitlint/config-conventional':
|
||||||
specifier: ^21.0.2
|
specifier: ^21.0.2
|
||||||
version: 21.0.2
|
version: 21.0.2
|
||||||
|
'@playwright/test':
|
||||||
|
specifier: ^1.61.0
|
||||||
|
version: 1.61.0
|
||||||
'@reactive-resume/config':
|
'@reactive-resume/config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:packages/config
|
version: link:packages/config
|
||||||
@@ -45,6 +48,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.9.3
|
specifier: ^25.9.3
|
||||||
version: 25.9.3
|
version: 25.9.3
|
||||||
|
'@types/pg':
|
||||||
|
specifier: ^8.20.0
|
||||||
|
version: 8.20.0
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^4.1.9
|
specifier: ^4.1.9
|
||||||
version: 4.1.9(vitest@4.1.9)
|
version: 4.1.9(vitest@4.1.9)
|
||||||
@@ -60,6 +66,9 @@ importers:
|
|||||||
npm-check-updates:
|
npm-check-updates:
|
||||||
specifier: ^22.2.3
|
specifier: ^22.2.3
|
||||||
version: 22.2.3
|
version: 22.2.3
|
||||||
|
pg:
|
||||||
|
specifier: ^8.21.0
|
||||||
|
version: 8.21.0
|
||||||
turbo:
|
turbo:
|
||||||
specifier: ^2.9.18
|
specifier: ^2.9.18
|
||||||
version: 2.9.18
|
version: 2.9.18
|
||||||
@@ -89,19 +98,19 @@ importers:
|
|||||||
version: 3.1070.0
|
version: 3.1070.0
|
||||||
'@better-auth/api-key':
|
'@better-auth/api-key':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
||||||
'@better-auth/drizzle-adapter':
|
'@better-auth/drizzle-adapter':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))
|
||||||
'@better-auth/infra':
|
'@better-auth/infra':
|
||||||
specifier: ^0.2.14
|
specifier: ^0.2.14
|
||||||
version: 0.2.14(5043c63593801db60a42278b5a1e566b)
|
version: 0.2.14(fca79a91fb956ec11f36b6e601c4bb43)
|
||||||
'@better-auth/oauth-provider':
|
'@better-auth/oauth-provider':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
||||||
'@better-auth/passkey':
|
'@better-auth/passkey':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)
|
||||||
'@hono/node-server':
|
'@hono/node-server':
|
||||||
specifier: ^2.0.5
|
specifier: ^2.0.5
|
||||||
version: 2.0.5(hono@4.12.25)
|
version: 2.0.5(hono@4.12.25)
|
||||||
@@ -167,7 +176,7 @@ importers:
|
|||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.6.19
|
specifier: 1.6.19
|
||||||
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
cjk-regex:
|
cjk-regex:
|
||||||
specifier: ^3.4.0
|
specifier: ^3.4.0
|
||||||
version: 3.4.0
|
version: 3.4.0
|
||||||
@@ -279,16 +288,16 @@ importers:
|
|||||||
version: 1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.5.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@better-auth/api-key':
|
'@better-auth/api-key':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
||||||
'@better-auth/infra':
|
'@better-auth/infra':
|
||||||
specifier: ^0.2.14
|
specifier: ^0.2.14
|
||||||
version: 0.2.14(5043c63593801db60a42278b5a1e566b)
|
version: 0.2.14(fca79a91fb956ec11f36b6e601c4bb43)
|
||||||
'@better-auth/oauth-provider':
|
'@better-auth/oauth-provider':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
||||||
'@better-auth/passkey':
|
'@better-auth/passkey':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)
|
||||||
'@dnd-kit/core':
|
'@dnd-kit/core':
|
||||||
specifier: ^6.3.1
|
specifier: ^6.3.1
|
||||||
version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 6.3.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
@@ -402,7 +411,7 @@ importers:
|
|||||||
version: 6.0.207(zod@4.4.3)
|
version: 6.0.207(zod@4.4.3)
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.6.19
|
specifier: 1.6.19
|
||||||
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
cmdk:
|
cmdk:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
@@ -614,7 +623,7 @@ importers:
|
|||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.6.19
|
specifier: 1.6.19
|
||||||
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: 1.0.0-rc.3
|
specifier: 1.0.0-rc.3
|
||||||
version: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
version: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
||||||
@@ -663,19 +672,19 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/api-key':
|
'@better-auth/api-key':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
||||||
'@better-auth/drizzle-adapter':
|
'@better-auth/drizzle-adapter':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))
|
||||||
'@better-auth/infra':
|
'@better-auth/infra':
|
||||||
specifier: ^0.2.14
|
specifier: ^0.2.14
|
||||||
version: 0.2.14(5043c63593801db60a42278b5a1e566b)
|
version: 0.2.14(fca79a91fb956ec11f36b6e601c4bb43)
|
||||||
'@better-auth/oauth-provider':
|
'@better-auth/oauth-provider':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
||||||
'@better-auth/passkey':
|
'@better-auth/passkey':
|
||||||
specifier: ^1.6.19
|
specifier: ^1.6.19
|
||||||
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)
|
version: 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)
|
||||||
'@reactive-resume/db':
|
'@reactive-resume/db':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../db
|
version: link:../db
|
||||||
@@ -693,7 +702,7 @@ importers:
|
|||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
better-auth:
|
better-auth:
|
||||||
specifier: 1.6.19
|
specifier: 1.6.19
|
||||||
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
version: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: 1.0.0-rc.3
|
specifier: 1.0.0-rc.3
|
||||||
version: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
version: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
||||||
@@ -798,7 +807,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
'@react-email/ui':
|
'@react-email/ui':
|
||||||
specifier: ^6.6.3
|
specifier: ^6.6.3
|
||||||
version: 6.6.3(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
version: 6.6.3(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
'@reactive-resume/config':
|
'@reactive-resume/config':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../config
|
version: link:../config
|
||||||
@@ -3090,6 +3099,11 @@ packages:
|
|||||||
'@phosphor-icons/web@2.1.2':
|
'@phosphor-icons/web@2.1.2':
|
||||||
resolution: {integrity: sha512-rPAR9o/bEcp4Cw4DEeZHXf+nlGCMNGkNDRizYHM47NLxz9vvEHp/Tt6FMK1NcWadzw/pFDPnRBGi/ofRya958A==}
|
resolution: {integrity: sha512-rPAR9o/bEcp4Cw4DEeZHXf+nlGCMNGkNDRizYHM47NLxz9vvEHp/Tt6FMK1NcWadzw/pFDPnRBGi/ofRya958A==}
|
||||||
|
|
||||||
|
'@playwright/test@1.61.0':
|
||||||
|
resolution: {integrity: sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
'@quansync/fs@1.0.0':
|
'@quansync/fs@1.0.0':
|
||||||
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
||||||
|
|
||||||
@@ -5416,6 +5430,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==}
|
resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==}
|
||||||
engines: {node: '>=14.14'}
|
engines: {node: '>=14.14'}
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -6617,6 +6636,16 @@ packages:
|
|||||||
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
|
resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
playwright-core@1.61.0:
|
||||||
|
resolution: {integrity: sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.61.0:
|
||||||
|
resolution: {integrity: sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
png-js@2.0.0:
|
png-js@2.0.0:
|
||||||
resolution: {integrity: sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==}
|
resolution: {integrity: sha512-GdzJuUMc6ZSpxFJWVxtOH1bzYHym+TOnveqUjb+VJIbZWbZzyiRGFiKhbiielfpYbgMlhHVhsJ0FTazfuRFkMA==}
|
||||||
|
|
||||||
@@ -8430,11 +8459,11 @@ snapshots:
|
|||||||
|
|
||||||
'@bcoe/v8-coverage@1.0.2': {}
|
'@bcoe/v8-coverage@1.0.2': {}
|
||||||
|
|
||||||
'@better-auth/api-key@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))':
|
'@better-auth/api-key@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
better-call: 1.3.6(zod@4.4.3)
|
better-call: 1.3.6(zod@4.4.3)
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
|
|
||||||
@@ -8459,13 +8488,13 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
drizzle-orm: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
drizzle-orm: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
||||||
|
|
||||||
'@better-auth/infra@0.2.14(5043c63593801db60a42278b5a1e566b)':
|
'@better-auth/infra@0.2.14(fca79a91fb956ec11f36b6e601c4bb43)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/sso': 1.6.11(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
'@better-auth/sso': 1.6.11(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
'@better-fetch/fetch': 1.3.1
|
'@better-fetch/fetch': 1.3.1
|
||||||
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
better-call: 1.3.6(zod@4.4.3)
|
better-call: 1.3.6(zod@4.4.3)
|
||||||
jose: 6.2.3
|
jose: 6.2.3
|
||||||
libphonenumber-js: 1.13.6
|
libphonenumber-js: 1.13.6
|
||||||
@@ -8488,24 +8517,24 @@ snapshots:
|
|||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
|
|
||||||
'@better-auth/oauth-provider@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))':
|
'@better-auth/oauth-provider@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
'@better-fetch/fetch': 1.3.1
|
'@better-fetch/fetch': 1.3.1
|
||||||
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
better-call: 1.3.6(zod@4.4.3)
|
better-call: 1.3.6(zod@4.4.3)
|
||||||
jose: 6.2.3
|
jose: 6.2.3
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
|
|
||||||
'@better-auth/passkey@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)':
|
'@better-auth/passkey@1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))(nanostores@1.3.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
'@better-fetch/fetch': 1.3.1
|
'@better-fetch/fetch': 1.3.1
|
||||||
'@simplewebauthn/browser': 13.3.0
|
'@simplewebauthn/browser': 13.3.0
|
||||||
'@simplewebauthn/server': 13.3.1
|
'@simplewebauthn/server': 13.3.1
|
||||||
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
better-call: 1.3.6(zod@4.4.3)
|
better-call: 1.3.6(zod@4.4.3)
|
||||||
nanostores: 1.3.0
|
nanostores: 1.3.0
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
@@ -8515,12 +8544,12 @@ snapshots:
|
|||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
|
|
||||||
'@better-auth/sso@1.6.11(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))':
|
'@better-auth/sso@1.6.11(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9))(better-call@1.3.6(zod@4.4.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/utils': 0.4.2
|
'@better-auth/utils': 0.4.2
|
||||||
'@better-fetch/fetch': 1.3.1
|
'@better-fetch/fetch': 1.3.1
|
||||||
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
better-auth: 1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9)
|
||||||
better-call: 1.3.6(zod@4.4.3)
|
better-call: 1.3.6(zod@4.4.3)
|
||||||
fast-xml-parser: 5.9.2
|
fast-xml-parser: 5.9.2
|
||||||
jose: 6.2.3
|
jose: 6.2.3
|
||||||
@@ -9849,6 +9878,10 @@ snapshots:
|
|||||||
|
|
||||||
'@phosphor-icons/web@2.1.2': {}
|
'@phosphor-icons/web@2.1.2': {}
|
||||||
|
|
||||||
|
'@playwright/test@1.61.0':
|
||||||
|
dependencies:
|
||||||
|
playwright: 1.61.0
|
||||||
|
|
||||||
'@quansync/fs@1.0.0':
|
'@quansync/fs@1.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
quansync: 1.0.0
|
quansync: 1.0.0
|
||||||
@@ -10002,10 +10035,10 @@ snapshots:
|
|||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|
||||||
'@react-email/ui@6.6.3(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
'@react-email/ui@6.6.3(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.28.1
|
esbuild: 0.28.1
|
||||||
next: 16.2.6(@babel/core@8.0.0)(@opentelemetry/api@1.9.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
next: 16.2.6(@babel/core@8.0.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- '@opentelemetry/api'
|
- '@opentelemetry/api'
|
||||||
@@ -11201,7 +11234,7 @@ snapshots:
|
|||||||
node-addon-api: 8.8.0
|
node-addon-api: 8.8.0
|
||||||
node-gyp-build: 4.8.4
|
node-gyp-build: 4.8.4
|
||||||
|
|
||||||
better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9):
|
better-auth@1.6.19(@opentelemetry/api@1.9.1)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))(next@16.2.6(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(pg@8.21.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(vitest@4.1.9):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
'@better-auth/core': 1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0)
|
||||||
'@better-auth/drizzle-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))
|
'@better-auth/drizzle-adapter': 1.6.19(@better-auth/core@1.6.19(@better-auth/utils@0.4.2)(@better-fetch/fetch@1.3.1)(@opentelemetry/api@1.9.1)(better-call@1.3.6(zod@4.4.3))(jose@6.2.3)(kysely@0.29.2)(nanostores@1.3.0))(@better-auth/utils@0.4.2)(drizzle-orm@1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3))
|
||||||
@@ -11222,7 +11255,7 @@ snapshots:
|
|||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
drizzle-orm: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
drizzle-orm: 1.0.0-rc.3(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)(zod@4.4.3)
|
||||||
next: 16.2.6(@babel/core@8.0.0)(@opentelemetry/api@1.9.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
next: 16.2.6(@babel/core@8.0.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||||
pg: 8.21.0
|
pg: 8.21.0
|
||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
@@ -12015,6 +12048,9 @@ snapshots:
|
|||||||
jsonfile: 6.2.1
|
jsonfile: 6.2.1
|
||||||
universalify: 2.0.1
|
universalify: 2.0.1
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -12929,7 +12965,7 @@ snapshots:
|
|||||||
react: 19.2.7
|
react: 19.2.7
|
||||||
react-dom: 19.2.7(react@19.2.7)
|
react-dom: 19.2.7(react@19.2.7)
|
||||||
|
|
||||||
next@16.2.6(@babel/core@8.0.0)(@opentelemetry/api@1.9.1)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
next@16.2.6(@babel/core@8.0.0)(@opentelemetry/api@1.9.1)(@playwright/test@1.61.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 16.2.6
|
'@next/env': 16.2.6
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
@@ -12949,6 +12985,7 @@ snapshots:
|
|||||||
'@next/swc-win32-arm64-msvc': 16.2.6
|
'@next/swc-win32-arm64-msvc': 16.2.6
|
||||||
'@next/swc-win32-x64-msvc': 16.2.6
|
'@next/swc-win32-x64-msvc': 16.2.6
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
|
'@playwright/test': 1.61.0
|
||||||
babel-plugin-react-compiler: 1.0.0
|
babel-plugin-react-compiler: 1.0.0
|
||||||
sharp: 0.34.5
|
sharp: 0.34.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -13269,6 +13306,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up: 3.0.0
|
find-up: 3.0.0
|
||||||
|
|
||||||
|
playwright-core@1.61.0: {}
|
||||||
|
|
||||||
|
playwright@1.61.0:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.61.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
|
||||||
png-js@2.0.0:
|
png-js@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fflate: 0.8.3
|
fflate: 0.8.3
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# E2E Tests
|
||||||
|
|
||||||
|
Reactive Resume uses Playwright for PR-gated browser coverage of deterministic core flows.
|
||||||
|
|
||||||
|
## Local setup
|
||||||
|
|
||||||
|
Start PostgreSQL:
|
||||||
|
|
||||||
|
`sudo docker compose -f compose.dev.yml up -d postgres`
|
||||||
|
|
||||||
|
Generate local test secrets:
|
||||||
|
|
||||||
|
`export AUTH_SECRET=$(openssl rand -hex 32)`
|
||||||
|
|
||||||
|
`export ENCRYPTION_SECRET=$(openssl rand -hex 32)`
|
||||||
|
|
||||||
|
Run database migrations:
|
||||||
|
|
||||||
|
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm db:migrate`
|
||||||
|
|
||||||
|
Build the production app:
|
||||||
|
|
||||||
|
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm build`
|
||||||
|
|
||||||
|
Run tests:
|
||||||
|
|
||||||
|
`APP_URL=http://localhost:3000 PORT=3000 DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres FLAG_DISABLE_SIGNUPS=false FLAG_DISABLE_EMAIL_AUTH=false FLAG_DISABLE_API_RATE_LIMIT=true LOCAL_STORAGE_PATH=/workspace/data/e2e pnpm test:e2e`
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
|
||||||
|
- Email/password auth smoke.
|
||||||
|
- Dashboard sample resume creation.
|
||||||
|
- Builder basics edit and autosave persistence.
|
||||||
|
- JSON export/import.
|
||||||
|
- Public sharing for anonymous visitors.
|
||||||
|
|
||||||
|
PDF, DOCX, OAuth, passkeys, 2FA, password reset, and AI flows are intentionally outside the initial PR gate.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import type { APIRequestContext, Browser, BrowserContext, Page } from "@playwright/test";
|
||||||
|
import type { E2EAccount } from "./data";
|
||||||
|
|
||||||
|
async function assertAuthResponse(response: Awaited<ReturnType<APIRequestContext["post"]>>) {
|
||||||
|
if (response.ok()) return;
|
||||||
|
|
||||||
|
throw new Error(`Authentication request failed with ${response.status()}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function registerViaUi(page: Page, account: E2EAccount) {
|
||||||
|
await page.goto("/auth/register");
|
||||||
|
await page.getByRole("textbox", { name: "Name", exact: true }).fill(account.name);
|
||||||
|
await page.getByLabel("Username").fill(account.username);
|
||||||
|
await page.getByLabel("Email Address", { exact: true }).fill(account.email);
|
||||||
|
await page.getByLabel("Password", { exact: true }).fill(account.password);
|
||||||
|
await page.getByRole("button", { name: "Sign up" }).click();
|
||||||
|
await page.getByRole("button", { name: "Continue" }).click();
|
||||||
|
await page.waitForURL(/\/dashboard/);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loginViaUi(page: Page, account: E2EAccount) {
|
||||||
|
await page.goto("/auth/login");
|
||||||
|
await page.getByLabel("Email Address", { exact: true }).fill(account.email);
|
||||||
|
await page.getByLabel("Password", { exact: true }).fill(account.password);
|
||||||
|
await page.getByRole("button", { name: "Sign in" }).click();
|
||||||
|
await page.waitForURL(/\/dashboard/);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logoutViaUi(page: Page, account: E2EAccount) {
|
||||||
|
await page.getByText(account.email).click();
|
||||||
|
await page.getByRole("menuitem", { name: "Logout" }).click();
|
||||||
|
await page.goto("/auth/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerViaApi(request: APIRequestContext, account: E2EAccount, baseURL: string) {
|
||||||
|
const response = await request.post("/api/auth/sign-up/email", {
|
||||||
|
headers: {
|
||||||
|
origin: baseURL,
|
||||||
|
referer: `${baseURL}/auth/register`,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
name: account.name,
|
||||||
|
email: account.email,
|
||||||
|
password: account.password,
|
||||||
|
username: account.username,
|
||||||
|
displayUsername: account.username,
|
||||||
|
callbackURL: "/dashboard",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertAuthResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createAuthenticatedContext(
|
||||||
|
browser: Browser,
|
||||||
|
request: APIRequestContext,
|
||||||
|
account: E2EAccount,
|
||||||
|
baseURL: string,
|
||||||
|
): Promise<BrowserContext> {
|
||||||
|
await registerViaApi(request, account, baseURL);
|
||||||
|
|
||||||
|
return browser.newContext({
|
||||||
|
baseURL,
|
||||||
|
storageState: await request.storageState(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { TestInfo } from "@playwright/test";
|
||||||
|
|
||||||
|
const sanitize = (value: string) =>
|
||||||
|
value
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9]+/g, "-")
|
||||||
|
.replace(/^-|-$/g, "");
|
||||||
|
|
||||||
|
export type E2EAccount = {
|
||||||
|
name: string;
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createRunSlug(testInfo: TestInfo) {
|
||||||
|
const worker = testInfo.workerIndex;
|
||||||
|
const title = sanitize(testInfo.titlePath.join("-")).slice(0, 32);
|
||||||
|
const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|
||||||
|
return `e2e-${worker}-${title}-${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAccount(testInfo: TestInfo): E2EAccount {
|
||||||
|
const username = createRunSlug(testInfo).replaceAll("-", "_").slice(0, 64);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: "E2E Test User",
|
||||||
|
username,
|
||||||
|
email: `${username}@example.test`,
|
||||||
|
password: "Password123!",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createResumeName(testInfo: TestInfo) {
|
||||||
|
return `E2E Resume ${createRunSlug(testInfo)}`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { E2EAccount } from "./data";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
function getDatabaseUrl() {
|
||||||
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
if (!databaseUrl) throw new Error("DATABASE_URL is required for E2E cleanup.");
|
||||||
|
|
||||||
|
return databaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteE2EUser(account: E2EAccount) {
|
||||||
|
const pool = new Pool({ connectionString: getDatabaseUrl() });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pool.query('delete from "user" where email = $1 or username = $2', [account.email, account.username]);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { Page, TestInfo } from "@playwright/test";
|
||||||
|
import { expect } from "@playwright/test";
|
||||||
|
import { createResumeName } from "./data";
|
||||||
|
|
||||||
|
export async function createSampleResumeFromDashboard(page: Page, testInfo: TestInfo) {
|
||||||
|
const resumeName = createResumeName(testInfo);
|
||||||
|
|
||||||
|
await page.goto("/dashboard/resumes");
|
||||||
|
await page.getByText("Create a new resume").click();
|
||||||
|
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Create a new resume" });
|
||||||
|
await dialog.getByLabel("Name").fill(resumeName);
|
||||||
|
|
||||||
|
const createGroup = dialog.getByRole("group", { name: "Create resume with options" });
|
||||||
|
await createGroup.getByRole("button").last().click();
|
||||||
|
await page.getByRole("menuitem", { name: "Create a Sample Resume" }).click();
|
||||||
|
|
||||||
|
const resumeLink = page.getByRole("link", { name: new RegExp(resumeName) });
|
||||||
|
await expect(resumeLink).toBeVisible();
|
||||||
|
await resumeLink.click();
|
||||||
|
await page.waitForURL(/\/builder\/.+/);
|
||||||
|
|
||||||
|
return resumeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openSidebarSection(page: Page, title: string) {
|
||||||
|
await page.getByTitle(title, { exact: true }).click();
|
||||||
|
await expect(page.getByRole("heading", { name: title, exact: true })).toBeVisible();
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import type { BrowserContext, Page } from "@playwright/test";
|
||||||
|
import type { E2EAccount } from "./data";
|
||||||
|
import { test as base, expect } from "@playwright/test";
|
||||||
|
import { createAuthenticatedContext } from "./auth";
|
||||||
|
import { createAccount } from "./data";
|
||||||
|
import { deleteE2EUser } from "./db";
|
||||||
|
|
||||||
|
type Fixtures = {
|
||||||
|
account: E2EAccount;
|
||||||
|
authContext: BrowserContext;
|
||||||
|
authPage: Page;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const test = base.extend<Fixtures>({
|
||||||
|
account: async ({ baseURL }, use, testInfo) => {
|
||||||
|
void baseURL;
|
||||||
|
const account = createAccount(testInfo);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await use(account);
|
||||||
|
} finally {
|
||||||
|
await deleteE2EUser(account);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authContext: async ({ browser, request, account }, use, testInfo) => {
|
||||||
|
const baseURL = String(testInfo.project.use.baseURL ?? "http://localhost:3000");
|
||||||
|
const context = await createAuthenticatedContext(browser, request, account, baseURL);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await use(context);
|
||||||
|
} finally {
|
||||||
|
await context.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
authPage: async ({ authContext }, use) => {
|
||||||
|
const page = await authContext.newPage();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await use(page);
|
||||||
|
} finally {
|
||||||
|
await page.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export { expect };
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { loginViaUi, logoutViaUi, registerViaUi } from "../fixtures/auth";
|
||||||
|
import { expect, test } from "../fixtures/test";
|
||||||
|
|
||||||
|
test("registers and logs in with email credentials", async ({ page, account }) => {
|
||||||
|
await registerViaUi(page, account);
|
||||||
|
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||||
|
|
||||||
|
await logoutViaUi(page, account);
|
||||||
|
await expect(page.getByRole("heading", { name: "Sign in to your account" })).toBeVisible();
|
||||||
|
|
||||||
|
await loginViaUi(page, account);
|
||||||
|
await expect(page.getByRole("heading", { name: "Resumes" })).toBeVisible();
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { readFile } from "node:fs/promises";
|
||||||
|
import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume";
|
||||||
|
import { expect, test } from "../fixtures/test";
|
||||||
|
|
||||||
|
test("exports and imports a resume JSON backup", async ({ authPage: page }, testInfo) => {
|
||||||
|
await createSampleResumeFromDashboard(page, testInfo);
|
||||||
|
|
||||||
|
await openSidebarSection(page, "Export");
|
||||||
|
|
||||||
|
const downloadPromise = page.waitForEvent("download");
|
||||||
|
await page.getByRole("button", { name: /^JSON/ }).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
expect(download.suggestedFilename()).toMatch(/\.json$/);
|
||||||
|
|
||||||
|
const downloadPath = testInfo.outputPath(download.suggestedFilename());
|
||||||
|
await download.saveAs(downloadPath);
|
||||||
|
const exportedData = JSON.parse(await readFile(downloadPath, "utf-8")) as { basics: { name: string } };
|
||||||
|
|
||||||
|
await page.goto("/dashboard/resumes");
|
||||||
|
await page.getByText("Import an existing resume").click();
|
||||||
|
const dialog = page.getByRole("dialog", { name: "Import an existing resume" });
|
||||||
|
await dialog.getByRole("combobox").click();
|
||||||
|
await page.getByRole("option", { name: "Reactive Resume (JSON)" }).click();
|
||||||
|
await dialog.locator('input[type="file"]').setInputFiles(downloadPath);
|
||||||
|
await dialog.getByRole("button", { name: "Import", exact: true }).click();
|
||||||
|
|
||||||
|
await page.waitForURL(/\/builder\/.+/);
|
||||||
|
await openSidebarSection(page, "Basics");
|
||||||
|
await expect(page.getByLabel("Name")).toHaveValue(exportedData.basics.name);
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume";
|
||||||
|
import { expect, test } from "../fixtures/test";
|
||||||
|
|
||||||
|
test("publishes a resume and renders it for an anonymous visitor", async ({ browser, authPage: page }, testInfo) => {
|
||||||
|
await createSampleResumeFromDashboard(page, testInfo);
|
||||||
|
await openSidebarSection(page, "Sharing");
|
||||||
|
|
||||||
|
await page.getByRole("switch", { name: /Allow Public Access/ }).click();
|
||||||
|
const sharingUrl = page.locator("#sharing-url");
|
||||||
|
await expect(sharingUrl).toHaveValue(/\/e2e_/);
|
||||||
|
const publicUrl = await sharingUrl.inputValue();
|
||||||
|
expect(publicUrl).toMatch(/\/e2e_/);
|
||||||
|
|
||||||
|
const anonymous = await browser.newPage();
|
||||||
|
try {
|
||||||
|
await anonymous.goto(publicUrl);
|
||||||
|
await expect(anonymous.getByRole("button", { name: "Download PDF" })).toBeVisible();
|
||||||
|
} finally {
|
||||||
|
await anonymous.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { createSampleResumeFromDashboard, openSidebarSection } from "../fixtures/resume";
|
||||||
|
import { expect, test } from "../fixtures/test";
|
||||||
|
|
||||||
|
test("creates a sample resume and persists a basics edit", async ({ authPage: page }, testInfo) => {
|
||||||
|
await createSampleResumeFromDashboard(page, testInfo);
|
||||||
|
|
||||||
|
const updatedName = `E2E Edited ${Date.now()}`;
|
||||||
|
await openSidebarSection(page, "Basics");
|
||||||
|
const savePromise = page.waitForResponse((response) => {
|
||||||
|
if (!response.url().includes("/api/rpc")) return false;
|
||||||
|
if (response.request().method() !== "POST") return false;
|
||||||
|
if (!response.ok()) return false;
|
||||||
|
|
||||||
|
const body = response.request().postData() ?? "";
|
||||||
|
return body.includes(updatedName);
|
||||||
|
});
|
||||||
|
await page.getByLabel("Name").fill(updatedName);
|
||||||
|
await savePromise;
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await openSidebarSection(page, "Basics");
|
||||||
|
await expect(page.getByLabel("Name")).toHaveValue(updatedName);
|
||||||
|
});
|
||||||
@@ -101,6 +101,9 @@
|
|||||||
"test": {},
|
"test": {},
|
||||||
"test:coverage": {},
|
"test:coverage": {},
|
||||||
"test:ci": {},
|
"test:ci": {},
|
||||||
|
"test:e2e": {
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
"test:agent": {},
|
"test:agent": {},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
|
|||||||
Reference in New Issue
Block a user