Skip to main content

max / balanced_breakfast

5.6 KB · 139 lines History Blame Raw
1 const { describe, test } = require('node:test');
2 const assert = require('node:assert');
3 const { BB } = require('./setup');
4
5 // settings-sync reads BB.api.sync and BB.ui at load time, so both are stood up
6 // before the module is required.
7 let syncStatusResult = {};
8 let syncToasts = [];
9 BB.ui.showToast = (msg, type) => { syncToasts.push({ msg, type }); };
10 BB.ui.openModal = () => {};
11
12 BB.api.sync = {
13 status: async () => syncStatusResult,
14 startAuth: async () => ({ authUrl: 'https://test.com', state: 's', codeVerifier: 'cv', port: 8080 }),
15 completeAuth: async () => {},
16 setupEncryptionNew: async () => {},
17 setupEncryptionExisting: async () => {},
18 now: async () => ({ pushed: 1, pulled: 2 }),
19 updateSettings: async () => {},
20 disconnect: async () => {},
21 };
22
23 require('../settings-sync');
24
25 // Clear the modal body between renders: openSettings appends into it.
26 function freshModalBody() {
27 const body = document.getElementById('modal-body');
28 body.innerHTML = '';
29 body.children = [];
30 return body;
31 }
32
33 describe('BB.sync.openSettings: renderState routing', () => {
34 test('not configured shows Connect button', async () => {
35 syncStatusResult = { configured: false, authenticated: false };
36 const body = freshModalBody();
37 await BB.sync.openSettings();
38 // renderConnect adds a div with class sync-connect
39 assert.ok(
40 body.children.some(c => c.className === 'sync-connect'),
41 'Should render connect state'
42 );
43 });
44
45 test('configured but not authenticated shows Connect button', async () => {
46 syncStatusResult = { configured: true, authenticated: false };
47 const body = freshModalBody();
48 await BB.sync.openSettings();
49 assert.ok(
50 body.children.some(c => c.className === 'sync-connect'),
51 'Should render connect state when not authenticated'
52 );
53 });
54
55 test('authenticated but no encryption and no server key shows Set Password', async () => {
56 syncStatusResult = { configured: true, authenticated: true, encryptionReady: false, hasServerKey: false };
57 const body = freshModalBody();
58 await BB.sync.openSettings();
59 // renderEncryption adds a div, check for Set Password button text
60 const hasForm = body.children.some(c => {
61 // The child div contains a form with submit button
62 return c.children && c.children.some(f =>
63 f.children && f.children.some(a =>
64 a.children && a.children.some(b => b._text === 'Set Password')
65 )
66 );
67 });
68 assert.ok(hasForm, 'Should show Set Password button');
69 });
70
71 test('authenticated but no encryption with server key shows Unlock', async () => {
72 syncStatusResult = { configured: true, authenticated: true, encryptionReady: false, hasServerKey: true };
73 const body = freshModalBody();
74 await BB.sync.openSettings();
75 const hasUnlock = body.children.some(c => {
76 return c.children && c.children.some(f =>
77 f.children && f.children.some(a =>
78 a.children && a.children.some(b => b._text === 'Unlock')
79 )
80 );
81 });
82 assert.ok(hasUnlock, 'Should show Unlock button');
83 });
84
85 test('fully ready shows sync-ready with Sync Now button', async () => {
86 syncStatusResult = {
87 configured: true, authenticated: true, encryptionReady: true,
88 lastSyncAt: null, pendingChanges: 0, autoSyncEnabled: false,
89 syncIntervalMinutes: 15,
90 };
91 const body = freshModalBody();
92 await BB.sync.openSettings();
93 assert.ok(
94 body.children.some(c => c.className === 'sync-ready'),
95 'Should render ready state'
96 );
97 });
98
99 test('ready state shows Never for null lastSyncAt', async () => {
100 syncStatusResult = {
101 configured: true, authenticated: true, encryptionReady: true,
102 lastSyncAt: null, pendingChanges: 0, autoSyncEnabled: true,
103 syncIntervalMinutes: 15,
104 };
105 const body = freshModalBody();
106 await BB.sync.openSettings();
107 const readyDiv = body.children.find(c => c.className === 'sync-ready');
108 assert.ok(readyDiv, 'Should have sync-ready div');
109 // The info div should contain "Never"
110 const infoDiv = readyDiv.children.find(c => c.className === 'sync-info');
111 assert.ok(infoDiv && infoDiv.innerHTML.includes('Never'), 'Should show Never for null lastSyncAt');
112 });
113
114 test('openSettings shows error toast on API failure', async () => {
115 syncToasts = [];
116 BB.api.sync.status = async () => { throw new Error('network error'); };
117 await BB.sync.openSettings();
118 assert.ok(syncToasts.some(t => t.type === 'error'), 'Should show error toast');
119 // Restore status mock
120 BB.api.sync.status = async () => syncStatusResult;
121 });
122
123 test('ready state has disconnect button', async () => {
124 syncStatusResult = {
125 configured: true, authenticated: true, encryptionReady: true,
126 lastSyncAt: '2026-01-01T00:00:00Z', pendingChanges: 3,
127 autoSyncEnabled: true, syncIntervalMinutes: 30,
128 };
129 const body = freshModalBody();
130 await BB.sync.openSettings();
131 const readyDiv = body.children.find(c => c.className === 'sync-ready');
132 assert.ok(readyDiv, 'Should have sync-ready div');
133 const hasDisconnect = readyDiv.children.some(c =>
134 c.className === 'button sync-disconnect' && c._text === 'Disconnect'
135 );
136 assert.ok(hasDisconnect, 'Should have Disconnect button');
137 });
138 });
139