Improve coverage for image upload and schedule helper logic with focused unit tests (#551)
Agent-Logs-Url: https://github.com/DanielVolz/medassist-ng/sessions/a5af7c91-2dd4-4a79-838e-dbb79fc08f6d Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DanielVolz <3275994+DanielVolz@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import sharp from "sharp";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
getThumbFilename,
|
||||
MAX_IMAGE_UPLOAD_BYTES,
|
||||
removeImageFiles,
|
||||
streamToBuffer,
|
||||
writeOptimizedImageSet,
|
||||
} from "../utils/image-upload";
|
||||
|
||||
describe("image-upload utils", () => {
|
||||
const MOCK_TIMESTAMP_MS = 1_700_000_000_000;
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("builds thumb filename with and without extension", () => {
|
||||
expect(getThumbFilename("avatar.png")).toBe("avatar-thumb.webp");
|
||||
expect(getThumbFilename("avatar")).toBe("avatar-thumb.webp");
|
||||
});
|
||||
|
||||
it("removes original and thumb files when they exist", () => {
|
||||
const imagesDir = mkdtempSync(join(tmpdir(), "medassist-image-upload-"));
|
||||
tempDirs.push(imagesDir);
|
||||
|
||||
const imageFilename = "profile.webp";
|
||||
const imagePath = join(imagesDir, imageFilename);
|
||||
const thumbPath = join(imagesDir, getThumbFilename(imageFilename));
|
||||
writeFileSync(imagePath, Buffer.from("image"));
|
||||
writeFileSync(thumbPath, Buffer.from("thumb"));
|
||||
|
||||
removeImageFiles(imagesDir, imageFilename);
|
||||
|
||||
expect(() => readFileSync(imagePath)).toThrow();
|
||||
expect(() => readFileSync(thumbPath)).toThrow();
|
||||
});
|
||||
|
||||
it("buffers stream chunks and rejects payloads above max size", async () => {
|
||||
const stream = Readable.from([Buffer.from("hello"), Buffer.from("world")]);
|
||||
await expect(streamToBuffer(stream)).resolves.toEqual(Buffer.from("helloworld"));
|
||||
|
||||
const oversized = Readable.from([Buffer.alloc(MAX_IMAGE_UPLOAD_BYTES + 1)]);
|
||||
await expect(streamToBuffer(oversized)).rejects.toThrow("IMAGE_TOO_LARGE");
|
||||
});
|
||||
|
||||
it("writes optimized full and thumbnail webp variants", async () => {
|
||||
const imagesDir = mkdtempSync(join(tmpdir(), "medassist-image-upload-"));
|
||||
tempDirs.push(imagesDir);
|
||||
vi.spyOn(Date, "now").mockReturnValue(MOCK_TIMESTAMP_MS);
|
||||
|
||||
const uploadBuffer = await sharp({
|
||||
create: {
|
||||
width: 64,
|
||||
height: 48,
|
||||
channels: 3,
|
||||
background: { r: 255, g: 0, b: 0 },
|
||||
},
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
const result = await writeOptimizedImageSet(imagesDir, "med-42", uploadBuffer, {
|
||||
maxEdgePx: 32,
|
||||
thumbSizePx: 16,
|
||||
});
|
||||
|
||||
expect(result.filename).toBe("med-42-1700000000000.webp");
|
||||
expect(result.thumbFilename).toBe("med-42-1700000000000-thumb.webp");
|
||||
|
||||
const optimizedMeta = await sharp(join(imagesDir, result.filename)).metadata();
|
||||
const thumbMeta = await sharp(join(imagesDir, result.thumbFilename)).metadata();
|
||||
expect(optimizedMeta.format).toBe("webp");
|
||||
expect(thumbMeta.format).toBe("webp");
|
||||
expect(Math.max(optimizedMeta.width ?? 0, optimizedMeta.height ?? 0)).toBeLessThanOrEqual(32);
|
||||
expect(thumbMeta.width).toBe(16);
|
||||
expect(thumbMeta.height).toBe(16);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { formatScheduleDoseUsageLabel, formatScheduleTotalUsageLabel } from "../../../features/schedule/formatters";
|
||||
|
||||
const t = (key: string, options?: Record<string, unknown>): string => {
|
||||
switch (key) {
|
||||
case "form.packageAmountUnitMl":
|
||||
return "ml";
|
||||
case "form.blisters.teaspoons":
|
||||
return Number(options?.count) === 1 ? "teaspoon" : "teaspoons";
|
||||
case "form.blisters.tablespoons":
|
||||
return Number(options?.count) === 1 ? "tablespoon" : "tablespoons";
|
||||
case "form.blisters.applications":
|
||||
return Number(options?.count) === 1 ? "application" : "applications";
|
||||
case "common.pill":
|
||||
return "pill";
|
||||
case "common.pills":
|
||||
return "pills";
|
||||
case "common.pillsTotal":
|
||||
return `${options?.count ?? 0} pills total`;
|
||||
default:
|
||||
return key;
|
||||
}
|
||||
};
|
||||
|
||||
describe("schedule formatters", () => {
|
||||
it("formats liquid dose labels in base and converted units", () => {
|
||||
expect(formatScheduleDoseUsageLabel({ packageType: "liquid_container" }, 0, t, "ml")).toBe("0 ml");
|
||||
expect(formatScheduleDoseUsageLabel({ packageType: "liquid_container" }, 2, t, "tsp")).toBe("2 teaspoons 10 ml");
|
||||
});
|
||||
|
||||
it("formats tube doses as applications by default and ml for liquid forms", () => {
|
||||
expect(formatScheduleDoseUsageLabel({ packageType: "tube" }, 1, t)).toBe("1 application");
|
||||
expect(formatScheduleDoseUsageLabel({ packageType: "tube", medicationForm: "liquid" }, 3, t)).toBe("3 ml");
|
||||
});
|
||||
|
||||
it("formats liquid totals from dose units and mixed-unit conversion", () => {
|
||||
expect(
|
||||
formatScheduleTotalUsageLabel(
|
||||
{ packageType: "liquid_container" },
|
||||
0,
|
||||
t,
|
||||
[
|
||||
{ usage: 1, intakeUnit: "tsp" },
|
||||
{ usage: 2, intakeUnit: "tsp" },
|
||||
],
|
||||
"ml"
|
||||
)
|
||||
).toBe("3 teaspoons 15 ml");
|
||||
|
||||
expect(
|
||||
formatScheduleTotalUsageLabel(
|
||||
{ packageType: "liquid_container" },
|
||||
0,
|
||||
t,
|
||||
[
|
||||
{ usage: 1, intakeUnit: "tsp" },
|
||||
{ usage: 1, intakeUnit: "tbsp" },
|
||||
],
|
||||
"ml"
|
||||
)
|
||||
).toBe("20 ml");
|
||||
});
|
||||
|
||||
it("falls back to total and non-liquid totals when dose list is not usable", () => {
|
||||
expect(
|
||||
formatScheduleTotalUsageLabel(
|
||||
{ packageType: "liquid_container" },
|
||||
4,
|
||||
t,
|
||||
[{ usage: -1, intakeUnit: "ml" }],
|
||||
"tbsp"
|
||||
)
|
||||
).toBe("4 tablespoons 60 ml");
|
||||
expect(formatScheduleTotalUsageLabel({ packageType: "blister" }, 3, t)).toBe("3 pills total");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
areAllDoseIdsTaken,
|
||||
countTakenDoseIds,
|
||||
resolveCollapsedState,
|
||||
toggleDateInSet,
|
||||
} from "../../../features/schedule/interactions";
|
||||
|
||||
describe("schedule interactions", () => {
|
||||
it("toggles dates without mutating the original set", () => {
|
||||
const previous = new Set(["2026-01-01"]);
|
||||
const added = toggleDateInSet(previous, "2026-01-02");
|
||||
const removed = toggleDateInSet(added, "2026-01-01");
|
||||
|
||||
expect(previous).toEqual(new Set(["2026-01-01"]));
|
||||
expect(added).toEqual(new Set(["2026-01-01", "2026-01-02"]));
|
||||
expect(removed).toEqual(new Set(["2026-01-02"]));
|
||||
});
|
||||
|
||||
it("resolves auto and manual collapsed states", () => {
|
||||
expect(resolveCollapsedState(true, "2026-01-01", new Set(), new Set())).toBe(true);
|
||||
expect(resolveCollapsedState(true, "2026-01-01", new Set(["2026-01-01"]), new Set())).toBe(false);
|
||||
expect(resolveCollapsedState(false, "2026-01-01", new Set(), new Set(["2026-01-01"]))).toBe(true);
|
||||
});
|
||||
|
||||
it("counts and checks taken dose ids", () => {
|
||||
const taken = new Set(["a", "c"]);
|
||||
const isDoseTaken = (doseId: string) => taken.has(doseId);
|
||||
|
||||
expect(countTakenDoseIds(["a", "b", "c"], isDoseTaken)).toBe(2);
|
||||
expect(areAllDoseIdsTaken(["a", "c"], isDoseTaken)).toBe(true);
|
||||
expect(areAllDoseIdsTaken(["a", "b"], isDoseTaken)).toBe(false);
|
||||
expect(areAllDoseIdsTaken([], isDoseTaken)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user