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.
- package/.claude/settings.local.json +6 -1
- package/.claude/skills/testing-add-unit-test.md +32 -0
- package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +230 -0
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +234 -0
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +368 -0
- package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +306 -0
- package/app/composables/tests/useCancellableTimer.spec.ts +135 -0
- package/app/composables/useCancellableTimer.ts +42 -0
- package/app/pages/samaritan.vue +332 -0
- package/package.json +1 -1
|
@@ -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
|
+
});
|