From 58ae04b400a831b6eb92e4241e22ea699680d0e4 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Wed, 1 Jul 2026 17:31:40 -0400 Subject: [PATCH] fix(BUG-15): DisplayView no longer re-sorts participants by initiative MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DisplayView called sortParticipantsByInitiative() on visibleParticipants, ignoring DM drag order. 1-list model = participants[] IS display source. After cross-init drag, player view diverged from AdminView/turnOrderIds. Repro: round 4 replay. [reorder Summon1(10)→before Merchant(11)] made turnOrderIds = [...,Summon2,Summon1,Merchant,OrcBoss]. AdminView correct. DisplayView re-sorted = Summon2,Merchant,Summon1 (init order) = visually Merchant appeared between Summon2 and Summon1, NOT at end. DM confused. Fix: removed sort. DisplayView now renders participants[] order directly (filter inactive monsters only), matching AdminView line 1222. Test: RED → GREEN (src/tests/DisplayView.drag-order.test.js). Seeds 3 monsters in drag order [High:20, Low:10, Mid:11]. Asserts DOM order = participants[] order, not init-sorted. No DisplayView regressions. --- TODO.md | 1 + src/App.js | 7 +-- src/tests/DisplayView.drag-order.test.js | 63 ++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/tests/DisplayView.drag-order.test.js diff --git a/TODO.md b/TODO.md index 36bc468..1055782 100644 --- a/TODO.md +++ b/TODO.md @@ -182,6 +182,7 @@ REWORK_PLAN.md. - [x] BUG-5: fixed (1-list model, 500 rounds clean) - [x] BUG-6: fixed structurally (1-list model) - [x] BUG-12: fixed — campaign selection follows activeDisplay +- [x] BUG-15: fixed — DisplayView no longer re-sorts (drag order preserved) - [ ] BUG-8: ws adapter reconnect - [ ] BUG-10: deact+reactivate double-act - [ ] BUG-11: FE Combat.scenario crash diff --git a/src/App.js b/src/App.js index 8305b07..d451fa0 100644 --- a/src/App.js +++ b/src/App.js @@ -2500,9 +2500,10 @@ function DisplayView() { let participantsToRender = []; if (participants) { - // Hide inactive monsters (pre-staged/summoned reserves) from the player view - const visibleParticipants = participants.filter(p => p.isActive || p.type !== 'monster'); - participantsToRender = sortParticipantsByInitiative(visibleParticipants, visibleParticipants); + // 1-list model: participants[] IS the display order (DM drag = source of + // truth). Do NOT re-sort by initiative — that diverges from AdminView / + // turnOrderIds after any cross-init drag (BUG-15). + participantsToRender = participants.filter(p => p.isActive || p.type !== 'monster'); } const displayStyles = campaignBackgroundUrl diff --git a/src/tests/DisplayView.drag-order.test.js b/src/tests/DisplayView.drag-order.test.js new file mode 100644 index 0000000..9f76e51 --- /dev/null +++ b/src/tests/DisplayView.drag-order.test.js @@ -0,0 +1,63 @@ +// RED test: DisplayView must render participants in turnOrderIds (drag) order, +// NOT re-sort by initiative. 1-list model: participants[] = display source. +// Bug: DisplayView line ~2505 calls sortParticipantsByInitiative(), ignoring +// DM drag order. After cross-init drag, display diverges from AdminView/turnOrderIds. +import React from 'react'; +import { render, waitFor, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import App from '../App'; +import { MOCK_DB } from '../__mocks__/firebase/_mock-db'; +import { resetAdapterCalls } from '../storage/firebase'; + +function seedDragOrder() { + const campaignPath = 'artifacts/ttrpg-initiative-tracker-default/public/data/campaigns/c1'; + const encounterPath = 'artifacts/ttrpg-initiative-tracker-default/public/data/campaigns/c1/encounters/e1'; + const activeDisplayPath = 'artifacts/ttrpg-initiative-tracker-default/public/data/activeDisplay/status'; + // Three monsters, init-sorted would be: high(20), mid(11), low(10). + // But participants[] = DRAG order: low BEFORE mid (DM dragged across init). + const participants = [ + { id: 'high', name: 'High', type: 'monster', initiative: 20, currentHp: 10, maxHp: 10, isActive: true }, + { id: 'low', name: 'Low', type: 'monster', initiative: 10, currentHp: 10, maxHp: 10, isActive: true }, + { id: 'mid', name: 'Mid', type: 'monster', initiative: 11, currentHp: 10, maxHp: 10, isActive: true }, + ]; + MOCK_DB.set(campaignPath, { name: 'Camp', playerDisplayBackgroundUrl: '' }); + MOCK_DB.set(encounterPath, { + name: 'Enc', + participants, + turnOrderIds: participants.map(p => p.id), + round: 1, + currentTurnParticipantId: 'high', + isStarted: true, + }); + MOCK_DB.set(activeDisplayPath, { activeCampaignId: 'c1', activeEncounterId: 'e1', hidePlayerHp: false }); +} + +describe('DisplayView drag order (BUG-15)', () => { + beforeEach(() => { + window.history.replaceState({}, '', '/display'); + global.alert = jest.fn(); + window.open = jest.fn(); + // jsdom lacks scrollIntoView (DisplayView auto-scrolls current actor) + Element.prototype.scrollIntoView = jest.fn(); + resetAdapterCalls(); + }); + afterEach(() => { + window.history.replaceState({}, '', '/'); + }); + + test('renders participants in participants[] order, not init-sorted', async () => { + seedDragOrder(); + render(); + + // wait for participant names to render + await waitFor(() => { + expect(screen.getAllByText(/High|Mid|Low/i).length).toBeGreaterThanOrEqual(3); + }, { timeout: 3000 }); + + // collect name elements in DOM order (strip Current marker) + const names = screen.getAllByText(/High|Mid|Low/i).map(el => el.textContent.replace(/\(Current\)/i, '').trim()); + // participants[] order = High, Low, Mid (drag moved Low before Mid). + // Display must mirror this. Init-sorted would be High, Mid, Low. + expect(names).toEqual(['High', 'Low', 'Mid']); + }); +});