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 + <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).
This commit is contained in:
@@ -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);
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
rootDir: '.',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testMatch: ['**/*.test.js'],
|
testMatch: ['<rootDir>/tests/**/*.test.js'],
|
||||||
testTimeout: 10000,
|
testTimeout: 10000,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,9 +10,9 @@
|
|||||||
|
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const os = require('os');
|
const os = require('os');
|
||||||
const { createServer } = require('../server/index');
|
const { createServer } = require('../index');
|
||||||
const { createWsStorage } = require('../src/storage/ws');
|
const { createWsStorage } = require('../../src/storage/ws');
|
||||||
const { runStorageContract } = require('../src/storage/contract');
|
const { runStorageContract } = require('../../src/storage/contract');
|
||||||
|
|
||||||
let nextPort = 4000 + Math.floor(Math.random() * 999);
|
let nextPort = 4000 + Math.floor(Math.random() * 999);
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
rootDir: '.',
|
||||||
testEnvironment: 'node',
|
testEnvironment: 'node',
|
||||||
testMatch: ['**/*.test.js'],
|
testMatch: ['<rootDir>/tests/**/*.test.js'],
|
||||||
collectCoverageFrom: ['turn.js'],
|
collectCoverageFrom: ['turn.js'],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
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';
|
import { renderApp, createCampaignViaUI, selectCampaignByName } from './testHelpers';
|
||||||
|
|
||||||
function findCall(fn, pathSub) {
|
function findCall(fn, pathSub) {
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
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';
|
import { setupReady, addMonsterViaUI, startCombatViaUI } from './testHelpers';
|
||||||
|
|
||||||
function findCallsEnc() {
|
function findCallsEnc() {
|
||||||
@@ -10,12 +10,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, fireEvent, waitFor, within } from '@testing-library/react';
|
import { screen, fireEvent, waitFor, within } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import App from './App';
|
import App from '../App';
|
||||||
import {
|
import {
|
||||||
renderApp, createCampaignViaUI, selectCampaignByName,
|
renderApp, createCampaignViaUI, selectCampaignByName,
|
||||||
createEncounterViaUI, selectEncounterByName, addMonsterViaUI, setupReady,
|
createEncounterViaUI, selectEncounterByName, addMonsterViaUI, setupReady,
|
||||||
} from './testHelpers';
|
} 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) ----------
|
// ---------- scenario helpers (UI only, same buttons as human) ----------
|
||||||
|
|
||||||
+3
-3
@@ -6,9 +6,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, waitFor } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
import '@testing-library/jest-dom';
|
||||||
import App from './App';
|
import App from '../App';
|
||||||
import { MOCK_DB } from './__mocks__/firebase/_mock-db';
|
import { MOCK_DB } from '../__mocks__/firebase/_mock-db';
|
||||||
import { getAdapterCalls, resetAdapterCalls } from './storage/firebase';
|
import { getAdapterCalls, resetAdapterCalls } from '../storage/firebase';
|
||||||
|
|
||||||
// Seed activeDisplay + campaign + encounter so DisplayView has data to subscribe to.
|
// Seed activeDisplay + campaign + encounter so DisplayView has data to subscribe to.
|
||||||
function seedActiveDisplay() {
|
function seedActiveDisplay() {
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
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';
|
import { renderApp, createCampaignViaUI, selectCampaignByName, createEncounterViaUI, selectEncounterByName } from './testHelpers';
|
||||||
|
|
||||||
function findCall(fn, pathSub) {
|
function findCall(fn, pathSub) {
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
import { screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
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';
|
import { setupReady, addMonsterViaUI, startCombatViaUI } from './testHelpers';
|
||||||
|
|
||||||
function findLogCalls() {
|
function findLogCalls() {
|
||||||
@@ -16,7 +16,7 @@ function lastEncCall() {
|
|||||||
|
|
||||||
// Navigate to /logs view. App reads pathname at mount; must re-render with path preset.
|
// Navigate to /logs view. App reads pathname at mount; must re-render with path preset.
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import App from './App';
|
import App from '../App';
|
||||||
async function goToLogs() {
|
async function goToLogs() {
|
||||||
// unmount current tree isn't needed; App checks pathname in useEffect.
|
// unmount current tree isn't needed; App checks pathname in useEffect.
|
||||||
// Re-render a fresh App instance in same container.
|
// Re-render a fresh App instance in same container.
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { screen, fireEvent, waitFor, within } from '@testing-library/react';
|
import { screen, fireEvent, waitFor, within } from '@testing-library/react';
|
||||||
import '@testing-library/jest-dom';
|
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';
|
import { setupReady, addMonsterViaUI, getParticipantForm, startCombatViaUI } from './testHelpers';
|
||||||
|
|
||||||
function findCallsEnc() {
|
function findCallsEnc() {
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
// TDD: contract = spec. Run against memory first. RED until memory.js built.
|
// TDD: contract = spec. Run against memory first. RED until memory.js built.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { runStorageContract } = require('./contract');
|
const { runStorageContract } = require('../storage/contract');
|
||||||
const { createMemoryStorage } = require('./memory');
|
const { createMemoryStorage } = require('../storage/memory');
|
||||||
|
|
||||||
runStorageContract('memory', () => createMemoryStorage());
|
runStorageContract('memory', () => createMemoryStorage());
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
// test helpers: drive App UI to states. Used across characterization suites.
|
// test helpers: drive App UI to states. Used across characterization suites.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
|
import { render, screen, fireEvent, waitFor, within } from '@testing-library/react';
|
||||||
import App from './App';
|
import App from '../App';
|
||||||
import { MOCK_DB } from './__mocks__/firebase/_mock-db';
|
import { MOCK_DB } from '../__mocks__/firebase/_mock-db';
|
||||||
|
|
||||||
// Scoped container: the "Add Participants" section (avoids label clashes with CharacterManager).
|
// Scoped container: the "Add Participants" section (avoids label clashes with CharacterManager).
|
||||||
export function getParticipantForm() {
|
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.change(screen.getByLabelText(/Campaign Name/i), { target: { value: name } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: /^Create$/i }));
|
fireEvent.click(screen.getByRole('button', { name: /^Create$/i }));
|
||||||
// wait for setDoc recorded
|
// 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/')));
|
await waitFor(() => getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/campaigns/')));
|
||||||
const call = 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
|
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));
|
await waitFor(() => screen.getByLabelText(/Encounter Name/i));
|
||||||
fireEvent.change(screen.getByLabelText(/Encounter Name/i), { target: { value: name } });
|
fireEvent.change(screen.getByLabelText(/Encounter Name/i), { target: { value: name } });
|
||||||
fireEvent.click(screen.getByRole('button', { name: /^Create$/i }));
|
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/')));
|
await waitFor(() => getCalls().find(c => c.fn === 'setDoc' && c.path.includes('/encounters/')));
|
||||||
const call = 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();
|
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(/Init Mod/i), { target: { value: String(initMod) } });
|
||||||
fireEvent.change(form.getByLabelText(/Max HP/i), { target: { value: String(maxHp) } });
|
fireEvent.change(form.getByLabelText(/Max HP/i), { target: { value: String(maxHp) } });
|
||||||
fireEvent.click(form.getByRole('button', { name: /Add to Encounter/i }));
|
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(() => {
|
await waitFor(() => {
|
||||||
const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
|
const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
|
||||||
const last = calls[calls.length - 1];
|
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.
|
// Start combat. Assumes encounter selected with active participants.
|
||||||
export async function startCombatViaUI() {
|
export async function startCombatViaUI() {
|
||||||
fireEvent.click(screen.getByRole('button', { name: /Start Combat/i }));
|
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(() => {
|
await waitFor(() => {
|
||||||
const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
|
const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
|
||||||
const last = calls[calls.length - 1];
|
const last = calls[calls.length - 1];
|
||||||
Reference in New Issue
Block a user