From 4f527c77cfa276489f446aad7cd5a61984c52ba5 Mon Sep 17 00:00:00 2001 From: Dominic Kleiner <36665423+dmncklnr@users.noreply.github.com> Date: Sat, 17 Oct 2026 10:22:07 +0200 Subject: [PATCH] Fix empty collections and watched counts --- scripts/collections.js | 2 +- tests/collection-progress.test.cjs | 72 ++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 tests/collection-progress.test.cjs diff --git a/scripts/collections.js b/scripts/collections.js index 417a4cd..79ecb6f 100644 --- a/scripts/collections.js +++ b/scripts/collections.js @@ -613,7 +613,7 @@ const totalItemCount = item.RecursiveItemCount || item.ChildCount || 0; const playedItemCount = totalItemCount - unplayedItemCount; - const isVideoCollection = document.querySelector('.libraryPage:not(.hide) .collectionItems:has(.card[data-mediatype="Video"])').length > 0; + const isVideoCollection = document.querySelector('.libraryPage:not(.hide) .collectionItems:has(.card[data-mediatype="Video"])') !== null; const progressText = isVideoCollection ? 'watched' : 'complete'; const incompleteText = isVideoCollection ? 'unwatched' : 'incomplete'; diff --git a/tests/collection-progress.test.cjs b/tests/collection-progress.test.cjs new file mode 100644 index 0000000..620f597 --- /dev/null +++ b/tests/collection-progress.test.cjs @@ -0,0 +1,72 @@ +// Run: node --test tests/collection-progress.test.cjs +// Exercise the actual progress renderer with DOM-shaped elements, not a copy of it. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); +const vm = require('node:vm'); + +const source = fs.readFileSync(path.join(__dirname, '../scripts/collections.js'), 'utf8'); +const renderer = source.match(/^([ \t]*)async function updateCollectionPage\(item\) \{[\s\S]*?^\1\}/m)?.[0]; +assert.ok(renderer, 'Collection progress renderer not found'); + +function fixture({ video = false, active = true } = {}) { + const page = { dataset: {} }; + const info = { children: [], appendChild(child) { this.children.push(child); } }; + // querySelector returns one Element or null. An Element has no list length. + const videoCollection = video ? { tagName: 'DIV' } : null; + const document = { + querySelector(selector) { + if (selector === '.libraryPage:not(.hide)') return active ? page : null; + if (selector.endsWith(' .collectionItems:has(.card[data-mediatype="Video"])')) return videoCollection; + if (selector.endsWith(' .itemMiscInfo-primary')) return info; + throw new Error('Unexpected selector: ' + selector); + }, + createElement(tagName) { return { tagName, className: '', textContent: '' }; } + }; + const render = vm.runInNewContext(renderer + '\nupdateCollectionPage;', { document }, { timeout: 1000 }); + return { page, render, texts: () => info.children.map(child => child.textContent) }; +} + +test('an empty collection finishes without throwing or adding progress counts', async () => { + const f = fixture(); + await f.render({ ChildCount: 0, UserData: { UnplayedItemCount: 0 } }); + assert.deepEqual(f.texts(), []); + assert.equal(f.page.dataset.kefinCollectionPage, 'true'); +}); + +test('video collections show watched and unwatched counts', async () => { + const f = fixture({ video: true }); + await f.render({ RecursiveItemCount: 3, UserData: { UnplayedItemCount: 2 } }); + assert.deepEqual(f.texts(), ['1 watched', '2 unwatched']); +}); + +test('collections without video cards retain complete and incomplete counts', async () => { + const f = fixture(); + await f.render({ ChildCount: 4, UserData: { UnplayedItemCount: 2 } }); + assert.deepEqual(f.texts(), ['2 complete', '2 incomplete']); +}); + +test('fully watched video collections add no extra progress counts', async () => { + const f = fixture({ video: true }); + await f.render({ ChildCount: 3, UserData: { UnplayedItemCount: 0 } }); + assert.deepEqual(f.texts(), []); + assert.equal(f.page.dataset.kefinCollectionPage, 'true'); +}); + +test('repeated rendering does not duplicate the progress counts', async () => { + const f = fixture({ video: true }); + const item = { ChildCount: 2, UserData: { UnplayedItemCount: 1 } }; + await f.render(item); + const first = f.texts(); + await f.render(item); + assert.equal(first.length, 2); + assert.deepEqual(f.texts(), first); +}); + +test('leaving the collection page does not add progress counts', async () => { + const f = fixture({ active: false, video: true }); + await f.render({ ChildCount: 2, UserData: { UnplayedItemCount: 1 } }); + assert.deepEqual(f.texts(), []); + assert.equal(f.page.dataset.kefinCollectionPage, undefined); +});