Skip to content

Commit dacb5ed

Browse files
committed
Fill remaining test coverage gaps to restore 100%
Add tests for: submitForReview error cleanup paths, perfPowerMetrics missing nested fields, checklist utils with undefined locale fields, listBuilds lite mode, and diagnostics non-array callStacks. Remove unreachable ?? fallbacks in submission-checklist-utils.
1 parent fd191e1 commit dacb5ed

6 files changed

Lines changed: 147 additions & 2 deletions

File tree

src/lib/submission-checklist-utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ export function computeChecklistFlags(
4444
primaryLocale: string,
4545
): ChecklistFlags {
4646
return {
47-
description: computeFieldIssues(localeData, primaryLocale, "description", FIELD_MIN_LIMITS.description ?? 10),
48-
whatsNew: computeFieldIssues(localeData, primaryLocale, "whatsNew", FIELD_MIN_LIMITS.whatsNew ?? 4),
47+
description: computeFieldIssues(localeData, primaryLocale, "description", FIELD_MIN_LIMITS.description),
48+
whatsNew: computeFieldIssues(localeData, primaryLocale, "whatsNew", FIELD_MIN_LIMITS.whatsNew),
4949
keywords: computeFieldIssues(localeData, primaryLocale, "keywords", 1),
5050
};
5151
}

tests/unit/asc/analytics.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3168,6 +3168,29 @@ describe("fetchPerfPowerMetrics", () => {
31683168
expect(result.metrics).toEqual([]);
31693169
});
31703170

3171+
it("handles missing nested fields (metricCategories, metrics, datasets, points)", async () => {
3172+
mockCacheGet.mockReturnValue(null);
3173+
3174+
mockAscFetch.mockResolvedValueOnce({
3175+
productData: [
3176+
{ platform: "iOS" }, // no metricCategories
3177+
{ platform: "macOS", metricCategories: [
3178+
{ identifier: "DISK" }, // no metrics
3179+
{ identifier: "LAUNCH", metrics: [
3180+
{ identifier: "launchTime", unit: { displayName: "ms" } }, // no datasets
3181+
{ identifier: "hangRate", unit: { displayName: "s" }, datasets: [
3182+
{ filterCriteria: { device: "all", percentile: "p50" } }, // no points
3183+
] },
3184+
] },
3185+
] },
3186+
],
3187+
});
3188+
3189+
const result = await fetchPerfPowerMetrics("app-perf-nested-null");
3190+
// All products have no usable data – every metric is skipped
3191+
expect(result.metrics).toEqual([]);
3192+
});
3193+
31713194
it("handles missing filterCriteria and unit", async () => {
31723195
mockCacheGet.mockReturnValue(null);
31733196

tests/unit/asc/testflight/builds.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,6 +871,50 @@ describe("listBuilds – branch coverage", () => {
871871
expect(result[0].groupIds).toContain("group-single");
872872
expect(result[0].groupIds).not.toContain("group-null");
873873
});
874+
875+
it("skips group and metrics lookups in lite mode", async () => {
876+
mockCacheGet.mockReturnValue(null);
877+
878+
mockAscFetch.mockImplementation((url: string) => {
879+
if (url.startsWith("/v1/builds?")) {
880+
return Promise.resolve({
881+
data: [
882+
{
883+
id: "b1",
884+
type: "builds",
885+
attributes: {
886+
version: "10",
887+
uploadedDate: "2026-02-01T00:00:00Z",
888+
expirationDate: null,
889+
expired: false,
890+
minOsVersion: null,
891+
processingState: "VALID",
892+
iconAssetToken: null,
893+
},
894+
relationships: {
895+
preReleaseVersion: { data: { id: "prv-1", type: "preReleaseVersions" } },
896+
buildBetaDetail: { data: { id: "bbd-1", type: "buildBetaDetails" } },
897+
betaBuildLocalizations: { data: [] },
898+
},
899+
},
900+
],
901+
included: [
902+
{ id: "prv-1", type: "preReleaseVersions", attributes: { version: "1.0", platform: "IOS" } },
903+
{ id: "bbd-1", type: "buildBetaDetails", attributes: { internalBuildState: "IN_BETA_TESTING", externalBuildState: null } },
904+
],
905+
});
906+
}
907+
return Promise.resolve({ data: [] });
908+
});
909+
910+
const result = await listBuilds("app-lite", false, { lite: true });
911+
912+
expect(result).toHaveLength(1);
913+
expect(result[0].groupIds).toEqual([]);
914+
expect(result[0].metrics).toBeUndefined();
915+
// listGroups should not have been called
916+
expect(mockListGroups).not.toHaveBeenCalled();
917+
});
874918
});
875919

876920
// ── Branch coverage: fetchBuildMetrics edge cases ───────────────

tests/unit/asc/testflight/diagnostics.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,28 @@ describe("getDiagnosticLogs", () => {
289289
expect(root.subFrames![0].subFrames![0].symbolName).toBe("grandchild");
290290
});
291291

292+
it("handles non-array callStacks and callStackRootFrames gracefully", async () => {
293+
mockAscFetch.mockResolvedValue({
294+
data: [
295+
{
296+
attributes: {
297+
callStackTree: [
298+
{ callStacks: "not-an-array" }, // non-array callStacks
299+
{ callStacks: [
300+
{ callStackRootFrames: "also-not-an-array" }, // non-array rootFrames
301+
{ /* missing callStackRootFrames */ },
302+
] },
303+
],
304+
},
305+
},
306+
],
307+
});
308+
309+
const result = await getDiagnosticLogs("sig-1");
310+
expect(result).toHaveLength(1);
311+
expect(result[0].callStack).toEqual([]);
312+
});
313+
292314
it("returns empty array on error (best-effort)", async () => {
293315
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
294316
mockAscFetch.mockRejectedValue(new Error("network error"));

tests/unit/asc/version-mutations.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,44 @@ describe("version-mutations", () => {
177177
const step3Body = JSON.parse(mockAscFetch.mock.calls[2][1].body);
178178
expect(step3Body.data.attributes.submitted).toBe(true);
179179
});
180+
181+
it("cleans up dangling submission when step 2 fails", async () => {
182+
mockAscFetch
183+
.mockResolvedValueOnce({ data: { id: "sub-1" } }) // step 1: create
184+
.mockRejectedValueOnce(new Error("add item failed")) // step 2: fails
185+
.mockResolvedValueOnce(null); // cleanup DELETE
186+
187+
await expect(submitForReview("app-1", "ver-1", "IOS")).rejects.toThrow("add item failed");
188+
189+
expect(mockAscFetch).toHaveBeenCalledWith(
190+
"/v1/reviewSubmissions/sub-1",
191+
expect.objectContaining({ method: "DELETE" }),
192+
);
193+
});
194+
195+
it("cleans up dangling submission when step 3 fails", async () => {
196+
mockAscFetch
197+
.mockResolvedValueOnce({ data: { id: "sub-1" } }) // step 1: create
198+
.mockResolvedValueOnce({}) // step 2: add item
199+
.mockRejectedValueOnce(new Error("confirm failed")) // step 3: fails
200+
.mockResolvedValueOnce(null); // cleanup DELETE
201+
202+
await expect(submitForReview("app-1", "ver-1", "IOS")).rejects.toThrow("confirm failed");
203+
204+
expect(mockAscFetch).toHaveBeenCalledWith(
205+
"/v1/reviewSubmissions/sub-1",
206+
expect.objectContaining({ method: "DELETE" }),
207+
);
208+
});
209+
210+
it("still throws original error when cleanup DELETE also fails", async () => {
211+
mockAscFetch
212+
.mockResolvedValueOnce({ data: { id: "sub-1" } }) // step 1: create
213+
.mockRejectedValueOnce(new Error("add item failed")) // step 2: fails
214+
.mockRejectedValueOnce(new Error("delete also failed")); // cleanup also fails
215+
216+
await expect(submitForReview("app-1", "ver-1", "IOS")).rejects.toThrow("add item failed");
217+
});
180218
});
181219

182220
describe("releaseVersion", () => {

tests/unit/submission-checklist-utils.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,24 @@ describe("computeFieldIssues", () => {
100100
const result = computeFieldIssues(data, "en-US", "description", 10);
101101
expect(result).toEqual({ status: "missing", localesWithIssues: [] });
102102
});
103+
104+
it("treats undefined field on primary locale as length 0", () => {
105+
// Field key absent entirely – exercises the ?.length ?? 0 fallback
106+
const data = { "en-US": { keywords: "ok", whatsNew: "ok" } as LocaleFields };
107+
const result = computeFieldIssues(data, "en-US", "description", 10);
108+
expect(result).toEqual({ status: "missing", localesWithIssues: [] });
109+
});
110+
111+
it("treats undefined field on secondary locale as length 0", () => {
112+
const data = makeLocaleData({
113+
"en-US": { description: "A valid description text" },
114+
});
115+
// Manually add a secondary locale missing the description field
116+
data["it"] = { keywords: "k", whatsNew: "w", promotionalText: "", supportUrl: "", marketingUrl: "" } as LocaleFields;
117+
const result = computeFieldIssues(data, "en-US", "description", 10);
118+
expect(result.status).toBe("warn");
119+
expect(result.localesWithIssues).toEqual(["it"]);
120+
});
103121
});
104122

105123
describe("computeChecklistFlags", () => {

0 commit comments

Comments
 (0)