mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-03 11:22:25 +00:00
Add per-profile settings store and mask wallet inputs
This commit is contained in:
1 parent
1ce38ba0de
commit
094559b618
6 files changed
+520
-40
No files matched your search
@@ -6,6 +6,7 @@ import {
|
||||
} from "../nwcSettings.js";
|
||||
import { MAX_WALLET_DEFAULT_ZAP } from "../config.js";
|
||||
import { devLogger, userLogger } from "../utils/logger.js";
|
||||
import createProfileSettingsStore from "../state/profileSettingsStore.js";
|
||||
|
||||
const NWC_URI_SCHEME = "nostr+walletconnect://";
|
||||
|
||||
@@ -53,7 +54,10 @@ export default class NwcSettingsService {
|
||||
this.createDefaultSettings = isFunction(createDefaultSettings)
|
||||
? createDefaultSettings
|
||||
: () => createDefaultNwcSettings();
|
||||
this.cache = new Map();
|
||||
this.cache = createProfileSettingsStore({
|
||||
clone: (value) => this.cloneSettings(value),
|
||||
logger: this.logger.dev,
|
||||
});
|
||||
}
|
||||
|
||||
cloneSettings(settings) {
|
||||
@@ -101,7 +105,8 @@ export default class NwcSettingsService {
|
||||
? { ...settings }
|
||||
: this.createDefaultNwcSettings();
|
||||
this.cache.set(normalized, record);
|
||||
return this.cloneSettings(record);
|
||||
const cached = this.cache.get(normalized);
|
||||
return cached || this.createDefaultNwcSettings();
|
||||
} catch (error) {
|
||||
if (isFunction(this.logger.user?.warn)) {
|
||||
this.logger.user.warn(
|
||||
@@ -111,7 +116,8 @@ export default class NwcSettingsService {
|
||||
}
|
||||
const fallback = this.createDefaultNwcSettings();
|
||||
this.cache.set(normalized, fallback);
|
||||
return this.cloneSettings(fallback);
|
||||
const cached = this.cache.get(normalized);
|
||||
return cached || this.createDefaultNwcSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +127,7 @@ export default class NwcSettingsService {
|
||||
return this.createDefaultNwcSettings();
|
||||
}
|
||||
const cached = this.cache.get(normalized);
|
||||
return cached ? this.cloneSettings(cached) : this.createDefaultNwcSettings();
|
||||
return cached ? cached : this.createDefaultNwcSettings();
|
||||
}
|
||||
|
||||
createDefaultNwcSettings() {
|
||||
@@ -178,7 +184,8 @@ export default class NwcSettingsService {
|
||||
? { ...updated }
|
||||
: this.createDefaultNwcSettings();
|
||||
this.cache.set(normalized, record);
|
||||
return this.cloneSettings(record);
|
||||
const cached = this.cache.get(normalized);
|
||||
return cached || this.createDefaultNwcSettings();
|
||||
} catch (error) {
|
||||
if (isFunction(this.logger.user?.warn)) {
|
||||
this.logger.user.warn(
|
||||
@@ -294,14 +301,6 @@ export default class NwcSettingsService {
|
||||
const normalizedActive = this.getNormalizedPubkey(
|
||||
pubkey !== undefined ? pubkey : this.getActivePubkey(),
|
||||
);
|
||||
const normalizedPrevious = this.getNormalizedPubkey(previousPubkey);
|
||||
|
||||
if (
|
||||
normalizedPrevious &&
|
||||
(!normalizedActive || normalizedPrevious !== normalizedActive)
|
||||
) {
|
||||
await this.clearStoredNwcSettings(normalizedPrevious, { silent: true });
|
||||
}
|
||||
|
||||
if (normalizedActive) {
|
||||
await this.hydrateNwcSettingsForPubkey(normalizedActive);
|
||||
@@ -315,13 +314,14 @@ export default class NwcSettingsService {
|
||||
previousPubkey !== undefined ? previousPubkey : pubkey,
|
||||
);
|
||||
if (normalizedPrevious) {
|
||||
await this.clearStoredNwcSettings(normalizedPrevious, { silent: false });
|
||||
this.cache.delete(normalizedPrevious);
|
||||
}
|
||||
this.clearCache();
|
||||
return this.createDefaultNwcSettings();
|
||||
}
|
||||
|
||||
onIdentityChanged() {
|
||||
this.clearCache();
|
||||
// Intentionally preserve cached settings so profile switches retain
|
||||
// per-account configuration. Explicit cache clearing remains
|
||||
// available via `clearCache()` when required by callers.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { devLogger } from "../utils/logger.js";
|
||||
|
||||
function noopClone(value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
export function createProfileSettingsStore({ clone = noopClone, logger = devLogger } = {}) {
|
||||
const cache = new Map();
|
||||
|
||||
const cloneValue = (value) => {
|
||||
try {
|
||||
return clone(value);
|
||||
} catch (error) {
|
||||
if (logger && typeof logger.warn === "function") {
|
||||
logger.warn("[profileSettingsStore] Failed to clone value", error);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
const store = {
|
||||
set(key, value) {
|
||||
if (!key) {
|
||||
return store;
|
||||
}
|
||||
cache.set(key, cloneValue(value));
|
||||
return store;
|
||||
},
|
||||
get(key) {
|
||||
if (!key || !cache.has(key)) {
|
||||
return undefined;
|
||||
}
|
||||
return cloneValue(cache.get(key));
|
||||
},
|
||||
has(key) {
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
return cache.has(key);
|
||||
},
|
||||
delete(key) {
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
return cache.delete(key);
|
||||
},
|
||||
clear() {
|
||||
cache.clear();
|
||||
return store;
|
||||
},
|
||||
keys() {
|
||||
return Array.from(cache.keys());
|
||||
},
|
||||
values() {
|
||||
return Array.from(cache.values()).map((value) => cloneValue(value));
|
||||
},
|
||||
entries() {
|
||||
return Array.from(cache.entries()).map(([key, value]) => [
|
||||
key,
|
||||
cloneValue(value),
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(store, "size", {
|
||||
get() {
|
||||
return cache.size;
|
||||
},
|
||||
});
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export default createProfileSettingsStore;
|
||||
+178
-24
@@ -21,6 +21,7 @@ const DEFAULT_ADMIN_DM_IMAGE_URL =
|
||||
"https://beta.bitvid.network/assets/jpg/video-thumbnail-fallback.jpg";
|
||||
const DEFAULT_BITVID_WEBSITE_URL = "https://bitvid.network/";
|
||||
const NWC_URI_SCHEME = "nostr+walletconnect://";
|
||||
const SECRET_PLACEHOLDER = "*****";
|
||||
const DEFAULT_MAX_WALLET_DEFAULT_ZAP = 100000000;
|
||||
const DEFAULT_SAVED_PROFILE_LABEL = "Saved profile";
|
||||
|
||||
@@ -1262,7 +1263,15 @@ export class ProfileModalController {
|
||||
}
|
||||
|
||||
if (this.walletUriInput instanceof HTMLElement) {
|
||||
this.walletUriInput.addEventListener("focus", () => {
|
||||
this.revealSecretInputValue(this.walletUriInput);
|
||||
});
|
||||
this.walletUriInput.addEventListener("blur", () => {
|
||||
this.handleSecretInputBlur(this.walletUriInput);
|
||||
this.applyWalletControlState();
|
||||
});
|
||||
this.walletUriInput.addEventListener("input", () => {
|
||||
this.handleSecretInputChange(this.walletUriInput);
|
||||
this.applyWalletControlState();
|
||||
});
|
||||
}
|
||||
@@ -2984,10 +2993,7 @@ export class ProfileModalController {
|
||||
applyWalletControlState() {
|
||||
const hasActive = Boolean(this.normalizeHexPubkey(this.getActivePubkey()));
|
||||
const busy = this.isWalletBusy();
|
||||
const uriValue =
|
||||
typeof this.walletUriInput?.value === "string"
|
||||
? this.walletUriInput.value.trim()
|
||||
: "";
|
||||
const uriValue = this.getSecretInputValue(this.walletUriInput);
|
||||
const hasUri = uriValue.length > 0;
|
||||
|
||||
const applyDisabledState = (element, disabled) => {
|
||||
@@ -3049,20 +3055,17 @@ export class ProfileModalController {
|
||||
|
||||
refreshWalletPaneState() {
|
||||
const hasActive = Boolean(this.normalizeHexPubkey(this.getActivePubkey()));
|
||||
const setInputValue = (element, value) => {
|
||||
if (element && typeof element === "object" && "value" in element) {
|
||||
if (!hasActive) {
|
||||
this.setSecretInputValue(this.walletUriInput, "");
|
||||
if (this.walletDefaultZapInput && "value" in this.walletDefaultZapInput) {
|
||||
try {
|
||||
element.value = value;
|
||||
this.walletDefaultZapInput.value = "";
|
||||
} catch (error) {
|
||||
if (element instanceof HTMLElement) {
|
||||
element.setAttribute("data-value", value);
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.setAttribute("data-value", "");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (!hasActive) {
|
||||
setInputValue(this.walletUriInput, "");
|
||||
setInputValue(this.walletDefaultZapInput, "");
|
||||
this.updateWalletStatus("Sign in to connect a wallet.", "info");
|
||||
this.applyWalletControlState();
|
||||
return;
|
||||
@@ -3072,13 +3075,20 @@ export class ProfileModalController {
|
||||
if (!settings || typeof settings !== "object") {
|
||||
settings = this.services.nwcSettings.createDefaultNwcSettings();
|
||||
}
|
||||
setInputValue(this.walletUriInput, settings.nwcUri || "");
|
||||
setInputValue(
|
||||
this.walletDefaultZapInput,
|
||||
settings.defaultZap === null || settings.defaultZap === undefined
|
||||
? ""
|
||||
: String(settings.defaultZap),
|
||||
);
|
||||
this.setSecretInputValue(this.walletUriInput, settings.nwcUri || "");
|
||||
if (this.walletDefaultZapInput && "value" in this.walletDefaultZapInput) {
|
||||
const defaultZapValue =
|
||||
settings.defaultZap === null || settings.defaultZap === undefined
|
||||
? ""
|
||||
: String(settings.defaultZap);
|
||||
try {
|
||||
this.walletDefaultZapInput.value = defaultZapValue;
|
||||
} catch (error) {
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.setAttribute("data-value", defaultZapValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.nwcUri) {
|
||||
this.updateWalletStatus(
|
||||
@@ -3092,11 +3102,155 @@ export class ProfileModalController {
|
||||
this.applyWalletControlState();
|
||||
}
|
||||
|
||||
isSecretInputElement(element) {
|
||||
if (!element || typeof element !== "object") {
|
||||
return false;
|
||||
}
|
||||
if (typeof HTMLInputElement !== "undefined" && element instanceof HTMLInputElement) {
|
||||
return true;
|
||||
}
|
||||
return typeof element.value === "string";
|
||||
}
|
||||
|
||||
sanitizeSecretValue(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
getSecretInputValue(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const placeholder =
|
||||
typeof element.dataset?.secretPlaceholder === "string"
|
||||
? element.dataset.secretPlaceholder
|
||||
: SECRET_PLACEHOLDER;
|
||||
const stored = this.sanitizeSecretValue(
|
||||
typeof element.dataset?.secretValue === "string"
|
||||
? element.dataset.secretValue
|
||||
: "",
|
||||
);
|
||||
const raw = this.sanitizeSecretValue(element.value);
|
||||
const isMasked = element.dataset?.secretMasked === "true";
|
||||
|
||||
if (isMasked && placeholder && raw === placeholder) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
if (!raw && isMasked) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
return raw;
|
||||
}
|
||||
|
||||
setSecretInputValue(element, value) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sanitized = this.sanitizeSecretValue(value);
|
||||
if (!sanitized) {
|
||||
if (element.dataset) {
|
||||
delete element.dataset.secretValue;
|
||||
delete element.dataset.secretMasked;
|
||||
delete element.dataset.secretPlaceholder;
|
||||
}
|
||||
element.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholder = SECRET_PLACEHOLDER;
|
||||
if (element.dataset) {
|
||||
element.dataset.secretValue = sanitized;
|
||||
element.dataset.secretPlaceholder = placeholder;
|
||||
element.dataset.secretMasked = "true";
|
||||
}
|
||||
element.value = placeholder;
|
||||
}
|
||||
|
||||
revealSecretInputValue(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stored = this.sanitizeSecretValue(
|
||||
typeof element.dataset?.secretValue === "string"
|
||||
? element.dataset.secretValue
|
||||
: "",
|
||||
);
|
||||
|
||||
if (!stored) {
|
||||
if (element.dataset) {
|
||||
delete element.dataset.secretMasked;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.dataset) {
|
||||
element.dataset.secretMasked = "false";
|
||||
}
|
||||
element.value = stored;
|
||||
try {
|
||||
if (typeof element.setSelectionRange === "function") {
|
||||
const length = stored.length;
|
||||
element.setSelectionRange(length, length);
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore selection errors on unsupported input types.
|
||||
}
|
||||
}
|
||||
|
||||
handleSecretInputChange(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.sanitizeSecretValue(element.value);
|
||||
if (!value) {
|
||||
if (element.dataset) {
|
||||
element.dataset.secretMasked = "false";
|
||||
delete element.dataset.secretValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.dataset) {
|
||||
element.dataset.secretValue = value;
|
||||
element.dataset.secretMasked = "false";
|
||||
if (!element.dataset.secretPlaceholder) {
|
||||
element.dataset.secretPlaceholder = SECRET_PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleSecretInputBlur(element) {
|
||||
if (!this.isSecretInputElement(element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.sanitizeSecretValue(this.getSecretInputValue(element));
|
||||
if (!value) {
|
||||
if (element.dataset) {
|
||||
delete element.dataset.secretValue;
|
||||
delete element.dataset.secretMasked;
|
||||
delete element.dataset.secretPlaceholder;
|
||||
}
|
||||
element.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.dataset) {
|
||||
element.dataset.secretValue = value;
|
||||
element.dataset.secretPlaceholder =
|
||||
element.dataset.secretPlaceholder || SECRET_PLACEHOLDER;
|
||||
element.dataset.secretMasked = "true";
|
||||
}
|
||||
element.value = element.dataset?.secretPlaceholder || SECRET_PLACEHOLDER;
|
||||
}
|
||||
|
||||
getWalletFormValues() {
|
||||
const uri =
|
||||
typeof this.walletUriInput?.value === "string"
|
||||
? this.walletUriInput.value.trim()
|
||||
: "";
|
||||
const uri = this.getSecretInputValue(this.walletUriInput);
|
||||
const defaultZapRaw =
|
||||
typeof this.walletDefaultZapInput?.value === "string"
|
||||
? this.walletDefaultZapInput.value.trim()
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import NwcSettingsService from "../js/services/nwcSettingsService.js";
|
||||
|
||||
const DEFAULT_SETTINGS = {
|
||||
nwcUri: "",
|
||||
defaultZap: null,
|
||||
lastChecked: null,
|
||||
version: "",
|
||||
};
|
||||
|
||||
function createService({ saved = new Map(), activeRef }) {
|
||||
const service = new NwcSettingsService({
|
||||
normalizeHexPubkey: (value) => (typeof value === "string" ? value : null),
|
||||
getActivePubkey: () => activeRef.value,
|
||||
loadSettings: async (pubkey) => saved.get(pubkey),
|
||||
saveSettings: async (pubkey, partial = {}) => {
|
||||
const current = saved.get(pubkey) || {};
|
||||
const next = { ...current, ...partial };
|
||||
saved.set(pubkey, next);
|
||||
return next;
|
||||
},
|
||||
clearSettings: async (pubkey) => {
|
||||
saved.delete(pubkey);
|
||||
},
|
||||
createDefaultSettings: () => ({ ...DEFAULT_SETTINGS }),
|
||||
});
|
||||
return service;
|
||||
}
|
||||
|
||||
test("nwc settings service preserves settings across profile switches", async () => {
|
||||
const saved = new Map([
|
||||
["pub1", { nwcUri: "nostr+walletconnect://pub1?relay=wss://one" }],
|
||||
["pub2", { nwcUri: "nostr+walletconnect://pub2?relay=wss://two" }],
|
||||
]);
|
||||
const activeRef = { value: null };
|
||||
const service = createService({ saved, activeRef });
|
||||
|
||||
activeRef.value = "pub1";
|
||||
await service.onLogin({ pubkey: "pub1", identityChanged: true });
|
||||
const first = service.getActiveNwcSettings();
|
||||
assert.equal(first.nwcUri, "nostr+walletconnect://pub1?relay=wss://one");
|
||||
|
||||
activeRef.value = "pub2";
|
||||
await service.onLogin({
|
||||
pubkey: "pub2",
|
||||
previousPubkey: "pub1",
|
||||
identityChanged: true,
|
||||
});
|
||||
const second = service.getActiveNwcSettings();
|
||||
assert.equal(second.nwcUri, "nostr+walletconnect://pub2?relay=wss://two");
|
||||
|
||||
assert.equal(saved.has("pub1"), true, "previous profile data should stay persisted");
|
||||
assert.equal(service.cache.has("pub1"), true, "previous profile should remain cached");
|
||||
|
||||
activeRef.value = "pub1";
|
||||
await service.onLogin({
|
||||
pubkey: "pub1",
|
||||
previousPubkey: "pub2",
|
||||
identityChanged: true,
|
||||
});
|
||||
const roundTrip = service.getActiveNwcSettings();
|
||||
assert.equal(roundTrip.nwcUri, "nostr+walletconnect://pub1?relay=wss://one");
|
||||
|
||||
roundTrip.nwcUri = "mutated";
|
||||
const unchanged = service.getActiveNwcSettings();
|
||||
assert.equal(
|
||||
unchanged.nwcUri,
|
||||
"nostr+walletconnect://pub1?relay=wss://one",
|
||||
"mutating returned settings should not affect cached data",
|
||||
);
|
||||
|
||||
activeRef.value = "pub2";
|
||||
await service.onLogout({ pubkey: "pub2" });
|
||||
assert.equal(saved.has("pub2"), true, "logout should not clear persisted settings");
|
||||
assert.equal(service.cache.has("pub2"), false, "logout should drop cached entry for account");
|
||||
});
|
||||
|
||||
test("updateActiveNwcSettings returns cloned values", async () => {
|
||||
const saved = new Map();
|
||||
const activeRef = { value: "pub3" };
|
||||
const service = createService({ saved, activeRef });
|
||||
|
||||
const updated = await service.updateActiveNwcSettings({
|
||||
nwcUri: "nostr+walletconnect://pub3?relay=wss://three",
|
||||
defaultZap: 777,
|
||||
});
|
||||
|
||||
assert.equal(updated.nwcUri, "nostr+walletconnect://pub3?relay=wss://three");
|
||||
assert.equal(updated.defaultZap, 777);
|
||||
|
||||
updated.nwcUri = "mutated";
|
||||
const cached = service.getActiveNwcSettings();
|
||||
assert.equal(cached.nwcUri, "nostr+walletconnect://pub3?relay=wss://three");
|
||||
assert.equal(saved.get("pub3").nwcUri, "nostr+walletconnect://pub3?relay=wss://three");
|
||||
});
|
||||
@@ -433,6 +433,69 @@ for (const _ of [0]) {
|
||||
});
|
||||
});
|
||||
|
||||
test('wallet URI input masks persisted values and restores on focus', async (t) => {
|
||||
const controller = createController();
|
||||
await controller.load();
|
||||
|
||||
controller.state.setActivePubkey('a'.repeat(64));
|
||||
|
||||
await controller.show('wallet');
|
||||
|
||||
const sampleUri =
|
||||
'nostr+walletconnect://pub1?relay=wss://relay.example.com';
|
||||
await controller.services.nwcSettings.updateActiveNwcSettings({
|
||||
nwcUri: sampleUri,
|
||||
defaultZap: 21,
|
||||
});
|
||||
|
||||
controller.refreshWalletPaneState();
|
||||
|
||||
assert.ok(
|
||||
controller.walletUriInput instanceof window.HTMLElement,
|
||||
'wallet URI input should exist',
|
||||
);
|
||||
assert.equal(controller.walletUriInput.value, '*****');
|
||||
assert.equal(controller.walletUriInput.dataset.secretValue, sampleUri);
|
||||
assert.equal(
|
||||
controller.walletDisconnectButton?.classList.contains('hidden'),
|
||||
false,
|
||||
'disconnect button should remain visible when a URI exists',
|
||||
);
|
||||
|
||||
const formValues = controller.getWalletFormValues();
|
||||
assert.equal(formValues.uri, sampleUri);
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
assert.equal(controller.walletUriInput.value, sampleUri);
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
assert.equal(controller.walletUriInput.value, '*****');
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
const updatedUri = `${sampleUri}&name=bitvid`;
|
||||
controller.walletUriInput.value = updatedUri;
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('input'));
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
|
||||
const updatedValues = controller.getWalletFormValues();
|
||||
assert.equal(updatedValues.uri, updatedUri);
|
||||
assert.equal(controller.walletUriInput.value, '*****');
|
||||
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('focus'));
|
||||
controller.walletUriInput.value = '';
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('input'));
|
||||
controller.walletUriInput.dispatchEvent(new window.Event('blur'));
|
||||
|
||||
assert.equal(controller.walletUriInput.value, '');
|
||||
assert.equal(controller.walletUriInput.dataset.secretValue, undefined);
|
||||
|
||||
t.after(() => {
|
||||
try {
|
||||
controller.hide({ silent: true });
|
||||
} catch {}
|
||||
});
|
||||
});
|
||||
|
||||
test('Profile modal uses abbreviated npub display', async () => {
|
||||
const sampleProfiles = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import createProfileSettingsStore from "../../js/state/profileSettingsStore.js";
|
||||
|
||||
function createClone(value) {
|
||||
if (!value || typeof value !== "object") {
|
||||
return value;
|
||||
}
|
||||
return { ...value };
|
||||
}
|
||||
|
||||
test("profile settings store clones values and tracks entries", () => {
|
||||
const store = createProfileSettingsStore({ clone: createClone });
|
||||
const first = { uri: "nostr+walletconnect://pub1" };
|
||||
const second = { uri: "nostr+walletconnect://pub2" };
|
||||
|
||||
const result = store.set("pub1", first);
|
||||
assert.strictEqual(result, store, "set() should return the store instance");
|
||||
assert.equal(store.size, 1);
|
||||
|
||||
store.set("pub2", second);
|
||||
assert.equal(store.size, 2);
|
||||
|
||||
const fromFirst = store.get("pub1");
|
||||
assert.deepEqual(fromFirst, first);
|
||||
assert.notStrictEqual(
|
||||
fromFirst,
|
||||
first,
|
||||
"get() should return a cloned copy to avoid external mutation",
|
||||
);
|
||||
|
||||
fromFirst.uri = "mutated";
|
||||
const reloaded = store.get("pub1");
|
||||
assert.equal(
|
||||
reloaded.uri,
|
||||
"nostr+walletconnect://pub1",
|
||||
"mutating a retrieved object should not impact stored values",
|
||||
);
|
||||
|
||||
const entries = store.entries();
|
||||
assert.deepEqual(
|
||||
entries,
|
||||
[
|
||||
["pub1", { uri: "nostr+walletconnect://pub1" }],
|
||||
["pub2", { uri: "nostr+walletconnect://pub2" }],
|
||||
],
|
||||
);
|
||||
|
||||
const keys = store.keys();
|
||||
assert.deepEqual(keys.sort(), ["pub1", "pub2"]);
|
||||
|
||||
const values = store.values();
|
||||
assert.deepEqual(values, [
|
||||
{ uri: "nostr+walletconnect://pub1" },
|
||||
{ uri: "nostr+walletconnect://pub2" },
|
||||
]);
|
||||
assert.notStrictEqual(values[0], fromFirst);
|
||||
|
||||
assert.equal(store.has("pub1"), true);
|
||||
assert.equal(store.delete("pub1"), true);
|
||||
assert.equal(store.has("pub1"), false);
|
||||
assert.equal(store.size, 1);
|
||||
|
||||
store.clear();
|
||||
assert.equal(store.size, 0);
|
||||
assert.equal(store.get("pub2"), undefined);
|
||||
});
|
||||
|
||||
test("profile settings store ignores falsy keys and survives clone failures", () => {
|
||||
const loggerCalls = [];
|
||||
const store = createProfileSettingsStore({
|
||||
clone: () => {
|
||||
throw new Error("clone failure");
|
||||
},
|
||||
logger: {
|
||||
warn: (message, error) => {
|
||||
loggerCalls.push([message, error?.message]);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.set(null, { foo: "bar" });
|
||||
assert.equal(store.size, 0, "falsy keys should be ignored");
|
||||
|
||||
store.set("pub1", { foo: "bar" });
|
||||
assert.equal(store.size, 1);
|
||||
|
||||
const value = store.get("pub1");
|
||||
assert.deepEqual(value, { foo: "bar" });
|
||||
assert.equal(loggerCalls.length > 0, true, "clone failure should be logged");
|
||||
});
|
||||
Reference in new issue
Block a user