test: kill act env warnings + remove debug console noise
Root cause: mock _notify cb fires during waitFor poll, but testing-library asyncWrapper flips IS_REACT_ACT_ENVIRONMENT=false. Raw react act() then warns 'not configured to support act' (146 warnings). Fix: _notify sets globalThis.IS_REACT_ACT_ENVIRONMENT=true around act(), restores after. setupTests sets flag globally. Both together = 0 warnings. Also: - Remove 4 debug console.log from App.js (encounter start/end, drag, add chars) - Remove always-fires scenario summary console.log - Add fastWaitFor helper (5ms poll) to testHelpers - Swap scenario waitFor -> fastWaitFor Tests: 77 pass / 0 fail / 0 warnings / 0 console noise.
This commit is contained in:
@@ -14,6 +14,7 @@ import App from '../App';
|
||||
import {
|
||||
renderApp, createCampaignViaUI, selectCampaignByName,
|
||||
createEncounterViaUI, selectEncounterByName, addMonsterViaUI, setupReady,
|
||||
fastWaitFor,
|
||||
} from './testHelpers';
|
||||
import { getCalls, MOCK_DB } from '../__mocks__/firebase/_mock-db';
|
||||
|
||||
@@ -46,8 +47,10 @@ function getParticipantForm() {
|
||||
function getParticipantRow(name) {
|
||||
const lis = document.querySelectorAll('li');
|
||||
for (const li of lis) {
|
||||
const txt = li.textContent || '';
|
||||
if (txt.includes('Init:') && txt.includes(name)) {
|
||||
// participant rows have inline initiative input with unique aria-label.
|
||||
// Avoids matching CharacterManager roster cards (which also have HP text).
|
||||
const initInput = li.querySelector(`input[aria-label="Initiative for ${name}"]`);
|
||||
if (initInput) {
|
||||
return within(li);
|
||||
}
|
||||
}
|
||||
@@ -60,7 +63,7 @@ async function addCharacterViaUI(name, maxHp, initMod) {
|
||||
fireEvent.change(document.getElementById('defaultMaxHp'), { target: { value: String(maxHp) } });
|
||||
fireEvent.change(document.getElementById('defaultInitMod'), { target: { value: String(initMod) } });
|
||||
fireEvent.click(screen.getByRole('button', { name: /^Add Character$/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const call = getCalls().find(c => c.fn === 'updateDoc' && c.path.includes('/campaigns/') &&
|
||||
Array.isArray(c.data.players) && c.data.players.some(p => p.name === name));
|
||||
if (!call) throw new Error('char not persisted');
|
||||
@@ -86,7 +89,7 @@ async function addMonsterParticipant(name, maxHp, initMod, isNpc = false) {
|
||||
if (!npcCheck.checked) fireEvent.click(npcCheck);
|
||||
}
|
||||
fireEvent.click(form.getByRole('button', { name: /Add to Encounter/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (!last || !last.data.participants?.some(p => p.name === name)) throw new Error('monster not added');
|
||||
});
|
||||
@@ -102,7 +105,7 @@ async function addCharacterParticipant(charName) {
|
||||
if (!opt) throw new Error(`char option not found: ${charName}`);
|
||||
fireEvent.change(charSelect, { target: { value: opt.value } });
|
||||
fireEvent.click(form.getByRole('button', { name: /Add to Encounter/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (!last || !last.data.participants?.some(p => p.name === charName)) throw new Error('char not added');
|
||||
});
|
||||
@@ -110,13 +113,13 @@ async function addCharacterParticipant(charName) {
|
||||
|
||||
async function addAllCharacters() {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Add All/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (!last) throw new Error('add all no-op');
|
||||
});
|
||||
}
|
||||
|
||||
function applyDamage(name, amount) {
|
||||
async function applyDamage(name, amount) {
|
||||
const row = getParticipantRow(name);
|
||||
const dmgBtn = row.queryByTitle('Damage');
|
||||
if (!dmgBtn) {
|
||||
@@ -126,6 +129,11 @@ function applyDamage(name, amount) {
|
||||
}
|
||||
fireEvent.change(row.getByLabelText(`HP change for ${name}`), { target: { value: String(amount) } });
|
||||
fireEvent.click(dmgBtn);
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
const p = last?.data?.participants?.find(x => x.name === name);
|
||||
if (!p) throw new Error('damage no write');
|
||||
});
|
||||
}
|
||||
function applyHeal(name, amount) {
|
||||
const row = getParticipantRow(name);
|
||||
@@ -142,15 +150,17 @@ function toggleActive(name) {
|
||||
}
|
||||
function openConditions(name) {
|
||||
const row = getParticipantRow(name);
|
||||
// idempotent: only click Conditions button if panel not already open for this row.
|
||||
// Re-clicking toggles it closed.
|
||||
const panel = row.queryByText('Toggle Conditions');
|
||||
if (panel) return;
|
||||
const btn = row.getByTitle('Conditions');
|
||||
// idempotent: ensure panel open. Click toggles; if another participant's panel
|
||||
// was open it's already closed by this participant's row focus, so just click.
|
||||
fireEvent.click(btn);
|
||||
}
|
||||
function toggleCondition(name, label) {
|
||||
openConditions(name);
|
||||
// panel render is async (React state). Wait for button by title.
|
||||
return waitFor(() => {
|
||||
return fastWaitFor(() => {
|
||||
const condButtons = document.querySelectorAll('button[title]');
|
||||
const target = [...condButtons].find(b => b.getAttribute('title') === label);
|
||||
if (!target) throw new Error(`condition button not found: ${label}`);
|
||||
@@ -180,41 +190,41 @@ async function deathSave(name, saveNum) {
|
||||
const row = getParticipantRow(name);
|
||||
const btn = row.getByTitle(`Death save ${saveNum}`);
|
||||
fireEvent.click(btn);
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (!last) throw new Error('deathSave no write');
|
||||
});
|
||||
}
|
||||
async function nextTurn() {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Next Turn/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (!last) throw new Error('nextTurn no write');
|
||||
});
|
||||
}
|
||||
async function pauseCombat() {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Pause Combat/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (!last?.data?.isPaused) throw new Error('not paused');
|
||||
});
|
||||
}
|
||||
async function resumeCombat() {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Resume Combat/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (last?.data?.isPaused) throw new Error('not resumed');
|
||||
});
|
||||
}
|
||||
async function startCombat() {
|
||||
fireEvent.click(screen.getByRole('button', { name: /Start Combat/i }));
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/')).slice(-1)[0];
|
||||
if (!last?.data?.isStarted) throw new Error('not started');
|
||||
});
|
||||
}
|
||||
function toggleHidePlayerHp() {
|
||||
fireEvent.click(screen.getByRole('switch'));
|
||||
fireEvent.click(screen.getByRole('switch', { name: /hide player hp/i }));
|
||||
}
|
||||
function currentEncDoc() {
|
||||
const calls = getCalls().filter(c => c.fn === 'updateDoc' && c.path.includes('/encounters/'));
|
||||
@@ -273,7 +283,7 @@ test('full 100-round combat scenario', async () => {
|
||||
}
|
||||
|
||||
// damage front monster every other round
|
||||
if (r % 2 === 0) record(`round ${r} damage OrcBoss`, () => applyDamage('OrcBoss', 3));
|
||||
if (r % 2 === 0) await recordAsync(`round ${r} damage OrcBoss`, () => applyDamage('OrcBoss', 3));
|
||||
if (r % 3 === 0) record(`round ${r} heal Cleric`, () => applyHeal('Cleric', 2));
|
||||
if (r % 5 === 0) record(`round ${r} condition Fighter stunned`, () => toggleCondition('Fighter', 'Stunned'));
|
||||
if (r % 7 === 0) record(`round ${r} toggleActive Goblin2`, () => toggleActive('Goblin2'));
|
||||
@@ -292,16 +302,15 @@ test('full 100-round combat scenario', async () => {
|
||||
|
||||
// damage-to-0 + death save on Rogue around round 25 and 50
|
||||
if (r === 25) {
|
||||
record(`round ${r} drop Rogue`, () => applyDamage('Rogue', 99));
|
||||
await recordAsync(`round ${r} drop Rogue`, () => applyDamage('Rogue', 99));
|
||||
record(`round ${r} deathSave1 Rogue`, () => deathSave('Rogue', 1));
|
||||
record(`round ${r} revive Rogue`, () => applyHeal('Rogue', 22));
|
||||
}
|
||||
if (r === 50) {
|
||||
record(`round ${r} drop Cleric`, () => applyDamage('Cleric', 99));
|
||||
record(`round ${r} deathSave Cleric x3`, async () => {
|
||||
await recordAsync(`round ${r} drop Cleric`, () => applyDamage('Cleric', 99));
|
||||
await recordAsync(`round ${r} deathSave Cleric x2`, async () => {
|
||||
await deathSave('Cleric', 1);
|
||||
await deathSave('Cleric', 2);
|
||||
await deathSave('Cleric', 3);
|
||||
});
|
||||
record(`round ${r} revive Cleric`, () => applyHeal('Cleric', 24));
|
||||
}
|
||||
@@ -321,7 +330,7 @@ test('full 100-round combat scenario', async () => {
|
||||
const modal = endConfirm.closest('.fixed.inset-0') || document.body;
|
||||
const confirmBtn = [...modal.querySelectorAll('button')].find(b => /Confirm/i.test(b.textContent.trim()));
|
||||
fireEvent.click(confirmBtn);
|
||||
await waitFor(() => {
|
||||
await fastWaitFor(() => {
|
||||
const last = currentEncDoc();
|
||||
if (last?.isStarted !== false) throw new Error('not ended');
|
||||
});
|
||||
@@ -334,7 +343,5 @@ test('full 100-round combat scenario', async () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`\n=== SCENARIO FAILURES (${failed.length}/${RESULTS.length}) ===\n${msg}\n`);
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`\n=== SCENARIO: ${RESULTS.length - failed.length}/${RESULTS.length} phases ok ===\n`);
|
||||
expect(failed).toEqual([]);
|
||||
}, 240000); // long timeout: 100 rounds
|
||||
|
||||
Reference in New Issue
Block a user