Increase success toast duration to 5 seconds with dynamic calculation (#9574)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Xingyao Wang 2025-07-07 14:33:47 -04:00 committed by GitHub
parent 8fe2e006ee
commit 7cfecb6e52
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 191 additions and 3 deletions

View File

@ -0,0 +1,104 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import toast from "react-hot-toast";
import {
displaySuccessToast,
displayErrorToast,
} from "../custom-toast-handlers";
// Mock react-hot-toast
vi.mock("react-hot-toast", () => ({
default: {
success: vi.fn(),
error: vi.fn(),
},
}));
describe("custom-toast-handlers", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("displaySuccessToast", () => {
it("should call toast.success with calculated duration for short message", () => {
const shortMessage = "Settings saved";
displaySuccessToast(shortMessage);
expect(toast.success).toHaveBeenCalledWith(
shortMessage,
expect.objectContaining({
duration: 5000, // Should use minimum duration of 5000ms
position: "top-right",
style: expect.any(Object),
}),
);
});
it("should call toast.success with longer duration for long message", () => {
const longMessage =
"Settings saved. For old conversations, you will need to stop and restart the conversation to see the changes.";
displaySuccessToast(longMessage);
expect(toast.success).toHaveBeenCalledWith(
longMessage,
expect.objectContaining({
duration: expect.any(Number),
position: "top-right",
style: expect.any(Object),
}),
);
// Get the actual duration that was passed
const callArgs = (
toast.success as unknown as { mock: { calls: unknown[][] } }
).mock.calls[0][1] as { duration: number };
const actualDuration = callArgs.duration;
// For a long message, duration should be more than the minimum 5000ms
expect(actualDuration).toBeGreaterThan(5000);
// But should not exceed the maximum 10000ms
expect(actualDuration).toBeLessThanOrEqual(10000);
});
});
describe("displayErrorToast", () => {
it("should call toast.error with calculated duration for short message", () => {
const shortMessage = "Error occurred";
displayErrorToast(shortMessage);
expect(toast.error).toHaveBeenCalledWith(
shortMessage,
expect.objectContaining({
duration: 4000, // Should use minimum duration of 4000ms for errors
position: "top-right",
style: expect.any(Object),
}),
);
});
it("should call toast.error with longer duration for long error message", () => {
const longMessage =
"A very long error message that should take more time to read and understand what went wrong with the operation.";
displayErrorToast(longMessage);
expect(toast.error).toHaveBeenCalledWith(
longMessage,
expect.objectContaining({
duration: expect.any(Number),
position: "top-right",
style: expect.any(Object),
}),
);
// Get the actual duration that was passed
const callArgs = (
toast.error as unknown as { mock: { calls: unknown[][] } }
).mock.calls[0][1] as { duration: number };
const actualDuration = callArgs.duration;
// For a long message, duration should be more than the minimum 4000ms
expect(actualDuration).toBeGreaterThan(4000);
// But should not exceed the maximum 10000ms
expect(actualDuration).toBeLessThanOrEqual(10000);
});
});
});

View File

@ -0,0 +1,53 @@
import { describe, it, expect } from "vitest";
import { calculateToastDuration } from "../toast-duration";
describe("calculateToastDuration", () => {
it("should return minimum duration for short messages", () => {
const shortMessage = "OK";
const duration = calculateToastDuration(shortMessage, 5000);
expect(duration).toBe(5000);
});
it("should return minimum duration for messages that calculate below minimum", () => {
const shortMessage = "Settings saved";
const duration = calculateToastDuration(shortMessage, 5000);
expect(duration).toBe(5000);
});
it("should calculate longer duration for long messages", () => {
const longMessage =
"Settings saved. For old conversations, you will need to stop and restart the conversation to see the changes.";
const duration = calculateToastDuration(longMessage, 5000);
expect(duration).toBeGreaterThan(5000);
expect(duration).toBeLessThanOrEqual(10000);
});
it("should respect maximum duration cap", () => {
const veryLongMessage = "A".repeat(10000); // Very long message
const duration = calculateToastDuration(veryLongMessage, 5000, 10000);
expect(duration).toBe(10000);
});
it("should use custom minimum and maximum durations", () => {
const message = "Test message";
const customMin = 3000;
const customMax = 8000;
const duration = calculateToastDuration(message, customMin, customMax);
expect(duration).toBeGreaterThanOrEqual(customMin);
expect(duration).toBeLessThanOrEqual(customMax);
});
it("should calculate duration based on reading speed", () => {
// Test with a message that should take exactly the calculated time
// At 200 WPM (1000 chars/min), 60 chars should take 3.6 seconds
// With 1.5x buffer, that's 5.4 seconds
const message = "This is a test message that contains exactly sixty chars.";
expect(message.length).toBe(57); // Close to 60 chars
const duration = calculateToastDuration(message, 0, 20000); // No min/max constraints
// Should be around 5.4 seconds (5400ms) for 57 characters
expect(duration).toBeGreaterThan(5000);
expect(duration).toBeLessThan(6000);
});
});

View File

@ -1,5 +1,6 @@
import { CSSProperties } from "react";
import toast, { ToastOptions } from "react-hot-toast";
import { calculateToastDuration } from "./toast-duration";
const TOAST_STYLE: CSSProperties = {
background: "#454545",
@ -14,9 +15,11 @@ const TOAST_OPTIONS: ToastOptions = {
};
export const displayErrorToast = (error: string) => {
toast.error(error, TOAST_OPTIONS);
const duration = calculateToastDuration(error, 4000);
toast.error(error, { ...TOAST_OPTIONS, duration });
};
export const displaySuccessToast = (message: string) => {
toast.success(message, TOAST_OPTIONS);
const duration = calculateToastDuration(message, 5000);
toast.success(message, { ...TOAST_OPTIONS, duration });
};

View File

@ -0,0 +1,27 @@
/**
* Calculate toast duration based on message length
* @param message - The message to display
* @param minDuration - Minimum duration in milliseconds (default: 5000 for success, 4000 for error)
* @param maxDuration - Maximum duration in milliseconds (default: 10000)
* @returns Duration in milliseconds
*/
export const calculateToastDuration = (
message: string,
minDuration: number = 5000,
maxDuration: number = 10000,
): number => {
// Calculate duration based on reading speed (average 200 words per minute)
// Assuming average word length of 5 characters
const wordsPerMinute = 200;
const charactersPerMinute = wordsPerMinute * 5;
const charactersPerSecond = charactersPerMinute / 60;
// Calculate time needed to read the message
const readingTimeMs = (message.length / charactersPerSecond) * 1000;
// Add some buffer time (50% extra) for processing
const durationWithBuffer = readingTimeMs * 1.5;
// Ensure duration is within min/max bounds
return Math.min(Math.max(durationWithBuffer, minDuration), maxDuration);
};

View File

@ -1,4 +1,5 @@
import toast from "react-hot-toast";
import { calculateToastDuration } from "./toast-duration";
const idMap = new Map<string, string>();
@ -37,7 +38,7 @@ export default {
toast(msg, {
position: "bottom-right",
className: "bg-tertiary",
duration: calculateToastDuration(msg, 5000),
icon: "⚙️",
style: {
background: "#333",