pinokiod 8.0.78 → 8.0.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,135 @@
1
+ const assert = require("node:assert/strict")
2
+ const fs = require("node:fs/promises")
3
+ const path = require("node:path")
4
+ const test = require("node:test")
5
+ const { JSDOM, VirtualConsole } = require("jsdom")
6
+
7
+ const root = path.resolve(__dirname, "..")
8
+ const notifierPath = path.resolve(root, "server/public/tab-idle-notifier.js")
9
+ const popoverPath = path.resolve(root, "server/public/tab-link-popover.js")
10
+ const popoverCssPath = path.resolve(root, "server/public/tab-link-popover.css")
11
+
12
+ const settle = () => new Promise((resolve) => setImmediate(resolve))
13
+
14
+ test("desktop notification settings mount inline and persist both scopes", async () => {
15
+ const script = await fs.readFile(notifierPath, "utf8")
16
+ const virtualConsole = new VirtualConsole()
17
+ const dom = new JSDOM(`<!doctype html><html><head></head><body>
18
+ <div class="appcanvas vertical">
19
+ <aside>
20
+ <div class="menu-container">
21
+ <a class="frame-link" href="/run/test" target="test-shell" data-can-notify="true">
22
+ <span class="tab"><span class="tab-main">Test</span></span>
23
+ </a>
24
+ </div>
25
+ </aside>
26
+ </div>
27
+ <div id="settings"></div>
28
+ </body></html>`, {
29
+ url: "http://127.0.0.1:42000/run/test",
30
+ runScripts: "outside-only",
31
+ pretendToBeVisual: true,
32
+ virtualConsole,
33
+ beforeParse(window) {
34
+ window.fetch = async () => ({
35
+ ok: true,
36
+ async json() {
37
+ return {
38
+ sounds: [{
39
+ url: "/sound/bell.mp3",
40
+ label: "Bell",
41
+ filename: "bell.mp3"
42
+ }]
43
+ }
44
+ }
45
+ })
46
+ window.Audio = class {
47
+ play() {
48
+ return Promise.resolve()
49
+ }
50
+ }
51
+ }
52
+ })
53
+
54
+ try {
55
+ dom.window.eval(script)
56
+ const link = dom.window.document.querySelector(".frame-link")
57
+ const settings = dom.window.document.getElementById("settings")
58
+ let enabledChange = null
59
+ let soundChange = null
60
+
61
+ const mounted = dom.window.PinokioIdleNotifier.mountSettingsForLink(link, settings, {
62
+ onEnabledChange(enabled) {
63
+ enabledChange = enabled
64
+ },
65
+ onSoundChange(choice) {
66
+ soundChange = choice
67
+ }
68
+ })
69
+
70
+ assert.equal(mounted, true)
71
+ assert.equal(settings.textContent.includes("Notifications for this tab"), true)
72
+ assert.equal(settings.textContent.includes("Sound"), true)
73
+
74
+ const toggle = settings.querySelector('[role="switch"]')
75
+ const status = settings.querySelector(".tab-link-notification-toggle-status")
76
+ const select = settings.querySelector("select")
77
+ assert.equal(toggle.getAttribute("aria-checked"), "true")
78
+ assert.equal(status.textContent, "On")
79
+ assert.equal(select.value, "__default__")
80
+
81
+ toggle.click()
82
+ assert.equal(toggle.getAttribute("aria-checked"), "false")
83
+ assert.equal(status.textContent, "Off")
84
+ assert.equal(enabledChange, false)
85
+ assert.deepEqual(JSON.parse(dom.window.localStorage.getItem("pinokio:idle-prefs")), {
86
+ "test-shell": false
87
+ })
88
+
89
+ toggle.click()
90
+ assert.equal(toggle.getAttribute("aria-checked"), "true")
91
+ assert.equal(status.textContent, "On")
92
+ assert.equal(enabledChange, true)
93
+ assert.equal(dom.window.localStorage.getItem("pinokio:idle-prefs"), null)
94
+
95
+ await settle()
96
+ assert.equal(Array.from(select.options).some((option) => option.textContent === "Bell"), true)
97
+ select.value = "/sound/bell.mp3"
98
+ select.dispatchEvent(new dom.window.Event("change", { bubbles: true }))
99
+ assert.equal(soundChange, "/sound/bell.mp3")
100
+ assert.deepEqual(JSON.parse(dom.window.localStorage.getItem("pinokio:idle-sound")), {
101
+ choice: "/sound/bell.mp3"
102
+ })
103
+ } finally {
104
+ dom.window.close()
105
+ }
106
+ })
107
+
108
+ test("tab actions expands notification settings instead of reopening at the sidebar", async () => {
109
+ const [popover, css] = await Promise.all([
110
+ fs.readFile(popoverPath, "utf8"),
111
+ fs.readFile(popoverCssPath, "utf8")
112
+ ])
113
+ const branchStart = popover.indexOf('if (action === "notifications")')
114
+ const branchEnd = popover.indexOf('const url = item.getAttribute("data-url")', branchStart)
115
+ assert.notEqual(branchStart, -1)
116
+ assert.notEqual(branchEnd, -1)
117
+ const branch = popover.slice(branchStart, branchEnd)
118
+
119
+ assert.match(branch, /mountSettingsForLink\(activeLink, panel/)
120
+ assert.match(branch, /tab-link-notification-settings/)
121
+ assert.match(branch, /panel\.setAttribute\("role", "group"\)/)
122
+ assert.doesNotMatch(branch, /hideTabLinkPopover/)
123
+ assert.doesNotMatch(branch, /openMenuForLink/)
124
+ assert.match(css, /\.tab-link-notification-settings\s*\{/)
125
+ assert.match(css, /\.tab-link-notification-switch:focus-visible/)
126
+ })
127
+
128
+ test("legacy notification popover stays inside the viewport", async () => {
129
+ const notifier = await fs.readFile(notifierPath, "utf8")
130
+
131
+ assert.match(notifier, /\.pinokio-notify-popover \{[\s\S]*position: fixed;/)
132
+ assert.match(notifier, /\.pinokio-notify-popover \{[\s\S]*max-height: calc\(100dvh - 24px\);/)
133
+ assert.match(notifier, /const topAbove = rect\.top - menuHeight - menuGap;/)
134
+ assert.match(notifier, /top \+ menuHeight > window\.innerHeight - viewportPadding/)
135
+ })
@@ -1247,6 +1247,7 @@ describe("automatic app checks", () => {
1247
1247
  await fs.promises.writeFile(second, "same-content")
1248
1248
  const vault = await makeVault(home)
1249
1249
  vault.lastScanCache.set("", { candidate_min_bytes: 1 })
1250
+ await vault.registry.setScanSetting(`app:${app}`, 0)
1250
1251
  const hashedPaths = []
1251
1252
  const originalHashFile = vault.hashFile.bind(vault)
1252
1253
  vault.hashFile = async (filePath, options) => {
@@ -29,13 +29,18 @@ const makeOutside = async () => {
29
29
  return directory
30
30
  }
31
31
 
32
- const makeVault = async () => {
32
+ const makeVault = async ({ candidateSize = CANDIDATE_SIZE_OPTIONS[0] } = {}) => {
33
33
  const home = await makeHome()
34
34
  const kernel = { homedir: home, platform: process.platform }
35
35
  const vault = new Vault(kernel)
36
36
  kernel.vault = vault
37
37
  await vault.init()
38
38
  vault.sizeThreshold = CANDIDATE_SIZE_OPTIONS[0]
39
+ if (candidateSize !== null) {
40
+ await vault.perform("set_candidate_size", {
41
+ candidate_size: candidateSize
42
+ })
43
+ }
39
44
  return { home, kernel, vault }
40
45
  }
41
46
 
@@ -175,15 +180,142 @@ describe("Save Space engine", () => {
175
180
  await close(vault)
176
181
  })
177
182
 
178
- test("the published global scan minimum is reused by automatic checks", async () => {
179
- const { vault } = await makeVault()
183
+ test("global and app minimum sizes persist and drive their own scans", async () => {
184
+ const { home, vault } = await makeVault()
185
+ const app = "minimum-size-app"
186
+ const appRoot = path.join(home, "api", app)
187
+ const appScope = `app:${app}`
188
+ const globalMinimum = CANDIDATE_SIZE_OPTIONS[2]
189
+ const appMinimum = CANDIDATE_SIZE_OPTIONS[1]
190
+ await fs.promises.mkdir(appRoot)
191
+ await vault.openWorkspace()
192
+
193
+ assert.equal((await vault.perform("set_candidate_size", {
194
+ candidate_size: globalMinimum
195
+ })).candidate_min_bytes, globalMinimum)
196
+ assert.equal((await vault.perform("set_candidate_size", {
197
+ scope_id: appScope,
198
+ candidate_size: appMinimum
199
+ })).candidate_min_bytes, appMinimum)
200
+ assert.equal((await vault.status()).candidate_min_bytes, globalMinimum)
201
+ assert.equal((await vault.status(appScope)).candidate_min_bytes, appMinimum)
202
+
203
+ const starts = []
204
+ vault.globalScanReady = async () => true
205
+ vault.startScan = (scopeId, threshold) => {
206
+ starts.push({ scope_id: scopeId, threshold })
207
+ return { started: true }
208
+ }
209
+ assert.deepEqual(await vault.perform("scan", {
210
+ candidate_size: CANDIDATE_SIZE_OPTIONS[0]
211
+ }), { started: true })
212
+ assert.deepEqual(await vault.perform("scan", {
213
+ scope_id: appScope,
214
+ candidate_size: CANDIDATE_SIZE_OPTIONS[0]
215
+ }), { started: true })
216
+ assert.deepEqual(starts, [
217
+ { scope_id: null, threshold: globalMinimum },
218
+ { scope_id: appScope, threshold: appMinimum }
219
+ ])
220
+ let folderThreshold = null
221
+ vault.startFolderDiscovery = (folderPath, threshold) => {
222
+ folderThreshold = threshold
223
+ return { started: true }
224
+ }
225
+ assert.deepEqual(await vault.perform("find_folders", {
226
+ path: home,
227
+ candidate_size: CANDIDATE_SIZE_OPTIONS[0]
228
+ }), { started: true })
229
+ assert.equal(folderThreshold, globalMinimum)
230
+ assert.equal((await vault.status()).candidate_min_bytes, globalMinimum)
231
+ assert.equal((await vault.status(appScope)).candidate_min_bytes, appMinimum)
232
+ assert.equal((await vault.automaticScans.candidatePolicy(app)).threshold,
233
+ appMinimum)
234
+
235
+ await close(vault)
236
+ const reopened = new Vault({ homedir: home, platform: process.platform })
237
+ await reopened.init()
238
+ assert.equal((await reopened.status()).candidate_min_bytes, globalMinimum)
239
+ assert.equal((await reopened.status(appScope)).candidate_min_bytes,
240
+ appMinimum)
241
+ await close(reopened)
242
+ })
243
+
244
+ test("reading a default minimum size does not persist it", async () => {
245
+ const { vault } = await makeVault({ candidateSize: null })
246
+
247
+ assert.equal(await vault.registry.scanSetting(), null)
248
+ assert.equal((await vault.status()).candidate_min_bytes, SIZE_THRESHOLD)
249
+ assert.equal(await vault.registry.scanSetting(), null)
250
+
251
+ await close(vault)
252
+ })
253
+
254
+ test("minimum size settings reject coerced values", async () => {
255
+ const { vault } = await makeVault({ candidateSize: null })
256
+
257
+ for (const candidateSize of [
258
+ null,
259
+ "",
260
+ String(CANDIDATE_SIZE_OPTIONS[1])
261
+ ]) {
262
+ assert.match((await vault.perform("set_candidate_size", {
263
+ candidate_size: candidateSize
264
+ })).error, /valid minimum file size/i)
265
+ }
266
+ assert.equal(await vault.registry.scanSetting(), null)
267
+
268
+ await close(vault)
269
+ })
270
+
271
+ test("minimum size settings reject malformed scopes", async () => {
272
+ const { vault } = await makeVault({ candidateSize: null })
273
+
274
+ const result = await vault.perform("set_candidate_size", {
275
+ scope_id: 123,
276
+ candidate_size: CANDIDATE_SIZE_OPTIONS[1]
277
+ })
278
+
279
+ assert.match(result.error, /valid scan scope/i)
280
+ assert.equal(await vault.registry.scanSetting(), null)
281
+
282
+ await close(vault)
283
+ })
284
+
285
+ test("scan history does not become a minimum size setting", async () => {
286
+ const home = await makeHome()
287
+ const vault = new Vault({ homedir: home, platform: process.platform })
288
+ await vault.init()
289
+ await vault.perform("set_candidate_size", {
290
+ candidate_size: CANDIDATE_SIZE_OPTIONS[2]
291
+ })
292
+ assert.equal((await vault.perform("scan")).started, true)
293
+ await waitForEngine(() => !vault.scanPromise &&
294
+ !vault.scanCompletionPromise)
295
+ await close(vault)
296
+
297
+ const database = new Database(path.join(home, "vault", "registry.sqlite3"))
298
+ database.exec("DROP TABLE minimum_size_settings")
299
+ database.close()
300
+
301
+ const reopened = new Vault({ homedir: home, platform: process.platform })
302
+ await reopened.init()
303
+ assert.equal((await reopened.status()).candidate_min_bytes,
304
+ SIZE_THRESHOLD)
305
+ assert.equal(await reopened.registry.scanSetting(), null)
306
+ await close(reopened)
307
+ })
308
+
309
+ test("the saved global minimum is reused by unscoped policy reads", async () => {
310
+ const { vault } = await makeVault({ candidateSize: null })
180
311
  const selected = CANDIDATE_SIZE_OPTIONS[2]
181
312
 
182
313
  assert.equal((await vault.status()).global_candidate_min_bytes,
183
314
  SIZE_THRESHOLD)
184
- assert.deepEqual(await vault.perform("scan", {
315
+ await vault.perform("set_candidate_size", {
185
316
  candidate_size: selected
186
- }), { started: true })
317
+ })
318
+ assert.deepEqual(await vault.perform("scan"), { started: true })
187
319
  await waitForEngine(() => !vault.scanPromise &&
188
320
  !vault.scanCompletionPromise)
189
321
 
@@ -994,6 +1126,9 @@ describe("Save Space engine", () => {
994
1126
  crypto.randomBytes(512))
995
1127
  vault.sizeThreshold = 0
996
1128
  await vault.sweeper.scan()
1129
+ await vault.perform("set_candidate_size", {
1130
+ candidate_size: selectedThreshold
1131
+ })
997
1132
 
998
1133
  const stageDiscoveryFiles = vault.registry.stageFolderDiscoveryFiles
999
1134
  .bind(vault.registry)
@@ -1039,18 +1174,22 @@ describe("Save Space engine", () => {
1039
1174
  await close(vault)
1040
1175
  })
1041
1176
 
1042
- test("Find folders requires an explicit supported threshold", async () => {
1177
+ test("Find folders ignores request thresholds and uses the saved setting", async () => {
1043
1178
  const { vault } = await makeVault()
1044
1179
  const outside = await makeOutside()
1180
+ const thresholds = []
1181
+ vault.startFolderDiscovery = (folderPath, threshold) => {
1182
+ thresholds.push(threshold)
1183
+ return { started: true }
1184
+ }
1045
1185
 
1046
- assert.match((await vault.perform("find_folders", {
1047
- path: outside
1048
- })).error, /valid minimum file size/i)
1049
- assert.match((await vault.perform("find_folders", {
1186
+ assert.deepEqual(await vault.perform("find_folders", {
1050
1187
  path: outside,
1051
1188
  candidate_size: 123
1052
- })).error, /valid minimum file size/i)
1053
- assert.equal(vault.folderDiscoveryPromise, null)
1189
+ }), { started: true })
1190
+ assert.deepEqual(thresholds, [CANDIDATE_SIZE_OPTIONS[0]])
1191
+ assert.equal((await vault.status()).candidate_min_bytes,
1192
+ CANDIDATE_SIZE_OPTIONS[0])
1054
1193
  await close(vault)
1055
1194
  })
1056
1195
 
@@ -414,7 +414,7 @@ const makePage = async (status, options = {}) => {
414
414
  const childManifest = parent
415
415
  ? folderDiscoveryChildren[parent]
416
416
  : null
417
- const response = parent
417
+ const response = await (parent
418
418
  ? typeof childManifest === "function"
419
419
  ? childManifest(childPage)
420
420
  : childManifest || {
@@ -425,7 +425,7 @@ const makePage = async (status, options = {}) => {
425
425
  page_size: 500,
426
426
  pages: 1
427
427
  }
428
- : typeof status === "function" ? status(url) : status
428
+ : typeof status === "function" ? status(url) : status)
429
429
  if (options.deferAutomaticStatus &&
430
430
  url === "/info/vault/automatic-scans") {
431
431
  await deferredAutomaticStatus
@@ -778,7 +778,7 @@ describe("Save Space interface", () => {
778
778
  request.action === "scan"))
779
779
  const scan = requests.find((request) => request.action === "scan")
780
780
  assert.equal(scan.scope_id, null)
781
- assert.equal(scan.candidate_size, 100 * candidateBase ** 2)
781
+ assert.equal("candidate_size" in scan, false)
782
782
  await settle()
783
783
  dom.window.close()
784
784
  })
@@ -866,12 +866,13 @@ describe("Save Space interface", () => {
866
866
  dom.window.close()
867
867
  })
868
868
 
869
- test("the scan minimum follows the latest global scan, not browser storage", async () => {
869
+ test("a scan relies on the saved scope setting, not browser storage", async () => {
870
870
  const candidateBase = process.platform === "win32" ? 1024 : 1000
871
- const published = 50 * candidateBase ** 2
871
+ const saved = 50 * candidateBase ** 2
872
872
  const stored = 10 * candidateBase ** 2
873
873
  const { dom, requests } = await makePage(fixture([], {
874
- global_candidate_min_bytes: published
874
+ candidate_min_bytes: saved,
875
+ global_candidate_min_bytes: 100 * candidateBase ** 2
875
876
  }), {
876
877
  storedCandidateSize: stored
877
878
  })
@@ -882,13 +883,39 @@ describe("Save Space interface", () => {
882
883
  document.getElementById("btn-scan").click()
883
884
  await waitFor(() => requests.some((request) =>
884
885
  request.action === "scan"))
885
- assert.equal(requests.find((request) =>
886
- request.action === "scan").candidate_size, published)
886
+ assert.equal("candidate_size" in requests.find((request) =>
887
+ request.action === "scan"), false)
887
888
 
888
889
  await settle()
889
890
  dom.window.close()
890
891
  })
891
892
 
893
+ test("an app displays and immediately saves its own minimum size", async () => {
894
+ const candidateBase = process.platform === "win32" ? 1024 : 1000
895
+ const appMinimum = candidateBase ** 2
896
+ const globalMinimum = 10 * candidateBase ** 2
897
+ const selected = 50 * candidateBase ** 2
898
+ const { dom, requests } = await makePage(fixture([], {
899
+ candidate_min_bytes: appMinimum,
900
+ global_candidate_min_bytes: globalMinimum
901
+ }), { appMode: true })
902
+ const document = dom.window.document
903
+ const selector = document.getElementById("vault-candidate-size")
904
+
905
+ assert.equal(selector.value, String(appMinimum))
906
+ selector.value = String(selected)
907
+ selector.dispatchEvent(new dom.window.Event("change", { bubbles: true }))
908
+ await waitFor(() => requests.some((request) =>
909
+ request.action === "set_candidate_size"))
910
+ const request = requests.find((entry) =>
911
+ entry.action === "set_candidate_size")
912
+ assert.equal(request.scope_id, "app:app")
913
+ assert.equal(request.candidate_size, selected)
914
+ assert.equal(selector.value, String(selected))
915
+
916
+ dom.window.close()
917
+ })
918
+
892
919
  test("a threshold chosen before initial status remains selected", async () => {
893
920
  const candidateBase = process.platform === "win32" ? 1024 : 1000
894
921
  const published = 50 * candidateBase ** 2
@@ -914,13 +941,87 @@ describe("Save Space interface", () => {
914
941
  document.getElementById("btn-scan").click()
915
942
  await waitFor(() => requests.some((request) =>
916
943
  request.action === "scan"))
917
- assert.equal(requests.find((request) =>
918
- request.action === "scan").candidate_size, selected)
944
+ const savedIndex = requests.findIndex((request) =>
945
+ request.action === "set_candidate_size")
946
+ const scanIndex = requests.findIndex((request) =>
947
+ request.action === "scan")
948
+ assert.ok(savedIndex >= 0 && savedIndex < scanIndex)
949
+ assert.equal("candidate_size" in requests[scanIndex], false)
919
950
 
920
951
  await settle()
921
952
  dom.window.close()
922
953
  })
923
954
 
955
+ test("a rejected minimum size save blocks a pending scan and restores the saved size", async () => {
956
+ const candidateBase = process.platform === "win32" ? 1024 : 1000
957
+ const saved = 50 * candidateBase ** 2
958
+ const selected = 10 * candidateBase ** 2
959
+ const { dom, requests } = await makePage(fixture([], {
960
+ candidate_min_bytes: saved,
961
+ global_candidate_min_bytes: saved
962
+ }), {
963
+ actionResults: {
964
+ set_candidate_size: { error: "Could not save minimum file size." }
965
+ }
966
+ })
967
+ const document = dom.window.document
968
+
969
+ document.querySelector(`[data-candidate-size="${selected}"]`).click()
970
+ document.getElementById("btn-scan").click()
971
+
972
+ await waitFor(() => /50 MB\+/.test(document.getElementById(
973
+ "vault-scan-size-label").textContent) &&
974
+ /could not save minimum file size/i.test(
975
+ document.getElementById("vault-feedback").textContent))
976
+ assert.equal(requests.some((request) => request.action === "scan"), false)
977
+
978
+ dom.window.close()
979
+ })
980
+
981
+ test("a stale minimum size failure does not replace a newer successful selection", async () => {
982
+ const candidateBase = process.platform === "win32" ? 1024 : 1000
983
+ const first = 10 * candidateBase ** 2
984
+ const second = 50 * candidateBase ** 2
985
+ const currentStatus = fixture([], {
986
+ candidate_min_bytes: 100 * candidateBase ** 2,
987
+ global_candidate_min_bytes: 100 * candidateBase ** 2
988
+ })
989
+ let statusCount = 0
990
+ let releaseRecovery
991
+ const recovery = new Promise((resolve) => { releaseRecovery = resolve })
992
+ let saveCount = 0
993
+ const actionResults = {}
994
+ Object.defineProperty(actionResults, "set_candidate_size", {
995
+ get() {
996
+ saveCount += 1
997
+ return saveCount === 1
998
+ ? { error: "Could not save minimum file size." }
999
+ : {}
1000
+ }
1001
+ })
1002
+ const { dom, requests } = await makePage(async () => {
1003
+ statusCount += 1
1004
+ if (statusCount === 2) await recovery
1005
+ return currentStatus
1006
+ }, { actionResults })
1007
+ const document = dom.window.document
1008
+
1009
+ document.querySelector(`[data-candidate-size="${first}"]`).click()
1010
+ await waitFor(() => statusCount === 2)
1011
+ document.querySelector(`[data-candidate-size="${second}"]`).click()
1012
+ releaseRecovery()
1013
+
1014
+ await waitFor(() => requests.filter((request) =>
1015
+ request.action === "set_candidate_size").length === 2)
1016
+ await settle()
1017
+ assert.match(document.getElementById("vault-scan-size-label").textContent,
1018
+ /50 MB\+/)
1019
+ assert.doesNotMatch(document.getElementById("vault-feedback").textContent,
1020
+ /could not save minimum file size/i)
1021
+
1022
+ dom.window.close()
1023
+ })
1024
+
924
1025
  test("a runtime write denial is reported as cannot deduplicate", async () => {
925
1026
  const duplicate = item({
926
1027
  status: "duplicate",
@@ -1090,15 +1191,49 @@ describe("Save Space interface", () => {
1090
1191
  request.action === "find_folders"))
1091
1192
  assert.equal(requests.find((entry) =>
1092
1193
  entry.action === "find_folders").path, "/Users/test")
1093
- assert.equal(requests.find((entry) =>
1094
- entry.action === "find_folders").candidate_size,
1095
- selectedThreshold)
1194
+ const savedIndex = requests.findIndex((request) =>
1195
+ request.action === "set_candidate_size")
1196
+ const findIndex = requests.findIndex((request) =>
1197
+ request.action === "find_folders")
1198
+ assert.ok(savedIndex >= 0 && savedIndex < findIndex)
1199
+ assert.equal("candidate_size" in requests[findIndex], false)
1096
1200
  assert.equal(pickerRequests.length, 0)
1097
1201
  await waitFor(() => document.getElementById(
1098
1202
  "vault-find-overlay").hidden)
1099
1203
  dom.window.close()
1100
1204
  })
1101
1205
 
1206
+ test("a rejected minimum size save blocks pending folder discovery", async () => {
1207
+ const candidateBase = process.platform === "win32" ? 1024 : 1000
1208
+ const saved = 100 * candidateBase ** 2
1209
+ const selected = 50 * candidateBase ** 2
1210
+ const { dom, requests } = await makePage(fixture([item()], {
1211
+ candidate_min_bytes: saved,
1212
+ global_candidate_min_bytes: saved
1213
+ }), {
1214
+ actionResults: {
1215
+ set_candidate_size: { error: "Could not save minimum file size." }
1216
+ }
1217
+ })
1218
+ const document = dom.window.document
1219
+
1220
+ document.getElementById("btn-find-folders").click()
1221
+ await waitFor(() => document.querySelector("[data-find-home-folder]"))
1222
+ const selector = document.getElementById("vault-find-candidate-size")
1223
+ selector.value = String(selected)
1224
+ selector.dispatchEvent(new dom.window.Event("change", { bubbles: true }))
1225
+ document.querySelector("[data-find-home-folder]").click()
1226
+
1227
+ await waitFor(() => document.getElementById(
1228
+ "vault-find-candidate-size").value === String(saved) &&
1229
+ /could not save minimum file size/i.test(
1230
+ document.getElementById("vault-feedback").textContent))
1231
+ assert.equal(requests.some((request) =>
1232
+ request.action === "find_folders"), false)
1233
+
1234
+ dom.window.close()
1235
+ })
1236
+
1102
1237
  test("Find folders explains when a lower threshold needs a global scan", async () => {
1103
1238
  const message = "Run a global scan with this minimum file size before searching folders."
1104
1239
  const { dom, requests } = await makePage(fixture([item()]), {
@@ -1121,9 +1256,8 @@ describe("Save Space interface", () => {
1121
1256
  assert.equal(document.querySelector(
1122
1257
  ".vault-find-partial[role='alert']").textContent.trim(), message)
1123
1258
  assert.ok(document.querySelector("[data-find-home-folder]"))
1124
- assert.equal(requests.find((request) =>
1125
- request.action === "find_folders").candidate_size,
1126
- 50 * candidateBase ** 2)
1259
+ assert.equal("candidate_size" in requests.find((request) =>
1260
+ request.action === "find_folders"), false)
1127
1261
  dom.window.close()
1128
1262
  })
1129
1263