mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-02 03:23:00 +02:00
241 lines
7.5 KiB
TypeScript
241 lines
7.5 KiB
TypeScript
import { act, renderHook } from '@testing-library/react';
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
getNotificationEnableToastInfo,
|
|
useBrowserNotifications,
|
|
} from '../hooks/useBrowserNotifications';
|
|
import type { Message } from '../types';
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
toast: {
|
|
success: vi.fn(),
|
|
warning: vi.fn(),
|
|
error: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
vi.mock('../components/ui/sonner', () => ({
|
|
toast: mocks.toast,
|
|
}));
|
|
|
|
const incomingChannelMessage: Message = {
|
|
id: 42,
|
|
type: 'CHAN',
|
|
conversation_key: 'ab'.repeat(16),
|
|
text: 'hello room',
|
|
sender_timestamp: 1700000000,
|
|
received_at: 1700000001,
|
|
paths: null,
|
|
txt_type: 0,
|
|
signature: null,
|
|
sender_key: 'cd'.repeat(32),
|
|
outgoing: false,
|
|
acked: 0,
|
|
sender_name: 'Alice',
|
|
channel_name: '#flightless',
|
|
};
|
|
|
|
describe('useBrowserNotifications', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
window.localStorage.clear();
|
|
window.location.hash = '';
|
|
vi.spyOn(window, 'open').mockReturnValue(null);
|
|
|
|
Object.defineProperty(document, 'visibilityState', {
|
|
configurable: true,
|
|
value: 'hidden',
|
|
});
|
|
vi.spyOn(document, 'hasFocus').mockReturnValue(false);
|
|
|
|
const NotificationMock = vi.fn().mockImplementation(function (this: Record<string, unknown>) {
|
|
this.close = vi.fn();
|
|
this.onclick = null;
|
|
});
|
|
Object.assign(NotificationMock, {
|
|
permission: 'granted',
|
|
requestPermission: vi.fn(async () => 'granted'),
|
|
});
|
|
Object.defineProperty(window, 'Notification', {
|
|
configurable: true,
|
|
value: NotificationMock,
|
|
});
|
|
Object.defineProperty(window, 'isSecureContext', {
|
|
configurable: true,
|
|
value: true,
|
|
});
|
|
});
|
|
|
|
it('stores notification opt-in per conversation', async () => {
|
|
const { result } = renderHook(() => useBrowserNotifications());
|
|
|
|
await act(async () => {
|
|
await result.current.toggleConversationNotifications(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key,
|
|
'#flightless'
|
|
);
|
|
});
|
|
|
|
expect(
|
|
result.current.isConversationNotificationsEnabled(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key
|
|
)
|
|
).toBe(true);
|
|
expect(result.current.isConversationNotificationsEnabled('contact', 'ef'.repeat(32))).toBe(
|
|
false
|
|
);
|
|
expect(window.Notification).toHaveBeenCalledWith('New message in #flightless', {
|
|
body: 'Notifications will look like this. These require the tab to stay open, and will not be reliable on mobile.',
|
|
icon: './favicon-256x256.png',
|
|
tag: `meshcore-notification-preview-channel-${incomingChannelMessage.conversation_key}`,
|
|
});
|
|
expect(mocks.toast.warning).toHaveBeenCalledWith('Notifications enabled with warning', {
|
|
description:
|
|
'Desktop notifications are on for this conversation, but you are using HTTP instead of HTTPS. Notifications will likely not work reliably.',
|
|
});
|
|
});
|
|
|
|
it('only sends desktop notifications for opted-in conversations', async () => {
|
|
const { result } = renderHook(() => useBrowserNotifications());
|
|
|
|
await act(async () => {
|
|
await result.current.toggleConversationNotifications(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key,
|
|
'#flightless'
|
|
);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.notifyIncomingMessage(incomingChannelMessage);
|
|
result.current.notifyIncomingMessage({
|
|
...incomingChannelMessage,
|
|
id: 43,
|
|
conversation_key: '34'.repeat(16),
|
|
channel_name: '#elsewhere',
|
|
});
|
|
});
|
|
|
|
expect(window.Notification).toHaveBeenCalledTimes(2);
|
|
expect(window.Notification).toHaveBeenNthCalledWith(2, 'New message in #flightless', {
|
|
body: 'hello room',
|
|
icon: './favicon-256x256.png',
|
|
tag: 'meshcore-message-42',
|
|
});
|
|
});
|
|
|
|
it('notification click deep-links to the conversation hash', async () => {
|
|
const focusSpy = vi.spyOn(window, 'focus').mockImplementation(() => {});
|
|
const { result } = renderHook(() => useBrowserNotifications());
|
|
|
|
await act(async () => {
|
|
await result.current.toggleConversationNotifications(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key,
|
|
'#flightless'
|
|
);
|
|
});
|
|
|
|
act(() => {
|
|
result.current.notifyIncomingMessage(incomingChannelMessage);
|
|
});
|
|
|
|
const notificationInstance = (window.Notification as unknown as ReturnType<typeof vi.fn>).mock
|
|
.instances[1] as {
|
|
onclick: (() => void) | null;
|
|
close: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
act(() => {
|
|
notificationInstance.onclick?.();
|
|
});
|
|
|
|
expect(window.open).toHaveBeenCalledWith(
|
|
`${window.location.origin}${window.location.pathname}#channel/${incomingChannelMessage.conversation_key}/%23flightless`,
|
|
'_self'
|
|
);
|
|
expect(focusSpy).toHaveBeenCalledTimes(1);
|
|
expect(notificationInstance.close).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('shows the browser guidance toast when notifications are blocked', async () => {
|
|
Object.assign(window.Notification, {
|
|
permission: 'denied',
|
|
});
|
|
|
|
const { result } = renderHook(() => useBrowserNotifications());
|
|
|
|
await act(async () => {
|
|
await result.current.toggleConversationNotifications(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key,
|
|
'#flightless'
|
|
);
|
|
});
|
|
|
|
expect(mocks.toast.error).toHaveBeenCalledWith('Notifications blocked', {
|
|
description:
|
|
'Desktop notifications are blocked by your browser. Allow notifications in browser settings, then try again. Non-HTTPS or untrusted HTTPS origins may also prevent notifications from working reliably.',
|
|
});
|
|
});
|
|
|
|
it('shows a warning toast when notifications are enabled on HTTP', async () => {
|
|
const { result } = renderHook(() => useBrowserNotifications());
|
|
|
|
await act(async () => {
|
|
await result.current.toggleConversationNotifications(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key,
|
|
'#flightless'
|
|
);
|
|
});
|
|
|
|
expect(mocks.toast.warning).toHaveBeenCalledWith('Notifications enabled with warning', {
|
|
description:
|
|
'Desktop notifications are on for this conversation, but you are using HTTP instead of HTTPS. Notifications will likely not work reliably.',
|
|
});
|
|
expect(mocks.toast.success).not.toHaveBeenCalledWith('Notifications enabled');
|
|
});
|
|
|
|
it('best-effort detects insecure HTTPS for the enable-warning copy', () => {
|
|
expect(
|
|
getNotificationEnableToastInfo({
|
|
protocol: 'https:',
|
|
isSecureContext: false,
|
|
})
|
|
).toEqual({
|
|
level: 'warning',
|
|
title: 'Notifications enabled with warning',
|
|
description:
|
|
'Desktop notifications are on for this conversation, but your HTTPS connection is untrusted, such as a self-signed certificate. Notification delivery may be inconsistent depending on your browser.',
|
|
});
|
|
});
|
|
|
|
it('shows a descriptive success toast when notifications are disabled', async () => {
|
|
const { result } = renderHook(() => useBrowserNotifications());
|
|
|
|
await act(async () => {
|
|
await result.current.toggleConversationNotifications(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key,
|
|
'#flightless'
|
|
);
|
|
});
|
|
|
|
await act(async () => {
|
|
await result.current.toggleConversationNotifications(
|
|
'channel',
|
|
incomingChannelMessage.conversation_key,
|
|
'#flightless'
|
|
);
|
|
});
|
|
|
|
expect(mocks.toast.success).toHaveBeenCalledWith('Notifications disabled', {
|
|
description: 'Desktop notifications are off for #flightless.',
|
|
});
|
|
});
|
|
});
|