mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
* feat(days): reorder whole days and insert a day at a position Adds reorderDays + insertDay to the day service and a PUT /days/reorder route (plus an optional position on create). Day rows stay stable so a day's assignments, notes, bookings and accommodations ride along by id; on a dated trip the calendar dates stay pinned to their slots while the content moves across them, and each booking's date is re-stamped onto its day's new date (time-of-day preserved) so day_id stays consistent. Renumbering uses the two-phase write to avoid the UNIQUE(trip_id, day_number) collision, and a move that would invert an accommodation's check-in/out span is rejected. * feat(planner): reorder days from a toolbar popup, and add days A new toolbar button opens a popup listing the days; drag a row by its grip or use the up/down arrows to reorder, and add a day from there. Reorders apply optimistically with rollback and sync over WebSocket; the day headers are left untouched, so the existing place drop-targets are unaffected. * i18n: add day-reorder strings across all languages
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Day reorder + insert integration tests (#589) — exercises the real
|
||||
* dayService against the real schema. Covers: position renumber, dates pinned
|
||||
* to slots while content rides along by id, booking-date re-stamp, permutation
|
||||
* validation, the accommodation-inversion guard, and insert (dated + dateless).
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
|
||||
const { testDb, dbMock } = vi.hoisted(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database(':memory:');
|
||||
db.exec('PRAGMA journal_mode = WAL');
|
||||
db.exec('PRAGMA foreign_keys = ON');
|
||||
db.exec('PRAGMA busy_timeout = 5000');
|
||||
return { testDb: db, dbMock: { db, closeDb: () => {}, reinitialize: () => {}, canAccessTrip: vi.fn() } };
|
||||
});
|
||||
|
||||
vi.mock('../../src/db/database', () => dbMock);
|
||||
vi.mock('../../src/config', () => ({
|
||||
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
||||
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
||||
updateJwtSecret: () => {},
|
||||
DEFAULT_LANGUAGE: 'en',
|
||||
}));
|
||||
|
||||
import { createTables } from '../../src/db/schema';
|
||||
import { runMigrations } from '../../src/db/migrations';
|
||||
import { resetTestDb } from '../helpers/test-db';
|
||||
import { createUser, createTrip, createPlace, createDay, createDayAssignment, createReservation, createDayAccommodation } from '../helpers/factories';
|
||||
import { reorderDays, insertDay, DayReorderError } from '../../src/services/dayService';
|
||||
|
||||
let userId: number;
|
||||
|
||||
beforeAll(() => {
|
||||
createTables(testDb);
|
||||
runMigrations(testDb);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
resetTestDb(testDb);
|
||||
userId = createUser(testDb).user.id;
|
||||
});
|
||||
|
||||
afterAll(() => testDb.close());
|
||||
|
||||
const orderedDays = (tripId: number) =>
|
||||
testDb.prepare('SELECT id, day_number, date FROM days WHERE trip_id = ? ORDER BY day_number').all(tripId) as
|
||||
{ id: number; day_number: number; date: string | null }[];
|
||||
|
||||
describe('reorderDays', () => {
|
||||
it('permutes positions, pins dates to slots, and content rides along by id', () => {
|
||||
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
|
||||
const [d1, d2, d3] = orderedDays(trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
createDayAssignment(testDb, d2.id, place.id); // place sits on day 2
|
||||
|
||||
// Move day 2 to the front: [d2, d1, d3]
|
||||
reorderDays(trip.id, [d2.id, d1.id, d3.id]);
|
||||
|
||||
const after = orderedDays(trip.id);
|
||||
expect(after.map(d => d.id)).toEqual([d2.id, d1.id, d3.id]);
|
||||
// Dates stay pinned to their calendar slots
|
||||
expect(after.map(d => d.date)).toEqual(['2026-03-01', '2026-03-02', '2026-03-03']);
|
||||
// The place rides along with its day row (still attached to d2.id, now at slot 1)
|
||||
const onD2 = testDb.prepare('SELECT * FROM day_assignments WHERE day_id = ?').all(d2.id);
|
||||
expect(onD2).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('re-stamps a booking\'s date onto its day\'s new date, keeping the time', () => {
|
||||
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
|
||||
const [d1, d2, d3] = orderedDays(trip.id);
|
||||
const res = createReservation(testDb, trip.id, { day_id: d2.id, type: 'restaurant' });
|
||||
testDb.prepare('UPDATE reservations SET reservation_time = ? WHERE id = ?').run('2026-03-02T19:00', res.id);
|
||||
|
||||
reorderDays(trip.id, [d2.id, d1.id, d3.id]); // d2 moves to the 2026-03-01 slot
|
||||
|
||||
const r = testDb.prepare('SELECT reservation_time FROM reservations WHERE id = ?').get(res.id) as { reservation_time: string };
|
||||
expect(r.reservation_time).toBe('2026-03-01T19:00');
|
||||
});
|
||||
|
||||
it('rejects an orderedIds list that is not a permutation of the trip days', () => {
|
||||
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
|
||||
const [d1, d2] = orderedDays(trip.id);
|
||||
expect(() => reorderDays(trip.id, [d1.id, d2.id])).toThrow(DayReorderError);
|
||||
});
|
||||
|
||||
it('blocks a move that would make an accommodation end before it starts, and rolls back', () => {
|
||||
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
|
||||
const [d1, d2, d3] = orderedDays(trip.id);
|
||||
const place = createPlace(testDb, trip.id);
|
||||
createDayAccommodation(testDb, trip.id, place.id, d1.id, d2.id); // stay spans day 1 -> day 2
|
||||
|
||||
// Put the start day (d1) after the end day (d2): [d2, d3, d1]
|
||||
expect(() => reorderDays(trip.id, [d2.id, d3.id, d1.id])).toThrow(DayReorderError);
|
||||
|
||||
// Transaction rolled back: original order intact
|
||||
expect(orderedDays(trip.id).map(d => d.id)).toEqual([d1.id, d2.id, d3.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insertDay', () => {
|
||||
it('inserts an empty day at a position on a dateless trip and shifts the rest', () => {
|
||||
const trip = createTrip(testDb, userId);
|
||||
const d1 = createDay(testDb, trip.id);
|
||||
const d2 = createDay(testDb, trip.id);
|
||||
const d3 = createDay(testDb, trip.id);
|
||||
|
||||
const created = insertDay(trip.id, 1);
|
||||
|
||||
const after = orderedDays(trip.id);
|
||||
expect(after).toHaveLength(4);
|
||||
expect(after[0].id).toBe(created.id);
|
||||
expect(after[0].date).toBeNull();
|
||||
expect(after.slice(1).map(d => d.id)).toEqual([d1.id, d2.id, d3.id]);
|
||||
});
|
||||
|
||||
it('inserts at the front of a dated trip: dates stay contiguous and the trip extends', () => {
|
||||
const trip = createTrip(testDb, userId, { start_date: '2026-03-01', end_date: '2026-03-03' });
|
||||
const [d1, d2, d3] = orderedDays(trip.id);
|
||||
|
||||
const created = insertDay(trip.id, 1);
|
||||
|
||||
const after = orderedDays(trip.id);
|
||||
expect(after).toHaveLength(4);
|
||||
expect(after[0].id).toBe(created.id);
|
||||
expect(after.map(d => d.date)).toEqual(['2026-03-01', '2026-03-02', '2026-03-03', '2026-03-04']);
|
||||
// Old content shifted down a slot
|
||||
expect(after.slice(1).map(d => d.id)).toEqual([d1.id, d2.id, d3.id]);
|
||||
// Trip range extended by one day
|
||||
const t = testDb.prepare('SELECT end_date FROM trips WHERE id = ?').get(trip.id) as { end_date: string };
|
||||
expect(t.end_date).toBe('2026-03-04');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user