pinokiod 8.0.78 → 8.0.79
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.
- package/kernel/api/notify/index.js +2 -1
- package/kernel/vault/automatic_scans.js +6 -7
- package/kernel/vault/index.js +59 -16
- package/kernel/vault/registry.js +2 -0
- package/kernel/vault/registry_core.js +39 -0
- package/package.json +1 -1
- package/server/public/tab-idle-notifier.js +151 -11
- package/server/public/tab-link-popover.css +128 -0
- package/server/public/tab-link-popover.js +36 -4
- package/server/public/vault.js +69 -14
- package/test/tab-notification-settings.test.js +135 -0
- package/test/vault-automatic-scans.test.js +1 -0
- package/test/vault-engine.test.js +151 -12
- package/test/vault-ui.test.js +150 -16
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const fs = require("fs")
|
|
2
2
|
const path = require("path")
|
|
3
3
|
const {
|
|
4
|
-
SIZE_THRESHOLD,
|
|
5
4
|
isCandidateFileSize,
|
|
6
5
|
SHA256_RE,
|
|
7
6
|
ENTRY_BATCH_SIZE
|
|
@@ -276,7 +275,7 @@ class AutomaticScans {
|
|
|
276
275
|
if (!collected.size) this.discardChangedPaths(app)
|
|
277
276
|
}
|
|
278
277
|
|
|
279
|
-
async candidatePolicy() {
|
|
278
|
+
async candidatePolicy(app = null) {
|
|
280
279
|
const cached = this.vault.lastScanCache &&
|
|
281
280
|
typeof this.vault.lastScanCache.get === "function"
|
|
282
281
|
? this.vault.lastScanCache.get("")
|
|
@@ -284,13 +283,13 @@ class AutomaticScans {
|
|
|
284
283
|
const scan = cached || (this.vault.registry
|
|
285
284
|
? await this.vault.registry.scanFor()
|
|
286
285
|
: null)
|
|
287
|
-
const threshold =
|
|
286
|
+
const threshold = app
|
|
287
|
+
? await this.vault.candidateSizeForApp(app)
|
|
288
|
+
: await this.vault.candidateSizeSetting(null)
|
|
288
289
|
const hasLinkableDevices = !!(
|
|
289
290
|
scan && Array.isArray(scan.linkable_devices))
|
|
290
291
|
return {
|
|
291
|
-
threshold
|
|
292
|
-
? threshold
|
|
293
|
-
: SIZE_THRESHOLD,
|
|
292
|
+
threshold,
|
|
294
293
|
linkability_known: hasLinkableDevices,
|
|
295
294
|
linkable_devices: new Set(hasLinkableDevices
|
|
296
295
|
? scan.linkable_devices
|
|
@@ -1039,7 +1038,7 @@ class AutomaticScans {
|
|
|
1039
1038
|
unstable_hashes: 0
|
|
1040
1039
|
}
|
|
1041
1040
|
markStage("policy-read")
|
|
1042
|
-
const policy = await this.candidatePolicy()
|
|
1041
|
+
const policy = await this.candidatePolicy(app)
|
|
1043
1042
|
const threshold = await this.candidateThreshold(policy)
|
|
1044
1043
|
const linkableDevices = policy.linkable_devices
|
|
1045
1044
|
const paths = [...new Set(active.paths || [])].filter((filePath) =>
|
package/kernel/vault/index.js
CHANGED
|
@@ -1899,6 +1899,25 @@ class Vault {
|
|
|
1899
1899
|
}
|
|
1900
1900
|
return this.runMutation(() =>
|
|
1901
1901
|
this.removeExternalSource(payload.source_id))
|
|
1902
|
+
case "set_candidate_size": {
|
|
1903
|
+
if (payload.scope_id != null &&
|
|
1904
|
+
typeof payload.scope_id !== "string") {
|
|
1905
|
+
return { error: "Choose a valid scan scope." }
|
|
1906
|
+
}
|
|
1907
|
+
const scopeId = typeof payload.scope_id === "string" && payload.scope_id
|
|
1908
|
+
? payload.scope_id
|
|
1909
|
+
: null
|
|
1910
|
+
if (scopeId) {
|
|
1911
|
+
const source = this.scanSource(scopeId)
|
|
1912
|
+
if (!source || source.kind !== "app") {
|
|
1913
|
+
return { error: "That app is no longer available." }
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return this.setCandidateSizeSetting(
|
|
1917
|
+
scopeId,
|
|
1918
|
+
payload.candidate_size
|
|
1919
|
+
)
|
|
1920
|
+
}
|
|
1902
1921
|
case "scan": {
|
|
1903
1922
|
const scanSource = payload.scope_id
|
|
1904
1923
|
? this.scanSource(payload.scope_id)
|
|
@@ -1913,22 +1932,21 @@ class Vault {
|
|
|
1913
1932
|
code: "global_scan_required"
|
|
1914
1933
|
}
|
|
1915
1934
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
1921
|
-
threshold = payload.candidate_size
|
|
1922
|
-
}
|
|
1935
|
+
const settingScopeId = scanSource && scanSource.kind === "app"
|
|
1936
|
+
? scanSource.id
|
|
1937
|
+
: null
|
|
1938
|
+
const threshold = await this.candidateSizeSetting(settingScopeId)
|
|
1923
1939
|
return this.startScan(payload.scope_id || null, threshold)
|
|
1924
1940
|
}
|
|
1925
1941
|
case "cancel_scan":
|
|
1926
1942
|
return this.cancelScan()
|
|
1927
|
-
case "find_folders":
|
|
1943
|
+
case "find_folders": {
|
|
1944
|
+
const threshold = await this.candidateSizeSetting(null)
|
|
1928
1945
|
return this.startFolderDiscovery(
|
|
1929
1946
|
payload.path,
|
|
1930
|
-
|
|
1947
|
+
threshold
|
|
1931
1948
|
)
|
|
1949
|
+
}
|
|
1932
1950
|
case "cancel_find_folders":
|
|
1933
1951
|
return this.cancelFolderDiscovery()
|
|
1934
1952
|
case "clear_find_folders":
|
|
@@ -3115,6 +3133,33 @@ class Vault {
|
|
|
3115
3133
|
return scoped
|
|
3116
3134
|
}
|
|
3117
3135
|
|
|
3136
|
+
async candidateSizeSetting(scopeId = null) {
|
|
3137
|
+
const setting = await this.registry.scanSetting(scopeId)
|
|
3138
|
+
const minimum = setting && Number(setting.candidate_min_bytes)
|
|
3139
|
+
if (CANDIDATE_SIZE_OPTIONS.includes(minimum)) return minimum
|
|
3140
|
+
return SIZE_THRESHOLD
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
async setCandidateSizeSetting(scopeId = null, candidateMinBytes) {
|
|
3144
|
+
if (!Number.isSafeInteger(candidateMinBytes) ||
|
|
3145
|
+
!CANDIDATE_SIZE_OPTIONS.includes(candidateMinBytes)) {
|
|
3146
|
+
return { error: "Choose a valid minimum file size." }
|
|
3147
|
+
}
|
|
3148
|
+
const setting = await this.registry.setScanSetting(
|
|
3149
|
+
scopeId,
|
|
3150
|
+
candidateMinBytes
|
|
3151
|
+
)
|
|
3152
|
+
return {
|
|
3153
|
+
scope_id: scopeId || null,
|
|
3154
|
+
candidate_min_bytes: setting.candidate_min_bytes,
|
|
3155
|
+
updated_at: setting.updated_at
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
candidateSizeForApp(app) {
|
|
3160
|
+
return this.candidateSizeSetting(sourceId("app", app))
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3118
3163
|
sourceCountMaps(summaryRows) {
|
|
3119
3164
|
const counts = {
|
|
3120
3165
|
all: {},
|
|
@@ -3391,13 +3436,10 @@ class Vault {
|
|
|
3391
3436
|
const lastScan = await this.scanForScope(scopeId)
|
|
3392
3437
|
const globalScan = scopeId ? await this.scanForScope(null) : lastScan
|
|
3393
3438
|
const globalScanReady = this.globalScanIsReady(globalScan)
|
|
3394
|
-
const
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
publishedCandidateMinimum) && publishedCandidateMinimum >= 0
|
|
3399
|
-
? publishedCandidateMinimum
|
|
3400
|
-
: SIZE_THRESHOLD
|
|
3439
|
+
const candidateMinimum = await this.candidateSizeSetting(scopeId)
|
|
3440
|
+
const globalCandidateMinimum = scopeId
|
|
3441
|
+
? await this.candidateSizeSetting(null)
|
|
3442
|
+
: candidateMinimum
|
|
3401
3443
|
const before = lastScan && Number.isFinite(lastScan.bytes_total)
|
|
3402
3444
|
? lastScan.bytes_total
|
|
3403
3445
|
: 0
|
|
@@ -3435,6 +3477,7 @@ class Vault {
|
|
|
3435
3477
|
const result = {
|
|
3436
3478
|
enabled: true,
|
|
3437
3479
|
global_scan_ready: globalScanReady,
|
|
3480
|
+
candidate_min_bytes: candidateMinimum,
|
|
3438
3481
|
global_candidate_min_bytes: globalCandidateMinimum,
|
|
3439
3482
|
mode: this.mode,
|
|
3440
3483
|
scan: this.scanStatus(),
|
package/kernel/vault/registry.js
CHANGED
|
@@ -306,6 +306,13 @@ class RegistryCore {
|
|
|
306
306
|
FROM automatic_app_scans
|
|
307
307
|
WHERE state = 'paused';
|
|
308
308
|
|
|
309
|
+
CREATE TABLE IF NOT EXISTS minimum_size_settings (
|
|
310
|
+
scope_id TEXT PRIMARY KEY,
|
|
311
|
+
candidate_min_bytes INTEGER NOT NULL CHECK (
|
|
312
|
+
candidate_min_bytes >= 0
|
|
313
|
+
),
|
|
314
|
+
updated_at INTEGER NOT NULL
|
|
315
|
+
);
|
|
309
316
|
`)
|
|
310
317
|
const automaticScanColumns = new Set(this.database.prepare(
|
|
311
318
|
"PRAGMA table_info(automatic_app_scans)"
|
|
@@ -2670,6 +2677,38 @@ class RegistryCore {
|
|
|
2670
2677
|
})
|
|
2671
2678
|
}
|
|
2672
2679
|
|
|
2680
|
+
scanSetting(scopeId = null) {
|
|
2681
|
+
const row = this.database.prepare(`
|
|
2682
|
+
SELECT candidate_min_bytes, updated_at
|
|
2683
|
+
FROM minimum_size_settings
|
|
2684
|
+
WHERE scope_id = ?
|
|
2685
|
+
`).get(scopeId || "")
|
|
2686
|
+
return row ? {
|
|
2687
|
+
candidate_min_bytes: Number(row.candidate_min_bytes),
|
|
2688
|
+
updated_at: Number(row.updated_at) || 0
|
|
2689
|
+
} : null
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
setScanSetting(scopeId = null, candidateMinBytes) {
|
|
2693
|
+
const minimum = candidateMinBytes
|
|
2694
|
+
if (!Number.isSafeInteger(minimum) || minimum < 0) {
|
|
2695
|
+
throw new Error("Invalid minimum file size setting.")
|
|
2696
|
+
}
|
|
2697
|
+
const updatedAt = Date.now()
|
|
2698
|
+
this.database.prepare(`
|
|
2699
|
+
INSERT INTO minimum_size_settings(
|
|
2700
|
+
scope_id, candidate_min_bytes, updated_at
|
|
2701
|
+
) VALUES (?, ?, ?)
|
|
2702
|
+
ON CONFLICT(scope_id) DO UPDATE SET
|
|
2703
|
+
candidate_min_bytes = excluded.candidate_min_bytes,
|
|
2704
|
+
updated_at = excluded.updated_at
|
|
2705
|
+
`).run(scopeId || "", minimum, updatedAt)
|
|
2706
|
+
return {
|
|
2707
|
+
candidate_min_bytes: minimum,
|
|
2708
|
+
updated_at: updatedAt
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2673
2712
|
automaticAppScanStates() {
|
|
2674
2713
|
return this.database.prepare(`
|
|
2675
2714
|
SELECT app, state, signature, updated_at
|
package/package.json
CHANGED
|
@@ -276,17 +276,24 @@
|
|
|
276
276
|
return;
|
|
277
277
|
}
|
|
278
278
|
const rect = anchor.getBoundingClientRect();
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
const top = rect.bottom + scrollY + 6;
|
|
282
|
-
let left = rect.left + scrollX;
|
|
279
|
+
const viewportPadding = 12;
|
|
280
|
+
const menuGap = 6;
|
|
283
281
|
const menuWidth = menu.offsetWidth || 0;
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
282
|
+
const menuHeight = menu.offsetHeight || 0;
|
|
283
|
+
let left = rect.left;
|
|
284
|
+
let top = rect.bottom + menuGap;
|
|
285
|
+
if (left + menuWidth > window.innerWidth - viewportPadding) {
|
|
286
|
+
left = Math.max(viewportPadding, window.innerWidth - menuWidth - viewportPadding);
|
|
287
|
+
}
|
|
288
|
+
if (left < viewportPadding) {
|
|
289
|
+
left = viewportPadding;
|
|
290
|
+
}
|
|
291
|
+
const topAbove = rect.top - menuHeight - menuGap;
|
|
292
|
+
if (top + menuHeight > window.innerHeight - viewportPadding && topAbove >= viewportPadding) {
|
|
293
|
+
top = topAbove;
|
|
294
|
+
} else {
|
|
295
|
+
top = Math.min(top, Math.max(viewportPadding, window.innerHeight - menuHeight - viewportPadding));
|
|
296
|
+
top = Math.max(viewportPadding, top);
|
|
290
297
|
}
|
|
291
298
|
menu.style.top = `${Math.round(top)}px`;
|
|
292
299
|
menu.style.left = `${Math.round(left)}px`;
|
|
@@ -857,10 +864,13 @@
|
|
|
857
864
|
color: var(--pinokio-focus-color, #4c9afe);
|
|
858
865
|
}
|
|
859
866
|
.pinokio-notify-popover {
|
|
860
|
-
position:
|
|
867
|
+
position: fixed;
|
|
861
868
|
z-index: 2147482000;
|
|
862
869
|
min-width: 220px;
|
|
863
870
|
max-width: 280px;
|
|
871
|
+
max-height: calc(100dvh - 24px);
|
|
872
|
+
overflow-y: auto;
|
|
873
|
+
overscroll-behavior: contain;
|
|
864
874
|
color: #f8fafc;
|
|
865
875
|
background: rgba(15, 23, 42, 0.97);
|
|
866
876
|
border-radius: 10px;
|
|
@@ -980,6 +990,133 @@ const syncToggleAppearance = (toggle, enabled) => {
|
|
|
980
990
|
});
|
|
981
991
|
};
|
|
982
992
|
|
|
993
|
+
const updateNotificationPreferenceForFrame = (frameName, enabled) => {
|
|
994
|
+
const state = getOrCreateState(frameName);
|
|
995
|
+
if (!state) {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
const next = Boolean(enabled);
|
|
999
|
+
state.notifyEnabled = next;
|
|
1000
|
+
setPreference(frameName, next);
|
|
1001
|
+
syncInlineToggleStateForFrame(frameName);
|
|
1002
|
+
return next;
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
const populateInlineSoundSelect = (select, options) => {
|
|
1006
|
+
if (!(select instanceof HTMLSelectElement)) {
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const effectiveOptions = Array.isArray(options) && options.length > 0
|
|
1010
|
+
? options
|
|
1011
|
+
: baseSoundOptions();
|
|
1012
|
+
const selectedChoice = globalSoundPreference?.choice || SOUND_DEFAULT_CHOICE;
|
|
1013
|
+
select.replaceChildren();
|
|
1014
|
+
effectiveOptions.forEach((option) => {
|
|
1015
|
+
if (!option || !option.value || !option.label) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const node = document.createElement('option');
|
|
1019
|
+
node.value = option.value;
|
|
1020
|
+
node.textContent = option.label;
|
|
1021
|
+
node.selected = option.value === selectedChoice;
|
|
1022
|
+
select.appendChild(node);
|
|
1023
|
+
});
|
|
1024
|
+
if (Array.from(select.options).some((option) => option.value === selectedChoice)) {
|
|
1025
|
+
select.value = selectedChoice;
|
|
1026
|
+
} else {
|
|
1027
|
+
select.value = SOUND_DEFAULT_CHOICE;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const mountNotificationSettingsForLink = (link, container, callbacks = {}) => {
|
|
1032
|
+
if (!(container instanceof HTMLElement)) {
|
|
1033
|
+
return false;
|
|
1034
|
+
}
|
|
1035
|
+
const context = getNotificationMenuStateForLink(link);
|
|
1036
|
+
if (!context.available || !context.frameName) {
|
|
1037
|
+
return false;
|
|
1038
|
+
}
|
|
1039
|
+
const state = getOrCreateState(context.frameName);
|
|
1040
|
+
if (!state) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
container.replaceChildren();
|
|
1045
|
+
|
|
1046
|
+
const toggleRow = document.createElement('div');
|
|
1047
|
+
toggleRow.className = 'tab-link-notification-setting-row';
|
|
1048
|
+
const toggleLabel = document.createElement('span');
|
|
1049
|
+
toggleLabel.className = 'tab-link-notification-setting-label';
|
|
1050
|
+
toggleLabel.textContent = 'Notifications for this tab';
|
|
1051
|
+
const toggleControl = document.createElement('div');
|
|
1052
|
+
toggleControl.className = 'tab-link-notification-toggle-control';
|
|
1053
|
+
const toggleStatus = document.createElement('span');
|
|
1054
|
+
toggleStatus.className = 'tab-link-notification-toggle-status';
|
|
1055
|
+
const toggle = document.createElement('button');
|
|
1056
|
+
toggle.type = 'button';
|
|
1057
|
+
toggle.className = 'tab-link-notification-switch';
|
|
1058
|
+
toggle.setAttribute('role', 'switch');
|
|
1059
|
+
toggle.setAttribute('aria-label', 'Notifications for this tab');
|
|
1060
|
+
const toggleThumb = document.createElement('span');
|
|
1061
|
+
toggleThumb.className = 'tab-link-notification-switch-thumb';
|
|
1062
|
+
toggle.setAttribute('aria-describedby', `${container.id || 'tab-link-notification-settings'}-status`);
|
|
1063
|
+
toggleStatus.id = `${container.id || 'tab-link-notification-settings'}-status`;
|
|
1064
|
+
toggle.appendChild(toggleThumb);
|
|
1065
|
+
toggleControl.append(toggleStatus, toggle);
|
|
1066
|
+
toggleRow.append(toggleLabel, toggleControl);
|
|
1067
|
+
|
|
1068
|
+
const soundRow = document.createElement('label');
|
|
1069
|
+
soundRow.className = 'tab-link-notification-setting-row';
|
|
1070
|
+
const soundLabel = document.createElement('span');
|
|
1071
|
+
soundLabel.className = 'tab-link-notification-setting-label';
|
|
1072
|
+
soundLabel.textContent = 'Sound';
|
|
1073
|
+
const soundSelect = document.createElement('select');
|
|
1074
|
+
soundSelect.className = 'tab-link-notification-sound-select';
|
|
1075
|
+
soundSelect.setAttribute('aria-label', 'Notification sound');
|
|
1076
|
+
soundRow.append(soundLabel, soundSelect);
|
|
1077
|
+
|
|
1078
|
+
const syncToggle = (enabled) => {
|
|
1079
|
+
toggle.setAttribute('aria-checked', enabled ? 'true' : 'false');
|
|
1080
|
+
toggle.dataset.enabled = enabled ? 'true' : 'false';
|
|
1081
|
+
toggleStatus.textContent = enabled ? 'On' : 'Off';
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
syncToggle(Boolean(state.notifyEnabled));
|
|
1085
|
+
populateInlineSoundSelect(soundSelect, soundOptionsCache || baseSoundOptions());
|
|
1086
|
+
container.append(toggleRow, soundRow);
|
|
1087
|
+
|
|
1088
|
+
toggle.addEventListener('click', () => {
|
|
1089
|
+
const current = getOrCreateState(context.frameName);
|
|
1090
|
+
if (!current) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
const enabled = updateNotificationPreferenceForFrame(context.frameName, !current.notifyEnabled);
|
|
1094
|
+
if (enabled === null) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
syncToggle(enabled);
|
|
1098
|
+
if (typeof callbacks.onEnabledChange === 'function') {
|
|
1099
|
+
callbacks.onEnabledChange(enabled);
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
soundSelect.addEventListener('change', () => {
|
|
1104
|
+
applySoundSelection(soundSelect.value);
|
|
1105
|
+
if (typeof callbacks.onSoundChange === 'function') {
|
|
1106
|
+
callbacks.onSoundChange(globalSoundPreference.choice);
|
|
1107
|
+
}
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
loadSoundOptions().then((options) => {
|
|
1111
|
+
if (!container.isConnected || !container.contains(soundSelect)) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
populateInlineSoundSelect(soundSelect, options);
|
|
1115
|
+
}).catch(() => {});
|
|
1116
|
+
|
|
1117
|
+
return true;
|
|
1118
|
+
};
|
|
1119
|
+
|
|
983
1120
|
const getNotificationMenuStateForLink = (link) => {
|
|
984
1121
|
if (!(link instanceof HTMLElement)) {
|
|
985
1122
|
return { available: false, enabled: true, frameName: null };
|
|
@@ -1498,5 +1635,8 @@ const ensureTabAccessories = aggregateDebounce(() => {
|
|
|
1498
1635
|
openMenuForLink(link, anchor) {
|
|
1499
1636
|
return openNotificationMenuForLink(link, anchor);
|
|
1500
1637
|
},
|
|
1638
|
+
mountSettingsForLink(link, container, callbacks) {
|
|
1639
|
+
return mountNotificationSettingsForLink(link, container, callbacks);
|
|
1640
|
+
},
|
|
1501
1641
|
};
|
|
1502
1642
|
})();
|
|
@@ -220,6 +220,113 @@ body.dark .tab-link-popover .tab-link-popover-item:hover,
|
|
|
220
220
|
body.dark .tab-link-popover .tab-link-popover-item:focus-visible {
|
|
221
221
|
background: rgba(148, 163, 184, 0.12);
|
|
222
222
|
}
|
|
223
|
+
.tab-link-popover .tab-link-popover-item[aria-expanded="true"] {
|
|
224
|
+
background: rgba(15, 23, 42, 0.04);
|
|
225
|
+
}
|
|
226
|
+
body.dark .tab-link-popover .tab-link-popover-item[aria-expanded="true"] {
|
|
227
|
+
background: rgba(148, 163, 184, 0.08);
|
|
228
|
+
}
|
|
229
|
+
.tab-link-notification-settings {
|
|
230
|
+
margin: 0 14px 10px 40px;
|
|
231
|
+
border: 1px solid rgba(15, 23, 42, 0.1);
|
|
232
|
+
border-radius: 7px;
|
|
233
|
+
background: rgba(15, 23, 42, 0.025);
|
|
234
|
+
overflow: hidden;
|
|
235
|
+
}
|
|
236
|
+
body.dark .tab-link-notification-settings {
|
|
237
|
+
border-color: rgba(148, 163, 184, 0.18);
|
|
238
|
+
background: rgba(148, 163, 184, 0.045);
|
|
239
|
+
}
|
|
240
|
+
.tab-link-notification-setting-row {
|
|
241
|
+
min-height: 44px;
|
|
242
|
+
padding: 7px 10px;
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
245
|
+
align-items: center;
|
|
246
|
+
gap: 12px;
|
|
247
|
+
box-sizing: border-box;
|
|
248
|
+
color: inherit;
|
|
249
|
+
}
|
|
250
|
+
.tab-link-notification-setting-row + .tab-link-notification-setting-row {
|
|
251
|
+
border-top: 1px solid rgba(15, 23, 42, 0.08);
|
|
252
|
+
}
|
|
253
|
+
body.dark .tab-link-notification-setting-row + .tab-link-notification-setting-row {
|
|
254
|
+
border-top-color: rgba(148, 163, 184, 0.14);
|
|
255
|
+
}
|
|
256
|
+
.tab-link-notification-setting-label {
|
|
257
|
+
min-width: 0;
|
|
258
|
+
font-size: 12px;
|
|
259
|
+
font-weight: 500;
|
|
260
|
+
color: rgba(15, 23, 42, 0.76);
|
|
261
|
+
}
|
|
262
|
+
body.dark .tab-link-notification-setting-label {
|
|
263
|
+
color: rgba(226, 232, 240, 0.86);
|
|
264
|
+
}
|
|
265
|
+
.tab-link-notification-toggle-control {
|
|
266
|
+
display: inline-flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
gap: 7px;
|
|
269
|
+
}
|
|
270
|
+
.tab-link-notification-toggle-status {
|
|
271
|
+
min-width: 18px;
|
|
272
|
+
font-size: 11px;
|
|
273
|
+
text-align: right;
|
|
274
|
+
color: rgba(15, 23, 42, 0.56);
|
|
275
|
+
}
|
|
276
|
+
body.dark .tab-link-notification-toggle-status {
|
|
277
|
+
color: rgba(226, 232, 240, 0.62);
|
|
278
|
+
}
|
|
279
|
+
.tab-link-notification-switch {
|
|
280
|
+
position: relative;
|
|
281
|
+
width: 36px;
|
|
282
|
+
height: 26px;
|
|
283
|
+
padding: 4px;
|
|
284
|
+
appearance: none;
|
|
285
|
+
border: 0;
|
|
286
|
+
border-radius: 999px;
|
|
287
|
+
background: rgba(100, 116, 139, 0.42);
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
transition: background-color 140ms ease;
|
|
290
|
+
}
|
|
291
|
+
.tab-link-notification-switch[data-enabled="true"] {
|
|
292
|
+
background: var(--pinokio-focus-color, #2563eb);
|
|
293
|
+
}
|
|
294
|
+
.tab-link-notification-switch-thumb {
|
|
295
|
+
display: block;
|
|
296
|
+
width: 18px;
|
|
297
|
+
height: 18px;
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
background: #f8fafc;
|
|
300
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.24);
|
|
301
|
+
transform: translateX(0);
|
|
302
|
+
transition: transform 140ms ease;
|
|
303
|
+
}
|
|
304
|
+
.tab-link-notification-switch[data-enabled="true"] .tab-link-notification-switch-thumb {
|
|
305
|
+
transform: translateX(10px);
|
|
306
|
+
}
|
|
307
|
+
.tab-link-notification-switch:focus-visible,
|
|
308
|
+
.tab-link-notification-sound-select:focus-visible {
|
|
309
|
+
outline: 2px solid var(--pinokio-focus-color, #2563eb);
|
|
310
|
+
outline-offset: 2px;
|
|
311
|
+
}
|
|
312
|
+
.tab-link-notification-sound-select {
|
|
313
|
+
width: min(160px, 44vw);
|
|
314
|
+
min-height: 32px;
|
|
315
|
+
padding: 5px 28px 5px 9px;
|
|
316
|
+
border: 1px solid rgba(15, 23, 42, 0.14);
|
|
317
|
+
border-radius: 6px;
|
|
318
|
+
background: var(--pinokio-sidebar-tabbar-bg, #ffffff);
|
|
319
|
+
color: rgba(15, 23, 42, 0.82);
|
|
320
|
+
font: inherit;
|
|
321
|
+
font-size: 12px;
|
|
322
|
+
cursor: pointer;
|
|
323
|
+
}
|
|
324
|
+
body.dark .tab-link-notification-sound-select {
|
|
325
|
+
border-color: rgba(148, 163, 184, 0.22);
|
|
326
|
+
background: rgba(15, 23, 42, 0.42);
|
|
327
|
+
color: rgba(241, 245, 249, 0.9);
|
|
328
|
+
color-scheme: dark;
|
|
329
|
+
}
|
|
223
330
|
.tab-link-popover .tab-link-popover-item .label {
|
|
224
331
|
font-size: 11px;
|
|
225
332
|
font-weight: 600;
|
|
@@ -344,6 +451,27 @@ body.dark .tab-link-popover .tab-link-popover-footer:focus-visible {
|
|
|
344
451
|
padding-right: 16px;
|
|
345
452
|
padding-left: 16px;
|
|
346
453
|
}
|
|
454
|
+
.tab-link-notification-settings {
|
|
455
|
+
margin-right: 16px;
|
|
456
|
+
margin-left: 42px;
|
|
457
|
+
}
|
|
458
|
+
.tab-link-notification-setting-row {
|
|
459
|
+
padding-right: 8px;
|
|
460
|
+
padding-left: 8px;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
@media (pointer: coarse) {
|
|
464
|
+
.tab-link-notification-switch {
|
|
465
|
+
width: 44px;
|
|
466
|
+
height: 30px;
|
|
467
|
+
padding: 4px;
|
|
468
|
+
}
|
|
469
|
+
.tab-link-notification-switch[data-enabled="true"] .tab-link-notification-switch-thumb {
|
|
470
|
+
transform: translateX(14px);
|
|
471
|
+
}
|
|
472
|
+
.tab-link-notification-sound-select {
|
|
473
|
+
min-height: 44px;
|
|
474
|
+
}
|
|
347
475
|
}
|
|
348
476
|
.appcanvas > aside .menu-container .tab-link-popover-host {
|
|
349
477
|
display: flex;
|
|
@@ -266,10 +266,39 @@
|
|
|
266
266
|
if (action === "notifications") {
|
|
267
267
|
const activeLink = tabLinkActiveLink
|
|
268
268
|
const notifier = typeof window !== "undefined" ? window.PinokioIdleNotifier : null
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
const existingPanel = item.nextElementSibling
|
|
270
|
+
if (existingPanel && existingPanel.classList.contains("tab-link-notification-settings")) {
|
|
271
|
+
existingPanel.remove()
|
|
272
|
+
item.setAttribute("aria-expanded", "false")
|
|
273
|
+
item.removeAttribute("aria-controls")
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
if (notifier && typeof notifier.mountSettingsForLink === "function") {
|
|
277
|
+
const panel = document.createElement("div")
|
|
278
|
+
panel.className = "tab-link-notification-settings"
|
|
279
|
+
panel.id = `${TAB_LINK_POPOVER_ID}-notification-settings`
|
|
280
|
+
panel.setAttribute("role", "group")
|
|
281
|
+
panel.setAttribute("aria-label", "Desktop notification settings")
|
|
282
|
+
item.insertAdjacentElement("afterend", panel)
|
|
283
|
+
const value = item.querySelector(".value")
|
|
284
|
+
const icon = item.querySelector(".tab-link-popover-action-icon i")
|
|
285
|
+
const mounted = notifier.mountSettingsForLink(activeLink, panel, {
|
|
286
|
+
onEnabledChange(enabled) {
|
|
287
|
+
if (value) {
|
|
288
|
+
value.textContent = enabled ? "Enabled for this tab" : "Disabled for this tab"
|
|
289
|
+
}
|
|
290
|
+
if (icon) {
|
|
291
|
+
icon.classList.toggle("fa-bell", enabled)
|
|
292
|
+
icon.classList.toggle("fa-bell-slash", !enabled)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
if (mounted) {
|
|
297
|
+
item.setAttribute("aria-controls", panel.id)
|
|
298
|
+
item.setAttribute("aria-expanded", "true")
|
|
299
|
+
} else {
|
|
300
|
+
panel.remove()
|
|
301
|
+
}
|
|
273
302
|
}
|
|
274
303
|
return
|
|
275
304
|
}
|
|
@@ -1898,6 +1927,9 @@
|
|
|
1898
1927
|
if (actionItem.action) {
|
|
1899
1928
|
item.setAttribute("data-action", actionItem.action)
|
|
1900
1929
|
}
|
|
1930
|
+
if (actionItem.action === "notifications") {
|
|
1931
|
+
item.setAttribute("aria-expanded", "false")
|
|
1932
|
+
}
|
|
1901
1933
|
if (actionItem.url) {
|
|
1902
1934
|
item.setAttribute("data-url", actionItem.url)
|
|
1903
1935
|
}
|
package/server/public/vault.js
CHANGED
|
@@ -402,6 +402,7 @@ const PAGE_SIZE = 500
|
|
|
402
402
|
const state = {
|
|
403
403
|
data: null,
|
|
404
404
|
candidateSize: defaultCandidateSize,
|
|
405
|
+
persistedCandidateSize: defaultCandidateSize,
|
|
405
406
|
candidateSizeInitialized: false,
|
|
406
407
|
view: "all",
|
|
407
408
|
sourceId: SCOPE_ID,
|
|
@@ -546,6 +547,52 @@ const post = async (payload) => {
|
|
|
546
547
|
if (!result) throw new Error(COPY.action_request_failed.replace("{status}", response.status))
|
|
547
548
|
return result
|
|
548
549
|
}
|
|
550
|
+
let candidateSizeSaveTail = Promise.resolve(true)
|
|
551
|
+
let candidateSizeSaveGeneration = 0
|
|
552
|
+
const saveCandidateSize = (size) => {
|
|
553
|
+
if (!candidateSizeOptions.includes(size)) return candidateSizeSaveTail
|
|
554
|
+
const generation = ++candidateSizeSaveGeneration
|
|
555
|
+
state.candidateSize = size
|
|
556
|
+
state.candidateSizeInitialized = true
|
|
557
|
+
renderCandidateSizeControl()
|
|
558
|
+
const save = candidateSizeSaveTail.then(async () => {
|
|
559
|
+
const result = await post({
|
|
560
|
+
action: "set_candidate_size",
|
|
561
|
+
scope_id: SCOPE_ID,
|
|
562
|
+
candidate_size: size
|
|
563
|
+
})
|
|
564
|
+
if (result.error) throw new Error(result.error)
|
|
565
|
+
state.persistedCandidateSize = size
|
|
566
|
+
return true
|
|
567
|
+
})
|
|
568
|
+
candidateSizeSaveTail = save.catch(async (error) => {
|
|
569
|
+
const isCurrent = () => generation === candidateSizeSaveGeneration
|
|
570
|
+
if (isCurrent() && candidateSize() === size) {
|
|
571
|
+
state.candidateSizeInitialized = false
|
|
572
|
+
const refreshed = await refresh(true)
|
|
573
|
+
if (!refreshed && isCurrent() && !state.candidateSizeInitialized) {
|
|
574
|
+
state.candidateSize = state.persistedCandidateSize
|
|
575
|
+
state.candidateSizeInitialized = true
|
|
576
|
+
renderCandidateSizeControl()
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (isCurrent()) {
|
|
580
|
+
state.feedback = {
|
|
581
|
+
error: true,
|
|
582
|
+
message: error && error.message ? error.message : String(error)
|
|
583
|
+
}
|
|
584
|
+
renderFeedback()
|
|
585
|
+
}
|
|
586
|
+
return false
|
|
587
|
+
})
|
|
588
|
+
const pending = candidateSizeSaveTail
|
|
589
|
+
pending.finally(() => {
|
|
590
|
+
if (candidateSizeSaveTail === pending) {
|
|
591
|
+
candidateSizeSaveTail = Promise.resolve(true)
|
|
592
|
+
}
|
|
593
|
+
})
|
|
594
|
+
return candidateSizeSaveTail
|
|
595
|
+
}
|
|
549
596
|
const openGlobalWorkspace = () => {
|
|
550
597
|
window.parent.location.assign("/vault")
|
|
551
598
|
}
|
|
@@ -2846,10 +2893,13 @@ const settleFolderDiscoveryStart = () => {
|
|
|
2846
2893
|
}
|
|
2847
2894
|
const applyFullData = (data) => {
|
|
2848
2895
|
if (!state.candidateSizeInitialized) {
|
|
2849
|
-
const
|
|
2850
|
-
|
|
2851
|
-
|
|
2896
|
+
const saved = Number(data && data.candidate_min_bytes)
|
|
2897
|
+
const fallback = Number(data && data.global_candidate_min_bytes)
|
|
2898
|
+
const selected = candidateSizeOptions.includes(saved) ? saved : fallback
|
|
2899
|
+
state.candidateSize = candidateSizeOptions.includes(selected)
|
|
2900
|
+
? selected
|
|
2852
2901
|
: defaultCandidateSize
|
|
2902
|
+
state.persistedCandidateSize = state.candidateSize
|
|
2853
2903
|
state.candidateSizeInitialized = true
|
|
2854
2904
|
}
|
|
2855
2905
|
const scanning = scanActive(data.scan)
|
|
@@ -3228,10 +3278,16 @@ const startFolderDiscovery = async (folderPath) => {
|
|
|
3228
3278
|
renderFolderDiscovery()
|
|
3229
3279
|
try {
|
|
3230
3280
|
resetFolderDiscoveryChoices()
|
|
3281
|
+
if (!await candidateSizeSaveTail) {
|
|
3282
|
+
state.folderDiscoveryStarting = false
|
|
3283
|
+
state.folderDiscoveryChoosingRoot = true
|
|
3284
|
+
renderFolderDiscovery()
|
|
3285
|
+
focusFolderDiscoveryDialog()
|
|
3286
|
+
return
|
|
3287
|
+
}
|
|
3231
3288
|
const result = await post({
|
|
3232
3289
|
action: "find_folders",
|
|
3233
|
-
path: folderPath
|
|
3234
|
-
candidate_size: candidateSize()
|
|
3290
|
+
path: folderPath
|
|
3235
3291
|
})
|
|
3236
3292
|
if (result.error) throw new Error(result.error)
|
|
3237
3293
|
if (!result.started && !result.already_running) {
|
|
@@ -3354,10 +3410,8 @@ document.addEventListener("click", async (event) => {
|
|
|
3354
3410
|
if (target.hasAttribute("data-candidate-size")) {
|
|
3355
3411
|
const size = Number(target.dataset.candidateSize)
|
|
3356
3412
|
if (candidateSizeOptions.includes(size)) {
|
|
3357
|
-
state.candidateSize = size
|
|
3358
|
-
state.candidateSizeInitialized = true
|
|
3359
3413
|
closeScanSizeMenu()
|
|
3360
|
-
|
|
3414
|
+
await saveCandidateSize(size)
|
|
3361
3415
|
}
|
|
3362
3416
|
return
|
|
3363
3417
|
}
|
|
@@ -3768,10 +3822,13 @@ document.addEventListener("click", async (event) => {
|
|
|
3768
3822
|
state.scanPreviewOpen = false
|
|
3769
3823
|
state.feedback = null
|
|
3770
3824
|
try {
|
|
3825
|
+
if (!await candidateSizeSaveTail) {
|
|
3826
|
+
state.scanRequested = false
|
|
3827
|
+
return
|
|
3828
|
+
}
|
|
3771
3829
|
const result = await post({
|
|
3772
3830
|
action: "scan",
|
|
3773
|
-
scope_id: SCOPE_ID
|
|
3774
|
-
candidate_size: candidateSize()
|
|
3831
|
+
scope_id: SCOPE_ID
|
|
3775
3832
|
})
|
|
3776
3833
|
if (result.error) throw new Error(result.error)
|
|
3777
3834
|
await refresh()
|
|
@@ -4227,11 +4284,9 @@ document.addEventListener("change", async (event) => {
|
|
|
4227
4284
|
if (["vault-candidate-size", "vault-find-candidate-size"]
|
|
4228
4285
|
.includes(event.target.id)) {
|
|
4229
4286
|
const size = Number(event.target.value)
|
|
4230
|
-
|
|
4287
|
+
await saveCandidateSize(candidateSizeOptions.includes(size)
|
|
4231
4288
|
? size
|
|
4232
|
-
: defaultCandidateSize
|
|
4233
|
-
state.candidateSizeInitialized = true
|
|
4234
|
-
renderCandidateSizeControl()
|
|
4289
|
+
: defaultCandidateSize)
|
|
4235
4290
|
return
|
|
4236
4291
|
}
|
|
4237
4292
|
if (event.target.id !== "vault-status-filter") return
|
|
@@ -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("
|
|
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
|
-
|
|
315
|
+
await vault.perform("set_candidate_size", {
|
|
185
316
|
candidate_size: selected
|
|
186
|
-
})
|
|
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
|
|
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.
|
|
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
|
-
})
|
|
1053
|
-
assert.
|
|
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
|
|
package/test/vault-ui.test.js
CHANGED
|
@@ -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(
|
|
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("
|
|
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
|
|
871
|
+
const saved = 50 * candidateBase ** 2
|
|
872
872
|
const stored = 10 * candidateBase ** 2
|
|
873
873
|
const { dom, requests } = await makePage(fixture([], {
|
|
874
|
-
|
|
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")
|
|
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
|
-
|
|
918
|
-
request.action === "
|
|
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
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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")
|
|
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
|
|