mirror of
https://github.com/bitsocialnet/5chan.git
synced 2026-08-03 07:41:04 +02:00
test: expand honest whole-repo coverage
Adds broad coverage across stores, hooks, and runtime utilities while switching `vitest.config.ts` to an explicit whole-repo include list. This turns the coverage report into a real repo-wide baseline instead of an imported-file subset.
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const testState = vi.hoisted(() => {
|
||||
const i18next = {
|
||||
init: vi.fn(),
|
||||
use: vi.fn(),
|
||||
};
|
||||
i18next.use.mockImplementation(() => i18next);
|
||||
|
||||
return {
|
||||
backendPlugin: { name: 'backend-plugin' },
|
||||
detectorPlugin: { name: 'detector-plugin' },
|
||||
i18next,
|
||||
reactPlugin: { name: 'react-plugin' },
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('i18next', () => ({
|
||||
default: testState.i18next,
|
||||
}));
|
||||
|
||||
vi.mock('i18next-http-backend', () => ({
|
||||
default: testState.backendPlugin,
|
||||
}));
|
||||
|
||||
vi.mock('i18next-browser-languagedetector', () => ({
|
||||
default: testState.detectorPlugin,
|
||||
}));
|
||||
|
||||
vi.mock('react-i18next', () => ({
|
||||
initReactI18next: testState.reactPlugin,
|
||||
}));
|
||||
|
||||
describe('init-translations', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
testState.i18next.use.mockClear();
|
||||
testState.i18next.init.mockClear();
|
||||
testState.i18next.use.mockImplementation(() => testState.i18next);
|
||||
});
|
||||
|
||||
it('initializes i18next with the backend, detector, react plugin, and supported languages', async () => {
|
||||
await import('../init-translations');
|
||||
|
||||
expect(testState.i18next.use).toHaveBeenNthCalledWith(1, testState.backendPlugin);
|
||||
expect(testState.i18next.use).toHaveBeenNthCalledWith(2, testState.detectorPlugin);
|
||||
expect(testState.i18next.use).toHaveBeenNthCalledWith(3, testState.reactPlugin);
|
||||
expect(testState.i18next.init).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fallbackLng: 'en',
|
||||
ns: ['default'],
|
||||
defaultNS: 'default',
|
||||
backend: { loadPath: './translations/{{lng}}/{{ns}}.json' },
|
||||
}),
|
||||
);
|
||||
expect(testState.i18next.init.mock.calls[0]?.[0]?.supportedLngs).toContain('en');
|
||||
expect(testState.i18next.init.mock.calls[0]?.[0]?.supportedLngs).toContain('ja');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
describe('snow', () => {
|
||||
beforeEach(() => {
|
||||
document.head.innerHTML = '';
|
||||
document.body.innerHTML = '';
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('creates and removes a deterministic snow field', async () => {
|
||||
const mathRandomSpy = vi.spyOn(Math, 'random').mockReturnValue(0.5);
|
||||
const { initSnow, removeSnow } = await import('../snow');
|
||||
|
||||
initSnow({ flakeCount: 3 });
|
||||
|
||||
const snowfield = document.getElementById('js-snowfield');
|
||||
expect(snowfield).toBeTruthy();
|
||||
expect(snowfield?.children).toHaveLength(2);
|
||||
expect(document.head.querySelector('style')?.textContent).toContain('fall-1');
|
||||
|
||||
removeSnow();
|
||||
expect(document.getElementById('js-snowfield')).toBeNull();
|
||||
|
||||
mathRandomSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('prefers the special theme store and otherwise falls back to christmas dates', async () => {
|
||||
const useSpecialThemeStore = (await import('../../stores/use-special-theme-store')).default;
|
||||
const { shouldShowSnow } = await import('../snow');
|
||||
|
||||
useSpecialThemeStore.setState({ isEnabled: true });
|
||||
expect(shouldShowSnow()).toBe(true);
|
||||
|
||||
useSpecialThemeStore.setState({ isEnabled: false });
|
||||
expect(shouldShowSnow()).toBe(false);
|
||||
|
||||
useSpecialThemeStore.setState({ isEnabled: null });
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-12-24T00:00:00Z'));
|
||||
expect(shouldShowSnow()).toBe(true);
|
||||
|
||||
vi.setSystemTime(new Date('2024-07-04T00:00:00Z'));
|
||||
expect(shouldShowSnow()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
describe('update-favicon', () => {
|
||||
beforeEach(() => {
|
||||
document.head.innerHTML = '';
|
||||
document.body.innerHTML = '';
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('creates a favicon link and replaces it only when the target icon changes', async () => {
|
||||
const { updateFavicon } = await import('../update-favicon');
|
||||
|
||||
updateFavicon(false);
|
||||
expect(document.querySelectorAll('link[rel="icon"]')).toHaveLength(1);
|
||||
expect(document.querySelector('link[rel="icon"]')?.getAttribute('href')).toBe('/favicon.ico');
|
||||
|
||||
updateFavicon(false);
|
||||
expect(document.querySelectorAll('link[rel="icon"]')).toHaveLength(1);
|
||||
|
||||
updateFavicon(true);
|
||||
expect(document.querySelectorAll('link[rel="icon"]')).toHaveLength(1);
|
||||
expect(document.querySelector('link[rel="icon"]')?.getAttribute('href')).toBe('/favicon2.ico');
|
||||
});
|
||||
|
||||
it('marks only non-special, non-routing aggregate sfw boards as sfw', async () => {
|
||||
const { isSfwBoard } = await import('../update-favicon');
|
||||
|
||||
expect(
|
||||
isSfwBoard({
|
||||
pathname: '/',
|
||||
isSpecialTheme: false,
|
||||
isInAllView: false,
|
||||
isInSubscriptionsView: false,
|
||||
isInModView: false,
|
||||
subplebbitAddress: 'music.eth',
|
||||
directories: [{ address: 'music.eth', nsfw: false }],
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
isSfwBoard({
|
||||
pathname: '/music.eth',
|
||||
isSpecialTheme: false,
|
||||
isInAllView: false,
|
||||
isInSubscriptionsView: false,
|
||||
isInModView: false,
|
||||
subplebbitAddress: 'music.eth',
|
||||
directories: [
|
||||
{ address: 'music.eth', nsfw: false },
|
||||
{ address: 'flash.eth', nsfw: true },
|
||||
],
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
isSfwBoard({
|
||||
pathname: '/flash.eth',
|
||||
isSpecialTheme: false,
|
||||
isInAllView: false,
|
||||
isInSubscriptionsView: false,
|
||||
isInModView: false,
|
||||
subplebbitAddress: 'flash.eth',
|
||||
directories: [{ address: 'flash.eth', nsfw: true }],
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { BLOTTER_PREVIEW_COUNT, formatBlotterDate, getBlotterPreview, isBlotterEntry, sortBlotterEntries } from '../blotter-utils';
|
||||
|
||||
describe('blotter-utils', () => {
|
||||
it('validates blotter entries and rejects malformed release entries', () => {
|
||||
expect(
|
||||
isBlotterEntry({
|
||||
id: 'release-1',
|
||||
kind: 'release',
|
||||
timestamp: 1_700_000_000,
|
||||
message: 'Released',
|
||||
version: '1.0.0',
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(isBlotterEntry({ id: '', kind: 'manual', timestamp: 1, message: 'note' })).toBe(false);
|
||||
expect(isBlotterEntry({ id: 'manual-1', kind: 'release', timestamp: 1, message: 'missing version' })).toBe(false);
|
||||
expect(isBlotterEntry({ id: 'manual-2', kind: 'manual', timestamp: -1, message: 'bad time' })).toBe(false);
|
||||
});
|
||||
|
||||
it('sorts entries descending by timestamp and slices previews', () => {
|
||||
const entries = [
|
||||
{ id: 'a', timestamp: 10 },
|
||||
{ id: 'b', timestamp: 30 },
|
||||
{ id: 'c', timestamp: 20 },
|
||||
{ id: 'd', timestamp: 5 },
|
||||
];
|
||||
|
||||
expect(sortBlotterEntries(entries).map((entry) => entry.id)).toEqual(['b', 'c', 'a', 'd']);
|
||||
expect(getBlotterPreview(entries).length).toBe(BLOTTER_PREVIEW_COUNT);
|
||||
expect(getBlotterPreview(entries, 2).map((entry) => entry.id)).toEqual(['a', 'b']);
|
||||
});
|
||||
|
||||
it('formats unix timestamps and rejects invalid values', () => {
|
||||
expect(formatBlotterDate(1_704_153_600)).toBe('01/02/24');
|
||||
expect(formatBlotterDate(-1)).toBe('');
|
||||
expect(formatBlotterDate(Number.NaN)).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,196 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { copyToClipboard } from '../clipboard-utils';
|
||||
import { hashStringToColor, getTextColorForBackground, removeMarkdown } from '../post-utils';
|
||||
import { preloadThemeAssets } from '../preload-utils';
|
||||
import { computeOmittedCount, getPreviewDisplayReplies, getTotalReplyCount } from '../replies-preview-utils';
|
||||
import { getQuotedCidsFromContent, mergeQuotedCids } from '../reply-quote-utils';
|
||||
import { formatUserIDForDisplay, truncateWithEllipsisInMiddle } from '../string-utils';
|
||||
import { getFormattedDate, getFormattedTimeAgo, isChristmas } from '../time-utils';
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
language: 'en',
|
||||
translateMock: vi.fn((key: string, options?: { count?: number }) => (typeof options?.count === 'number' ? `${key}:${options.count}` : key)),
|
||||
}));
|
||||
|
||||
vi.mock('i18next', () => ({
|
||||
default: {
|
||||
get language() {
|
||||
return testState.language;
|
||||
},
|
||||
t: (key: string, options?: { count?: number }) => testState.translateMock(key, options),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../generated/asset-manifest', () => ({
|
||||
THEME_BUTTON_IMAGES: ['buttons/default.png', 'buttons/hover.png'],
|
||||
THEME_BACKGROUND_IMAGES: ['backgrounds/wallpaper.png'],
|
||||
}));
|
||||
|
||||
describe('misc utils', () => {
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
let dateTimeFormatSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
testState.language = 'en';
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
dateTimeFormatSpy = vi.spyOn(Intl, 'DateTimeFormat').mockImplementation(function MockDateTimeFormat(..._args: unknown[]) {
|
||||
return {
|
||||
format: () => '01/02/24, Tue, 03:04:05',
|
||||
} as Intl.DateTimeFormat;
|
||||
} as unknown as typeof Intl.DateTimeFormat);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleErrorSpy.mockRestore();
|
||||
dateTimeFormatSpy.mockRestore();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('copies through Electron first and falls back to the browser clipboard on Electron failure', async () => {
|
||||
const electronCopy = vi.fn().mockResolvedValue({ success: true });
|
||||
const webCopy = vi.fn().mockResolvedValue(undefined);
|
||||
|
||||
Object.defineProperty(window, 'electronApi', {
|
||||
configurable: true,
|
||||
value: { copyToClipboard: electronCopy },
|
||||
});
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText: webCopy },
|
||||
});
|
||||
|
||||
await copyToClipboard('hello');
|
||||
expect(electronCopy).toHaveBeenCalledWith('hello');
|
||||
expect(webCopy).not.toHaveBeenCalled();
|
||||
|
||||
electronCopy.mockResolvedValueOnce({ success: false, error: 'denied' });
|
||||
await copyToClipboard('fallback');
|
||||
expect(webCopy).toHaveBeenCalledWith('fallback');
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Electron clipboard failed:', expect.any(Error));
|
||||
});
|
||||
|
||||
it('throws clear clipboard errors when no supported API succeeds', async () => {
|
||||
Object.defineProperty(window, 'electronApi', {
|
||||
configurable: true,
|
||||
value: undefined,
|
||||
});
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText: vi.fn().mockRejectedValue(new Error('blocked')) },
|
||||
});
|
||||
|
||||
await expect(copyToClipboard('hello')).rejects.toThrow('Failed to copy to clipboard. Your browser may not support this feature.');
|
||||
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: undefined,
|
||||
});
|
||||
|
||||
await expect(copyToClipboard('hello')).rejects.toThrow('Your browser does not support clipboard API');
|
||||
});
|
||||
|
||||
it('formats post colors and strips markdown syntax while keeping user-visible text', () => {
|
||||
expect(hashStringToColor('abc')).toMatch(/^rgb\(-?\d+, -?\d+, -?\d+\)$/);
|
||||
expect(hashStringToColor('')).toBe('');
|
||||
expect(getTextColorForBackground('rgb(255, 255, 255)')).toBe('black');
|
||||
expect(getTextColorForBackground('rgb(10, 10, 10)')).toBe('white');
|
||||
expect(removeMarkdown('[spoiler]secret[/spoiler]\n>greentext\n**bold** [label](https://example.com) `code` ```block``` ')).toBe(
|
||||
'secret\ngreentext\nbold label code block',
|
||||
);
|
||||
});
|
||||
|
||||
it('preloads theme asset URLs through Image instances', () => {
|
||||
const loadedSources: string[] = [];
|
||||
|
||||
class FakeImage {
|
||||
set src(value: string) {
|
||||
loadedSources.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(globalThis, 'Image', {
|
||||
configurable: true,
|
||||
value: FakeImage,
|
||||
});
|
||||
|
||||
preloadThemeAssets();
|
||||
|
||||
expect(loadedSources).toEqual(['/buttons/default.png', '/buttons/hover.png', '/backgrounds/wallpaper.png']);
|
||||
});
|
||||
|
||||
it('builds reply previews, omitted counts, and fallback reply totals', () => {
|
||||
expect(
|
||||
getPreviewDisplayReplies(
|
||||
[
|
||||
{ cid: 'old', timestamp: 1 },
|
||||
{ cid: 'pending', pendingApproval: true },
|
||||
{ cid: 'middle', timestamp: 5 },
|
||||
{ cid: 'draft', index: 99 },
|
||||
{ cid: 'newest', timestamp: 10 },
|
||||
],
|
||||
4,
|
||||
),
|
||||
).toEqual([
|
||||
{ cid: 'middle', timestamp: 5 },
|
||||
{ cid: 'newest', timestamp: 10 },
|
||||
{ cid: 'draft', index: 99 },
|
||||
{ cid: 'pending', pendingApproval: true },
|
||||
]);
|
||||
|
||||
expect(computeOmittedCount({ totalReplyCount: 2, visibleCount: 5 })).toBe(0);
|
||||
expect(computeOmittedCount({ totalReplyCount: 9, visibleCount: 5 })).toBe(4);
|
||||
expect(getTotalReplyCount({ replyCount: undefined, fullLoadedCount: 7, previewLoadedCount: 5 })).toBe(7);
|
||||
expect(getTotalReplyCount({ replyCount: 12, fullLoadedCount: 7, previewLoadedCount: 5 })).toBe(12);
|
||||
});
|
||||
|
||||
it('extracts quoted cids and merges quoted cid payloads without duplicates', () => {
|
||||
expect(getQuotedCidsFromContent('>>12 >>45 >>12', { 12: 'cid-12', 45: 'cid-45' })).toEqual(['cid-12', 'cid-45']);
|
||||
expect(getQuotedCidsFromContent(undefined, { 12: 'cid-12' })).toBeUndefined();
|
||||
expect(
|
||||
mergeQuotedCids(
|
||||
{
|
||||
author: { address: '0x123' },
|
||||
quotedCids: ['cid-12'],
|
||||
},
|
||||
['cid-12', 'cid-45'],
|
||||
),
|
||||
).toEqual({
|
||||
author: { address: '0x123' },
|
||||
quotedCids: ['cid-12', 'cid-45'],
|
||||
});
|
||||
expect(mergeQuotedCids(undefined, ['cid-12'])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('formats ids, truncates long strings, and localizes time labels', () => {
|
||||
expect(formatUserIDForDisplay('board.eth')).toBe('board.eth');
|
||||
expect(formatUserIDForDisplay('averyverylongdomainname.eth', 12)).toBe('averyvery...');
|
||||
expect(formatUserIDForDisplay('0123456789abcdef')).toBe('01234567');
|
||||
expect(truncateWithEllipsisInMiddle('abcdefghijklmnopqrstuvwxyz', 10)).toBe('abc...xyz');
|
||||
expect(truncateWithEllipsisInMiddle('abc', 10)).toBe('abc');
|
||||
|
||||
expect(getFormattedDate(1_704_153_600)).toBe('01/02/24(Tue)03:04:05');
|
||||
testState.language = 'ar';
|
||||
expect(getFormattedDate(1_704_153_600)).toBe('01/02/24, Tue, 03:04:05');
|
||||
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2024-01-02T00:00:00Z'));
|
||||
|
||||
expect(getFormattedTimeAgo(1_704_153_540)).toBe('time_1_minute_ago');
|
||||
expect(getFormattedTimeAgo(1_704_153_200)).toBe('time_x_minutes_ago:6');
|
||||
expect(getFormattedTimeAgo(1_704_146_800)).toBe('time_1_hour_ago');
|
||||
expect(getFormattedTimeAgo(1_704_139_200)).toBe('time_x_hours_ago:4');
|
||||
expect(getFormattedTimeAgo(1_704_067_200)).toBe('time_1_day_ago');
|
||||
expect(getFormattedTimeAgo(1_703_980_800)).toBe('time_x_days_ago:2');
|
||||
expect(getFormattedTimeAgo(1_701_561_600)).toBe('time_1_month_ago');
|
||||
expect(getFormattedTimeAgo(1_698_969_600)).toBe('time_x_months_ago:2');
|
||||
expect(getFormattedTimeAgo(1_672_617_600)).toBe('time_1_year_ago');
|
||||
expect(getFormattedTimeAgo(1_641_081_600)).toBe('time_x_years_ago:2');
|
||||
|
||||
vi.setSystemTime(new Date('2024-12-24T00:00:00Z'));
|
||||
expect(isChristmas()).toBe(true);
|
||||
|
||||
vi.setSystemTime(new Date('2024-07-04T00:00:00Z'));
|
||||
expect(isChristmas()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const testState = vi.hoisted(() => ({
|
||||
copyToClipboardMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../clipboard-utils', () => ({
|
||||
copyToClipboard: (text: string) => testState.copyToClipboardMock(text),
|
||||
}));
|
||||
|
||||
import { copyShareLinkToClipboard, getHostname, is5chanLink, isValidCrossboardPattern, isValidURL, transform5chanLinkToInternal } from '../url-utils';
|
||||
|
||||
describe('url-utils', () => {
|
||||
beforeEach(() => {
|
||||
testState.copyToClipboardMock.mockReset();
|
||||
});
|
||||
|
||||
it('extracts hostnames and validates urls', () => {
|
||||
expect(getHostname('https://www.5chan.app/#/music.eth')).toBe('5chan.app');
|
||||
expect(getHostname('not-a-url')).toBe('');
|
||||
expect(isValidURL('https://5chan.app')).toBe(true);
|
||||
expect(isValidURL('not-a-url')).toBe(false);
|
||||
});
|
||||
|
||||
it('copies share links for threads and catalog pages using the production fallback base url', async () => {
|
||||
await copyShareLinkToClipboard('music.eth', 'thread', 'cid-123');
|
||||
expect(testState.copyToClipboardMock).toHaveBeenCalledWith('https://5chan.app/#/music.eth/thread/cid-123');
|
||||
|
||||
await copyShareLinkToClipboard('music.eth', 'catalog');
|
||||
expect(testState.copyToClipboardMock).toHaveBeenCalledWith('https://5chan.app/#/music.eth/catalog');
|
||||
|
||||
const copyThreadWithoutCid = copyShareLinkToClipboard as (boardIdentifier: string, linkType: 'thread', cid?: string) => Promise<void>;
|
||||
await expect(copyThreadWithoutCid('music.eth', 'thread')).rejects.toThrow('copyShareLinkToClipboard: thread links require a cid');
|
||||
});
|
||||
|
||||
it('recognizes supported 5chan urls and rejects unrelated domains', () => {
|
||||
expect(is5chanLink('https://5chan.app/music.eth')).toBe(true);
|
||||
expect(is5chanLink('https://5chan.app/music.eth/thread/cid-123')).toBe(true);
|
||||
expect(is5chanLink('https://5chan.app/#/music.eth/catalog')).toBe(true);
|
||||
expect(is5chanLink('https://5chan.app/p/music.eth/c/cid-123')).toBe(true);
|
||||
expect(is5chanLink('https://5chan.app/all/catalog')).toBe(true);
|
||||
expect(is5chanLink('https://example.com/music.eth')).toBe(false);
|
||||
});
|
||||
|
||||
it('transforms legacy and hash-based share links into internal routes', () => {
|
||||
expect(transform5chanLinkToInternal('https://5chan.app/p/music.eth/c/cid-123?redirect=https://example.com')).toBe('/music.eth/thread/cid-123');
|
||||
expect(transform5chanLinkToInternal('https://5chan.app/p/music.eth?foo=1')).toBe('/music.eth?foo=1');
|
||||
expect(transform5chanLinkToInternal('https://5chan.app/#/music.eth/catalog')).toBe('/music.eth/catalog');
|
||||
expect(transform5chanLinkToInternal('https://example.com/music.eth')).toBeNull();
|
||||
});
|
||||
|
||||
it('validates cross-board quote patterns for board codes, domains, and ipns keys', () => {
|
||||
const ipnsKey = `12D3KooW${'a'.repeat(44)}`;
|
||||
|
||||
expect(isValidCrossboardPattern('>>>/biz/')).toBe(true);
|
||||
expect(isValidCrossboardPattern(`>>>/biz/${'a'.repeat(46)}`)).toBe(true);
|
||||
expect(isValidCrossboardPattern(`>>>/board.eth/${'b'.repeat(46)}`)).toBe(true);
|
||||
expect(isValidCrossboardPattern(`>>>/${ipnsKey}`)).toBe(true);
|
||||
expect(isValidCrossboardPattern('>>>/invalid/thread-with-short-cid')).toBe(false);
|
||||
expect(isValidCrossboardPattern('>>/biz/')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
isAllView,
|
||||
isBoardView,
|
||||
isCatalogView,
|
||||
isHomeView,
|
||||
isModQueueView,
|
||||
isModView,
|
||||
isNotFoundView,
|
||||
isPendingPostView,
|
||||
isPostPageView,
|
||||
isSettingsView,
|
||||
isSubscriptionsView,
|
||||
} from '../view-utils';
|
||||
|
||||
describe('view-utils', () => {
|
||||
it('classifies aggregate routes and special app views', () => {
|
||||
expect(isAllView('/all')).toBe(true);
|
||||
expect(isHomeView('/')).toBe(true);
|
||||
expect(isModView('/mod/queue')).toBe(true);
|
||||
expect(isModQueueView('/music.eth/mod/queue')).toBe(true);
|
||||
expect(isSubscriptionsView('/subs/catalog/settings', {})).toBe(true);
|
||||
expect(isPendingPostView('/pending/42/settings', { accountCommentIndex: '42' })).toBe(true);
|
||||
});
|
||||
|
||||
it('detects board, catalog, post, and settings routes using board params', () => {
|
||||
const params = {
|
||||
boardIdentifier: 'music.eth',
|
||||
commentCid: 'cid-123',
|
||||
accountCommentIndex: '42',
|
||||
};
|
||||
|
||||
expect(isBoardView('/music.eth', params)).toBe(true);
|
||||
expect(isBoardView('/all', params)).toBe(false);
|
||||
expect(isCatalogView('/music.eth/catalog', params)).toBe(true);
|
||||
expect(isPostPageView('/music.eth/thread/cid-123', params)).toBe(true);
|
||||
expect(isSettingsView('/music.eth/thread/cid-123/settings', params)).toBe(true);
|
||||
});
|
||||
|
||||
it('supports deprecated subplebbitAddress params and marks unknown routes as not found', () => {
|
||||
const params = {
|
||||
subplebbitAddress: 'emoji-🎵.eth',
|
||||
commentCid: 'cid-123',
|
||||
};
|
||||
|
||||
expect(isBoardView('/emoji-%F0%9F%8E%B5.eth', params)).toBe(true);
|
||||
expect(isCatalogView('/emoji-%F0%9F%8E%B5.eth/catalog/settings', params)).toBe(true);
|
||||
expect(isPostPageView('/emoji-%F0%9F%8E%B5.eth/thread/cid-123', params)).toBe(true);
|
||||
expect(isNotFoundView('/definitely-not-a-route', params)).toBe(true);
|
||||
expect(isNotFoundView('/emoji-%F0%9F%8E%B5.eth/thread/cid-123', params)).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user