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.
- package/ARCHITECTURE-v2.md +225 -0
- package/dist/sdk.iife.js +348 -0
- package/package.json +22 -0
- package/src/__tests__/day1.test.ts +37 -0
- package/src/__tests__/day2.test.ts +447 -0
- package/src/__tests__/day3.test.ts +110 -0
- package/src/__tests__/day4.test.ts +115 -0
- package/src/__tests__/day5.test.ts +102 -0
- package/src/__tests__/snapshot-dom-collector.test.ts +153 -0
- package/src/__tests__/snapshot-sender.test.ts +111 -0
- package/src/__tests__/v2-integration.test.ts +305 -0
- package/src/__tests__/v2-positioner.test.ts +115 -0
- package/src/__tests__/v2-renderer.test.ts +189 -0
- package/src/__tests__/v2-types.test.ts +74 -0
- package/src/__tests__/week2-day1.test.ts +62 -0
- package/src/__tests__/week2-day2.test.ts +128 -0
- package/src/__tests__/week2-day3.test.ts +128 -0
- package/src/__tests__/week2-day4.test.ts +177 -0
- package/src/__tests__/week2-day5.test.ts +294 -0
- package/src/__tests__/week3-day1.test.ts +169 -0
- package/src/__tests__/week3-day2.test.ts +267 -0
- package/src/__tests__/week3-day3.test.ts +213 -0
- package/src/__tests__/week3-day4.test.ts +213 -0
- package/src/__tests__/week3-day5.test.ts +350 -0
- package/src/__tests__/week4-day1.test.ts +277 -0
- package/src/__tests__/week4-day2.test.ts +227 -0
- package/src/__tests__/week4-day3.test.ts +323 -0
- package/src/__tests__/week4-day4.test.ts +210 -0
- package/src/__tests__/week4-day5.test.ts +503 -0
- package/src/__tests__/week5-day1.test.ts +152 -0
- package/src/__tests__/week5-day2.test.ts +222 -0
- package/src/__tests__/week5-day3.test.ts +297 -0
- package/src/__tests__/week5-day4.test.ts +306 -0
- package/src/__tests__/week5-day5.test.ts +345 -0
- package/src/__tests__/week7-day5-api-flows.test.ts +353 -0
- package/src/auto-generate/context-collector.ts +47 -0
- package/src/auto-generate/flow-generator-client.ts +97 -0
- package/src/browser.ts +5 -0
- package/src/components/celebration.ts +44 -0
- package/src/components/checklist-css.ts +159 -0
- package/src/components/checklist.ts +295 -0
- package/src/components/modal-css.ts +96 -0
- package/src/components/modal.ts +171 -0
- package/src/components/shadow-host.ts +30 -0
- package/src/core/api-client.ts +39 -0
- package/src/core/api-flows.ts +204 -0
- package/src/core/config.ts +37 -0
- package/src/core/event-batcher.ts +169 -0
- package/src/core/sdk.ts +301 -0
- package/src/detection/user-detection.ts +55 -0
- package/src/index.ts +95 -0
- package/src/snapshot/dom-collector.ts +193 -0
- package/src/snapshot/sender.ts +105 -0
- package/src/storage/event-listener.ts +59 -0
- package/src/storage/progress-tracker.ts +78 -0
- package/src/styles/checklist-css.ts +159 -0
- package/src/styles/checklist.css +166 -0
- package/src/styles/modal-css.ts +96 -0
- package/src/styles/modal.css +102 -0
- package/src/utils/dom.ts +49 -0
- package/src/utils/fingerprint.ts +20 -0
- package/src/utils/logger.ts +17 -0
- package/src/v2/positioner.ts +105 -0
- package/src/v2/renderer.ts +287 -0
- package/src/v2/styles.ts +89 -0
- package/src/v2/types.ts +53 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +28 -0
- 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
|
+
});
|