Add tests and deprecation warnings

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2026-02-05 21:34:31 +00:00
committed by Patrik Oldsberg
parent a47f27c0a9
commit eea95b8ae2
12 changed files with 868 additions and 19 deletions
+56
View File
@@ -0,0 +1,56 @@
---
'@backstage/frontend-plugin-api': patch
'@backstage/core-plugin-api': patch
'@backstage/core-app-api': patch
---
Deprecated `AlertApi` in favor of the new `ToastApi`.
`AlertApi` is now deprecated and will be removed in a future release. Please migrate to `ToastApi` which provides richer notification features.
**Why migrate?**
`ToastApi` offers enhanced capabilities over `AlertApi`:
- **Title and Description**: Display a prominent title with optional description text
- **Action Links**: Include clickable links within notifications
- **Custom Icons**: Override default icons or disable them entirely
- **Per-toast Timeout**: Control auto-dismiss timing for each notification individually
- **Programmatic Dismiss**: Close notifications via the key returned from `post()`
**Migration Guide**
| AlertApi | ToastApi |
| -------------------------------------------- | ------------------------------------------- |
| `message: string` | `title: ReactNode` |
| `severity: 'error'` | `status: 'danger'` |
| `severity: 'success' \| 'info' \| 'warning'` | `status: 'success' \| 'info' \| 'warning'` |
| `display: 'transient'` | `timeout: 5000` (or custom ms) |
| `display: 'permanent'` | omit `timeout` |
| `post()` returns `void` | `post()` returns `string` (key for dismiss) |
**Example Migration**
```typescript
// Before (AlertApi)
import { alertApiRef, useApi } from '@backstage/core-plugin-api';
const alertApi = useApi(alertApiRef);
alertApi.post({
message: 'Entity saved successfully',
severity: 'success',
display: 'transient',
});
// After (ToastApi)
import { toastApiRef, useApi } from '@backstage/frontend-plugin-api';
const toastApi = useApi(toastApiRef);
toastApi.post({
title: 'Entity saved successfully',
status: 'success',
timeout: 5000,
});
```
**Note**: During the migration period, both APIs work simultaneously. The `ToastDisplay` component subscribes to both `AlertApi` and `ToastApi`, so existing code continues to work while you migrate incrementally.
+1 -1
View File
@@ -70,7 +70,7 @@ import { StorageValueSnapshot } from '@backstage/core-plugin-api';
import { SubRouteRef } from '@backstage/core-plugin-api';
import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api';
// @public
// @public @deprecated
export class AlertApiForwarder implements AlertApi {
// (undocumented)
alert$(): Observable<AlertMessage>;
@@ -26,13 +26,25 @@ import ObservableImpl from 'zen-observable';
* missing alerts that were posted before subscription.
*
* @public
* @deprecated Use ToastApi instead. AlertApi will be removed in a future release.
*/
export class AlertApiForwarder implements AlertApi {
private readonly subject = new PublishSubject<AlertMessage>();
private readonly recentAlerts: AlertMessage[] = [];
private readonly maxBufferSize = 10;
private hasWarnedDeprecation = false;
post(alert: AlertMessage) {
if (!this.hasWarnedDeprecation) {
this.hasWarnedDeprecation = true;
// eslint-disable-next-line no-console
console.warn(
'AlertApi is deprecated and will be removed in a future release. ' +
'Please migrate to ToastApi from @backstage/frontend-plugin-api. ' +
'ToastApi provides richer features including title/description, links, icons, and per-toast timeouts. ' +
'Example: toastApi.post({ title: "Saved!", status: "success", timeout: 5000 })',
);
}
this.recentAlerts.push(alert);
if (this.recentAlerts.length > this.maxBufferSize) {
this.recentAlerts.shift();
@@ -14,6 +14,23 @@
* limitations under the License.
*/
/**
* @deprecated AlertApi is deprecated. Use ToastApi from `@backstage/frontend-plugin-api` instead.
*
* ToastApi provides richer notification features including title/description,
* action links, custom icons, per-toast timeout control, and programmatic dismiss.
*
* @example
* ```typescript
* // Before (AlertApi)
* import { alertApiRef } from '@backstage/core-plugin-api';
* alertApi.post({ message: 'Saved!', severity: 'success', display: 'transient' });
*
* // After (ToastApi)
* import { toastApiRef } from '@backstage/frontend-plugin-api';
* toastApi.post({ title: 'Saved!', status: 'success', timeout: 5000 });
* ```
*/
export {
type AlertApi,
type AlertMessage,
+3 -3
View File
@@ -24,16 +24,16 @@ import { ReactNode } from 'react';
import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api';
import type { z } from 'zod';
// @public
// @public @deprecated
export type AlertApi = {
post(alert: AlertMessage): void;
alert$(): Observable<AlertMessage>;
};
// @public
// @public @deprecated
export const alertApiRef: ApiRef<AlertApi>;
// @public
// @public @deprecated
export type AlertMessage = {
message: string;
severity?: 'success' | 'info' | 'warning' | 'error';
@@ -21,6 +21,14 @@ import { Observable } from '@backstage/types';
* Message handled by the {@link AlertApi}.
*
* @public
* @deprecated Use {@link ToastMessage} from {@link ToastApi} instead. AlertApi will be removed in a future release.
*
* Migration guide:
* - `message` becomes `title`
* - `severity: 'error'` becomes `status: 'danger'`
* - `severity: 'success' | 'info' | 'warning'` becomes `status: 'success' | 'info' | 'warning'`
* - `display: 'transient'` becomes `timeout: 5000` (or custom milliseconds)
* - `display: 'permanent'` means omitting `timeout`
*/
export type AlertMessage = {
message: string;
@@ -33,6 +41,23 @@ export type AlertMessage = {
* The alert API is used to report alerts to the app, and display them to the user.
*
* @public
* @deprecated Use {@link ToastApi} instead. AlertApi will be removed in a future release.
*
* ToastApi provides richer notification features including:
* - Title and optional description
* - Action links
* - Custom icons
* - Per-toast timeout control
* - Programmatic dismiss via returned key
*
* @example
* ```typescript
* // Before (AlertApi)
* alertApi.post({ message: 'Saved!', severity: 'success', display: 'transient' });
*
* // After (ToastApi)
* toastApi.post({ title: 'Saved!', status: 'success', timeout: 5000 });
* ```
*/
export type AlertApi = {
/**
@@ -50,6 +75,7 @@ export type AlertApi = {
* The {@link ApiRef} of {@link AlertApi}.
*
* @public
* @deprecated Use {@link toastApiRef} instead. AlertApi will be removed in a future release.
*/
export const alertApiRef: ApiRef<AlertApi> = createApiRef({
id: 'core.alert',
+1 -1
View File
@@ -65,7 +65,7 @@
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.61",
"@react-aria/button": "^3.13.3",
"@react-aria/button": "^3.14.3",
"@react-aria/toast": "^3.0.9",
"@react-hookz/web": "^24.0.0",
"@react-stately/toast": "^3.1.2",
@@ -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();
});
});
+133 -6
View File
@@ -4301,7 +4301,7 @@ __metadata:
"@material-ui/core": "npm:^4.9.13"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:^4.0.0-alpha.61"
"@react-aria/button": "npm:^3.13.3"
"@react-aria/button": "npm:^3.14.3"
"@react-aria/toast": "npm:^3.0.9"
"@react-hookz/web": "npm:^24.0.0"
"@react-stately/toast": "npm:^3.1.2"
@@ -15564,7 +15564,25 @@ __metadata:
languageName: node
linkType: hard
"@react-aria/button@npm:^3.13.3, @react-aria/button@npm:^3.14.4":
"@react-aria/button@npm:^3.14.3":
version: 3.14.3
resolution: "@react-aria/button@npm:3.14.3"
dependencies:
"@react-aria/interactions": "npm:^3.26.0"
"@react-aria/toolbar": "npm:3.0.0-beta.22"
"@react-aria/utils": "npm:^3.32.0"
"@react-stately/toggle": "npm:^3.9.3"
"@react-types/button": "npm:^3.14.1"
"@react-types/shared": "npm:^3.32.1"
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/383046c3b4ea5972f81f56251fc19b207d6d811d7ac06db866ccd809dbcc1b72358f4b1535fc65817929496c1acb15f7c2611e4777a371333bb1b150b954ccb8
languageName: node
linkType: hard
"@react-aria/button@npm:^3.14.4":
version: 3.14.4
resolution: "@react-aria/button@npm:3.14.4"
dependencies:
@@ -15903,6 +15921,21 @@ __metadata:
languageName: node
linkType: hard
"@react-aria/landmark@npm:^3.0.8":
version: 3.0.8
resolution: "@react-aria/landmark@npm:3.0.8"
dependencies:
"@react-aria/utils": "npm:^3.32.0"
"@react-types/shared": "npm:^3.32.1"
"@swc/helpers": "npm:^0.5.0"
use-sync-external-store: "npm:^1.4.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/7711871d004bbce5d8fadec3d81350cb9ce1896b01e4cc5f247003fedb9b785d3ca24947ce3f688ca5a8145004dd2cbbf43f1ac44e884c8ed6a950fd40a9e9f2
languageName: node
linkType: hard
"@react-aria/landmark@npm:^3.0.9":
version: 3.0.9
resolution: "@react-aria/landmark@npm:3.0.9"
@@ -16310,7 +16343,7 @@ __metadata:
languageName: node
linkType: hard
"@react-aria/toast@npm:^3.0.10, @react-aria/toast@npm:^3.0.9":
"@react-aria/toast@npm:^3.0.10":
version: 3.0.10
resolution: "@react-aria/toast@npm:3.0.10"
dependencies:
@@ -16329,6 +16362,25 @@ __metadata:
languageName: node
linkType: hard
"@react-aria/toast@npm:^3.0.9":
version: 3.0.9
resolution: "@react-aria/toast@npm:3.0.9"
dependencies:
"@react-aria/i18n": "npm:^3.12.14"
"@react-aria/interactions": "npm:^3.26.0"
"@react-aria/landmark": "npm:^3.0.8"
"@react-aria/utils": "npm:^3.32.0"
"@react-stately/toast": "npm:^3.1.2"
"@react-types/button": "npm:^3.14.1"
"@react-types/shared": "npm:^3.32.1"
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/f63cf143eec3e763ddd2885172685c6b85aaac1fb4423ced81aeceba06121788664909bfde862815e2562a7acd03c54df9a83e3fd0ddc60885806f76df30d2bf
languageName: node
linkType: hard
"@react-aria/toggle@npm:^3.12.4":
version: 3.12.4
resolution: "@react-aria/toggle@npm:3.12.4"
@@ -16608,7 +16660,7 @@ __metadata:
languageName: node
linkType: hard
"@react-stately/disclosure@npm:^3.0.10, @react-stately/disclosure@npm:^3.0.9":
"@react-stately/disclosure@npm:^3.0.10":
version: 3.0.10
resolution: "@react-stately/disclosure@npm:3.0.10"
dependencies:
@@ -16621,6 +16673,19 @@ __metadata:
languageName: node
linkType: hard
"@react-stately/disclosure@npm:^3.0.9":
version: 3.0.9
resolution: "@react-stately/disclosure@npm:3.0.9"
dependencies:
"@react-stately/utils": "npm:^3.11.0"
"@react-types/shared": "npm:^3.32.1"
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/b0458bc5255672af6b587a0b478b55077c481f223abb6094af04eeb8a922774a0da524dce6c540aec014f6efab4eecfa4d43a72eadd94e232dbdd959d49afd36
languageName: node
linkType: hard
"@react-stately/dnd@npm:^3.7.2, @react-stately/dnd@npm:^3.7.3":
version: 3.7.3
resolution: "@react-stately/dnd@npm:3.7.3"
@@ -16703,7 +16768,7 @@ __metadata:
languageName: node
linkType: hard
"@react-stately/menu@npm:^3.9.10, @react-stately/menu@npm:^3.9.9":
"@react-stately/menu@npm:^3.9.10":
version: 3.9.10
resolution: "@react-stately/menu@npm:3.9.10"
dependencies:
@@ -16717,6 +16782,20 @@ __metadata:
languageName: node
linkType: hard
"@react-stately/menu@npm:^3.9.9":
version: 3.9.9
resolution: "@react-stately/menu@npm:3.9.9"
dependencies:
"@react-stately/overlays": "npm:^3.6.21"
"@react-types/menu": "npm:^3.10.5"
"@react-types/shared": "npm:^3.32.1"
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/e71ac18a795fd125813d856c5889aaaa0dfd70ce01960e24b1926d2b98e7129610e04e586033c7b063a43594abdc4e88a8cc08f314630076091dad21f670e933
languageName: node
linkType: hard
"@react-stately/numberfield@npm:^3.10.3, @react-stately/numberfield@npm:^3.10.4":
version: 3.10.4
resolution: "@react-stately/numberfield@npm:3.10.4"
@@ -16877,7 +16956,7 @@ __metadata:
languageName: node
linkType: hard
"@react-stately/tooltip@npm:^3.5.10, @react-stately/tooltip@npm:^3.5.9":
"@react-stately/tooltip@npm:^3.5.10":
version: 3.5.10
resolution: "@react-stately/tooltip@npm:3.5.10"
dependencies:
@@ -16890,6 +16969,19 @@ __metadata:
languageName: node
linkType: hard
"@react-stately/tooltip@npm:^3.5.9":
version: 3.5.9
resolution: "@react-stately/tooltip@npm:3.5.9"
dependencies:
"@react-stately/overlays": "npm:^3.6.21"
"@react-types/tooltip": "npm:^3.5.0"
"@swc/helpers": "npm:^0.5.0"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/67c26c7e1777b61ee74411f12a0c971ad8550b5675eddc746761890b1854488d8774dcc0e91b30e46e5935c9c6751e7c2a38e189eba9a1efb4c97385a4b7a5e9
languageName: node
linkType: hard
"@react-stately/tree@npm:^3.9.4, @react-stately/tree@npm:^3.9.5":
version: 3.9.5
resolution: "@react-stately/tree@npm:3.9.5"
@@ -17081,6 +17173,18 @@ __metadata:
languageName: node
linkType: hard
"@react-types/menu@npm:^3.10.5":
version: 3.10.5
resolution: "@react-types/menu@npm:3.10.5"
dependencies:
"@react-types/overlays": "npm:^3.9.2"
"@react-types/shared": "npm:^3.32.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/0466f5b7602ccfc8cb8fec3cd2482d587acbfd1701d427c7d79a121eb3f2b137feca70e62fd401aea95022fd616460575c4d5dd541cf3d69d8644ca54a2446c8
languageName: node
linkType: hard
"@react-types/menu@npm:^3.10.6":
version: 3.10.6
resolution: "@react-types/menu@npm:3.10.6"
@@ -17115,6 +17219,17 @@ __metadata:
languageName: node
linkType: hard
"@react-types/overlays@npm:^3.9.2":
version: 3.9.2
resolution: "@react-types/overlays@npm:3.9.2"
dependencies:
"@react-types/shared": "npm:^3.32.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/6cab7f2cbb813f710696095db1169f902cfe7e4a9aeef496848343ff5116be3782bfea68dffbeaf3f984a0475c2fb6c4a26ad9fb563172c4ec3e47110ca1e672
languageName: node
linkType: hard
"@react-types/overlays@npm:^3.9.3":
version: 3.9.3
resolution: "@react-types/overlays@npm:3.9.3"
@@ -17236,6 +17351,18 @@ __metadata:
languageName: node
linkType: hard
"@react-types/tooltip@npm:^3.5.0":
version: 3.5.0
resolution: "@react-types/tooltip@npm:3.5.0"
dependencies:
"@react-types/overlays": "npm:^3.9.2"
"@react-types/shared": "npm:^3.32.1"
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
checksum: 10/66c20eb0e0d66628e739510e5fba8eb56534beec1887f7efcf1867fb5e43b8da3502c3f865059d08ce672e3c6e02876470497ab77dd3378f9ba6fbb8f1ca1de2
languageName: node
linkType: hard
"@react-types/tooltip@npm:^3.5.1":
version: 3.5.1
resolution: "@react-types/tooltip@npm:3.5.1"