diff --git a/frontend/src/components/SyncBanner.jsx b/frontend/src/components/SyncBanner.jsx
index 24026d434..5b0a0d7a4 100644
--- a/frontend/src/components/SyncBanner.jsx
+++ b/frontend/src/components/SyncBanner.jsx
@@ -1,6 +1,7 @@
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useStore } from '../store/useStore.js'
+import { useUI } from '../store/useUI.js'
import { DEMO } from '../lib/demo.js'
import { connectionView, actionLabel, syncNowWithToast, pairAgain, connectServer, signInAgain, useOnline } from './ServerSync.jsx'
import Icon from './Icon.jsx'
@@ -10,10 +11,12 @@ import Icon from './Icon.jsx'
// every one of them would only teach people to ignore the line.
export const PENDING_GRACE_MS = 5000
-/* The connection, always in view while the app is not connected to a server: offline, the server
- unreachable or answering with an error (its HTTP code, for whoever runs it), a server that no
- longer accepts this device, an answer that is not openGym's, and no server at all — a phone
- kept local, a guest in a browser. It cannot be dismissed; it goes when the condition does.
+/* The connection, in view while the app is not connected to a server unless hidden in Settings:
+ offline, the server unreachable or answering with an error (its HTTP code, for whoever runs
+ it), a server that no longer accepts this device, an answer that is not openGym's, and no
+ server at all — a phone kept local or a guest in a browser. The device-local display preference
+ can hide it; its details
+ remain in Settings → Server & sync.
The first ones say what is wrong and that the changes are kept here, with the one thing to do
about it (retry, pair again, sign in); the deliberate local setup only says so, quietly.
@@ -27,6 +30,7 @@ export default function SyncBanner() {
const sync = useStore(s => s.sync)
const guest = useStore(s => s.isGuest())
const onboarding = useStore(s => s.needsMobileOnboarding)
+ const showSyncBanner = useUI(s => s.showSyncBanner)
const online = useOnline()
const [waited, setWaited] = useState(false)
const row = useRef(null)
@@ -41,7 +45,7 @@ export default function SyncBanner() {
const view = connectionView(sync, { online })
// Signed out on the web, the sign-in screen is the whole app: it hears only that the server
// ended the session, and that the changes are still here.
- const show = !DEMO && !onboarding && !!view?.banner && (!!user || guest || status === 'auth') && (status !== 'pending' || waited)
+ const show = showSyncBanner && !DEMO && !onboarding && !!view?.banner && (!!user || guest || status === 'auth') && (status !== 'pending' || waited)
useLayoutEffect(() => {
const root = document.documentElement
diff --git a/frontend/src/components/SyncBanner.test.jsx b/frontend/src/components/SyncBanner.test.jsx
index 2fa4ec48d..c152c7aca 100644
--- a/frontend/src/components/SyncBanner.test.jsx
+++ b/frontend/src/components/SyncBanner.test.jsx
@@ -7,20 +7,22 @@ import { connectionView } from './ServerSync.jsx'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
-/* The connection indicator: every way the app can be without its server gets a line that stays
- while the condition lasts — offline, an error with its HTTP code, a server that refuses this
- device, an answer that is not openGym's, no server at all — and says what to do about it.
- Never in the public demo, never during the phone's first-launch choice, and a change merely
- waiting for its push does not flash it. The store is a stand-in: its `sync` is what each test
- sets; ServerSync.jsx (the words and the actions) is the real one. */
+/* By default, the connection indicator gives every way the app can be without its server a line
+ that stays while the condition lasts — offline, an error with its HTTP code, a server that
+ refuses this device, an answer that is not openGym's, no server at all — and says what to do
+ about it. The device can hide it in Settings, where the full status remains. Never in the public
+ demo, never during the phone's first-launch choice, and a change merely waiting for its push
+ does not flash it. The store is a stand-in: its `sync` is what each test sets; ServerSync.jsx
+ (the words and the actions) is the real one. */
const mocks = vi.hoisted(() => {
- const state = { MOBILE: false, DEMO: false, webauthn: true, user: null, guest: false, onboarding: false, sync: null, sheets: [], navs: [] }
+ const state = { MOBILE: false, DEMO: false, webauthn: true, user: null, guest: false, onboarding: false, sync: null, showSyncBanner: true, sheets: [], navs: [] }
state.toast = vi.fn()
state.syncNow = vi.fn(async () => state.sync)
state.passkeyLogin = vi.fn(async () => ({ id: 'u1', name: 'andi' }))
state.snapshot = () => ({
user: state.user, sync: state.sync, needsMobileOnboarding: state.onboarding,
isGuest: () => state.guest, syncNow: state.syncNow,
+ showSyncBanner: state.showSyncBanner,
setUser: vi.fn(), adoptProfile: vi.fn(async () => ({})),
})
return state
@@ -31,7 +33,7 @@ vi.mock('../store/useStore.js', () => {
return { useStore }
})
vi.mock('../store/useUI.js', () => {
- const snap = () => ({ toast: (...a) => mocks.toast(...a), openSheet: (render, opts) => { mocks.sheets.push({ render, opts }); return {} } })
+ const snap = () => ({ showSyncBanner: mocks.showSyncBanner, toast: (...a) => mocks.toast(...a), openSheet: (render, opts) => { mocks.sheets.push({ render, opts }); return {} } })
const useUI = selector => selector ? selector(snap()) : snap()
useUI.getState = snap
return { useUI }
@@ -56,7 +58,7 @@ const network = on => {
let host, root
beforeEach(() => {
network(true)
- Object.assign(mocks, { MOBILE: false, DEMO: false, webauthn: true, user: { id: 'u1', name: 'andi' }, guest: false, onboarding: false, sync: sync('ok') })
+ Object.assign(mocks, { MOBILE: false, DEMO: false, webauthn: true, user: { id: 'u1', name: 'andi' }, guest: false, onboarding: false, sync: sync('ok'), showSyncBanner: true })
mocks.sheets.length = 0
mocks.navs.length = 0
mocks.toast.mockClear(); mocks.syncNow.mockClear(); mocks.passkeyLogin.mockClear()
@@ -110,6 +112,16 @@ describe('connected and in step', () => {
})
})
+describe('the device-local display preference', () => {
+ it('hides the banner and gives its space back when switched off', () => {
+ mocks.showSyncBanner = false
+ network(false)
+ render()
+ expect(bar()).toBeNull()
+ expect(conn()).toBe('')
+ })
+})
+
describe('not connected — it says so, and what to do', () => {
it('offline with changes waiting: kept on this device, and a retry that reports back', async () => {
network(false)
diff --git a/frontend/src/locales/ar.js b/frontend/src/locales/ar.js
index 07697206c..3bc6aa264 100644
--- a/frontend/src/locales/ar.js
+++ b/frontend/src/locales/ar.js
@@ -26,6 +26,9 @@ export default {
'Exercises': 'التمارين',
'History': 'السجل',
'Settings': 'الإعدادات',
+ 'Display': 'العرض',
+ 'Show sync banner': 'إظهار شريط المزامنة',
+ 'Show connection and sync status at the top of the screen.': 'إظهار حالة الاتصال والمزامنة أعلى الشاشة.',
'Freestyle': 'تمرين حر',
'Routine': 'روتين',
'routine': 'روتين',
diff --git a/frontend/src/locales/de.js b/frontend/src/locales/de.js
index 663a5a5bb..a6dd961a6 100644
--- a/frontend/src/locales/de.js
+++ b/frontend/src/locales/de.js
@@ -33,6 +33,9 @@ export default {
'Also trains': 'Trainiert auch',
'History': 'Verlauf',
'Settings': 'Einstellungen',
+ 'Display': 'Anzeige',
+ 'Show sync banner': 'Sync-Banner anzeigen',
+ 'Show connection and sync status at the top of the screen.': 'Verbindungs- und Synchronisierungsstatus oben auf dem Bildschirm anzeigen.',
'Freestyle': 'Freies Training',
'Routine': 'Routine',
'routine': 'Routine',
diff --git a/frontend/src/locales/es.js b/frontend/src/locales/es.js
index 8e1d9aa99..da0f2a34c 100644
--- a/frontend/src/locales/es.js
+++ b/frontend/src/locales/es.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'También trabaja',
'History': 'Historial',
'Settings': 'Ajustes',
+ 'Display': 'Visualización',
+ 'Show sync banner': 'Mostrar banner de sincronización',
+ 'Show connection and sync status at the top of the screen.': 'Mostrar el estado de conexión y sincronización en la parte superior de la pantalla.',
'Freestyle': 'Libre',
'Routine': 'Rutina',
'routine': 'rutina',
diff --git a/frontend/src/locales/fr.js b/frontend/src/locales/fr.js
index ee904194c..412473b86 100644
--- a/frontend/src/locales/fr.js
+++ b/frontend/src/locales/fr.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'Sollicite aussi',
'History': 'Historique',
'Settings': 'Réglages',
+ 'Display': 'Affichage',
+ 'Show sync banner': 'Afficher la bannière de synchronisation',
+ 'Show connection and sync status at the top of the screen.': 'Afficher l’état de la connexion et de la synchronisation en haut de l’écran.',
'Freestyle': 'Libre',
'Routine': 'Routine',
'routine': 'routine',
diff --git a/frontend/src/locales/hi.js b/frontend/src/locales/hi.js
index ccb0ca2aa..6bbbdd38a 100644
--- a/frontend/src/locales/hi.js
+++ b/frontend/src/locales/hi.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'यह भी प्रशिक्षित करता है',
'History': 'इतिहास',
'Settings': 'सेटिंग्स',
+ 'Display': 'दिखावट',
+ 'Show sync banner': 'सिंक बैनर दिखाएँ',
+ 'Show connection and sync status at the top of the screen.': 'स्क्रीन के ऊपर कनेक्शन और सिंक की स्थिति दिखाएँ।',
'Freestyle': 'फ्रीस्टाइल',
'Routine': 'रूटीन',
'routine': 'रूटीन',
diff --git a/frontend/src/locales/hu.js b/frontend/src/locales/hu.js
index 61b151edc..237273feb 100644
--- a/frontend/src/locales/hu.js
+++ b/frontend/src/locales/hu.js
@@ -34,6 +34,9 @@ export default {
'Also trains': 'Emellett edzi',
'History': 'Előzmények',
'Settings': 'Beállítások',
+ 'Display': 'Megjelenítés',
+ 'Show sync banner': 'Szinkronizálási sáv megjelenítése',
+ 'Show connection and sync status at the top of the screen.': 'A kapcsolat és a szinkronizálás állapotának megjelenítése a képernyő tetején.',
'Freestyle': 'Szabad edzés',
'Routine': 'Rutin',
'routine': 'rutin',
diff --git a/frontend/src/locales/it.js b/frontend/src/locales/it.js
index 48e5dd34d..5c59b79f2 100644
--- a/frontend/src/locales/it.js
+++ b/frontend/src/locales/it.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'Allena anche',
'History': 'Cronologia',
'Settings': 'Impostazioni',
+ 'Display': 'Visualizzazione',
+ 'Show sync banner': 'Mostra il banner di sincronizzazione',
+ 'Show connection and sync status at the top of the screen.': 'Mostra lo stato della connessione e della sincronizzazione in alto sullo schermo.',
'Freestyle': 'Libero',
'Routine': 'Routine',
'routine': 'routine',
diff --git a/frontend/src/locales/ko.js b/frontend/src/locales/ko.js
index 02628fa0a..74f48dba2 100644
--- a/frontend/src/locales/ko.js
+++ b/frontend/src/locales/ko.js
@@ -32,6 +32,9 @@ export default {
'Also trains': '함께 훈련',
'History': '기록',
'Settings': '설정',
+ 'Display': '화면 표시',
+ 'Show sync banner': '동기화 배너 표시',
+ 'Show connection and sync status at the top of the screen.': '화면 상단에 연결 및 동기화 상태를 표시합니다.',
'Freestyle': '자유 운동',
'Routine': '루틴',
'routine': '루틴',
diff --git a/frontend/src/locales/pl.js b/frontend/src/locales/pl.js
index f5910a684..4257529ee 100644
--- a/frontend/src/locales/pl.js
+++ b/frontend/src/locales/pl.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'Angażuje również',
'History': 'Historia',
'Settings': 'Ustawienia',
+ 'Display': 'Wyświetlanie',
+ 'Show sync banner': 'Pokaż baner synchronizacji',
+ 'Show connection and sync status at the top of the screen.': 'Pokazuj stan połączenia i synchronizacji u góry ekranu.',
'Freestyle': 'Dowolny',
'Routine': 'Plan treningowy',
'routine': 'planu',
diff --git a/frontend/src/locales/pt-BR.js b/frontend/src/locales/pt-BR.js
index 41505d76e..2e92324c2 100644
--- a/frontend/src/locales/pt-BR.js
+++ b/frontend/src/locales/pt-BR.js
@@ -24,6 +24,9 @@ export const PT_BR_OVERRIDES = {
'Save': 'Salvar',
'Log': 'Registrar',
'Settings': 'Configurações',
+ 'Display': 'Exibição',
+ 'Show sync banner': 'Mostrar faixa de sincronização',
+ 'Show connection and sync status at the top of the screen.': 'Mostrar o status da conexão e da sincronização no topo da tela.',
'English exercise names': 'Nomes de exercícios em inglês',
'Show the English name in parentheses next to the translated one.': 'Mostra o nome em inglês entre parênteses ao lado do traduzido.',
'English names only': 'Apenas nomes em inglês',
diff --git a/frontend/src/locales/pt.js b/frontend/src/locales/pt.js
index 65dd8ff54..59ad5ebdd 100644
--- a/frontend/src/locales/pt.js
+++ b/frontend/src/locales/pt.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'Também trabalha',
'History': 'Histórico',
'Settings': 'Definições',
+ 'Display': 'Apresentação',
+ 'Show sync banner': 'Mostrar faixa de sincronização',
+ 'Show connection and sync status at the top of the screen.': 'Mostrar o estado da ligação e da sincronização no topo do ecrã.',
'Freestyle': 'Livre',
'Routine': 'Rotina',
'routine': 'rotina',
diff --git a/frontend/src/locales/ru.js b/frontend/src/locales/ru.js
index 2df1568f7..689f37524 100644
--- a/frontend/src/locales/ru.js
+++ b/frontend/src/locales/ru.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'Также тренирует',
'History': 'История',
'Settings': 'Настройки',
+ 'Display': 'Отображение',
+ 'Show sync banner': 'Показывать баннер синхронизации',
+ 'Show connection and sync status at the top of the screen.': 'Показывать состояние подключения и синхронизации вверху экрана.',
'Freestyle': 'Свободная',
'Routine': 'Программа',
'routine': 'программу',
diff --git a/frontend/src/locales/th.js b/frontend/src/locales/th.js
index 3604d80fa..2c70594cd 100644
--- a/frontend/src/locales/th.js
+++ b/frontend/src/locales/th.js
@@ -34,6 +34,9 @@ export default {
'Also trains': 'ฝึกกล้ามเนื้อนี้ด้วย',
'History': 'ประวัติ',
'Settings': 'ตั้งค่า',
+ 'Display': 'การแสดงผล',
+ 'Show sync banner': 'แสดงแบนเนอร์การซิงค์',
+ 'Show connection and sync status at the top of the screen.': 'แสดงสถานะการเชื่อมต่อและการซิงค์ที่ด้านบนของหน้าจอ',
'Freestyle': 'ฟรีสไตล์',
'Routine': 'รูทีน',
'routine': 'รูทีน',
diff --git a/frontend/src/locales/tr.js b/frontend/src/locales/tr.js
index 1c96ebc30..ec6af5409 100644
--- a/frontend/src/locales/tr.js
+++ b/frontend/src/locales/tr.js
@@ -32,6 +32,9 @@ export default {
'Also trains': 'Ayrıca çalıştırır',
'History': 'Geçmiş',
'Settings': 'Ayarlar',
+ 'Display': 'Görünüm',
+ 'Show sync banner': 'Senkronizasyon bandını göster',
+ 'Show connection and sync status at the top of the screen.': 'Bağlantı ve senkronizasyon durumunu ekranın üst kısmında göster.',
'Freestyle': 'Serbest',
'Routine': 'Rutin',
'routine': 'rutine',
diff --git a/frontend/src/locales/uk.js b/frontend/src/locales/uk.js
index ba239e0c8..858adc8b3 100644
--- a/frontend/src/locales/uk.js
+++ b/frontend/src/locales/uk.js
@@ -25,6 +25,9 @@ export default {
'Exercises': 'Вправи',
'History': 'Історія',
'Settings': 'Налаштування',
+ 'Display': 'Вигляд',
+ 'Show sync banner': 'Показувати банер синхронізації',
+ 'Show connection and sync status at the top of the screen.': 'Показувати стан з’єднання та синхронізації у верхній частині екрана.',
'Freestyle': 'Довільна',
'Routine': 'Програма',
'routine': 'програму',
diff --git a/frontend/src/locales/zh.js b/frontend/src/locales/zh.js
index 84237894a..bac08b4a1 100644
--- a/frontend/src/locales/zh.js
+++ b/frontend/src/locales/zh.js
@@ -32,6 +32,9 @@ export default {
'Also trains': '同时训练',
'History': '历史',
'Settings': '设置',
+ 'Display': '显示',
+ 'Show sync banner': '显示同步横幅',
+ 'Show connection and sync status at the top of the screen.': '在屏幕顶部显示连接和同步状态。',
'Freestyle': '自由训练',
'Routine': '训练日',
'routine': '训练日',
diff --git a/frontend/src/store/useUI.js b/frontend/src/store/useUI.js
index 74f803eb4..0db53ea3a 100644
--- a/frontend/src/store/useUI.js
+++ b/frontend/src/store/useUI.js
@@ -77,6 +77,10 @@ let workInt = null
let workTick = null
let workDone = null
const MAX_WORK_OVERTIME_SEC = 15 * 60
+const SYNC_BANNER_KEY = 'gym_show_sync_banner'
+const readShowSyncBanner = () => {
+ try { return localStorage.getItem(SYNC_BANNER_KEY) !== '0' } catch { return true }
+}
const stopRestTicking = () => {
if (timerInt) clearInterval(timerInt); timerInt = null
@@ -123,6 +127,7 @@ const runRest = (set, get) => {
export const useUI = create((set, get) => ({
sheets: [], // { id, render:(close)=>JSX, kind:'sheet'|'center', locked }
toastMsg: '',
+ showSyncBanner: readShowSyncBanner(), // device-local display choice; never part of the synced profile
timer: null, // rest countdown between sets — { left, total, endsAt, forIdx, ready?, paused? }
// forIdx: index of the active entry whose set started the rest (undefined when unknown)
// paused: held at `left`; `endsAt` means nothing until resumeRest sets it again
@@ -143,6 +148,13 @@ export const useUI = create((set, get) => ({
closeSheet(id) { set(s => ({ sheets: s.sheets.filter(x => x.id !== id) })) },
closeAll() { set({ sheets: [] }) },
+ setShowSyncBanner(show) {
+ const value = !!show
+ if (get().showSyncBanner === value) return
+ try { localStorage.setItem(SYNC_BANNER_KEY, value ? '1' : '0') } catch { /* keep the in-memory choice */ }
+ set({ showSyncBanner: value })
+ },
+
toast(msg) {
set({ toastMsg: msg })
clearTimeout(toastTm)
diff --git a/frontend/src/store/useUI.test.js b/frontend/src/store/useUI.test.js
index cf9faee01..5912912a1 100644
--- a/frontend/src/store/useUI.test.js
+++ b/frontend/src/store/useUI.test.js
@@ -4,6 +4,21 @@ import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest'
import { useUI } from './useUI.js'
import { useStore } from './useStore.js'
+describe('device-local sync banner preference', () => {
+ afterEach(() => {
+ useUI.getState().setShowSyncBanner(true)
+ localStorage.removeItem('gym_show_sync_banner')
+ })
+
+ it('persists the display choice on this device', () => {
+ useUI.getState().setShowSyncBanner(false)
+ expect(useUI.getState().showSyncBanner).toBe(false)
+ expect(localStorage.getItem('gym_show_sync_banner')).toBe('0')
+ useUI.getState().setShowSyncBanner(true)
+ expect(localStorage.getItem('gym_show_sync_banner')).toBe('1')
+ })
+})
+
// "Off" has to hold at the timer itself, not at the four places that start one — the same
// reason the rest-after-a-set rule is a shared condition rather than four copies.
describe('rest timer set to Off', () => {
diff --git a/frontend/src/views/Settings.jsx b/frontend/src/views/Settings.jsx
index d04cc7246..445551386 100644
--- a/frontend/src/views/Settings.jsx
+++ b/frontend/src/views/Settings.jsx
@@ -49,6 +49,8 @@ export default function Settings() {
const credsChanged = () => { passkeys.load(); setCredsV(v => v + 1) }
const { update, importConflict, importBackup, setUnit, resetEverything: resetAll, setUser, pullState, pushState, resetDemo } = useStore()
const toast = useUI(s => s.toast)
+ const showSyncBanner = useUI(s => s.showSyncBanner)
+ const setShowSyncBanner = useUI(s => s.setShowSyncBanner)
const fileRef = useRef(null)
const importRef = useRef(null)
const wakeOK = wakeLockSupported()
@@ -541,6 +543,14 @@ export default function Settings() {
+ {/* The sync banner is a per-device display choice; the full status remains in Settings. */}
+
+
{/* ---------- data: fill it, bring things over, back it up, wipe it ---------- */}
diff --git a/frontend/src/views/Settings.server-sync.test.jsx b/frontend/src/views/Settings.server-sync.test.jsx
index e63008b13..6c7be48d5 100644
--- a/frontend/src/views/Settings.server-sync.test.jsx
+++ b/frontend/src/views/Settings.server-sync.test.jsx
@@ -12,7 +12,7 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true
it never checked, and which asks before leaving changes the server has not got: try again,
export a backup, or go ahead anyway with the changes kept on the device. */
const mocks = vi.hoisted(() => {
- const state = { S: null, user: null, sync: null, MOBILE: false, unsynced: { owed: false, count: 0 }, kept: [], sheets: [], navs: [] }
+ const state = { S: null, user: null, sync: null, MOBILE: false, showSyncBanner: true, unsynced: { owed: false, count: 0 }, kept: [], sheets: [], navs: [] }
state.toast = vi.fn()
state.confirmSheet = vi.fn()
state.syncNow = vi.fn(async () => state.sync)
@@ -21,7 +21,7 @@ const mocks = vi.hoisted(() => {
state.signOutAll = vi.fn()
state.shareExport = vi.fn(async () => {})
state.snapshot = () => ({
- S: state.S, user: state.user, sync: state.sync, coachLocal: null,
+ S: state.S, user: state.user, sync: state.sync, coachLocal: null, showSyncBanner: state.showSyncBanner,
update: vi.fn(), replaceState: vi.fn(), setUser: vi.fn(), pullState: vi.fn(), pushState: vi.fn(), resetDemo: vi.fn(),
syncNow: state.syncNow, unsyncedChanges: () => state.unsynced, keptChanges: async () => state.kept,
disconnectServer: state.disconnectServer, signOut: state.signOut, signOutAll: state.signOutAll,
@@ -35,7 +35,7 @@ vi.mock('../store/useStore.js', () => {
return { useStore, DEF: { reminder: { time: '17:30' } }, hasData: () => false }
})
vi.mock('../store/useUI.js', () => {
- const snap = () => ({ toast: (...a) => mocks.toast(...a), openSheet: (render, opts) => { mocks.sheets.push({ render, opts }); return {} } })
+ const snap = () => ({ showSyncBanner: mocks.showSyncBanner, toast: (...a) => mocks.toast(...a), setShowSyncBanner: v => { mocks.showSyncBanner = v }, openSheet: (render, opts) => { mocks.sheets.push({ render, opts }); return {} } })
const useUI = selector => selector ? selector(snap()) : snap()
useUI.getState = snap
return { useUI }
@@ -76,6 +76,7 @@ beforeEach(() => {
mocks.S = { unit: 'kg', restSec: 90, restPauseSec: 15, sound: false, effort: 'none', gifSize: 'full', workouts: [], routines: [], exWeights: {} }
mocks.user = USER
mocks.MOBILE = true
+ mocks.showSyncBanner = true
mocks.sync = sync('ok', { lastSynced: Date.now() - 5 * 60000 })
mocks.unsynced = { owed: false, count: 0 }
mocks.kept = []
@@ -174,6 +175,17 @@ describe('Server & sync', () => {
})
})
+describe('the sync banner display preference', () => {
+ it('is available in Settings and can be switched off on this device', () => {
+ const page = mount()
+ const row = rowByTitle(page, 'Show sync banner')
+ expect(row).toBeTruthy()
+ expect(row.querySelector('[role="switch"]').getAttribute('aria-checked')).toBe('true')
+ act(() => row.querySelector('[role="switch"]').click())
+ expect(mocks.showSyncBanner).toBe(false)
+ })
+})
+
describe('leaving the server', () => {
const confirm = () => mocks.confirmSheet.mock.calls.at(-1)[0]