mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-10-15 19:45:42 -04:00
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:
14 files changed
+166
-6
No files matched your search
@@ -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();
|
||||
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
);
|
||||
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>に移動する"
|
||||
|
||||
@@ -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>로 이동하세요"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
@@ -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>"
|
||||
|
||||
Reference in new issue
Block a user