feat(play-next): add series and studio rows
Build, Test & Push Frontend / quality-check (push) Successful in 34s
Build, Test & Push Frontend / docker (push) Successful in 23s

This commit is contained in:
lobo committed 2026-10-06 20:22:53 +02:00
1 parent df3d6d2537
commit e041c71cf8
5 files changed
+41 -8

No files matched your search

+7 -1
View File
@@ -27,7 +27,7 @@ const BUDGETS = [
// games without that data.
const MOODS = ['ANY', 'STORY', 'COOP', 'CHILL', 'ACTION', 'SCARY', 'SPEEDRUN'];
// The rows in the order Core sends them; RANDOM is the mood row.
const ROW_ORDER = ['NEXT_UP', 'SIMILAR', 'LONGEST', 'RANDOM'];
const ROW_ORDER = ['NEXT_UP', 'SERIES', 'SIMILAR', 'STUDIO', 'LONGEST', 'RANDOM'];
// The backend reads only this many excludes. With a bigger backlog the oldest seen games can come back.
const MAX_EXCLUDES = 100;
const ROW_CLASS = 'flex gap-4 overflow-x-auto snap-x snap-mandatory pb-2 sm:grid sm:grid-cols-3 sm:overflow-visible';
@@ -253,6 +253,12 @@ const reasonText = (reason, t, language) => {
playtime: formatPlaytime(reason.anchorPlaytimeMinutes),
});
}
if (reason?.source === 'SERIES') {
return t('PLAY_NEXT.REASON.SERIES', {name: reason.seriesName});
}
if (reason?.source === 'STUDIO') {
return t('PLAY_NEXT.REASON.STUDIO', {name: reason.studioName});
}
if (reason?.source === 'LONGEST') {
return t('PLAY_NEXT.REASON.LONGEST', {date: formatDate(reason.firstSeenAt, language, {dateStyle: 'medium'})});
}
+21 -3
View File
@@ -18,7 +18,9 @@ const game = (appid, extra = {}) => ({
const row = (source, ...games) => ({source, games});
const ALL = () => [
row('NEXT_UP', game(1), game(2)),
row('SERIES', game(6, {reason: {source: 'SERIES', seriesName: 'Mass Effect'}})),
row('SIMILAR', game(3, {reason: {source: 'SIMILAR', anchorAppid: 9, anchorName: 'Celeste', anchorPlaytimeMinutes: 600}})),
row('STUDIO', game(8, {reason: {source: 'STUDIO', studioName: 'BioWare'}})),
row('LONGEST', game(4, {reason: {source: 'LONGEST', firstSeenAt: '2024-03-12T10:00:00Z'}})),
row('RANDOM', game(5, {reason: {source: 'RANDOM'}})),
];
@@ -75,8 +77,8 @@ describe('PlayNextPage', () => {
await screen.findByText('Game 1');
expect(screen.getAllByRole('heading', {level: 2}).map((h) => h.textContent)).toEqual([
'PLAY_NEXT.CATEGORY.NEXT_UP', 'PLAY_NEXT.CATEGORY.SIMILAR', 'PLAY_NEXT.CATEGORY.LONGEST',
'PLAY_NEXT.CATEGORY.RANDOM']);
'PLAY_NEXT.CATEGORY.NEXT_UP', 'PLAY_NEXT.CATEGORY.SERIES', 'PLAY_NEXT.CATEGORY.SIMILAR',
'PLAY_NEXT.CATEGORY.STUDIO', 'PLAY_NEXT.CATEGORY.LONGEST', 'PLAY_NEXT.CATEGORY.RANDOM']);
const params = pickerUrls()[0];
expect(params.has('count')).toBe(false);
expect(params.has('category')).toBe(false);
@@ -175,6 +177,20 @@ describe('PlayNextPage', () => {
expect(pickerUrls()[2].has('mood')).toBe(false);
});
it('rerolls the series row with category=SERIES and without a mood', async () => {
mockRows(ALL(), {SERIES: [row('SERIES', game(10))]});
renderPage();
await screen.findByText('Game 1');
fireEvent.click(screen.getByRole('button', {name: 'PLAY_NEXT.MOOD.STORY'}));
await screen.findByText('Game 5');
fireEvent.click(rerollOf('PLAY_NEXT.CATEGORY.SERIES'));
await screen.findByText('Game 10');
expect(pickerUrls()[2].get('category')).toBe('SERIES');
expect(pickerUrls()[2].has('mood')).toBe(false);
});
it('starts a row over when it has no unseen game left', async () => {
mockBackend((params) => {
if (!params.get('category')) return ALL();
@@ -228,12 +244,14 @@ describe('PlayNextPage', () => {
expect(await screen.findByText('PLAY_NEXT.EMPTY_MOOD')).toBeTruthy();
});
it('shows a reason line for the similar and longest rows only', async () => {
it('shows a reason line for the series, similar, studio and longest rows only', async () => {
mockRows();
renderPage();
expect(await screen.findByText('PLAY_NEXT.REASON.SIMILAR')).toBeTruthy();
expect(screen.getByText('PLAY_NEXT.REASON.SERIES')).toBeTruthy();
expect(screen.getByText('PLAY_NEXT.REASON.STUDIO')).toBeTruthy();
expect(screen.getByText('PLAY_NEXT.REASON.LONGEST')).toBeTruthy();
expect(screen.queryByText(/PLAY_NEXT\.REASON\.(RANDOM|NEXT_UP)/)).toBeNull();
});