fix: gate horn/wings steps by pony type — a Jordpony can no longer pick them
Horn/wings choices were free-standing toggles independent of the chosen pony type, so e.g. a Jordpony (which has neither in the game's own lore/ stats) could still be given a horn or wings in the configurator. Wizard steps are now computed from the selected type's hasHorn/hasWings flags: Jordpony gets 5 steps (type/body/eyes/mane/tail, no horn or wings step at all), Pegasus 6 (+wings), Enhjørning 6 (+horn), Alicorn 7 (+both). appearance.hasHorn/hasWings are set directly from the type on pick and are no longer separately toggleable — the on/off buttons are gone since the type alone decides whether they exist. App.test.js's pickPonyAndStartGame helper now clicks "Næste" until "Start eventyr" appears rather than a fixed count, since step count depends on which pony type the test picks. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -58,7 +58,7 @@ jest.mock('./components/SpeakButton', () => function SpeakButton() { return null
|
||||
jest.mock('./components/VoiceButton', () => function VoiceButton() { return null; });
|
||||
|
||||
import React from 'react';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { act, render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import App from './App';
|
||||
import * as SFX from './SceneMusic';
|
||||
@@ -127,12 +127,13 @@ function mockMultiFetch(responses) {
|
||||
const findByText = (t) => screen.getByText(t);
|
||||
|
||||
// Clicking a pony type on the configurator's first step only selects the
|
||||
// type and advances to the body-color step; five more "Næste" clicks walk
|
||||
// through eyes/mane/tail/horn/wings to the final step, whose "Start eventyr"
|
||||
// button actually starts the game.
|
||||
// type and advances to the body-color step. How many "Næste" clicks it
|
||||
// takes to reach the end depends on the type — a Jordpony has neither
|
||||
// horn nor wings steps, so its wizard is shorter than an Alicorn's — so
|
||||
// just click "Næste" until "Start eventyr" appears instead of a fixed count.
|
||||
async function pickPonyAndStartGame(name = 'Jordpony') {
|
||||
await userEvent.click(screen.getByText(name));
|
||||
for (let i = 0; i < 5; i++) {
|
||||
while (screen.queryByRole('button', { name: 'Næste trin' })) {
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Næste trin' }));
|
||||
}
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Start eventyr' }));
|
||||
@@ -332,6 +333,10 @@ test('shows four story choices instead of dice in a choice scene', async () => {
|
||||
const choices = screen.getAllByRole('button', { name: /^Vælg / });
|
||||
expect(choices).toHaveLength(4);
|
||||
expect(choices[0].closest('.game-controls')).toHaveClass('game-controls-options');
|
||||
act(() => window.dispatchEvent(new CustomEvent('pony-narration-status', {
|
||||
detail: { status: 'preparing' },
|
||||
})));
|
||||
choices.forEach(choice => expect(choice).toBeEnabled());
|
||||
expect(screen.queryByRole('button', { name: 'Kast terningerne' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Pixel Pony Configurator — build your pony in steps: type, body color,
|
||||
* eyes, mane, tail, horn, wings. Replaces the old plain pony-type select
|
||||
* screen; finishing the wizard starts the game with the chosen type.
|
||||
* eyes, mane, tail, and (only if the chosen type actually has them) horn
|
||||
* and/or wings. Replaces the old plain pony-type select screen; finishing
|
||||
* the wizard starts the game with the chosen type.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
import SceneMusic from '../SceneMusic';
|
||||
import VolumeControl from '../components/VolumeControl';
|
||||
@@ -18,7 +19,6 @@ import {
|
||||
} from '../pixelPony/spriteData';
|
||||
import { loadAppearance, saveAppearance } from '../services/ponyAppearance';
|
||||
|
||||
const STEPS = ['type', 'body', 'eyes', 'mane', 'tail', 'horn', 'wings'];
|
||||
const STEP_TITLES = {
|
||||
type: 'Vælg din Pony! 🐴',
|
||||
body: 'Vælg krop-farve 🎨',
|
||||
@@ -54,6 +54,18 @@ export default function PixelPonyConfiguratorPage({ ponies, onSelectType, volume
|
||||
const [appearance, setAppearance] = useState(loadAppearance);
|
||||
const [bobFrame, setBobFrame] = useState(false);
|
||||
|
||||
const selectedType = typeIdx !== null ? (PONY_TYPES[typeIdx] || PONY_TYPES[0]) : null;
|
||||
|
||||
// Horn/wings only appear as steps — and only exist at all — if the
|
||||
// chosen pony type actually has them. A Jordpony has neither, so it
|
||||
// never gets the chance to pick a horn or wings.
|
||||
const steps = useMemo(() => {
|
||||
const base = ['type', 'body', 'eyes', 'mane', 'tail'];
|
||||
if (selectedType?.hasHorn) base.push('horn');
|
||||
if (selectedType?.hasWings) base.push('wings');
|
||||
return base;
|
||||
}, [selectedType]);
|
||||
|
||||
useEffect(() => {
|
||||
const id = window.setInterval(() => setBobFrame(b => !b), 500);
|
||||
return () => window.clearInterval(id);
|
||||
@@ -76,23 +88,25 @@ export default function PixelPonyConfiguratorPage({ ponies, onSelectType, volume
|
||||
}
|
||||
};
|
||||
|
||||
const goNext = () => setStep(s => Math.min(s + 1, STEPS.length - 1));
|
||||
const goNext = () => setStep(s => Math.min(s + 1, steps.length - 1));
|
||||
|
||||
const handleStart = () => {
|
||||
saveAppearance(appearance);
|
||||
onSelectType(typeIdx ?? 0);
|
||||
};
|
||||
|
||||
const stepName = STEPS[step];
|
||||
const isLastStep = step === STEPS.length - 1;
|
||||
const stepName = steps[step];
|
||||
const isLastStep = step === steps.length - 1;
|
||||
const narration = {
|
||||
type: 'Vælg din pony type. Tryk på den pony du vil være.',
|
||||
body: 'Vælg en farve til din ponys krop.',
|
||||
eyes: 'Vælg en farve til din ponys øjne.',
|
||||
mane: 'Vælg en manke og en mankefarve til din pony.',
|
||||
tail: 'Vælg en hale og en halefarve til din pony.',
|
||||
horn: 'Vælg om din pony skal have horn, og vælg form og farve.',
|
||||
wings: 'Vælg om din pony skal have vinger, og vælg form og farve. Tryk på start eventyr når du er klar.',
|
||||
tail: isLastStep
|
||||
? 'Vælg en hale og en halefarve til din pony. Tryk på start eventyr når du er klar.'
|
||||
: 'Vælg en hale og en halefarve til din pony.',
|
||||
horn: 'Vælg form og farve på hornet.',
|
||||
wings: 'Vælg form og farve på vingerne. Tryk på start eventyr når du er klar.',
|
||||
}[stepName];
|
||||
|
||||
return (
|
||||
@@ -114,7 +128,7 @@ export default function PixelPonyConfiguratorPage({ ponies, onSelectType, volume
|
||||
<motion.h1 initial={{ opacity: 0, y: -20 }} animate={{ opacity: 1, y: 0 }} className="title">
|
||||
{STEP_TITLES[stepName]}
|
||||
</motion.h1>
|
||||
<p className="pixel-configurator-progress">Trin {step + 1} af {STEPS.length}</p>
|
||||
<p className="pixel-configurator-progress">Trin {step + 1} af {steps.length}</p>
|
||||
<SpeakButton text={narration} volume={volume} label="Læs siden højt" />
|
||||
|
||||
{stepName !== 'type' && (
|
||||
@@ -248,100 +262,68 @@ export default function PixelPonyConfiguratorPage({ ponies, onSelectType, volume
|
||||
{stepName === 'horn' && (
|
||||
<>
|
||||
<section className="pixel-configurator-section">
|
||||
<div className="pixel-toggle-row">
|
||||
<button
|
||||
type="button"
|
||||
className={`pixel-toggle ${appearance.hasHorn ? 'is-selected' : ''}`}
|
||||
onClick={() => set('hasHorn', !appearance.hasHorn)}
|
||||
aria-pressed={appearance.hasHorn}
|
||||
>
|
||||
🦄 Horn til/fra
|
||||
</button>
|
||||
<h2>Horn-form</h2>
|
||||
<div className="pixel-mane-grid">
|
||||
{HORN_STYLES.map(h => (
|
||||
<button
|
||||
key={h.id}
|
||||
type="button"
|
||||
className={`pixel-mane-swatch ${appearance.horn === h.id ? 'is-selected' : ''}`}
|
||||
onClick={() => set('horn', h.id)}
|
||||
aria-label={`Vælg horn: ${h.label}`}
|
||||
aria-pressed={appearance.horn === h.id}
|
||||
title={h.label}
|
||||
>
|
||||
<HornIcon horn={h.id} hornColor={appearance.hornColor} scale={2} />
|
||||
<span>{h.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{appearance.hasHorn && (
|
||||
<>
|
||||
<section className="pixel-configurator-section">
|
||||
<h2>Horn-form</h2>
|
||||
<div className="pixel-mane-grid">
|
||||
{HORN_STYLES.map(h => (
|
||||
<button
|
||||
key={h.id}
|
||||
type="button"
|
||||
className={`pixel-mane-swatch ${appearance.horn === h.id ? 'is-selected' : ''}`}
|
||||
onClick={() => set('horn', h.id)}
|
||||
aria-label={`Vælg horn: ${h.label}`}
|
||||
aria-pressed={appearance.horn === h.id}
|
||||
title={h.label}
|
||||
>
|
||||
<HornIcon horn={h.id} hornColor={appearance.hornColor} scale={2} />
|
||||
<span>{h.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="pixel-configurator-section">
|
||||
<h2>Hornfarve</h2>
|
||||
<ColorSwatches
|
||||
options={COLOR_OPTIONS}
|
||||
value={appearance.hornColor}
|
||||
onPick={(id) => set('hornColor', id)}
|
||||
labelPrefix="Hornfarve"
|
||||
swatchColor="#ffe066"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
<section className="pixel-configurator-section">
|
||||
<h2>Hornfarve</h2>
|
||||
<ColorSwatches
|
||||
options={COLOR_OPTIONS}
|
||||
value={appearance.hornColor}
|
||||
onPick={(id) => set('hornColor', id)}
|
||||
labelPrefix="Hornfarve"
|
||||
swatchColor="#ffe066"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
{stepName === 'wings' && (
|
||||
<>
|
||||
<section className="pixel-configurator-section">
|
||||
<div className="pixel-toggle-row">
|
||||
<button
|
||||
type="button"
|
||||
className={`pixel-toggle ${appearance.hasWings ? 'is-selected' : ''}`}
|
||||
onClick={() => set('hasWings', !appearance.hasWings)}
|
||||
aria-pressed={appearance.hasWings}
|
||||
>
|
||||
🪽 Vinger til/fra
|
||||
</button>
|
||||
<h2>Vinge-form</h2>
|
||||
<div className="pixel-mane-grid">
|
||||
{WING_STYLES.map(w => (
|
||||
<button
|
||||
key={w.id}
|
||||
type="button"
|
||||
className={`pixel-mane-swatch ${appearance.wing === w.id ? 'is-selected' : ''}`}
|
||||
onClick={() => set('wing', w.id)}
|
||||
aria-label={`Vælg vinge: ${w.label}`}
|
||||
aria-pressed={appearance.wing === w.id}
|
||||
title={w.label}
|
||||
>
|
||||
<WingIcon wing={w.id} wingColor={appearance.wingColor} scale={2} />
|
||||
<span>{w.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
{appearance.hasWings && (
|
||||
<>
|
||||
<section className="pixel-configurator-section">
|
||||
<h2>Vinge-form</h2>
|
||||
<div className="pixel-mane-grid">
|
||||
{WING_STYLES.map(w => (
|
||||
<button
|
||||
key={w.id}
|
||||
type="button"
|
||||
className={`pixel-mane-swatch ${appearance.wing === w.id ? 'is-selected' : ''}`}
|
||||
onClick={() => set('wing', w.id)}
|
||||
aria-label={`Vælg vinge: ${w.label}`}
|
||||
aria-pressed={appearance.wing === w.id}
|
||||
title={w.label}
|
||||
>
|
||||
<WingIcon wing={w.id} wingColor={appearance.wingColor} scale={2} />
|
||||
<span>{w.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section className="pixel-configurator-section">
|
||||
<h2>Vingefarve</h2>
|
||||
<ColorSwatches
|
||||
options={COLOR_OPTIONS}
|
||||
value={appearance.wingColor}
|
||||
onPick={(id) => set('wingColor', id)}
|
||||
labelPrefix="Vingefarve"
|
||||
swatchColor="#f5c8af"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
<section className="pixel-configurator-section">
|
||||
<h2>Vingefarve</h2>
|
||||
<ColorSwatches
|
||||
options={COLOR_OPTIONS}
|
||||
value={appearance.wingColor}
|
||||
onPick={(id) => set('wingColor', id)}
|
||||
labelPrefix="Vingefarve"
|
||||
swatchColor="#f5c8af"
|
||||
/>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -354,7 +336,7 @@ export default function PixelPonyConfiguratorPage({ ponies, onSelectType, volume
|
||||
Næste ➡️
|
||||
</motion.button>
|
||||
)}
|
||||
{isLastStep && (
|
||||
{isLastStep && stepName !== 'type' && (
|
||||
<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} className="btn-start" onClick={handleStart} aria-label="Start eventyr">
|
||||
Start eventyr! 🎮
|
||||
</motion.button>
|
||||
|
||||
@@ -16,6 +16,8 @@ jest.mock('framer-motion', () => {
|
||||
jest.mock('../SceneMusic', () => function SceneMusic() { return null; });
|
||||
jest.mock('../services/tts', () => ({ speakDanish: jest.fn(), cancelSpeech: jest.fn() }));
|
||||
|
||||
// Order matches spriteData.PONY_TYPES: jordpony (neither), pegasus (wings
|
||||
// only), enhjorning (horn only), alicorn (both).
|
||||
const PONIES = [
|
||||
{ navn: 'Jordpony', emoji: '🐴', bonus: 'Stærk 💪' },
|
||||
{ navn: 'Pegasus', emoji: '🦅', bonus: 'Flyver 🪽' },
|
||||
@@ -23,9 +25,6 @@ const PONIES = [
|
||||
{ navn: 'Alicorn', emoji: '👑', bonus: 'Magi + vinger 🌟' },
|
||||
];
|
||||
|
||||
// type -> body -> eyes -> mane -> tail -> horn -> wings
|
||||
const STEP_COUNT = 7;
|
||||
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
});
|
||||
@@ -45,15 +44,12 @@ function renderWizard(onSelectType = jest.fn()) {
|
||||
}
|
||||
|
||||
const next = () => userEvent.click(screen.getByRole('button', { name: 'Næste trin' }));
|
||||
const pickJordpony = () => userEvent.click(screen.getByRole('button', { name: 'Vælg Jordpony - Stærk 💪' }));
|
||||
const pick = (label) => userEvent.click(screen.getByRole('button', { name: label }));
|
||||
|
||||
async function pickTypeAndAdvanceToWings() {
|
||||
await pickJordpony();
|
||||
await next(); // body -> eyes
|
||||
await next(); // eyes -> mane
|
||||
await next(); // mane -> tail
|
||||
await next(); // tail -> horn
|
||||
await next(); // horn -> wings
|
||||
async function clickThroughToEnd() {
|
||||
while (screen.queryByRole('button', { name: 'Næste trin' })) {
|
||||
await next();
|
||||
}
|
||||
}
|
||||
|
||||
test('step 1 shows all four pony types', () => {
|
||||
@@ -62,13 +58,13 @@ test('step 1 shows all four pony types', () => {
|
||||
expect(screen.getByText('Pegasus')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enhjørning')).toBeInTheDocument();
|
||||
expect(screen.getByText('Alicorn')).toBeInTheDocument();
|
||||
expect(screen.getByText(`Trin 1 af ${STEP_COUNT}`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Trin 1 af 5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('picking a pony type advances to the body-color step, not mane', async () => {
|
||||
renderWizard();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Vælg Pegasus - Flyver 🪽' }));
|
||||
expect(screen.getByText(`Trin 2 af ${STEP_COUNT}`)).toBeInTheDocument();
|
||||
await pick('Vælg Pegasus - Flyver 🪽');
|
||||
expect(screen.getByText('Trin 2 af 6')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Kropsfarve: Lilla' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Vælg manke: Boglig' })).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -76,106 +72,76 @@ test('picking a pony type advances to the body-color step, not mane', async () =
|
||||
test('mane list excludes the eye-icon files misfiled as mane styles', () => {
|
||||
renderWizard();
|
||||
expect(screen.queryByText('Dramatisk')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Fabelagtig')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Nysgerrig')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Klog')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Kæk')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Praktisk')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Tuf')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('mane step shows only mane swatches', async () => {
|
||||
renderWizard();
|
||||
await pickJordpony();
|
||||
await next(); // body -> eyes
|
||||
await next(); // eyes -> mane
|
||||
expect(screen.getByText(`Trin 4 af ${STEP_COUNT}`)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg manke: Boglig' })).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Kropsfarve: Lilla' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('tail step offers multiple tail shapes with their own color', async () => {
|
||||
renderWizard();
|
||||
await pickJordpony();
|
||||
test('Jordpony has no horn or wings step — the wizard is 5 steps and ends after tail', async () => {
|
||||
const { onSelectType } = renderWizard();
|
||||
await pick('Vælg Jordpony - Stærk 💪');
|
||||
expect(screen.getByText('Trin 2 af 5')).toBeInTheDocument();
|
||||
await next(); // body -> eyes
|
||||
await next(); // eyes -> mane
|
||||
await next(); // mane -> tail
|
||||
expect(screen.getByText(`Trin 5 af ${STEP_COUNT}`)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg hale: Lang' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg hale: Kort' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg hale: Krøllet' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Halefarve: Grøn' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Trin 5 af 5')).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Næste trin' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Start eventyr' })).toBeInTheDocument();
|
||||
await pick('Start eventyr');
|
||||
const saved = JSON.parse(window.localStorage.getItem('pony_appearance'));
|
||||
expect(saved.hasHorn).toBe(false);
|
||||
expect(saved.hasWings).toBe(false);
|
||||
expect(onSelectType).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('horn step is off by default and hides style/color pickers until toggled on', async () => {
|
||||
test('Pegasus gets a wings step but no horn step', async () => {
|
||||
renderWizard();
|
||||
await pickJordpony();
|
||||
await pick('Vælg Pegasus - Flyver 🪽');
|
||||
expect(screen.getByText('Trin 2 af 6')).toBeInTheDocument();
|
||||
await clickThroughToEnd();
|
||||
expect(screen.getByText('Vælg vinger 🪽')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg vinge: Foldet' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Enhjørning gets a horn step but no wings step', async () => {
|
||||
renderWizard();
|
||||
await pick('Vælg Enhjørning - Magisk horn ✨');
|
||||
expect(screen.getByText('Trin 2 af 6')).toBeInTheDocument();
|
||||
await clickThroughToEnd();
|
||||
expect(screen.getByText('Vælg horn 🦄')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg horn: Spids' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Alicorn gets both a horn and a wings step', async () => {
|
||||
const { onSelectType } = renderWizard();
|
||||
await pick('Vælg Alicorn - Magi + vinger 🌟');
|
||||
expect(screen.getByText('Trin 2 af 7')).toBeInTheDocument();
|
||||
await next(); // body -> eyes
|
||||
await next(); // eyes -> mane
|
||||
await next(); // mane -> tail
|
||||
await next(); // tail -> horn
|
||||
expect(screen.getByText(`Trin 6 af ${STEP_COUNT}`)).toBeInTheDocument();
|
||||
const toggle = screen.getByRole('button', { name: '🦄 Horn til/fra' });
|
||||
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(screen.queryByRole('button', { name: 'Vælg horn: Spids' })).not.toBeInTheDocument();
|
||||
await userEvent.click(toggle);
|
||||
expect(screen.getByRole('button', { name: 'Vælg horn: Spids' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg horn: Snoet' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg horn: Lille' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Hornfarve: Gul' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('wings step is off by default and hides style/color pickers until toggled on', async () => {
|
||||
renderWizard();
|
||||
await pickTypeAndAdvanceToWings();
|
||||
expect(screen.getByText(`Trin 7 af ${STEP_COUNT}`)).toBeInTheDocument();
|
||||
const toggle = screen.getByRole('button', { name: '🪽 Vinger til/fra' });
|
||||
expect(toggle).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(screen.queryByRole('button', { name: 'Vælg vinge: Foldet' })).not.toBeInTheDocument();
|
||||
await userEvent.click(toggle);
|
||||
expect(screen.getByRole('button', { name: 'Vælg vinge: Foldet' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Vælg vinge: Udspredt' })).toBeInTheDocument();
|
||||
expect(screen.getByText('Vælg horn 🦄')).toBeInTheDocument();
|
||||
await pick('Vælg horn: Snoet');
|
||||
await next(); // horn -> wings
|
||||
expect(screen.getByText('Vælg vinger 🪽')).toBeInTheDocument();
|
||||
await pick('Vælg vinge: Udspredt');
|
||||
await pick('Start eventyr');
|
||||
const saved = JSON.parse(window.localStorage.getItem('pony_appearance'));
|
||||
expect(saved.hasHorn).toBe(true);
|
||||
expect(saved.horn).toBe('swirl');
|
||||
expect(saved.hasWings).toBe(true);
|
||||
expect(saved.wing).toBe('spread');
|
||||
expect(onSelectType).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
test('back on the first step exits to home', async () => {
|
||||
const { onNavigate } = renderWizard();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Tilbage' }));
|
||||
await pick('Tilbage');
|
||||
expect(onNavigate).toHaveBeenCalledWith('home');
|
||||
});
|
||||
|
||||
test('back on a later step returns to the previous step, not home', async () => {
|
||||
const { onNavigate } = renderWizard();
|
||||
await pickJordpony();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Tilbage' }));
|
||||
await pick('Vælg Jordpony - Stærk 💪');
|
||||
await pick('Tilbage');
|
||||
expect(onNavigate).not.toHaveBeenCalled();
|
||||
expect(screen.getByText(`Trin 1 af ${STEP_COUNT}`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('walks through every step before starting the game', async () => {
|
||||
const { onSelectType } = renderWizard();
|
||||
await pickTypeAndAdvanceToWings();
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Start eventyr' }));
|
||||
expect(onSelectType).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
test('starting the game saves the full appearance, including chosen horn/wing styles', async () => {
|
||||
renderWizard();
|
||||
await pickJordpony();
|
||||
await next(); // body -> eyes
|
||||
await next(); // eyes -> mane
|
||||
await next(); // mane -> tail
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Vælg hale: Krøllet' }));
|
||||
await next(); // tail -> horn
|
||||
await userEvent.click(screen.getByRole('button', { name: '🦄 Horn til/fra' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Vælg horn: Snoet' }));
|
||||
await next(); // horn -> wings
|
||||
await userEvent.click(screen.getByRole('button', { name: '🪽 Vinger til/fra' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Vælg vinge: Udspredt' }));
|
||||
await userEvent.click(screen.getByRole('button', { name: 'Start eventyr' }));
|
||||
const saved = JSON.parse(window.localStorage.getItem('pony_appearance'));
|
||||
expect(saved.tail).toBe('curly');
|
||||
expect(saved.hasHorn).toBe(true);
|
||||
expect(saved.horn).toBe('swirl');
|
||||
expect(saved.hasWings).toBe(true);
|
||||
expect(saved.wing).toBe('spread');
|
||||
expect(screen.getByText('Trin 1 af 5')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user