Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion src/pages/ModelChooser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs';
import { StrictMode } from 'react';
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter, useLocation } from 'react-router-dom';
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
import { describe, expect, test } from 'vitest';
import { findLesson } from '../content/manifest';
import { DATASET_FILES } from '../r/session';
Expand All @@ -15,12 +15,19 @@ function Location() {
return <p data-testid="location">{location.pathname + location.search}</p>;
}

/** Stands in for the browser's Back button. */
function BrowserBack() {
const navigate = useNavigate();
return <button type="button" onClick={() => navigate(-1)}>Browser back</button>;
}

function renderChooser(url = '/which-model') {
return render(
<StrictMode>
<MemoryRouter initialEntries={[url]}>
<ModelChooser />
<Location />
<BrowserBack />
</MemoryRouter>
</StrictMode>,
);
Expand Down Expand Up @@ -556,3 +563,18 @@ describe('research question', () => {
expect(screen.getByText(/comes from keywords in your question, not from AI/i)).toBeTruthy();
});
});

test("the browser's Back button goes back one question, not off the page", async () => {
const user = userEvent.setup();
renderChooser();
const first = heading().textContent;
await user.click(screen.getByRole('button', { name: TO_OUTCOME }));
const second = heading().textContent;
await user.click(screen.getByRole('button', { name: /^a number/i }));
expect(heading().textContent).not.toBe(second);
await user.click(screen.getByRole('button', { name: 'Browser back' }));
expect(heading().textContent).toBe(second);
await user.click(screen.getByRole('button', { name: 'Browser back' }));
expect(heading().textContent).toBe(first);
expect(screen.getByTestId('location').textContent).toBe('/which-model');
});
11 changes: 7 additions & 4 deletions src/pages/ModelChooser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,13 @@ const startsWith = (path: string[], prefix: string[]) => prefix.every((label, i)

export default function ModelChooser() {
const [searchParams, setSearchParams] = useSearchParams();
const [path, setPath] = useState<string[]>(() => pathTo(searchParams.get('model') ?? '') ?? []);
// The answers so far live in the URL, one history entry per question, so the browser's Back button goes back one question.
const path = useMemo(() => {
const model = searchParams.get('model');
return model ? (pathTo(model) ?? []) : follow(searchParams.getAll('step')).path;
}, [searchParams]);
// The furthest path taken or suggested, so stepping back shows the choice made there.
const [planned, setPlanned] = useState<{ path: string[]; suggested: boolean }>({ path, suggested: false });
const [planned, setPlanned] = useState<{ path: string[]; suggested: boolean }>(() => ({ path, suggested: false }));
const { node } = follow(path);
const headingRef = useRef<HTMLHeadingElement>(null);
const treeRef = useRef<HTMLElement>(null);
Expand All @@ -391,10 +395,9 @@ export default function ModelChooser() {

function go(next: string[], plan?: { path: string[]; suggested: boolean }) {
moveFocus.current = true;
setPath(next);
setPlanned(plan ?? (startsWith(planned.path, next) ? planned : { path: next, suggested: false }));
const reached = follow(next).node;
setSearchParams(reached.kind === 'answer' ? { model: reached.id } : {}, { replace: true });
setSearchParams(reached.kind === 'answer' ? { model: reached.id } : { step: next });
}

function pick(id: string) {
Expand Down
21 changes: 20 additions & 1 deletion src/pages/SampleSize.test.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom';
import { MemoryRouter, useNavigate } from 'react-router-dom';
import { describe, expect, test } from 'vitest';
import { findLesson } from '../content/manifest';
import SampleSize from './SampleSize';
import { DESIGNS } from './sampleSizeDesigns';

/** Stands in for the browser's Back button. */
function BrowserBack() {
const navigate = useNavigate();
return <button type="button" onClick={() => navigate(-1)}>Browser back</button>;
}

function setup() {
const user = userEvent.setup();
render(
<MemoryRouter>
<SampleSize />
<BrowserBack />
</MemoryRouter>,
);
const pick = (name: RegExp) => user.click(screen.getByRole('button', { name }));
Expand Down Expand Up @@ -185,6 +192,18 @@ describe('sample size page', () => {
expect(document.querySelector('.ss-meaning')!.textContent).toContain('f = 0.16');
});

test("the browser's Back button goes back one question, keeping the answers", async () => {
const { user, pick } = setup();
await pick(/^Compare two separate groups/);
await pick(/How many people do I need/);
expect(question()).toBe('How big is the effect you want to be able to find?');
await user.click(screen.getByRole('button', { name: 'Browser back' }));
expect(question()).toBe('What do you want to find out?');
expect(screen.getByRole('button', { name: /How many people do I need/ }).textContent).toContain('Your earlier choice');
await user.click(screen.getByRole('button', { name: 'Browser back' }));
expect(question()).toBe('What will you test?');
});

test('every design links to a lesson that exists', () => {
for (const design of DESIGNS) expect(findLesson(design.lessonId), design.id).toBeTruthy();
});
Expand Down
16 changes: 10 additions & 6 deletions src/pages/SampleSize.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { Link, useSearchParams } from 'react-router-dom';
import AvatarTip from '../components/AvatarTip';
import { pnorm } from '../stats/distributions';
import { HAS_SIDES, minN, PER_GROUP, powerAt, rCode, solveEffect, solveN, stimulusSimCode, totalN, type DesignId, type Plan } from '../stats/power';
Expand Down Expand Up @@ -222,7 +222,11 @@ function Choice<T>({ options, chosen, onPick }: { options: Option<T>[]; chosen:

export default function SampleSize() {
const [answers, setAnswers] = useState<Answers>(START);
const [at, setAt] = useState(0);
// The step lives in the URL, one history entry per question, so the browser's Back button goes back one question.
// Answers are not in the URL, so a step is only shown once it has been reached in this visit.
const [params, setParams] = useSearchParams();
const [reached, setReached] = useState(0);
const at = Math.min(Math.max(0, Math.floor(Number(params.get('step')) || 0)), reached);
const [copied, setCopied] = useState('');
const headingRef = useRef<HTMLHeadingElement>(null);
const moveFocus = useRef(false);
Expand All @@ -240,12 +244,12 @@ export default function SampleSize() {

function go(index: number) {
moveFocus.current = true;
setAt(index);
setParams(index > 0 ? { step: String(index) } : {});
}
function answer(patch: Partial<Answers>) {
moveFocus.current = true;
setAnswers((prev) => ({ ...prev, ...patch }));
setAt((i) => i + 1);
setReached(at + 1);
go(at + 1);
}
function type(key: string, value: string) {
setAnswers((prev) => ({ ...prev, text: { ...prev.text, [key]: value } }));
Expand Down Expand Up @@ -603,7 +607,7 @@ export default function SampleSize() {
</ol>
<div className="model-chooser-trail-actions">
<button type="button" onClick={() => go(at - 1)}>Back</button>
<button type="button" onClick={() => { go(0); setAnswers(START); }}>Start over</button>
<button type="button" onClick={() => { go(0); setReached(0); setAnswers(START); }}>Start over</button>
</div>
</nav>
)}
Expand Down