A toast is a short message that pops up on a page and goes away on its own after a few seconds. Write a class ToastQueue that decides which toasts are on screen. It only holds the state; drawing the toasts is someone else's job.
new ToastQueue(options) takes maxVisible, how many toasts can be on screen at once (3 by default), and duration, how many milliseconds each toast stays on screen (5000 by default). options and both of its properties are optional.
show(message) adds a toast and returns its id: 1 for the first toast, 2 for the next, and so on. The toast goes on screen when there's room. Otherwise it waits in line behind the toasts already waiting.
visible and queued are getters for the toasts on screen and the toasts waiting, oldest first. Each returns a new array of { id, message } objects.
A toast is dismissed duration ms after it goes on screen. Time spent waiting doesn't count.
dismiss(id) dismisses a toast early, whether it's on screen or waiting. An id that isn't on screen or waiting is ignored.
When a toast on screen is dismissed, by time or by dismiss, the oldest waiting toast takes its place, and its duration starts then.
subscribe(listener) calls listener(visible) every time the list of visible toasts changes, and returns a function that unsubscribes. Each change calls it once: a dismissal that brings a waiting toast on screen is one change. Adding a toast that has to wait, or dismissing a waiting one, doesn't change the visible list, so it calls no one.
const toasts = new ToastQueue({ maxVisible: 2, duration: 3000 });
toasts.subscribe((visible) => console.log(visible.map((toast) => toast.message)));
// At 0 ms:
const saved = toasts.show("Saved"); // 1, logs ["Saved"]
toasts.show("Copied"); // 2, logs ["Saved", "Copied"]
toasts.show("Deleted"); // 3, logs nothing: it waits
toasts.queued; // [{ id: 3, message: "Deleted" }]
// At 1000 ms:
toasts.dismiss(saved); // logs ["Copied", "Deleted"]
// At 3000 ms, "Copied" is dismissed: logs ["Deleted"]
// At 4000 ms, "Deleted" is dismissed, 3000 ms after it went on screen: logs []
useFakeTimers();
const toasts = new ToastQueue({ maxVisible: 2, duration: 1000 });
expect(toasts.show("a")).toBe(1);
expect(toasts.show("b")).toBe(2);
expect(toasts.show("c")).toBe(3);
expect(toasts.show("d")).toBe(4);
expect(toasts.visible).toEqual([{ id: 1, message: "a" }, { id: 2, message: "b" }]);
expect(toasts.queued).toEqual([{ id: 3, message: "c" }, { id: 4, message: "d" }]);
const clock = useFakeTimers();
const toasts = new ToastQueue({ duration: 1000 });
toasts.show("a");
clock.tick(500);
toasts.show("b");
clock.tick(499);
expect(toasts.visible.map((toast) => toast.message)).toEqual(["a", "b"]);
clock.tick(1);
expect(toasts.visible.map((toast) => toast.message)).toEqual(["b"]);
clock.tick(500);
expect(toasts.visible).toEqual([]);
const clock = useFakeTimers();
const toasts = new ToastQueue({ maxVisible: 1, duration: 1000 });
toasts.show("a");
toasts.show("b");
clock.tick(1000);
expect(toasts.visible).toEqual([{ id: 2, message: "b" }]);
expect(toasts.queued).toEqual([]);
clock.tick(999);
expect(toasts.visible).toEqual([{ id: 2, message: "b" }]);
clock.tick(1);
expect(toasts.visible).toEqual([]);
const clock = useFakeTimers();
const toasts = new ToastQueue({ maxVisible: 2, duration: 1000 });
const a = toasts.show("a");
toasts.show("b");
toasts.show("c");
clock.tick(400);
toasts.dismiss(a);
expect(toasts.visible.map((toast) => toast.message)).toEqual(["b", "c"]);
clock.tick(600);
expect(toasts.visible.map((toast) => toast.message)).toEqual(["c"]);
clock.tick(399);
expect(toasts.visible.map((toast) => toast.message)).toEqual(["c"]);
clock.tick(1);
expect(toasts.visible).toEqual([]);
const clock = useFakeTimers();
const toasts = new ToastQueue({ maxVisible: 1, duration: 1000 });
const listener = spy();
toasts.show("a");
const b = toasts.show("b");
toasts.show("c");
toasts.subscribe(listener);
toasts.dismiss(b);
toasts.dismiss(99);
expect(toasts.queued).toEqual([{ id: 3, message: "c" }]);
expect(toasts.visible).toEqual([{ id: 1, message: "a" }]);
expect(listener).not.toHaveBeenCalled();
clock.tick(1000);
expect(toasts.visible).toEqual([{ id: 3, message: "c" }]);
const clock = useFakeTimers();
const toasts = new ToastQueue({ maxVisible: 1, duration: 1000 });
const listener = spy();
toasts.subscribe(listener);
const a = toasts.show("a");
toasts.show("b");
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenLastCalledWith([{ id: 1, message: "a" }]);
clock.tick(400);
toasts.dismiss(a);
expect(listener).toHaveBeenCalledTimes(2);
expect(listener).toHaveBeenLastCalledWith([{ id: 2, message: "b" }]);
clock.tick(999);
expect(listener).toHaveBeenCalledTimes(2);
clock.tick(1);
expect(listener).toHaveBeenCalledTimes(3);
expect(listener).toHaveBeenLastCalledWith([]);
useFakeTimers();
const toasts = new ToastQueue();
const listener = spy();
const unsubscribe = toasts.subscribe(listener);
toasts.show("a");
unsubscribe();
toasts.show("b");
expect(listener).toHaveBeenCalledTimes(1);
const clock = useFakeTimers();
const toasts = new ToastQueue();
for (const message of ["a", "b", "c", "d"]) toasts.show(message);
expect(toasts.visible).toHaveLength(3);
expect(toasts.queued).toHaveLength(1);
clock.tick(4999);
expect(toasts.visible).toHaveLength(3);
clock.tick(1);
expect(toasts.visible.map((toast) => toast.message)).toEqual(["d"]);
const single = new ToastQueue({ maxVisible: 1 });
single.show("x");
clock.tick(4999);
expect(single.visible).toHaveLength(1);
clock.tick(1);
expect(single.visible).toHaveLength(0);
useFakeTimers();
const toasts = new ToastQueue({ maxVisible: 1 });
toasts.show("a");
toasts.show("b");
toasts.visible.pop();
toasts.queued.push({ id: 9, message: "x" });
expect(toasts.visible).toEqual([{ id: 1, message: "a" }]);
expect(toasts.queued).toEqual([{ id: 2, message: "b" }]);
Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.
Ctrl + Enter runs the tests (⌘ + Enter on a Mac).
I'm here to help with Toast queue. I give hints, never the solution, so you get to solve it yourself. Ask for a hint, select code to ask about it, or press Ask Tuto why on a failing test.
Your first 3 answers need no account. for free to keep asking.