Skip to content

Commit d2695f9

Browse files
committed
SAVE
1 parent 4cb842e commit d2695f9

File tree

10 files changed

+654
-101
lines changed

10 files changed

+654
-101
lines changed

sources/app/layout/src/main.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@
22

33
import { z_username } from "@~/app.core/schema/z_username";
44
import type { UserInfoVariables_Context } from "@~/app.middleware/set_userinfo";
5-
import { ToasterContainer } from "@~/app.ui/toast/components";
5+
import {
6+
ErrorIcon,
7+
SuccessIcon,
8+
ToasterContainer,
9+
WarningIcon,
10+
} from "@~/app.ui/toast/components";
611
import { urls } from "@~/app.urls";
712
import type { PropsWithChildren } from "hono/jsx";
813
import { useRequestContext } from "hono/jsx-renderer";
@@ -36,7 +41,14 @@ export function Main_Layout({ children }: PropsWithChildren) {
3641
</header>
3742
<div class="relative flex flex-1 flex-col">{children}</div>
3843
</div>
39-
<ToasterContainer />
44+
<ToasterContainer
45+
duration="5s"
46+
iconsByTypes={{
47+
success: <SuccessIcon />,
48+
error: <ErrorIcon />,
49+
warning: <WarningIcon />,
50+
}}
51+
/>
4052
</Root_Layout>
4153
);
4254
}
Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
//
2+
3+
import { Htmx_Events } from "@~/app.core/htmx";
4+
import { describe, expect, test } from "bun:test";
5+
import _hyperscript from "hyperscript.org";
6+
import { render_html } from "../../testing";
7+
import { ToasterContainer } from "./ToasterContainer";
8+
9+
//
10+
11+
describe("ToasterContainer with Hyperscript", () => {
12+
test("renders custom icons when iconsByTypes provided", () => {
13+
const customIcons = {
14+
success: <div class="custom-success"></div>,
15+
error: <div class="custom-error"></div>,
16+
warning: <div class="custom-warning"></div>,
17+
};
18+
19+
const html = (<ToasterContainer iconsByTypes={customIcons} />).toString();
20+
document.body.innerHTML = html;
21+
const container = document.querySelector("hyyyper-toast-container")!;
22+
23+
_hyperscript.processNode(document.body);
24+
25+
document.body.dispatchEvent(
26+
new CustomEvent("toast:show", {
27+
detail: { type: "success", message: "Success with icon" },
28+
}),
29+
);
30+
31+
expect(render_html(container.outerHTML)).resolves.toMatchInlineSnapshot(`
32+
"<hyyyper-toast-container class="flex flex-col gap-2"
33+
><div
34+
_="
35+
init wait 5s then remove me
36+
on click from <button[aria-label='Close'] /> remove me
37+
"
38+
class="flex w-full max-w-xs items-center rounded-lg border border-solid border-gray-300 bg-white p-4 text-gray-500 shadow-sm"
39+
role="alert"
40+
>
41+
<div
42+
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-500"
43+
>
44+
<slot name="icon"><div class="custom-success">✓</div></slot
45+
><span class="sr-only">Icon</span>
46+
</div>
47+
<div class="ms-3 text-sm font-normal">
48+
<slot name="message">Success with icon</slot>
49+
</div>
50+
<button
51+
type="button"
52+
class="-mx-1.5 -my-1.5 ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-white p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-900 focus:ring-2 focus:ring-gray-300"
53+
aria-label="Close"
54+
>
55+
<span class="sr-only">Close</span
56+
><svg
57+
class="h-3 w-3"
58+
aria-hidden="true"
59+
xmlns="http://www.w3.org/2000/svg"
60+
fill="none"
61+
viewBox="0 0 14 14"
62+
>
63+
<path
64+
stroke="currentColor"
65+
stroke-linecap="round"
66+
stroke-linejoin="round"
67+
stroke-width="2"
68+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
69+
></path>
70+
</svg>
71+
</button></div
72+
></hyyyper-toast-container>
73+
"
74+
`);
75+
});
76+
77+
test("processes hyperscript and responds to toast events", () => {
78+
// Setup DOM
79+
const html = (<ToasterContainer />).toString();
80+
document.body.innerHTML = html;
81+
const container = document.querySelector("hyyyper-toast-container")!;
82+
83+
// Process hyperscript
84+
_hyperscript.processNode(document.body);
85+
86+
// Verify initial state
87+
expect(render_html(container.outerHTML)).resolves.toMatchInlineSnapshot(`
88+
"<hyyyper-toast-container class="flex flex-col gap-2"></hyyyper-toast-container>
89+
"
90+
`);
91+
92+
// Trigger toast event on body (as specified in hyperscript)
93+
const event = new CustomEvent("toast:show", {
94+
detail: { type: "success", message: "Test message" },
95+
});
96+
97+
document.body.dispatchEvent(event);
98+
99+
// Check that toast was added with actual message content
100+
expect(render_html(container.outerHTML)).resolves.toMatchInlineSnapshot(`
101+
"<hyyyper-toast-container class="flex flex-col gap-2"
102+
><div
103+
_="
104+
init wait 5s then remove me
105+
on click from <button[aria-label='Close'] /> remove me
106+
"
107+
class="flex w-full max-w-xs items-center rounded-lg border border-solid border-gray-300 bg-white p-4 text-gray-500 shadow-sm"
108+
role="alert"
109+
>
110+
<div
111+
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-500"
112+
>
113+
<slot name="icon"></slot><span class="sr-only">Icon</span>
114+
</div>
115+
<div class="ms-3 text-sm font-normal">
116+
<slot name="message">Test message</slot>
117+
</div>
118+
<button
119+
type="button"
120+
class="-mx-1.5 -my-1.5 ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-white p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-900 focus:ring-2 focus:ring-gray-300"
121+
aria-label="Close"
122+
>
123+
<span class="sr-only">Close</span
124+
><svg
125+
class="h-3 w-3"
126+
aria-hidden="true"
127+
xmlns="http://www.w3.org/2000/svg"
128+
fill="none"
129+
viewBox="0 0 14 14"
130+
>
131+
<path
132+
stroke="currentColor"
133+
stroke-linecap="round"
134+
stroke-linejoin="round"
135+
stroke-width="2"
136+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
137+
></path>
138+
</svg>
139+
</button></div
140+
></hyyyper-toast-container>
141+
"
142+
`);
143+
});
144+
145+
test("listen to HTMX ", () => {
146+
document.body.innerHTML = (<ToasterContainer />).toString();
147+
const container = document.querySelector("hyyyper-toast-container")!;
148+
149+
_hyperscript.processNode(document.body);
150+
151+
document.body.dispatchEvent(
152+
new CustomEvent(Htmx_Events.enum.responseError),
153+
);
154+
155+
expect(render_html(container.outerHTML)).resolves.toMatchInlineSnapshot(`
156+
"<hyyyper-toast-container class="flex flex-col gap-2"
157+
><div
158+
_="
159+
init wait 5s then remove me
160+
on click from <button[aria-label='Close'] /> remove me
161+
"
162+
class="flex w-full max-w-xs items-center rounded-lg border border-solid border-gray-300 bg-white p-4 text-gray-500 shadow-sm"
163+
role="alert"
164+
>
165+
<div
166+
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-500"
167+
>
168+
<slot name="icon"></slot><span class="sr-only">Icon</span>
169+
</div>
170+
<div class="ms-3 text-sm font-normal">
171+
<slot name="message">Une erreur est survenue !</slot>
172+
</div>
173+
<button
174+
type="button"
175+
class="-mx-1.5 -my-1.5 ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-white p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-900 focus:ring-2 focus:ring-gray-300"
176+
aria-label="Close"
177+
>
178+
<span class="sr-only">Close</span
179+
><svg
180+
class="h-3 w-3"
181+
aria-hidden="true"
182+
xmlns="http://www.w3.org/2000/svg"
183+
fill="none"
184+
viewBox="0 0 14 14"
185+
>
186+
<path
187+
stroke="currentColor"
188+
stroke-linecap="round"
189+
stroke-linejoin="round"
190+
stroke-width="2"
191+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
192+
></path>
193+
</svg>
194+
</button></div
195+
></hyyyper-toast-container>
196+
"
197+
`);
198+
});
199+
200+
test("handles multiple toast events", () => {
201+
document.body.innerHTML = (<ToasterContainer />).toString();
202+
const container = document.querySelector("hyyyper-toast-container")!;
203+
204+
_hyperscript.processNode(document.body);
205+
206+
// Add multiple toasts
207+
document.body.dispatchEvent(
208+
new CustomEvent("toast:show", {
209+
detail: { type: "success", message: "First toast" },
210+
}),
211+
);
212+
213+
document.body.dispatchEvent(
214+
new CustomEvent("toast:show", {
215+
detail: { type: "error", message: "Second toast" },
216+
}),
217+
);
218+
219+
expect(render_html(container.outerHTML)).resolves.toMatchInlineSnapshot(`
220+
"<hyyyper-toast-container class="flex flex-col gap-2"
221+
><div
222+
_="
223+
init wait 5s then remove me
224+
on click from <button[aria-label='Close'] /> remove me
225+
"
226+
class="flex w-full max-w-xs items-center rounded-lg border border-solid border-gray-300 bg-white p-4 text-gray-500 shadow-sm"
227+
role="alert"
228+
>
229+
<div
230+
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-500"
231+
>
232+
<slot name="icon"></slot><span class="sr-only">Icon</span>
233+
</div>
234+
<div class="ms-3 text-sm font-normal">
235+
<slot name="message">Second toast</slot>
236+
</div>
237+
<button
238+
type="button"
239+
class="-mx-1.5 -my-1.5 ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-white p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-900 focus:ring-2 focus:ring-gray-300"
240+
aria-label="Close"
241+
>
242+
<span class="sr-only">Close</span
243+
><svg
244+
class="h-3 w-3"
245+
aria-hidden="true"
246+
xmlns="http://www.w3.org/2000/svg"
247+
fill="none"
248+
viewBox="0 0 14 14"
249+
>
250+
<path
251+
stroke="currentColor"
252+
stroke-linecap="round"
253+
stroke-linejoin="round"
254+
stroke-width="2"
255+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
256+
></path>
257+
</svg>
258+
</button></div
259+
></hyyyper-toast-container>
260+
"
261+
`);
262+
});
263+
264+
test("moderation workflow integration", () => {
265+
document.body.innerHTML = (<ToasterContainer />).toString();
266+
const container = document.querySelector("hyyyper-toast-container")!;
267+
268+
_hyperscript.processNode(document.body);
269+
270+
// Simulate moderation validation workflow
271+
document.body.dispatchEvent(
272+
new CustomEvent("toast:show", {
273+
detail: {
274+
type: "success",
275+
message: "Modération validée !",
276+
action: {
277+
label: "Retour à la liste",
278+
event: "navigate:moderations",
279+
},
280+
},
281+
}),
282+
);
283+
284+
expect(render_html(container.outerHTML)).resolves.toMatchInlineSnapshot(`
285+
"<hyyyper-toast-container class="flex flex-col gap-2"
286+
><div
287+
_="
288+
init wait 5s then remove me
289+
on click from <button[aria-label='Close'] /> remove me
290+
"
291+
class="flex w-full max-w-xs items-center rounded-lg border border-solid border-gray-300 bg-white p-4 text-gray-500 shadow-sm"
292+
role="alert"
293+
>
294+
<div
295+
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blue-100 text-blue-500"
296+
>
297+
<slot name="icon"></slot><span class="sr-only">Icon</span>
298+
</div>
299+
<div class="ms-3 text-sm font-normal">
300+
<slot name="message">Modération validée !</slot>
301+
</div>
302+
<button
303+
type="button"
304+
class="-mx-1.5 -my-1.5 ms-auto inline-flex h-8 w-8 items-center justify-center rounded-lg bg-white p-1.5 text-gray-400 hover:bg-gray-100 hover:text-gray-900 focus:ring-2 focus:ring-gray-300"
305+
aria-label="Close"
306+
>
307+
<span class="sr-only">Close</span
308+
><svg
309+
class="h-3 w-3"
310+
aria-hidden="true"
311+
xmlns="http://www.w3.org/2000/svg"
312+
fill="none"
313+
viewBox="0 0 14 14"
314+
>
315+
<path
316+
stroke="currentColor"
317+
stroke-linecap="round"
318+
stroke-linejoin="round"
319+
stroke-width="2"
320+
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
321+
></path>
322+
</svg>
323+
</button></div
324+
></hyyyper-toast-container>
325+
"
326+
`);
327+
});
328+
329+
test("auto-removes toast after custom duration", async () => {
330+
// Use a short duration for fast testing
331+
document.body.innerHTML = (<ToasterContainer duration="1s" />).toString();
332+
const container = document.querySelector("hyyyper-toast-container")!;
333+
334+
_hyperscript.processNode(document.body);
335+
336+
// Add a toast
337+
document.body.dispatchEvent(
338+
new CustomEvent("toast:show", {
339+
detail: { type: "success", message: "Quick removal test" },
340+
}),
341+
);
342+
343+
// Verify toast was added
344+
expect(container.children).toHaveLength(1);
345+
346+
// Wait for auto-removal (1s + small buffer)
347+
await new Promise(resolve => setTimeout(resolve, 1100));
348+
349+
// Toast should be auto-removed
350+
expect(container.children).toHaveLength(0);
351+
});
352+
});
353+
354+
//

0 commit comments

Comments
 (0)