From 81a69b5bd32b9da01f5b97f2837fd37dc416ea09 Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Tue, 21 Apr 2026 09:46:43 +0200 Subject: [PATCH 1/7] fix: drain quest events between commands in _execute_cheat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cheat sequence completes the entire golden path in one call, which means items like Rare Flower are taken AND given away before the frontend ever calls pollQuestEvents(). Because _execute_cheat never called next_activated_quest / next_completed_quest between commands, the 'Oh deer, oh deer' quest was never activated during the cheat, and by the time the frontend drained quest events afterward, check_completion returned False (the flower was already with Mira). Fix: add the same quest-draining loop that dev_execute_commands already uses, inside _execute_cheat β€” one pass after each command. Also add DummyGameState stub to test_CommandParser.py so the three existing cheat unit tests still compile and pass. Test: test_cheat_act_1_completes_oh_deer_oh_deer_quest (new, in test_Integration.py) fails before the fix and passes after. --- src/retroquest/engine/CommandParser.py | 6 ++++ tests/retroquest/act1/test_Integration.py | 31 +++++++++++++++++++ tests/retroquest/engine/test_CommandParser.py | 18 +++++++++++ 3 files changed, 55 insertions(+) diff --git a/src/retroquest/engine/CommandParser.py b/src/retroquest/engine/CommandParser.py index 9d27e79..a0f5e17 100644 --- a/src/retroquest/engine/CommandParser.py +++ b/src/retroquest/engine/CommandParser.py @@ -313,4 +313,10 @@ def _execute_cheat(self, commands: list[str]) -> str: result = self.parse(command) if result is not None: results.append(str(result)) + while self.game.state.next_activated_quest(): + pass + while self.game.state.next_updated_quest(): + pass + while self.game.state.next_completed_quest(): + pass return '\n'.join(results) diff --git a/tests/retroquest/act1/test_Integration.py b/tests/retroquest/act1/test_Integration.py index 46432a9..c736816 100644 --- a/tests/retroquest/act1/test_Integration.py +++ b/tests/retroquest/act1/test_Integration.py @@ -508,3 +508,34 @@ def test_golden_path_act1_completion(): # Final check: Act I should be completed assert game.acts[game.current_act].is_completed(game.state), "Act I is not marked as completed." + +def test_cheat_act_1_completes_oh_deer_oh_deer_quest(): + """cheat act 1 must fully complete the 'Oh deer, oh deer' side quest. + + The cheat sequence gives the Rare Flower to Mira before any quest + processing normally occurs. The cheat engine must therefore drain + quest-activation and quest-completion state between commands. Without + that, check_completion returns False when the frontend later calls + completeQuest() because the flower is no longer in inventory or rooms. + """ + act = Act1() + act.music_file = '' + game = Game([act]) + + game.command_parser.parse("cheat act 1") + + # Drain quest events, simulating what the web frontend's pollQuestEvents() + # does after a command returns. + while game.state.next_activated_quest(): + pass + while game.state.next_updated_quest(): + pass + while game.state.next_completed_quest(): + pass + + completed_names = [q.name for q in game.state.completed_quests] + assert "Oh deer, oh deer" in completed_names, ( + f"'Oh deer, oh deer' should be completed after 'cheat act 1', " + f"but completed quests are: {completed_names}" + ) + diff --git a/tests/retroquest/engine/test_CommandParser.py b/tests/retroquest/engine/test_CommandParser.py index c3325d2..6e099c3 100644 --- a/tests/retroquest/engine/test_CommandParser.py +++ b/tests/retroquest/engine/test_CommandParser.py @@ -3,12 +3,30 @@ import pytest from engine.CommandParser import CommandParser + +class DummyGameState: + """Minimal stub for GameState used by DummyGame in CommandParser tests.""" + + def next_activated_quest(self): + """Always return None: no quests to activate in unit tests.""" + return None + + def next_updated_quest(self): + """Always return None: no quests to update in unit tests.""" + return None + + def next_completed_quest(self): + """Always return None: no quests to complete in unit tests.""" + return None + + class DummyGame: """A minimal fake Game that records calls for CommandParser tests.""" def __init__(self): """Create the fake game and a list to record calls.""" self.calls = [] + self.state = DummyGameState() def move(self, direction, arg=None): """Record a movement request.""" From 11d036081b6401f37cb6801c9fae104726334e5f Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Tue, 21 Apr 2026 09:55:58 +0200 Subject: [PATCH 2/7] test: extend cheat act 1 test to verify all side quests complete and only main quest remains active --- tests/retroquest/act1/test_Integration.py | 43 +++++++++++++++++++---- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/tests/retroquest/act1/test_Integration.py b/tests/retroquest/act1/test_Integration.py index c736816..e220a58 100644 --- a/tests/retroquest/act1/test_Integration.py +++ b/tests/retroquest/act1/test_Integration.py @@ -509,14 +509,20 @@ def test_golden_path_act1_completion(): assert game.acts[game.current_act].is_completed(game.state), "Act I is not marked as completed." -def test_cheat_act_1_completes_oh_deer_oh_deer_quest(): - """cheat act 1 must fully complete the 'Oh deer, oh deer' side quest. +def test_cheat_act_1_completes_all_side_quests_and_leaves_main_active(): + """cheat act 1 must complete every completable side quest and leave only the main quest active. The cheat sequence gives the Rare Flower to Mira before any quest processing normally occurs. The cheat engine must therefore drain quest-activation and quest-completion state between commands. Without that, check_completion returns False when the frontend later calls completeQuest() because the flower is no longer in inventory or rooms. + + Note: FadedPhotograph and LostLetter are intentionally excluded β€” + FadedPhotograph requires 'search' in EliorsCottage which the cheat + sequence does not perform; LostLetter references flags that are not + set by any game action yet (incomplete implementation). + Both remain in non_activated_quests after the cheat. """ act = Act1() act.music_file = '' @@ -533,9 +539,34 @@ def test_cheat_act_1_completes_oh_deer_oh_deer_quest(): while game.state.next_completed_quest(): pass - completed_names = [q.name for q in game.state.completed_quests] - assert "Oh deer, oh deer" in completed_names, ( - f"'Oh deer, oh deer' should be completed after 'cheat act 1', " - f"but completed quests are: {completed_names}" + active_names = [q.name for q in game.state.activated_quests] + active_main = [q.name for q in game.state.activated_quests if q.is_main()] + active_side = [q.name for q in game.state.activated_quests if not q.is_main()] + + assert active_main == ["Shadows Over Willowbrook"], ( + f"Only the main quest should remain active after 'cheat act 1', " + f"but active quests are: {active_names}" + ) + assert active_side == [], ( + f"No side quests should remain active after 'cheat act 1', " + f"but active side quests are: {active_side}" + ) + completed_side = sorted( + q.name for q in game.state.completed_quests if not q.is_main() + ) + expected_side_quests = sorted([ + "Hint of Magic", + "Curiosity killed the cat", + "Fishing expedition", + "Know your village", + "Let there be light", + "Magic for real", + "Magnet fishing expedition", + "Oh deer, oh deer", + "Preparing for the road", + ]) + assert completed_side == expected_side_quests, ( + f"All completable side quests should be completed after 'cheat act 1', " + f"but completed side quests are: {completed_side}" ) From d3889dff08cf5bf493ad903c4258adec64a36aa8 Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Tue, 21 Apr 2026 11:13:02 +0200 Subject: [PATCH 3/7] feat: add Faded Photograph quest to cheat sequence; remove LostLetter quest - Add 'search', 'take faded photograph', 'examine faded photograph' and 'give faded photograph to grandmother' to the Act 1 cheat command list so the Faded Photograph quest is triggered and completed by cheat act 1 - Remove the unimplemented LostLetter quest (class, story flags, Act1 registration) whose trigger/completion flags were never set by any game action - Update the integration test to expect The Faded Photograph in completed side quests and drop the LostLetter import / exclusion note --- src/retroquest/act1/Act1.py | 2 -- src/retroquest/act1/Act1StoryFlags.py | 2 -- src/retroquest/act1/quests/LostLetter.py | 35 ----------------------- src/retroquest/engine/CommandParser.py | 7 +++-- tests/retroquest/act1/test_Integration.py | 9 +----- 5 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 src/retroquest/act1/quests/LostLetter.py diff --git a/src/retroquest/act1/Act1.py b/src/retroquest/act1/Act1.py index 15d60af..16836e3 100644 --- a/src/retroquest/act1/Act1.py +++ b/src/retroquest/act1/Act1.py @@ -26,7 +26,6 @@ from retroquest.act1.quests.PreparingForTheRoad import PreparingForTheRoadQuest from retroquest.act1.quests.FadedPhotograph import FadedPhotographQuest from retroquest.act1.quests.ShadowsOverWillowbrook import ShadowsOverWillowbrookQuest -from retroquest.act1.quests.LostLetter import LostLetterQuest from retroquest.act1.Act1StoryFlags import FLAG_SHADOWS_OVER_WILLOWBROOK_COMPLETED class Act1(Act): @@ -60,7 +59,6 @@ def __init__(self) -> None: OhDeerOhDeerQuest(), PreparingForTheRoadQuest(), FadedPhotographQuest(), - LostLetterQuest(), ] music_file = "Conquest - Market (freetouse.com).mp3" super().__init__(name="Act1", rooms=rooms, quests=quests, music_file=music_file, music_info='Market by Conquest\nSource: https://freetouse.com/music\nCopyright Free Background Music') diff --git a/src/retroquest/act1/Act1StoryFlags.py b/src/retroquest/act1/Act1StoryFlags.py index 22e1fa4..5233afa 100644 --- a/src/retroquest/act1/Act1StoryFlags.py +++ b/src/retroquest/act1/Act1StoryFlags.py @@ -17,8 +17,6 @@ These constants are intended for use throughout the game's codebase to ensure consistent reference to story flags and facilitate maintainable game logic. """ -FLAG_FOUND_LOST_LETTER = "found_lost_letter" -FLAG_ASKED_GRANDMOTHER_ABOUT_LETTER = "asked_grandmother_about_letter" FLAG_INVESTIGATED_WITHERED_CROPS = "investigated_withered_crops" FLAG_WITNESSED_SHADOW_EVENT = "witnessed_shadow_event" FLAG_ACCEPTED_MIRA_APPRENTICESHIP = "accepted_mira_apprenticeship" diff --git a/src/retroquest/act1/quests/LostLetter.py b/src/retroquest/act1/quests/LostLetter.py deleted file mode 100644 index e45a2fb..0000000 --- a/src/retroquest/act1/quests/LostLetter.py +++ /dev/null @@ -1,35 +0,0 @@ -from ...engine.Quest import Quest -from ...engine.GameState import GameState -from ..Act1StoryFlags import ( - FLAG_FOUND_LOST_LETTER, - FLAG_ASKED_GRANDMOTHER_ABOUT_LETTER -) -from typing import Any - -# The Lost Letter Quest - Steps to Completion -# 1. The player searches the floorboard in Elior's Cottage and finds the faded letter. -# 2. The game sets FLAG_FOUND_LOST_LETTER to indicate the letter has been found. -# 3. The letter item is given to the player and can be examined. -# 4. The player talks to Grandmother about the letter, triggering special dialogue. -# 5. The game sets FLAG_ASKED_GRANDMOTHER_ABOUT_LETTER after the conversation. -# 6. Optionally, a clue or follow-up quest is unlocked by this letter. -# 7. The quest is now complete and connects to the main story thread. - -class LostLetterQuest(Quest): - def __init__(self) -> None: - super().__init__( - name="The Lost Letter", - description=( - "While cleaning Elior’s cottage, you discover a faded letter hidden beneath a floorboard. " - "The letter is addressed to your grandmother and mentions a secret your parents were investigating before they vanished." - ), - completion="You have uncovered a clue about your parents' fate. The letter hints at a mystery that will unfold in future acts." - ) - - def check_trigger(self, game_state: GameState) -> bool: - # Triggered when the player searches the floorboard in Elior's Cottage - return game_state.get_story_flag(FLAG_FOUND_LOST_LETTER) - - def check_completion(self, game_state: GameState) -> bool: - # Completed when the player talks to Grandmother about the letter - return game_state.get_story_flag(FLAG_ASKED_GRANDMOTHER_ABOUT_LETTER) diff --git a/src/retroquest/engine/CommandParser.py b/src/retroquest/engine/CommandParser.py index a0f5e17..516f8e5 100644 --- a/src/retroquest/engine/CommandParser.py +++ b/src/retroquest/engine/CommandParser.py @@ -4,7 +4,9 @@ from typing import Any, TYPE_CHECKING _ACT_1_CHEAT_COMMANDS: list[str] = [ - "use lantern", "take bread", "use journal", "talk to grandmother", + "use lantern", "take bread", "use journal", "search", + "take faded photograph", "examine faded photograph", + "talk to grandmother", "e", "talk to villager", "w", "s", "take carrot", "use hoe", "take knife", "s", "take egg", "take feather", "use bread with chicken", "take key", "n", "e", "examine well", @@ -32,7 +34,8 @@ "s", "s", "talk to priest", "n", "n", "n", "w", "n", "n", "e", "n", "talk to shopkeeper", "s", "w", "s", "s", "e", "s", "s", "s", "use matches with candle", "take locket", "n", "n", "n", - "w", "n", "n", "w", "n", "give locket to grandmother", + "w", "n", "n", "w", "n", "give faded photograph to grandmother", + "give locket to grandmother", "cast bless", "s", "e", "s", "s", "e", "s", "s", "s", "e", "give shiny ring to merchant", ] diff --git a/tests/retroquest/act1/test_Integration.py b/tests/retroquest/act1/test_Integration.py index e220a58..f388695 100644 --- a/tests/retroquest/act1/test_Integration.py +++ b/tests/retroquest/act1/test_Integration.py @@ -27,7 +27,6 @@ from retroquest.act1.quests.OhDeerOhDeer import OhDeerOhDeerQuest from retroquest.act1.quests.PreparingForTheRoad import PreparingForTheRoadQuest from retroquest.act1.quests.FadedPhotograph import FadedPhotographQuest -from retroquest.act1.quests.LostLetter import LostLetterQuest from retroquest.act1.quests.ShadowsOverWillowbrook import ShadowsOverWillowbrookQuest from ..utils.utils import (check_character_in_room, check_current_room, check_quests, execute_commands, check_item_in_inventory, check_item_in_room, @@ -65,7 +64,6 @@ PreparingForTheRoadQuest(), FadedPhotographQuest(), ShadowsOverWillowbrookQuest(), - LostLetterQuest() ] def test_golden_path_act1_completion(): @@ -517,12 +515,6 @@ def test_cheat_act_1_completes_all_side_quests_and_leaves_main_active(): quest-activation and quest-completion state between commands. Without that, check_completion returns False when the frontend later calls completeQuest() because the flower is no longer in inventory or rooms. - - Note: FadedPhotograph and LostLetter are intentionally excluded β€” - FadedPhotograph requires 'search' in EliorsCottage which the cheat - sequence does not perform; LostLetter references flags that are not - set by any game action yet (incomplete implementation). - Both remain in non_activated_quests after the cheat. """ act = Act1() act.music_file = '' @@ -564,6 +556,7 @@ def test_cheat_act_1_completes_all_side_quests_and_leaves_main_active(): "Magnet fishing expedition", "Oh deer, oh deer", "Preparing for the road", + "The Faded Photograph", ]) assert completed_side == expected_side_quests, ( f"All completable side quests should be completed after 'cheat act 1', " From 313084af5dc283315b5763e6cf139530df67c94b Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Tue, 21 Apr 2026 07:32:10 +0200 Subject: [PATCH 4/7] fix: show act transition and intro modals automatically on act completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a command completes an act, automatically drive the transition from ACT_TRANSITION -> ACT_INTRO -> ACT_RUNNING and show the transition message and new act intro as popup modals (same UX as game startup). Previously the game would silently block input with no visual feedback, requiring the user to press Enter (not possible on mobile with a readonly input field) twice to advance - causing the game to appear stuck. The fix: - Add triggerActTransitionModals() that calls b.advanceToRunning() to collect both the 'Act Complete!' text and new act intro text, then queues them as modals ('πŸ† Act Complete!' + 'πŸ“– Act Intro') - Sets pendingLookOnDismiss so look() + quest activation fires after the intro modal is dismissed (same as game startup) - Hook is called from submitCommand() when acceptInput becomes false and no modal is showing (direct transition, no quest events) - Hook is also called from dismissModal() when the last quest-completion modal is dismissed but input is still blocked (transition after quests) - Add isActRunning(): boolean and advanceToRunning(): string[] to the GameBridge interface (already in useBridge.ts and mock) - 5 new unit tests covering all transition scenarios --- web/src/stores/useGameStore.test.ts | 80 +++++++++++++++++++++++++++++ web/src/stores/useGameStore.ts | 33 ++++++++++++ 2 files changed, 113 insertions(+) diff --git a/web/src/stores/useGameStore.test.ts b/web/src/stores/useGameStore.test.ts index 15655dc..87dd70d 100644 --- a/web/src/stores/useGameStore.test.ts +++ b/web/src/stores/useGameStore.test.ts @@ -888,6 +888,86 @@ describe('useGameStore', () => { }) }) + // --- act transition --- + + describe('act transition', () => { + function setupTransitionMocks() { + bridge.isAcceptingInput.mockReturnValue(false) + bridge.isActRunning.mockReturnValue(false) + bridge.activateQuest.mockReturnValue(null) + bridge.updateQuest.mockReturnValue(null) + bridge.completeQuest.mockReturnValue(null) + bridge.advanceToRunning.mockReturnValue([ + 'Congratulations Act 1!', + 'Act 2 begins.', + '', + ]) + } + + beforeEach(async () => { + await store.initGame() + store.dismissModal() // dismiss init intro modal + }) + + it('shows act transition modal immediately when command causes transition', () => { + setupTransitionMocks() + store.submitCommand('use map') + expect(store.showModal).toBe(true) + expect(store.modalTitle).toBe('πŸ† Act Complete!') + expect(store.modalBody).toBe( + 'Congratulations Act 1!', + ) + }) + + it('shows act intro modal after dismissing transition modal', () => { + setupTransitionMocks() + store.submitCommand('use map') + store.dismissModal() + expect(store.showModal).toBe(true) + expect(store.modalTitle).toBe('πŸ“– Act Intro') + expect(store.modalBody).toBe('Act 2 begins.') + }) + + it('fires look after all transition modals are dismissed', () => { + setupTransitionMocks() + store.submitCommand('use map') + store.dismissModal() // dismiss transition modal + bridge.look.mockReturnValue('You are in Act 2.') + store.dismissModal() // dismiss intro modal + expect(bridge.look).toHaveBeenCalled() + }) + + it('shows transition modal after quest completion modals when act transitions', () => { + bridge.completeQuest + .mockReturnValueOnce('Main quest complete!') + .mockReturnValueOnce(null) + bridge.activateQuest.mockReturnValue(null) + bridge.updateQuest.mockReturnValue(null) + bridge.isAcceptingInput.mockReturnValue(false) + bridge.isActRunning.mockReturnValue(false) + bridge.advanceToRunning.mockReturnValue([ + 'Congratulations Act 1!', + 'Act 2 begins.', + '', + ]) + store.submitCommand('use map') + expect(store.modalTitle).toBe('πŸ† Quest Complete!') + store.dismissModal() + expect(store.showModal).toBe(true) + expect(store.modalTitle).toBe('πŸ† Act Complete!') + }) + + it('does not trigger transition modals when act is running normally', () => { + // isActRunning stays true (default), so no transition modals + bridge.activateQuest.mockReturnValue(null) + bridge.updateQuest.mockReturnValue(null) + bridge.completeQuest.mockReturnValue(null) + store.submitCommand('look') + expect(store.showModal).toBe(false) + expect(bridge.advanceToRunning).not.toHaveBeenCalled() + }) + }) + // --- getSaveSlots / saveToSlot / loadFromSlot --- describe('getSaveSlots', () => { diff --git a/web/src/stores/useGameStore.ts b/web/src/stores/useGameStore.ts index d568226..415b639 100644 --- a/web/src/stores/useGameStore.ts +++ b/web/src/stores/useGameStore.ts @@ -41,8 +41,10 @@ export interface GameBridge { saveToSlot(slot: number): string loadFromSlot(slot: number): string isAcceptingInput(): boolean + isActRunning(): boolean getCommandCompletions(): CompletionTree advanceTurn(): string + advanceToRunning(): string[] getMusicInfo(): { musicFile: string; musicInfo: string } look(): string } @@ -166,6 +168,33 @@ export const useGameStore = defineStore('game', () => { refreshPanels() pollQuestEvents() refreshPanels() + if (!acceptInput.value && !showModal.value && !b.isActRunning()) { + triggerActTransitionModals() + } + } + + function triggerActTransitionModals(): void { + const b = requireBridge() + const texts = b.advanceToRunning().filter((t) => t.trim() !== '') + if (texts.length === 0) return + const transitionTexts = texts.slice(0, -1) + const actIntroText = texts[texts.length - 1] + for (const text of transitionTexts) { + modalQueue.value.push({ + title: 'πŸ† Act Complete!', + body: renderMarkup(text), + sound: null, + }) + } + modalQueue.value.push({ + title: 'πŸ“– Act Intro', + body: renderMarkup(actIntroText), + sound: null, + }) + pendingLookOnDismiss = true + if (!showModal.value) { + showNextModal() + } } function advanceTurn(): void { @@ -319,6 +348,10 @@ export const useGameStore = defineStore('game', () => { lastOutput.value = renderMarkup(b.look()) refreshPanels() pollQuestEvents() + } else if (!acceptInput.value && !b.isActRunning()) { + // Quest completion modals were dismissed but the act has transitioned; + // drive the transition forward and show the act intro modal. + triggerActTransitionModals() } } } From 1d64c8d8a281a3f3a6bd184acfd4899447558b81 Mon Sep 17 00:00:00 2001 From: Henrik Kurelid Date: Tue, 21 Apr 2026 13:15:11 +0200 Subject: [PATCH 5/7] fix: resolve 3 pre-existing e2e test failures (mobile drawer/toolbar) - Add data-testid='side-panel' to SidePanel.vue so the sidebar visibility test has a specific locator (the generic .max-md:hidden class now matches 6 elements, causing a strict-mode violation) - Add data-testid='top-bar' to TopBar.vue so toolbar-button tests can be scoped to the TopBar; the Quick Action Bar in CommandInput also has an always-visible Help button with the same text - Use try/finally in MobileDrawer.emitAndClose so the drawer always closes even if the action handler (e.g. store.saveGame) throws (bridge not initialized in e2e mock context) - Update e2e/game.spec.ts tests 1-3 to use the new data-testid selectors and scoped getByRole queries All 35 e2e + 259 unit + 472 Python tests pass. Formatting constraints verified. --- web/e2e/game.spec.ts | 13 +++++++------ web/src/components/MobileDrawer.vue | 15 +++++++++------ web/src/components/SidePanel.vue | 1 + web/src/components/TopBar.vue | 1 + 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/web/e2e/game.spec.ts b/web/e2e/game.spec.ts index dce4a90..8c0b6d7 100644 --- a/web/e2e/game.spec.ts +++ b/web/e2e/game.spec.ts @@ -357,7 +357,7 @@ test.describe('Mobile viewport', () => { }) test('sidebar is hidden on mobile', async ({ page }) => { - const sidebar = page.locator('.max-md\\:hidden') + const sidebar = page.getByTestId('side-panel') await expect(sidebar).toBeHidden() }) @@ -394,20 +394,21 @@ test.describe('Mobile toolbar overflow fix', () => { test('Quicksave, Quickload, Save, Load, Help are hidden in toolbar on mobile', async ({ page, }) => { + const topBar = page.getByTestId('top-bar') await expect( - page.getByRole('button', { name: /Quicksave/i }).first(), + topBar.getByRole('button', { name: /Quicksave/i }), ).toBeHidden() await expect( - page.getByRole('button', { name: /Quickload/i }).first(), + topBar.getByRole('button', { name: /Quickload/i }), ).toBeHidden() await expect( - page.getByRole('button', { name: /^πŸ’Ύ Save$/i }), + topBar.getByRole('button', { name: /^πŸ’Ύ Save$/i }), ).toBeHidden() await expect( - page.getByRole('button', { name: /^πŸ“‚ Load$/i }), + topBar.getByRole('button', { name: /^πŸ“‚ Load$/i }), ).toBeHidden() await expect( - page.getByRole('button', { name: /^❓ Help$/i }), + topBar.getByRole('button', { name: /^❓ Help$/i }), ).toBeHidden() }) diff --git a/web/src/components/MobileDrawer.vue b/web/src/components/MobileDrawer.vue index a615840..5c4efaa 100644 --- a/web/src/components/MobileDrawer.vue +++ b/web/src/components/MobileDrawer.vue @@ -34,12 +34,15 @@ const emit = defineEmits<{ function emitAndClose( action: 'quickSave' | 'quickLoad' | 'save' | 'load' | 'help', ) { - if (action === 'quickSave') emit('quickSave') - else if (action === 'quickLoad') emit('quickLoad') - else if (action === 'save') emit('save') - else if (action === 'load') emit('load') - else emit('help') - emit('close') + try { + if (action === 'quickSave') emit('quickSave') + else if (action === 'quickLoad') emit('quickLoad') + else if (action === 'save') emit('save') + else if (action === 'load') emit('load') + else emit('help') + } finally { + emit('close') + } } diff --git a/web/src/components/SidePanel.vue b/web/src/components/SidePanel.vue index 2712602..5ceab76 100644 --- a/web/src/components/SidePanel.vue +++ b/web/src/components/SidePanel.vue @@ -34,6 +34,7 @@ defineEmits<{