/*
* Copyright 2015 Palantir Technologies, Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "@blueprintjs/test-commons/vitest";
import { Classes } from "../../common";
import { SPINNER_WARN_CLASSES_SIZE } from "../../common/errors";
import { Spinner, SpinnerSize } from "./spinner";
describe("Spinner", () => {
it("renders a spinner and two paths", () => {
render();
const spinner = screen.getByRole("progressbar");
expect(spinner).toHaveClass(Classes.SPINNER);
expect(spinner.querySelectorAll("path")).toHaveLength(2);
});
describe("accessibility", () => {
it("sets 'aria-valuenow' attribute", () => {
const VALUE = 0.4;
render();
const spinner = screen.getByRole("progressbar");
expect(spinner).toHaveAttribute("aria-valuenow", String(VALUE * 100));
});
it("supports arbitrary ARIA HTML attributes", () => {
const LABEL = "widget loading";
render();
const spinner = screen.getByRole("progressbar");
expect(spinner).toHaveAttribute("aria-label", LABEL);
});
});
it("tagName determines both container elements", () => {
render();
const spinner = screen.getByRole("progressbar");
expect(spinner.tagName.toLowerCase()).toBe("article");
expect(spinner.firstElementChild).toBeInTheDocument();
expect(spinner.firstElementChild!.tagName.toLowerCase()).toBe("article");
});
it("Classes.SMALL determines default size", () => {
const { container } = render();
expect(container.querySelector("svg")).toHaveAttribute("height", String(SpinnerSize.SMALL));
});
it("Classes.LARGE determines default size", () => {
const { container } = render();
expect(container.querySelector("svg")).toHaveAttribute("height", String(SpinnerSize.LARGE));
});
it("size overrides Classes.LARGE/SMALL uses size prop and warns", () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(vi.fn());
const { container } = render();
expect(container.querySelector("svg")).toHaveAttribute("height", "32");
expect(warnSpy).toHaveBeenCalledWith(SPINNER_WARN_CLASSES_SIZE);
warnSpy.mockRestore();
});
it("defaults to spinning quarter circle", () => {
render();
const spinner = screen.getByRole("progressbar");
expect(spinner).not.toHaveClass(Classes.SPINNER_NO_SPIN);
expectStrokePercent(spinner, 0.25);
});
it("value sets stroke-dashoffset", () => {
render();
const spinner = screen.getByRole("progressbar");
expect(spinner).toHaveClass(Classes.SPINNER_NO_SPIN);
expectStrokePercent(spinner, 0.35);
});
describe("viewBox adjusts based on size", () => {
it("adjusts viewBox for small size", () => {
render();
const spinner = screen.getByRole("progressbar");
expect(spinner.querySelector("svg")!.getAttribute("viewBox")).toBe("-3.00 -3.00 106.00 106.00");
});
it("adjusts viewBox for large size", () => {
render();
const spinner = screen.getByRole("progressbar");
expect(spinner.querySelector("svg")!.getAttribute("viewBox")).toBe("3.00 3.00 94.00 94.00");
});
});
});
function expectStrokePercent(container: HTMLElement, percent: number) {
const head = container.querySelector(`.${Classes.SPINNER_HEAD}`);
expect(head).not.toBeNull();
// strokeDasharray is "X X", parseInt terminates at non-numeric character
const pathLength = parseInt(head!.getAttribute("stroke-dasharray")!, 10);
const offset = Number(head!.getAttribute("stroke-dashoffset"));
expect(offset).toBe(pathLength * (1 - percent));
}