diff --git a/__tests__/ad-manager.test.tsx b/__tests__/ad-manager.test.tsx index d41bed3..6514236 100644 --- a/__tests__/ad-manager.test.tsx +++ b/__tests__/ad-manager.test.tsx @@ -96,6 +96,7 @@ describe("AdManager", () => { it("shows adblock modal when fetch fails", async () => { vi.stubEnv("NEXT_PUBLIC_ADSENSE_ID", MOCK_ADSENSE_ID); + vi.stubEnv("NEXT_PUBLIC_ENABLE_ADS", "true"); mockFetch.mockRejectedValueOnce(new Error("blocked")); render(); @@ -107,6 +108,7 @@ describe("AdManager", () => { it("closes the adblock modal without reloading when dismissed", async () => { vi.stubEnv("NEXT_PUBLIC_ADSENSE_ID", MOCK_ADSENSE_ID); + vi.stubEnv("NEXT_PUBLIC_ENABLE_ADS", "true"); mockFetch.mockRejectedValueOnce(new Error("blocked")); render(); @@ -122,6 +124,7 @@ describe("AdManager", () => { it("reloads the page and closes the modal when confirming adblock is disabled", async () => { vi.stubEnv("NEXT_PUBLIC_ADSENSE_ID", MOCK_ADSENSE_ID); + vi.stubEnv("NEXT_PUBLIC_ENABLE_ADS", "true"); mockFetch.mockRejectedValueOnce(new Error("blocked")); render(); @@ -172,4 +175,17 @@ describe("AdManager", () => { expect(localStorage.getItem(VIDEO_AD_KEY)).not.toBeNull(); expect(screen.queryByText("Vídeo da Semana")).toBeNull(); }); + + it("never nags about adblock while ads are switched off", async () => { + vi.stubEnv("NEXT_PUBLIC_ADSENSE_ID", MOCK_ADSENSE_ID); + vi.stubEnv("NEXT_PUBLIC_ENABLE_ADS", "false"); + mockFetch.mockRejectedValueOnce(new Error("blocked")); + + render(); + + await vi.waitFor(() => { + expect(mockFetch).not.toHaveBeenCalled(); + }); + expect(screen.queryByText("Opa! Uma ajudinha?")).toBeNull(); + }); }); diff --git a/__tests__/duration-row.test.tsx b/__tests__/duration-row.test.tsx index e51a990..48fa6bd 100644 --- a/__tests__/duration-row.test.tsx +++ b/__tests__/duration-row.test.tsx @@ -17,9 +17,9 @@ describe("DurationRow", () => { expect(screen.getByText("0h 0m")).toBeInTheDocument(); }); - it("rounds fractional minutes", () => { + it("rounds fractional minutes up into the next hour", () => { render(); - expect(screen.getByText("0h 60m")).toBeInTheDocument(); + expect(screen.getByText("1h 0m")).toBeInTheDocument(); }); }); diff --git a/__tests__/duration.test.ts b/__tests__/duration.test.ts new file mode 100644 index 0000000..823b62c --- /dev/null +++ b/__tests__/duration.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; +import { + formatClock, + formatHoursAndMinutes, + formatPaddedDuration, + formatSignedHoursAndMinutes, + minutesToSeconds, + parsePaddedDuration, + splitHoursAndMinutes, +} from "@/lib/duration"; + +describe("splitHoursAndMinutes", () => { + it("splits minutes into whole hours and a rounded remainder", () => { + expect(splitHoursAndMinutes(0)).toEqual({ hours: 0, minutes: 0 }); + expect(splitHoursAndMinutes(59)).toEqual({ hours: 0, minutes: 59 }); + expect(splitHoursAndMinutes(60)).toEqual({ hours: 1, minutes: 0 }); + expect(splitHoursAndMinutes(528)).toEqual({ hours: 8, minutes: 48 }); + expect(splitHoursAndMinutes(90.4)).toEqual({ hours: 1, minutes: 30 }); + }); + + it("carries a rounded-up remainder into the hour instead of reporting 60 minutes", () => { + expect(splitHoursAndMinutes(59.6)).toEqual({ hours: 1, minutes: 0 }); + expect(splitHoursAndMinutes(119.6)).toEqual({ hours: 2, minutes: 0 }); + expect(formatHoursAndMinutes(59.6)).toBe("1h 0m"); + expect(formatPaddedDuration(59.6)).toBe("01:00"); + }); +}); + +describe("formatHoursAndMinutes", () => { + it("reads as hours and minutes", () => { + expect(formatHoursAndMinutes(0)).toBe("0h 0m"); + expect(formatHoursAndMinutes(125)).toBe("2h 5m"); + }); +}); + +describe("formatSignedHoursAndMinutes", () => { + it("always carries a sign", () => { + expect(formatSignedHoursAndMinutes(0)).toBe("+0h 0m"); + expect(formatSignedHoursAndMinutes(75)).toBe("+1h 15m"); + expect(formatSignedHoursAndMinutes(-75)).toBe("-1h 15m"); + }); +}); + +describe("formatPaddedDuration and parsePaddedDuration", () => { + it("round-trips a padded duration", () => { + expect(formatPaddedDuration(528)).toBe("08:48"); + expect(formatPaddedDuration(0)).toBe("00:00"); + expect(parsePaddedDuration("08:48")).toBe(528); + expect(parsePaddedDuration(formatPaddedDuration(479))).toBe(479); + }); +}); + +describe("formatClock", () => { + it("pads every part of the clock", () => { + expect(formatClock(0)).toBe("00:00:00"); + expect(formatClock(3661)).toBe("01:01:01"); + expect(formatClock(86399)).toBe("23:59:59"); + expect(formatClock(1.9)).toBe("00:00:01"); + }); +}); + +describe("minutesToSeconds", () => { + it("converts minutes to seconds", () => { + expect(minutesToSeconds(0)).toBe(0); + expect(minutesToSeconds(90)).toBe(5400); + }); +}); diff --git a/app/globals.css b/app/globals.css index aacad52..36ea869 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,5 +1,3 @@ -/* biome-ignore-all lint/complexity/noImportantStyles: a universal selector loses to every utility class, so the reduced-motion reset below cannot take effect without it */ - @import "tailwindcss"; @custom-variant dark (&:where(.dark, .dark *)); @@ -13,6 +11,7 @@ } @media (prefers-reduced-motion: reduce) { + /* biome-ignore-start lint/complexity/noImportantStyles: a universal selector loses to every utility class, so this reset cannot take effect without it */ *, *::before, *::after { @@ -21,4 +20,5 @@ transition-duration: 0.01ms !important; scroll-behavior: auto !important; } + /* biome-ignore-end lint/complexity/noImportantStyles: scoped to the reduced-motion reset */ } diff --git a/app/page.tsx b/app/page.tsx index aedab40..f306055 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -31,7 +31,7 @@ export default function Home() { <>