Files
embyhomescreenedit/tests/run-tests.js
T
2026-04-27 21:54:20 +12:00

557 lines
13 KiB
JavaScript

import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import {
applySectionStandards,
createBoxSetSection,
createRecentlyWatchedSection,
getWatchlistLabelsForTarget
} from '../src/lib/constants.js';
import { normalizeLookupItem, rankRecommendationResults } from '../src/lib/collection-tools.js';
import * as embyUserCache from '../src/lib/server/emby-user-cache.js';
const tests = [];
function test(name, fn) {
tests.push({ name, fn });
}
function withTempCache(testFn) {
const tempDir = mkdtempSync(join(tmpdir(), 'homescreenpal-cache-'));
const cachePath = join(tempDir, 'emby-users.db');
process.env.EMBY_USER_CACHE_DB_PATH = cachePath;
try {
return testFn(cachePath);
} finally {
delete process.env.EMBY_USER_CACHE_DB_PATH;
rmSync(tempDir, { recursive: true, force: true });
}
}
test('watchlist labels use the target Emby name for new watchlists', () => {
const labels = getWatchlistLabelsForTarget(
{
Name: "Matt's Watchlist",
CustomName: "Matt's Watchlist",
Query: { IsFavorite: true }
},
{
name: 'User 11',
embyName: 'Bob',
sections: []
}
);
assert.equal(labels.Name, "Bob's Watchlist");
assert.equal(labels.CustomName, "Bob's Watchlist");
});
test('watchlist labels preserve an existing matching target watchlist', () => {
const labels = getWatchlistLabelsForTarget(
{
Name: "Matt's Watchlist",
CustomName: "Matt's Watchlist",
Query: { IsFavorite: true }
},
{
name: 'User 14',
embyName: 'Bob',
sections: [
{
Name: "Bob's Watchlist",
CustomName: "Bob's Watchlist",
Query: { IsFavorite: true }
}
]
}
);
assert.equal(labels.Name, "Bob's Watchlist");
assert.equal(labels.CustomName, "Bob's Watchlist");
});
test('watchlist labels correct an existing mismatched target watchlist', () => {
const labels = getWatchlistLabelsForTarget(
{
Name: "Matt's Watchlist",
CustomName: "Matt's Watchlist",
Query: { IsFavorite: true }
},
{
name: 'User 14',
embyName: 'Bob',
sections: [
{
Name: "Matt's Watchlist",
CustomName: "Matt's Watchlist",
Query: { IsFavorite: true }
}
]
}
);
assert.equal(labels.Name, "Bob's Watchlist");
assert.equal(labels.CustomName, "Bob's Watchlist");
});
test('cached Emby users round-trip through the local SQLite cache', () => {
withTempCache(() => {
const written = embyUserCache.writeCachedEmbyUsers([
{ embyGuid: 'ABC-123', name: 'Matt' },
{ embyGuid: 'def456', name: 'Bob' }
]);
const readBack = embyUserCache.readCachedEmbyUsers();
assert.equal(written.users.length, 2);
assert.deepEqual(readBack.users, [
{ embyGuid: 'def456', name: 'Bob' },
{ embyGuid: 'abc123', name: 'Matt' }
]);
assert.ok(readBack.lastSyncedAt);
});
});
test('cached Emby users override DB display names without losing the original DB name', () => {
withTempCache(() => {
embyUserCache.writeCachedEmbyUsers([{ embyGuid: 'abc123', name: 'Matt' }]);
const enriched = embyUserCache.applyCachedEmbyNames([
{ id: 11, name: 'User 11', embyGuid: 'abc123' },
{ id: 12, name: 'Already Good', embyGuid: 'zzz999' }
]);
assert.equal(enriched.users[0].name, 'Matt');
assert.equal(enriched.users[0].embyName, 'Matt');
assert.equal(enriched.users[0].dbName, 'User 11');
assert.equal(enriched.users[1].name, 'Already Good');
assert.equal(enriched.cache.matchedCount, 1);
});
});
test('recently watched section template uses played-content defaults', () => {
const section = createRecentlyWatchedSection('abc123', 'Matt');
assert.equal(section.UserId, 'abc123');
assert.equal(section.Name, 'Recently Watched - Matt');
assert.equal(section.CustomName, 'Recently Watched - Matt');
assert.equal(section.SectionType, 'items');
assert.equal(section.SortBy, 'DatePlayed');
assert.equal(section.SortOrder, 'Descending');
assert.deepEqual(section.ItemTypes, ['Movie', 'Series']);
assert.equal(section.Query.IsPlayed, true);
});
test('box set section template links a created collection', () => {
const section = createBoxSetSection('abc123', 'Recommended for Matt', 'boxset-55');
assert.equal(section.UserId, 'abc123');
assert.equal(section.SectionType, 'boxset');
assert.equal(section.Name, 'Recommended for Matt');
assert.equal(section.CustomName, 'Recommended for Matt');
assert.equal(section.ParentId, 'boxset-55');
assert.deepEqual(section.ParentItem, {
Name: 'Recommended for Matt',
Id: 'boxset-55'
});
});
test('section standards normalize up next label', () => {
const normalized = applySectionStandards(
{
Name: 'Resume / Up Next',
CustomName: 'Resume / Up Next',
SectionType: 'resume'
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, 'Up Next');
assert.equal(normalized.CustomName, 'Up Next');
});
test('section standards normalize watchlist label for target user', () => {
const normalized = applySectionStandards(
{
Name: "Matt's Watchlist",
CustomName: "Matt's Watchlist",
SectionType: 'items',
Query: { IsFavorite: true }
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, "Bob's Watchlist");
assert.equal(normalized.CustomName, "Bob's Watchlist");
});
test('section standards keep new to emby fixed to date added descending', () => {
const normalized = applySectionStandards(
{
Name: 'New To Emby',
CustomName: 'New To Emby',
SectionType: 'items',
SortBy: 'ProductionYear,PremiereDate,SortName',
SortOrder: 'Ascending'
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, 'New to Emby');
assert.equal(normalized.CustomName, 'New to Emby');
assert.equal(normalized.SortBy, 'DateLastContentAdded,SortName');
assert.equal(normalized.SortOrder, 'Descending');
});
test('section standards randomize regular curated sections', () => {
const normalized = applySectionStandards(
{
Name: 'Crime / Drama Shows',
CustomName: 'Crime / Drama Shows',
SectionType: 'items',
SortBy: 'ProductionYear,PremiereDate,SortName',
SortOrder: 'Ascending',
Query: { GenreIds: ['4910', '62'] }
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.SortBy, 'Random');
assert.equal(normalized.SortOrder, 'Descending');
});
test('section standards preserve recently watched ordering semantics', () => {
const normalized = applySectionStandards(
{
Name: 'Recently Watched - Matt',
CustomName: 'Recently Watched - Matt',
SectionType: 'items',
SortBy: 'Random',
SortOrder: 'Ascending',
Query: { IsPlayed: true }
},
{
embyName: 'Bob',
sections: []
}
);
assert.equal(normalized.Name, 'Recently Watched - Bob');
assert.equal(normalized.CustomName, 'Recently Watched - Bob');
assert.equal(normalized.SortBy, 'DatePlayed');
assert.equal(normalized.SortOrder, 'Descending');
});
test('normalizeLookupItem preserves already-normalized recommendation items', () => {
const normalized = normalizeLookupItem({
id: 'pick-1',
name: 'Alpha',
type: 'Movie',
overview: 'Overview',
year: 1998,
communityRating: 7.4,
providerIds: { Tmdb: '123' },
genres: ['Comedy']
});
assert.deepEqual(normalized, {
id: 'pick-1',
name: 'Alpha',
type: 'Movie',
overview: 'Overview',
year: 1998,
communityRating: 7.4,
providerIds: { Tmdb: '123' },
genres: ['Comedy'],
parentId: null
});
});
test('recommendation ranking deduplicates and prioritizes overlaps across seeds', () => {
const ranked = rankRecommendationResults(
['seed-a', 'seed-b'],
[
[
{ Id: 'pick-1', Name: 'Alpha', Type: 'Movie' },
{ Id: 'pick-2', Name: 'Bravo', Type: 'Movie' }
],
[
{ Id: 'pick-2', Name: 'Bravo', Type: 'Movie' },
{ Id: 'pick-3', Name: 'Charlie', Type: 'Movie' }
]
],
5
);
assert.deepEqual(
ranked.map((item) => item.id),
['pick-2', 'pick-1', 'pick-3']
);
assert.equal(ranked[0].matchCount, 2);
});
test('recommendation ranking supports already-normalized result items', () => {
const ranked = rankRecommendationResults(
['seed-a'],
[
[
{ id: 'pick-1', name: 'Alpha', type: 'Movie' },
{ id: 'pick-2', name: 'Bravo', type: 'Movie' }
]
],
5
);
assert.deepEqual(
ranked.map((item) => item.id),
['pick-1', 'pick-2']
);
});
test('recommendation ranking filters results to the selected seed media type', () => {
const ranked = rankRecommendationResults(
['seed-a'],
[
[
{ Id: 'movie-1', Name: 'Movie Match', Type: 'Movie', Genres: ['Drama'] },
{ Id: 'series-1', Name: 'Series Match', Type: 'Series', Genres: ['Drama'] }
]
],
{
limit: 5,
allowedTypes: ['Movie']
}
);
assert.deepEqual(
ranked.map((item) => item.id),
['movie-1']
);
});
test('recommendation ranking heavily favors TMDB similar and recommendation sources over discover', () => {
const ranked = rankRecommendationResults(
['seed-a'],
[
{
items: [{ Id: 'pick-seed-like', Name: 'Seed Like', Type: 'Movie', Genres: ['Drama', 'Romance'] }],
sourceWeight: 6
},
{
items: [{ Id: 'pick-discover', Name: 'Broad Discover', Type: 'Movie', Genres: ['Drama'] }],
sourceWeight: 1
},
{
items: [{ Id: 'pick-discover', Name: 'Broad Discover', Type: 'Movie', Genres: ['Drama'] }],
sourceWeight: 1
},
{
items: [{ Id: 'pick-discover', Name: 'Broad Discover', Type: 'Movie', Genres: ['Drama'] }],
sourceWeight: 1
}
],
5
);
assert.deepEqual(
ranked.map((item) => item.id),
['pick-seed-like', 'pick-discover']
);
});
test('recommendation ranking excludes watched items passed in options', () => {
const ranked = rankRecommendationResults(
['seed-a'],
[
[
{ Id: 'watched-1', Name: 'Already Seen', Type: 'Movie' },
{ Id: 'pick-1', Name: 'Fresh Pick', Type: 'Movie' }
],
[
{ Id: 'pick-1', Name: 'Fresh Pick', Type: 'Movie' },
{ Id: 'pick-2', Name: 'Another Pick', Type: 'Movie' }
]
],
{
limit: 5,
excludeIds: ['watched-1']
}
);
assert.deepEqual(
ranked.map((item) => item.id),
['pick-1', 'pick-2']
);
});
test('recommendation ranking penalizes horror and sci-fi mismatches for romance drama seeds', () => {
const ranked = rankRecommendationResults(
['seed-a'],
[
[
{
Id: 'pick-warm',
Name: 'Quiet Hearts',
Type: 'Movie',
Overview: 'A love story about family, friendship, and second chances.',
Genres: ['Romance', 'Drama']
},
{
Id: 'pick-mismatch',
Name: 'Night Terrors',
Type: 'Movie',
Overview: 'A horror thriller about murder, zombies, and survival.',
Genres: ['Horror', 'Science Fiction', 'Thriller']
}
]
],
{
limit: 5,
seeds: [
{
Id: 'seed-a',
Name: 'The English Patient',
Type: 'Movie',
Genres: ['Romance', 'Drama']
}
]
}
);
assert.deepEqual(
ranked.map((item) => item.id),
['pick-warm', 'pick-mismatch']
);
assert.ok(ranked[0].affinityScore > ranked[1].affinityScore);
});
test('recommendation ranking ignores duplicate items within a single seed result set', () => {
const ranked = rankRecommendationResults(
['seed-a'],
[
[
{ Id: 'pick-1', Name: 'Alpha', Type: 'Movie' },
{ Id: 'pick-1', Name: 'Alpha', Type: 'Movie' },
{ Id: 'pick-2', Name: 'Bravo', Type: 'Movie' }
]
],
5
);
assert.deepEqual(
ranked.map((item) => item.id),
['pick-1', 'pick-2']
);
assert.equal(ranked[0].matchCount, 1);
});
test('classic comfort profile filters modern and mismatched picks while promoting older warm titles', () => {
const ranked = rankRecommendationResults(
['seed-a', 'seed-b'],
[
[
{
Id: 'pick-classic',
Name: 'Warm Hearts',
Type: 'Movie',
Overview: 'A romantic family comedy about friendship, second chances, and love.',
ProductionYear: 1998,
CommunityRating: 7.6,
Genres: ['Romance', 'Comedy']
},
{
Id: 'pick-modern',
Name: 'Future War',
Type: 'Movie',
Overview: 'A modern action thriller about combat and alien invasion.',
ProductionYear: 2021,
CommunityRating: 7.8,
Genres: ['Action', 'Science Fiction']
}
],
[
{
Id: 'pick-classic',
Name: 'Warm Hearts',
Type: 'Movie',
Overview: 'A romantic family comedy about friendship, second chances, and love.',
ProductionYear: 1998,
CommunityRating: 7.6,
Genres: ['Romance', 'Comedy']
},
{
Id: 'pick-animation',
Name: 'Toon Town',
Type: 'Movie',
Overview: 'An animated adventure about a magical family.',
ProductionYear: 2002,
CommunityRating: 8.0,
Genres: ['Animation', 'Family']
}
]
],
{
limit: 5,
profile: 'classicComfort',
seeds: [
{
Id: 'seed-a',
Name: 'Sleepless Style',
Type: 'Movie',
Overview: 'A romantic comedy about love and second chances.',
ProductionYear: 1993,
CommunityRating: 7.1,
Genres: ['Romance', 'Comedy']
},
{
Id: 'seed-b',
Name: 'Family Ties',
Type: 'Movie',
Overview: 'A family friendship story with warmth.',
ProductionYear: 1995,
CommunityRating: 7.0,
Genres: ['Comedy', 'Drama']
}
]
}
);
assert.deepEqual(ranked.map((item) => item.id), ['pick-classic']);
assert.equal(ranked[0].matchCount, 2);
assert.ok(ranked[0].styleScore >= 16);
assert.equal(ranked[0].qualityScore, 76);
});
let failures = 0;
for (const { name, fn } of tests) {
try {
await fn();
console.log(`PASS ${name}`);
} catch (error) {
failures++;
console.error(`FAIL ${name}`);
console.error(error);
}
}
if (failures > 0) {
console.error(`\n${failures} test(s) failed.`);
process.exit(1);
}
console.log(`\n${tests.length} test(s) passed.`);