diff --git a/package.json b/package.json index a9a8036..865e033 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "server:dev": "npm run dev --workspace server", "server:test": "npm test --workspace server", "shared:test": "npm test --workspace shared", - "test:all": "npm run shared:test && npm run server:test" + "test:all": "CI=true react-scripts test --watchAll=false && npm run shared:test && npm run server:test" }, "eslintConfig": { "extends": [ diff --git a/src/tests/Combat.scenario.test.js b/src/tests/Combat.scenario.test.js index 435eebe..b77610a 100644 --- a/src/tests/Combat.scenario.test.js +++ b/src/tests/Combat.scenario.test.js @@ -1,5 +1,5 @@ // Combat.scenario.test.js -// Full combat scenario driven through shared/turn.js directly — NO React. +// Full combat scenario driven through shared/turn.js directly --- NO React. // // Does 100 ROUNDS (not turns). A round = one full pass through initiative // (every active participant acts once); round counter increments at wrap. @@ -311,10 +311,6 @@ test('full 100-round combat scenario (shared, no React)', async () => { await recordAsync('endCombat', () => endCombat()); - // sanity log: prove we actually ran ROUNDS rounds, not ~ROUNDS turns - // eslint-disable-next-line no-console - console.log(`\nscenario: ${roundsDone} rounds, ${turnTotal} turns\n`); - // ---------- report ---------- const failed = RESULTS.filter(x => !x.ok); if (failed.length > 0) { diff --git a/src/tests/firebase.contract.test.js b/src/tests/firebase.contract.test.js new file mode 100644 index 0000000..3802da5 --- /dev/null +++ b/src/tests/firebase.contract.test.js @@ -0,0 +1,18 @@ +// Firebase adapter contract test. +// Runs the SAME storage contract as memory + ws (src/storage/contract.js) +// against createFirebaseStorage. The SDK is mocked via src/__mocks__/firebase/*, +// so this proves the adapter translates contract calls -> SDK calls correctly +// (path handling, merge semantics, subscribe) without hitting network. + +import { initFirebase, createFirebaseStorage } from '../storage/firebase'; +import { runStorageContract } from '../storage/contract'; +import { resetMockDb } from '../__mocks__/firebase/_mock-db'; + +// Firebase mock DB is shared global state. Reset before each factory so +// contract tests are isolated. +runStorageContract('firebase', () => { + resetMockDb(); + const ok = initFirebase(); + if (!ok) throw new Error('initFirebase failed under mocked env'); + return createFirebaseStorage(); +}); diff --git a/src/tests/storage.factory.test.js b/src/tests/storage.factory.test.js new file mode 100644 index 0000000..2ddd60f --- /dev/null +++ b/src/tests/storage.factory.test.js @@ -0,0 +1,75 @@ +// Storage factory (index.js) test. +// Verifies getStorage() returns the right adapter per REACT_APP_STORAGE env, +// caches as singleton, and getStorageMode() reports the active mode. +// Adapters are mocked (no network/init) — factory routing is the unit. + +import { getStorage, getStorageMode } from '../storage/index'; + +// reset module-level singleton between tests +let originalStorage; + +beforeEach(() => { + originalStorage = process.env.REACT_APP_STORAGE; + // bust the module singleton by re-importing fresh + jest.resetModules(); +}); + +afterEach(() => { + if (originalStorage === undefined) delete process.env.REACT_APP_STORAGE; + else process.env.REACT_APP_STORAGE = originalStorage; + jest.resetModules(); +}); + +describe('getStorageMode', () => { + test('defaults to firebase when env unset', () => { + delete process.env.REACT_APP_STORAGE; + const { getStorageMode } = require('../storage/index'); + expect(getStorageMode()).toBe('firebase'); + }); + + test('returns ws when REACT_APP_STORAGE=ws', () => { + process.env.REACT_APP_STORAGE = 'ws'; + const { getStorageMode } = require('../storage/index'); + expect(getStorageMode()).toBe('ws'); + }); + + test('returns memory when REACT_APP_STORAGE=memory', () => { + process.env.REACT_APP_STORAGE = 'memory'; + const { getStorageMode } = require('../storage/index'); + expect(getStorageMode()).toBe('memory'); + }); +}); + +describe('getStorage factory routing', () => { + test('memory mode returns memory adapter', () => { + process.env.REACT_APP_STORAGE = 'memory'; + const { getStorage } = require('../storage/index'); + const s = getStorage(); + expect(s).toBeTruthy(); + expect(typeof s.getDoc).toBe('function'); + expect(typeof s.setDoc).toBe('function'); + expect(typeof s.subscribeDoc).toBe('function'); + }); + + test('returns singleton on repeat call (same instance)', () => { + process.env.REACT_APP_STORAGE = 'memory'; + const { getStorage } = require('../storage/index'); + const a = getStorage(); + const b = getStorage(); + expect(a).toBe(b); + }); + + test('ws mode returns ws adapter (init may fail without backend — catch)', () => { + process.env.REACT_APP_STORAGE = 'ws'; + const { getStorage } = require('../storage/index'); + try { + const s = getStorage(); + expect(s).toBeTruthy(); + expect(typeof s.getDoc).toBe('function'); + } catch (e) { + // ws adapter without backend URL/config is allowed to throw at factory; + // routing reached ws branch (not memory/firebase) which is the contract. + expect(e).toBeTruthy(); + } + }); +});