fix: migrate item_loader from jQuery to fetch (#266)

This commit is contained in:
David Lane authored and GitHub committed 2026-05-31 10:23:46 -04:00
1 parent ccd883a7d2
commit 074c8b00e0
4 files changed
+163 -123

No files matched your search

-1
View File
@@ -15,7 +15,6 @@ export default [
globals: {
...globals.browser,
...globals.node,
...globals.jquery,
// cross-file globals injected by item_detail.js into the browser scope
"base_url": "readonly",
"base_path": "readonly",
+118 -97
View File
@@ -400,108 +400,126 @@ function createPlatformCardElement(platform, platform_region_flag_map, metadata_
return column;
}
$(document).ready(function(){
// Set cache = false for all jquery ajax requests.
$.ajaxSetup({
cache: false,
})
/**
* Map platform release regions to display flags and Bootstrap font sizes.
*
* @type {Object.<string, {code: string, size: string}>}
*/
const platform_region_flag_map = {
"europe": {
"code": String.fromCodePoint(0x1F1EA, 0x1F1FA),
"size": "fs-2",
},
"north_america": {
"code": String.fromCodePoint(0x1F1FA, 0x1F1F8),
"size": "fs-2",
},
"australia": {
"code": String.fromCodePoint(0x1F1E6, 0x1F1FA),
"size": "fs-2",
},
"new_zealand": {
"code": String.fromCodePoint(0x1F1F3, 0x1F1FF),
"size": "fs-2",
},
"japan": {
"code": String.fromCodePoint(0x1F1EF, 0x1F1F5),
"size": "fs-2",
},
"china": {
"code": String.fromCodePoint(0x1F1E8, 0x1F1F3),
"size": "fs-2",
},
"asia": {
"code": String.fromCodePoint(0x1F30F),
"size": "fs-4",
},
"worldwide": {
"code": String.fromCodePoint(0x1F30E),
"size": "fs-4",
},
"korea": {
"code": String.fromCodePoint(0x1F1F0, 0x1F1F7),
"size": "fs-2",
},
"brazil": {
"code": String.fromCodePoint(0x1F1E7, 0x1F1F7),
"size": "fs-2",
},
}
// get platform cross-reference from json
let platform_xref
let get_platform_xref = function() {
$.ajax({
url: `${base_url}/platforms/cross-reference.json`,
type: "GET",
dataType: "json",
async: false, // this is false, so we can set the platform_xref variable
success: function (result) {
platform_xref = result
}
})
/**
* Map platform version metadata keys to Material Symbols icon names.
*
* Keys with null values are rendered by specialized handlers.
*
* @type {Object.<string, string|null>}
*/
const metadata_key_icon_map = {
// material icons
'os': 'code_blocks',
'cpu': 'memory',
'graphics': 'developer_board',
'memory': 'memory_alt',
'storage': 'storage',
'media': 'save',
'connectivity': 'cable',
'output': 'settings_input_component',
'resolutions': 'aspect_ratio',
'sound': 'volume_up',
// these will be processed slightly differently
'platform_version_release_dates': null,
'summary': null,
}
/**
* Fetch a JSON resource without using the browser HTTP cache.
*
* @param {string} url - URL to load.
* @returns {Promise<object>} Parsed JSON response.
*/
function fetchJson(url) {
return fetch(url, { cache: "no-store" }).then(response => response.json());
}
/**
* Render platform cards after enriching IGDB platform data with cross-reference metadata.
*
* @param {Object.<string, object>} result - Platform records keyed by source identifier.
* @param {Object.<string, object>} platform_xref - Platform cross-reference records.
* @returns {void}
*/
function renderPlatformCards(result, platform_xref) {
const platforms = processPlatformsData(result, platform_xref);
const sorted = platforms.toSorted(globalThis.rankingSorter("name", "id")).reverse();
for(let item in sorted) {
const column = createPlatformCardElement(sorted[item], platform_region_flag_map, metadata_key_icon_map, base_path);
platforms_container.appendChild(column);
}
}
let platform_region_flag_map = {
"europe": {
"code": String.fromCodePoint(0x1F1EA, 0x1F1FA),
"size": "fs-2",
},
"north_america": {
"code": String.fromCodePoint(0x1F1FA, 0x1F1F8),
"size": "fs-2",
},
"australia": {
"code": String.fromCodePoint(0x1F1E6, 0x1F1FA),
"size": "fs-2",
},
"new_zealand": {
"code": String.fromCodePoint(0x1F1F3, 0x1F1FF),
"size": "fs-2",
},
"japan": {
"code": String.fromCodePoint(0x1F1EF, 0x1F1F5),
"size": "fs-2",
},
"china": {
"code": String.fromCodePoint(0x1F1E8, 0x1F1F3),
"size": "fs-2",
},
"asia": {
"code": String.fromCodePoint(0x1F30F),
"size": "fs-4",
},
"worldwide": {
"code": String.fromCodePoint(0x1F30E),
"size": "fs-4",
},
"korea": {
"code": String.fromCodePoint(0x1F1F0, 0x1F1F7),
"size": "fs-2",
},
"brazil": {
"code": String.fromCodePoint(0x1F1E7, 0x1F1F7),
"size": "fs-2",
},
}
/**
* Load platform cross-reference and platform data, then render the platform card grid.
*
* @returns {Promise<void>} Resolves when platform cards have been rendered.
*/
function initializePlatformCards() {
return fetchJson(`${base_url}/platforms/cross-reference.json`)
.then(platform_xref => fetchJson(`${base_url}/platforms/all.json`)
.then(result => {
renderPlatformCards(result, platform_xref);
}));
}
let metadata_key_icon_map = {
// material icons
'os': 'code_blocks',
'cpu': 'memory',
'graphics': 'developer_board',
'memory': 'memory_alt',
'storage': 'storage',
'media': 'save',
'connectivity': 'cable',
'output': 'settings_input_component',
'resolutions': 'aspect_ratio',
'sound': 'volume_up',
// these will be processed slightly differently
'platform_version_release_dates': null,
'summary': null,
}
// create platform cards
let initialize = function(){
$.ajax({
url: `${base_url}/platforms/all.json`,
type: "GET",
dataType:"json",
success: function (result) {
const platforms = processPlatformsData(result, platform_xref);
const sorted = platforms.toSorted(globalThis.rankingSorter("name", "id")).reverse();
for(let item in sorted) {
const column = createPlatformCardElement(sorted[item], platform_region_flag_map, metadata_key_icon_map, base_path);
platforms_container.appendChild(column);
}
}
/* istanbul ignore next */
if (typeof module === "undefined") {
document.addEventListener("DOMContentLoaded", () => {
initializePlatformCards().catch(error => {
console.error("Failed to initialize platform cards", error);
});
}
get_platform_xref()
initialize()
})
});
}
/**
* Search for games by name across all buckets.
@@ -609,6 +627,9 @@ if (typeof module !== "undefined") {
addMetadataItemToFooter,
processPlatformsData,
createPlatformCardElement,
fetchJson,
renderPlatformCards,
initializePlatformCards,
run_search,
};
}
+2 -3
View File
@@ -11,9 +11,8 @@ after-content:
ext-css:
- href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200
ext-js:
- https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
- https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/levenshtein-distance.js
- https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/ranking-sorter.js
- href: "https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/levenshtein-distance.js"
- href: "https://cdn.jsdelivr.net/npm/@lizardbyte/shared-web@2026.314.32913/dist/ranking-sorter.js"
js:
- /GameDB/assets/js/item_loader.js
---
+43 -22
View File
@@ -22,28 +22,6 @@ document.body.innerHTML = `
<input id="search_term" value="" />
`;
// Mock jQuery — fire ready callback synchronously, and fire ajax success immediately
globalThis.$ = function() {
return { ready: (fn) => fn() };
};
globalThis.$.ajaxSetup = () => {};
globalThis.$.ajax = function(opts) {
if (opts?.success) {
// Return mock data appropriate to the URL
if (opts?.url?.includes('cross-reference')) {
opts.success({});
} else if (opts?.url?.includes('all.json')) {
opts.success({
'1': {
id: 1, name: 'PC', url: 'https://igdb.com', summary: 'Personal computer.',
screenscraper_id: null, screenscraper_region: null,
},
});
}
}
};
globalThis.jQuery = globalThis.$;
const {
splitString,
fetchGameData,
@@ -58,6 +36,8 @@ const {
addMetadataItemToFooter,
processPlatformsData,
createPlatformCardElement,
fetchJson,
initializePlatformCards,
run_search,
} = require('../gh-pages-template/assets/js/item_loader.js');
@@ -117,6 +97,16 @@ describe('item_loader.js', () => {
});
});
describe('fetchJson', () => {
test('fetches JSON with no-store cache behavior', async () => {
globalThis.fetch = jest.fn().mockResolvedValue({
json: () => Promise.resolve({ ok: true }),
});
await expect(fetchJson('/data.json')).resolves.toEqual({ ok: true });
expect(globalThis.fetch).toHaveBeenCalledWith('/data.json', { cache: 'no-store' });
});
});
describe('createGameCard', () => {
test('renders card with cover image', () => {
const col = createGameCard(1, {
@@ -398,7 +388,38 @@ describe('item_loader.js', () => {
});
});
describe('initializePlatformCards', () => {
test('loads cross-reference before rendering platform cards', async () => {
globalThis.fetch = jest.fn(url => Promise.resolve({
json: () => Promise.resolve(
url.includes('cross-reference')
? { pc: { ids: { igdb: 1, screenscraper: 123 }, variables: { screenscraper: { region: 'us' } } } }
: { '1': { id: 1, name: 'PC', url: 'https://igdb.com', summary: 'Personal computer.' } },
),
}));
await initializePlatformCards();
expect(globalThis.fetch).toHaveBeenNthCalledWith(
1,
'http://localhost/GameDB/platforms/cross-reference.json',
{ cache: 'no-store' },
);
expect(globalThis.fetch).toHaveBeenNthCalledWith(
2,
'http://localhost/GameDB/platforms/all.json',
{ cache: 'no-store' },
);
expect(document.getElementById('platforms-container').textContent).toContain('PC');
});
});
describe('run_search', () => {
/**
* Wait for queued promise callbacks in fetch-driven rendering tests.
*
* @returns {Promise<void>} Resolves after the current macrotask.
*/
const flushPromises = () => new Promise(r => setTimeout(r, 0));
test('does nothing when search term is empty', () => {