Add tests and deprecation warnings
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
committed by
Patrik Oldsberg
parent
a47f27c0a9
commit
eea95b8ae2
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ToastApiForwarder } from './ToastApiForwarder';
|
||||
|
||||
describe('ToastApiForwarder', () => {
|
||||
let forwarder: ToastApiForwarder;
|
||||
|
||||
beforeEach(() => {
|
||||
forwarder = new ToastApiForwarder();
|
||||
});
|
||||
|
||||
describe('post', () => {
|
||||
it('should return a unique key for each toast', () => {
|
||||
const key1 = forwarder.post({ title: 'Toast 1' });
|
||||
const key2 = forwarder.post({ title: 'Toast 2' });
|
||||
|
||||
expect(key1).toBeDefined();
|
||||
expect(key2).toBeDefined();
|
||||
expect(key1).not.toBe(key2);
|
||||
});
|
||||
|
||||
it('should emit toast to subscribers', () => {
|
||||
const received: Array<{ title: unknown; key: string }> = [];
|
||||
|
||||
forwarder.toast$().subscribe(toast => {
|
||||
received.push(toast);
|
||||
});
|
||||
|
||||
forwarder.post({ title: 'Test Toast', status: 'success' });
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].title).toBe('Test Toast');
|
||||
expect(received[0].key).toBeDefined();
|
||||
});
|
||||
|
||||
it('should include all toast properties in emitted message', () => {
|
||||
const received: Array<{
|
||||
title: unknown;
|
||||
description?: unknown;
|
||||
status?: string;
|
||||
timeout?: number;
|
||||
}> = [];
|
||||
|
||||
forwarder.toast$().subscribe(toast => {
|
||||
received.push(toast);
|
||||
});
|
||||
|
||||
forwarder.post({
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
status: 'warning',
|
||||
timeout: 5000,
|
||||
links: [{ label: 'Link', href: '/test' }],
|
||||
});
|
||||
|
||||
expect(received[0]).toMatchObject({
|
||||
title: 'Title',
|
||||
description: 'Description',
|
||||
status: 'warning',
|
||||
timeout: 5000,
|
||||
links: [{ label: 'Link', href: '/test' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
it('should emit close event to subscribers', () => {
|
||||
const closedKeys: string[] = [];
|
||||
|
||||
forwarder.close$().subscribe(key => {
|
||||
closedKeys.push(key);
|
||||
});
|
||||
|
||||
const key = forwarder.post({ title: 'Test' });
|
||||
forwarder.close(key);
|
||||
|
||||
expect(closedKeys).toHaveLength(1);
|
||||
expect(closedKeys[0]).toBe(key);
|
||||
});
|
||||
|
||||
it('should remove toast from replay buffer', () => {
|
||||
const key = forwarder.post({ title: 'Test' });
|
||||
forwarder.close(key);
|
||||
|
||||
// New subscriber should not receive the closed toast
|
||||
const received: Array<{ key: string }> = [];
|
||||
forwarder.toast$().subscribe(toast => {
|
||||
received.push(toast);
|
||||
});
|
||||
|
||||
expect(received).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toast$ replay', () => {
|
||||
it('should replay recent toasts to new subscribers', async () => {
|
||||
forwarder.post({ title: 'Toast 1' });
|
||||
forwarder.post({ title: 'Toast 2' });
|
||||
|
||||
const received: Array<{ title: unknown }> = [];
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
const subscription = forwarder.toast$().subscribe({
|
||||
next: toast => {
|
||||
received.push(toast);
|
||||
// After receiving replayed toasts, unsubscribe
|
||||
if (received.length === 2) {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
});
|
||||
// Also resolve after a short timeout in case no toasts are replayed
|
||||
setTimeout(() => resolve(), 100);
|
||||
});
|
||||
|
||||
expect(received).toHaveLength(2);
|
||||
expect(received[0].title).toBe('Toast 1');
|
||||
expect(received[1].title).toBe('Toast 2');
|
||||
});
|
||||
|
||||
it('should not replay closed toasts to new subscribers', async () => {
|
||||
const key1 = forwarder.post({ title: 'Toast 1' });
|
||||
forwarder.post({ title: 'Toast 2' });
|
||||
|
||||
forwarder.close(key1);
|
||||
|
||||
const received: Array<{ title: unknown }> = [];
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
const subscription = forwarder.toast$().subscribe({
|
||||
next: toast => {
|
||||
received.push(toast);
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
},
|
||||
});
|
||||
setTimeout(() => resolve(), 100);
|
||||
});
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].title).toBe('Toast 2');
|
||||
});
|
||||
|
||||
it('should limit replay buffer size', async () => {
|
||||
// Post more than maxBufferSize (10) toasts
|
||||
for (let i = 0; i < 15; i++) {
|
||||
forwarder.post({ title: `Toast ${i}` });
|
||||
}
|
||||
|
||||
const received: Array<{ title: unknown }> = [];
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
const subscription = forwarder.toast$().subscribe({
|
||||
next: toast => {
|
||||
received.push(toast);
|
||||
if (received.length === 10) {
|
||||
subscription.unsubscribe();
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
});
|
||||
setTimeout(() => resolve(), 100);
|
||||
});
|
||||
|
||||
// Should only have last 10 toasts
|
||||
expect(received).toHaveLength(10);
|
||||
expect(received[0].title).toBe('Toast 5');
|
||||
expect(received[9].title).toBe('Toast 14');
|
||||
});
|
||||
});
|
||||
|
||||
describe('close$ observable', () => {
|
||||
it('should allow multiple subscribers', () => {
|
||||
const subscriber1: string[] = [];
|
||||
const subscriber2: string[] = [];
|
||||
|
||||
forwarder.close$().subscribe(key => subscriber1.push(key));
|
||||
forwarder.close$().subscribe(key => subscriber2.push(key));
|
||||
|
||||
const key = forwarder.post({ title: 'Test' });
|
||||
forwarder.close(key);
|
||||
|
||||
expect(subscriber1).toEqual([key]);
|
||||
expect(subscriber2).toEqual([key]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('subscription cleanup', () => {
|
||||
it('should stop receiving toasts after unsubscribe', () => {
|
||||
const received: Array<{ title: unknown }> = [];
|
||||
|
||||
const subscription = forwarder.toast$().subscribe(toast => {
|
||||
received.push(toast);
|
||||
});
|
||||
|
||||
forwarder.post({ title: 'Before unsubscribe' });
|
||||
subscription.unsubscribe();
|
||||
forwarder.post({ title: 'After unsubscribe' });
|
||||
|
||||
expect(received).toHaveLength(1);
|
||||
expect(received[0].title).toBe('Before unsubscribe');
|
||||
});
|
||||
|
||||
it('should stop receiving close events after unsubscribe', () => {
|
||||
const closedKeys: string[] = [];
|
||||
|
||||
const subscription = forwarder.close$().subscribe(key => {
|
||||
closedKeys.push(key);
|
||||
});
|
||||
|
||||
const key1 = forwarder.post({ title: 'Toast 1' });
|
||||
forwarder.close(key1);
|
||||
|
||||
subscription.unsubscribe();
|
||||
|
||||
const key2 = forwarder.post({ title: 'Toast 2' });
|
||||
forwarder.close(key2);
|
||||
|
||||
expect(closedKeys).toHaveLength(1);
|
||||
expect(closedKeys[0]).toBe(key1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render, screen, act, waitFor } from '@testing-library/react';
|
||||
import { TestApiProvider } from '@backstage/test-utils';
|
||||
import {
|
||||
alertApiRef,
|
||||
AlertApi,
|
||||
AlertMessage,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { toastApiRef } from '@backstage/frontend-plugin-api';
|
||||
import { Observable } from '@backstage/types';
|
||||
import ObservableImpl from 'zen-observable';
|
||||
import { ToastDisplay } from './ToastDisplay';
|
||||
import { ToastApiForwarder } from '../../apis';
|
||||
import { toastQueue } from './ToastQueue';
|
||||
|
||||
// Mock AlertApi with proper Observable implementation
|
||||
class MockAlertApi implements AlertApi {
|
||||
private subscribers = new Set<
|
||||
ZenObservable.SubscriptionObserver<AlertMessage>
|
||||
>();
|
||||
|
||||
post(alert: AlertMessage) {
|
||||
this.subscribers.forEach(subscriber => subscriber.next(alert));
|
||||
}
|
||||
|
||||
alert$(): Observable<AlertMessage> {
|
||||
return new ObservableImpl<AlertMessage>(subscriber => {
|
||||
this.subscribers.add(subscriber);
|
||||
return () => {
|
||||
this.subscribers.delete(subscriber);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('ToastDisplay', () => {
|
||||
let toastApi: ToastApiForwarder;
|
||||
let alertApi: MockAlertApi;
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear the toast queue before each test
|
||||
while (toastQueue.visibleToasts.length > 0) {
|
||||
toastQueue.close(toastQueue.visibleToasts[0].key);
|
||||
}
|
||||
toastApi = new ToastApiForwarder();
|
||||
alertApi = new MockAlertApi();
|
||||
});
|
||||
|
||||
const renderToastDisplay = () => {
|
||||
return render(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[alertApiRef, alertApi],
|
||||
[toastApiRef, toastApi],
|
||||
]}
|
||||
>
|
||||
<ToastDisplay />
|
||||
</TestApiProvider>,
|
||||
);
|
||||
};
|
||||
|
||||
describe('ToastApi integration', () => {
|
||||
it('should display a toast when posted via ToastApi', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
toastApi.post({
|
||||
title: 'Test Toast Title',
|
||||
status: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByText('Test Toast Title'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display toast with description', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
toastApi.post({
|
||||
title: 'Title',
|
||||
description: 'This is a description',
|
||||
status: 'info',
|
||||
});
|
||||
});
|
||||
|
||||
await expect(screen.findByText('Title')).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText('This is a description'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display toast with links', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
toastApi.post({
|
||||
title: 'Toast with link',
|
||||
links: [{ label: 'Click here', href: '/test' }],
|
||||
});
|
||||
});
|
||||
|
||||
const link = await screen.findByText('Click here');
|
||||
expect(link).toBeInTheDocument();
|
||||
expect(link).toHaveAttribute('href', '/test');
|
||||
});
|
||||
|
||||
it('should display multiple toasts', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
toastApi.post({ title: 'Toast 1', status: 'success' });
|
||||
toastApi.post({ title: 'Toast 2', status: 'warning' });
|
||||
toastApi.post({ title: 'Toast 3', status: 'danger' });
|
||||
});
|
||||
|
||||
await expect(screen.findByText('Toast 1')).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByText('Toast 2')).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByText('Toast 3')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should allow programmatic dismiss via close()', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
let toastKey: string;
|
||||
|
||||
await act(async () => {
|
||||
toastKey = toastApi.post({
|
||||
title: 'Dismissable Toast',
|
||||
status: 'info',
|
||||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByText('Dismissable Toast'),
|
||||
).resolves.toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
toastApi.close(toastKey!);
|
||||
// Wait for animation
|
||||
await new Promise(resolve => setTimeout(resolve, 600));
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Dismissable Toast')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AlertApi integration (legacy)', () => {
|
||||
it('should display alert as toast when posted via AlertApi', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
alertApi.post({
|
||||
message: 'Legacy Alert Message',
|
||||
severity: 'success',
|
||||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
screen.findByText('Legacy Alert Message'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should map alert error severity to danger status', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
alertApi.post({
|
||||
message: 'Error Alert',
|
||||
severity: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
const toast = await screen.findByText('Error Alert');
|
||||
expect(toast.closest('[data-status]')).toHaveAttribute(
|
||||
'data-status',
|
||||
'danger',
|
||||
);
|
||||
});
|
||||
|
||||
it('should default to success status when no severity', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
alertApi.post({
|
||||
message: 'No Severity Alert',
|
||||
});
|
||||
});
|
||||
|
||||
const toast = await screen.findByText('No Severity Alert');
|
||||
expect(toast.closest('[data-status]')).toHaveAttribute(
|
||||
'data-status',
|
||||
'success',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('concurrent usage', () => {
|
||||
it('should handle both ToastApi and AlertApi messages', async () => {
|
||||
renderToastDisplay();
|
||||
|
||||
await act(async () => {
|
||||
toastApi.post({ title: 'New Toast', status: 'success' });
|
||||
alertApi.post({ message: 'Legacy Alert', severity: 'warning' });
|
||||
});
|
||||
|
||||
await expect(screen.findByText('New Toast')).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByText('Legacy Alert'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -39,15 +39,20 @@ function mapSeverity(
|
||||
* ToastDisplay bridges both the ToastApi and AlertApi with the Toast notification system.
|
||||
*
|
||||
* @remarks
|
||||
* This component subscribes to:
|
||||
* - `toastApi.toast$()` - New toast notifications with full features (title, description, links, icons)
|
||||
* - `alertApi.alert$()` - Legacy alerts for backward compatibility (message maps to title only)
|
||||
* This component provides a migration bridge between the deprecated AlertApi and the new ToastApi.
|
||||
* During the migration period, it subscribes to both APIs simultaneously, allowing plugins to
|
||||
* migrate incrementally without breaking existing functionality.
|
||||
*
|
||||
* For ToastApi:
|
||||
* **Subscriptions:**
|
||||
* - `toastApi.toast$()` - New toast notifications with full features (title, description, links, icons)
|
||||
* - `alertApi.alert$()` - Deprecated alerts for backward compatibility (message maps to title only)
|
||||
*
|
||||
* **ToastApi (recommended):**
|
||||
* - Uses toast content directly (title, description, status, icon, links)
|
||||
* - Uses the provided timeout from the toast message
|
||||
* - Supports programmatic dismiss via returned key
|
||||
*
|
||||
* For AlertApi (legacy):
|
||||
* **AlertApi (deprecated - please migrate to ToastApi):**
|
||||
* - `alert.message` → `toast.title`
|
||||
* - `alert.severity` → `toast.status` ('error' maps to 'danger')
|
||||
* - `alert.display` → `timeout` (transient gets default timeout, permanent stays until dismissed)
|
||||
@@ -57,7 +62,9 @@ function mapSeverity(
|
||||
* // In your app root element extension
|
||||
* <ToastDisplay transientTimeoutMs={5000} />
|
||||
*
|
||||
* // Using the new ToastApi:
|
||||
* // Using the new ToastApi (recommended):
|
||||
* import { toastApiRef, useApi } from '@backstage/frontend-plugin-api';
|
||||
* const toastApi = useApi(toastApiRef);
|
||||
* toastApi.post({
|
||||
* title: 'Entity saved',
|
||||
* description: 'Your changes have been saved successfully.',
|
||||
@@ -65,7 +72,9 @@ function mapSeverity(
|
||||
* timeout: 5000,
|
||||
* });
|
||||
*
|
||||
* // Using the legacy AlertApi:
|
||||
* // Using the deprecated AlertApi (migrate to ToastApi):
|
||||
* import { alertApiRef, useApi } from '@backstage/core-plugin-api';
|
||||
* const alertApi = useApi(alertApiRef);
|
||||
* alertApi.post({ message: 'Saved!', severity: 'success', display: 'transient' });
|
||||
* ```
|
||||
*
|
||||
@@ -115,7 +124,8 @@ export function ToastDisplay(props: ToastDisplayProps) {
|
||||
return () => subscription.unsubscribe();
|
||||
}, [toastApi]);
|
||||
|
||||
// Subscribe to AlertApi (legacy support)
|
||||
// Subscribe to AlertApi (deprecated - provides backward compatibility during migration)
|
||||
// This subscription will be removed when AlertApi is fully deprecated
|
||||
useEffect(() => {
|
||||
const subscription = alertApi.alert$().subscribe(alert => {
|
||||
const content: ToastContent = {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useInvertedThemeMode } from './useInvertedThemeMode';
|
||||
|
||||
describe('useInvertedThemeMode', () => {
|
||||
const originalBodyTheme = document.body.getAttribute('data-theme-mode');
|
||||
const originalHtmlTheme =
|
||||
document.documentElement.getAttribute('data-theme-mode');
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original attributes
|
||||
if (originalBodyTheme) {
|
||||
document.body.setAttribute('data-theme-mode', originalBodyTheme);
|
||||
} else {
|
||||
document.body.removeAttribute('data-theme-mode');
|
||||
}
|
||||
if (originalHtmlTheme) {
|
||||
document.documentElement.setAttribute(
|
||||
'data-theme-mode',
|
||||
originalHtmlTheme,
|
||||
);
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme-mode');
|
||||
}
|
||||
});
|
||||
|
||||
it('should return dark when no theme is set', () => {
|
||||
document.body.removeAttribute('data-theme-mode');
|
||||
document.documentElement.removeAttribute('data-theme-mode');
|
||||
|
||||
const { result } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
expect(result.current).toBe('dark');
|
||||
});
|
||||
|
||||
it('should return light when body theme is dark', () => {
|
||||
document.body.setAttribute('data-theme-mode', 'dark');
|
||||
|
||||
const { result } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
expect(result.current).toBe('light');
|
||||
});
|
||||
|
||||
it('should return dark when body theme is light', () => {
|
||||
document.body.setAttribute('data-theme-mode', 'light');
|
||||
|
||||
const { result } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
expect(result.current).toBe('dark');
|
||||
});
|
||||
|
||||
it('should prefer body theme over html theme', () => {
|
||||
document.documentElement.setAttribute('data-theme-mode', 'light');
|
||||
document.body.setAttribute('data-theme-mode', 'dark');
|
||||
|
||||
const { result } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
// Body is dark, so inverted should be light
|
||||
expect(result.current).toBe('light');
|
||||
});
|
||||
|
||||
it('should fall back to html theme when body has no theme', () => {
|
||||
document.body.removeAttribute('data-theme-mode');
|
||||
document.documentElement.setAttribute('data-theme-mode', 'dark');
|
||||
|
||||
const { result } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
expect(result.current).toBe('light');
|
||||
});
|
||||
|
||||
it('should update when body theme changes', async () => {
|
||||
document.body.setAttribute('data-theme-mode', 'light');
|
||||
|
||||
const { result } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
expect(result.current).toBe('dark');
|
||||
|
||||
// Change theme
|
||||
await act(async () => {
|
||||
document.body.setAttribute('data-theme-mode', 'dark');
|
||||
// Wait for MutationObserver to fire
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(result.current).toBe('light');
|
||||
});
|
||||
|
||||
it('should update when html theme changes', async () => {
|
||||
document.body.removeAttribute('data-theme-mode');
|
||||
document.documentElement.setAttribute('data-theme-mode', 'light');
|
||||
|
||||
const { result } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
expect(result.current).toBe('dark');
|
||||
|
||||
// Change theme
|
||||
await act(async () => {
|
||||
document.documentElement.setAttribute('data-theme-mode', 'dark');
|
||||
// Wait for MutationObserver to fire
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
});
|
||||
|
||||
expect(result.current).toBe('light');
|
||||
});
|
||||
|
||||
it('should clean up observer on unmount', () => {
|
||||
const disconnectSpy = jest.spyOn(MutationObserver.prototype, 'disconnect');
|
||||
|
||||
const { unmount } = renderHook(() => useInvertedThemeMode());
|
||||
|
||||
unmount();
|
||||
|
||||
expect(disconnectSpy).toHaveBeenCalled();
|
||||
disconnectSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user