mirror of
https://github.com/kamranahmedse/driver.js.git
synced 2026-08-30 17:05:33 +08:00
Add tests covering driver, overlay and popover branches
This commit is contained in:
@@ -179,3 +179,53 @@ describe("button interactions", () => {
|
||||
expect(d.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("overlay click behaviour", () => {
|
||||
it("closes the tour when the overlay is clicked and behaviour is 'close'", async () => {
|
||||
const d = createDriver({ animate: false, overlayClickBehavior: "close", steps: SAMPLE_STEPS });
|
||||
d.drive();
|
||||
await nextFrame();
|
||||
|
||||
clickOverlay();
|
||||
|
||||
expect(d.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("runs a custom overlayClickBehavior with the active element, step and driver", async () => {
|
||||
const overlayClickBehavior = vi.fn();
|
||||
const d = createDriver({ animate: false, overlayClickBehavior, steps: SAMPLE_STEPS });
|
||||
d.drive();
|
||||
await nextFrame();
|
||||
|
||||
clickOverlay();
|
||||
|
||||
expect(overlayClickBehavior).toHaveBeenCalledTimes(1);
|
||||
const [element, step, options] = overlayClickBehavior.mock.calls[0];
|
||||
expect(element).toBe(document.querySelector("#intro"));
|
||||
expect(step.popover?.title).toBe("Step 1");
|
||||
expect(options.driver).toBe(d);
|
||||
expect(d.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it("advances to the next step when 'nextStep' overlay is clicked without onNextClick", async () => {
|
||||
const d = createDriver({ animate: false, overlayClickBehavior: "nextStep", steps: SAMPLE_STEPS });
|
||||
d.drive();
|
||||
await nextFrame();
|
||||
|
||||
clickOverlay();
|
||||
|
||||
expect(d.getActiveIndex()).toBe(1);
|
||||
});
|
||||
|
||||
it("runs onDoneClick when a 'nextStep' overlay is clicked on the final step", async () => {
|
||||
const onDoneClick = vi.fn();
|
||||
const d = createDriver({ animate: false, overlayClickBehavior: "nextStep", onDoneClick, steps: SAMPLE_STEPS });
|
||||
d.drive(SAMPLE_STEPS.length - 1);
|
||||
await nextFrame();
|
||||
|
||||
clickOverlay();
|
||||
|
||||
expect(onDoneClick).toHaveBeenCalledTimes(1);
|
||||
expect(d.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,4 +72,37 @@ describe("keyboard control", () => {
|
||||
pressKey("Escape");
|
||||
expect(d.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it("runs onPrevClick instead of moving back when ArrowLeft is pressed", async () => {
|
||||
const onPrevClick = vi.fn();
|
||||
const d = createDriver({ animate: false, steps: SAMPLE_STEPS, onPrevClick });
|
||||
d.drive(1);
|
||||
await nextFrame();
|
||||
|
||||
pressKey("ArrowLeft");
|
||||
|
||||
expect(onPrevClick).toHaveBeenCalledTimes(1);
|
||||
expect(d.getActiveIndex()).toBe(1);
|
||||
});
|
||||
|
||||
it("runs onNextClick instead of advancing when ArrowRight is pressed", async () => {
|
||||
const onNextClick = vi.fn();
|
||||
const d = createDriver({ animate: false, steps: SAMPLE_STEPS, onNextClick });
|
||||
d.drive();
|
||||
await nextFrame();
|
||||
|
||||
pressKey("ArrowRight");
|
||||
|
||||
expect(onNextClick).toHaveBeenCalledTimes(1);
|
||||
expect(d.getActiveIndex()).toBe(0);
|
||||
});
|
||||
|
||||
it("ignores arrow keys while a highlight transition is in flight", () => {
|
||||
const d = createDriver({ animate: true, steps: SAMPLE_STEPS });
|
||||
d.drive();
|
||||
// No frame awaited: the transition callback is still set, so arrows no-op.
|
||||
pressKey("ArrowRight");
|
||||
|
||||
expect(d.getActiveIndex()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
+68
-2
@@ -1,5 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createDriver, popoverDescription, popoverEl, popoverTitle, useDriverHarness } from "./utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createDriver,
|
||||
nextFrame,
|
||||
popoverDescription,
|
||||
popoverEl,
|
||||
popoverTitle,
|
||||
pressKey,
|
||||
SAMPLE_STEPS,
|
||||
useDriverHarness,
|
||||
} from "./utils";
|
||||
|
||||
useDriverHarness();
|
||||
|
||||
@@ -77,3 +86,60 @@ describe("lifecycle", () => {
|
||||
expect(d.getActiveIndex()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("drive guards", () => {
|
||||
it("logs an error and stays inactive when there are no steps", () => {
|
||||
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const d = createDriver({ animate: false });
|
||||
d.drive();
|
||||
|
||||
expect(error).toHaveBeenCalledWith("No steps to drive through");
|
||||
expect(d.isActive()).toBe(false);
|
||||
error.mockRestore();
|
||||
});
|
||||
|
||||
it("destroys instead of driving an out-of-range step index", () => {
|
||||
const d = createDriver({ animate: false, steps: SAMPLE_STEPS });
|
||||
d.drive(99);
|
||||
|
||||
expect(d.isActive()).toBe(false);
|
||||
});
|
||||
|
||||
it("ignores moveNext and movePrevious before the tour starts", () => {
|
||||
const d = createDriver({ animate: false, steps: SAMPLE_STEPS });
|
||||
d.moveNext();
|
||||
d.movePrevious();
|
||||
|
||||
expect(d.isActive()).toBe(false);
|
||||
expect(d.getActiveIndex()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dummy-element hooks", () => {
|
||||
it("passes an undefined element to onDeselected and onDestroyed for a dummy highlight", async () => {
|
||||
const onDeselected = vi.fn();
|
||||
const onDestroyed = vi.fn();
|
||||
const d = createDriver({ animate: false, onDeselected, onDestroyed });
|
||||
d.highlight({ popover: { title: "Modal" } });
|
||||
await nextFrame();
|
||||
|
||||
d.destroy();
|
||||
|
||||
expect(onDeselected).toHaveBeenCalledTimes(1);
|
||||
expect(onDestroyed).toHaveBeenCalledTimes(1);
|
||||
expect(onDeselected.mock.calls[0][0]).toBeUndefined();
|
||||
expect(onDestroyed.mock.calls[0][0]).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes an undefined element to onDestroyStarted for a dummy highlight", async () => {
|
||||
const onDestroyStarted = vi.fn();
|
||||
const d = createDriver({ animate: false, onDestroyStarted });
|
||||
d.highlight({ popover: { title: "Modal" } });
|
||||
await nextFrame();
|
||||
|
||||
pressKey("Escape");
|
||||
|
||||
expect(onDestroyStarted).toHaveBeenCalledTimes(1);
|
||||
expect(onDestroyStarted.mock.calls[0][0]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,4 +51,32 @@ describe("overlay configuration", () => {
|
||||
|
||||
expect(overlayPath()?.getAttribute("d")).toContain("M-15,-20");
|
||||
});
|
||||
|
||||
it("falls back to a solid black fill when overlayColor is empty", async () => {
|
||||
const d = createDriver({ animate: false, overlayColor: "", steps: SAMPLE_STEPS });
|
||||
d.drive();
|
||||
await nextFrame();
|
||||
|
||||
expect(overlayPath()?.style.fill).toBe("rgb(0, 0, 0)");
|
||||
});
|
||||
|
||||
it("draws square corners with no inset when padding and radius are zero", async () => {
|
||||
const d = createDriver({ animate: false, stagePadding: 0, stageRadius: 0, steps: SAMPLE_STEPS });
|
||||
d.drive();
|
||||
await nextFrame();
|
||||
|
||||
// Zero radius removes the rounded-corner arcs and the cutout starts at the origin.
|
||||
expect(overlayPath()?.getAttribute("d")).toContain("M0,0");
|
||||
expect(overlayPath()?.getAttribute("d")).not.toContain("a5,5");
|
||||
});
|
||||
|
||||
it("does not close the tour when the backdrop outside the cutout path is clicked", async () => {
|
||||
const d = createDriver({ animate: false, steps: SAMPLE_STEPS });
|
||||
d.drive();
|
||||
await nextFrame();
|
||||
|
||||
document.querySelector(".driver-overlay")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
|
||||
expect(d.isActive()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,6 +187,58 @@ describe("popover rendering", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("popover interaction edge cases", () => {
|
||||
it("disables the close button when close is in disableButtons", () => {
|
||||
const d = createDriver({ animate: false });
|
||||
d.highlight({
|
||||
element: "#intro",
|
||||
popover: { title: "Intro", showButtons: ["close"], disableButtons: ["close"] },
|
||||
});
|
||||
|
||||
expect(navButton("close")?.disabled).toBe(true);
|
||||
expect(navButton("close")?.classList.contains("driver-popover-btn-disabled")).toBe(true);
|
||||
});
|
||||
|
||||
it("emits without navigating when next/prev are clicked on a bare highlight", () => {
|
||||
const d = createDriver({ animate: false });
|
||||
d.highlight({ element: "#intro", popover: { title: "Intro", showButtons: ["next", "previous"] } });
|
||||
|
||||
navButton("next")?.click();
|
||||
navButton("prev")?.click();
|
||||
|
||||
expect(d.isActive()).toBe(true);
|
||||
expect(d.getActiveElement()).toBe(document.querySelector("#intro"));
|
||||
});
|
||||
|
||||
it("lets links inside the description behave normally", () => {
|
||||
const d = createDriver({ animate: false });
|
||||
d.highlight({
|
||||
element: "#intro",
|
||||
popover: { title: "Intro", description: '<a class="doc-link" href="#doc">Docs</a>' },
|
||||
});
|
||||
|
||||
document.querySelector<HTMLElement>(".doc-link")?.click();
|
||||
|
||||
expect(d.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it("repositions the popover when a lazy image inside it finishes loading", () => {
|
||||
let lazyImage: HTMLImageElement | undefined;
|
||||
const d = createDriver({
|
||||
animate: false,
|
||||
onPopoverRender: popover => {
|
||||
lazyImage = document.createElement("img");
|
||||
Object.defineProperty(lazyImage, "complete", { value: false });
|
||||
popover.description.appendChild(lazyImage);
|
||||
},
|
||||
});
|
||||
d.highlight({ element: "#intro", popover: { title: "Intro", description: "With image" } });
|
||||
|
||||
expect(() => lazyImage?.dispatchEvent(new Event("load"))).not.toThrow();
|
||||
expect(popoverEl()).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("popover arrow", () => {
|
||||
const rect = (over: Partial<DOMRect>): DOMRect =>
|
||||
({ top: 0, left: 0, right: 0, bottom: 0, width: 0, height: 0, x: 0, y: 0, toJSON() {}, ...over }) as DOMRect;
|
||||
|
||||
Reference in New Issue
Block a user