From a200b3803eb60d0f0145897a14c9af58681a0e04 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sun, 4 Oct 2026 13:43:47 -0400 Subject: [PATCH] fix: handle game list and search promise failures Await search platform and game loading inside the existing error handler, preserving the platform fallback and avoiding nested promise chains. Handle asynchronous game-list rendering failures and cover both error paths with regression tests. --- gh-pages-template/assets/js/item_detail.js | 2 + gh-pages-template/assets/js/item_loader.js | 42 +++++++---------- tests/item_detail.test.js | 19 ++++++++ tests/item_loader.test.js | 54 ++++++++++++++++++++++ 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/gh-pages-template/assets/js/item_detail.js b/gh-pages-template/assets/js/item_detail.js index 009f4303f986..486eebe51266 100644 --- a/gh-pages-template/assets/js/item_detail.js +++ b/gh-pages-template/assets/js/item_detail.js @@ -197,6 +197,8 @@ function renderGameList(container, games) { const coverUrl = data?.cover ? igdbImageUrl(data.cover.url, "t_cover_small_2x") : null; renderGameCard(row, id, name, coverUrl); }); + }).catch(error => { + console.error("Failed to render game list", error); }); } } diff --git a/gh-pages-template/assets/js/item_loader.js b/gh-pages-template/assets/js/item_loader.js index f4653e62df76..7811a7d1c280 100644 --- a/gh-pages-template/assets/js/item_loader.js +++ b/gh-pages-template/assets/js/item_loader.js @@ -550,7 +550,7 @@ function run_search() { if (!r.ok) throw new Error(`Bucket not found`) return r.json() }) - .then(bucket_data => { + .then(async bucket_data => { loading.remove() // Filter results by name (case-insensitive) @@ -577,30 +577,22 @@ function run_search() { search_container.appendChild(row) // Fetch platform names to display - fetch(`${base_url}/platforms/all.json`) - .then(r => r.json()) - .then(allPlatforms => { - // Fetch full game data for each match - const gamePromises = matches.slice(0, 60).map(([id, _game]) => - fetchGameData(id, _game.name) - ) - - Promise.all(gamePromises).then(results => { - renderSearchResults(results, row, allPlatforms) - addMoreResultsNote(search_container, matches.length, 60) - }) - }) - .catch(() => { - // Fallback if platforms can't be loaded - still fetch game data - const gamePromises = matches.slice(0, 60).map(([id, _game]) => - fetchGameData(id, _game.name) - ) - - Promise.all(gamePromises).then(results => { - renderSearchResults(results, row, null) - addMoreResultsNote(search_container, matches.length, 60) - }) - }) + let allPlatforms + try { + const response = await fetch(`${base_url}/platforms/all.json`) + allPlatforms = await response.json() + } catch { + // Fallback if platforms can't be loaded - still fetch game data + allPlatforms = null + } + + // Fetch full game data for each match + const gamePromises = matches.slice(0, 60).map(([id, _game]) => + fetchGameData(id, _game.name) + ) + const results = await Promise.all(gamePromises) + renderSearchResults(results, row, allPlatforms) + addMoreResultsNote(search_container, matches.length, 60) }) .catch(err => { loading.remove() diff --git a/tests/item_detail.test.js b/tests/item_detail.test.js index 6fae7bc099ac..477e21c95178 100644 --- a/tests/item_detail.test.js +++ b/tests/item_detail.test.js @@ -284,6 +284,25 @@ describe('item_detail.js', () => { expect(container.querySelector('.row')).not.toBeNull(); }); + test('handles errors while rendering fetched game cards', async () => { + globalThis.fetch = jest.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ name: 'Fetched Game' }), + }); + const error = new Error('Rendering failed'); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const container = document.createElement('div'); + renderGameList(container, [42]); + jest.spyOn(container.querySelector('.row'), 'appendChild').mockImplementation(() => { + throw error; + }); + + await new Promise(r => setTimeout(r, 0)); + + expect(errorSpy).toHaveBeenCalledWith('Failed to render game list', error); + expect(errorSpy).toHaveBeenCalledTimes(1); + }); + test('renders game without cover (fetch returns not-ok)', async () => { globalThis.fetch = jest.fn().mockResolvedValue({ ok: false }); const container = document.createElement('div'); diff --git a/tests/item_loader.test.js b/tests/item_loader.test.js index e38569e1e78b..9f7278307dba 100644 --- a/tests/item_loader.test.js +++ b/tests/item_loader.test.js @@ -516,5 +516,59 @@ describe('item_loader.js', () => { const container = document.getElementById('search-container'); expect(container.textContent).toContain('Mario'); }); + + test('renders results even when platform JSON parsing fails', async () => { + document.getElementById('search_term').value = 'mario'; + globalThis.fetch = jest.fn() + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ '10': { name: 'Mario' } }), + }) + .mockResolvedValueOnce({ + json: () => Promise.reject(new Error('Invalid platform JSON')), + }) + .mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ name: 'Mario', platforms: [6] }), + }); + + run_search(); + await flushPromises(); + + const container = document.getElementById('search-container'); + expect(container.querySelector('a').textContent).toContain('Mario'); + expect(container.textContent).not.toContain('Search failed'); + expect(globalThis.fetch).toHaveBeenCalledTimes(3); + }); + + test.each([true, false])('shows rendering errors with platform fetch success=%s', async platformsAvailable => { + document.getElementById('search_term').value = 'halo'; + globalThis.fetch = jest.fn() + .mockResolvedValueOnce({ + ok: true, + json: () => Promise.resolve({ '1': { name: 'Halo' } }), + }); + if (platformsAvailable) { + globalThis.fetch.mockResolvedValueOnce({ json: () => Promise.resolve({}) }); + } else { + globalThis.fetch.mockRejectedValueOnce(new Error('Platform fetch failed')); + } + globalThis.fetch.mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ name: 'Halo' }), + }); + const createElement = document.createElement.bind(document); + jest.spyOn(document, 'createElement').mockImplementation(tagName => { + if (tagName === 'a') throw new Error('Rendering failed'); + return createElement(tagName); + }); + + run_search(); + await flushPromises(); + + const container = document.getElementById('search-container'); + expect(container.querySelector('.text-danger').textContent).toBe('Search failed: Rendering failed'); + expect(globalThis.fetch).toHaveBeenCalledTimes(3); + }); }); });