dorfteich/apps/web/src/layout/reorder.test.ts
Claude Opus 4.8 69b00fcf2f
All checks were successful
CD / Build and push images (push) Successful in 3m1s
CI / Lint, typecheck, test (push) Successful in 2m13s
CI / Auth e2e pack (push) Successful in 2m36s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m13s
CD / Promote to Int (push) Successful in 11s
Add manual page ordering with drag-and-drop (#45)
Enable the third sidebar sort mode — a freely defined order.

- api: `PATCH /pages/:id/position` (before/after neighbour) recomputes only
  the moved page's fractional `sort_key`. Pure `sort-key.ts` helpers
  (`nextKeyOrRebalance`, `evenlySpacedKeys`) decide between the cheap
  single-key path and a full pond rebalance to evenly-spaced keys when a key
  would exceed MAX_SORT_KEY_LENGTH or the client's neighbours are stale;
  rebalance runs in one transaction. Order is server-authoritative.
- web: enable 'manual' in the sort-mode switch; in manual mode the owner can
  reorder via native drag-and-drop (drop above/below by pointer half) or the
  keyboard (per-row up/down buttons), each announced through an aria-live
  region. Reordering is hidden while a label filter narrows the list. New
  pages already append at the end (create uses generateKeyBetween(last, null)).
  Pure `reorder.ts` neighbour helpers, unit-tested.
- i18n: manual sort mode + reorder strings (de + en).
- tests: sort-key property test (10.000 adversarial reorders never collide or
  overflow — rebalance verified); reposition db test (persist, server-order,
  sort-mode switch keeps manual order); reorder e2e pack (keyboard reorder
  persists across reload + identical on a fresh read; aria-live announced).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
2026-07-09 12:18:44 +02:00

38 lines
1.3 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { dropIndex, neighborsForMove } from './reorder';
const ids = ['A', 'B', 'C', 'D'];
describe('neighborsForMove (issue #45)', () => {
it('moves up one: D between B and C', () => {
// D at index 3 → newIndex 2 in the without-array [A, B, C].
expect(neighborsForMove(ids, 'D', 2)).toEqual({ afterId: 'B', beforeId: 'C' });
});
it('moves to the top: afterId null', () => {
expect(neighborsForMove(ids, 'C', 0)).toEqual({ afterId: null, beforeId: 'A' });
});
it('moves to the bottom: beforeId null', () => {
expect(neighborsForMove(ids, 'A', 3)).toEqual({ afterId: 'D', beforeId: null });
});
it('clamps an out-of-range index', () => {
expect(neighborsForMove(ids, 'A', 99)).toEqual({ afterId: 'D', beforeId: null });
expect(neighborsForMove(ids, 'A', -5)).toEqual({ afterId: null, beforeId: 'B' });
});
});
describe('dropIndex (issue #45)', () => {
it('drops before the target by default', () => {
// Drop A onto C (upper half) → before C. without = [B, C, D], C at 1.
expect(dropIndex(ids, 'A', 'C', false)).toBe(1);
});
it('drops after the target on the lower half — reaches the bottom', () => {
// Drop A onto D lower half → after D. without = [B, C, D], D at 2 → 3 (end).
expect(dropIndex(ids, 'A', 'D', true)).toBe(3);
});
});