fix(native): keep widget refreshes after resets and don't claim nothing is upcoming

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-05 22:34:42 -04:00
1 parent 6b56e3eff3
commit 45667f3540
14 files changed
+166 -6

No files matched your search

+104 -1
View File
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
const CACHED_WIDGET_IMAGE_RE = /^file:\/\/\/group\/cw_/;
type LibraryResult = { items: Array<Record<string, unknown>> };
type LibraryResult = { items: Array<Record<string, unknown>>; nextCursor?: string | null };
type SnapshotProps = Record<string, unknown>;
type TimelineEntry = { date?: Date; props?: Record<string, unknown> };
@@ -373,6 +373,7 @@ describe("refreshWidgets", () => {
backdropPath: "/c.jpg",
},
],
nextCursor: null,
});
const { refreshWidgets } = await loadWidgetsModule();
@@ -419,6 +420,87 @@ describe("refreshWidgets", () => {
expect(upcomingWidget.updateSnapshot).not.toHaveBeenCalled();
});
test("writes an Upcoming timeline that ends with an open-the-app prompt when the list was truncated", async () => {
// Local time, so the Today/Tomorrow boundaries don't depend on the machine's timezone.
vi.setSystemTime(new Date(2026, 2, 24, 12));
upcoming.mockResolvedValue({
items: [
{
titleId: "movie-1",
titleName: "A Movie",
titleType: "movie",
date: "2026-03-24",
episodeCount: 1,
backdropPath: "/a.jpg",
},
{
titleId: "show-1",
titleName: "A Show",
titleType: "tv",
date: "2026-03-26",
seasonNumber: 2,
episodeNumber: 1,
episodeCount: 8,
backdropPath: "/b.jpg",
},
{
titleId: "show-2",
titleName: "Another Show",
titleType: "tv",
date: "2026-04-10",
seasonNumber: 1,
episodeNumber: 4,
episodeCount: 1,
backdropPath: "/c.jpg",
},
],
nextCursor: "more",
});
const { refreshWidgets } = await loadWidgetsModule();
await refreshWidgets();
const entries = upcomingWidget.updateTimeline.mock.calls[0]?.[0];
expect(entries).toHaveLength(7);
expect(entries.map((entry) => entry.props?.dateLabel)).toEqual([
"Today",
"Tomorrow",
"Today",
"Apr 10",
"Tomorrow",
"Today",
"",
]);
expect(entries.map((entry) => entry.props?.titleId)).toEqual([
"movie-1",
"show-1",
"show-1",
"show-2",
"show-2",
"show-2",
"",
]);
expect(entries.map((entry) => entry.props?.episodeLabel)).toEqual([
"Movie",
"S2 · 8 episodes",
"S2 · 8 episodes",
"S1 E4",
"S1 E4",
"S1 E4",
"",
]);
const last = entries[6]?.props;
expect(last?.titleName).toBe("");
expect(last?.emptyLabel).toBe("Open Sofa to see what's next");
expect(entries[6]?.date).toEqual(new Date(2026, 3, 11));
// Each item's artwork is downloaded once, however many entries show it.
expect(downloadWidgetImage).toHaveBeenCalledTimes(3);
expect(entries[1]?.props?.imageFilePath).toBe(entries[2]?.props?.imageFilePath);
expect(upcomingWidget.updateSnapshot).not.toHaveBeenCalled();
});
test("is a no-op outside iOS", async () => {
platform.OS = "android";
@@ -498,6 +580,27 @@ describe("widget refresh ordering", () => {
);
});
test("a refresh requested after a reset runs after it, not merged into one queued before it", async () => {
const pending = deferred<LibraryResult>();
continueWatching.mockReturnValueOnce(pending.promise);
continueWatching.mockResolvedValue({ items: [watchingItem] });
const { refreshWidgets, resetWidgets } = await loadWidgetsModule();
const a = refreshWidgets();
await vi.waitFor(() => expect(continueWatching).toHaveBeenCalledTimes(1));
const b = refreshWidgets();
const reset = resetWidgets();
const c = refreshWidgets();
expect(c).not.toBe(b);
pending.resolve({ items: [watchingItem] });
await Promise.all([a, b, reset, c]);
expect(continueWatching).toHaveBeenCalledTimes(3);
const lastTimeline = continueWatchingWidget.updateTimeline.mock.invocationCallOrder.at(-1)!;
expect(lastTimeline).toBeGreaterThan(clearWidgetImages.mock.invocationCallOrder[0]!);
});
test("refresh requests made while one is waiting share it", async () => {
const { refreshWidgets } = await loadWidgetsModule();
+14 -5
View File
@@ -209,7 +209,10 @@ function emptyContinueWatchingProps(iconFilePath: string): ContinueWatchingProps
});
}
function emptyUpcomingProps(iconFilePath: string): UpcomingProps {
function emptyUpcomingProps(
iconFilePath: string,
emptyLabel = i18n._(msg`Nothing upcoming`),
): UpcomingProps {
return sanitizeProps<UpcomingProps>({
iconFilePath,
titleId: "",
@@ -217,7 +220,7 @@ function emptyUpcomingProps(iconFilePath: string): UpcomingProps {
imageFilePath: "",
dateLabel: "",
episodeLabel: "",
emptyLabel: i18n._(msg`Nothing upcoming`),
emptyLabel,
});
}
@@ -276,7 +279,10 @@ function enqueue(task: () => Promise<void>): Promise<void> {
/** Replace widget content with empty states and delete cached artwork (sign-out / server change). */
export function resetWidgets(): Promise<void> {
if (Platform.OS !== "ios") return Promise.resolve();
return enqueue(runReset);
const run = enqueue(runReset);
// A refresh requested after this reset must run after it, not merge into one queued before it.
pendingRefresh = null;
return run;
}
export function refreshWidgets(): Promise<void> {
@@ -353,7 +359,7 @@ async function refreshUpcoming(
iconFilePath: string,
): Promise<boolean> {
try {
const { items } = await client.library.upcoming({
const { items, nextCursor } = await client.library.upcoming({
days: UPCOMING_DAYS,
limit: 5,
});
@@ -364,6 +370,9 @@ async function refreshUpcoming(
return true;
}
// The list is capped, so running out of fetched items doesn't mean nothing is upcoming.
const terminalLabel = nextCursor ? i18n._(msg`Open Sofa to see what's next`) : undefined;
const refreshToken = nextRefreshToken("up");
// Download each item's art once, however many entries show it.
@@ -399,7 +408,7 @@ async function refreshUpcoming(
episodeLabel: upcomingEpisodeLabel(item),
emptyLabel: "",
})
: emptyUpcomingProps(iconFilePath),
: emptyUpcomingProps(iconFilePath, terminalLabel),
})),
);
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "افتح Radarr، وانتقل إلى الإعدادات > Import Lists"
msgid "Open registration"
msgstr "فتح التسجيل"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "افتح Sonarr، وانتقل إلى <0>الإعدادات > Import Lists</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Öffne Radarr, gehe zu Settings > Import Lists"
msgid "Open registration"
msgstr "Offene Registrierung"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Öffne Sonarr, gehe zu <0>Settings > Import Lists</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr ""
msgid "Open registration"
msgstr ""
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr "Open Sofa to see what's next"
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr ""
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Abre Radarr, ve a Settings > Import Lists"
msgid "Open registration"
msgstr "Registro abierto"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Abre Sonarr, ve a <0>Settings > Import Lists</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Ouvrez Radarr, allez dans Settings > Import Lists"
msgid "Open registration"
msgstr "Inscription ouverte"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Ouvrez Sonarr, allez dans <0>Settings > Import Lists</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "פתח את Radarr, עבור אל Settings > Import Lists"
msgid "Open registration"
msgstr "הרשמה פתוחה"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "פתח את Sonarr, עבור אל <0>Settings > Import Lists</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Apri Radarr, vai su Impostazioni > Liste di importazione"
msgid "Open registration"
msgstr "Registrazione aperta"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Apri Sonarr, vai su <0>Impostazioni > Liste di importazione</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Radarrを開き、設定 > インポートリストに移動する"
msgid "Open registration"
msgstr "公開登録"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Sonarrを開き、<0>設定 > インポートリスト</0>に移動する"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Radarr를 열고, Settings > Import Lists로 이동하세요"
msgid "Open registration"
msgstr "회원가입 허용"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Sonarr를 열고, <0>Settings > Import Lists</0>로 이동하세요"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Open Radarr, ga naar Instellingen > Importlijsten"
msgid "Open registration"
msgstr "Vrije registratie"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Open Sonarr, ga naar <0>Instellingen > Importlijsten</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "Abra o Radarr, vá para Settings > Import Lists"
msgid "Open registration"
msgstr "Registro aberto"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "Abra o Sonarr, vá para <0>Settings > Import Lists</0>"
+4
View File
@@ -2379,6 +2379,10 @@ msgstr "打开 Radarr,前往 Settings > Import Lists"
msgid "Open registration"
msgstr "开放注册"
#: apps/native/src/lib/widgets.ts
msgid "Open Sofa to see what's next"
msgstr ""
#: apps/web/src/components/settings/integration-configs.tsx
msgid "Open Sonarr, go to <0>Settings > Import Lists</0>"
msgstr "打开 Sonarr,前往 <0>Settings > Import Lists</0>"