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']); + }); +});