From f81308a0dfa155e011c0318d9a9f19beb902f4cb Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Mon, 29 Jun 2026 16:02:22 -0400 Subject: [PATCH] tests: consolidate into tests/ dirs, fix import paths Move all test files out of source dirs into per-workspace tests/: - shared/tests/ (3 unit test files) - server/tests/ (1 integration test) - src/tests/ (8 characterization + scenario tests + testHelpers) Fix all relative import paths (App, storage, __mocks__, testHelpers). Fix jest.config testMatch globs in shared/ and server/ (rootDir + /tests pattern). Delete scripts/repro-pause-bug.js (debug scratch, superseded by turn.pause-add.test.js). Keep scripts/replay-combat.js + scripts/audit-rotation.js as manual demo/exploratory tools (NOT unit tests, not deterministic). No logic changes. All green: shared 49 + 1 validated RED, server 23, FE 62. Scenario test unchanged (240s timeout, pre-existing slow). --- scripts/repro-pause-bug.js | 76 ------------------- server/jest.config.js | 3 +- server/{ => tests}/ws-contract.test.js | 6 +- shared/jest.config.js | 3 +- .../{ => tests}/turn.characterization.test.js | 0 shared/{ => tests}/turn.pause-add.test.js | 0 .../{ => tests}/turn.round-rotation.test.js | 0 src/{ => tests}/App.characterization.test.js | 2 +- .../Combat.characterization.test.js | 2 +- src/{ => tests}/Combat.scenario.test.js | 4 +- .../DisplayView.characterization.test.js | 6 +- .../Encounter.characterization.test.js | 2 +- src/{ => tests}/Logs.characterization.test.js | 4 +- .../Participant.characterization.test.js | 2 +- src/{storage => tests}/storage.test.js | 4 +- src/{ => tests}/testHelpers.js | 12 +-- 16 files changed, 26 insertions(+), 100 deletions(-) delete mode 100644 scripts/repro-pause-bug.js rename server/{ => tests}/ws-contract.test.js (88%) rename shared/{ => tests}/turn.characterization.test.js (100%) rename shared/{ => tests}/turn.pause-add.test.js (100%) rename shared/{ => tests}/turn.round-rotation.test.js (100%) rename src/{ => tests}/App.characterization.test.js (98%) rename src/{ => tests}/Combat.characterization.test.js (98%) rename src/{ => tests}/Combat.scenario.test.js (99%) rename src/{ => tests}/DisplayView.characterization.test.js (93%) rename src/{ => tests}/Encounter.characterization.test.js (98%) rename src/{ => tests}/Logs.characterization.test.js (98%) rename src/{ => tests}/Participant.characterization.test.js (98%) rename src/{storage => tests}/storage.test.js (62%) rename src/{ => tests}/testHelpers.js (92%) diff --git a/scripts/repro-pause-bug.js b/scripts/repro-pause-bug.js deleted file mode 100644 index 6f50f6c..0000000 --- a/scripts/repro-pause-bug.js +++ /dev/null @@ -1,76 +0,0 @@ -// scripts/repro-pause-bug.js -// Minimal repro: pause+resume causes nextTurn to repeat same participant forever. -'use strict'; -const shared = require('../shared'); -const { makeParticipant, startEncounter, nextTurn, togglePause, addParticipant } = shared; - -function p(id, init) { - return makeParticipant({ id, name: id, type: 'monster', initiative: init, maxHp: 100, currentHp: 100 }); -} - -let e = { - name: 't', participants: [p('a', 20), p('b', 15), p('c', 10)], - isStarted: false, isPaused: false, round: 0, currentTurnParticipantId: null, turnOrderIds: [], -}; -e = { ...e, ...startEncounter(e).patch }; -console.log('start:', { current: e.currentTurnParticipantId, order: e.turnOrderIds, round: e.round }); - -// advance 1 turn -e = { ...e, ...nextTurn(e).patch }; -console.log('turn1:', { current: e.currentTurnParticipantId, round: e.round }); - -// pause then resume immediately -e = { ...e, ...togglePause(e).patch }; -console.log('paused:', { isPaused: e.isPaused, current: e.currentTurnParticipantId, order: e.turnOrderIds }); -e = { ...e, ...togglePause(e).patch }; -console.log('resumed:', { isPaused: e.isPaused, current: e.currentTurnParticipantId, order: e.turnOrderIds }); - -// advance 5 turns — should visit b, c, a, b, c -const visited = [e.currentTurnParticipantId]; -for (let i = 0; i < 5; i++) { - e = { ...e, ...nextTurn(e).patch }; - visited.push(e.currentTurnParticipantId); -} -console.log('5 turns after resume:', visited); - -// now repro with addParticipant while paused -let e2 = { - name: 't', participants: [p('a', 20), p('b', 15), p('c', 10)], - isStarted: false, isPaused: false, round: 0, currentTurnParticipantId: null, turnOrderIds: [], -}; -e2 = { ...e2, ...startEncounter(e2).patch }; -e2 = { ...e2, ...nextTurn(e2).patch }; // current=b -const newP = makeParticipant({ id: 'x', name: 'x', type: 'monster', initiative: 25, maxHp: 100, currentHp: 100 }); -e2 = { ...e2, ...addParticipant(e2, newP).patch }; -console.log('\nadded x while running:', { current: e2.currentTurnParticipantId, order: e2.turnOrderIds }); -e2 = { ...e2, ...togglePause(e2).patch }; -e2 = { ...e2, ...togglePause(e2).patch }; -console.log('after pause/resume:', { current: e2.currentTurnParticipantId, order: e2.turnOrderIds }); -const v2 = [e2.currentTurnParticipantId]; -for (let i = 0; i < 5; i++) { - e2 = { ...e2, ...nextTurn(e2).patch }; - v2.push(e2.currentTurnParticipantId); -} -console.log('5 turns after add+pause/resume:', v2); - -// repro 3: addParticipant WHILE paused, then resume -let e3 = { - name: 't', participants: [p('a', 20), p('b', 15), p('c', 10)], - isStarted: false, isPaused: false, round: 0, currentTurnParticipantId: null, turnOrderIds: [], -}; -e3 = { ...e3, ...startEncounter(e3).patch }; -e3 = { ...e3, ...nextTurn(e3).patch }; // current=b -console.log('\n--- add while PAUSED ---'); -e3 = { ...e3, ...togglePause(e3).patch }; // pause -console.log('paused:', { current: e3.currentTurnParticipantId, order: e3.turnOrderIds }); -const np = makeParticipant({ id: 'x', name: 'x', type: 'monster', initiative: 25, maxHp: 100, currentHp: 100 }); -e3 = { ...e3, ...addParticipant(e3, np).patch }; -console.log('add-while-paused:', { current: e3.currentTurnParticipantId, order: e3.turnOrderIds }); -e3 = { ...e3, ...togglePause(e3).patch }; // resume (rebuilds order) -console.log('resumed:', { current: e3.currentTurnParticipantId, order: e3.turnOrderIds }); -const v3 = [e3.currentTurnParticipantId]; -for (let i = 0; i < 5; i++) { - e3 = { ...e3, ...nextTurn(e3).patch }; - v3.push(e3.currentTurnParticipantId); -} -console.log('5 turns after add-while-paused+resume:', v3); diff --git a/server/jest.config.js b/server/jest.config.js index 61f4584..ab5859e 100644 --- a/server/jest.config.js +++ b/server/jest.config.js @@ -1,5 +1,6 @@ module.exports = { + rootDir: '.', testEnvironment: 'node', - testMatch: ['**/*.test.js'], + testMatch: ['/tests/**/*.test.js'], testTimeout: 10000, }; diff --git a/server/ws-contract.test.js b/server/tests/ws-contract.test.js similarity index 88% rename from server/ws-contract.test.js rename to server/tests/ws-contract.test.js index 6ad9c13..373a72d 100644 --- a/server/ws-contract.test.js +++ b/server/tests/ws-contract.test.js @@ -10,9 +10,9 @@ const path = require('path'); const os = require('os'); -const { createServer } = require('../server/index'); -const { createWsStorage } = require('../src/storage/ws'); -const { runStorageContract } = require('../src/storage/contract'); +const { createServer } = require('../index'); +const { createWsStorage } = require('../../src/storage/ws'); +const { runStorageContract } = require('../../src/storage/contract'); let nextPort = 4000 + Math.floor(Math.random() * 999); diff --git a/shared/jest.config.js b/shared/jest.config.js index fb453b6..610cf77 100644 --- a/shared/jest.config.js +++ b/shared/jest.config.js @@ -1,5 +1,6 @@ module.exports = { + rootDir: '.', testEnvironment: 'node', - testMatch: ['**/*.test.js'], + testMatch: ['/tests/**/*.test.js'], collectCoverageFrom: ['turn.js'], }; diff --git a/shared/turn.characterization.test.js b/shared/tests/turn.characterization.test.js similarity index 100% rename from shared/turn.characterization.test.js rename to shared/tests/turn.characterization.test.js diff --git a/shared/turn.pause-add.test.js b/shared/tests/turn.pause-add.test.js similarity index 100% rename from shared/turn.pause-add.test.js rename to shared/tests/turn.pause-add.test.js diff --git a/shared/turn.round-rotation.test.js b/shared/tests/turn.round-rotation.test.js similarity index 100% rename from shared/turn.round-rotation.test.js rename to shared/tests/turn.round-rotation.test.js diff --git a/src/App.characterization.test.js b/src/tests/App.characterization.test.js similarity index 98% rename from src/App.characterization.test.js rename to src/tests/App.characterization.test.js index edee5a2..5c8e1a1 100644 --- a/src/App.characterization.test.js +++ b/src/tests/App.characterization.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { getCalls, MOCK_DB } from './__mocks__/firebase/_mock-db'; +import { getCalls, MOCK_DB } from '../__mocks__/firebase/_mock-db'; import { renderApp, createCampaignViaUI, selectCampaignByName } from './testHelpers'; function findCall(fn, pathSub) { diff --git a/src/Combat.characterization.test.js b/src/tests/Combat.characterization.test.js similarity index 98% rename from src/Combat.characterization.test.js rename to src/tests/Combat.characterization.test.js index 20ba7de..9a4a852 100644 --- a/src/Combat.characterization.test.js +++ b/src/tests/Combat.characterization.test.js @@ -3,7 +3,7 @@ import React from 'react'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { getCalls } from './__mocks__/firebase/_mock-db'; +import { getCalls } from '../__mocks__/firebase/_mock-db'; import { setupReady, addMonsterViaUI, startCombatViaUI } from './testHelpers'; function findCallsEnc() { diff --git a/src/Combat.scenario.test.js b/src/tests/Combat.scenario.test.js similarity index 99% rename from src/Combat.scenario.test.js rename to src/tests/Combat.scenario.test.js index 31ff618..1867cea 100644 --- a/src/Combat.scenario.test.js +++ b/src/tests/Combat.scenario.test.js @@ -10,12 +10,12 @@ import React from 'react'; import { screen, fireEvent, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; -import App from './App'; +import App from '../App'; import { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName, addMonsterViaUI, setupReady, } from './testHelpers'; -import { getCalls, MOCK_DB } from './__mocks__/firebase/_mock-db'; +import { getCalls, MOCK_DB } from '../__mocks__/firebase/_mock-db'; // ---------- scenario helpers (UI only, same buttons as human) ---------- diff --git a/src/DisplayView.characterization.test.js b/src/tests/DisplayView.characterization.test.js similarity index 93% rename from src/DisplayView.characterization.test.js rename to src/tests/DisplayView.characterization.test.js index 633edec..e60077e 100644 --- a/src/DisplayView.characterization.test.js +++ b/src/tests/DisplayView.characterization.test.js @@ -6,9 +6,9 @@ import React from 'react'; import { render, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import App from './App'; -import { MOCK_DB } from './__mocks__/firebase/_mock-db'; -import { getAdapterCalls, resetAdapterCalls } from './storage/firebase'; +import App from '../App'; +import { MOCK_DB } from '../__mocks__/firebase/_mock-db'; +import { getAdapterCalls, resetAdapterCalls } from '../storage/firebase'; // Seed activeDisplay + campaign + encounter so DisplayView has data to subscribe to. function seedActiveDisplay() { diff --git a/src/Encounter.characterization.test.js b/src/tests/Encounter.characterization.test.js similarity index 98% rename from src/Encounter.characterization.test.js rename to src/tests/Encounter.characterization.test.js index 7fca5a3..4a9e959 100644 --- a/src/Encounter.characterization.test.js +++ b/src/tests/Encounter.characterization.test.js @@ -3,7 +3,7 @@ import React from 'react'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { getCalls } from './__mocks__/firebase/_mock-db'; +import { getCalls } from '../__mocks__/firebase/_mock-db'; import { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName } from './testHelpers'; function findCall(fn, pathSub) { diff --git a/src/Logs.characterization.test.js b/src/tests/Logs.characterization.test.js similarity index 98% rename from src/Logs.characterization.test.js rename to src/tests/Logs.characterization.test.js index b9ab4e6..6511ead 100644 --- a/src/Logs.characterization.test.js +++ b/src/tests/Logs.characterization.test.js @@ -3,7 +3,7 @@ import React from 'react'; import { screen, fireEvent, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { getCalls } from './__mocks__/firebase/_mock-db'; +import { getCalls } from '../__mocks__/firebase/_mock-db'; import { setupReady, addMonsterViaUI, startCombatViaUI } from './testHelpers'; function findLogCalls() { @@ -16,7 +16,7 @@ function lastEncCall() { // Navigate to /logs view. App reads pathname at mount; must re-render with path preset. import { render } from '@testing-library/react'; -import App from './App'; +import App from '../App'; async function goToLogs() { // unmount current tree isn't needed; App checks pathname in useEffect. // Re-render a fresh App instance in same container. diff --git a/src/Participant.characterization.test.js b/src/tests/Participant.characterization.test.js similarity index 98% rename from src/Participant.characterization.test.js rename to src/tests/Participant.characterization.test.js index 03f4821..9a53408 100644 --- a/src/Participant.characterization.test.js +++ b/src/tests/Participant.characterization.test.js @@ -3,7 +3,7 @@ import React from 'react'; import { screen, fireEvent, waitFor, within } from '@testing-library/react'; import '@testing-library/jest-dom'; -import { getCalls } from './__mocks__/firebase/_mock-db'; +import { getCalls } from '../__mocks__/firebase/_mock-db'; import { setupReady, addMonsterViaUI, getParticipantForm, startCombatViaUI } from './testHelpers'; function findCallsEnc() { diff --git a/src/storage/storage.test.js b/src/tests/storage.test.js similarity index 62% rename from src/storage/storage.test.js rename to src/tests/storage.test.js index fa4be45..e4c9d44 100644 --- a/src/storage/storage.test.js +++ b/src/tests/storage.test.js @@ -2,7 +2,7 @@ // TDD: contract = spec. Run against memory first. RED until memory.js built. 'use strict'; -const { runStorageContract } = require('./contract'); -const { createMemoryStorage } = require('./memory'); +const { runStorageContract } = require('../storage/contract'); +const { createMemoryStorage } = require('../storage/memory'); runStorageContract('memory', () => createMemoryStorage()); diff --git a/src/testHelpers.js b/src/tests/testHelpers.js similarity index 92% rename from src/testHelpers.js rename to src/tests/testHelpers.js index 0101ac0..4373afa 100644 --- a/src/testHelpers.js +++ b/src/tests/testHelpers.js @@ -1,8 +1,8 @@ // test helpers: drive App UI to states. Used across characterization suites. import React from 'react'; import { render, screen, fireEvent, waitFor, within } from '@testing-library/react'; -import App from './App'; -import { MOCK_DB } from './__mocks__/firebase/_mock-db'; +import App from '../App'; +import { MOCK_DB } from '../__mocks__/firebase/_mock-db'; // Scoped container: the "Add Participants" section (avoids label clashes with CharacterManager). export function getParticipantForm() { @@ -34,7 +34,7 @@ export async function createCampaignViaUI(name = 'Test Campaign') { fireEvent.change(screen.getByLabelText(/Campaign Name/i), { target: { value: name } }); fireEvent.click(screen.getByRole('button', { name: /^Create$/i })); // wait for setDoc recorded - const { getCalls } = require('./__mocks__/firebase/_mock-db'); + const { getCalls } = require('../__mocks__/firebase/_mock-db'); await waitFor(() => getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/campaigns/'))); const call = getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/campaigns/')); return call.path.split('/').pop(); // campaign id @@ -53,7 +53,7 @@ export async function createEncounterViaUI(name = 'Test Encounter') { await waitFor(() => screen.getByLabelText(/Encounter Name/i)); fireEvent.change(screen.getByLabelText(/Encounter Name/i), { target: { value: name } }); fireEvent.click(screen.getByRole('button', { name: /^Create$/i })); - const { getCalls } = require('./__mocks__/firebase/_mock-db'); + const { getCalls } = require('../__mocks__/firebase/_mock-db'); await waitFor(() => getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/encounters/'))); const call = getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/encounters/')); return call.path.split('/').pop(); @@ -73,7 +73,7 @@ export async function addMonsterViaUI(name = 'Goblin', maxHp = 7, initMod = 2) { fireEvent.change(form.getByLabelText(/Init Mod/i), { target: { value: String(initMod) } }); fireEvent.change(form.getByLabelText(/Max HP/i), { target: { value: String(maxHp) } }); fireEvent.click(form.getByRole('button', { name: /Add to Encounter/i })); - const { getCalls } = require('./__mocks__/firebase/_mock-db'); + const { getCalls } = require('../__mocks__/firebase/_mock-db'); await waitFor(() => { const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')); const last = calls[calls.length - 1]; @@ -93,7 +93,7 @@ export async function setupReady(campName = 'Camp', encName = 'Enc') { // Start combat. Assumes encounter selected with active participants. export async function startCombatViaUI() { fireEvent.click(screen.getByRole('button', { name: /Start Combat/i })); - const { getCalls } = require('./__mocks__/firebase/_mock-db'); + const { getCalls } = require('../__mocks__/firebase/_mock-db'); await waitFor(() => { const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')); const last = calls[calls.length - 1];