mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 22:31:46 +00:00
test(front): add test suite frontend (WIP)
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* Pure data builder functions for frontend tests.
|
||||
* These return typed objects matching interfaces in src/types.ts.
|
||||
* They do NOT touch a database.
|
||||
*/
|
||||
|
||||
import type {
|
||||
User,
|
||||
Trip,
|
||||
Day,
|
||||
Place,
|
||||
Assignment,
|
||||
DayNote,
|
||||
PackingItem,
|
||||
TodoItem,
|
||||
BudgetItem,
|
||||
Reservation,
|
||||
TripFile,
|
||||
Tag,
|
||||
Category,
|
||||
Settings,
|
||||
AppConfig,
|
||||
} from '../../src/types';
|
||||
|
||||
// ── Counters ──────────────────────────────────────────────────────────────────
|
||||
|
||||
let _seq = 0;
|
||||
function next(): number {
|
||||
return ++_seq;
|
||||
}
|
||||
|
||||
// ── InAppNotification (local interface, not in types.ts) ──────────────────────
|
||||
|
||||
export interface InAppNotification {
|
||||
id: number;
|
||||
type: string;
|
||||
message: string;
|
||||
read: boolean;
|
||||
created_at: string;
|
||||
trip_id?: number | null;
|
||||
}
|
||||
|
||||
// ── Builders ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export function buildUser(overrides: Partial<User> = {}): User {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
username: `user${id}`,
|
||||
email: `user${id}@example.com`,
|
||||
role: 'user',
|
||||
avatar_url: null,
|
||||
maps_api_key: null,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
mfa_enabled: false,
|
||||
must_change_password: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAdmin(overrides: Partial<User> = {}): User {
|
||||
return buildUser({ role: 'admin', ...overrides });
|
||||
}
|
||||
|
||||
export function buildTrip(overrides: Partial<Trip> = {}): Trip {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
name: `Trip ${id}`,
|
||||
description: null,
|
||||
start_date: '2025-06-01',
|
||||
end_date: '2025-06-05',
|
||||
cover_url: null,
|
||||
is_archived: false,
|
||||
reminder_days: 7,
|
||||
owner_id: 1,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
updated_at: '2025-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDay(overrides: Partial<Day> = {}): Day {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
trip_id: 1,
|
||||
date: '2025-06-01',
|
||||
title: null,
|
||||
notes: null,
|
||||
assignments: [],
|
||||
notes_items: [],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPlace(overrides: Partial<Place> = {}): Place {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
trip_id: 1,
|
||||
name: `Place ${id}`,
|
||||
description: null,
|
||||
lat: 48.8566,
|
||||
lng: 2.3522,
|
||||
address: null,
|
||||
category_id: null,
|
||||
icon: null,
|
||||
price: null,
|
||||
image_url: null,
|
||||
google_place_id: null,
|
||||
osm_id: null,
|
||||
route_geometry: null,
|
||||
place_time: null,
|
||||
end_time: null,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAssignment(overrides: Partial<Assignment> = {}): Assignment {
|
||||
const id = next();
|
||||
const place = overrides.place ?? buildPlace();
|
||||
return {
|
||||
id,
|
||||
day_id: 1,
|
||||
place_id: place.id,
|
||||
order_index: 0,
|
||||
notes: null,
|
||||
place,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDayNote(overrides: Partial<DayNote> = {}): DayNote {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
day_id: 1,
|
||||
text: 'Test note',
|
||||
time: null,
|
||||
icon: null,
|
||||
sort_order: 0,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPackingItem(overrides: Partial<PackingItem> = {}): PackingItem {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
trip_id: 1,
|
||||
name: `Packing item ${id}`,
|
||||
category: null,
|
||||
checked: 0,
|
||||
quantity: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTodoItem(overrides: Partial<TodoItem> = {}): TodoItem {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
trip_id: 1,
|
||||
name: `Todo ${id}`,
|
||||
category: null,
|
||||
checked: 0,
|
||||
sort_order: 0,
|
||||
due_date: null,
|
||||
description: null,
|
||||
assigned_user_id: null,
|
||||
priority: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBudgetItem(overrides: Partial<BudgetItem> = {}): BudgetItem {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
trip_id: 1,
|
||||
name: `Budget item ${id}`,
|
||||
amount: 100,
|
||||
currency: 'EUR',
|
||||
category: null,
|
||||
paid_by: null,
|
||||
persons: 1,
|
||||
members: [],
|
||||
expense_date: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildReservation(overrides: Partial<Reservation> = {}): Reservation {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
trip_id: 1,
|
||||
name: `Reservation ${id}`,
|
||||
type: 'restaurant',
|
||||
status: 'confirmed',
|
||||
date: null,
|
||||
time: null,
|
||||
confirmation_number: null,
|
||||
notes: null,
|
||||
url: null,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTripFile(overrides: Partial<TripFile> = {}): TripFile {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
trip_id: 1,
|
||||
filename: 'test.pdf',
|
||||
original_name: 'test.pdf',
|
||||
mime_type: 'application/pdf',
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTag(overrides: Partial<Tag> = {}): Tag {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
name: `Tag ${id}`,
|
||||
color: '#ff0000',
|
||||
user_id: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCategory(overrides: Partial<Category> = {}): Category {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
name: `Category ${id}`,
|
||||
icon: 'restaurant',
|
||||
user_id: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildSettings(overrides: Partial<Settings> = {}): Settings {
|
||||
return {
|
||||
map_tile_url: '',
|
||||
default_lat: 48.8566,
|
||||
default_lng: 2.3522,
|
||||
default_zoom: 10,
|
||||
dark_mode: false,
|
||||
default_currency: 'USD',
|
||||
language: 'en',
|
||||
temperature_unit: 'fahrenheit',
|
||||
time_format: '12h',
|
||||
show_place_description: false,
|
||||
route_calculation: false,
|
||||
blur_booking_codes: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildInAppNotification(overrides: Partial<InAppNotification> = {}): InAppNotification {
|
||||
const id = next();
|
||||
return {
|
||||
id,
|
||||
type: 'trip_invite',
|
||||
message: `Notification ${id}`,
|
||||
read: false,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
trip_id: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAppConfig(overrides: Partial<AppConfig> = {}): AppConfig {
|
||||
return {
|
||||
has_users: true,
|
||||
allow_registration: true,
|
||||
demo_mode: false,
|
||||
oidc_configured: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const addonHandlers = [
|
||||
http.get('/api/addons', () => {
|
||||
return HttpResponse.json({
|
||||
addons: [
|
||||
{ id: 'vacay', name: 'Vacay', type: 'feature', icon: 'calendar', enabled: true },
|
||||
{ id: 'atlas', name: 'Atlas', type: 'feature', icon: 'map', enabled: true },
|
||||
],
|
||||
});
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,125 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildUser, buildAdmin } from '../../factories';
|
||||
|
||||
export const adminHandlers = [
|
||||
http.get('/api/admin/users', () => {
|
||||
const user1 = buildUser({ username: 'alice', email: 'alice@example.com' });
|
||||
const admin1 = buildAdmin({ username: 'admin', email: 'admin@example.com' });
|
||||
return HttpResponse.json({ users: [admin1, user1] });
|
||||
}),
|
||||
|
||||
http.post('/api/admin/users', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const user = buildUser({ ...body });
|
||||
return HttpResponse.json({ user });
|
||||
}),
|
||||
|
||||
http.put('/api/admin/users/:id', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const user = buildUser({ id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ user });
|
||||
}),
|
||||
|
||||
http.delete('/api/admin/users/:id', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/stats', () => {
|
||||
return HttpResponse.json({
|
||||
totalUsers: 2,
|
||||
totalTrips: 5,
|
||||
totalPlaces: 42,
|
||||
totalFiles: 8,
|
||||
});
|
||||
}),
|
||||
|
||||
http.get('/api/admin/invites', () => {
|
||||
return HttpResponse.json({ invites: [] });
|
||||
}),
|
||||
|
||||
http.post('/api/admin/invites', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ invite: { id: 1, token: 'test-invite-token', ...body } });
|
||||
}),
|
||||
|
||||
http.delete('/api/admin/invites/:id', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/oidc', () => {
|
||||
return HttpResponse.json({
|
||||
issuer: '',
|
||||
client_id: '',
|
||||
client_secret: '',
|
||||
client_secret_set: false,
|
||||
display_name: '',
|
||||
oidc_only: false,
|
||||
discovery_url: '',
|
||||
});
|
||||
}),
|
||||
|
||||
http.put('/api/admin/oidc', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ ...body });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/version-check', () => {
|
||||
return HttpResponse.json({ update_available: false, latest: '1.0.0', current: '1.0.0' });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/bag-tracking', () => {
|
||||
return HttpResponse.json({ enabled: false });
|
||||
}),
|
||||
|
||||
http.put('/api/admin/bag-tracking', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ enabled: body.enabled });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/addons', () => {
|
||||
return HttpResponse.json({ addons: [] });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/packing-templates', () => {
|
||||
return HttpResponse.json({ templates: [] });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/audit-log', () => {
|
||||
return HttpResponse.json({ logs: [], total: 0 });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/mcp-tokens', () => {
|
||||
return HttpResponse.json({ tokens: [] });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/permissions', () => {
|
||||
return HttpResponse.json({ permissions: {} });
|
||||
}),
|
||||
|
||||
http.get('/api/admin/notification-preferences', () => {
|
||||
return HttpResponse.json({
|
||||
event_types: [],
|
||||
available_channels: {},
|
||||
implemented_combos: {},
|
||||
preferences: {},
|
||||
});
|
||||
}),
|
||||
|
||||
// Auth settings endpoints used by AdminPage
|
||||
http.get('/api/auth/app-settings', () => {
|
||||
return HttpResponse.json({});
|
||||
}),
|
||||
|
||||
http.put('/api/auth/app-settings', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ ...body });
|
||||
}),
|
||||
|
||||
http.get('/api/auth/me/settings', () => {
|
||||
return HttpResponse.json({ settings: { maps_api_key: '', openweather_api_key: '' } });
|
||||
}),
|
||||
|
||||
http.get('/api/auth/validate-keys', () => {
|
||||
return HttpResponse.json({ maps: true, weather: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildAssignment, buildPlace } from '../../factories';
|
||||
|
||||
export const assignmentsHandlers = [
|
||||
http.post('/api/trips/:id/days/:dayId/assignments', async ({ params, request }) => {
|
||||
const body = await request.json() as { place_id: number };
|
||||
const place = buildPlace({ id: body.place_id, trip_id: Number(params.id) });
|
||||
const assignment = buildAssignment({
|
||||
day_id: Number(params.dayId),
|
||||
place_id: body.place_id,
|
||||
place,
|
||||
order_index: 0,
|
||||
});
|
||||
return HttpResponse.json({ assignment });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/days/:dayId/assignments/:assignmentId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/days/:dayId/assignments/reorder', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/assignments/:assignmentId/move', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,31 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildUser, buildAppConfig } from '../../factories';
|
||||
|
||||
export const authHandlers = [
|
||||
http.post('/api/auth/login', () => {
|
||||
const user = buildUser();
|
||||
return HttpResponse.json({ user, token: 'mock-token' });
|
||||
}),
|
||||
|
||||
http.get('/api/auth/me', () => {
|
||||
const user = buildUser();
|
||||
return HttpResponse.json({ user });
|
||||
}),
|
||||
|
||||
http.post('/api/auth/register', () => {
|
||||
const user = buildUser();
|
||||
return HttpResponse.json({ user, token: 'mock-token' });
|
||||
}),
|
||||
|
||||
http.get('/api/auth/app-config', () => {
|
||||
return HttpResponse.json(buildAppConfig());
|
||||
}),
|
||||
|
||||
http.post('/api/auth/ws-token', () => {
|
||||
return HttpResponse.json({ token: 'mock-ws-token' });
|
||||
}),
|
||||
|
||||
http.post('/api/auth/logout', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildBudgetItem } from '../../factories';
|
||||
|
||||
export const budgetHandlers = [
|
||||
http.get('/api/trips/:id/budget', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
items: [buildBudgetItem({ trip_id: Number(params.id) })],
|
||||
});
|
||||
}),
|
||||
|
||||
http.post('/api/trips/:id/budget', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const item = buildBudgetItem({ trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ item });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/budget/:itemId', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const item = buildBudgetItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ item });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/budget/:itemId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/budget/:itemId/members', async ({ params, request }) => {
|
||||
const body = await request.json() as { user_ids: number[] };
|
||||
const members = body.user_ids.map(uid => ({ user_id: uid, paid: false }));
|
||||
const item = buildBudgetItem({ id: Number(params.itemId), trip_id: Number(params.id), persons: body.user_ids.length, members });
|
||||
return HttpResponse.json({ members, item });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/budget/:itemId/members/:userId/paid', async ({ params, request }) => {
|
||||
const body = await request.json() as { paid: boolean };
|
||||
return HttpResponse.json({ success: true, paid: body.paid });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,31 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildDayNote } from '../../factories';
|
||||
|
||||
export const dayNotesHandlers = [
|
||||
http.get('/api/trips/:id/days/:dayId/notes', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
notes: [buildDayNote({ day_id: Number(params.dayId) })],
|
||||
});
|
||||
}),
|
||||
|
||||
http.post('/api/trips/:id/days/:dayId/notes', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const note = buildDayNote({ day_id: Number(params.dayId), ...body });
|
||||
return HttpResponse.json({ note });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/days/:dayId/notes/:noteId', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const note = buildDayNote({ id: Number(params.noteId), day_id: Number(params.dayId), ...body });
|
||||
return HttpResponse.json({ note });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/days/:dayId/notes/:noteId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/days/:dayId', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
return HttpResponse.json({ day: { id: Number(params.dayId), trip_id: Number(params.id), ...body } });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,19 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildTripFile } from '../../factories';
|
||||
|
||||
export const filesHandlers = [
|
||||
http.get('/api/trips/:id/files', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
files: [buildTripFile({ trip_id: Number(params.id) })],
|
||||
});
|
||||
}),
|
||||
|
||||
http.post('/api/trips/:id/files', ({ params }) => {
|
||||
const file = buildTripFile({ trip_id: Number(params.id) });
|
||||
return HttpResponse.json({ file });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/files/:fileId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,37 @@
|
||||
import { authHandlers } from './auth';
|
||||
import { settingsHandlers } from './settings';
|
||||
import { addonHandlers } from './addons';
|
||||
import { notificationHandlers } from './notifications';
|
||||
import { vacayHandlers } from './vacay';
|
||||
import { tripsHandlers } from './trips';
|
||||
import { placesHandlers } from './places';
|
||||
import { assignmentsHandlers } from './assignments';
|
||||
import { packingHandlers } from './packing';
|
||||
import { todoHandlers } from './todo';
|
||||
import { budgetHandlers } from './budget';
|
||||
import { reservationsHandlers } from './reservations';
|
||||
import { filesHandlers } from './files';
|
||||
import { tagsHandlers } from './tags';
|
||||
import { dayNotesHandlers } from './dayNotes';
|
||||
import { adminHandlers } from './admin';
|
||||
import { sharedHandlers } from './shared';
|
||||
|
||||
export const defaultHandlers = [
|
||||
...authHandlers,
|
||||
...settingsHandlers,
|
||||
...addonHandlers,
|
||||
...notificationHandlers,
|
||||
...vacayHandlers,
|
||||
...tripsHandlers,
|
||||
...placesHandlers,
|
||||
...assignmentsHandlers,
|
||||
...packingHandlers,
|
||||
...todoHandlers,
|
||||
...budgetHandlers,
|
||||
...reservationsHandlers,
|
||||
...filesHandlers,
|
||||
...tagsHandlers,
|
||||
...dayNotesHandlers,
|
||||
...adminHandlers,
|
||||
...sharedHandlers,
|
||||
];
|
||||
@@ -0,0 +1,90 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const notificationHandlers = [
|
||||
http.get('/api/notifications/in-app', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
|
||||
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
||||
|
||||
const allNotifications = Array.from({ length: 25 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
type: 'simple',
|
||||
scope: 'trip',
|
||||
target: 1,
|
||||
sender_id: 2,
|
||||
sender_username: 'alice',
|
||||
sender_avatar: null,
|
||||
recipient_id: 1,
|
||||
title_key: 'notif.title',
|
||||
title_params: '{}',
|
||||
text_key: 'notif.text',
|
||||
text_params: '{}',
|
||||
positive_text_key: null,
|
||||
negative_text_key: null,
|
||||
response: null,
|
||||
navigate_text_key: null,
|
||||
navigate_target: null,
|
||||
is_read: i < 5 ? 0 : 1,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
}));
|
||||
|
||||
const page = allNotifications.slice(offset, offset + limit);
|
||||
|
||||
return HttpResponse.json({
|
||||
notifications: page,
|
||||
total: allNotifications.length,
|
||||
unread_count: 5,
|
||||
});
|
||||
}),
|
||||
|
||||
http.get('/api/notifications/in-app/unread-count', () => {
|
||||
return HttpResponse.json({ count: 5 });
|
||||
}),
|
||||
|
||||
http.put('/api/notifications/in-app/:id/read', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.put('/api/notifications/in-app/:id/unread', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.put('/api/notifications/in-app/read-all', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.delete('/api/notifications/in-app/:id', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.delete('/api/notifications/in-app/all', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/notifications/in-app/:id/respond', async ({ request, params }) => {
|
||||
const body = await request.json() as { response: string };
|
||||
return HttpResponse.json({
|
||||
notification: {
|
||||
id: Number(params.id),
|
||||
type: 'boolean',
|
||||
scope: 'trip',
|
||||
target: 1,
|
||||
sender_id: 2,
|
||||
sender_username: 'alice',
|
||||
sender_avatar: null,
|
||||
recipient_id: 1,
|
||||
title_key: 'notif.title',
|
||||
title_params: '{}',
|
||||
text_key: 'notif.text',
|
||||
text_params: '{}',
|
||||
positive_text_key: 'accept',
|
||||
negative_text_key: 'decline',
|
||||
response: body.response,
|
||||
navigate_text_key: null,
|
||||
navigate_target: null,
|
||||
is_read: 1,
|
||||
created_at: '2025-01-01T00:00:00.000Z',
|
||||
},
|
||||
});
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildPackingItem } from '../../factories';
|
||||
|
||||
export const packingHandlers = [
|
||||
http.get('/api/trips/:id/packing', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
items: [buildPackingItem({ trip_id: Number(params.id) })],
|
||||
});
|
||||
}),
|
||||
|
||||
http.post('/api/trips/:id/packing', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const item = buildPackingItem({ trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ item });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/packing/:itemId', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const item = buildPackingItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ item });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/packing/:itemId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,25 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildPlace } from '../../factories';
|
||||
|
||||
export const placesHandlers = [
|
||||
http.get('/api/trips/:id/places', ({ params }) => {
|
||||
const tripId = Number(params.id);
|
||||
return HttpResponse.json({ places: [buildPlace({ trip_id: tripId }), buildPlace({ trip_id: tripId })] });
|
||||
}),
|
||||
|
||||
http.post('/api/trips/:id/places', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const place = buildPlace({ trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ place });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/places/:placeId', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const place = buildPlace({ id: Number(params.placeId), trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ place });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/places/:placeId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,30 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildReservation } from '../../factories';
|
||||
|
||||
export const reservationsHandlers = [
|
||||
http.get('/api/trips/:id/reservations', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
reservations: [buildReservation({ trip_id: Number(params.id) })],
|
||||
});
|
||||
}),
|
||||
|
||||
http.post('/api/trips/:id/reservations', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const reservation = buildReservation({ trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ reservation });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/reservations/:reservationId', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const reservation = buildReservation({
|
||||
id: Number(params.reservationId),
|
||||
trip_id: Number(params.id),
|
||||
...body,
|
||||
});
|
||||
return HttpResponse.json({ reservation });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/reservations/:reservationId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,16 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildSettings } from '../../factories';
|
||||
|
||||
export const settingsHandlers = [
|
||||
http.get('/api/settings', () => {
|
||||
return HttpResponse.json({ settings: buildSettings() });
|
||||
}),
|
||||
|
||||
http.put('/api/settings', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/settings/bulk', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,36 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildTrip, buildDay, buildPlace } from '../../factories';
|
||||
|
||||
export const sharedHandlers = [
|
||||
http.get('/api/shared/:token', ({ params }) => {
|
||||
const { token } = params;
|
||||
|
||||
if (token === 'invalid-token' || token === 'expired-token') {
|
||||
return new HttpResponse(null, { status: 404 });
|
||||
}
|
||||
|
||||
const trip = { ...buildTrip({ start_date: '2026-07-01', end_date: '2026-07-05' }), title: 'Shared Paris Trip' };
|
||||
const day1 = buildDay({ trip_id: trip.id, date: '2026-07-01' });
|
||||
const place1 = buildPlace({ trip_id: trip.id, name: 'Eiffel Tower', lat: 48.8584, lng: 2.2945 });
|
||||
|
||||
return HttpResponse.json({
|
||||
trip,
|
||||
days: [day1],
|
||||
assignments: {},
|
||||
dayNotes: {},
|
||||
places: [place1],
|
||||
reservations: [],
|
||||
accommodations: [],
|
||||
packing: [],
|
||||
budget: [],
|
||||
categories: [],
|
||||
permissions: {
|
||||
share_bookings: true,
|
||||
share_packing: false,
|
||||
share_budget: false,
|
||||
share_collab: false,
|
||||
},
|
||||
collab: [],
|
||||
});
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,24 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildTag, buildCategory } from '../../factories';
|
||||
|
||||
export const tagsHandlers = [
|
||||
http.get('/api/tags', () => {
|
||||
return HttpResponse.json({ tags: [buildTag(), buildTag()] });
|
||||
}),
|
||||
|
||||
http.post('/api/tags', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const tag = buildTag(body);
|
||||
return HttpResponse.json({ tag });
|
||||
}),
|
||||
|
||||
http.get('/api/categories', () => {
|
||||
return HttpResponse.json({ categories: [buildCategory(), buildCategory()] });
|
||||
}),
|
||||
|
||||
http.post('/api/categories', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const category = buildCategory(body);
|
||||
return HttpResponse.json({ category });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,26 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildTodoItem } from '../../factories';
|
||||
|
||||
export const todoHandlers = [
|
||||
http.get('/api/trips/:id/todo', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
items: [buildTodoItem({ trip_id: Number(params.id) })],
|
||||
});
|
||||
}),
|
||||
|
||||
http.post('/api/trips/:id/todo', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const item = buildTodoItem({ trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ item });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id/todo/:itemId', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const item = buildTodoItem({ id: Number(params.itemId), trip_id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ item });
|
||||
}),
|
||||
|
||||
http.delete('/api/trips/:id/todo/:itemId', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,49 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
import { buildTrip, buildDay, buildUser } from '../../factories';
|
||||
|
||||
export const tripsHandlers = [
|
||||
// List all trips (active or archived)
|
||||
http.get('/api/trips', ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const archived = url.searchParams.get('archived');
|
||||
if (archived) {
|
||||
return HttpResponse.json({ trips: [] });
|
||||
}
|
||||
const trip1 = buildTrip({ title: 'Paris Adventure', start_date: '2026-07-01', end_date: '2026-07-10' });
|
||||
const trip2 = buildTrip({ title: 'Tokyo Trip', start_date: '2026-09-01', end_date: '2026-09-15' });
|
||||
return HttpResponse.json({ trips: [trip1, trip2] });
|
||||
}),
|
||||
|
||||
http.get('/api/trips/:id', ({ params }) => {
|
||||
const trip = buildTrip({ id: Number(params.id) });
|
||||
return HttpResponse.json({ trip });
|
||||
}),
|
||||
|
||||
http.get('/api/trips/:id/days', ({ params }) => {
|
||||
const tripId = Number(params.id);
|
||||
const day1 = buildDay({ trip_id: tripId, assignments: [], notes_items: [] });
|
||||
const day2 = buildDay({ trip_id: tripId, assignments: [], notes_items: [] });
|
||||
return HttpResponse.json({ days: [day1, day2] });
|
||||
}),
|
||||
|
||||
http.put('/api/trips/:id', async ({ params, request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const trip = buildTrip({ id: Number(params.id), ...body });
|
||||
return HttpResponse.json({ trip });
|
||||
}),
|
||||
|
||||
http.post('/api/trips', async ({ request }) => {
|
||||
const body = await request.json() as Record<string, unknown>;
|
||||
const trip = buildTrip({ ...body });
|
||||
return HttpResponse.json({ trip });
|
||||
}),
|
||||
|
||||
http.get('/api/trips/:id/members', ({ params }) => {
|
||||
const owner = buildUser();
|
||||
return HttpResponse.json({ owner, members: [] });
|
||||
}),
|
||||
|
||||
http.get('/api/trips/:id/accommodations', () => {
|
||||
return HttpResponse.json({ accommodations: [] });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,127 @@
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const vacayHandlers = [
|
||||
http.get('/api/addons/vacay/plan', () => {
|
||||
return HttpResponse.json({
|
||||
plan: {
|
||||
id: 1,
|
||||
holidays_enabled: false,
|
||||
holidays_region: null,
|
||||
holiday_calendars: [],
|
||||
block_weekends: true,
|
||||
carry_over_enabled: false,
|
||||
company_holidays_enabled: false,
|
||||
},
|
||||
users: [{ id: 1, username: 'user1', color: '#3b82f6' }],
|
||||
pendingInvites: [],
|
||||
incomingInvites: [],
|
||||
isOwner: true,
|
||||
isFused: false,
|
||||
});
|
||||
}),
|
||||
|
||||
http.put('/api/addons/vacay/plan', () => {
|
||||
return HttpResponse.json({
|
||||
plan: {
|
||||
id: 1,
|
||||
holidays_enabled: true,
|
||||
holidays_region: null,
|
||||
holiday_calendars: [],
|
||||
block_weekends: true,
|
||||
carry_over_enabled: false,
|
||||
company_holidays_enabled: false,
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
http.get('/api/addons/vacay/years', () => {
|
||||
return HttpResponse.json({ years: [2025, 2026] });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/years', () => {
|
||||
return HttpResponse.json({ years: [2025, 2026, 2027] });
|
||||
}),
|
||||
|
||||
http.delete('/api/addons/vacay/years/:year', () => {
|
||||
return HttpResponse.json({ years: [2025] });
|
||||
}),
|
||||
|
||||
http.get('/api/addons/vacay/entries/:year', () => {
|
||||
return HttpResponse.json({
|
||||
entries: [
|
||||
{ date: '2025-06-15', user_id: 1 },
|
||||
{ date: '2025-06-16', user_id: 1 },
|
||||
],
|
||||
companyHolidays: [],
|
||||
});
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/entries/toggle', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/entries/company-holiday', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.get('/api/addons/vacay/stats/:year', () => {
|
||||
return HttpResponse.json({
|
||||
stats: [{ user_id: 1, vacation_days: 30, used: 2 }],
|
||||
});
|
||||
}),
|
||||
|
||||
http.put('/api/addons/vacay/stats/:year', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.get('/api/addons/vacay/holidays/countries', () => {
|
||||
return HttpResponse.json({ countries: ['DE', 'US', 'FR'] });
|
||||
}),
|
||||
|
||||
http.get('/api/addons/vacay/holidays/:year/:country', () => {
|
||||
return HttpResponse.json([
|
||||
{ date: '2025-12-25', name: 'Christmas', localName: 'Weihnachten', global: true, counties: null },
|
||||
{ date: '2025-01-01', name: 'New Year', localName: 'Neujahr', global: true, counties: null },
|
||||
]);
|
||||
}),
|
||||
|
||||
http.put('/api/addons/vacay/color', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/invite', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/invite/accept', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/invite/decline', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/invite/cancel', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/dissolve', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
http.post('/api/addons/vacay/plan/holiday-calendars', () => {
|
||||
return HttpResponse.json({
|
||||
calendar: { id: 1, plan_id: 1, region: 'DE', label: null, color: '#ef4444', sort_order: 0 },
|
||||
});
|
||||
}),
|
||||
|
||||
http.put('/api/addons/vacay/plan/holiday-calendars/:id', () => {
|
||||
return HttpResponse.json({
|
||||
calendar: { id: 1, plan_id: 1, region: 'US', label: 'US Holidays', color: '#3b82f6', sort_order: 0 },
|
||||
});
|
||||
}),
|
||||
|
||||
http.delete('/api/addons/vacay/plan/holiday-calendars/:id', () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,4 @@
|
||||
import { setupServer } from 'msw/node';
|
||||
import { defaultHandlers } from './handlers';
|
||||
|
||||
export const server = setupServer(...defaultHandlers);
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { render, type RenderOptions } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { TranslationProvider } from '../../src/i18n/TranslationContext';
|
||||
|
||||
interface RenderWithProvidersOptions extends Omit<RenderOptions, 'wrapper'> {
|
||||
initialEntries?: string[];
|
||||
}
|
||||
|
||||
function renderWithProviders(
|
||||
ui: React.ReactElement,
|
||||
{ initialEntries = ['/'], ...options }: RenderWithProvidersOptions = {},
|
||||
) {
|
||||
function Wrapper({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<MemoryRouter initialEntries={initialEntries}>
|
||||
<TranslationProvider>{children}</TranslationProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
return render(ui, { wrapper: Wrapper, ...options });
|
||||
}
|
||||
|
||||
export * from '@testing-library/react';
|
||||
export { renderWithProviders as render };
|
||||
@@ -0,0 +1,33 @@
|
||||
import { useAuthStore } from '../../src/store/authStore';
|
||||
import { useTripStore } from '../../src/store/tripStore';
|
||||
import { useSettingsStore } from '../../src/store/settingsStore';
|
||||
import { useVacayStore } from '../../src/store/vacayStore';
|
||||
import { useAddonStore } from '../../src/store/addonStore';
|
||||
import { useInAppNotificationStore } from '../../src/store/inAppNotificationStore';
|
||||
import { usePermissionsStore } from '../../src/store/permissionsStore';
|
||||
|
||||
// Capture initial states at import time (before any test modifies them)
|
||||
const initialAuthState = useAuthStore.getState();
|
||||
const initialTripState = useTripStore.getState();
|
||||
const initialSettingsState = useSettingsStore.getState();
|
||||
const initialVacayState = useVacayStore.getState();
|
||||
const initialAddonState = useAddonStore.getState();
|
||||
const initialNotifState = useInAppNotificationStore.getState();
|
||||
const initialPermsState = usePermissionsStore.getState();
|
||||
|
||||
export function resetAllStores(): void {
|
||||
useAuthStore.setState(initialAuthState, true);
|
||||
useTripStore.setState(initialTripState, true);
|
||||
useSettingsStore.setState(initialSettingsState, true);
|
||||
useVacayStore.setState(initialVacayState, true);
|
||||
useAddonStore.setState(initialAddonState, true);
|
||||
useInAppNotificationStore.setState(initialNotifState, true);
|
||||
usePermissionsStore.setState(initialPermsState, true);
|
||||
}
|
||||
|
||||
export function seedStore<T extends object>(
|
||||
store: { setState: (partial: Partial<T>, replace?: boolean) => void },
|
||||
state: Partial<T>,
|
||||
): void {
|
||||
store.setState(state);
|
||||
}
|
||||
Reference in New Issue
Block a user