diff --git a/.api.log b/.api.log deleted file mode 100644 index 17da6b0..0000000 --- a/.api.log +++ /dev/null @@ -1,2 +0,0 @@ -SQLite database ready at /Users/nateemmert/Documents/study-app/server/data/projects.db -Bible Study API running on http://localhost:3001 diff --git a/.api.pid b/.api.pid deleted file mode 100644 index d5378a1..0000000 --- a/.api.pid +++ /dev/null @@ -1 +0,0 @@ -24084 diff --git a/.gitignore b/.gitignore index 0e82917..4511232 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ coverage/ *.local .vite.pid .vite.log +.api.pid +.api.log diff --git a/src/App.test.jsx b/src/App.test.jsx index 71b210a..40540c7 100644 --- a/src/App.test.jsx +++ b/src/App.test.jsx @@ -16,15 +16,15 @@ const mockChapterData = { }, }; -const mockGreekDefinition = [ - { - topic: 'G4102', - lexeme: 'πίστις', - transliteration: 'pistis', - short_definition: 'faith, belief', - definition: '

Part(s) of speech: Noun

Faith or belief.

', +// Mirrors the OpenScriptures Strong's Greek dictionary format loaded from jsdelivr. +const mockGreekDict = { + G4102: { + lemma: 'πίστις', + translit: 'pistis', + kjv_def: 'faith, belief', + strongs_def: 'persuasion, i.e. credence; moral conviction', }, -]; +}; function buildFetchMock({ chapterData = mockChapterData, greekData = null } = {}) { return vi.fn((url) => { @@ -34,8 +34,11 @@ function buildFetchMock({ chapterData = mockChapterData, greekData = null } = {} if (url.includes('bible.helloao.org')) { return Promise.resolve({ ok: true, json: () => Promise.resolve(chapterData) }); } - if (url.includes('bolls.life') && greekData !== null) { - return Promise.resolve({ ok: true, json: () => Promise.resolve(greekData) }); + if (url.includes('strongs-greek-dictionary') && greekData !== null) { + return Promise.resolve({ + ok: true, + text: () => Promise.resolve(`var strongsGreekDictionary = ${JSON.stringify(greekData)};`), + }); } return Promise.resolve({ ok: false }); }); @@ -45,10 +48,11 @@ function buildFetchMock({ chapterData = mockChapterData, greekData = null } = {} // Test lifecycle // --------------------------------------------------------------------------- beforeEach(() => { - vi.stubGlobal('fetch', buildFetchMock()); - vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:mock'), revokeObjectURL: vi.fn() }); - vi.spyOn(window, 'confirm').mockReturnValue(false); localStorage.clear(); + vi.stubGlobal('fetch', buildFetchMock()); + vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock'); + vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {}); + vi.spyOn(window, 'confirm').mockReturnValue(false); }); afterEach(() => { @@ -182,7 +186,7 @@ describe('Chunk management', () => { test('creates a chunk by clicking a verse range', async () => { await loadChapter(); addChunk('Paul, a servant', 'Grace and peace'); - await screen.findByText(/1-3/); + await screen.findByText(/1[-–]3/); }); test('chunk count increments after each addition', async () => { @@ -299,7 +303,7 @@ describe('Greek word lookup', () => { }); test('populates fields after a successful lookup', async () => { - await goToStudyAndAddGreekWord(buildFetchMock({ greekData: mockGreekDefinition })); + await goToStudyAndAddGreekWord(buildFetchMock({ greekData: mockGreekDict })); fireEvent.change(screen.getByPlaceholderText(/G4102, H7225, 4102/i), { target: { value: 'G4102' } }); fireEvent.click(screen.getByRole('button', { name: /look up greek/i })); await screen.findByDisplayValue('πίστις'); @@ -308,7 +312,7 @@ describe('Greek word lookup', () => { }); test('shows "No definition found." when the API returns an empty array', async () => { - await goToStudyAndAddGreekWord(buildFetchMock({ greekData: [] })); + await goToStudyAndAddGreekWord(buildFetchMock({ greekData: {} })); fireEvent.change(screen.getByPlaceholderText(/G4102, H7225, 4102/i), { target: { value: 'G4102' } }); fireEvent.click(screen.getByRole('button', { name: /look up greek/i })); await screen.findByDisplayValue('No definition found.'); diff --git a/src/test-setup.js b/src/test-setup.js index 7b0828b..a06322a 100644 --- a/src/test-setup.js +++ b/src/test-setup.js @@ -1 +1,18 @@ import '@testing-library/jest-dom'; + +// Node 22+ defines experimental localStorage/sessionStorage globals that are +// undefined unless --localstorage-file is passed. Vitest's jsdom environment +// skips copying window keys that already exist on the Node global, so jsdom's +// storage objects get shadowed. Bridge them from the raw jsdom instance. +const jsdomWindow = globalThis.jsdom?.window; +if (jsdomWindow) { + for (const key of ['localStorage', 'sessionStorage']) { + if (typeof globalThis[key] === 'undefined' && jsdomWindow[key]) { + Object.defineProperty(globalThis, key, { + value: jsdomWindow[key], + writable: true, + configurable: true, + }); + } + } +} diff --git a/src/utils.test.js b/src/utils.test.js index 21ba68f..dcb1067 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -263,9 +263,16 @@ describe('migrateChunk', () => { expect(result.crossReferences).toEqual([]); }); - test('is a no-op when chunk already has observation field', () => { + test('preserves existing fields and backfills new ones when chunk already has observation field', () => { const modern = { id: 'c1', startVerse: 1, endVerse: 1, observation: 'Already migrated.', interpretation: '', application: '', crossReferences: [], greekWords: [] }; - expect(migrateChunk(modern)).toBe(modern); + const result = migrateChunk(modern); + expect(result).toMatchObject(modern); + expect(result.tags).toEqual([]); + expect(result.spilloverEndVerse).toBeNull(); + expect(result.generalNotes).toBe(''); + expect(result.episodeNumber).toBe(''); + expect(result.episodeTitle).toBe(''); + expect(result.finalScript).toBe(''); }); test('handles missing notes gracefully', () => { diff --git a/vite.config.js b/vite.config.js index e89189a..3dc6e77 100644 --- a/vite.config.js +++ b/vite.config.js @@ -16,6 +16,9 @@ export default defineConfig({ }, test: { environment: 'jsdom', + environmentOptions: { + jsdom: { url: 'http://localhost:3000/' }, + }, globals: true, setupFiles: './src/test-setup.js', coverage: {