Discover request surface — taste-aware, rotating, snoozable, tag-targeted (milestone #268) #116

Merged
bvandeusen merged 14 commits from dev into main 2026-08-03 08:38:25 -04:00
2 changed files with 22 additions and 9 deletions
Showing only changes of commit 18a61f1065 - Show all commits
@@ -98,13 +98,14 @@ data class SuggestionSnoozeRef(
* better than rendering an empty line. * better than rendering an empty line.
*/ */
fun returnsIn(nowMs: Long = System.currentTimeMillis()): String { fun returnsIn(nowMs: Long = System.currentTimeMillis()): String {
val untilMs = runCatching { Instant.parse(snoozedUntil).toEpochMilliseconds() } val remainingMs = runCatching { Instant.parse(snoozedUntil).toEpochMilliseconds() }
.getOrNull() ?: return "shortly" .getOrNull()?.minus(nowMs)
// Already lapsed by our clock, yet the server still returned it — the // Two ways to have nothing to state: an unparseable timestamp, or one
// two disagree. Say something plausible rather than "today", which // already lapsed by our clock though the server still returned the row
// would read as a real prediction. // (the two disagree). Neither is "today", which would read as a real
if (untilMs <= nowMs) return "shortly" // prediction.
val days = ((untilMs - nowMs).toDouble() / MILLIS_PER_DAY).roundToInt() if (remainingMs == null || remainingMs <= 0) return "shortly"
val days = (remainingMs.toDouble() / MILLIS_PER_DAY).roundToInt()
return when { return when {
days < 1 -> "today" days < 1 -> "today"
days == 1 -> "tomorrow" days == 1 -> "tomorrow"
+14 -2
View File
@@ -21,8 +21,16 @@ vi.mock('$lib/api/lidarr', () => ({
createLidarrSearchQuery: vi.fn() createLidarrSearchQuery: vi.fn()
})); }));
// SuggestionFeed reaches for the snooze surface too (#2375). These are
// stubbed here even though this page-level suite asserts nothing about
// snoozing: a factory-shaped module mock must export everything the
// component tree imports, or rendering the feed throws before any
// assertion runs.
vi.mock('$lib/api/suggestions', () => ({ vi.mock('$lib/api/suggestions', () => ({
createSuggestionsQuery: vi.fn() createSuggestionsQuery: vi.fn(),
createSnoozesQuery: vi.fn(),
snoozeSuggestion: vi.fn().mockResolvedValue(undefined),
unsnoozeSuggestion: vi.fn().mockResolvedValue(undefined)
})); }));
vi.mock('$lib/api/requests', () => ({ vi.mock('$lib/api/requests', () => ({
@@ -42,11 +50,12 @@ vi.mock('@tanstack/svelte-query', async (importOriginal) => {
import DiscoverPage from './+page.svelte'; import DiscoverPage from './+page.svelte';
import { createLidarrSearchQuery } from '$lib/api/lidarr'; import { createLidarrSearchQuery } from '$lib/api/lidarr';
import { createSuggestionsQuery } from '$lib/api/suggestions'; import { createSuggestionsQuery, createSnoozesQuery } from '$lib/api/suggestions';
import { createRequest } from '$lib/api/requests'; import { createRequest } from '$lib/api/requests';
const mockedCreateQuery = createLidarrSearchQuery as ReturnType<typeof vi.fn>; const mockedCreateQuery = createLidarrSearchQuery as ReturnType<typeof vi.fn>;
const mockedCreateSuggestionsQuery = createSuggestionsQuery as ReturnType<typeof vi.fn>; const mockedCreateSuggestionsQuery = createSuggestionsQuery as ReturnType<typeof vi.fn>;
const mockedCreateSnoozesQuery = createSnoozesQuery as ReturnType<typeof vi.fn>;
const mockedCreateRequest = createRequest as ReturnType<typeof vi.fn>; const mockedCreateRequest = createRequest as ReturnType<typeof vi.fn>;
function result(over: Partial<LidarrSearchResult> = {}): LidarrSearchResult { function result(over: Partial<LidarrSearchResult> = {}): LidarrSearchResult {
@@ -69,6 +78,9 @@ beforeEach(() => {
// Default: empty suggestion feed so its empty-state copy renders without // Default: empty suggestion feed so its empty-state copy renders without
// interfering with search-mode tests. // interfering with search-mode tests.
mockedCreateSuggestionsQuery.mockReturnValue(mockQuery({ data: [] })); mockedCreateSuggestionsQuery.mockReturnValue(mockQuery({ data: [] }));
// Nothing parked, which keeps the feed's empty-state copy on the
// "no signal yet" branch that this suite's assertions expect.
mockedCreateSnoozesQuery.mockReturnValue(mockQuery({ data: [] }));
}); });
afterEach(() => { afterEach(() => {