Przeglądaj źródła

Hold error and warning toasts for twice as long

Every pop-up notification auto-dismissed after three seconds regardless
of what it said. That suits "Settings saved" -- a confirmation of
something the user just did, skimmed rather than read -- but errors and
warnings are a different kind of message. They carry a reason, often one
relayed from the printer or the backend, and they run to a couple of
lines. Three seconds was not enough to finish reading one, and there is
no notification history to go back to once it slides away.

Errors and warnings now hold for six seconds; success and info keep the
three-second default. The duration was a bare literal in showToast and
is now derived from the toast type, with the long window expressed as
twice the base so the two cannot drift apart if the base is retuned.

showPersistentToast never had an auto-dismiss timer and is untouched, as
is the background dispatch toast -- its timer measures "the summary has
stopped changing" rather than reading time. Manual dismissal is
unchanged for every type.
maziggy 1 miesiąc temu
rodzic
commit
9bc96aeb83

Plik diff jest za duży
+ 0 - 0
CHANGELOG.md


+ 62 - 1
frontend/src/__tests__/contexts/ToastContext.test.tsx

@@ -12,7 +12,7 @@
  * paths no-op instead of crashing.
  * paths no-op instead of crashing.
  */
  */
 
 
-import { describe, it, expect, beforeEach, vi } from 'vitest';
+import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
 import { act, render, renderHook } from '@testing-library/react';
 import { act, render, renderHook } from '@testing-library/react';
 import { type ReactNode } from 'react';
 import { type ReactNode } from 'react';
 import { ToastProvider, useToast } from '../../contexts/ToastContext';
 import { ToastProvider, useToast } from '../../contexts/ToastContext';
@@ -163,3 +163,64 @@ describe('ToastContext viewport suppression', () => {
     expect(toast?.style.maxWidth).toContain('safe-area-inset-right');
     expect(toast?.style.maxWidth).toContain('safe-area-inset-right');
   });
   });
 });
 });
+
+describe('ToastContext auto-dismiss timing by type', () => {
+  // Errors and warnings carry more text than a success confirmation — a
+  // backend failure reason often runs to a couple of lines — so they hold
+  // for 6s while success/info keep the 3s default.
+  function TypedToastProbe({ type }: { type: 'success' | 'error' | 'warning' | 'info' }) {
+    const { showToast } = useToast();
+    return <button data-testid="show" onClick={() => showToast(`a ${type} message`, type)} />;
+  }
+
+  function showAndAdvance(
+    type: 'success' | 'error' | 'warning' | 'info',
+    ms: number,
+  ): boolean {
+    const { getByTestId, queryByText, unmount } = render(
+      <ToastProvider>
+        <TypedToastProbe type={type} />
+      </ToastProvider>
+    );
+    act(() => {
+      getByTestId('show').click();
+    });
+    // Present before any time passes, otherwise a "gone" assertion below
+    // would pass on a toast that never rendered.
+    expect(queryByText(`a ${type} message`)).not.toBeNull();
+    act(() => {
+      vi.advanceTimersByTime(ms);
+    });
+    const stillThere = queryByText(`a ${type} message`) !== null;
+    unmount();
+    return stillThere;
+  }
+
+  beforeEach(() => {
+    vi.useFakeTimers();
+  });
+
+  afterEach(() => {
+    vi.useRealTimers();
+  });
+
+  it('keeps error toasts up for 6s', () => {
+    // Just past the old 3s window — an error must still be readable here.
+    expect(showAndAdvance('error', 3100)).toBe(true);
+    expect(showAndAdvance('error', 5999)).toBe(true);
+    expect(showAndAdvance('error', 6000)).toBe(false);
+  });
+
+  it('keeps warning toasts up for 6s', () => {
+    expect(showAndAdvance('warning', 3100)).toBe(true);
+    expect(showAndAdvance('warning', 5999)).toBe(true);
+    expect(showAndAdvance('warning', 6000)).toBe(false);
+  });
+
+  it('leaves success and info toasts on the 3s default', () => {
+    expect(showAndAdvance('success', 2999)).toBe(true);
+    expect(showAndAdvance('success', 3000)).toBe(false);
+    expect(showAndAdvance('info', 2999)).toBe(true);
+    expect(showAndAdvance('info', 3000)).toBe(false);
+  });
+});

+ 12 - 2
frontend/src/contexts/ToastContext.tsx

@@ -92,6 +92,16 @@ const bgColors = {
 const DISPATCH_TOAST_ID = 'background-dispatch';
 const DISPATCH_TOAST_ID = 'background-dispatch';
 const DISPATCH_TERMINAL_DISMISS_MS = 3500;
 const DISPATCH_TERMINAL_DISMISS_MS = 3500;
 
 
+// Auto-dismiss windows for the plain (non-persistent) toasts. Errors and
+// warnings get double the default because they carry far more text than a
+// success confirmation — a backend failure reason or a validation message
+// often runs to a couple of lines, and 3s isn't long enough to finish
+// reading one before it slides away. Success/info stay short: they confirm
+// something the user just did and are skimmed, not read.
+const TOAST_DISMISS_MS = 3000;
+const TOAST_DISMISS_LONG_MS = 2 * TOAST_DISMISS_MS;
+const LONG_LIVED_TOAST_TYPES: ReadonlySet<ToastType> = new Set(['error', 'warning']);
+
 interface DispatchEventDetail {
 interface DispatchEventDetail {
   type: string;
   type: string;
   queue_item_id: number;
   queue_item_id: number;
@@ -156,12 +166,12 @@ export function ToastProvider({ children }: { children: ReactNode }) {
     const id = Math.random().toString(36).substr(2, 9);
     const id = Math.random().toString(36).substr(2, 9);
     setToasts((prev) => [...prev, { id, message, type }]);
     setToasts((prev) => [...prev, { id, message, type }]);
 
 
-    // Auto-dismiss after 3 seconds
+    // Auto-dismiss — longer for the types that carry more to read.
     const timeout = setTimeout(() => {
     const timeout = setTimeout(() => {
       if (!isMountedRef.current) return;
       if (!isMountedRef.current) return;
       setToasts((prev) => prev.filter((t) => t.id !== id));
       setToasts((prev) => prev.filter((t) => t.id !== id));
       timeoutRefs.current.delete(id);
       timeoutRefs.current.delete(id);
-    }, 3000);
+    }, LONG_LIVED_TOAST_TYPES.has(type) ? TOAST_DISMISS_LONG_MS : TOAST_DISMISS_MS);
     timeoutRefs.current.set(id, timeout);
     timeoutRefs.current.set(id, timeout);
   }, []);
   }, []);
 
 

Plik diff jest za duży
+ 0 - 0
static/assets/index-B2_C9nlv.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CFRqaod2.js"></script>
+    <script type="module" crossorigin src="/assets/index-B2_C9nlv.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-GBTQ2eaA.css">
     <link rel="stylesheet" crossorigin href="/assets/index-GBTQ2eaA.css">
   </head>
   </head>
   <body>
   <body>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików