Преглед изворни кода

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 пре 3 недеља
родитељ
комит
4ecfd9ab4f

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

@@ -12,7 +12,7 @@
  * 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 { type ReactNode } from 'react';
 import { ToastProvider, useToast } from '../../contexts/ToastContext';
@@ -163,3 +163,64 @@ describe('ToastContext viewport suppression', () => {
     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_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 {
   type: string;
   queue_item_id: number;
@@ -156,12 +166,12 @@ export function ToastProvider({ children }: { children: ReactNode }) {
     const id = Math.random().toString(36).substr(2, 9);
     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(() => {
       if (!isMountedRef.current) return;
       setToasts((prev) => prev.filter((t) => t.id !== id));
       timeoutRefs.current.delete(id);
-    }, 3000);
+    }, LONG_LIVED_TOAST_TYPES.has(type) ? TOAST_DISMISS_LONG_MS : TOAST_DISMISS_MS);
     timeoutRefs.current.set(id, timeout);
   }, []);
 

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
static/assets/index-B2_C9nlv.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <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">
   </head>
   <body>

Неке датотеке нису приказане због велике количине промена