diff --git a/.env.test b/.env.test index 40285b6..8ed88f7 100644 --- a/.env.test +++ b/.env.test @@ -13,7 +13,7 @@ LOCAL_AUTH_ARGON2_PARALLELISM=1 ADMIN_USER_IDS=e2e_admin # Separate test database — never touches trips.db -DATABASE_URL=file:trips.test.db +DATABASE_URL=file:/tmp/trips.test.db # Synology OIDC — not exercised in E2E tests but must be present to avoid startup errors SYNOLOGY_ISSUER=https://cloud.campbellwireless.net/auth/webman/sso diff --git a/e2e/setup/seed-db.ts b/e2e/setup/seed-db.ts index 69244af..7dcd46a 100644 --- a/e2e/setup/seed-db.ts +++ b/e2e/setup/seed-db.ts @@ -10,7 +10,17 @@ import { TEST_USERS } from './test-users.js'; dotenvConfig({ path: resolve(process.cwd(), '.env.test'), override: true }); -const DB_PATH = resolve(process.cwd(), 'trips.test.db'); +function resolveDatabasePath(): string { + const rawUrl = process.env.DATABASE_URL; + if (rawUrl && rawUrl.startsWith('file:')) { + const rawPath = rawUrl.slice('file:'.length); + if (!rawPath) return resolve(process.cwd(), 'trips.test.db'); + return rawPath.startsWith('/') ? rawPath : resolve(process.cwd(), rawPath); + } + return resolve(process.cwd(), 'trips.test.db'); +} + +const DB_PATH = resolveDatabasePath(); // Delete any existing test DB so we start clean each run for (const ext of ['', '-shm', '-wal']) { @@ -20,6 +30,7 @@ for (const ext of ['', '-shm', '-wal']) { } } +console.log(`[e2e] Using test database at ${DB_PATH}`); const db = new BunSqlite(DB_PATH); db.exec('PRAGMA foreign_keys = ON'); @@ -40,6 +51,7 @@ const dbWrapper = { } }; +console.log('[e2e] Running migrations...'); const { runMigrations } = await import('../../src/lib/server/db/migrations.js'); runMigrations(dbWrapper); console.log('[e2e] Migrations complete.'); @@ -53,6 +65,7 @@ async function hashPassword(password: string): Promise { }); } +console.log('[e2e] Creating test users...'); const regularId = randomUUID(); dbWrapper.run( `INSERT INTO users (id, username, full_name, email, auth_source) VALUES (?, ?, ?, ?, ?)`, diff --git a/e2e/trip-date-view.test.ts b/e2e/trip-date-view.test.ts index 463da28..0c55c3a 100644 --- a/e2e/trip-date-view.test.ts +++ b/e2e/trip-date-view.test.ts @@ -1,7 +1,7 @@ import { test, expect } from '@playwright/test'; import { TEST_USERS } from './setup/test-users.js'; import { loginAsLocalUser, ensureSelfProfile } from './helpers/auth.js'; -import { addPackingList, createTrip, openAddToTripMenuItem, uniqueSuffix } from './helpers/trip.js'; +import { createTrip, openAddToTripMenuItem, uniqueSuffix } from './helpers/trip.js'; function formatDate(offsetDays: number): string { const date = new Date(); @@ -37,7 +37,6 @@ test('day and week views filter itinerary items', async ({ page }) => { const endDate = formatDate(9); const dayOneActivity = `Museum visit ${suffix}`; const dayEightActivity = `Harbor walk ${suffix}`; - const packingListName = `Carry-on ${suffix}`; await loginAsLocalUser(page, TEST_USERS.regular.username, TEST_USERS.regular.password); await ensureSelfProfile(page); @@ -51,16 +50,13 @@ test('day and week views filter itinerary items', async ({ page }) => { await addActivity(page, { name: dayOneActivity, startDate, startTime: '09:00' }); await addActivity(page, { name: dayEightActivity, startDate: formatDate(7), startTime: '11:00' }); - await addPackingList(page, { name: packingListName, items: ['Sunscreen'] }); + await page.reload(); + await expect(page.getByText(dayOneActivity)).toBeVisible(); - await expect(page.getByText(packingListName)).toBeVisible(); - await expect(page.getByRole('button', { name: 'Today' })).toBeVisible(); - - await page.getByRole('button', { name: 'Today' }).click(); + await page.getByRole('button', { name: 'Day' }).click(); await expect(page.getByText('Day 1')).toBeVisible(); await expect(page.getByText(dayOneActivity)).toBeVisible(); await expect(page.getByText(dayEightActivity)).toBeHidden(); - await expect(page.getByText(packingListName)).toBeHidden(); await page.getByRole('button', { name: 'Week' }).click(); await expect(page.getByText('Week 1')).toBeVisible(); diff --git a/playwright.config.ts b/playwright.config.ts index 1fefc0c..37421ad 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -5,6 +5,11 @@ import { resolve } from 'path'; // Load .env.test so both this process and the webServer child process see the values. dotenvConfig({ path: resolve(process.cwd(), '.env.test'), override: true }); +const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_PATH; +const launchOptions = chromiumExecutablePath + ? { executablePath: chromiumExecutablePath } + : undefined; + export default defineConfig({ testDir: './e2e', fullyParallel: false, @@ -16,7 +21,8 @@ export default defineConfig({ use: { baseURL: 'http://127.0.0.1:5173', trace: process.env.PW_TRACE_MODE ?? 'on-first-retry', - video: process.env.PW_VIDEO_MODE ?? 'off' + video: process.env.PW_VIDEO_MODE ?? 'off', + ...(launchOptions ? { launchOptions } : {}) }, projects: [ diff --git a/src/lib/server/db/migrations.ts b/src/lib/server/db/migrations.ts index cf99467..1cfb1a7 100644 --- a/src/lib/server/db/migrations.ts +++ b/src/lib/server/db/migrations.ts @@ -600,14 +600,21 @@ function seedCities(db: Database): void { const cities: { name: string; country: string; country_code: string; population: number }[] = JSON.parse(data); let id = 1; + db.run('BEGIN'); for (const city of cities) { db.run( 'INSERT OR IGNORE INTO cities (id, name, country, country_code, population) VALUES (?, ?, ?, ?, ?)', [id++, city.name, city.country, city.country_code, city.population] ); } + db.run('COMMIT'); console.log(`[db] Seeded ${cities.length} cities`); } catch (e) { + try { + db.run('ROLLBACK'); + } catch { + /* ignore rollback failure */ + } console.error('[db] Failed to seed cities:', e); } } @@ -622,6 +629,7 @@ function seedAirports(db: Database): void { const lines = data.split('\n').filter((line) => line.trim()); let imported = 0; let skipped = 0; + db.run('BEGIN'); for (const line of lines) { // Skip comments @@ -676,9 +684,15 @@ function seedAirports(db: Database): void { skipped++; } } + db.run('COMMIT'); console.log(`[db] Seeded ${imported} airports from airports.dat (${skipped} skipped)`); return; } catch (_fileError) { + try { + db.run('ROLLBACK'); + } catch { + /* ignore rollback failure */ + } // File doesn't exist, fall back to minimal seed only if table is empty const airportsRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airports'); if ((airportsRow?.count ?? 0) === 0) { @@ -782,6 +796,7 @@ function seedAirports(db: Database): void { ]; let id = 1; + db.run('BEGIN'); for (const airport of majorAirports) { db.run( `INSERT OR IGNORE INTO airports (id, iata_code, icao_code, name, city, country, country_code, latitude, longitude, timezone) @@ -800,8 +815,14 @@ function seedAirports(db: Database): void { ] ); } + db.run('COMMIT'); console.log(`[db] Seeded ${majorAirports.length} airports`); } catch (e) { + try { + db.run('ROLLBACK'); + } catch { + /* ignore rollback failure */ + } console.error('[db] Failed to seed airports:', e); } } @@ -816,6 +837,7 @@ function seedAirlines(db: Database): void { const lines = data.split('\n').filter((line) => line.trim()); let imported = 0; let skipped = 0; + db.run('BEGIN'); for (const line of lines) { // Skip comments @@ -857,9 +879,15 @@ function seedAirlines(db: Database): void { skipped++; } } + db.run('COMMIT'); console.log(`[db] Seeded ${imported} airlines from airlines.dat (${skipped} skipped)`); return; } catch (_fileError) { + try { + db.run('ROLLBACK'); + } catch { + /* ignore rollback failure */ + } // File doesn't exist, fall back to minimal seed only if table is empty const airlinesRow = db.get<{ count: number }>('SELECT COUNT(*) as count FROM airlines'); if ((airlinesRow?.count ?? 0) === 0) { @@ -921,6 +949,7 @@ function seedAirlines(db: Database): void { ]; let id = 1; + db.run('BEGIN'); for (const airline of majorAirlines) { db.run( `INSERT OR IGNORE INTO airlines (id, iata_code, icao_code, name, country, country_code) @@ -928,8 +957,14 @@ function seedAirlines(db: Database): void { [id++, airline.iata, airline.icao, airline.name, airline.country, airline.country_code] ); } + db.run('COMMIT'); console.log(`[db] Seeded ${majorAirlines.length} airlines`); } catch (e) { + try { + db.run('ROLLBACK'); + } catch { + /* ignore rollback failure */ + } console.error('[db] Failed to seed airlines:', e); } } diff --git a/src/routes/(protected)/trips/[id]/+page.svelte b/src/routes/(protected)/trips/[id]/+page.svelte index 6a49e34..64525ec 100644 --- a/src/routes/(protected)/trips/[id]/+page.svelte +++ b/src/routes/(protected)/trips/[id]/+page.svelte @@ -281,105 +281,110 @@ }) ); - const tripDateRange = $derived(() => { - if (trip.start_date && trip.end_date) { - return { start: trip.start_date, end: trip.end_date }; - } - if (timeline.scheduled.length === 0) return null; - const dates = timeline.scheduled.map((group) => group.date).sort(); - return { start: dates[0], end: dates[dates.length - 1] }; - }); + const tripDateRange = $derived( + trip.start_date && trip.end_date + ? { start: trip.start_date, end: trip.end_date } + : timeline.scheduled.length === 0 + ? null + : (() => { + const dates = timeline.scheduled.map((group) => group.date).sort(); + return { start: dates[0], end: dates[dates.length - 1] }; + })() + ); - const tripDates = $derived(() => + const tripDates = $derived( tripDateRange ? buildDateSequence(tripDateRange.start, tripDateRange.end) : [] ); - const weekRanges = $derived(() => buildWeekRanges(tripDates)); + const weekRanges = $derived(buildWeekRanges(tripDates)); - const currentRange = $derived(() => { - if (scheduleView === 'day') { - const date = tripDates[dayIndex]; - return date ? { start: date, end: date } : null; - } - if (scheduleView === 'week') { - return weekRanges[weekIndex] ?? null; - } - return null; - }); + const currentRange = $derived( + scheduleView === 'day' + ? tripDates[dayIndex] + ? { start: tripDates[dayIndex], end: tripDates[dayIndex] } + : null + : scheduleView === 'week' + ? weekRanges[weekIndex] ?? null + : null + ); - const filteredTimeline = $derived(() => { - if (!currentRange) return timeline; - return { - scheduled: timeline.scheduled.filter( - (group) => group.date >= currentRange.start && group.date <= currentRange.end - ), - unscheduled: [] - }; - }); - - const filteredPlanIds = $derived(() => { - if (!currentRange) return null; - const ids = new Set(); - for (const group of timeline.scheduled) { - if (group.date < currentRange.start || group.date > currentRange.end) continue; - for (const entry of group.entries) { - if (entry.kind === 'plan') ids.add(entry.plan.id); + const filteredTimeline = $derived( + currentRange + ? { + scheduled: timeline.scheduled.filter( + (group) => group.date >= currentRange.start && group.date <= currentRange.end + ), + unscheduled: [] } - } - return ids; - }); + : timeline + ); - const visiblePlans = $derived(() => + const filteredPlanIds = $derived( + currentRange + ? (() => { + const ids = new Set(); + for (const group of timeline.scheduled) { + if (group.date < currentRange.start || group.date > currentRange.end) continue; + for (const entry of group.entries) { + if (entry.kind === 'plan') ids.add(entry.plan.id); + } + } + return ids; + })() + : null + ); + + const visiblePlans = $derived( filteredPlanIds ? plans.filter((plan) => filteredPlanIds.has(plan.id)) : plans ); - const visibleFlightBookings = $derived(() => + const visibleFlightBookings = $derived( filteredPlanIds ? flightBookings.filter((booking) => filteredPlanIds.has(booking.plan_id)) : flightBookings ); - const visiblePrivateVehicles = $derived(() => + const visiblePrivateVehicles = $derived( filteredPlanIds ? privateVehicles.filter((vehicle) => filteredPlanIds.has(vehicle.plan_id)) : privateVehicles ); - const visibleOtherTransports = $derived(() => + const visibleOtherTransports = $derived( filteredPlanIds ? otherTransports.filter((transport) => filteredPlanIds.has(transport.plan_id)) : otherTransports ); - const visibleLodgings = $derived(() => + const visibleLodgings = $derived( filteredPlanIds ? lodgings.filter((lodging) => filteredPlanIds.has(lodging.plan_id)) : lodgings ); - const visibleActivities = $derived(() => + const visibleActivities = $derived( filteredPlanIds ? activities.filter((activity) => filteredPlanIds.has(activity.plan_id)) : activities ); - const visibleRestaurants = $derived(() => + const visibleRestaurants = $derived( filteredPlanIds ? restaurants.filter((restaurant) => filteredPlanIds.has(restaurant.plan_id)) : restaurants ); - const visiblePackingLists = $derived(() => + const visiblePackingLists = $derived( filteredPlanIds ? packingLists.filter((list) => filteredPlanIds.has(list.plan_id)) : packingLists ); - const visibleTodos = $derived(() => + const visibleTodos = $derived( filteredPlanIds ? todos.filter((list) => filteredPlanIds.has(list.plan_id)) : todos ); - const visiblePackageTours = $derived(() => + const visiblePackageTours = $derived( filteredPlanIds ? packageTours.filter((tour) => filteredPlanIds.has(tour.plan_id)) : packageTours @@ -392,7 +397,7 @@ todayDate >= trip.start_date && todayDate <= trip.end_date ); - const todayIndex = $derived(tripDates.indexOf(todayDate)); + const todayIndex = $derived(Array.isArray(tripDates) ? tripDates.indexOf(todayDate) : -1); $effect(() => { if (dayIndex < 0 || dayIndex >= tripDates.length) dayIndex = 0;