onboardme-sdk 0.0.1

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.
Files changed (69) hide show
  1. package/ARCHITECTURE-v2.md +225 -0
  2. package/dist/sdk.iife.js +348 -0
  3. package/package.json +22 -0
  4. package/src/__tests__/day1.test.ts +37 -0
  5. package/src/__tests__/day2.test.ts +447 -0
  6. package/src/__tests__/day3.test.ts +110 -0
  7. package/src/__tests__/day4.test.ts +115 -0
  8. package/src/__tests__/day5.test.ts +102 -0
  9. package/src/__tests__/snapshot-dom-collector.test.ts +153 -0
  10. package/src/__tests__/snapshot-sender.test.ts +111 -0
  11. package/src/__tests__/v2-integration.test.ts +305 -0
  12. package/src/__tests__/v2-positioner.test.ts +115 -0
  13. package/src/__tests__/v2-renderer.test.ts +189 -0
  14. package/src/__tests__/v2-types.test.ts +74 -0
  15. package/src/__tests__/week2-day1.test.ts +62 -0
  16. package/src/__tests__/week2-day2.test.ts +128 -0
  17. package/src/__tests__/week2-day3.test.ts +128 -0
  18. package/src/__tests__/week2-day4.test.ts +177 -0
  19. package/src/__tests__/week2-day5.test.ts +294 -0
  20. package/src/__tests__/week3-day1.test.ts +169 -0
  21. package/src/__tests__/week3-day2.test.ts +267 -0
  22. package/src/__tests__/week3-day3.test.ts +213 -0
  23. package/src/__tests__/week3-day4.test.ts +213 -0
  24. package/src/__tests__/week3-day5.test.ts +350 -0
  25. package/src/__tests__/week4-day1.test.ts +277 -0
  26. package/src/__tests__/week4-day2.test.ts +227 -0
  27. package/src/__tests__/week4-day3.test.ts +323 -0
  28. package/src/__tests__/week4-day4.test.ts +210 -0
  29. package/src/__tests__/week4-day5.test.ts +503 -0
  30. package/src/__tests__/week5-day1.test.ts +152 -0
  31. package/src/__tests__/week5-day2.test.ts +222 -0
  32. package/src/__tests__/week5-day3.test.ts +297 -0
  33. package/src/__tests__/week5-day4.test.ts +306 -0
  34. package/src/__tests__/week5-day5.test.ts +345 -0
  35. package/src/__tests__/week7-day5-api-flows.test.ts +353 -0
  36. package/src/auto-generate/context-collector.ts +47 -0
  37. package/src/auto-generate/flow-generator-client.ts +97 -0
  38. package/src/browser.ts +5 -0
  39. package/src/components/celebration.ts +44 -0
  40. package/src/components/checklist-css.ts +159 -0
  41. package/src/components/checklist.ts +295 -0
  42. package/src/components/modal-css.ts +96 -0
  43. package/src/components/modal.ts +171 -0
  44. package/src/components/shadow-host.ts +30 -0
  45. package/src/core/api-client.ts +39 -0
  46. package/src/core/api-flows.ts +204 -0
  47. package/src/core/config.ts +37 -0
  48. package/src/core/event-batcher.ts +169 -0
  49. package/src/core/sdk.ts +301 -0
  50. package/src/detection/user-detection.ts +55 -0
  51. package/src/index.ts +95 -0
  52. package/src/snapshot/dom-collector.ts +193 -0
  53. package/src/snapshot/sender.ts +105 -0
  54. package/src/storage/event-listener.ts +59 -0
  55. package/src/storage/progress-tracker.ts +78 -0
  56. package/src/styles/checklist-css.ts +159 -0
  57. package/src/styles/checklist.css +166 -0
  58. package/src/styles/modal-css.ts +96 -0
  59. package/src/styles/modal.css +102 -0
  60. package/src/utils/dom.ts +49 -0
  61. package/src/utils/fingerprint.ts +20 -0
  62. package/src/utils/logger.ts +17 -0
  63. package/src/v2/positioner.ts +105 -0
  64. package/src/v2/renderer.ts +287 -0
  65. package/src/v2/styles.ts +89 -0
  66. package/src/v2/types.ts +53 -0
  67. package/tsconfig.json +11 -0
  68. package/vite.config.ts +28 -0
  69. package/vitest.config.ts +7 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Week 3 Day 4 — completionEvent Auto-Check
3
+ *
4
+ * Tests watchCompletionEvents() in isolation and through renderChecklist():
5
+ *
6
+ * watchCompletionEvents (unit):
7
+ * ✓ Returns an unsubscribe function
8
+ * ✓ track(matchingEvent) → onItemComplete called with the correct itemId
9
+ * ✓ track(nonMatchingEvent) → onItemComplete NOT called
10
+ * ✓ Item with no completionEvent is not watched
11
+ * ✓ After unsubscribe, track no longer triggers onItemComplete
12
+ * ✓ After unsubscribe, OnboardMe.track is restored to original
13
+ *
14
+ * Integration via renderChecklist:
15
+ * ✓ OnboardMe.track(matchingEvent) auto-checks the item visually
16
+ * ✓ Auto-check updates the progress label
17
+ * ✓ Auto-check calls markStepComplete (isStepComplete returns true)
18
+ * ✓ Second track call for the same event is idempotent
19
+ * ✓ Re-rendering cleans up the previous watcher (no double-fire)
20
+ */
21
+
22
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
23
+ import { watchCompletionEvents } from '../storage/event-listener.js';
24
+ import { renderChecklist } from '../components/checklist.js';
25
+ import { getShadowRoot } from '../components/shadow-host.js';
26
+ import { isStepComplete, clearProgress } from '../storage/progress-tracker.js';
27
+ import OnboardMe, { _resetIndex } from '../index.js';
28
+ import type { FlowConfig, FlowStep, ChecklistItem } from '@onboardme/types';
29
+ import type { Progress } from '../storage/progress-tracker.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Helpers
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const PID = 'day4-product';
36
+ const FID = 'day4-flow';
37
+
38
+ function makeItem(overrides: Partial<ChecklistItem> = {}): ChecklistItem {
39
+ return { id: 'item-1', label: 'Do the thing', order: 1, ...overrides };
40
+ }
41
+
42
+ function makeFlow(items: ChecklistItem[]): FlowConfig {
43
+ const step: FlowStep = {
44
+ id: 'cl-step',
45
+ type: 'checklist',
46
+ order: 1,
47
+ title: 'Getting started',
48
+ body: '',
49
+ items,
50
+ trigger: { type: 'page_load' },
51
+ completionGoal: 'done',
52
+ } as unknown as FlowStep;
53
+ return { id: FID, name: 'Onboarding', steps: [step], completionGoal: 'done', priority: 1 };
54
+ }
55
+
56
+ const EMPTY: Progress = { completedSteps: [], lastUpdated: 0 };
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Setup / teardown
60
+ // ---------------------------------------------------------------------------
61
+
62
+ // Keep a reference to the original track so we can always restore it
63
+ const originalTrack = OnboardMe.track;
64
+
65
+ let shadowRoot: ShadowRoot;
66
+
67
+ beforeEach(() => {
68
+ document.body.innerHTML = '';
69
+ localStorage.clear();
70
+ _resetIndex();
71
+ OnboardMe.track = originalTrack; // guarantee clean state before each test
72
+ shadowRoot = getShadowRoot();
73
+ });
74
+
75
+ afterEach(() => {
76
+ OnboardMe.track = originalTrack; // restore in case a test left a patch in place
77
+ });
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // watchCompletionEvents — unit tests
81
+ // ---------------------------------------------------------------------------
82
+
83
+ describe('Week 3 Day 4 — watchCompletionEvents (unit)', () => {
84
+ it('returns a function (the unsubscribe function)', () => {
85
+ const unsub = watchCompletionEvents(makeFlow([makeItem()]), () => {});
86
+ expect(typeof unsub).toBe('function');
87
+ unsub();
88
+ });
89
+
90
+ it('track with a matching completionEvent calls onItemComplete with the correct itemId', () => {
91
+ const items = [makeItem({ id: 'step-a', completionEvent: 'created_project' })];
92
+ const onComplete = vi.fn();
93
+ const unsub = watchCompletionEvents(makeFlow(items), onComplete);
94
+
95
+ OnboardMe.track('created_project');
96
+ expect(onComplete).toHaveBeenCalledOnce();
97
+ expect(onComplete).toHaveBeenCalledWith('step-a');
98
+ unsub();
99
+ });
100
+
101
+ it('track with a non-matching event does NOT call onItemComplete', () => {
102
+ const items = [makeItem({ id: 'step-a', completionEvent: 'created_project' })];
103
+ const onComplete = vi.fn();
104
+ const unsub = watchCompletionEvents(makeFlow(items), onComplete);
105
+
106
+ OnboardMe.track('some_other_event');
107
+ expect(onComplete).not.toHaveBeenCalled();
108
+ unsub();
109
+ });
110
+
111
+ it('item with no completionEvent is not watched', () => {
112
+ const items = [makeItem({ id: 'step-a' })]; // no completionEvent
113
+ const onComplete = vi.fn();
114
+ const unsub = watchCompletionEvents(makeFlow(items), onComplete);
115
+
116
+ OnboardMe.track('created_project');
117
+ expect(onComplete).not.toHaveBeenCalled();
118
+ unsub();
119
+ });
120
+
121
+ it('after unsubscribe, track no longer triggers onItemComplete', () => {
122
+ const items = [makeItem({ id: 'step-a', completionEvent: 'created_project' })];
123
+ const onComplete = vi.fn();
124
+ const unsub = watchCompletionEvents(makeFlow(items), onComplete);
125
+ unsub();
126
+
127
+ OnboardMe.track('created_project');
128
+ expect(onComplete).not.toHaveBeenCalled();
129
+ });
130
+
131
+ it('after unsubscribe, OnboardMe.track is restored to the original', () => {
132
+ const before = OnboardMe.track;
133
+ const unsub = watchCompletionEvents(makeFlow([makeItem()]), () => {});
134
+ // While patched, track should be a different function reference
135
+ expect(OnboardMe.track).not.toBe(before);
136
+ unsub();
137
+ expect(OnboardMe.track).toBe(before);
138
+ });
139
+ });
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Integration via renderChecklist
143
+ // ---------------------------------------------------------------------------
144
+
145
+ describe('Week 3 Day 4 — integration via renderChecklist', () => {
146
+ it('OnboardMe.track with matching event auto-checks the item visually', () => {
147
+ const items = [makeItem({ id: 'step-a', completionEvent: 'project_created', order: 1 })];
148
+ renderChecklist(makeFlow(items), EMPTY, shadowRoot, false, PID, FID);
149
+
150
+ OnboardMe.track('project_created');
151
+
152
+ const li = shadowRoot.querySelector('[data-item-id="step-a"]');
153
+ expect(li?.classList.contains('om-checklist__item--done')).toBe(true);
154
+ });
155
+
156
+ it('auto-check updates the progress label', () => {
157
+ const items = [
158
+ makeItem({ id: 'a', completionEvent: 'ev_a', order: 1 }),
159
+ makeItem({ id: 'b', order: 2 }),
160
+ ];
161
+ renderChecklist(makeFlow(items), EMPTY, shadowRoot, false, PID, FID);
162
+
163
+ OnboardMe.track('ev_a');
164
+
165
+ const label = shadowRoot.querySelector('[data-onboardme="progress-label"]');
166
+ expect(label?.textContent).toBe('1 of 2 complete');
167
+ });
168
+
169
+ it('auto-check calls markStepComplete — isStepComplete returns true', () => {
170
+ clearProgress(PID, FID);
171
+ const items = [makeItem({ id: 'step-x', completionEvent: 'did_thing', order: 1 })];
172
+ renderChecklist(makeFlow(items), EMPTY, shadowRoot, false, PID, FID);
173
+
174
+ OnboardMe.track('did_thing');
175
+
176
+ expect(isStepComplete(PID, FID, 'step-x')).toBe(true);
177
+ });
178
+
179
+ it('firing the same event twice is idempotent — no duplicate in completedSteps', () => {
180
+ const items = [makeItem({ id: 'step-y', completionEvent: 'did_thing', order: 1 })];
181
+ renderChecklist(makeFlow(items), EMPTY, shadowRoot, false, PID, FID);
182
+
183
+ OnboardMe.track('did_thing');
184
+ OnboardMe.track('did_thing');
185
+
186
+ const label = shadowRoot.querySelector('[data-onboardme="progress-label"]');
187
+ expect(label?.textContent).toBe('1 of 1 complete');
188
+ });
189
+
190
+ it('re-rendering cleans up the previous watcher — event fires only once', () => {
191
+ const onComplete = vi.fn();
192
+ const items = [makeItem({ id: 'step-z', completionEvent: 'my_event', order: 1 })];
193
+ const flow = makeFlow(items);
194
+
195
+ // Render once with a spy wired in via a wrapper
196
+ // We'll verify by checking that the item is only marked done once (no double-fire)
197
+ renderChecklist(flow, EMPTY, shadowRoot, false, PID, FID);
198
+ // Re-render — previous watcher should be unsubscribed
199
+ renderChecklist(flow, EMPTY, shadowRoot, false, PID, FID);
200
+
201
+ OnboardMe.track('my_event');
202
+
203
+ // If the old watcher were still alive, the item could be double-processed.
204
+ // Verify by checking the DOM — item should be done exactly once (not duplicated/glitched).
205
+ const li = shadowRoot.querySelector('[data-item-id="step-z"]');
206
+ expect(li?.classList.contains('om-checklist__item--done')).toBe(true);
207
+ // And only one panel exists (dedup guard still holds)
208
+ expect(shadowRoot.querySelectorAll('[data-onboardme="checklist"]')).toHaveLength(1);
209
+
210
+ // Suppress unused var warning
211
+ void onComplete;
212
+ });
213
+ });
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Week 3 Day 5 — Full integration tests + Week 3 verification checklist
3
+ *
4
+ * Exercises every item on the Week 3 checklist end-to-end:
5
+ * ✓ Checklist renders — items in order sequence, correct initial checked state
6
+ * ✓ Manual check — item checks, progress bar updates immediately
7
+ * ✓ Page reload — checked items still checked, progress bar correct
8
+ * ✓ required: false item — optional badge shown, does not block "all done" state
9
+ * ✓ Auto-check via track() — matching item checks without click
10
+ * ✓ Collapse — panel collapses to pill; pill shows count; click expands
11
+ * ✓ >7 items — debug warn in console; items still render
12
+ * ✓ pnpm test — all unit tests pass (this file is the final gate)
13
+ * ✓ Full lifecycle: render → manual check → auto-check → all required done
14
+ *
15
+ * These tests wire together everything built in Days 1–4.
16
+ */
17
+
18
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
19
+ import { renderChecklist } from '../components/checklist.js';
20
+ import { getShadowRoot } from '../components/shadow-host.js';
21
+ import {
22
+ loadProgress,
23
+ markStepComplete,
24
+ clearProgress,
25
+ isStepComplete,
26
+ } from '../storage/progress-tracker.js';
27
+ import OnboardMe, { _resetIndex } from '../index.js';
28
+ import type { FlowConfig, FlowStep, ChecklistItem } from '@onboardme/types';
29
+ import type { Progress } from '../storage/progress-tracker.js';
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Shared test fixtures
33
+ // ---------------------------------------------------------------------------
34
+
35
+ const PID = 'w3-checklist';
36
+ const FID = 'w3-flow';
37
+
38
+ /**
39
+ * The reference flow used across all verification tests.
40
+ *
41
+ * Items (by order):
42
+ * 1. "Create your profile" — required, completionEvent: 'profile_created'
43
+ * 2. "Invite a teammate" — required, completionEvent: 'teammate_invited'
44
+ * 3. "Create first project" — required, no completionEvent (manual only)
45
+ * 4. "Explore the docs" — optional, no completionEvent
46
+ */
47
+ const REF_ITEMS: ChecklistItem[] = [
48
+ { id: 'profile', label: 'Create your profile', order: 1, required: true, completionEvent: 'profile_created' },
49
+ { id: 'teammate', label: 'Invite a teammate', order: 2, required: true, completionEvent: 'teammate_invited' },
50
+ { id: 'project', label: 'Create first project', order: 3, required: true },
51
+ { id: 'docs', label: 'Explore the docs', order: 4, required: false },
52
+ ];
53
+
54
+ function makeFlow(items: ChecklistItem[] = REF_ITEMS): FlowConfig {
55
+ const step: FlowStep = {
56
+ id: 'cl-step',
57
+ type: 'checklist',
58
+ order: 1,
59
+ title: 'Getting started',
60
+ body: '',
61
+ items,
62
+ trigger: { type: 'page_load' },
63
+ completionGoal: 'onboarding_complete',
64
+ } as unknown as FlowStep;
65
+ return {
66
+ id: FID,
67
+ name: 'Onboarding checklist',
68
+ steps: [step],
69
+ completionGoal: 'onboarding_complete',
70
+ priority: 1,
71
+ };
72
+ }
73
+
74
+ const EMPTY: Progress = { completedSteps: [], lastUpdated: 0 };
75
+
76
+ function click(el: Element | null): void {
77
+ el?.dispatchEvent(new MouseEvent('click', { bubbles: true }));
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Setup / teardown
82
+ // ---------------------------------------------------------------------------
83
+
84
+ const originalTrack = OnboardMe.track;
85
+ let shadowRoot: ShadowRoot;
86
+
87
+ beforeEach(() => {
88
+ document.body.innerHTML = '';
89
+ localStorage.clear();
90
+ _resetIndex();
91
+ OnboardMe.track = originalTrack;
92
+ shadowRoot = getShadowRoot();
93
+ clearProgress(PID, FID);
94
+ });
95
+
96
+ afterEach(() => {
97
+ OnboardMe.track = originalTrack;
98
+ });
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Verification checklist item 1 — Checklist renders
102
+ // ---------------------------------------------------------------------------
103
+
104
+ describe('W3 checklist: Checklist renders', () => {
105
+ it('items appear in order field sequence regardless of array order', () => {
106
+ // Pass items shuffled — should still render 1→2→3→4
107
+ const shuffled = [REF_ITEMS[2], REF_ITEMS[0], REF_ITEMS[3], REF_ITEMS[1]];
108
+ renderChecklist(makeFlow(shuffled), EMPTY, shadowRoot, false, PID, FID);
109
+
110
+ const rendered = shadowRoot.querySelectorAll('[data-item-id]');
111
+ expect(rendered[0]?.getAttribute('data-item-id')).toBe('profile');
112
+ expect(rendered[1]?.getAttribute('data-item-id')).toBe('teammate');
113
+ expect(rendered[2]?.getAttribute('data-item-id')).toBe('project');
114
+ expect(rendered[3]?.getAttribute('data-item-id')).toBe('docs');
115
+ });
116
+
117
+ it('already-completed items are shown as done on first render', () => {
118
+ const progress: Progress = { completedSteps: ['profile', 'teammate'], lastUpdated: Date.now() };
119
+ renderChecklist(makeFlow(), progress, shadowRoot, false, PID, FID);
120
+
121
+ expect(shadowRoot.querySelector('[data-item-id="profile"]')?.classList.contains('om-checklist__item--done')).toBe(true);
122
+ expect(shadowRoot.querySelector('[data-item-id="teammate"]')?.classList.contains('om-checklist__item--done')).toBe(true);
123
+ expect(shadowRoot.querySelector('[data-item-id="project"]')?.classList.contains('om-checklist__item--done')).toBe(false);
124
+ });
125
+ });
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Verification checklist item 2 — Manual check
129
+ // ---------------------------------------------------------------------------
130
+
131
+ describe('W3 checklist: Manual check', () => {
132
+ it('clicking an item marks it done and updates the progress bar immediately', () => {
133
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
134
+
135
+ click(shadowRoot.querySelector('[data-item-id="project"]'));
136
+
137
+ const li = shadowRoot.querySelector('[data-item-id="project"]');
138
+ expect(li?.classList.contains('om-checklist__item--done')).toBe(true);
139
+
140
+ // 1 of 3 required done → 33%
141
+ const fill = shadowRoot.querySelector<HTMLElement>('.om-checklist__progress-bar-fill');
142
+ expect(fill?.style.width).toBe('33%');
143
+ });
144
+
145
+ it('clicking an item updates the progress label immediately', () => {
146
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
147
+ click(shadowRoot.querySelector('[data-item-id="project"]'));
148
+
149
+ const label = shadowRoot.querySelector('[data-onboardme="progress-label"]');
150
+ expect(label?.textContent).toBe('1 of 4 complete');
151
+ });
152
+ });
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Verification checklist item 3 — Page reload
156
+ // ---------------------------------------------------------------------------
157
+
158
+ describe('W3 checklist: Page reload', () => {
159
+ it('checked items are still checked after re-render with saved progress', () => {
160
+ // Mark two items complete via the storage layer (as a click would do)
161
+ markStepComplete(PID, FID, 'profile');
162
+ markStepComplete(PID, FID, 'project');
163
+
164
+ // Simulate reload: read from storage, re-render
165
+ const saved = loadProgress(PID, FID);
166
+ renderChecklist(makeFlow(), saved, shadowRoot, false, PID, FID);
167
+
168
+ expect(shadowRoot.querySelector('[data-item-id="profile"]')?.classList.contains('om-checklist__item--done')).toBe(true);
169
+ expect(shadowRoot.querySelector('[data-item-id="project"]')?.classList.contains('om-checklist__item--done')).toBe(true);
170
+ expect(shadowRoot.querySelector('[data-item-id="teammate"]')?.classList.contains('om-checklist__item--done')).toBe(false);
171
+ });
172
+
173
+ it('progress bar is correct after reload reflecting saved state', () => {
174
+ markStepComplete(PID, FID, 'profile');
175
+ markStepComplete(PID, FID, 'teammate');
176
+
177
+ const saved = loadProgress(PID, FID);
178
+ renderChecklist(makeFlow(), saved, shadowRoot, false, PID, FID);
179
+
180
+ // 2 of 3 required → 67%
181
+ const fill = shadowRoot.querySelector<HTMLElement>('.om-checklist__progress-bar-fill');
182
+ expect(fill?.style.width).toBe('67%');
183
+ });
184
+ });
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Verification checklist item 4 — required: false item
188
+ // ---------------------------------------------------------------------------
189
+
190
+ describe('W3 checklist: required: false item', () => {
191
+ it('optional item shows the "Optional" badge', () => {
192
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
193
+ const badge = shadowRoot.querySelector('[data-item-id="docs"] .om-badge');
194
+ expect(badge?.textContent).toBe('Optional');
195
+ });
196
+
197
+ it('completing all required items fills bar to 100% even if optional item is unchecked', () => {
198
+ const progress: Progress = {
199
+ completedSteps: ['profile', 'teammate', 'project'], // docs (optional) not done
200
+ lastUpdated: Date.now(),
201
+ };
202
+ renderChecklist(makeFlow(), progress, shadowRoot, false, PID, FID);
203
+
204
+ const fill = shadowRoot.querySelector<HTMLElement>('.om-checklist__progress-bar-fill');
205
+ expect(fill?.style.width).toBe('100%');
206
+ });
207
+
208
+ it('optional item does not block the required bar — clicking it leaves fill unchanged', () => {
209
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
210
+ click(shadowRoot.querySelector('[data-item-id="docs"]'));
211
+
212
+ // Required bar should still be 0% (no required item done)
213
+ const fill = shadowRoot.querySelector<HTMLElement>('.om-checklist__progress-bar-fill');
214
+ expect(fill?.style.width).toBe('0%');
215
+
216
+ // But label counts all items — 1 of 4 complete
217
+ const label = shadowRoot.querySelector('[data-onboardme="progress-label"]');
218
+ expect(label?.textContent).toBe('1 of 4 complete');
219
+ });
220
+ });
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Verification checklist item 5 — Auto-check via track()
224
+ // ---------------------------------------------------------------------------
225
+
226
+ describe('W3 checklist: Auto-check via track()', () => {
227
+ it('calling OnboardMe.track(completionEvent) checks the matching item without a click', () => {
228
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
229
+
230
+ OnboardMe.track('profile_created');
231
+
232
+ const li = shadowRoot.querySelector('[data-item-id="profile"]');
233
+ expect(li?.classList.contains('om-checklist__item--done')).toBe(true);
234
+ });
235
+
236
+ it('auto-check persists to localStorage', () => {
237
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
238
+ OnboardMe.track('teammate_invited');
239
+ expect(isStepComplete(PID, FID, 'teammate')).toBe(true);
240
+ });
241
+
242
+ it('auto-check updates the progress label and bar', () => {
243
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
244
+ OnboardMe.track('profile_created');
245
+
246
+ const label = shadowRoot.querySelector('[data-onboardme="progress-label"]');
247
+ const fill = shadowRoot.querySelector<HTMLElement>('.om-checklist__progress-bar-fill');
248
+ expect(label?.textContent).toBe('1 of 4 complete');
249
+ expect(fill?.style.width).toBe('33%');
250
+ });
251
+ });
252
+
253
+ // ---------------------------------------------------------------------------
254
+ // Verification checklist item 6 — Collapse
255
+ // ---------------------------------------------------------------------------
256
+
257
+ describe('W3 checklist: Collapse', () => {
258
+ it('collapse button shrinks panel to pill and pill shows completion count', () => {
259
+ const progress: Progress = { completedSteps: ['profile'], lastUpdated: Date.now() };
260
+ renderChecklist(makeFlow(), progress, shadowRoot, false, PID, FID);
261
+
262
+ click(shadowRoot.querySelector('[data-onboardme="checklist-collapse"]'));
263
+
264
+ const panel = shadowRoot.querySelector('[data-onboardme="checklist"]');
265
+ expect(panel?.classList.contains('om-checklist--collapsed')).toBe(true);
266
+
267
+ const count = shadowRoot.querySelector('[data-onboardme="collapsed-count"]');
268
+ expect(count?.textContent).toBe('1 / 4');
269
+ });
270
+
271
+ it('clicking the collapsed pill expands the panel', () => {
272
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
273
+ const panel = shadowRoot.querySelector('[data-onboardme="checklist"]');
274
+
275
+ click(shadowRoot.querySelector('[data-onboardme="checklist-collapse"]'));
276
+ expect(panel?.classList.contains('om-checklist--collapsed')).toBe(true);
277
+
278
+ click(panel);
279
+ expect(panel?.classList.contains('om-checklist--collapsed')).toBe(false);
280
+ });
281
+ });
282
+
283
+ // ---------------------------------------------------------------------------
284
+ // Verification checklist item 7 — >7 items
285
+ // ---------------------------------------------------------------------------
286
+
287
+ describe('W3 checklist: >7 items', () => {
288
+ it('logs a console.warn when more than 7 items exist (debug mode)', () => {
289
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
290
+ const bigItems = Array.from({ length: 8 }, (_, i) => ({
291
+ id: `item-${i}`,
292
+ label: `Item ${i}`,
293
+ order: i + 1,
294
+ }));
295
+ renderChecklist(makeFlow(bigItems), EMPTY, shadowRoot, true, PID, FID);
296
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('more than 7 items'));
297
+ warnSpy.mockRestore();
298
+ });
299
+
300
+ it('all items still render even when >7 are present', () => {
301
+ const bigItems = Array.from({ length: 8 }, (_, i) => ({
302
+ id: `item-${i}`,
303
+ label: `Item ${i}`,
304
+ order: i + 1,
305
+ }));
306
+ renderChecklist(makeFlow(bigItems), EMPTY, shadowRoot, true, PID, FID);
307
+ expect(shadowRoot.querySelectorAll('.om-checklist__item')).toHaveLength(8);
308
+ });
309
+ });
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // Full lifecycle test
313
+ // ---------------------------------------------------------------------------
314
+
315
+ describe('W3 checklist: Full Week 3 lifecycle', () => {
316
+ it('render → manual check → auto-check → all required done → bar at 100%', () => {
317
+ // 1. Render with no prior progress
318
+ renderChecklist(makeFlow(), EMPTY, shadowRoot, false, PID, FID);
319
+ expect(shadowRoot.querySelector('[data-onboardme="progress-label"]')?.textContent)
320
+ .toBe('0 of 4 complete');
321
+
322
+ // 2. Manual check: click "Create first project" (required, no completionEvent)
323
+ click(shadowRoot.querySelector('[data-item-id="project"]'));
324
+ expect(shadowRoot.querySelector('[data-item-id="project"]')?.classList.contains('om-checklist__item--done')).toBe(true);
325
+
326
+ // 3. Auto-check "Create your profile" via track
327
+ OnboardMe.track('profile_created');
328
+ expect(shadowRoot.querySelector('[data-item-id="profile"]')?.classList.contains('om-checklist__item--done')).toBe(true);
329
+
330
+ // 4. Auto-check "Invite a teammate" via track
331
+ OnboardMe.track('teammate_invited');
332
+ expect(shadowRoot.querySelector('[data-item-id="teammate"]')?.classList.contains('om-checklist__item--done')).toBe(true);
333
+
334
+ // 5. All 3 required items done → progress bar at 100%
335
+ const fill = shadowRoot.querySelector<HTMLElement>('.om-checklist__progress-bar-fill');
336
+ expect(fill?.style.width).toBe('100%');
337
+
338
+ // 6. Label shows 3 of 4 complete (optional "docs" not checked)
339
+ expect(shadowRoot.querySelector('[data-onboardme="progress-label"]')?.textContent)
340
+ .toBe('3 of 4 complete');
341
+
342
+ // 7. Simulate reload — all three required steps persisted in localStorage
343
+ expect(isStepComplete(PID, FID, 'profile')).toBe(true);
344
+ expect(isStepComplete(PID, FID, 'teammate')).toBe(true);
345
+ expect(isStepComplete(PID, FID, 'project')).toBe(true);
346
+
347
+ const saved = loadProgress(PID, FID);
348
+ expect(saved.completedSteps).toHaveLength(3);
349
+ });
350
+ });