diff --git a/apps/web/src/lib/components/OfflineChip.svelte b/apps/web/src/lib/components/OfflineChip.svelte new file mode 100644 index 0000000..1015b77 --- /dev/null +++ b/apps/web/src/lib/components/OfflineChip.svelte @@ -0,0 +1,41 @@ + + +{#if !$isOnline} +
+ + {#if !compact} + {m.pwa_offline_chip()} + {/if} + {#if $pendingOpsCount > 0} + + {$pendingOpsCount} + + {/if} +
+{/if} diff --git a/apps/web/src/lib/components/layout/DesktopSidebar.svelte b/apps/web/src/lib/components/layout/DesktopSidebar.svelte index 977e4a7..0e912ea 100644 --- a/apps/web/src/lib/components/layout/DesktopSidebar.svelte +++ b/apps/web/src/lib/components/layout/DesktopSidebar.svelte @@ -7,6 +7,7 @@ import type { SectionKey } from '@colectivo/types'; import Avatar from '$lib/components/Avatar.svelte'; import CreateCollectiveModal from '$lib/components/CreateCollectiveModal.svelte'; + import OfflineChip from '$lib/components/OfflineChip.svelte'; import * as m from '$lib/paraglide/messages'; import { ShoppingCart, CheckSquare, FileText, Search, Settings, Plus, Shield } from 'lucide-svelte'; @@ -101,6 +102,9 @@
+
+ +
{#if $isServerAdmin} diff --git a/apps/web/src/lib/components/layout/MobileTopBar.svelte b/apps/web/src/lib/components/layout/MobileTopBar.svelte index 1c80b11..af142fb 100644 --- a/apps/web/src/lib/components/layout/MobileTopBar.svelte +++ b/apps/web/src/lib/components/layout/MobileTopBar.svelte @@ -2,6 +2,7 @@ import { displayName } from '$lib/stores/auth'; import { currentCollective } from '$lib/stores/collective'; import Avatar from '$lib/components/Avatar.svelte'; + import OfflineChip from '$lib/components/OfflineChip.svelte'; import { Menu } from 'lucide-svelte'; import * as m from '$lib/paraglide/messages'; @@ -26,6 +27,8 @@
+ + diff --git a/apps/web/tests/e2e/offline.test.ts b/apps/web/tests/e2e/offline.test.ts index 25118fa..f94aeae 100644 --- a/apps/web/tests/e2e/offline.test.ts +++ b/apps/web/tests/e2e/offline.test.ts @@ -40,6 +40,37 @@ test.describe('Offline queue + reconnect flush', () => { await context.setOffline(false); }); + test('OF-01: going offline shows the persistent chip + a queue badge', async ({ + page, + context + }) => { + await loginAs(page, USERS.borja); + await page.goto(SEED_LIST_PATH); + await expect(page.getByPlaceholder(ADD_ITEM_PLACEHOLDER)).toBeVisible({ timeout: 15_000 }); + + // Online → chip hidden. + await expect(page.getByTestId('pwa-offline-chip')).toHaveCount(0); + + await context.setOffline(true); + // At least one chip (mobile + desktop both mount; only the visible one + // matters but locator-count-based assertions need both). + await expect(page.getByTestId('pwa-offline-chip').first()).toBeVisible({ timeout: 3_000 }); + + const itemName = `OF-01-${Date.now()}`; + const input = page.getByPlaceholder(ADD_ITEM_PLACEHOLDER); + await input.fill(itemName); + await input.press('Enter'); + + // pending_ops should now contain at least the INSERT for the new item. + await expect(page.getByTestId('pwa-offline-chip-badge').first()).toHaveText(/\d+/, { + timeout: 3_000 + }); + await expect(page.getByText(itemName)).toBeVisible({ timeout: 3_000 }); + + await context.setOffline(false); + await expect(page.getByTestId('pwa-offline-chip')).toHaveCount(0, { timeout: 5_000 }); + }); + test('O-02: going back online flushes the queue to the server', async ({ browser, context