mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 03:42:26 +00:00
Merge pull request #1666 from PR0M3TH3AN/ai/refactor-share-nostr-20260125-14923769825350051949
refactor(ai): extract ShareNostrController from js/app.js
This commit is contained in:
5 files changed
+402
-192
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 519 KiB |
@@ -169,6 +169,7 @@ import SimilarContentController from "./ui/similarContentController.js";
|
||||
import UrlHealthController from "./ui/urlHealthController.js";
|
||||
import VideoModalCommentController from "./ui/videoModalCommentController.js";
|
||||
import TorrentStatusController from "./ui/torrentStatusController.js";
|
||||
import ShareNostrController from "./ui/shareNostrController.js";
|
||||
import ModerationActionController from "./services/moderationActionController.js";
|
||||
import ModerationDecorator from "./services/moderationDecorator.js";
|
||||
import { bootstrapTrustedSeeds } from "./services/trustBootstrap.js";
|
||||
@@ -372,6 +373,20 @@ class Application {
|
||||
},
|
||||
});
|
||||
|
||||
this.shareNostrController = new ShareNostrController({
|
||||
ui: {
|
||||
showError: (msg) => this.showError(msg),
|
||||
showSuccess: (msg) => this.showSuccess(msg),
|
||||
getModal: () => this.shareNostrModal,
|
||||
},
|
||||
state: {
|
||||
getPubkey: () => this.pubkey,
|
||||
normalizeHexPubkey: (key) => this.normalizeHexPubkey(key),
|
||||
getCurrentVideo: () => this.currentVideo,
|
||||
buildShareUrlFromEventId: (id) => this.buildShareUrlFromEventId(id),
|
||||
},
|
||||
});
|
||||
|
||||
this.initializeModerationActionController();
|
||||
this.initializeSimilarContentController();
|
||||
|
||||
@@ -10116,165 +10131,9 @@ class Application {
|
||||
}
|
||||
|
||||
async handleShareNostrPost(payload = {}) {
|
||||
const video = payload?.video || null;
|
||||
const videoId = typeof video?.id === "string" ? video.id.trim() : "";
|
||||
const videoTitle =
|
||||
typeof video?.title === "string" ? video.title.trim() : "";
|
||||
const videoPubkey =
|
||||
typeof video?.pubkey === "string" ? video.pubkey.trim() : "";
|
||||
|
||||
if (!videoId || !videoTitle) {
|
||||
userLogger.warn("[Application] Share post missing video details.");
|
||||
this.showError("Missing video details for sharing.");
|
||||
throw new Error("share-missing-video-details");
|
||||
if (this.shareNostrController) {
|
||||
return this.shareNostrController.handleShare(payload);
|
||||
}
|
||||
|
||||
const signer = getActiveSigner();
|
||||
if (!signer || typeof signer.signEvent !== "function") {
|
||||
userLogger.warn("[Application] No active signer available for share.");
|
||||
this.showError("Connect a Nostr signer to share.");
|
||||
throw new Error("share-missing-signer");
|
||||
}
|
||||
|
||||
const activePubkey = this.normalizeHexPubkey(this.pubkey);
|
||||
const signerPubkey = this.normalizeHexPubkey(signer.pubkey);
|
||||
const eventPubkey = activePubkey || signerPubkey;
|
||||
|
||||
if (!eventPubkey) {
|
||||
userLogger.warn("[Application] Share post missing active pubkey.");
|
||||
this.showError("Please log in to share on Nostr.");
|
||||
throw new Error("share-missing-pubkey");
|
||||
}
|
||||
|
||||
if (activePubkey && signerPubkey && activePubkey !== signerPubkey) {
|
||||
userLogger.error(
|
||||
"[Application] Active signer does not match current account for share.",
|
||||
);
|
||||
this.showError("Active signer does not match your account.");
|
||||
throw new Error("share-signer-mismatch");
|
||||
}
|
||||
|
||||
if (!nostrClient?.pool) {
|
||||
userLogger.error("[Application] Share publish failed: relays not ready.");
|
||||
this.showError("Nostr relays are not ready yet. Please try again.");
|
||||
throw new Error("share-relays-unavailable");
|
||||
}
|
||||
|
||||
const relayEntries = Array.isArray(payload?.relays) ? payload.relays : [];
|
||||
const relayUrls = relayEntries
|
||||
.map((entry) => {
|
||||
if (typeof entry === "string") {
|
||||
return entry;
|
||||
}
|
||||
if (Array.isArray(entry) && entry.length) {
|
||||
if (entry[0] === "r") {
|
||||
return typeof entry[1] === "string" ? entry[1] : "";
|
||||
}
|
||||
return typeof entry[0] === "string" ? entry[0] : "";
|
||||
}
|
||||
if (entry && typeof entry === "object") {
|
||||
if (typeof entry.url === "string") {
|
||||
return entry.url;
|
||||
}
|
||||
if (typeof entry.relay === "string") {
|
||||
return entry.relay;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
const relayTargets = sanitizeRelayList(relayUrls);
|
||||
|
||||
if (!relayTargets.length) {
|
||||
userLogger.warn("[Application] Share post missing relay targets.");
|
||||
this.showError("Please choose at least one relay to share to.");
|
||||
throw new Error("share-missing-relays");
|
||||
}
|
||||
|
||||
if (signer.type === "extension" && nostrClient.ensureExtensionPermissions) {
|
||||
const permissionResult = await nostrClient.ensureExtensionPermissions(
|
||||
DEFAULT_NIP07_PERMISSION_METHODS,
|
||||
);
|
||||
if (!permissionResult?.ok) {
|
||||
userLogger.warn(
|
||||
"[Application] Share publish blocked by signer permissions.",
|
||||
permissionResult?.error,
|
||||
);
|
||||
this.showError("Signer permissions are required to post.");
|
||||
throw new Error("share-permission-denied");
|
||||
}
|
||||
}
|
||||
|
||||
const event = buildShareEvent({
|
||||
pubkey: eventPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: typeof payload?.content === "string" ? payload.content : "",
|
||||
video: { id: videoId, pubkey: videoPubkey },
|
||||
relays: relayEntries,
|
||||
});
|
||||
|
||||
let signedEvent;
|
||||
try {
|
||||
signedEvent = await queueSignEvent(signer, event);
|
||||
} catch (error) {
|
||||
userLogger.error("[Application] Failed to sign share event.", error);
|
||||
this.showError("Unable to sign the share event.");
|
||||
throw error;
|
||||
}
|
||||
|
||||
const publishResults = await publishEventToRelays(
|
||||
nostrClient.pool,
|
||||
relayTargets,
|
||||
signedEvent,
|
||||
);
|
||||
|
||||
let publishSummary;
|
||||
try {
|
||||
publishSummary = assertAnyRelayAccepted(publishResults, {
|
||||
context: "share note",
|
||||
});
|
||||
} catch (publishError) {
|
||||
if (publishError?.relayFailures?.length) {
|
||||
publishError.relayFailures.forEach(
|
||||
({ url, error: relayError, reason }) => {
|
||||
userLogger.error(
|
||||
`[Application] Relay ${url} rejected share note: ${reason}`,
|
||||
relayError || reason,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
this.showError("Failed to share on Nostr. Please try again.");
|
||||
throw publishError;
|
||||
}
|
||||
|
||||
if (publishSummary.failed.length) {
|
||||
publishSummary.failed.forEach(({ url, error: relayError }) => {
|
||||
const reason =
|
||||
relayError instanceof Error
|
||||
? relayError.message
|
||||
: relayError
|
||||
? String(relayError)
|
||||
: "publish failed";
|
||||
userLogger.warn(
|
||||
`[Application] Relay ${url} did not accept share note: ${reason}`,
|
||||
relayError,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
userLogger.info(
|
||||
"[Application] Share note published.",
|
||||
publishSummary.accepted.map(({ url }) => url),
|
||||
);
|
||||
this.showSuccess("Shared to Nostr!");
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
event: signedEvent,
|
||||
accepted: publishSummary.accepted.map(({ url }) => url),
|
||||
failed: publishSummary.failed.map(({ url }) => url),
|
||||
};
|
||||
}
|
||||
|
||||
shareActiveVideo() {
|
||||
@@ -10296,40 +10155,8 @@ class Application {
|
||||
}
|
||||
|
||||
async openShareNostrModal({ video, triggerElement } = {}) {
|
||||
const targetVideo =
|
||||
video && typeof video === "object" ? video : this.currentVideo || null;
|
||||
if (!targetVideo) {
|
||||
this.showError("No video is available to share.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.shareNostrModal) {
|
||||
devLogger.warn("[Application] Share Nostr modal is unavailable.");
|
||||
this.showError("Share modal is not ready yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
const shareUrl =
|
||||
typeof targetVideo.shareUrl === "string" && targetVideo.shareUrl.trim()
|
||||
? targetVideo.shareUrl.trim()
|
||||
: this.buildShareUrlFromEventId(targetVideo.id);
|
||||
const payload = {
|
||||
id: targetVideo.id,
|
||||
title: targetVideo.title,
|
||||
pubkey: targetVideo.pubkey,
|
||||
authorName: targetVideo.creatorName || targetVideo.authorName || "",
|
||||
thumbnail: targetVideo.thumbnail,
|
||||
shareUrl,
|
||||
};
|
||||
|
||||
try {
|
||||
await this.shareNostrModal.open({
|
||||
video: payload,
|
||||
triggerElement,
|
||||
});
|
||||
} catch (error) {
|
||||
devLogger.error("[Application] Failed to open Share Nostr modal:", error);
|
||||
this.showError("Unable to open the share modal.");
|
||||
if (this.shareNostrController) {
|
||||
return this.shareNostrController.openModal({ video, triggerElement });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
import { sanitizeRelayList as defaultSanitizeRelayList } from "../nostr/nip46Client.js";
|
||||
import { buildShareEvent as defaultBuildShareEvent } from "../nostrEventSchemas.js";
|
||||
import {
|
||||
publishEventToRelays as defaultPublishEventToRelays,
|
||||
assertAnyRelayAccepted as defaultAssertAnyRelayAccepted,
|
||||
} from "../nostrPublish.js";
|
||||
import {
|
||||
getActiveSigner as defaultGetActiveSigner,
|
||||
} from "../nostrClientRegistry.js";
|
||||
import { queueSignEvent as defaultQueueSignEvent } from "../nostr/signRequestQueue.js";
|
||||
import { DEFAULT_NIP07_PERMISSION_METHODS as defaultPermissionMethods } from "../nostr/nip07Permissions.js";
|
||||
import { nostrClient as defaultNostrClient } from "../nostrClientFacade.js";
|
||||
import { userLogger as defaultUserLogger, devLogger as defaultDevLogger } from "../utils/logger.js";
|
||||
|
||||
export default class ShareNostrController {
|
||||
constructor({ ui, state, services = {} }) {
|
||||
this.ui = ui;
|
||||
this.state = state;
|
||||
this.services = {
|
||||
sanitizeRelayList: services.sanitizeRelayList || defaultSanitizeRelayList,
|
||||
buildShareEvent: services.buildShareEvent || defaultBuildShareEvent,
|
||||
publishEventToRelays: services.publishEventToRelays || defaultPublishEventToRelays,
|
||||
assertAnyRelayAccepted: services.assertAnyRelayAccepted || defaultAssertAnyRelayAccepted,
|
||||
getActiveSigner: services.getActiveSigner || defaultGetActiveSigner,
|
||||
queueSignEvent: services.queueSignEvent || defaultQueueSignEvent,
|
||||
permissionMethods: services.permissionMethods || defaultPermissionMethods,
|
||||
nostrClient: services.nostrClient || defaultNostrClient,
|
||||
userLogger: services.userLogger || defaultUserLogger,
|
||||
devLogger: services.devLogger || defaultDevLogger,
|
||||
};
|
||||
}
|
||||
|
||||
async openModal({ video, triggerElement } = {}) {
|
||||
const currentVideo = this.state.getCurrentVideo();
|
||||
const targetVideo =
|
||||
video && typeof video === "object" ? video : currentVideo || null;
|
||||
|
||||
if (!targetVideo) {
|
||||
this.ui.showError("No video is available to share.");
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = this.ui.getModal();
|
||||
if (!modal) {
|
||||
this.services.devLogger.warn("[ShareNostrController] Share Nostr modal is unavailable.");
|
||||
this.ui.showError("Share modal is not ready yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
const shareUrl =
|
||||
typeof targetVideo.shareUrl === "string" && targetVideo.shareUrl.trim()
|
||||
? targetVideo.shareUrl.trim()
|
||||
: this.state.buildShareUrlFromEventId(targetVideo.id);
|
||||
|
||||
const payload = {
|
||||
id: targetVideo.id,
|
||||
title: targetVideo.title,
|
||||
pubkey: targetVideo.pubkey,
|
||||
authorName: targetVideo.creatorName || targetVideo.authorName || "",
|
||||
thumbnail: targetVideo.thumbnail,
|
||||
shareUrl,
|
||||
};
|
||||
|
||||
try {
|
||||
await modal.open({
|
||||
video: payload,
|
||||
triggerElement,
|
||||
});
|
||||
} catch (error) {
|
||||
this.services.devLogger.error("[ShareNostrController] Failed to open Share Nostr modal:", error);
|
||||
this.ui.showError("Unable to open the share modal.");
|
||||
}
|
||||
}
|
||||
|
||||
async handleShare(payload = {}) {
|
||||
const video = payload?.video || null;
|
||||
const videoId = typeof video?.id === "string" ? video.id.trim() : "";
|
||||
const videoTitle =
|
||||
typeof video?.title === "string" ? video.title.trim() : "";
|
||||
const videoPubkey =
|
||||
typeof video?.pubkey === "string" ? video.pubkey.trim() : "";
|
||||
|
||||
if (!videoId || !videoTitle) {
|
||||
this.services.userLogger.warn("[ShareNostrController] Share post missing video details.");
|
||||
this.ui.showError("Missing video details for sharing.");
|
||||
throw new Error("share-missing-video-details");
|
||||
}
|
||||
|
||||
const signer = this.services.getActiveSigner();
|
||||
if (!signer || typeof signer.signEvent !== "function") {
|
||||
this.services.userLogger.warn("[ShareNostrController] No active signer available for share.");
|
||||
this.ui.showError("Connect a Nostr signer to share.");
|
||||
throw new Error("share-missing-signer");
|
||||
}
|
||||
|
||||
const pubkey = this.state.getPubkey();
|
||||
const activePubkey = this.state.normalizeHexPubkey(pubkey);
|
||||
const signerPubkey = this.state.normalizeHexPubkey(signer.pubkey);
|
||||
const eventPubkey = activePubkey || signerPubkey;
|
||||
|
||||
if (!eventPubkey) {
|
||||
this.services.userLogger.warn("[ShareNostrController] Share post missing active pubkey.");
|
||||
this.ui.showError("Please log in to share on Nostr.");
|
||||
throw new Error("share-missing-pubkey");
|
||||
}
|
||||
|
||||
if (activePubkey && signerPubkey && activePubkey !== signerPubkey) {
|
||||
this.services.userLogger.error(
|
||||
"[ShareNostrController] Active signer does not match current account for share.",
|
||||
);
|
||||
this.ui.showError("Active signer does not match your account.");
|
||||
throw new Error("share-signer-mismatch");
|
||||
}
|
||||
|
||||
if (!this.services.nostrClient?.pool) {
|
||||
this.services.userLogger.error("[ShareNostrController] Share publish failed: relays not ready.");
|
||||
this.ui.showError("Nostr relays are not ready yet. Please try again.");
|
||||
throw new Error("share-relays-unavailable");
|
||||
}
|
||||
|
||||
const relayEntries = Array.isArray(payload?.relays) ? payload.relays : [];
|
||||
const relayUrls = relayEntries
|
||||
.map((entry) => {
|
||||
if (typeof entry === "string") {
|
||||
return entry;
|
||||
}
|
||||
if (Array.isArray(entry) && entry.length) {
|
||||
if (entry[0] === "r") {
|
||||
return typeof entry[1] === "string" ? entry[1] : "";
|
||||
}
|
||||
return typeof entry[0] === "string" ? entry[0] : "";
|
||||
}
|
||||
if (entry && typeof entry === "object") {
|
||||
if (typeof entry.url === "string") {
|
||||
return entry.url;
|
||||
}
|
||||
if (typeof entry.relay === "string") {
|
||||
return entry.relay;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean);
|
||||
const relayTargets = this.services.sanitizeRelayList(relayUrls);
|
||||
|
||||
if (!relayTargets.length) {
|
||||
this.services.userLogger.warn("[ShareNostrController] Share post missing relay targets.");
|
||||
this.ui.showError("Please choose at least one relay to share to.");
|
||||
throw new Error("share-missing-relays");
|
||||
}
|
||||
|
||||
if (signer.type === "extension" && this.services.nostrClient.ensureExtensionPermissions) {
|
||||
const permissionResult = await this.services.nostrClient.ensureExtensionPermissions(
|
||||
this.services.permissionMethods,
|
||||
);
|
||||
if (!permissionResult?.ok) {
|
||||
this.services.userLogger.warn(
|
||||
"[ShareNostrController] Share publish blocked by signer permissions.",
|
||||
permissionResult?.error,
|
||||
);
|
||||
this.ui.showError("Signer permissions are required to post.");
|
||||
throw new Error("share-permission-denied");
|
||||
}
|
||||
}
|
||||
|
||||
const event = this.services.buildShareEvent({
|
||||
pubkey: eventPubkey,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
content: typeof payload?.content === "string" ? payload.content : "",
|
||||
video: { id: videoId, pubkey: videoPubkey },
|
||||
relays: relayEntries,
|
||||
});
|
||||
|
||||
let signedEvent;
|
||||
try {
|
||||
signedEvent = await this.services.queueSignEvent(signer, event);
|
||||
} catch (error) {
|
||||
this.services.userLogger.error("[ShareNostrController] Failed to sign share event.", error);
|
||||
this.ui.showError("Unable to sign the share event.");
|
||||
throw error;
|
||||
}
|
||||
|
||||
const publishResults = await this.services.publishEventToRelays(
|
||||
this.services.nostrClient.pool,
|
||||
relayTargets,
|
||||
signedEvent,
|
||||
);
|
||||
|
||||
let publishSummary;
|
||||
try {
|
||||
publishSummary = this.services.assertAnyRelayAccepted(publishResults, {
|
||||
context: "share note",
|
||||
});
|
||||
} catch (publishError) {
|
||||
if (publishError?.relayFailures?.length) {
|
||||
publishError.relayFailures.forEach(
|
||||
({ url, error: relayError, reason }) => {
|
||||
this.services.userLogger.error(
|
||||
`[ShareNostrController] Relay ${url} rejected share note: ${reason}`,
|
||||
relayError || reason,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
this.ui.showError("Failed to share on Nostr. Please try again.");
|
||||
throw publishError;
|
||||
}
|
||||
|
||||
if (publishSummary.failed.length) {
|
||||
publishSummary.failed.forEach(({ url, error: relayError }) => {
|
||||
const reason =
|
||||
relayError instanceof Error
|
||||
? relayError.message
|
||||
: relayError
|
||||
? String(relayError)
|
||||
: "publish failed";
|
||||
this.services.userLogger.warn(
|
||||
`[ShareNostrController] Relay ${url} did not acknowledge share note: ${reason}`,
|
||||
relayError,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
this.services.userLogger.info(
|
||||
"[ShareNostrController] Share note published.",
|
||||
publishSummary.accepted.map(({ url }) => url),
|
||||
);
|
||||
this.ui.showSuccess("Shared to Nostr!");
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
event: signedEvent,
|
||||
accepted: publishSummary.accepted.map(({ url }) => url),
|
||||
failed: publishSummary.failed.map(({ url }) => url),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import test from 'node:test';
|
||||
import { strict as assert } from 'node:assert';
|
||||
import ShareNostrController from '../js/ui/shareNostrController.js';
|
||||
|
||||
test('ShareNostrController', async (t) => {
|
||||
const mockGetActiveSigner = t.mock.fn(() => ({
|
||||
pubkey: 'signerpubkey',
|
||||
signEvent: async () => ({ id: 'signed_event' }),
|
||||
type: 'extension'
|
||||
}));
|
||||
|
||||
const mockQueueSignEvent = t.mock.fn(async (signer, event) => ({
|
||||
...event,
|
||||
id: 'signed_event',
|
||||
sig: 'signature'
|
||||
}));
|
||||
|
||||
const mockPublishEventToRelays = t.mock.fn(async () => ({
|
||||
accepted: ['wss://relay.example.com'],
|
||||
failed: []
|
||||
}));
|
||||
|
||||
const mockAssertAnyRelayAccepted = t.mock.fn((summary) => summary);
|
||||
|
||||
const mockSanitizeRelayList = t.mock.fn((list) => list);
|
||||
|
||||
const mockBuildShareEvent = t.mock.fn(({ pubkey, content, video }) => ({
|
||||
kind: 1,
|
||||
pubkey,
|
||||
content,
|
||||
tags: [['e', video.id], ['p', video.pubkey]]
|
||||
}));
|
||||
|
||||
const mockNostrClient = {
|
||||
pool: {},
|
||||
ensureExtensionPermissions: async () => ({ ok: true })
|
||||
};
|
||||
|
||||
const mockLogger = {
|
||||
warn: t.mock.fn(),
|
||||
error: t.mock.fn(),
|
||||
info: t.mock.fn(),
|
||||
log: t.mock.fn()
|
||||
};
|
||||
|
||||
const services = {
|
||||
sanitizeRelayList: mockSanitizeRelayList,
|
||||
buildShareEvent: mockBuildShareEvent,
|
||||
publishEventToRelays: mockPublishEventToRelays,
|
||||
assertAnyRelayAccepted: mockAssertAnyRelayAccepted,
|
||||
getActiveSigner: mockGetActiveSigner,
|
||||
queueSignEvent: mockQueueSignEvent,
|
||||
permissionMethods: {},
|
||||
nostrClient: mockNostrClient,
|
||||
userLogger: mockLogger,
|
||||
devLogger: mockLogger
|
||||
};
|
||||
|
||||
await t.test('handleShare shares video successfully', async () => {
|
||||
const ui = {
|
||||
showError: t.mock.fn(),
|
||||
showSuccess: t.mock.fn(),
|
||||
getModal: () => ({})
|
||||
};
|
||||
const state = {
|
||||
getPubkey: () => 'signerpubkey', // Match the signer pubkey
|
||||
normalizeHexPubkey: (k) => k,
|
||||
getCurrentVideo: () => ({ id: 'video1', title: 'Video 1' })
|
||||
};
|
||||
|
||||
const controller = new ShareNostrController({ ui, state, services });
|
||||
|
||||
const payload = {
|
||||
video: { id: 'video1', title: 'Video 1', pubkey: 'creator1' },
|
||||
content: 'Check this out',
|
||||
relays: ['wss://relay.example.com']
|
||||
};
|
||||
|
||||
const result = await controller.handleShare(payload);
|
||||
|
||||
assert.ok(result.ok);
|
||||
assert.equal(ui.showSuccess.mock.calls.length, 1);
|
||||
assert.equal(ui.showError.mock.calls.length, 0);
|
||||
});
|
||||
|
||||
await t.test('handleShare throws error if missing video details', async () => {
|
||||
const ui = {
|
||||
showError: t.mock.fn(),
|
||||
showSuccess: t.mock.fn(),
|
||||
getModal: () => ({})
|
||||
};
|
||||
const state = {
|
||||
getPubkey: () => 'userpubkey',
|
||||
normalizeHexPubkey: (k) => k
|
||||
};
|
||||
const controller = new ShareNostrController({ ui, state, services });
|
||||
|
||||
const payload = {
|
||||
video: { id: '', title: '' } // Invalid
|
||||
};
|
||||
|
||||
await assert.rejects(async () => {
|
||||
await controller.handleShare(payload);
|
||||
}, /share-missing-video-details/);
|
||||
|
||||
assert.equal(ui.showError.mock.calls.length, 1);
|
||||
});
|
||||
|
||||
await t.test('openModal shows error if no video', async () => {
|
||||
const ui = {
|
||||
showError: t.mock.fn(),
|
||||
getModal: () => ({})
|
||||
};
|
||||
const state = {
|
||||
getCurrentVideo: () => null
|
||||
};
|
||||
const controller = new ShareNostrController({ ui, state, services });
|
||||
|
||||
await controller.openModal({ video: null });
|
||||
assert.equal(ui.showError.mock.calls.length, 1);
|
||||
assert.match(ui.showError.mock.calls[0].arguments[0], /No video/);
|
||||
});
|
||||
|
||||
await t.test('openModal opens modal with correct payload', async () => {
|
||||
const modalMock = {
|
||||
open: t.mock.fn(async () => {})
|
||||
};
|
||||
const ui = {
|
||||
showError: t.mock.fn(),
|
||||
getModal: () => modalMock
|
||||
};
|
||||
const video = { id: 'v1', title: 'T1', pubkey: 'p1' };
|
||||
const state = {
|
||||
getCurrentVideo: () => video,
|
||||
buildShareUrlFromEventId: (id) => `https://example.com/${id}`
|
||||
};
|
||||
const controller = new ShareNostrController({ ui, state, services });
|
||||
|
||||
await controller.openModal();
|
||||
|
||||
assert.equal(modalMock.open.mock.calls.length, 1);
|
||||
const openArg = modalMock.open.mock.calls[0].arguments[0];
|
||||
assert.equal(openArg.video.id, 'v1');
|
||||
assert.equal(openArg.video.shareUrl, 'https://example.com/v1');
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 519 KiB |
Reference in new issue
Block a user