mirror of
https://github.com/kamranahmedse/driver.js.git
synced 2026-11-01 21:22:09 +08:00
647 lines
22 KiB
TypeScript
647 lines
22 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { driver, type Driver } from "../src/driver";
|
|
import { checklist, type Checklist, type ChecklistConfig, type ChecklistItem } from "../src/checklist";
|
|
import { DEMO_HTML, navButton, nextFrame, pressKey, SAMPLE_STEPS } from "./utils";
|
|
|
|
let active: Checklist | undefined;
|
|
let activeDriver: Driver | undefined;
|
|
|
|
function createChecklist(config: Partial<ChecklistConfig> = {}): Checklist {
|
|
active = checklist({ id: "onboarding", items: ITEMS, ...config });
|
|
return active;
|
|
}
|
|
|
|
const ITEMS: ChecklistItem[] = [
|
|
{ id: "tour", title: "Take the tour", tour: { animate: false, steps: SAMPLE_STEPS.slice(0, 2) } },
|
|
{ id: "docs", title: "Read the docs", href: "#docs" },
|
|
{ id: "invite", title: "Invite a teammate", action: () => {} },
|
|
];
|
|
|
|
const root = () => document.querySelector<HTMLElement>(".driver-checklist");
|
|
const panel = () => document.querySelector<HTMLElement>(".driver-checklist-panel");
|
|
const launcher = () => document.querySelector<HTMLButtonElement>(".driver-checklist-launcher");
|
|
const launcherCount = () => document.querySelector(".driver-checklist-launcher-count")?.textContent;
|
|
const progressText = () => document.querySelector(".driver-checklist-progress-text")?.textContent;
|
|
const trigger = (id: string) =>
|
|
document.querySelector<HTMLElement>(`.driver-checklist-item-trigger[data-item-id="${id}"]`);
|
|
const isDone = (id: string) => !!trigger(id)?.closest(".driver-checklist-item-done");
|
|
|
|
async function finishTour() {
|
|
await nextFrame();
|
|
navButton("next")?.click();
|
|
navButton("next")?.click();
|
|
}
|
|
|
|
beforeEach(() => {
|
|
document.body.innerHTML = DEMO_HTML;
|
|
});
|
|
|
|
afterEach(() => {
|
|
active?.hide();
|
|
active = undefined;
|
|
activeDriver?.destroy();
|
|
activeDriver = undefined;
|
|
document.body.innerHTML = "";
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("checklist rendering", () => {
|
|
it("renders a launcher with the progress count and a closed panel", () => {
|
|
createChecklist().show();
|
|
|
|
expect(root()?.classList.contains("driver-checklist-bottom-right")).toBe(true);
|
|
expect(launcherCount()).toBe("0/3");
|
|
expect(progressText()).toBe("0 of 3");
|
|
expect(panel()?.hidden).toBe(true);
|
|
expect(launcher()?.getAttribute("aria-expanded")).toBe("false");
|
|
expect(launcher()?.getAttribute("aria-controls")).toBe(panel()?.id);
|
|
});
|
|
|
|
it("renders link items as anchors, actionable items as buttons and the rest as plain rows", () => {
|
|
createChecklist({ items: [...ITEMS, { id: "info", title: "Done in the app" }] }).show();
|
|
|
|
expect(trigger("docs")?.tagName).toBe("A");
|
|
expect(trigger("docs")?.getAttribute("href")).toBe("#docs");
|
|
expect(trigger("tour")?.tagName).toBe("BUTTON");
|
|
expect(trigger("invite")?.tagName).toBe("BUTTON");
|
|
expect(trigger("info")?.tagName).toBe("DIV");
|
|
});
|
|
|
|
it("uses the configured title, launcher text, progress text and position", () => {
|
|
createChecklist({
|
|
title: "Welcome",
|
|
launcherText: "Setup",
|
|
progressText: "{{done}}/{{total}} steps",
|
|
position: "bottom-left",
|
|
}).show();
|
|
|
|
expect(document.querySelector(".driver-checklist-title")?.textContent).toBe("Welcome");
|
|
expect(document.querySelector(".driver-checklist-launcher-text")?.textContent).toBe("Setup");
|
|
expect(progressText()).toBe("0/3 steps");
|
|
expect(root()?.classList.contains("driver-checklist-bottom-left")).toBe(true);
|
|
});
|
|
|
|
it("formats the launcher count with launcherProgressText, and hides it when empty", () => {
|
|
const list = createChecklist({ launcherProgressText: "{{done}} of {{total}}" });
|
|
list.show();
|
|
list.complete("tour");
|
|
expect(launcherCount()).toBe("1 of 3");
|
|
list.hide();
|
|
|
|
createChecklist({ launcherProgressText: "" }).show();
|
|
expect(document.querySelector<HTMLElement>(".driver-checklist-launcher-count")?.hidden).toBe(true);
|
|
});
|
|
|
|
it("shows the full label as a tooltip only when it is cut off", () => {
|
|
const widths = { scrollWidth: 300, clientWidth: 200 };
|
|
vi.spyOn(HTMLElement.prototype, "scrollWidth", "get").mockImplementation(() => widths.scrollWidth);
|
|
vi.spyOn(HTMLElement.prototype, "clientWidth", "get").mockImplementation(() => widths.clientWidth);
|
|
|
|
const list = createChecklist({ launcherText: "A <b>very</b> long launcher label" });
|
|
list.show();
|
|
expect(launcher()?.title).toBe("A very long launcher label");
|
|
|
|
widths.scrollWidth = 200;
|
|
window.dispatchEvent(new Event("resize"));
|
|
expect(launcher()?.hasAttribute("title")).toBe(false);
|
|
});
|
|
|
|
it("fills in {{percent}} and {{remaining}}, and exposes the progress to CSS", () => {
|
|
const list = createChecklist({
|
|
progressText: "{{percent}}% complete",
|
|
launcherProgressText: "{{remaining}} left",
|
|
});
|
|
list.show();
|
|
list.complete("tour");
|
|
|
|
expect(progressText()).toBe("33% complete");
|
|
expect(launcherCount()).toBe("2 left");
|
|
expect(root()?.style.getPropertyValue("--driver-checklist-progress")).toBe("33%");
|
|
});
|
|
|
|
it("replaces every occurrence of a placeholder", () => {
|
|
createChecklist({ progressText: "{{done}}/{{total}}, {{done}} done" }).show();
|
|
|
|
expect(progressText()).toBe("0/3, 0 done");
|
|
});
|
|
|
|
it("switches to the complete texts once every item is done, and back when one is unticked", () => {
|
|
const list = createChecklist({
|
|
title: "Get started",
|
|
description: "Three steps.",
|
|
launcherText: "Setup",
|
|
completeTitle: "You're all set! 🎉",
|
|
completeDescription: "Thanks for getting started.",
|
|
completeLauncherText: "All done",
|
|
});
|
|
list.show();
|
|
|
|
const title = () => document.querySelector(".driver-checklist-title")?.textContent;
|
|
const description = () => document.querySelector<HTMLElement>(".driver-checklist-description");
|
|
const launcherText = () => document.querySelector(".driver-checklist-launcher-text")?.textContent;
|
|
|
|
["tour", "docs", "invite"].forEach(id => list.complete(id));
|
|
expect(title()).toBe("You're all set! 🎉");
|
|
expect(description()?.textContent).toBe("Thanks for getting started.");
|
|
expect(launcherText()).toBe("All done");
|
|
expect(panel()?.getAttribute("aria-label")).toBe("You're all set! 🎉");
|
|
|
|
list.uncomplete("docs");
|
|
expect(title()).toBe("Get started");
|
|
expect(description()?.textContent).toBe("Three steps.");
|
|
expect(launcherText()).toBe("Setup");
|
|
});
|
|
|
|
it("keeps the regular texts when no complete texts are set, and hides an empty description", () => {
|
|
createChecklist({ title: "Get started", completed: ["tour", "docs", "invite"] }).show();
|
|
|
|
expect(document.querySelector(".driver-checklist-title")?.textContent).toBe("Get started");
|
|
expect(document.querySelector(".driver-checklist-launcher-text")?.textContent).toBe("Get started");
|
|
expect(document.querySelector<HTMLElement>(".driver-checklist-description")?.hidden).toBe(true);
|
|
});
|
|
|
|
it("can clear the description once done with an empty completeDescription", () => {
|
|
createChecklist({
|
|
description: "Three steps.",
|
|
completeDescription: "",
|
|
completed: ["tour", "docs", "invite"],
|
|
}).show();
|
|
|
|
expect(document.querySelector<HTMLElement>(".driver-checklist-description")?.hidden).toBe(true);
|
|
});
|
|
|
|
it("supports the top corners and a custom offset", () => {
|
|
createChecklist({ position: "top-left", offset: { x: 32, y: 90 } }).show();
|
|
|
|
expect(root()?.classList.contains("driver-checklist-top-left")).toBe(true);
|
|
expect(root()?.style.getPropertyValue("--driver-checklist-offset-x")).toBe("32px");
|
|
expect(root()?.style.getPropertyValue("--driver-checklist-offset-y")).toBe("90px");
|
|
});
|
|
|
|
it("leaves the offset to the stylesheet when it is not set", () => {
|
|
createChecklist({ offset: { y: 0 } }).show();
|
|
|
|
expect(root()?.style.getPropertyValue("--driver-checklist-offset-x")).toBe("");
|
|
expect(root()?.style.getPropertyValue("--driver-checklist-offset-y")).toBe("0px");
|
|
});
|
|
|
|
it("marks items passed in through completed without firing completion hooks", () => {
|
|
const onItemComplete = vi.fn();
|
|
const onComplete = vi.fn();
|
|
createChecklist({ completed: ["tour", "docs", "invite"], onItemComplete, onComplete }).show();
|
|
|
|
expect(isDone("tour")).toBe(true);
|
|
expect(launcherCount()).toBe("3/3");
|
|
expect(root()?.classList.contains("driver-checklist-complete")).toBe(true);
|
|
expect(onItemComplete).not.toHaveBeenCalled();
|
|
expect(onComplete).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not replace the description DOM when the HTML string is unchanged", () => {
|
|
const list = createChecklist({ description: '<img src="a.gif" />' });
|
|
list.show();
|
|
|
|
const img = document.querySelector<HTMLElement>(".driver-checklist-description img");
|
|
list.complete("tour");
|
|
|
|
expect(document.querySelector(".driver-checklist-description img")).toBe(img);
|
|
});
|
|
|
|
it("removes everything on hide()", () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
list.hide();
|
|
|
|
expect(root()).toBeNull();
|
|
expect(list.isVisible()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("opening and closing", () => {
|
|
it("toggles the panel from the launcher", () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
|
|
launcher()?.click();
|
|
expect(panel()?.hidden).toBe(false);
|
|
expect(launcher()?.getAttribute("aria-expanded")).toBe("true");
|
|
expect(list.isOpen()).toBe(true);
|
|
|
|
launcher()?.click();
|
|
expect(panel()?.hidden).toBe(true);
|
|
});
|
|
|
|
it("closes from the close button and on Escape inside the checklist", () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
list.open();
|
|
|
|
document.querySelector<HTMLButtonElement>(".driver-checklist-close-btn")?.click();
|
|
expect(list.isOpen()).toBe(false);
|
|
expect(document.activeElement).toBe(launcher());
|
|
|
|
list.open();
|
|
trigger("tour")?.focus();
|
|
pressKey("Escape");
|
|
expect(list.isOpen()).toBe(false);
|
|
});
|
|
|
|
it("renders without a launcher when launcher is false", () => {
|
|
const list = createChecklist({ launcher: false });
|
|
list.show();
|
|
|
|
expect(launcher()).toBeNull();
|
|
expect(root()?.classList.contains("driver-checklist-no-launcher")).toBe(true);
|
|
|
|
list.open();
|
|
expect(panel()?.hidden).toBe(false);
|
|
});
|
|
|
|
it("hands focus back to the page's own trigger when there is no launcher", () => {
|
|
const list = createChecklist({ launcher: false });
|
|
list.show();
|
|
|
|
const own = document.getElementById("card-1") as HTMLButtonElement;
|
|
own.focus();
|
|
list.open();
|
|
trigger("tour")?.focus();
|
|
pressKey("Escape");
|
|
|
|
expect(list.isOpen()).toBe(false);
|
|
expect(document.activeElement).toBe(own);
|
|
|
|
own.focus();
|
|
list.open();
|
|
document.querySelector<HTMLButtonElement>(".driver-checklist-close-btn")?.click();
|
|
expect(document.activeElement).toBe(own);
|
|
});
|
|
|
|
it("ignores Escape pressed elsewhere on the page", () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
list.open();
|
|
|
|
pressKey("Escape");
|
|
|
|
expect(list.isOpen()).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("progress", () => {
|
|
it("completes items, reports progress and fires onComplete once", () => {
|
|
const onItemComplete = vi.fn();
|
|
const onProgress = vi.fn();
|
|
const onComplete = vi.fn();
|
|
const list = createChecklist({ onItemComplete, onProgress, onComplete });
|
|
list.show();
|
|
|
|
list.complete("tour");
|
|
expect(isDone("tour")).toBe(true);
|
|
expect(onItemComplete).toHaveBeenCalledWith(ITEMS[0], { checklist: list });
|
|
expect(onProgress).toHaveBeenLastCalledWith({ completed: ["tour"], done: 1, total: 3 });
|
|
|
|
list.complete("tour");
|
|
expect(onItemComplete).toHaveBeenCalledTimes(1);
|
|
|
|
list.complete("docs");
|
|
list.complete("invite");
|
|
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
expect(root()?.classList.contains("driver-checklist-complete")).toBe(true);
|
|
});
|
|
|
|
it("ignores unknown item ids", () => {
|
|
const onProgress = vi.fn();
|
|
const list = createChecklist({ onProgress });
|
|
|
|
list.complete("missing");
|
|
|
|
expect(onProgress).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps completed ids of items that are not in the list", () => {
|
|
const list = createChecklist({ completed: ["elsewhere"] });
|
|
|
|
list.complete("docs");
|
|
|
|
expect(list.getProgress()).toEqual({ completed: ["elsewhere", "docs"], done: 1, total: 3 });
|
|
});
|
|
|
|
it("works before show() and renders the progress once shown", () => {
|
|
const list = createChecklist();
|
|
list.complete("docs");
|
|
list.show();
|
|
|
|
expect(isDone("docs")).toBe(true);
|
|
});
|
|
|
|
it("clears progress on reset()", () => {
|
|
const onProgress = vi.fn();
|
|
const onComplete = vi.fn();
|
|
const list = createChecklist({ completed: ["tour", "docs"], onProgress, onComplete });
|
|
list.show();
|
|
|
|
list.reset();
|
|
expect(isDone("tour")).toBe(false);
|
|
expect(onProgress).toHaveBeenLastCalledWith({ completed: [], done: 0, total: 3 });
|
|
|
|
list.complete("tour");
|
|
list.complete("docs");
|
|
list.complete("invite");
|
|
expect(onComplete).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("completes items whose completeWhen passes on show() and refresh()", () => {
|
|
let connected = false;
|
|
const list = createChecklist({
|
|
items: [{ id: "github", title: "Connect GitHub", completeWhen: () => connected }],
|
|
});
|
|
|
|
list.show();
|
|
expect(isDone("github")).toBe(false);
|
|
|
|
connected = true;
|
|
list.refresh();
|
|
expect(isDone("github")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("tours", () => {
|
|
it("closes the panel, runs the tour and ticks the item when it is finished", async () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
list.open();
|
|
|
|
trigger("tour")?.click();
|
|
expect(list.isOpen()).toBe(false);
|
|
expect(document.body.classList.contains("driver-active")).toBe(true);
|
|
|
|
await finishTour();
|
|
|
|
expect(document.body.classList.contains("driver-active")).toBe(false);
|
|
expect(isDone("tour")).toBe(true);
|
|
expect(list.isOpen()).toBe(true);
|
|
});
|
|
|
|
it("hands focus back to the item once its tour ends", async () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
list.open();
|
|
|
|
trigger("tour")?.focus();
|
|
trigger("tour")?.click();
|
|
// Browsers drop focus from the hidden panel; jsdom keeps it.
|
|
trigger("tour")?.blur();
|
|
await finishTour();
|
|
|
|
expect(document.activeElement).toBe(trigger("tour"));
|
|
});
|
|
|
|
it("leaves the item open when the tour is closed early, and reopens the panel", async () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
|
|
list.start("tour");
|
|
await nextFrame();
|
|
navButton("close")?.click();
|
|
|
|
expect(isDone("tour")).toBe(false);
|
|
expect(list.isOpen()).toBe(true);
|
|
});
|
|
|
|
it("accepts plain steps", async () => {
|
|
const list = createChecklist({
|
|
items: [{ id: "steps", title: "Steps", tour: [{ element: "#intro", popover: { title: "Only" } }] }],
|
|
});
|
|
list.show();
|
|
|
|
list.start("steps");
|
|
// Plain steps get the default animation; wait for it to settle.
|
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
navButton("next")?.click();
|
|
|
|
expect(isDone("steps")).toBe(true);
|
|
});
|
|
|
|
it("runs a driver instance, keeping and then restoring its own onDestroyed", async () => {
|
|
const onDestroyed = vi.fn();
|
|
activeDriver = driver({ animate: false, steps: SAMPLE_STEPS.slice(0, 2), onDestroyed });
|
|
const list = createChecklist({ items: [{ id: "own", title: "Own tour", tour: activeDriver }] });
|
|
list.show();
|
|
|
|
list.start("own");
|
|
await finishTour();
|
|
|
|
expect(onDestroyed).toHaveBeenCalledTimes(1);
|
|
expect(onDestroyed.mock.calls[0][2].reason).toBe("done");
|
|
expect(isDone("own")).toBe(true);
|
|
expect(activeDriver.getConfig().onDestroyed).toBe(onDestroyed);
|
|
});
|
|
|
|
it("does not reopen when another tour replaces the item's tour", async () => {
|
|
const list = createChecklist();
|
|
list.show();
|
|
|
|
list.start("tour");
|
|
await nextFrame();
|
|
activeDriver = driver({ animate: false, steps: SAMPLE_STEPS });
|
|
activeDriver.drive();
|
|
|
|
expect(isDone("tour")).toBe(false);
|
|
expect(list.isOpen()).toBe(false);
|
|
});
|
|
|
|
it("lets completeWhen decide for tour items", async () => {
|
|
let created = false;
|
|
const list = createChecklist({
|
|
items: [{ ...ITEMS[0], completeWhen: () => created }],
|
|
});
|
|
list.show();
|
|
|
|
list.start("tour");
|
|
await finishTour();
|
|
expect(isDone("tour")).toBe(false);
|
|
|
|
created = true;
|
|
list.refresh();
|
|
expect(isDone("tour")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("links and actions", () => {
|
|
it("ticks a link item on click and lets the link navigate", () => {
|
|
let defaultPrevented: boolean | undefined;
|
|
document.addEventListener("click", event => {
|
|
defaultPrevented = event.defaultPrevented;
|
|
event.preventDefault();
|
|
});
|
|
|
|
createChecklist().show();
|
|
trigger("docs")?.click();
|
|
|
|
expect(isDone("docs")).toBe(true);
|
|
expect(defaultPrevented).toBe(false);
|
|
});
|
|
|
|
it("navigates to the link when started from code", () => {
|
|
const assign = vi.fn();
|
|
vi.spyOn(window, "location", "get").mockReturnValue({ ...window.location, assign });
|
|
|
|
createChecklist().start("docs");
|
|
|
|
expect(assign).toHaveBeenCalledWith("#docs");
|
|
});
|
|
|
|
it("runs the action and ticks the item", () => {
|
|
const action = vi.fn();
|
|
const list = createChecklist({ items: [{ id: "invite", title: "Invite", action }] });
|
|
list.show();
|
|
|
|
trigger("invite")?.click();
|
|
|
|
expect(action).toHaveBeenCalledWith(list.getItems()[0], { checklist: list });
|
|
expect(isDone("invite")).toBe(true);
|
|
});
|
|
|
|
it("checks completeWhen after the action instead of ticking right away", () => {
|
|
let invited = false;
|
|
const list = createChecklist({
|
|
items: [{ id: "invite", title: "Invite", action: () => {}, completeWhen: () => invited }],
|
|
});
|
|
list.show();
|
|
|
|
trigger("invite")?.click();
|
|
expect(isDone("invite")).toBe(false);
|
|
|
|
invited = true;
|
|
trigger("invite")?.click();
|
|
expect(isDone("invite")).toBe(true);
|
|
});
|
|
|
|
it("keeps focus on the clicked item after the list re-renders", () => {
|
|
createChecklist().show();
|
|
trigger("invite")?.focus();
|
|
|
|
trigger("invite")?.click();
|
|
|
|
expect(document.activeElement).toBe(trigger("invite"));
|
|
});
|
|
});
|
|
|
|
describe("manual completion", () => {
|
|
const check = (id: string) => trigger(id)?.parentElement?.querySelector<HTMLElement>(".driver-checklist-check");
|
|
|
|
it("keeps the check circle decorative by default, starting the item on click", () => {
|
|
const action = vi.fn();
|
|
createChecklist({ items: [{ id: "invite", title: "Invite", action }] }).show();
|
|
|
|
expect(check("invite")?.tagName).toBe("SPAN");
|
|
check("invite")?.click();
|
|
|
|
expect(action).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("lets users tick and untick items with allowManualComplete", () => {
|
|
const onProgress = vi.fn();
|
|
const onEvent = vi.fn();
|
|
createChecklist({ allowManualComplete: true, onProgress, onEvent }).show();
|
|
|
|
expect(check("tour")?.tagName).toBe("BUTTON");
|
|
expect(check("tour")?.getAttribute("aria-label")).toBe('Mark "Take the tour" as done');
|
|
|
|
check("tour")?.click();
|
|
expect(isDone("tour")).toBe(true);
|
|
expect(check("tour")?.getAttribute("aria-pressed")).toBe("true");
|
|
expect(document.body.classList.contains("driver-active")).toBe(false);
|
|
|
|
check("tour")?.click();
|
|
expect(isDone("tour")).toBe(false);
|
|
expect(onProgress).toHaveBeenLastCalledWith({ completed: [], done: 0, total: 3 });
|
|
expect(onEvent.mock.calls.map(([event]) => event.type)).toContain("item_uncompleted");
|
|
});
|
|
|
|
it("ticks and unticks a row with nothing to start when its text is clicked", () => {
|
|
createChecklist({ allowManualComplete: true, items: [{ id: "plain", title: "Plain" }] }).show();
|
|
|
|
trigger("plain")?.click();
|
|
expect(isDone("plain")).toBe(true);
|
|
|
|
trigger("plain")?.click();
|
|
expect(isDone("plain")).toBe(false);
|
|
});
|
|
|
|
it("leaves rows with nothing to start alone without allowManualComplete", () => {
|
|
createChecklist({ items: [{ id: "plain", title: "Plain" }] }).show();
|
|
|
|
trigger("plain")?.click();
|
|
expect(isDone("plain")).toBe(false);
|
|
});
|
|
|
|
it("can be turned on or off per item", () => {
|
|
createChecklist({
|
|
allowManualComplete: true,
|
|
items: [
|
|
{ id: "a", title: "A", allowManualComplete: false },
|
|
{ id: "b", title: "B" },
|
|
],
|
|
}).show();
|
|
|
|
expect(check("a")?.tagName).toBe("SPAN");
|
|
expect(check("b")?.tagName).toBe("BUTTON");
|
|
});
|
|
|
|
it("fires onComplete again after an item is unticked and the list is finished again", () => {
|
|
const onComplete = vi.fn();
|
|
const list = createChecklist({ completed: ["tour", "docs"], onComplete });
|
|
list.show();
|
|
|
|
list.complete("invite");
|
|
list.uncomplete("invite");
|
|
list.complete("invite");
|
|
|
|
expect(onComplete).toHaveBeenCalledTimes(2);
|
|
});
|
|
});
|
|
|
|
describe("events", () => {
|
|
it("reports what happens through onEvent", async () => {
|
|
const onEvent = vi.fn();
|
|
const list = createChecklist({ onEvent });
|
|
list.show();
|
|
list.open();
|
|
list.start("tour");
|
|
await nextFrame();
|
|
navButton("close")?.click();
|
|
list.complete("docs");
|
|
|
|
const events = onEvent.mock.calls.map(([event]) => [event.type, event.itemId, event.reason]);
|
|
expect(events).toEqual([
|
|
["shown", undefined, undefined],
|
|
["opened", undefined, undefined],
|
|
["item_started", "tour", undefined],
|
|
["closed", undefined, undefined],
|
|
["tour_ended", "tour", "close"],
|
|
["opened", undefined, undefined],
|
|
["item_completed", "docs", undefined],
|
|
]);
|
|
expect(onEvent.mock.calls[0][0]).toMatchObject({ kind: "checklist", id: "onboarding" });
|
|
expect(typeof onEvent.mock.calls[0][0].timestamp).toBe("number");
|
|
});
|
|
});
|
|
|
|
describe("ids", () => {
|
|
it("warns about duplicate and missing ids", () => {
|
|
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
|
|
createChecklist({
|
|
items: [
|
|
{ id: "a", title: "A" },
|
|
{ id: "a", title: "Again" },
|
|
{ id: "", title: "Nameless" },
|
|
],
|
|
});
|
|
|
|
expect(warn).toHaveBeenCalledWith('[driver.js] Duplicate checklist item id "a".');
|
|
expect(warn).toHaveBeenCalledWith('[driver.js] Checklist item "Nameless" is missing an id.');
|
|
});
|
|
});
|