refactor(frontend): remove addToast bridge — migrate all call sites to notifications API

BREAKING: addToast() removed. Use notify() or notifications facade instead.

- Replace addToast(msg, type, duration?) → notify({ message, type, duration? })
- Introduce notifications.success/info/warning/error/show() semantic facade
- Add dismissAllToasts(), timer management (clearTimeout on remove)
- Unify Toast component: single viewport, a11y (role/aria-live by type)
- Migrate 16 models, 2 stores, 38 components, 23 pages (76 files total)
- 147 test files / 3152 tests pass with updated mocks and assertions
This commit is contained in:
2026-07-16 07:40:54 +03:00
parent 20071b8c7a
commit 8e2f393267
122 changed files with 1293 additions and 923 deletions

View File

@@ -10,7 +10,7 @@ import { render, screen, fireEvent } from '@testing-library/svelte';
// ── Hoisted mutable state (before vi.mock calls, hoisted to top) ──
const { mockStartMaintenance, mockAddToast, mockEnvStore } = vi.hoisted(() => {
const { mockStartMaintenance, mockEnvStore } = vi.hoisted(() => {
// Mutable env store state — tests can change this before render
let _selectedEnvId = 'env-prod';
@@ -23,7 +23,6 @@ const { mockStartMaintenance, mockAddToast, mockEnvStore } = vi.hoisted(() => {
return {
mockStartMaintenance: vi.fn(),
mockAddToast: vi.fn(),
mockEnvStore: {
obj: mockEnvStoreObj,
setSelectedEnvId(id: string) { _selectedEnvId = id; },
@@ -38,7 +37,7 @@ vi.mock('$lib/api/maintenance.js', () => ({
}));
vi.mock('$lib/toasts.svelte.js', () => ({
addToast: mockAddToast,
notifications: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn(), show: vi.fn() },
}));
vi.mock('$lib/stores/environmentContext.svelte.js', () => ({
@@ -94,6 +93,7 @@ vi.mock('$lib/i18n/index.svelte.js', () => {
// ── Imports (after mocks) ─────────────────────────────────────
import { notifications } from '$lib/toasts.svelte.js';
import StartMaintenanceForm from '$lib/components/StartMaintenanceForm.svelte';
describe('StartMaintenanceForm', () => {
@@ -153,9 +153,8 @@ describe('StartMaintenanceForm', () => {
expect(mockStartMaintenance).not.toHaveBeenCalled();
// Error toast should have been shown with the missing environment message
expect(mockAddToast).toHaveBeenCalledWith(
expect(notifications.error).toHaveBeenCalledWith(
expect.stringContaining('No environment selected'),
'error',
);
});
// #endregion test_validation_fails_without_environment

View File

@@ -28,8 +28,8 @@ class MockWebSocket {
vi.stubGlobal('WebSocket', MockWebSocket);
// Mock toasts
vi.mock('$lib/toasts.svelte.js', () => ({
addToast: vi.fn()
vi.mock('$lib/toasts.svelte.js', () => ({
notifications: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn(), show: vi.fn() },
}));
// Mock i18n
@@ -294,21 +294,21 @@ describe('MaintenanceStore', () => {
});
it('endEvent handles non-Error rejection with fallback message', async () => {
const { addToast } = await import('$lib/toasts.svelte.js');
const { notifications } = await import('$lib/toasts.svelte.js');
mockEndMaintenance.mockRejectedValue('string error');
try {
await store.endEvent('ev-fail');
} catch {}
expect(addToast).toHaveBeenCalledWith('Failed to end maintenance event', 'error');
expect(notifications.error).toHaveBeenCalledWith('Failed to end maintenance event');
});
it('endAllEvents handles non-Error rejection with fallback message', async () => {
const { addToast } = await import('$lib/toasts.svelte.js');
const { notifications } = await import('$lib/toasts.svelte.js');
mockEndAllMaintenance.mockRejectedValue('string error');
try {
await store.endAllEvents();
} catch {}
expect(addToast).toHaveBeenCalledWith('Failed to end all events', 'error');
expect(notifications.error).toHaveBeenCalledWith('Failed to end all events');
});
it('WebSocket onerror and onclose callbacks fire', async () => {
@@ -364,24 +364,26 @@ describe('MaintenanceStore', () => {
expect(mockListDashboardBanners).toHaveBeenCalled();
});
it('endEvent without task_id does not call addToast', async () => {
const { addToast } = await import('$lib/toasts.svelte.js');
it('endEvent without task_id does not show toast', async () => {
const { notifications } = await import('$lib/toasts.svelte.js');
mockEndMaintenance.mockResolvedValue({ status: 'pending' }); // no task_id
mockListEvents.mockResolvedValue({ active: [], completed: [] });
await store.endEvent('ev-no-task');
expect(addToast).not.toHaveBeenCalled();
expect(notifications.info).not.toHaveBeenCalled();
expect(notifications.error).not.toHaveBeenCalled();
});
it('endAllEvents without task_id does not call addToast', async () => {
const { addToast } = await import('$lib/toasts.svelte.js');
it('endAllEvents without task_id does not show toast', async () => {
const { notifications } = await import('$lib/toasts.svelte.js');
mockEndAllMaintenance.mockResolvedValue({ status: 'completed' }); // no task_id
mockListEvents.mockResolvedValue({ active: [], completed: [] });
await store.endAllEvents();
expect(addToast).not.toHaveBeenCalled();
expect(notifications.info).not.toHaveBeenCalled();
expect(notifications.error).not.toHaveBeenCalled();
});
});
// #endregion MaintenanceStoreTests

View File

@@ -52,7 +52,7 @@ function resetMockStore() {
// Mock toasts
vi.mock('$lib/toasts.svelte.js', () => ({
addToast: vi.fn()
notifications: { success: vi.fn(), error: vi.fn(), info: vi.fn(), warning: vi.fn(), show: vi.fn() },
}));
// Mock i18n — $t is a derived store, needs subscribe method for Svelte $ prefix