import React from "react"; import { render, screen } from "@testing-library/react-native"; import { ProgressIndicator } from "."; describe("ProgressIndicator", () => { describe("public API", () => { it("renders with role='progressbar'", () => { render(); expect(screen.getByRole("progressbar")).toBeTruthy(); }); it("sets accessibilityValue now/min/max", () => { render(); expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual( expect.objectContaining({ now: 50, min: 0, max: 100 }), ); }); it("uses max in accessibilityValue when provided", () => { render(); expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual( expect.objectContaining({ now: 3, max: 4 }), ); }); it("renders a continuous fill by default", () => { render(); expect(screen.getByTestId("progressindicator-fill")).toBeTruthy(); expect(screen.queryByTestId("progressindicator-step-filled")).toBeNull(); }); it("applies style to the root", () => { render(); expect(screen.getByRole("progressbar").props.style).toEqual( expect.arrayContaining([expect.objectContaining({ margin: 8 })]), ); }); it("rounds the continuous track so both ends are pill-shaped", () => { render(); // base height is 16px, so the track radius is 8. Without this the // unfilled (right) end of the track renders with square corners. expect(screen.getByRole("progressbar").props.style).toEqual( expect.arrayContaining([expect.objectContaining({ borderRadius: 8 })]), ); }); it("does not round the stepped root (segments carry their own radius)", () => { render(); const rootStyles = screen .getByRole("progressbar") .props.style.filter(Boolean); expect(rootStyles).not.toContainEqual( expect.objectContaining({ borderRadius: expect.anything() }), ); }); }); describe("accessibility label", () => { it("defaults to '{value}%' when max is omitted", () => { render(); expect(screen.getByLabelText("42%")).toBeTruthy(); }); it("defaults to '{value}%' when max === 100", () => { render(); expect(screen.getByLabelText("42%")).toBeTruthy(); }); it("defaults to '{value} / {max}' when max !== 100", () => { render(); expect(screen.getByLabelText("3 / 4")).toBeTruthy(); }); it("defaults to the pending template when pendingValue > 0", () => { render(); expect(screen.getByLabelText("2 of 10, 3 pending")).toBeTruthy(); }); it("uses a consumer-supplied accessibilityLabel when provided", () => { render( , ); expect(screen.getByLabelText("Uploading file")).toBeTruthy(); }); }); /** * Native screen readers (VoiceOver / TalkBack) announce a progressbar from * its `accessibilityRole` plus the resolved `accessibilityValue.text` / * `accessibilityLabel`. These assertions lock that announced payload * deterministically — the automatable proxy for the on-device smoke, which * cannot capture live screen-reader speech in CI. */ describe("screen reader announcement contract", () => { it.each([ ["percent (max omitted)", { value: 50 }, "50%"], ["ratio (max !== 100)", { value: 3, max: 4 }, "3 / 4"], ["pending", { value: 2, pendingValue: 3, max: 10 }, "2 of 10, 3 pending"], [ "custom label", { value: 50, accessibilityLabel: "Uploading file" }, "Uploading file", ], ])( "announces %s as a progressbar with the derived text", (_n, props, text) => { render(); const root = screen.getByRole("progressbar"); expect(root.props.accessibilityRole).toBe("progressbar"); expect(root.props.accessibilityLabel).toBe(text); expect(root.props.accessibilityValue).toEqual( expect.objectContaining({ text }), ); }, ); it("carries the numeric value/min/max alongside the announced text", () => { render(); expect(screen.getByRole("progressbar").props.accessibilityValue).toEqual({ min: 0, max: 4, now: 3, text: "3 / 4", }); }); }); describe("sizes", () => { it.each([ ["smaller", 4], ["small", 8], ["base", 16], ] as const)("applies %s height (%dpx) to the root", (size, height) => { render(); expect(screen.getByRole("progressbar").props.style).toEqual( expect.arrayContaining([expect.objectContaining({ height })]), ); }); }); describe("pending overlay (continuous)", () => { it("renders a pending band when pendingValue > 0", () => { render(); expect(screen.getByTestId("progressindicator-pending")).toBeTruthy(); }); it("renders no pending band when pendingValue is 0", () => { render(); expect(screen.queryByTestId("progressindicator-pending")).toBeNull(); }); }); describe("stepped variation", () => { it("renders max segments with the first value filled", () => { render(); expect( screen.getAllByTestId("progressindicator-step-filled"), ).toHaveLength(2); expect( screen.getAllByTestId("progressindicator-step-empty"), ).toHaveLength(3); }); it("classifies filled, pending, and empty segments", () => { render( , ); expect( screen.getAllByTestId("progressindicator-step-filled"), ).toHaveLength(3); expect( screen.getAllByTestId("progressindicator-step-pending"), ).toHaveLength(2); expect( screen.getAllByTestId("progressindicator-step-empty"), ).toHaveLength(1); }); it("keeps a single progressbar on the root and none on segments", () => { render(); expect(screen.getAllByRole("progressbar")).toHaveLength(1); screen.getAllByTestId(/progressindicator-step-/).forEach(segment => { expect(segment.props.accessibilityRole).toBeUndefined(); expect(segment.props.accessibilityValue).toBeUndefined(); }); }); }); });