blindspot / frontend /src /tutorial /steps.test.ts
yashwrites's picture
Signs on the film, draw tool, My search off by default, example films and Radiopaedia links (part 2)
2e6a666 verified
Raw History Blame Contribute Delete
6.53 kB
import { describe, expect, it } from 'vitest';
import { availableSteps, hasTutorialFlag, placeCard, SEARCH_TIP_STEPS, shouldOpenSearchTip, shouldOpenTutorial, STEPS, tourStep, type Rect } from './steps';
describe('tutorial steps', () => {
it('has the seven X-ray steps in order, each pointing at a real control; the two volume steps sit between them', () => {
expect(STEPS.filter((s) => !s.volumetric).map((s) => s.id)).toEqual(['film', 'magnifier', 'pick', 'confidence', 'whole', 'hints', 'submit']);
expect(STEPS.map((s) => s.id)).toEqual(['film', 'slices', 'magnifier', 'pick', 'confidence', 'measure', 'whole', 'hints', 'submit']);
for (const s of STEPS) expect(s.target).toMatch(/^\[data-tour="[a-z-]+"\]$/);
});
it('a chest film never sees the volume steps, even when their controls are on the page; a CT sees them only when they are', () => {
expect(availableSteps(() => true).map((s) => s.id)).toEqual(['film', 'magnifier', 'pick', 'confidence', 'whole', 'hints', 'submit']);
expect(availableSteps(() => true, STEPS, true).map((s) => s.id)).toEqual(['film', 'slices', 'magnifier', 'pick', 'confidence', 'measure', 'whole', 'hints', 'submit']);
expect(availableSteps((sel) => sel !== '[data-tour="measure"]', STEPS, true).map((s) => s.id)).not.toContain('measure');
});
it('step 3 mentions tracing an outline with D (round 5)', () => {
expect(STEPS.find((s) => s.id === 'pick')!.body).toMatch(/trace an outline .* with D/);
});
it('the search tip is one step on the "My search" toggle, and opens once per browser (automation: only with ?tutorial=1)', () => {
expect(SEARCH_TIP_STEPS).toHaveLength(1);
expect(SEARCH_TIP_STEPS[0]).toMatchObject({ id: 'search', target: '[data-tour="my-search"]', title: 'See how you looked' });
expect(SEARCH_TIP_STEPS[0].body).toContain('Turn on My search');
expect(availableSteps(() => true, SEARCH_TIP_STEPS)).toHaveLength(1);
expect(availableSteps(() => false, SEARCH_TIP_STEPS)).toHaveLength(0);
expect(shouldOpenSearchTip({ webdriver: false, bootFlag: false, done: false })).toBe(true);
expect(shouldOpenSearchTip({ webdriver: false, bootFlag: false, done: true })).toBe(false);
expect(shouldOpenSearchTip({ webdriver: true, bootFlag: false, done: false })).toBe(false);
expect(shouldOpenSearchTip({ webdriver: true, bootFlag: true, done: true })).toBe(true);
});
it('never mentions a points cost for hints', () => {
for (const s of STEPS) expect(`${s.title} ${s.body}`).not.toMatch(/points?|cost/i);
});
it('keeps the reference tip when hints are off (test set), and drops steps whose control is missing', () => {
const noHints = availableSteps((sel) => sel !== '[data-tour="hints"]');
expect(noHints).toHaveLength(7);
expect(noHints[5]).toMatchObject({ id: 'hints', target: '[data-tour="pick"]', title: 'Not sure what a finding looks like?' });
expect(availableSteps((sel) => sel !== '[data-tour="magnifier"]').map((s) => s.id)).not.toContain('magnifier');
expect(availableSteps(() => false)).toEqual([]);
});
});
describe('shouldOpenTutorial', () => {
const base = { webdriver: false, bootFlag: false, urlFlag: false, done: false, firstCase: false };
it('opens on a first session in a real browser', () => {
expect(shouldOpenTutorial({ ...base, urlFlag: true })).toBe(true);
expect(shouldOpenTutorial({ ...base, firstCase: true })).toBe(true);
expect(shouldOpenTutorial(base)).toBe(false);
});
it('stays closed once finished or skipped', () => {
expect(shouldOpenTutorial({ ...base, urlFlag: true, firstCase: true, done: true })).toBe(false);
});
it('under automation opens only when the page was loaded with ?tutorial=1', () => {
expect(shouldOpenTutorial({ ...base, webdriver: true, urlFlag: true, firstCase: true })).toBe(false);
expect(shouldOpenTutorial({ ...base, webdriver: true, bootFlag: true })).toBe(true);
expect(shouldOpenTutorial({ ...base, webdriver: true, bootFlag: true, done: true })).toBe(true);
});
it('reads the flag from a query string', () => {
expect(hasTutorialFlag('?mock=1&tutorial=1')).toBe(true);
expect(hasTutorialFlag('?tutorial=0')).toBe(false);
expect(hasTutorialFlag('')).toBe(false);
});
});
describe('tourStep', () => {
it('walks forward and back and ends after the last step', () => {
expect(tourStep({ i: 0, n: 7 }, 'next')).toEqual({ i: 1, n: 7 });
expect(tourStep({ i: 0, n: 7 }, 'back')).toEqual({ i: 0, n: 7 });
expect(tourStep({ i: 3, n: 7 }, 'back')).toEqual({ i: 2, n: 7 });
expect(tourStep({ i: 6, n: 7 }, 'next')).toBeNull();
});
});
describe('placeCard', () => {
const vp = { w: 1280, h: 800 };
const film: Rect = { left: 0, top: 90, width: 870, height: 664 };
const card = { w: 320, h: 190 };
const hit = (a: Rect, b: Rect) => a.left < b.left + b.width && b.left < a.left + a.width && a.top < b.top + b.height && b.top < a.top + a.height;
const rectOf = (p: { left: number; top: number }): Rect => ({ left: p.left, top: p.top, width: card.w, height: card.h });
it('puts the film step beside the film, not on it', () => {
const p = placeCard(film, card, vp, film);
expect(p.side).toBe('right');
expect(hit(rectOf(p), film)).toBe(false);
});
it('keeps every rail and toolbar step off the film', () => {
const targets: Rect[] = [
{ left: 730, top: 760, width: 130, height: 30 }, // magnifier button, right end of the bar under the film
{ left: 892, top: 150, width: 366, height: 200 }, // pick list
{ left: 892, top: 360, width: 366, height: 70 }, // marks
{ left: 892, top: 440, width: 366, height: 300 }, // whole film + normal
{ left: 892, top: 600, width: 366, height: 60 }, // hints
{ left: 892, top: 700, width: 366, height: 90 }, // submit bar
];
for (const t of targets) {
const p = placeCard(t, card, vp, film);
expect(hit(rectOf(p), film), `card for target at ${t.left},${t.top} (${p.side})`).toBe(false);
expect(hit(rectOf(p), t)).toBe(false);
expect(p.left).toBeGreaterThanOrEqual(8);
expect(p.left + card.w).toBeLessThanOrEqual(vp.w - 8);
expect(p.top).toBeGreaterThanOrEqual(8);
expect(p.top + card.h).toBeLessThanOrEqual(vp.h - 8);
}
});
it('falls back to sitting inside a target that fills the viewport', () => {
const p = placeCard({ left: 0, top: 0, width: 1280, height: 800 }, card, vp, null);
expect(p.side).toBe('inside');
expect(p.top + card.h).toBeLessThanOrEqual(vp.h - 8);
});
});