Owner feedback on the forced tour. Each step rings what it is about and, while forced, dims the rest of the page around it. "Pick a layout" and the Timeline step hold Continue until both options have been on screen, scroll their subject to the top of the window and, on wide screens, move the card to the right edge so the page has room to show the difference. Guards for the reported snap-back and endless tour, which did not reproduce: the effect that moves an unseen reader to Relaxed fires at most once per page load and never after the tab saw the tour done, and setup saves only the prefs it changed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
import { renderToStaticMarkup as render } from "react-dom/server";
|
|
import { SetupWizard } from "../src/client/components/redesign/SetupWizard.tsx";
|
|
import { ReturningTour } from "../src/client/components/redesign/ReturningTour.tsx";
|
|
import { migrateLegacy } from "../src/client/state/reader/migration.ts";
|
|
test("initial preferences follow the Round 6 setup and defer upcoming events to tracking", () => {
|
|
const s = migrateLegacy(
|
|
{},
|
|
[],
|
|
"2026-10-16T12:00:00.000Z",
|
|
() => "account:test",
|
|
);
|
|
const html = render(<SetupWizard state={s} onFinish={async () => {}} />);
|
|
for (const label of [
|
|
"Step 1 of 4",
|
|
"Make it yours",
|
|
"Default game server",
|
|
"Appearance",
|
|
"Colour scheme",
|
|
"Your timezone",
|
|
"Next: choose your games",
|
|
])
|
|
expect(html).toContain(label);
|
|
expect(html).not.toContain("Show events that haven't started");
|
|
});
|
|
import {
|
|
TOUR_DONE,
|
|
TOUR_STEPS,
|
|
tourMode,
|
|
untriedOptions,
|
|
} from "../src/client/components/redesign/ReturningTour.tsx";
|
|
import { setupReader } from "../src/client/state/reader/onboarding.ts";
|
|
|
|
const tourProps = {
|
|
looks: { interface: "relaxed", timelineLook: "new" },
|
|
onLook: () => {},
|
|
onStep: () => {},
|
|
onFinish: () => {},
|
|
};
|
|
test("the tour opens on its first step, with no way to skip it when forced", () => {
|
|
const html = render(
|
|
<ReturningTour {...tourProps} intro="returning" closable={false} />,
|
|
);
|
|
expect(html).toContain("Welcome back. A little has moved");
|
|
expect(html).toContain("1 of " + TOUR_STEPS.length);
|
|
expect(html).toContain(TOUR_STEPS[0]!.title);
|
|
expect(html).not.toContain("Skip");
|
|
expect(html).not.toContain("Close tour");
|
|
expect(html).not.toContain("Keep browsing");
|
|
});
|
|
test("a new reader is welcomed, and a replay can be closed", () => {
|
|
expect(
|
|
render(<ReturningTour {...tourProps} intro="new" closable={false} />),
|
|
).toContain("Welcome to Event Clock");
|
|
const replay = render(<ReturningTour {...tourProps} closable />);
|
|
expect(replay).toContain("Close tour");
|
|
expect(replay).not.toContain("Welcome");
|
|
});
|
|
test("the tour lets the reader try both layouts and both Timeline looks", () => {
|
|
const choices = TOUR_STEPS.filter((s) => s.choice).map((s) => [
|
|
s.stop,
|
|
s.choice,
|
|
]);
|
|
expect(choices).toEqual([
|
|
["soon", "interface"],
|
|
["timeline", "timelineLook"],
|
|
]);
|
|
});
|
|
test("a choice holds Continue until both options have been tried", () => {
|
|
expect(
|
|
untriedOptions("interface", ["structured"]).map((o) => o.label),
|
|
).toEqual(["Relaxed"]);
|
|
expect(untriedOptions("interface", ["structured", "relaxed"])).toEqual([]);
|
|
expect(untriedOptions("timelineLook", []).map((o) => o.value)).toEqual([
|
|
"new",
|
|
"classic",
|
|
]);
|
|
});
|
|
test("every step highlights what it is about", () => {
|
|
for (const step of TOUR_STEPS) expect(step.target).toBeTruthy();
|
|
expect(TOUR_STEPS.find((s) => s.stop === "routines")!.target).toBe(
|
|
'[data-page="routines"]',
|
|
);
|
|
});
|
|
test("the tour introduces the Timeline right after the Checklist, and codes", () => {
|
|
const stops = TOUR_STEPS.map((s) => s.stop);
|
|
expect(stops.indexOf("timeline")).toBe(stops.lastIndexOf("soon") + 1);
|
|
expect(stops).toContain("codes");
|
|
});
|
|
test("everyone takes this tour once, including readers who saw the last one", () => {
|
|
expect(tourMode(TOUR_DONE)).toBe("done");
|
|
for (const seen of [undefined, null, "2026-10-redesign", 7])
|
|
expect(tourMode(seen)).toBe("unseen");
|
|
expect(tourMode("new")).toBe("new");
|
|
expect(tourMode("returning")).toBe("returning");
|
|
expect(tourMode("replay")).toBe("replay");
|
|
});
|
|
test("finishing setup sends a new reader into the forced tour", () => {
|
|
const s = migrateLegacy({}, [], "2026-10-16T12:00:00.000Z", () => "a");
|
|
const next = setupReader(
|
|
{
|
|
region: "europe",
|
|
interface: "structured",
|
|
theme: "dark",
|
|
scheme: "default",
|
|
timezone: null,
|
|
showUpcoming: true,
|
|
view: "soon",
|
|
games: [],
|
|
tracking: {},
|
|
},
|
|
s,
|
|
"2026-10-16T12:00:00.000Z",
|
|
() => "account:b",
|
|
);
|
|
expect(tourMode(next.prefs.redesignTourSeen)).toBe("new");
|
|
expect(next.prefs.interface).toBe("structured");
|
|
});
|