mirror of
https://github.com/hoornet/vega.git
synced 2026-05-11 14:41:18 -07:00
Bump to v0.7.1 — relay health checker, advanced search
This commit is contained in:
109
src/stores/feed.test.ts
Normal file
109
src/stores/feed.test.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { NDKEvent } from "@nostr-dev-kit/ndk";
|
||||
|
||||
// Mock the nostr module
|
||||
vi.mock("../lib/nostr", () => ({
|
||||
connectToRelays: vi.fn(),
|
||||
fetchGlobalFeed: vi.fn(),
|
||||
fetchBatchEngagement: vi.fn(),
|
||||
getNDK: vi.fn(() => ({ pool: { relays: new Map() } })),
|
||||
}));
|
||||
|
||||
// Mock the db module
|
||||
vi.mock("../lib/db", () => ({
|
||||
dbLoadFeed: vi.fn().mockResolvedValue([]),
|
||||
dbSaveNotes: vi.fn(),
|
||||
}));
|
||||
|
||||
import { useFeedStore } from "./feed";
|
||||
import { fetchGlobalFeed, fetchBatchEngagement } from "../lib/nostr";
|
||||
|
||||
function makeMockNote(id: string, created_at: number): NDKEvent {
|
||||
const event = { id, created_at, content: "test", kind: 1, pubkey: "pk", tags: [], sig: "", rawEvent: () => ({ id, created_at, content: "test", kind: 1, pubkey: "pk", tags: [], sig: "" }) } as unknown as NDKEvent;
|
||||
return event;
|
||||
}
|
||||
|
||||
describe("useFeedStore - loadTrendingFeed", () => {
|
||||
beforeEach(() => {
|
||||
useFeedStore.setState({
|
||||
notes: [],
|
||||
trendingNotes: [],
|
||||
trendingLoading: false,
|
||||
loading: false,
|
||||
connected: false,
|
||||
error: null,
|
||||
focusedNoteIndex: -1,
|
||||
});
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
it("scores and sorts notes by engagement", async () => {
|
||||
const notes = [
|
||||
makeMockNote("a", 1000),
|
||||
makeMockNote("b", 1001),
|
||||
makeMockNote("c", 1002),
|
||||
];
|
||||
|
||||
const engagement = new Map([
|
||||
["a", { reactions: 10, replies: 0, zapSats: 0 }], // score: 10
|
||||
["b", { reactions: 0, replies: 5, zapSats: 0 }], // score: 15
|
||||
["c", { reactions: 1, replies: 1, zapSats: 100 }], // score: 5
|
||||
]);
|
||||
|
||||
vi.mocked(fetchGlobalFeed).mockResolvedValue(notes);
|
||||
vi.mocked(fetchBatchEngagement).mockResolvedValue(engagement);
|
||||
|
||||
await useFeedStore.getState().loadTrendingFeed(true);
|
||||
|
||||
const trending = useFeedStore.getState().trendingNotes;
|
||||
expect(trending).toHaveLength(3);
|
||||
expect(trending[0].id).toBe("b"); // highest score: 15
|
||||
expect(trending[1].id).toBe("a"); // score: 10
|
||||
expect(trending[2].id).toBe("c"); // score: 5
|
||||
});
|
||||
|
||||
it("filters out notes with zero engagement", async () => {
|
||||
const notes = [
|
||||
makeMockNote("a", 1000),
|
||||
makeMockNote("b", 1001),
|
||||
];
|
||||
|
||||
const engagement = new Map([
|
||||
["a", { reactions: 5, replies: 0, zapSats: 0 }],
|
||||
["b", { reactions: 0, replies: 0, zapSats: 0 }],
|
||||
]);
|
||||
|
||||
vi.mocked(fetchGlobalFeed).mockResolvedValue(notes);
|
||||
vi.mocked(fetchBatchEngagement).mockResolvedValue(engagement);
|
||||
|
||||
await useFeedStore.getState().loadTrendingFeed(true);
|
||||
|
||||
const trending = useFeedStore.getState().trendingNotes;
|
||||
expect(trending).toHaveLength(1);
|
||||
expect(trending[0].id).toBe("a");
|
||||
});
|
||||
|
||||
it("limits results to 50", async () => {
|
||||
const notes = Array.from({ length: 60 }, (_, i) => makeMockNote(`n${i}`, i));
|
||||
const engagement = new Map(
|
||||
notes.map((n) => [n.id, { reactions: 10, replies: 1, zapSats: 0 }])
|
||||
);
|
||||
|
||||
vi.mocked(fetchGlobalFeed).mockResolvedValue(notes);
|
||||
vi.mocked(fetchBatchEngagement).mockResolvedValue(engagement);
|
||||
|
||||
await useFeedStore.getState().loadTrendingFeed(true);
|
||||
|
||||
expect(useFeedStore.getState().trendingNotes).toHaveLength(50);
|
||||
});
|
||||
|
||||
it("handles empty feed gracefully", async () => {
|
||||
vi.mocked(fetchGlobalFeed).mockResolvedValue([]);
|
||||
|
||||
await useFeedStore.getState().loadTrendingFeed(true);
|
||||
|
||||
expect(useFeedStore.getState().trendingNotes).toHaveLength(0);
|
||||
expect(useFeedStore.getState().trendingLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
28
src/stores/relayHealth.ts
Normal file
28
src/stores/relayHealth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { create } from "zustand";
|
||||
import { checkAllRelays, type RelayHealthResult } from "../lib/nostr/relayHealth";
|
||||
import { getStoredRelayUrls } from "../lib/nostr";
|
||||
|
||||
interface RelayHealthState {
|
||||
results: RelayHealthResult[];
|
||||
checking: boolean;
|
||||
lastChecked: number | null;
|
||||
checkAll: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const useRelayHealthStore = create<RelayHealthState>((set, get) => ({
|
||||
results: [],
|
||||
checking: false,
|
||||
lastChecked: null,
|
||||
|
||||
checkAll: async () => {
|
||||
if (get().checking) return;
|
||||
set({ checking: true });
|
||||
try {
|
||||
const urls = getStoredRelayUrls();
|
||||
const results = await checkAllRelays(urls);
|
||||
set({ results, lastChecked: Date.now(), checking: false });
|
||||
} catch {
|
||||
set({ checking: false });
|
||||
}
|
||||
},
|
||||
}));
|
||||
100
src/stores/ui.test.ts
Normal file
100
src/stores/ui.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { useUIStore } from "./ui";
|
||||
|
||||
describe("useUIStore", () => {
|
||||
beforeEach(() => {
|
||||
// Reset store to initial state
|
||||
useUIStore.setState({
|
||||
currentView: "feed",
|
||||
selectedPubkey: null,
|
||||
selectedNote: null,
|
||||
previousView: "feed",
|
||||
feedTab: "global",
|
||||
pendingSearch: null,
|
||||
pendingDMPubkey: null,
|
||||
pendingArticleNaddr: null,
|
||||
showHelp: false,
|
||||
feedLanguageFilter: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("setFeedTab changes the tab", () => {
|
||||
useUIStore.getState().setFeedTab("following");
|
||||
expect(useUIStore.getState().feedTab).toBe("following");
|
||||
});
|
||||
|
||||
it("setFeedTab to trending", () => {
|
||||
useUIStore.getState().setFeedTab("trending");
|
||||
expect(useUIStore.getState().feedTab).toBe("trending");
|
||||
});
|
||||
|
||||
it("openProfile sets pubkey and view", () => {
|
||||
useUIStore.getState().openProfile("abc123");
|
||||
const state = useUIStore.getState();
|
||||
expect(state.currentView).toBe("profile");
|
||||
expect(state.selectedPubkey).toBe("abc123");
|
||||
expect(state.previousView).toBe("feed");
|
||||
});
|
||||
|
||||
it("openThread sets note and previousView", () => {
|
||||
const mockNote = { id: "note1" } as any;
|
||||
useUIStore.getState().openThread(mockNote, "feed");
|
||||
const state = useUIStore.getState();
|
||||
expect(state.currentView).toBe("thread");
|
||||
expect(state.selectedNote).toBe(mockNote);
|
||||
expect(state.previousView).toBe("feed");
|
||||
});
|
||||
|
||||
it("goBack returns to previous view", () => {
|
||||
useUIStore.getState().openProfile("abc123");
|
||||
expect(useUIStore.getState().currentView).toBe("profile");
|
||||
|
||||
useUIStore.getState().goBack();
|
||||
expect(useUIStore.getState().currentView).toBe("feed");
|
||||
expect(useUIStore.getState().selectedNote).toBeNull();
|
||||
});
|
||||
|
||||
it("goBack defaults to feed when previousView equals currentView", () => {
|
||||
// Both are "feed" initially
|
||||
useUIStore.getState().goBack();
|
||||
expect(useUIStore.getState().currentView).toBe("feed");
|
||||
});
|
||||
|
||||
it("openSearch sets pending search and view", () => {
|
||||
useUIStore.getState().openSearch("bitcoin");
|
||||
const state = useUIStore.getState();
|
||||
expect(state.currentView).toBe("search");
|
||||
expect(state.pendingSearch).toBe("bitcoin");
|
||||
});
|
||||
|
||||
it("openDM sets pending DM pubkey", () => {
|
||||
useUIStore.getState().openDM("pubkey123");
|
||||
const state = useUIStore.getState();
|
||||
expect(state.currentView).toBe("dm");
|
||||
expect(state.pendingDMPubkey).toBe("pubkey123");
|
||||
});
|
||||
|
||||
it("setView changes the current view", () => {
|
||||
useUIStore.getState().setView("settings");
|
||||
expect(useUIStore.getState().currentView).toBe("settings");
|
||||
});
|
||||
|
||||
it("toggleHelp toggles showHelp", () => {
|
||||
expect(useUIStore.getState().showHelp).toBe(false);
|
||||
useUIStore.getState().toggleHelp();
|
||||
expect(useUIStore.getState().showHelp).toBe(true);
|
||||
useUIStore.getState().toggleHelp();
|
||||
expect(useUIStore.getState().showHelp).toBe(false);
|
||||
});
|
||||
|
||||
it("setFeedLanguageFilter sets the filter", () => {
|
||||
useUIStore.getState().setFeedLanguageFilter("Latin");
|
||||
expect(useUIStore.getState().feedLanguageFilter).toBe("Latin");
|
||||
});
|
||||
|
||||
it("setFeedLanguageFilter clears with null", () => {
|
||||
useUIStore.getState().setFeedLanguageFilter("Latin");
|
||||
useUIStore.getState().setFeedLanguageFilter(null);
|
||||
expect(useUIStore.getState().feedLanguageFilter).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user