mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 10:36:41 +02:00
feat(shared-stores,shared-ui): add shared reminder system
Add notificationService (Browser Notification API wrapper), createReminderScheduler (30s poller with source pattern for checking due reminders), and ReminderPicker UI component. Todo module gets todoReminderSource (checks task dueDate - minutesBefore) and ReminderSelector now delegates to shared ReminderPicker. Scheduler supports multiple sources (todo, calendar, planta, etc.), tag-based dedup, graceful error handling, and runtime source addition. 22 new tests (8 notification + 8 scheduler + 6 ReminderPicker). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b995d52146
commit
4fa096147c
11 changed files with 624 additions and 26 deletions
88
packages/shared-stores/src/notifications.test.ts
Normal file
88
packages/shared-stores/src/notifications.test.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { notificationService } from './notifications';
|
||||
|
||||
describe('notificationService', () => {
|
||||
const originalNotification = globalThis.Notification;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Notification constructor
|
||||
const MockNotification = vi.fn() as unknown as typeof Notification;
|
||||
Object.defineProperty(MockNotification, 'permission', {
|
||||
get: () => 'granted',
|
||||
configurable: true,
|
||||
});
|
||||
MockNotification.requestPermission = vi.fn().mockResolvedValue('granted');
|
||||
(globalThis as Record<string, unknown>).Notification = MockNotification;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(globalThis as Record<string, unknown>).Notification = originalNotification;
|
||||
});
|
||||
|
||||
describe('isSupported', () => {
|
||||
it('returns true when Notification is available', () => {
|
||||
expect(notificationService.isSupported()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasPermission', () => {
|
||||
it('returns true when permission is granted', () => {
|
||||
expect(notificationService.hasPermission()).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when permission is denied', () => {
|
||||
Object.defineProperty(Notification, 'permission', {
|
||||
get: () => 'denied',
|
||||
configurable: true,
|
||||
});
|
||||
expect(notificationService.hasPermission()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestPermission', () => {
|
||||
it('returns true when permission granted', async () => {
|
||||
const result = await notificationService.requestPermission();
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when permission denied', async () => {
|
||||
Object.defineProperty(Notification, 'permission', {
|
||||
get: () => 'denied',
|
||||
configurable: true,
|
||||
});
|
||||
const result = await notificationService.requestPermission();
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('send', () => {
|
||||
it('creates a Notification with title and body', () => {
|
||||
notificationService.send('Test Title', { body: 'Test Body' });
|
||||
expect(Notification).toHaveBeenCalledWith(
|
||||
'Test Title',
|
||||
expect.objectContaining({
|
||||
body: 'Test Body',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('does nothing without permission', () => {
|
||||
Object.defineProperty(Notification, 'permission', {
|
||||
get: () => 'denied',
|
||||
configurable: true,
|
||||
});
|
||||
notificationService.send('Test');
|
||||
expect(Notification).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes tag for deduplication', () => {
|
||||
notificationService.send('Test', { tag: 'my-tag' });
|
||||
expect(Notification).toHaveBeenCalledWith(
|
||||
'Test',
|
||||
expect.objectContaining({
|
||||
tag: 'my-tag',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue