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();
});
});
});
});