Skip to content

Commit cf02c84

Browse files
jeremymanningclaude
andcommitted
Fix button positioning, tutorial API, and test reliability
- Move reset/download/upload buttons to LEFT side of header (insertBefore) - Add goToStep export to tutorial.js for programmatic step navigation - Expose tutorialGoToStep on window.__mapper for test access - Fix tutorial-verify tests to use goToStep instead of localStorage - Fix answer timing in tests: waitForFunction instead of fixed timeouts - Add null-check for Issue #48 suggest modal element Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent dec2ace commit cf02c84

4 files changed

Lines changed: 74 additions & 36 deletions

File tree

src/app.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import * as modes from './ui/modes.js';
3030
import * as insights from './ui/insights.js';
3131
import * as share from './ui/share.js';
3232
import { checkMilestone, highlightExpertiseButton, updateProgressDisplay } from './ui/milestones.js';
33-
import { initTutorial, advanceTutorial, isTutorialActive, resetTutorial, dismissTutorial, setAnswerFeedback } from './ui/tutorial.js';
33+
import { initTutorial, advanceTutorial, isTutorialActive, resetTutorial, dismissTutorial, setAnswerFeedback, goToStep as tutorialGoToStep } from './ui/tutorial.js';
3434
import * as videoModal from './ui/video-modal.js';
3535
import * as videoLoader from './domain/video-loader.js';
3636
import * as videoPanel from './ui/video-panel.js';
@@ -176,15 +176,16 @@ async function boot() {
176176
controls.onExport(handleExport);
177177
controls.onImport(handleImport);
178178

179-
// Move action buttons (upload/download/reset) from domain-selector to header-right
180-
// Append AFTER main icons so they're off-screen to the right on mobile (scroll right to reveal)
179+
// Move action buttons (reset/download/upload) from domain-selector to header-right
180+
// Insert BEFORE main icons (left side, after map icon/dropdown)
181181
const headerRight = headerEl.querySelector('.header-right');
182182
const actionBtns = controls.getActionButtons();
183183
if (headerRight && actionBtns.importButton) {
184-
// Append in order: reset, download, upload (revealed by scrolling right)
185-
headerRight.appendChild(actionBtns.resetButton);
186-
headerRight.appendChild(actionBtns.exportButton);
187-
headerRight.appendChild(actionBtns.importButton);
184+
const first = headerRight.firstChild;
185+
// Insert in order: reset, download, upload (left side of icon bar)
186+
headerRight.insertBefore(actionBtns.resetButton, first);
187+
headerRight.insertBefore(actionBtns.exportButton, first);
188+
headerRight.insertBefore(actionBtns.importButton, first);
188189
for (const btn of [actionBtns.resetButton, actionBtns.exportButton, actionBtns.importButton]) {
189190
btn.classList.add('btn-icon');
190191
}
@@ -324,7 +325,7 @@ async function boot() {
324325
}
325326

326327
if (import.meta.env.DEV) {
327-
window.__mapper = { registry, estimator, sampler, renderer, minimap, $activeDomain, $estimates, $responses, getCurrentQuestion: quiz.getCurrentQuestion };
328+
window.__mapper = { registry, estimator, sampler, renderer, minimap, $activeDomain, $estimates, $responses, getCurrentQuestion: quiz.getCurrentQuestion, tutorialGoToStep };
328329
}
329330

330331
const quizToggle = document.getElementById('quiz-toggle');

src/ui/tutorial.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,20 @@ export function getTutorialStep() {
436436
return { step: state.step, subStep: state.subStep };
437437
}
438438

439+
/** Programmatically jump to a specific tutorial step (for testing). */
440+
export function goToStep(step, subStep = 1) {
441+
if (!state) state = defaultState();
442+
state.completed = false;
443+
state.dismissed = false;
444+
state.welcomeShown = true;
445+
state.step = step;
446+
state.subStep = subStep;
447+
_questionsAnsweredInStep = 0;
448+
_inFollowUp = false;
449+
saveState();
450+
renderCurrentStep();
451+
}
452+
439453
// ── Step navigation helpers ─────────────────────────────────────────
440454

441455
function getStepDef(id) {

tests/visual/tutorial-verify.spec.js

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -23,23 +23,35 @@ async function clickStartAndWaitForMap(page) {
2323
await page.waitForTimeout(2000);
2424
}
2525

26-
async function setTutorialState(page, step, subStep = 1) {
26+
async function goToTutorialStep(page, step, subStep = 1) {
2727
await page.evaluate(({ step, subStep }) => {
28-
localStorage.setItem('mapper-tutorial', JSON.stringify({
29-
completed: false, dismissed: false,
30-
step, subStep,
31-
hasSkippedQuestion: false, skipToastShown: false, returningUser: false,
32-
}));
28+
if (window.__mapper && window.__mapper.tutorialGoToStep) {
29+
window.__mapper.tutorialGoToStep(step, subStep);
30+
}
3331
}, { step, subStep });
32+
await page.waitForTimeout(500);
3433
}
3534

3635
async function answerQuestion(page) {
37-
// Wait for quiz option to be visible
38-
const opt = page.locator('.quiz-option:not([disabled])').first();
3936
try {
37+
await page.waitForSelector('.quiz-question', { timeout: 5000 });
38+
const prevText = await page.textContent('.quiz-question');
39+
const opt = page.locator('.quiz-option:not([disabled])').first();
4040
await opt.waitFor({ state: 'visible', timeout: 5000 });
4141
await opt.click({ timeout: 5000 });
42-
await page.waitForTimeout(1500);
42+
// Wait for question to change (auto-advance) or timeout
43+
try {
44+
await page.waitForFunction(
45+
(prev) => {
46+
const q = document.querySelector('.quiz-question');
47+
return q && q.textContent !== prev;
48+
},
49+
prevText,
50+
{ timeout: 5000 }
51+
);
52+
} catch {
53+
await page.waitForTimeout(1500);
54+
}
4355
return true;
4456
} catch { return false; }
4557
}
@@ -67,8 +79,8 @@ test.describe('Tutorial visual verification', () => {
6779
await page.setViewportSize({ width: 1280, height: 800 });
6880
await resetAndLoad(page);
6981
// Set tutorial to step 6 (Switch Domains) to test highlight on dropdown
70-
await setTutorialState(page, 6);
7182
await clickStartAndWaitForMap(page);
83+
await goToTutorialStep(page, 6);
7284
await page.waitForTimeout(2000);
7385

7486
// Take screenshot showing the domain selector highlighted (#42)
@@ -133,8 +145,8 @@ test.describe('Tutorial visual verification', () => {
133145
test('Issue #41: Title says "Videos in View"', async ({ page }) => {
134146
await page.setViewportSize({ width: 1280, height: 800 });
135147
await resetAndLoad(page);
136-
await setTutorialState(page, 3, 1);
137148
await clickStartAndWaitForMap(page);
149+
await goToTutorialStep(page, 3, 1);
138150
await page.waitForSelector('#tutorial-modal', { timeout: 10000 });
139151
await page.waitForTimeout(800);
140152

@@ -146,8 +158,8 @@ test.describe('Tutorial visual verification', () => {
146158
test('Issue #44: Expertise text softened', async ({ page }) => {
147159
await page.setViewportSize({ width: 1280, height: 800 });
148160
await resetAndLoad(page);
149-
await setTutorialState(page, 8);
150161
await clickStartAndWaitForMap(page);
162+
await goToTutorialStep(page, 8);
151163
await page.waitForSelector('#tutorial-modal', { timeout: 10000 });
152164
await page.waitForTimeout(500);
153165

@@ -159,32 +171,26 @@ test.describe('Tutorial visual verification', () => {
159171
test('Issue #45: New tutorial steps exist (modes, save/load, about)', async ({ page }) => {
160172
await page.setViewportSize({ width: 1280, height: 800 });
161173
await resetAndLoad(page);
174+
await clickStartAndWaitForMap(page);
162175

163176
// Step 11: Question Modes
164-
await setTutorialState(page, 11);
165-
await clickStartAndWaitForMap(page);
177+
await goToTutorialStep(page, 11);
166178
await page.waitForSelector('#tutorial-modal', { timeout: 10000 });
167179
await page.waitForTimeout(500);
168180
const title11 = await page.textContent('[data-tutorial-title]');
169181
console.log('Issue #45: Step 11 title:', title11);
170182
await page.screenshot({ path: `${SHOTS}/issue45-step11-modes.png` });
171183

172184
// Step 12: Save & Load
173-
await setTutorialState(page, 12);
174-
await page.goto(BASE);
175-
await page.waitForSelector('#landing-start-btn[data-ready="true"]', { timeout: 30000 });
176-
await clickStartAndWaitForMap(page);
185+
await goToTutorialStep(page, 12);
177186
await page.waitForSelector('#tutorial-modal', { timeout: 10000 });
178187
await page.waitForTimeout(500);
179188
const title12 = await page.textContent('[data-tutorial-title]');
180189
console.log('Issue #45: Step 12 title:', title12);
181190
await page.screenshot({ path: `${SHOTS}/issue45-step12-save-load.png` });
182191

183192
// Step 13: Learn More
184-
await setTutorialState(page, 13);
185-
await page.goto(BASE);
186-
await page.waitForSelector('#landing-start-btn[data-ready="true"]', { timeout: 30000 });
187-
await clickStartAndWaitForMap(page);
193+
await goToTutorialStep(page, 13);
188194
await page.waitForSelector('#tutorial-modal', { timeout: 10000 });
189195
await page.waitForTimeout(500);
190196
const title13 = await page.textContent('[data-tutorial-title]');
@@ -277,8 +283,8 @@ test.describe('Tutorial visual verification', () => {
277283
const list = document.querySelector('#insights-modal-body .insights-modal-list');
278284
return {
279285
count: items.length,
280-
maxHeight: list?.style.maxHeight || getComputedStyle(list).maxHeight,
281-
overflowY: list?.style.overflowY || getComputedStyle(list).overflowY,
286+
maxHeight: list ? (list.style.maxHeight || getComputedStyle(list).maxHeight) : 'not-found',
287+
overflowY: list ? (list.style.overflowY || getComputedStyle(list).overflowY) : 'not-found',
282288
};
283289
});
284290
console.log('Issue #48: Items:', info.count, 'maxHeight:', info.maxHeight, 'overflow:', info.overflowY);

tests/visual/verify-expertise-trim.spec.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,29 @@ test('expertise modal: names are trimmed and limited to top 10', async ({ page }
1212

1313
// Answer 6 questions to unlock expertise areas
1414
for (let i = 0; i < 6; i++) {
15-
const optionBtn = await page.waitForSelector('.quiz-option', { timeout: 10000 });
16-
await optionBtn.click();
17-
await page.waitForTimeout(900);
15+
// Wait for a fresh quiz-question to appear
16+
await page.waitForSelector('.quiz-question', { timeout: 10000 });
17+
const questionText = await page.textContent('.quiz-question');
18+
// Click the first option
19+
await page.locator('.quiz-option').first().click();
20+
// Wait for either a new question or for the current question to change
21+
try {
22+
await page.waitForFunction(
23+
(prevText) => {
24+
const q = document.querySelector('.quiz-question');
25+
return q && q.textContent !== prevText;
26+
},
27+
questionText,
28+
{ timeout: 5000 }
29+
);
30+
} catch {
31+
// If question didn't change (e.g., last question), just wait
32+
await page.waitForTimeout(1500);
33+
}
1834
}
1935

20-
// Click the trophy button to open the expertise modal
36+
// Wait for trophy button to become enabled (updateInsightButtons)
37+
await page.waitForSelector('#trophy-btn:not([disabled])', { timeout: 10000 });
2138
await page.click('#trophy-btn');
2239
await page.waitForSelector('#insights-modal:not([hidden])', { timeout: 5000 });
2340

0 commit comments

Comments
 (0)