diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts new file mode 100644 index 0000000..39624a3 --- /dev/null +++ b/e2e/global-setup.ts @@ -0,0 +1,10 @@ +import { seedTestDatabase } from "./seed"; + +export default async function globalSetup() { + await seedTestDatabase(); +} + +// Allow direct invocation: bun run e2e/global-setup.ts +if (import.meta.main) { + await globalSetup(); +} diff --git a/e2e/seed.ts b/e2e/seed.ts new file mode 100644 index 0000000..96a69ec --- /dev/null +++ b/e2e/seed.ts @@ -0,0 +1,220 @@ +import { Database } from "bun:sqlite"; +import { unlink } from "node:fs/promises"; +import { drizzle } from "drizzle-orm/bun-sqlite"; +import { migrate } from "drizzle-orm/bun-sqlite/migrator"; +import * as schema from "../src/db/schema"; + +const DB_PATH = "./e2e/test.db"; + +export async function seedTestDatabase() { + // Remove old test DB if it exists + try { + await unlink(DB_PATH); + } catch { + // File doesn't exist, that's fine + } + + const sqlite = new Database(DB_PATH); + sqlite.run("PRAGMA journal_mode = WAL"); + sqlite.run("PRAGMA foreign_keys = ON"); + + const db = drizzle(sqlite, { schema }); + migrate(db, { migrationsFolder: "./drizzle" }); + + // ── Categories ── + const [uncategorized] = db + .insert(schema.categories) + .values({ name: "Uncategorized", icon: "package" }) + .returning() + .all(); + + const [shelter] = db + .insert(schema.categories) + .values({ name: "Shelter", icon: "tent" }) + .returning() + .all(); + + const [sleep] = db + .insert(schema.categories) + .values({ name: "Sleep System", icon: "moon" }) + .returning() + .all(); + + const [cook] = db + .insert(schema.categories) + .values({ name: "Cook Kit", icon: "flame" }) + .returning() + .all(); + + // ── Items ── + const tent = db + .insert(schema.items) + .values({ + name: "Zpacks Duplex", + weightGrams: 539, + priceCents: 67900, + categoryId: shelter.id, + notes: "DCF shelter, 2-person", + }) + .returning() + .get(); + + db.insert(schema.items) + .values({ + name: "Borah Gear Tarp", + weightGrams: 156, + priceCents: 11000, + categoryId: shelter.id, + }) + .run(); + + const quilt = db + .insert(schema.items) + .values({ + name: "Enlightened Equipment Enigma 20", + weightGrams: 595, + priceCents: 34000, + categoryId: sleep.id, + notes: "20F quilt", + }) + .returning() + .get(); + + const pad = db + .insert(schema.items) + .values({ + name: "Therm-a-Rest NeoAir XLite", + weightGrams: 354, + priceCents: 20999, + categoryId: sleep.id, + }) + .returning() + .get(); + + const stove = db + .insert(schema.items) + .values({ + name: "BRS-3000T Stove", + weightGrams: 25, + priceCents: 2000, + categoryId: cook.id, + }) + .returning() + .get(); + + db.insert(schema.items) + .values({ + name: "Toaks 750ml Pot", + weightGrams: 103, + priceCents: 3000, + categoryId: cook.id, + }) + .run(); + + // ── Active Thread with 3 Candidates ── + const activeThread = db + .insert(schema.threads) + .values({ + name: "New Backpack", + status: "active", + categoryId: uncategorized.id, + }) + .returning() + .get(); + + db.insert(schema.threadCandidates) + .values({ + threadId: activeThread.id, + name: "ULA Circuit", + weightGrams: 1077, + priceCents: 27500, + categoryId: uncategorized.id, + pros: "Great hip belt\nLarge capacity", + cons: "Heavier than competitors", + sortOrder: 1000, + status: "researching", + }) + .run(); + + db.insert(schema.threadCandidates) + .values({ + threadId: activeThread.id, + name: "Gossamer Gear Mariposa", + weightGrams: 737, + priceCents: 28500, + categoryId: uncategorized.id, + pros: "Very lightweight\nGood ventilation", + cons: "Smaller hip belt pockets", + sortOrder: 2000, + status: "researching", + }) + .run(); + + db.insert(schema.threadCandidates) + .values({ + threadId: activeThread.id, + name: "Granite Gear Crown2 38", + weightGrams: 850, + priceCents: 18000, + categoryId: uncategorized.id, + sortOrder: 3000, + status: "ordered", + }) + .run(); + + // ── Resolved Thread ── + const resolvedThread = db + .insert(schema.threads) + .values({ + name: "Camp Stove", + status: "resolved", + categoryId: cook.id, + resolvedCandidateId: 1, + }) + .returning() + .get(); + + db.insert(schema.threadCandidates) + .values({ + threadId: resolvedThread.id, + name: "BRS-3000T", + weightGrams: 25, + priceCents: 2000, + categoryId: cook.id, + sortOrder: 1000, + status: "arrived", + }) + .run(); + + // ── Setup with Items ── + const setup = db + .insert(schema.setups) + .values({ name: "Weekend Overnighter" }) + .returning() + .get(); + + db.insert(schema.setupItems) + .values([ + { setupId: setup.id, itemId: tent.id, classification: "base" }, + { setupId: setup.id, itemId: quilt.id, classification: "base" }, + { setupId: setup.id, itemId: pad.id, classification: "base" }, + { setupId: setup.id, itemId: stove.id, classification: "consumable" }, + ]) + .run(); + + // ── User ── + const passwordHash = await Bun.password.hash("password123"); + db.insert(schema.users).values({ username: "admin", passwordHash }).run(); + + // ── Settings ── + db.insert(schema.settings) + .values([ + { key: "weightUnit", value: "g" }, + { key: "currency", value: "USD" }, + { key: "onboardingComplete", value: "true" }, + ]) + .run(); + + sqlite.close(); + console.log("E2E test database seeded at", DB_PATH); +}