From 6aea7e7288a4916bba7b191f2de3dfb788ea2b19 Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 09:34:04 -0300 Subject: [PATCH 01/13] fix: disable 'Save Map' button while mutation is pending MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'Save Map' button in both the settings sidebar and the mobile overlay was always enabled, but clicking it while createMap.isPending truly does nothing — the previous mutation is still in flight. Disable both instances while createMap.isPending to prevent confusing no-op clicks. --- src/screens/MapScreen/MapScreen.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/screens/MapScreen/MapScreen.tsx b/src/screens/MapScreen/MapScreen.tsx index fc90a3d2..34b32b99 100644 --- a/src/screens/MapScreen/MapScreen.tsx +++ b/src/screens/MapScreen/MapScreen.tsx @@ -319,7 +319,11 @@ export function MapScreen() { )); })()} - @@ -424,7 +428,11 @@ export function MapScreen() { ) : null} {drawMode !== 'draw_rectangle' && (
-
From 46ed469fde06c5bae2b132045ce545f98e3ad3ac Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 10:07:51 -0300 Subject: [PATCH 02/13] test: assert Save Map buttons are disabled while mutation is pending Adds a unit test verifying that both Save Map trigger buttons (desktop sidebar and mobile floating button) enter disabled state while createMap.isPending is true, preventing re-opening the name dialog during an in-flight save mutation. Reported by Greptile on PR #139. --- .../unit/screens/MapScreen/MapScreen.test.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index b9835be3..7d984fe5 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -248,4 +248,36 @@ describe('MapScreen', () => { ).toBeInTheDocument(); expect(screen.getByLabelText('Map name')).toHaveValue('Field map'); }); + + it('disables both Save Map trigger buttons while a save mutation is pending', async () => { + const user = userEvent.setup(); + + // Never-resolving promise keeps the mutation permanently pending + let resolveAdd: (value: string) => void; + const pendingPromise = new Promise((resolve) => { + resolveAdd = resolve; + }); + vi.spyOn(getDb().maps, 'add').mockReturnValueOnce(pendingPromise as any); + + render(); + + // Open the name dialog via one of the trigger buttons + await user.click( + (await screen.findAllByRole('button', { name: 'Save Map' })).at(-1)!, + ); + await user.type(await screen.findByLabelText('Map name'), 'Field map'); + await user.click(screen.getByRole('button', { name: 'Save draft' })); + + // The dialog closes on submit, so the trigger buttons are visible again. + // While the mutation is pending, both Save Map buttons should be disabled. + const saveButtons = await screen.findAllByRole('button', { + name: 'Save Map', + }); + for (const btn of saveButtons) { + expect(btn).toBeDisabled(); + } + + // Cleanup: resolve the promise so the test can finish + resolveAdd!('done'); + }); }); From 84a91163dffd732885617a498e36afe7cb182717 Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 10:09:18 -0300 Subject: [PATCH 03/13] test: dismiss dialog before checking disabled Save Map buttons The handleSaveMap function awaits mutateAsync before closing the dialog, so the buttons stay hidden behind the modal overlay while the mutation is pending. Click Cancel to dismiss first, then verify both trigger buttons are disabled. Amended test from Greptile feedback on PR #139. --- tests/unit/screens/MapScreen/MapScreen.test.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index 7d984fe5..bb0ba025 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -261,18 +261,23 @@ describe('MapScreen', () => { render(); - // Open the name dialog via one of the trigger buttons + // Open the name dialog and submit — the mutation stays pending await user.click( (await screen.findAllByRole('button', { name: 'Save Map' })).at(-1)!, ); await user.type(await screen.findByLabelText('Map name'), 'Field map'); await user.click(screen.getByRole('button', { name: 'Save draft' })); - // The dialog closes on submit, so the trigger buttons are visible again. - // While the mutation is pending, both Save Map buttons should be disabled. - const saveButtons = await screen.findAllByRole('button', { + // Dismiss the dialog (ESC) so the trigger buttons are visible again. + // The mutation is still pending — handleSaveMap awaits mutateAsync + // before closing, so the dialog stays open until we force-close. + await user.click(screen.getByRole('button', { name: 'Cancel' })); + + // Both Save Map trigger buttons should be disabled while pending + const saveButtons = screen.getAllByRole('button', { name: 'Save Map', }); + expect(saveButtons).toHaveLength(2); for (const btn of saveButtons) { expect(btn).toBeDisabled(); } From 95699d9a0ed181cf2058278723e3ef8c11987454 Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 10:23:28 -0300 Subject: [PATCH 04/13] fix: suppres eslint no-explicit-any for Dexie PromiseExtended mock Dexie.add() returns PromiseExtended, not a standard Promise. The lint-safe cast would require importing Dexie's internal type. Use eslint-disable-next-line with explanation instead. --- tests/unit/screens/MapScreen/MapScreen.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index bb0ba025..4506e62e 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -257,6 +257,7 @@ describe('MapScreen', () => { const pendingPromise = new Promise((resolve) => { resolveAdd = resolve; }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Dexie's PromiseExtended vs standard Promise vi.spyOn(getDb().maps, 'add').mockReturnValueOnce(pendingPromise as any); render(); From 2138a804878507b4dc7b35b7bd7a9df6f15be7f8 Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 10:34:15 -0300 Subject: [PATCH 05/13] fix: disable settings-sheet Save Map when nothing configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings-sheet Save Map button (mobile 'Map settings' panel) was always enabled, but clicking it with default bbox/zoom/basemap creates a meaningless draft. Now disabled until the user actually changes at least one config option (basemap, bounds, or zoom range). The floating Save Map button on the map (mobile, bottom-right) remains always enabled — its context implies active map work. Adds hasConfigChanges memo comparing bbox, zoomRange, and selectedStyle against DEFAULT_* constants. Tests updated: - New test: disabled with defaults, enabled after basemap change - Existing tests: change basemap first to enable button before clicking --- src/screens/MapScreen/MapScreen.tsx | 14 +++++- .../unit/screens/MapScreen/MapScreen.test.tsx | 50 +++++++++++++++---- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/src/screens/MapScreen/MapScreen.tsx b/src/screens/MapScreen/MapScreen.tsx index 34b32b99..47c51d22 100644 --- a/src/screens/MapScreen/MapScreen.tsx +++ b/src/screens/MapScreen/MapScreen.tsx @@ -224,6 +224,18 @@ export function MapScreen() { ); useShellSlot(shellSlot); + const hasConfigChanges = useMemo( + () => + bbox[0] !== DEFAULT_BBOX[0] || + bbox[1] !== DEFAULT_BBOX[1] || + bbox[2] !== DEFAULT_BBOX[2] || + bbox[3] !== DEFAULT_BBOX[3] || + zoomRange.minZoom !== DEFAULT_ZOOM.minZoom || + zoomRange.maxZoom !== DEFAULT_ZOOM.maxZoom || + selectedStyle.id !== DEFAULT_BASEMAP_ID, + [bbox, zoomRange, selectedStyle], + ); + function openNameDialog() { setNameError(null); setNameDialogOpen(true); @@ -322,7 +334,7 @@ export function MapScreen() { diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index 4506e62e..7d68eaec 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -234,8 +234,13 @@ describe('MapScreen', () => { render(); + // Change something from defaults so the settings sheet Save Map button is enabled await user.click( - (await screen.findAllByRole('button', { name: 'Save Map' })).at(-1)!, + await screen.findByRole('button', { name: 'OpenStreetMap' }), + ); + + await user.click( + (await screen.findAllByRole('button', { name: 'Save Map' })).slice(-1)[0]!, ); await user.type(await screen.findByLabelText('Map name'), 'Field map'); await user.click(screen.getByRole('button', { name: 'Save draft' })); @@ -249,7 +254,27 @@ describe('MapScreen', () => { expect(screen.getByLabelText('Map name')).toHaveValue('Field map'); }); - it('disables both Save Map trigger buttons while a save mutation is pending', async () => { + it('disables the settings sheet Save Map button when nothing has been configured', async () => { + const user = userEvent.setup(); + + render(); + + // With default bbox, zoom, and basemap, the settings sheet button + // (last Save Map button in DOM order) should be disabled. + const saveButtons = await screen.findAllByRole('button', { + name: 'Save Map', + }); + const settingsSheetButton = saveButtons[saveButtons.length - 1]!; + expect(settingsSheetButton).toBeDisabled(); + + // After changing the basemap, the button should become enabled + await user.click( + screen.getByRole('button', { name: 'OpenStreetMap' }), + ); + expect(settingsSheetButton).toBeEnabled(); + }); + + it('disables the settings sheet Save Map button while a save mutation is pending', async () => { const user = userEvent.setup(); // Never-resolving promise keeps the mutation permanently pending @@ -262,26 +287,29 @@ describe('MapScreen', () => { render(); - // Open the name dialog and submit — the mutation stays pending + // Change basemap so the settings sheet Save Map button is enabled await user.click( - (await screen.findAllByRole('button', { name: 'Save Map' })).at(-1)!, + await screen.findByRole('button', { name: 'OpenStreetMap' }), ); + + // Click the settings sheet Save Map button (last in DOM order) + const saveButtons = await screen.findAllByRole('button', { + name: 'Save Map', + }); + await user.click(saveButtons[saveButtons.length - 1]!); await user.type(await screen.findByLabelText('Map name'), 'Field map'); await user.click(screen.getByRole('button', { name: 'Save draft' })); - // Dismiss the dialog (ESC) so the trigger buttons are visible again. + // Dismiss the dialog (Cancel) so the trigger buttons are visible again. // The mutation is still pending — handleSaveMap awaits mutateAsync // before closing, so the dialog stays open until we force-close. await user.click(screen.getByRole('button', { name: 'Cancel' })); - // Both Save Map trigger buttons should be disabled while pending - const saveButtons = screen.getAllByRole('button', { + // The settings sheet Save Map button should be disabled while pending + const refreshedButtons = screen.getAllByRole('button', { name: 'Save Map', }); - expect(saveButtons).toHaveLength(2); - for (const btn of saveButtons) { - expect(btn).toBeDisabled(); - } + expect(refreshedButtons[refreshedButtons.length - 1]).toBeDisabled(); // Cleanup: resolve the promise so the test can finish resolveAdd!('done'); From b77b443281b78d4b9ab10b5ac8ef1d55a208494d Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 10:39:34 -0300 Subject: [PATCH 06/13] style: prettier fix for MapScreen test --- tests/unit/screens/MapScreen/MapScreen.test.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index 7d68eaec..c38795ff 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -240,7 +240,9 @@ describe('MapScreen', () => { ); await user.click( - (await screen.findAllByRole('button', { name: 'Save Map' })).slice(-1)[0]!, + (await screen.findAllByRole('button', { name: 'Save Map' })).slice( + -1, + )[0]!, ); await user.type(await screen.findByLabelText('Map name'), 'Field map'); await user.click(screen.getByRole('button', { name: 'Save draft' })); @@ -268,9 +270,7 @@ describe('MapScreen', () => { expect(settingsSheetButton).toBeDisabled(); // After changing the basemap, the button should become enabled - await user.click( - screen.getByRole('button', { name: 'OpenStreetMap' }), - ); + await user.click(screen.getByRole('button', { name: 'OpenStreetMap' })); expect(settingsSheetButton).toBeEnabled(); }); From 0ae8653652557c906d38b199201325dc9ee5dfbf Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 10:51:01 -0300 Subject: [PATCH 07/13] fix: distinguish online vs offline active map badge text The online active badge (shown when a map is set active but SMP blob not yet downloaded) was reusing the 'Active offline map' i18n message, making it appear the map was already available offline when it wasn't. Added a separate activeMapOnlineBadge message ('Active map (online)') and updated the online badge to use it. The offline badge retains its original 'Active offline map' label. --- src/components/shared/MapContainer/MapContainer.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/shared/MapContainer/MapContainer.tsx b/src/components/shared/MapContainer/MapContainer.tsx index 5b3ab038..03573205 100644 --- a/src/components/shared/MapContainer/MapContainer.tsx +++ b/src/components/shared/MapContainer/MapContainer.tsx @@ -40,6 +40,10 @@ const messages = defineMessages({ id: 'map.activeMap.badge', defaultMessage: 'Active offline map: {name}', }, + activeMapOnlineBadge: { + id: 'map.activeMap.onlineBadge', + defaultMessage: 'Active map (online): {name}', + }, basemapDisabledHint: { id: 'map.basemapSwitcher.disabledHint', defaultMessage: 'Basemap used when offline map is turned off', @@ -403,7 +407,7 @@ function MapContainer({ strokeLinecap="round" /> - {intl.formatMessage(messages.activeMapBadge, { + {intl.formatMessage(messages.activeMapOnlineBadge, { name: activeSavedMap?.name ?? '', })} From c2cdbe7687a02e78d9064994919d3b388158d4cc Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 10:55:26 -0300 Subject: [PATCH 08/13] i18n: extract new activeMapOnlineBadge message --- src/i18n/messages/en.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 09d1ac10..be6e3def 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -962,6 +962,9 @@ "map.activeMap.badge": { "defaultMessage": "Active offline map: {name}" }, + "map.activeMap.onlineBadge": { + "defaultMessage": "Active map (online): {name}" + }, "map.basemap.label": { "defaultMessage": "Basemap" }, From 391d51af0f8aa50957ff4565268930724e15c104 Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 11:56:31 -0300 Subject: [PATCH 09/13] fix: close settings sheet before opening name dialog on mobile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings sheet (z-[51]) was covering the name dialog Modal (z-50), making the Save Map button appear broken — tapping it opened a dialog the user could never see or interact with. Added setSettingsOpen(false) in openNameDialog() so the sheet closes before the name dialog opens. Desktop is unaffected because the sidebar is a plain aside, not a z-indexed dialog. Added test verifying single dialog remains after opening name dialog from mobile settings sheet. --- src/screens/MapScreen/MapScreen.tsx | 1 + .../unit/screens/MapScreen/MapScreen.test.tsx | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/screens/MapScreen/MapScreen.tsx b/src/screens/MapScreen/MapScreen.tsx index 47c51d22..554cf1c1 100644 --- a/src/screens/MapScreen/MapScreen.tsx +++ b/src/screens/MapScreen/MapScreen.tsx @@ -238,6 +238,7 @@ export function MapScreen() { function openNameDialog() { setNameError(null); + setSettingsOpen(false); setNameDialogOpen(true); } diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index c38795ff..5ecf1fe3 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -202,6 +202,35 @@ describe('MapScreen', () => { // Frame stays in draw mode instead of confirming an inverted bbox expect(screen.queryByText('Map area updated')).not.toBeInTheDocument(); }); + it('closes the settings sheet when opening the name dialog on mobile', async () => { + const user = userEvent.setup(); + + render(); + + // Open settings sheet via the mobile Settings button + await user.click( + await screen.findByRole('button', { name: 'Map settings' }), + ); + + // Change basemap so the Save Map button is enabled + await user.click( + await screen.findByRole('button', { name: 'OpenStreetMap' }), + ); + + // Click the settings sheet Save Map button (last in DOM order) + const saveButtons = await screen.findAllByRole('button', { + name: 'Save Map', + }); + await user.click(saveButtons[saveButtons.length - 1]!); + + // The name dialog should be open + expect( + await screen.findByRole('dialog', { name: 'Save map' }), + ).toBeInTheDocument(); + + // The settings sheet should be closed — only one dialog should exist + expect(screen.getAllByRole('dialog')).toHaveLength(1); + }); }); it('updates the canvas map style when the selected style changes', async () => { From 7b36295c86b2429ed15108b9a5e7b03c1b0ff146 Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 23:22:16 -0300 Subject: [PATCH 10/13] test: settle pending map mutation cleanly - await deferred mutation resolution and verify recovery\n- add online active-map translations for Portuguese and Spanish --- src/i18n/messages/es.json | 1 + src/i18n/messages/pt.json | 1 + .../unit/screens/MapScreen/MapScreen.test.tsx | 22 +++++++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 4781a22a..82e49e4c 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -477,5 +477,6 @@ "theme.light": "Claro", "theme.system": "Sistema", "map.activeMap.badge": "Mapa sin conexión activo: {name}", + "map.activeMap.onlineBadge": "Mapa activo (en línea): {name}", "map.basemapSwitcher.disabledHint": "Mapa base usado cuando el mapa sin conexión está desactivado" } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index d343b508..6034f1c0 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -477,5 +477,6 @@ "theme.light": "Claro", "theme.system": "Sistema", "map.activeMap.badge": "Mapa offline ativo: {name}", + "map.activeMap.onlineBadge": "Mapa ativo (online): {name}", "map.basemapSwitcher.disabledHint": "Mapa base usado quando o mapa offline está desativado" } diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index 5ecf1fe3..70d21c68 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -1,4 +1,10 @@ -import { render, screen, userEvent, waitFor } from '@tests/mocks/test-utils'; +import { + act, + render, + screen, + userEvent, + waitFor, +} from '@tests/mocks/test-utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import React from 'react'; @@ -306,7 +312,7 @@ describe('MapScreen', () => { it('disables the settings sheet Save Map button while a save mutation is pending', async () => { const user = userEvent.setup(); - // Never-resolving promise keeps the mutation permanently pending + // Deferred promise keeps the mutation pending until we resolve it let resolveAdd: (value: string) => void; const pendingPromise = new Promise((resolve) => { resolveAdd = resolve; @@ -340,7 +346,15 @@ describe('MapScreen', () => { }); expect(refreshedButtons[refreshedButtons.length - 1]).toBeDisabled(); - // Cleanup: resolve the promise so the test can finish - resolveAdd!('done'); + // Resolve the mutation inside act() so React state updates are flushed + await act(async () => { + resolveAdd!('done'); + }); + + // Button re-enables after the mutation settles + await waitFor(() => { + const buttons = screen.getAllByRole('button', { name: 'Save Map' }); + expect(buttons[buttons.length - 1]).toBeEnabled(); + }); }); }); From 8d839eddb0a94dbc5d0cce38ebc4d6813750b58a Mon Sep 17 00:00:00 2001 From: Terrastories Date: Tue, 21 Jul 2026 23:34:11 -0300 Subject: [PATCH 11/13] test: cover both pending save triggers - verify online active-map badge copy and map name\n- assert both mobile Save Map triggers disable and recover --- .../shared/MapContainer/MapContainer.test.tsx | 6 ++-- .../unit/screens/MapScreen/MapScreen.test.tsx | 36 ++++++++++++------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/tests/unit/components/shared/MapContainer/MapContainer.test.tsx b/tests/unit/components/shared/MapContainer/MapContainer.test.tsx index f1e26082..3bd72019 100644 --- a/tests/unit/components/shared/MapContainer/MapContainer.test.tsx +++ b/tests/unit/components/shared/MapContainer/MapContainer.test.tsx @@ -387,8 +387,10 @@ describe('MapContainer', () => { const mapEl = screen.getByTestId('mock-map'); expect(mapEl.dataset.mapStyle).toBe('StyleSpecification'); }); - // Should show online active badge - expect(screen.getByTestId('map-online-active-badge')).toBeInTheDocument(); + // Should show online active badge with map name + const badge = screen.getByTestId('map-online-active-badge'); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent(/Active map.*online.*Draft Map/); // Should show tooltip on basemap switcher const trigger = screen.getByTestId('basemap-switcher-trigger'); expect(trigger.getAttribute('title')).toBeTruthy(); diff --git a/tests/unit/screens/MapScreen/MapScreen.test.tsx b/tests/unit/screens/MapScreen/MapScreen.test.tsx index 70d21c68..e3cb2f1e 100644 --- a/tests/unit/screens/MapScreen/MapScreen.test.tsx +++ b/tests/unit/screens/MapScreen/MapScreen.test.tsx @@ -309,7 +309,7 @@ describe('MapScreen', () => { expect(settingsSheetButton).toBeEnabled(); }); - it('disables the settings sheet Save Map button while a save mutation is pending', async () => { + it('disables both Save Map triggers (floating + settings-sheet) while a save mutation is pending', async () => { const user = userEvent.setup(); // Deferred promise keeps the mutation pending until we resolve it @@ -322,16 +322,19 @@ describe('MapScreen', () => { render(); - // Change basemap so the settings sheet Save Map button is enabled + // Change basemap so both Save Map buttons are enabled await user.click( await screen.findByRole('button', { name: 'OpenStreetMap' }), ); - // Click the settings sheet Save Map button (last in DOM order) - const saveButtons = await screen.findAllByRole('button', { + // Confirm both triggers exist before starting the save flow + const allSaveButtons = await screen.findAllByRole('button', { name: 'Save Map', }); - await user.click(saveButtons[saveButtons.length - 1]!); + expect(allSaveButtons.length).toBeGreaterThanOrEqual(2); + + // Trigger save via the settings-sheet button (last in DOM order) + await user.click(allSaveButtons[allSaveButtons.length - 1]!); await user.type(await screen.findByLabelText('Map name'), 'Field map'); await user.click(screen.getByRole('button', { name: 'Save draft' })); @@ -340,21 +343,28 @@ describe('MapScreen', () => { // before closing, so the dialog stays open until we force-close. await user.click(screen.getByRole('button', { name: 'Cancel' })); - // The settings sheet Save Map button should be disabled while pending - const refreshedButtons = screen.getAllByRole('button', { - name: 'Save Map', - }); - expect(refreshedButtons[refreshedButtons.length - 1]).toBeDisabled(); + // BOTH Save Map buttons should be disabled while createMap is pending: + // 1) floating quick action (bottom-right, mobile) + // 2) settings-sheet trigger (inside aside / sheet) + const pendingButtons = screen.getAllByRole('button', { name: 'Save Map' }); + expect(pendingButtons.length).toBeGreaterThanOrEqual(2); + for (const btn of pendingButtons) { + expect(btn).toBeDisabled(); + } // Resolve the mutation inside act() so React state updates are flushed await act(async () => { resolveAdd!('done'); }); - // Button re-enables after the mutation settles + // BOTH buttons re-enable after the mutation settles await waitFor(() => { - const buttons = screen.getAllByRole('button', { name: 'Save Map' }); - expect(buttons[buttons.length - 1]).toBeEnabled(); + const resolvedButtons = screen.getAllByRole('button', { + name: 'Save Map', + }); + for (const btn of resolvedButtons) { + expect(btn).toBeEnabled(); + } }); }); }); From b8adce6440bcb9726d91d0633d6502023e459d4a Mon Sep 17 00:00:00 2001 From: Terrastories Date: Wed, 22 Jul 2026 00:08:38 -0300 Subject: [PATCH 12/13] test: exercise pending saves at mobile viewport - verify both mobile save triggers during pending and recovery\n- document intentional quick-action behavior for default map views --- src/screens/MapScreen/MapScreen.tsx | 3 + .../unit/screens/MapScreen/MapScreen.test.tsx | 150 +++++++++++------- 2 files changed, 94 insertions(+), 59 deletions(-) diff --git a/src/screens/MapScreen/MapScreen.tsx b/src/screens/MapScreen/MapScreen.tsx index 554cf1c1..0cd5ff2c 100644 --- a/src/screens/MapScreen/MapScreen.tsx +++ b/src/screens/MapScreen/MapScreen.tsx @@ -441,6 +441,9 @@ export function MapScreen() { ) : null} {drawMode !== 'draw_rectangle' && (
+ {/* Intentionally no !hasConfigChanges guard: this quick action + may save the current/default map view at any time. + Only pending-disable is needed here. */}