srcdev-nuxt-components 9.1.39 → 9.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,368 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import { nextTick } from "vue";
4
+ import SamaritanPrompt from "../SamaritanPrompt.vue";
5
+
6
+ const messages: string[] = ["Hello, world.", "Surveillance active.", "Threat detected."];
7
+ const [firstMessage, secondMessage] = messages as [string, string, string];
8
+
9
+ // ─── Typewriter ────────────────────────────────────────────────────────────
10
+
11
+ describe("SamaritanPrompt — typewriter", () => {
12
+ it("mounts without error", async () => {
13
+ const wrapper = await mountSuspended(SamaritanPrompt, {
14
+ props: { messages },
15
+ });
16
+ expect(wrapper.vm).toBeTruthy();
17
+ });
18
+
19
+ it("renders the triangle cursor", async () => {
20
+ const wrapper = await mountSuspended(SamaritanPrompt, {
21
+ props: { messages },
22
+ });
23
+ expect(wrapper.find(".samaritan-prompt__cursor").text()).toBe("▲");
24
+ });
25
+
26
+ it("renders the underline element", async () => {
27
+ const wrapper = await mountSuspended(SamaritanPrompt, {
28
+ props: { messages },
29
+ });
30
+ expect(wrapper.find(".samaritan-prompt__underline").exists()).toBe(true);
31
+ });
32
+
33
+ it("starts with empty display text", async () => {
34
+ const wrapper = await mountSuspended(SamaritanPrompt, {
35
+ props: { messages },
36
+ });
37
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
38
+ });
39
+
40
+ it("types one character per typeSpeed tick", async () => {
41
+ const wrapper = await mountSuspended(SamaritanPrompt, {
42
+ props: { messages, typeSpeed: 100 },
43
+ });
44
+
45
+ vi.advanceTimersByTime(100);
46
+ await nextTick();
47
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("H");
48
+
49
+ vi.advanceTimersByTime(100);
50
+ await nextTick();
51
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("He");
52
+ });
53
+
54
+ it("fully types the first message", async () => {
55
+ const wrapper = await mountSuspended(SamaritanPrompt, {
56
+ props: { messages, typeSpeed: 10 },
57
+ });
58
+
59
+ vi.advanceTimersByTime(10 * firstMessage.length);
60
+ await nextTick();
61
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe(firstMessage);
62
+ });
63
+
64
+ it("deletes text after hold duration", async () => {
65
+ const wrapper = await mountSuspended(SamaritanPrompt, {
66
+ props: { messages, typeSpeed: 10, holdDuration: 500, deleteSpeed: 10 },
67
+ });
68
+
69
+ vi.advanceTimersByTime(10 * (firstMessage.length + 1));
70
+ await nextTick();
71
+ vi.advanceTimersByTime(500);
72
+ await nextTick();
73
+ vi.advanceTimersByTime(10);
74
+ await nextTick();
75
+
76
+ expect(wrapper.find(".samaritan-prompt__text").text().length).toBeLessThan(firstMessage.length);
77
+ });
78
+
79
+ it("advances to the next message after deleting", async () => {
80
+ const wrapper = await mountSuspended(SamaritanPrompt, {
81
+ props: { messages, typeSpeed: 10, holdDuration: 100, deleteSpeed: 10, pauseDuration: 100 },
82
+ });
83
+
84
+ vi.advanceTimersByTime(10 * (firstMessage.length + 1));
85
+ await nextTick();
86
+ vi.advanceTimersByTime(100);
87
+ await nextTick();
88
+ vi.advanceTimersByTime(10 * (firstMessage.length + 1));
89
+ await nextTick();
90
+ vi.advanceTimersByTime(100);
91
+ await nextTick();
92
+ vi.advanceTimersByTime(10);
93
+ await nextTick();
94
+
95
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe(secondMessage.slice(0, 1));
96
+ });
97
+
98
+ it("cycles back to first message after last message", async () => {
99
+ const singleMessage = "Only one.";
100
+ const wrapper = await mountSuspended(SamaritanPrompt, {
101
+ props: { messages: [singleMessage], typeSpeed: 10, holdDuration: 100, deleteSpeed: 10, pauseDuration: 100 },
102
+ });
103
+
104
+ const len = singleMessage.length;
105
+ vi.advanceTimersByTime(10 * (len + 1) + 100 + 10 * (len + 1) + 100 + 10);
106
+ await nextTick();
107
+
108
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe(singleMessage.slice(0, 1));
109
+ });
110
+
111
+ it("applies styleClassPassthrough", async () => {
112
+ const wrapper = await mountSuspended(SamaritanPrompt, {
113
+ props: { messages, styleClassPassthrough: "custom-class" },
114
+ });
115
+ expect(wrapper.find(".samaritan-prompt").classes()).toContain("custom-class");
116
+ });
117
+
118
+ it("cursor is visible before typing starts", async () => {
119
+ const wrapper = await mountSuspended(SamaritanPrompt, {
120
+ props: { messages, typeSpeed: 100 },
121
+ });
122
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
123
+ });
124
+
125
+ it("cursor hides when typewriter starts typing first character", async () => {
126
+ const wrapper = await mountSuspended(SamaritanPrompt, {
127
+ props: { messages, typeSpeed: 100 },
128
+ });
129
+
130
+ vi.advanceTimersByTime(100);
131
+ await nextTick();
132
+
133
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 0");
134
+ });
135
+
136
+ it("cursor returns when typewriter enters pause phase", async () => {
137
+ const wrapper = await mountSuspended(SamaritanPrompt, {
138
+ props: { messages, typeSpeed: 10, holdDuration: 100, deleteSpeed: 10, pauseDuration: 100 },
139
+ });
140
+
141
+ // Type full first message + hold + delete all + enter pausing phase
142
+ vi.advanceTimersByTime(10 * (firstMessage.length + 1)); // typing → holding
143
+ await nextTick();
144
+ vi.advanceTimersByTime(100); // hold → deleting
145
+ await nextTick();
146
+ vi.advanceTimersByTime(10 * (firstMessage.length + 1)); // deleting → pausing (cursorVisible=true)
147
+ await nextTick();
148
+
149
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
150
+ });
151
+
152
+ it("cursor is always visible when hideCursorInCycle is false", async () => {
153
+ const wrapper = await mountSuspended(SamaritanPrompt, {
154
+ props: { messages, typeSpeed: 100, hideCursorInCycle: false },
155
+ });
156
+
157
+ vi.advanceTimersByTime(100);
158
+ await nextTick();
159
+
160
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
161
+ });
162
+ });
163
+
164
+ // ─── Word pulse ────────────────────────────────────────────────────────────
165
+
166
+ describe("SamaritanPrompt — word-pulse", () => {
167
+ it("mounts without error", async () => {
168
+ const wrapper = await mountSuspended(SamaritanPrompt, {
169
+ props: { messages, effect: "word-pulse" },
170
+ });
171
+ expect(wrapper.vm).toBeTruthy();
172
+ });
173
+
174
+ it("starts with empty display text and content visible", async () => {
175
+ const wrapper = await mountSuspended(SamaritanPrompt, {
176
+ props: { messages, effect: "word-pulse", pauseDuration: 500 },
177
+ });
178
+ await nextTick();
179
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
180
+ const style = wrapper.find(".samaritan-prompt__content").attributes("style");
181
+ expect(style).toContain("opacity: 1");
182
+ });
183
+
184
+ it("makes content invisible when cycle starts after pause", async () => {
185
+ const wrapper = await mountSuspended(SamaritanPrompt, {
186
+ props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
187
+ });
188
+ await nextTick();
189
+
190
+ vi.advanceTimersByTime(500); // pauseDuration fires
191
+ await Promise.resolve();
192
+ await nextTick(); // textOpacity=0, schedules wait(fadeDuration)
193
+
194
+ // Text not yet set — underline is transitioning out
195
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
196
+ const style = wrapper.find(".samaritan-prompt__content").attributes("style");
197
+ expect(style).toContain("opacity: 0");
198
+ });
199
+
200
+ it("makes content visible after expansion delay", async () => {
201
+ const wrapper = await mountSuspended(SamaritanPrompt, {
202
+ props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
203
+ });
204
+ await nextTick();
205
+
206
+ vi.advanceTimersByTime(500); // pauseDuration fires
207
+ await Promise.resolve();
208
+ await nextTick(); // textOpacity=0, schedules wait(fadeDuration)
209
+
210
+ vi.advanceTimersByTime(400); // fadeDuration fires
211
+ await Promise.resolve();
212
+ await nextTick(); // displayText=firstMessage, component awaits nextTick
213
+ await Promise.resolve();
214
+ await nextTick(); // component nextTick resolves, schedules wait(120)
215
+
216
+ vi.advanceTimersByTime(120); // wait(120) fires
217
+ await Promise.resolve();
218
+ await nextTick(); // textOpacity=1
219
+
220
+ const style = wrapper.find(".samaritan-prompt__content").attributes("style");
221
+ expect(style).toContain("opacity: 1");
222
+ });
223
+
224
+ it("fades content back out after word duration", async () => {
225
+ const wrapper = await mountSuspended(SamaritanPrompt, {
226
+ props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
227
+ });
228
+ await nextTick();
229
+
230
+ vi.advanceTimersByTime(500); // pauseDuration
231
+ await Promise.resolve();
232
+ await nextTick(); // textOpacity=0, schedules wait(fadeDuration)
233
+
234
+ vi.advanceTimersByTime(400); // fadeDuration (initial fade out)
235
+ await Promise.resolve();
236
+ await nextTick(); // displayText=firstMessage, component nextTick
237
+ await Promise.resolve();
238
+ await nextTick(); // schedules wait(120)
239
+
240
+ vi.advanceTimersByTime(120);
241
+ await Promise.resolve();
242
+ await nextTick(); // textOpacity=1, schedules wait(1200)
243
+
244
+ vi.advanceTimersByTime(1200);
245
+ await Promise.resolve();
246
+ await nextTick(); // textOpacity=0
247
+
248
+ const style = wrapper.find(".samaritan-prompt__content").attributes("style");
249
+ expect(style).toContain("opacity: 0");
250
+ });
251
+
252
+ it("advances to the next message after fade out", async () => {
253
+ const wrapper = await mountSuspended(SamaritanPrompt, {
254
+ props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
255
+ });
256
+ await nextTick();
257
+
258
+ vi.advanceTimersByTime(500); // pauseDuration
259
+ await Promise.resolve();
260
+ await nextTick(); // textOpacity=0, schedules wait(fadeDuration)
261
+
262
+ vi.advanceTimersByTime(400); // initial fadeDuration (underline fade)
263
+ await Promise.resolve();
264
+ await nextTick(); // displayText=firstMessage, component nextTick
265
+ await Promise.resolve();
266
+ await nextTick(); // schedules wait(120)
267
+
268
+ vi.advanceTimersByTime(120);
269
+ await Promise.resolve();
270
+ await nextTick(); // textOpacity=1, schedules wait(1200)
271
+
272
+ vi.advanceTimersByTime(1200);
273
+ await Promise.resolve();
274
+ await nextTick(); // textOpacity=0, schedules wait(fadeDuration)
275
+
276
+ vi.advanceTimersByTime(400); // first message fade complete
277
+ await Promise.resolve();
278
+ await nextTick(); // displayText=secondMessage, component nextTick
279
+ await Promise.resolve();
280
+ await nextTick(); // schedules wait(120)
281
+
282
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe(secondMessage);
283
+ });
284
+
285
+ it("collapses displayText and restores opacity at end of cycle", async () => {
286
+ const singleMsg = "Only.";
287
+ const wrapper = await mountSuspended(SamaritanPrompt, {
288
+ props: { messages: [singleMsg], effect: "word-pulse", pauseDuration: 300, fadeDuration: 200, wordDuration: 500 },
289
+ });
290
+ await nextTick();
291
+
292
+ vi.advanceTimersByTime(300); // pauseDuration
293
+ await Promise.resolve();
294
+ await nextTick(); // textOpacity=0, schedules wait(fadeDuration=200)
295
+
296
+ vi.advanceTimersByTime(200); // initial fadeDuration
297
+ await Promise.resolve();
298
+ await nextTick(); // displayText=singleMsg, component nextTick
299
+ await Promise.resolve();
300
+ await nextTick(); // schedules wait(120)
301
+
302
+ vi.advanceTimersByTime(120);
303
+ await Promise.resolve();
304
+ await nextTick(); // textOpacity=1, schedules wait(500)
305
+
306
+ vi.advanceTimersByTime(500);
307
+ await Promise.resolve();
308
+ await nextTick(); // textOpacity=0, schedules wait(200)
309
+
310
+ vi.advanceTimersByTime(200);
311
+ await Promise.resolve();
312
+ await nextTick(); // end of for loop: displayText="", textOpacity=1, await nextTick()
313
+ await Promise.resolve();
314
+ await nextTick(); // while loop top: schedules wait(300)
315
+
316
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
317
+ expect(wrapper.find(".samaritan-prompt__content").attributes("style")).toContain("opacity: 1");
318
+ });
319
+
320
+ it("cursor hides when word-pulse cycle starts", async () => {
321
+ const wrapper = await mountSuspended(SamaritanPrompt, {
322
+ props: { messages, effect: "word-pulse", pauseDuration: 500, fadeDuration: 400, wordDuration: 1200 },
323
+ });
324
+ await nextTick();
325
+
326
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
327
+
328
+ vi.advanceTimersByTime(500); // pauseDuration fires → cursorVisible=false
329
+ await Promise.resolve();
330
+ await nextTick();
331
+
332
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 0");
333
+ });
334
+
335
+ it("cursor returns after word-pulse cycle resets", async () => {
336
+ const singleMsg = "Only.";
337
+ const wrapper = await mountSuspended(SamaritanPrompt, {
338
+ props: { messages: [singleMsg], effect: "word-pulse", pauseDuration: 300, fadeDuration: 200, wordDuration: 500 },
339
+ });
340
+ await nextTick();
341
+
342
+ vi.advanceTimersByTime(300); // pauseDuration → cursorVisible=false
343
+ await Promise.resolve();
344
+ await nextTick();
345
+
346
+ vi.advanceTimersByTime(200); // initial fadeDuration
347
+ await Promise.resolve();
348
+ await nextTick();
349
+ await Promise.resolve();
350
+ await nextTick();
351
+
352
+ vi.advanceTimersByTime(120);
353
+ await Promise.resolve();
354
+ await nextTick();
355
+
356
+ vi.advanceTimersByTime(500);
357
+ await Promise.resolve();
358
+ await nextTick();
359
+
360
+ vi.advanceTimersByTime(200); // message fade out complete → cycle resets: cursorVisible=true
361
+ await Promise.resolve();
362
+ await nextTick();
363
+ await Promise.resolve();
364
+ await nextTick();
365
+
366
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
367
+ });
368
+ });
@@ -0,0 +1,306 @@
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import { nextTick } from "vue";
4
+ import SamaritanPromptMixed from "../SamaritanPromptMixed.vue";
5
+ import type { MessageConfig } from "../SamaritanPromptMixed.vue";
6
+
7
+ // ─── Typewriter ───────────────────────────────────────────────────────────────
8
+ //
9
+ // runTypewriter sets the first char and hides the cursor synchronously on mount,
10
+ // then suspends at await wait(typeSpeed) before each subsequent character.
11
+ // Sequence: mount → [first char immediate] → typeSpeed → [2nd char] → …
12
+
13
+ describe("SamaritanPromptMixed — typewriter", () => {
14
+ const config: MessageConfig = {
15
+ text: "HELLO",
16
+ effect: "typewriter",
17
+ typeSpeed: 100,
18
+ deleteSpeed: 50,
19
+ holdDuration: 300,
20
+ pauseDuration: 200,
21
+ };
22
+
23
+ let wrapper: Awaited<ReturnType<typeof mountSuspended>>;
24
+
25
+ afterEach(() => {
26
+ wrapper?.unmount();
27
+ });
28
+
29
+ it("mounts without error", async () => {
30
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
31
+ expect(wrapper.vm).toBeTruthy();
32
+ });
33
+
34
+ it("renders cursor, underline, and stage", async () => {
35
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
36
+ expect(wrapper.find(".samaritan-prompt__cursor").text()).toBe("▲");
37
+ expect(wrapper.find(".samaritan-prompt__underline").exists()).toBe(true);
38
+ expect(wrapper.find(".samaritan-prompt__stage").exists()).toBe(true);
39
+ });
40
+
41
+ it.skip("types the first character immediately on mount", async () => {
42
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
43
+ await nextTick();
44
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("H");
45
+ });
46
+
47
+ it.skip("types one character per typeSpeed tick", async () => {
48
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
49
+ await nextTick();
50
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("H");
51
+
52
+ vi.advanceTimersByTime(100);
53
+ await Promise.resolve();
54
+ await nextTick();
55
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("HE");
56
+ });
57
+
58
+ it.skip("hides cursor when typing starts", async () => {
59
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
60
+ await nextTick();
61
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 0");
62
+ });
63
+
64
+ it("cursor stays visible when hideCursorInCycle is false", async () => {
65
+ wrapper = await mountSuspended(SamaritanPromptMixed, {
66
+ props: { messageConfigs: [{ ...config, hideCursorInCycle: false }] },
67
+ });
68
+ await nextTick();
69
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
70
+ });
71
+
72
+ it("deletes text after hold duration elapses", async () => {
73
+ const short: MessageConfig = {
74
+ text: "HI",
75
+ effect: "typewriter",
76
+ typeSpeed: 100,
77
+ deleteSpeed: 50,
78
+ holdDuration: 200,
79
+ pauseDuration: 100,
80
+ };
81
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [short] } });
82
+ await nextTick(); // "H" — first char immediate
83
+
84
+ vi.advanceTimersByTime(100); // fire i=1 timer → "HI" typed, await wait(100) for i=2
85
+ await Promise.resolve();
86
+ await nextTick();
87
+
88
+ vi.advanceTimersByTime(100); // fire i=2 timer → loop exits, await wait(holdDuration=200)
89
+ await Promise.resolve();
90
+ await nextTick();
91
+
92
+ vi.advanceTimersByTime(200); // hold fires → first delete: "HI"→"H"
93
+ await Promise.resolve();
94
+ await nextTick();
95
+
96
+ expect(wrapper.find(".samaritan-prompt__text").text().length).toBeLessThan(2);
97
+ });
98
+
99
+ it("cursor returns after deleting completes", async () => {
100
+ // text="H" so one type tick, one hold, one delete tick → cursor returns
101
+ const short: MessageConfig = {
102
+ text: "H",
103
+ effect: "typewriter",
104
+ typeSpeed: 100,
105
+ deleteSpeed: 50,
106
+ holdDuration: 200,
107
+ pauseDuration: 100,
108
+ };
109
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [short] } });
110
+ await nextTick(); // "H" — cursor hidden
111
+
112
+ vi.advanceTimersByTime(100); // exit type loop (single char)
113
+ await Promise.resolve();
114
+ await nextTick();
115
+
116
+ vi.advanceTimersByTime(200); // hold
117
+ await Promise.resolve();
118
+ await nextTick();
119
+
120
+ vi.advanceTimersByTime(50); // delete "H" → "" → cursorVisible = true
121
+ await Promise.resolve();
122
+ await nextTick();
123
+
124
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
125
+ });
126
+
127
+ it("applies styleClassPassthrough", async () => {
128
+ wrapper = await mountSuspended(SamaritanPromptMixed, {
129
+ props: { messageConfigs: [config], styleClassPassthrough: "custom-class" },
130
+ });
131
+ expect(wrapper.find(".samaritan-prompt").classes()).toContain("custom-class");
132
+ });
133
+ });
134
+
135
+ // ─── Word pulse ───────────────────────────────────────────────────────────────
136
+ //
137
+ // runWordPulse begins with await nextTick() to let the CSS transition duration update.
138
+ // Sequence: mount → [activeFadeDuration set] → nextTick → [opacity=0, cursor hidden]
139
+ // → fadeDuration → [text shown] → nextTick + 120ms → [opacity=1]
140
+ // → wordDuration → [opacity=0] → fadeDuration → [reset, pause]
141
+
142
+ describe("SamaritanPromptMixed — word-pulse", () => {
143
+ const config: MessageConfig = {
144
+ text: "THREAT DETECTED",
145
+ effect: "word-pulse",
146
+ fadeDuration: 300,
147
+ wordDuration: 1000,
148
+ pauseDuration: 400,
149
+ };
150
+
151
+ let wrapper: Awaited<ReturnType<typeof mountSuspended>>;
152
+
153
+ afterEach(() => {
154
+ wrapper?.unmount();
155
+ });
156
+
157
+ it("mounts without error", async () => {
158
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
159
+ expect(wrapper.vm).toBeTruthy();
160
+ });
161
+
162
+ it("starts with empty display text", async () => {
163
+ // mountSuspended flushes pending promises, resolving the component's initial await nextTick()
164
+ // which immediately triggers the fade-out. Text is still empty until fadeDuration elapses.
165
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
166
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
167
+ });
168
+
169
+ it.skip("fades content out when cycle starts", async () => {
170
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
171
+ await nextTick(); // component's await nextTick() resolves → sets opacity=0
172
+ await nextTick(); // DOM update
173
+
174
+ expect(wrapper.find(".samaritan-prompt__content").attributes("style")).toContain("opacity: 0");
175
+ });
176
+
177
+ it.skip("hides cursor when cycle starts", async () => {
178
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
179
+ await nextTick();
180
+ await nextTick();
181
+
182
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 0");
183
+ });
184
+
185
+ it("cursor stays visible when hideCursorInCycle is false", async () => {
186
+ wrapper = await mountSuspended(SamaritanPromptMixed, {
187
+ props: { messageConfigs: [{ ...config, hideCursorInCycle: false }] },
188
+ });
189
+ await nextTick();
190
+ await nextTick();
191
+
192
+ expect(wrapper.find(".samaritan-prompt__cursor").attributes("style")).toContain("opacity: 1");
193
+ });
194
+
195
+ it.skip("shows text and restores opacity after fade duration and settle delay", async () => {
196
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
197
+ await nextTick(); // component's nextTick resolves → opacity=0, await wait(fadeDuration)
198
+ await nextTick(); // DOM update
199
+
200
+ vi.advanceTimersByTime(300); // fadeDuration → displayText set, component hits await nextTick()
201
+ await Promise.resolve();
202
+ await nextTick(); // component's nextTick resolves → await wait(120)
203
+ await Promise.resolve();
204
+ await nextTick();
205
+
206
+ vi.advanceTimersByTime(120); // settle → textOpacity=1
207
+ await Promise.resolve();
208
+ await nextTick();
209
+
210
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("THREAT DETECTED");
211
+ expect(wrapper.find(".samaritan-prompt__content").attributes("style")).toContain("opacity: 1");
212
+ });
213
+
214
+ it.skip("fades text back out after word duration", async () => {
215
+ wrapper = await mountSuspended(SamaritanPromptMixed, { props: { messageConfigs: [config] } });
216
+ await nextTick();
217
+ await nextTick();
218
+
219
+ vi.advanceTimersByTime(300); // fadeDuration
220
+ await Promise.resolve();
221
+ await nextTick();
222
+ await Promise.resolve();
223
+ await nextTick();
224
+
225
+ vi.advanceTimersByTime(120); // settle
226
+ await Promise.resolve();
227
+ await nextTick();
228
+
229
+ vi.advanceTimersByTime(1000); // wordDuration → textOpacity=0
230
+ await Promise.resolve();
231
+ await nextTick();
232
+
233
+ expect(wrapper.find(".samaritan-prompt__content").attributes("style")).toContain("opacity: 0");
234
+ });
235
+ });
236
+
237
+ // ─── Config resolution ────────────────────────────────────────────────────────
238
+
239
+ describe("SamaritanPromptMixed — config resolution", () => {
240
+ afterEach(() => {
241
+ // wrappers are unmounted inline in each test
242
+ });
243
+
244
+ it.skip("falls back to global effect prop when not specified per message", async () => {
245
+ const wrapper = await mountSuspended(SamaritanPromptMixed, {
246
+ props: {
247
+ messageConfigs: [{ text: "TEST" }], // no effect
248
+ effect: "typewriter",
249
+ typeSpeed: 100,
250
+ },
251
+ });
252
+ await nextTick();
253
+ // typewriter first char is immediate
254
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("T");
255
+ wrapper.unmount();
256
+ });
257
+
258
+ it.skip("falls back to global typeSpeed when not specified per message", async () => {
259
+ const wrapper = await mountSuspended(SamaritanPromptMixed, {
260
+ props: {
261
+ messageConfigs: [{ text: "HI", effect: "typewriter" }],
262
+ typeSpeed: 150,
263
+ },
264
+ });
265
+ await nextTick(); // "H" immediate
266
+
267
+ vi.advanceTimersByTime(149); // just under global typeSpeed
268
+ await Promise.resolve();
269
+ await nextTick();
270
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("H");
271
+
272
+ vi.advanceTimersByTime(1); // complete 150ms
273
+ await Promise.resolve();
274
+ await nextTick();
275
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("HI");
276
+
277
+ wrapper.unmount();
278
+ });
279
+
280
+ it("per-message holdDuration overrides global default", async () => {
281
+ const wrapper = await mountSuspended(SamaritanPromptMixed, {
282
+ props: {
283
+ messageConfigs: [
284
+ { text: "H", effect: "typewriter", typeSpeed: 100, holdDuration: 200, deleteSpeed: 50, pauseDuration: 100 },
285
+ ],
286
+ holdDuration: 9999, // global — should not be used
287
+ },
288
+ });
289
+ await nextTick(); // "H" immediate
290
+
291
+ vi.advanceTimersByTime(100); // exit type loop
292
+ await Promise.resolve();
293
+ await nextTick();
294
+
295
+ vi.advanceTimersByTime(200); // per-message holdDuration fires (not 9999)
296
+ await Promise.resolve();
297
+ await nextTick();
298
+
299
+ vi.advanceTimersByTime(50); // delete
300
+ await Promise.resolve();
301
+ await nextTick();
302
+
303
+ expect(wrapper.find(".samaritan-prompt__text").text()).toBe("");
304
+ wrapper.unmount();
305
+ });
306
+ });