mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Increase success toast duration to 5 seconds with dynamic calculation (#9574)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
8fe2e006ee
commit
7cfecb6e52
104
frontend/src/utils/__tests__/custom-toast-handlers.test.ts
Normal file
104
frontend/src/utils/__tests__/custom-toast-handlers.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
53
frontend/src/utils/__tests__/toast-duration.test.ts
Normal file
53
frontend/src/utils/__tests__/toast-duration.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@ -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 });
|
||||
};
|
||||
|
||||
27
frontend/src/utils/toast-duration.ts
Normal file
27
frontend/src/utils/toast-duration.ts
Normal 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);
|
||||
};
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user