diff --git a/gh-pages-template/browse/franchises.html b/gh-pages-template/browse/franchises.html
index ada56e0030c5..0e9690029727 100644
--- a/gh-pages-template/browse/franchises.html
+++ b/gh-pages-template/browse/franchises.html
@@ -6,7 +6,7 @@
- /assets/css/hide-heading.css
js:
- /GameDB/assets/js/item_detail.js
- - /GameDB/assets/js/franchise_detail.js
+ - /GameDB/assets/js/group_detail.js
ext-css:
- href: https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200
---
@@ -15,6 +15,7 @@
globalThis.GAMEDB_CONFIG = {
base_path: "{{ site.baseurl }}"
};
+globalThis.GAMEDB_GROUP_TYPE = "franchise";
diff --git a/tests/collection_detail.test.js b/tests/collection_detail.test.js
deleted file mode 100644
index 700a2725db67..000000000000
--- a/tests/collection_detail.test.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
-import {
- describe,
- test,
- expect,
- beforeEach,
- afterEach,
-} from '@jest/globals';
-
-globalThis.GAMEDB_CONFIG = { base_path: '/GameDB' };
-
-const itemDetail = require('../gh-pages-template/assets/js/item_detail.js');
-globalThis.makeBadge = itemDetail.makeBadge;
-globalThis.addDlRow = itemDetail.addDlRow;
-globalThis.loadItemDetail = itemDetail.loadItemDetail;
-globalThis.renderGameList = itemDetail.renderGameList;
-globalThis.igdbImageUrl = itemDetail.igdbImageUrl;
-globalThis.base_path = '/GameDB';
-globalThis.base_url = 'http://localhost/GameDB';
-
-const { renderCollection } = require('../gh-pages-template/assets/js/collection_detail.js');
-
-const baseDom = `
-
-
-
-
-`;
-
-describe('collection_detail.js', () => {
- beforeEach(() => {
- document.body.innerHTML = baseDom;
- });
-
- afterEach(() => {
- document.body.innerHTML = '';
- });
-
- describe('renderCollection', () => {
- test('sets title and name', () => {
- renderCollection({ name: 'Sonic the Hedgehog' });
- expect(document.title).toContain('Sonic the Hedgehog');
- expect(document.getElementById('collection-name').textContent).toBe('Sonic the Hedgehog');
- });
-
- test('uses fallback title and name when name absent', () => {
- renderCollection({});
- expect(document.title).toContain('Series');
- expect(document.getElementById('collection-name').textContent).toBe('Unknown Series');
- });
-
- test('shows IGDB link when URL provided', () => {
- renderCollection({ name: 'Mario', url: 'https://igdb.com/collections/mario' });
- const link = document.getElementById('collection-igdb-link');
- expect(link.classList.contains('d-none')).toBe(false);
- expect(link.href).toContain('igdb.com');
- });
-
- test('keeps IGDB link hidden when no URL', () => {
- renderCollection({ name: 'Zelda' });
- expect(document.getElementById('collection-igdb-link').classList.contains('d-none')).toBe(true);
- });
-
- test('shows games section when games present', () => {
- renderCollection({
- name: 'Metroid',
- games: [{ id: 1, name: 'Metroid Prime', cover: { url: '//img.igdb.com/t_thumb/c.jpg' } }],
- });
- expect(document.getElementById('collection-games-section').classList.contains('d-none')).toBe(false);
- });
-
- test('keeps games section hidden when no games', () => {
- renderCollection({ name: 'Empty' });
- expect(document.getElementById('collection-games-section').classList.contains('d-none')).toBe(true);
- });
- });
-
- describe('DOMContentLoaded integration', () => {
- test('fires loadItemDetail on DOMContentLoaded', () => {
- globalThis.history.pushState(null, '', '/');
- expect(() => document.dispatchEvent(new Event('DOMContentLoaded'))).not.toThrow();
- });
- });
-});
diff --git a/tests/franchise_detail.test.js b/tests/franchise_detail.test.js
deleted file mode 100644
index 047ecb645e39..000000000000
--- a/tests/franchise_detail.test.js
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
-import {
- describe,
- test,
- expect,
- beforeEach,
- afterEach,
-} from '@jest/globals';
-
-globalThis.GAMEDB_CONFIG = { base_path: '/GameDB' };
-
-const itemDetail = require('../gh-pages-template/assets/js/item_detail.js');
-globalThis.makeBadge = itemDetail.makeBadge;
-globalThis.addDlRow = itemDetail.addDlRow;
-globalThis.loadItemDetail = itemDetail.loadItemDetail;
-globalThis.renderGameList = itemDetail.renderGameList;
-globalThis.igdbImageUrl = itemDetail.igdbImageUrl;
-globalThis.base_path = '/GameDB';
-globalThis.base_url = 'http://localhost/GameDB';
-
-const { renderFranchise } = require('../gh-pages-template/assets/js/franchise_detail.js');
-
-const baseDom = `
-
-
-
-
-`;
-
-describe('franchise_detail.js', () => {
- beforeEach(() => {
- document.body.innerHTML = baseDom;
- });
-
- afterEach(() => {
- document.body.innerHTML = '';
- });
-
- describe('renderFranchise', () => {
- test('sets title and name', () => {
- renderFranchise({ name: 'The Legend of Zelda' });
- expect(document.title).toContain('The Legend of Zelda');
- expect(document.getElementById('franchise-name').textContent).toBe('The Legend of Zelda');
- });
-
- test('uses fallback title and name when name absent', () => {
- renderFranchise({});
- expect(document.title).toContain('Franchise');
- expect(document.getElementById('franchise-name').textContent).toBe('Unknown Franchise');
- });
-
- test('shows IGDB link when URL provided', () => {
- renderFranchise({ name: 'Zelda', url: 'https://igdb.com/franchises/zelda' });
- const link = document.getElementById('franchise-igdb-link');
- expect(link.classList.contains('d-none')).toBe(false);
- expect(link.href).toContain('igdb.com');
- });
-
- test('keeps IGDB link hidden when no URL', () => {
- renderFranchise({ name: 'Mario' });
- expect(document.getElementById('franchise-igdb-link').classList.contains('d-none')).toBe(true);
- });
-
- test('shows games section when games present', () => {
- renderFranchise({
- name: 'Halo',
- games: [{ id: 5, name: 'Halo: CE', cover: { url: '//img.igdb.com/t_thumb/c.jpg' } }],
- });
- expect(document.getElementById('franchise-games-section').classList.contains('d-none')).toBe(false);
- });
-
- test('keeps games section hidden when no games', () => {
- renderFranchise({ name: 'Empty' });
- expect(document.getElementById('franchise-games-section').classList.contains('d-none')).toBe(true);
- });
- });
-
- describe('DOMContentLoaded integration', () => {
- test('fires loadItemDetail on DOMContentLoaded', () => {
- globalThis.history.pushState(null, '', '/');
- expect(() => document.dispatchEvent(new Event('DOMContentLoaded'))).not.toThrow();
- });
- });
-});
diff --git a/tests/game_detail.test.js b/tests/game_detail.test.js
index 57f263eb9d2a..a68253d5155b 100644
--- a/tests/game_detail.test.js
+++ b/tests/game_detail.test.js
@@ -24,11 +24,11 @@ globalThis.makeBadge = itemDetail.makeBadge;
globalThis.addDlRow = itemDetail.addDlRow;
globalThis.loadItemDetail = itemDetail.loadItemDetail;
globalThis.renderGameList = itemDetail.renderGameList;
+globalThis.getRegionFlag = itemDetail.getRegionFlag;
globalThis.base_path = '/GameDB';
globalThis.base_url = 'http://localhost/GameDB';
const {
- getRegionFlag,
renderGameCover,
renderGameBadges,
renderGameRatings,
@@ -79,26 +79,6 @@ describe('game_detail.js', () => {
jest.restoreAllMocks();
});
- describe('getRegionFlag', () => {
- test('returns correct flags for all known regions', () => {
- expect(getRegionFlag('europe')).toBe('πͺπΊ');
- expect(getRegionFlag('north_america')).toBe('πΊπΈ');
- expect(getRegionFlag('australia')).toBe('π¦πΊ');
- expect(getRegionFlag('new_zealand')).toBe('π³πΏ');
- expect(getRegionFlag('japan')).toBe('π―π΅');
- expect(getRegionFlag('china')).toBe('π¨π³');
- expect(getRegionFlag('asia')).toBe('π');
- expect(getRegionFlag('worldwide')).toBe('π');
- expect(getRegionFlag('korea')).toBe('π°π·');
- expect(getRegionFlag('brazil')).toBe('π§π·');
- });
-
- test('returns default globe for unknown region', () => {
- expect(getRegionFlag('unknown')).toBe('π');
- expect(getRegionFlag('')).toBe('π');
- });
- });
-
describe('renderGameCover', () => {
test('shows cover image when URL provided', () => {
renderGameCover({ name: 'Test', cover: { url: '//images.igdb.com/t_thumb/x.jpg' } });
diff --git a/tests/group_detail.test.js b/tests/group_detail.test.js
new file mode 100644
index 000000000000..ad12eda02670
--- /dev/null
+++ b/tests/group_detail.test.js
@@ -0,0 +1,153 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import {
+ describe,
+ test,
+ expect,
+ beforeEach,
+ afterEach,
+} from '@jest/globals';
+
+globalThis.GAMEDB_CONFIG = { base_path: '/GameDB' };
+
+const itemDetail = require('../gh-pages-template/assets/js/item_detail.js');
+globalThis.makeBadge = itemDetail.makeBadge;
+globalThis.addDlRow = itemDetail.addDlRow;
+globalThis.loadItemDetail = itemDetail.loadItemDetail;
+globalThis.renderGameList = itemDetail.renderGameList;
+globalThis.igdbImageUrl = itemDetail.igdbImageUrl;
+globalThis.base_path = '/GameDB';
+globalThis.base_url = 'http://localhost/GameDB';
+
+const { GROUP_CONFIG, renderGroup } = require('../gh-pages-template/assets/js/group_detail.js');
+
+// --- Helpers ---
+
+function makeGroupDom(type) {
+ return `
+
+
+
+
+ `;
+}
+
+/**
+ * Shared renderGroup behaviour tests, run for each supported type.
+ * @param {'collection'|'franchise'} type
+ * @param {{ name: string, defaultTitle: string, defaultName: string }} config
+ */
+function describeRenderGroup(type, config) {
+ describe(`renderGroup β ${type} type`, () => {
+ beforeEach(() => {
+ globalThis.GAMEDB_GROUP_TYPE = type;
+ document.body.innerHTML = makeGroupDom(type);
+ });
+
+ test('sets title and name', () => {
+ renderGroup({ name: config.name });
+ expect(document.title).toContain(config.name);
+ expect(document.getElementById(`${type}-name`).textContent).toBe(config.name);
+ });
+
+ test('uses fallback title and name when name absent', () => {
+ renderGroup({});
+ expect(document.title).toContain(config.defaultTitle);
+ expect(document.getElementById(`${type}-name`).textContent).toBe(config.defaultName);
+ });
+
+ test('shows IGDB link when URL provided', () => {
+ renderGroup({ name: config.name, url: `https://igdb.com/${type}s/test` });
+ const link = document.getElementById(`${type}-igdb-link`);
+ expect(link.classList.contains('d-none')).toBe(false);
+ expect(link.href).toContain('igdb.com');
+ });
+
+ test('keeps IGDB link hidden when no URL', () => {
+ renderGroup({ name: config.name });
+ expect(document.getElementById(`${type}-igdb-link`).classList.contains('d-none')).toBe(true);
+ });
+
+ test('shows games section when games present', () => {
+ renderGroup({
+ name: config.name,
+ games: [{ id: 1, name: 'Test Game', cover: { url: '//img.igdb.com/t_thumb/c.jpg' } }],
+ });
+ expect(document.getElementById(`${type}-games-section`).classList.contains('d-none')).toBe(false);
+ });
+
+ test('keeps games section hidden when no games', () => {
+ renderGroup({ name: config.name });
+ expect(document.getElementById(`${type}-games-section`).classList.contains('d-none')).toBe(true);
+ });
+ });
+}
+
+// --- Tests ---
+
+describe('group_detail.js', () => {
+ afterEach(() => {
+ document.body.innerHTML = '';
+ delete globalThis.GAMEDB_GROUP_TYPE;
+ });
+
+ describe('GROUP_CONFIG', () => {
+ test('defines collection config', () => {
+ expect(GROUP_CONFIG.collection.endpoint).toBe('collections');
+ expect(GROUP_CONFIG.collection.defaultTitle).toBe('Series');
+ expect(GROUP_CONFIG.collection.defaultName).toBe('Unknown Series');
+ });
+
+ test('defines franchise config', () => {
+ expect(GROUP_CONFIG.franchise.endpoint).toBe('franchises');
+ expect(GROUP_CONFIG.franchise.defaultTitle).toBe('Franchise');
+ expect(GROUP_CONFIG.franchise.defaultName).toBe('Unknown Franchise');
+ });
+ });
+
+ describeRenderGroup('collection', {
+ name: 'Sonic the Hedgehog',
+ defaultTitle: 'Series',
+ defaultName: 'Unknown Series',
+ });
+
+ describeRenderGroup('franchise', {
+ name: 'The Legend of Zelda',
+ defaultTitle: 'Franchise',
+ defaultName: 'Unknown Franchise',
+ });
+
+ describe('renderGroup β defaults to collection type when GAMEDB_GROUP_TYPE is unset', () => {
+ beforeEach(() => {
+ document.body.innerHTML = makeGroupDom('collection');
+ });
+
+ test('falls back to collection type', () => {
+ renderGroup({ name: 'Default Series' });
+ expect(document.getElementById('collection-name').textContent).toBe('Default Series');
+ });
+ });
+
+ describe('DOMContentLoaded integration', () => {
+ test('fires loadItemDetail for collection on DOMContentLoaded', () => {
+ globalThis.GAMEDB_GROUP_TYPE = 'collection';
+ globalThis.history.pushState(null, '', '/');
+ expect(() => document.dispatchEvent(new Event('DOMContentLoaded'))).not.toThrow();
+ });
+
+ test('fires loadItemDetail for franchise on DOMContentLoaded', () => {
+ globalThis.GAMEDB_GROUP_TYPE = 'franchise';
+ globalThis.history.pushState(null, '', '/');
+ expect(() => document.dispatchEvent(new Event('DOMContentLoaded'))).not.toThrow();
+ });
+
+ test('fires loadItemDetail with default collection type when GAMEDB_GROUP_TYPE is unset', () => {
+ globalThis.history.pushState(null, '', '/');
+ expect(() => document.dispatchEvent(new Event('DOMContentLoaded'))).not.toThrow();
+ });
+ });
+});
diff --git a/tests/item_detail.test.js b/tests/item_detail.test.js
index a6608340cc00..392d8f14930c 100644
--- a/tests/item_detail.test.js
+++ b/tests/item_detail.test.js
@@ -16,6 +16,7 @@ globalThis.GAMEDB_CONFIG = { base_path: '/GameDB' };
const {
igdbImageUrl,
+ getRegionFlag,
makeBadge,
addDlRow,
showError,
@@ -70,6 +71,26 @@ describe('item_detail.js', () => {
});
});
+ describe('getRegionFlag', () => {
+ test('returns correct flags for all known regions', () => {
+ expect(getRegionFlag('europe')).toBe('πͺπΊ');
+ expect(getRegionFlag('north_america')).toBe('πΊπΈ');
+ expect(getRegionFlag('australia')).toBe('π¦πΊ');
+ expect(getRegionFlag('new_zealand')).toBe('π³πΏ');
+ expect(getRegionFlag('japan')).toBe('π―π΅');
+ expect(getRegionFlag('china')).toBe('π¨π³');
+ expect(getRegionFlag('asia')).toBe('π');
+ expect(getRegionFlag('worldwide')).toBe('π');
+ expect(getRegionFlag('korea')).toBe('π°π·');
+ expect(getRegionFlag('brazil')).toBe('π§π·');
+ });
+
+ test('returns default globe for unknown region', () => {
+ expect(getRegionFlag('unknown')).toBe('π');
+ expect(getRegionFlag('')).toBe('π');
+ });
+ });
+
describe('makeBadge', () => {
test('creates badge with provided class', () => {
const badge = makeBadge('Action', 'bg-primary');
diff --git a/tests/platform_detail.test.js b/tests/platform_detail.test.js
index 1593ddc1e908..9420e4d49f31 100644
--- a/tests/platform_detail.test.js
+++ b/tests/platform_detail.test.js
@@ -18,11 +18,11 @@ globalThis.makeBadge = itemDetail.makeBadge;
globalThis.addDlRow = itemDetail.addDlRow;
globalThis.loadItemDetail = itemDetail.loadItemDetail;
globalThis.renderGameList = itemDetail.renderGameList;
+globalThis.getRegionFlag = itemDetail.getRegionFlag;
globalThis.base_path = '/GameDB';
globalThis.base_url = 'http://localhost/GameDB';
const {
- getRegionFlag,
renderPlatformLogo,
renderPlatformBadges,
renderPlatformMetadata,
@@ -59,26 +59,6 @@ describe('platform_detail.js', () => {
document.body.innerHTML = '';
});
- describe('getRegionFlag', () => {
- test('returns correct flags for all known regions', () => {
- expect(getRegionFlag('europe')).toBe('πͺπΊ');
- expect(getRegionFlag('north_america')).toBe('πΊπΈ');
- expect(getRegionFlag('australia')).toBe('π¦πΊ');
- expect(getRegionFlag('new_zealand')).toBe('π³πΏ');
- expect(getRegionFlag('japan')).toBe('π―π΅');
- expect(getRegionFlag('china')).toBe('π¨π³');
- expect(getRegionFlag('asia')).toBe('π');
- expect(getRegionFlag('worldwide')).toBe('π');
- expect(getRegionFlag('korea')).toBe('π°π·');
- expect(getRegionFlag('brazil')).toBe('π§π·');
- });
-
- test('returns default globe for unknown region', () => {
- expect(getRegionFlag('unknown')).toBe('π');
- expect(getRegionFlag('')).toBe('π');
- });
- });
-
describe('renderPlatformLogo', () => {
test('renders logo when URL provided', () => {
renderPlatformLogo({