@syntrologie/adapt-nav 2.4.0 → 2.5.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 (63) hide show
  1. package/dist/NavWidget.d.ts +3 -3
  2. package/dist/NavWidget.d.ts.map +1 -1
  3. package/dist/NavWidget.js +47 -81
  4. package/dist/cdn.d.ts.map +1 -1
  5. package/dist/editor.d.ts.map +1 -1
  6. package/dist/editor.js +50 -20
  7. package/dist/runtime.d.ts +1 -1
  8. package/dist/runtime.d.ts.map +1 -1
  9. package/dist/runtime.js +5 -4
  10. package/dist/schema.d.ts +554 -95
  11. package/dist/schema.d.ts.map +1 -1
  12. package/dist/schema.js +6 -8
  13. package/dist/summarize.d.ts +2 -2
  14. package/dist/summarize.d.ts.map +1 -1
  15. package/dist/summarize.js +5 -5
  16. package/dist/types.d.ts +7 -49
  17. package/dist/types.d.ts.map +1 -1
  18. package/dist/types.js +1 -1
  19. package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +105 -2
  20. package/node_modules/@syntrologie/sdk-contracts/dist/index.js +5 -3
  21. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +798 -1
  22. package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +21 -1
  23. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts +2 -0
  24. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts.map +1 -0
  25. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.js +224 -0
  26. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +102 -0
  27. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.js +58 -6
  28. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +18 -0
  29. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +61 -2
  30. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +478 -7
  31. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +54 -0
  32. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts +2 -0
  33. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts.map +1 -0
  34. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.js +257 -0
  35. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts +2 -0
  36. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts.map +1 -0
  37. package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.js +1015 -0
  38. package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +1 -1
  39. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +4 -4
  40. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +1 -1
  41. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +2 -2
  42. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +2 -1
  43. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +1 -1
  44. package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +20 -3
  45. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +10 -8
  46. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +1 -1
  47. package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +350 -87
  48. package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +1 -1
  49. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +3 -3
  50. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +1 -1
  51. package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +1 -1
  52. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +1 -1
  53. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
  54. package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +5 -2
  55. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +24 -0
  56. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -0
  57. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/{useShowWhenStatus.js → useTriggerWhenStatus.js} +18 -15
  58. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +3 -3
  59. package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +1 -1
  60. package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +1 -1
  61. package/package.json +1 -1
  62. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts +0 -24
  63. package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts.map +0 -1
@@ -6,6 +6,15 @@
6
6
  */
7
7
  import { z } from 'zod';
8
8
  // =============================================================================
9
+ // ANCHOR ID SCHEMA
10
+ // =============================================================================
11
+ export const AnchorIdZ = z
12
+ .object({
13
+ selector: z.string(),
14
+ route: z.union([z.string(), z.array(z.string())]),
15
+ })
16
+ .strict();
17
+ // =============================================================================
9
18
  // CONDITION SCHEMAS
10
19
  // =============================================================================
11
20
  export const PageUrlConditionZ = z.object({
@@ -60,12 +69,21 @@ export const FrequencyLimitConditionZ = z.object({
60
69
  limit: z.number(),
61
70
  inverted: z.boolean().optional(),
62
71
  });
72
+ export const MatchOpZ = z.object({
73
+ equals: z.union([z.string(), z.number(), z.boolean()]).optional(),
74
+ contains: z.string().optional(),
75
+ });
76
+ export const CounterDefZ = z.object({
77
+ events: z.array(z.string()).min(1),
78
+ match: z.record(z.string(), MatchOpZ).optional(),
79
+ });
63
80
  export const EventCountConditionZ = z.object({
64
81
  type: z.literal('event_count'),
65
82
  key: z.string(),
66
83
  operator: z.enum(['gte', 'lte', 'eq', 'gt', 'lt']),
67
84
  count: z.number().int().min(0),
68
85
  withinMs: z.number().positive().optional(),
86
+ counter: CounterDefZ.optional(),
69
87
  });
70
88
  export const ConditionZ = z.discriminatedUnion('type', [
71
89
  PageUrlConditionZ,
@@ -119,6 +137,8 @@ export const DecisionStrategyZ = z.discriminatedUnion('type', [
119
137
  ModelStrategyZ,
120
138
  ExternalStrategyZ,
121
139
  ]);
140
+ /** Canonical Zod schema for the optional triggerWhen field on actions and adaptive items. */
141
+ export const TriggerWhenZ = DecisionStrategyZ.nullable().optional();
122
142
  // =============================================================================
123
143
  // EVENT SCOPE SCHEMA
124
144
  // =============================================================================
@@ -131,7 +151,7 @@ export const EventScopeZ = z.object({
131
151
  // =============================================================================
132
152
  // NOTIFY SCHEMA
133
153
  // =============================================================================
134
- /** Toast notification config for showWhen transitions. */
154
+ /** Toast notification config for triggerWhen transitions. */
135
155
  export const NotifyZ = z
136
156
  .object({
137
157
  title: z.string().optional(),
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=AnchorPicker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnchorPicker.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/AnchorPicker.test.tsx"],"names":[],"mappings":""}
@@ -0,0 +1,224 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { act, render } from '@testing-library/react';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { AnchorPicker } from '../components/AnchorPicker';
5
+ // Mock ResizeObserver for jsdom
6
+ vi.stubGlobal('ResizeObserver', class {
7
+ observe() { }
8
+ unobserve() { }
9
+ disconnect() { }
10
+ });
11
+ // Mock selectorGenerator utilities
12
+ vi.mock('../utils/selectorGenerator', () => ({
13
+ generateSelector: vi.fn((el) => `mock-selector-${el.tagName.toLowerCase()}`),
14
+ validateSelector: vi.fn(() => true),
15
+ getElementDescription: vi.fn((el) => `mock-desc-${el.tagName.toLowerCase()}`),
16
+ }));
17
+ import { generateSelector, getElementDescription, validateSelector, } from '../utils/selectorGenerator';
18
+ describe('AnchorPicker', () => {
19
+ let mockElementAtPoint;
20
+ beforeEach(() => {
21
+ mockElementAtPoint = document.createElement('div');
22
+ mockElementAtPoint.textContent = 'Target Element';
23
+ document.body.appendChild(mockElementAtPoint);
24
+ vi.spyOn(mockElementAtPoint, 'getBoundingClientRect').mockReturnValue({
25
+ top: 100,
26
+ left: 200,
27
+ width: 300,
28
+ height: 150,
29
+ bottom: 250,
30
+ right: 500,
31
+ x: 200,
32
+ y: 100,
33
+ toJSON: () => ({}),
34
+ });
35
+ vi.mocked(generateSelector).mockClear();
36
+ vi.mocked(validateSelector).mockClear();
37
+ vi.mocked(getElementDescription).mockClear();
38
+ vi.mocked(generateSelector).mockImplementation((el) => `mock-selector-${el.tagName.toLowerCase()}`);
39
+ vi.mocked(validateSelector).mockReturnValue(true);
40
+ vi.mocked(getElementDescription).mockImplementation((el) => `mock-desc-${el.tagName.toLowerCase()}`);
41
+ // Stub elementFromPoint on document (jsdom does not define it)
42
+ if (!document.elementFromPoint) {
43
+ document.elementFromPoint = vi.fn(() => null);
44
+ }
45
+ else {
46
+ vi.spyOn(document, 'elementFromPoint').mockReturnValue(null);
47
+ }
48
+ });
49
+ afterEach(() => {
50
+ if (mockElementAtPoint.parentNode) {
51
+ mockElementAtPoint.remove();
52
+ }
53
+ vi.restoreAllMocks();
54
+ });
55
+ it('renders nothing when isActive is false', () => {
56
+ render(_jsx(AnchorPicker, { isActive: false, onPick: vi.fn(), onCancel: vi.fn() }));
57
+ const picker = document.body.querySelector('[data-syntro-anchor-picker]');
58
+ expect(picker).toBeNull();
59
+ });
60
+ it('renders overlay portal to document.body when active', () => {
61
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
62
+ const picker = document.body.querySelector('[data-syntro-anchor-picker]');
63
+ expect(picker).toBeTruthy();
64
+ unmount();
65
+ });
66
+ it('sets crosshair cursor on overlay', () => {
67
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
68
+ const picker = document.body.querySelector('[data-syntro-anchor-picker]');
69
+ expect(picker.style.cursor).toBe('crosshair');
70
+ unmount();
71
+ });
72
+ it('sets highest z-index on overlay', () => {
73
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
74
+ const picker = document.body.querySelector('[data-syntro-anchor-picker]');
75
+ expect(picker.style.zIndex).toBe('2147483644');
76
+ unmount();
77
+ });
78
+ it('calls onCancel when Escape key is pressed', () => {
79
+ const onCancel = vi.fn();
80
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: onCancel }));
81
+ act(() => {
82
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
83
+ });
84
+ expect(onCancel).toHaveBeenCalledTimes(1);
85
+ unmount();
86
+ });
87
+ it('does not call onCancel for non-Escape keys', () => {
88
+ const onCancel = vi.fn();
89
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: onCancel }));
90
+ act(() => {
91
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
92
+ });
93
+ expect(onCancel).not.toHaveBeenCalled();
94
+ unmount();
95
+ });
96
+ it('highlights element on mousemove via elementFromPoint', () => {
97
+ document.elementFromPoint.mockReturnValue(mockElementAtPoint);
98
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
99
+ act(() => {
100
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
101
+ });
102
+ // generateSelector should have been called with the element
103
+ expect(generateSelector).toHaveBeenCalledWith(mockElementAtPoint);
104
+ unmount();
105
+ });
106
+ it('clears hover state when elementFromPoint returns null', () => {
107
+ document.elementFromPoint.mockReturnValue(null);
108
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
109
+ act(() => {
110
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
111
+ });
112
+ // No highlight should be shown (just the overlay container + the semi-transparent bg div)
113
+ const picker = document.body.querySelector('[data-syntro-anchor-picker]');
114
+ expect(picker.children.length).toBe(1);
115
+ unmount();
116
+ });
117
+ it('excludes editor panel elements from picking', () => {
118
+ const editorPanel = document.createElement('div');
119
+ editorPanel.setAttribute('data-syntro-editor-panel', '');
120
+ const innerEl = document.createElement('span');
121
+ editorPanel.appendChild(innerEl);
122
+ document.body.appendChild(editorPanel);
123
+ document.elementFromPoint.mockReturnValue(innerEl);
124
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
125
+ act(() => {
126
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 100, bubbles: true }));
127
+ });
128
+ // Should not call generateSelector for excluded elements
129
+ expect(generateSelector).not.toHaveBeenCalled();
130
+ editorPanel.remove();
131
+ unmount();
132
+ });
133
+ it('excludes HTML, BODY, HEAD elements from picking', () => {
134
+ document.elementFromPoint.mockReturnValue(document.body);
135
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
136
+ act(() => {
137
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: 100, clientY: 100, bubbles: true }));
138
+ });
139
+ expect(generateSelector).not.toHaveBeenCalled();
140
+ unmount();
141
+ });
142
+ it('calls onPick with valid selector on click after hover', () => {
143
+ const onPick = vi.fn();
144
+ document.elementFromPoint.mockReturnValue(mockElementAtPoint);
145
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: onPick, onCancel: vi.fn() }));
146
+ // First hover to set the hovered element
147
+ act(() => {
148
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
149
+ });
150
+ // Then click
151
+ act(() => {
152
+ document.dispatchEvent(new MouseEvent('click', { clientX: 250, clientY: 150, bubbles: true }));
153
+ });
154
+ expect(onPick).toHaveBeenCalledTimes(1);
155
+ expect(onPick).toHaveBeenCalledWith({
156
+ element: mockElementAtPoint,
157
+ selector: 'mock-selector-div',
158
+ description: 'mock-desc-div',
159
+ });
160
+ unmount();
161
+ });
162
+ it('regenerates selector when validation fails on click', () => {
163
+ const onPick = vi.fn();
164
+ document.elementFromPoint.mockReturnValue(mockElementAtPoint);
165
+ vi.mocked(validateSelector).mockReturnValue(false);
166
+ vi.mocked(generateSelector).mockReturnValue('regenerated-selector');
167
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: onPick, onCancel: vi.fn() }));
168
+ // Hover to set element
169
+ act(() => {
170
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
171
+ });
172
+ // Click
173
+ act(() => {
174
+ document.dispatchEvent(new MouseEvent('click', { clientX: 250, clientY: 150, bubbles: true }));
175
+ });
176
+ expect(onPick).toHaveBeenCalledWith(expect.objectContaining({
177
+ selector: 'regenerated-selector',
178
+ }));
179
+ unmount();
180
+ });
181
+ it('does not call onPick when no element is hovered on click', () => {
182
+ const onPick = vi.fn();
183
+ document.elementFromPoint.mockReturnValue(null);
184
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: onPick, onCancel: vi.fn() }));
185
+ act(() => {
186
+ document.dispatchEvent(new MouseEvent('click', { clientX: 250, clientY: 150, bubbles: true }));
187
+ });
188
+ expect(onPick).not.toHaveBeenCalled();
189
+ unmount();
190
+ });
191
+ it('removes event listeners when deactivated', () => {
192
+ const onCancel = vi.fn();
193
+ const { rerender, unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: onCancel }));
194
+ // Deactivate
195
+ rerender(_jsx(AnchorPicker, { isActive: false, onPick: vi.fn(), onCancel: onCancel }));
196
+ // Escape should not trigger onCancel after deactivation
197
+ act(() => {
198
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
199
+ });
200
+ expect(onCancel).not.toHaveBeenCalled();
201
+ unmount();
202
+ });
203
+ it('temporarily disables pointer events on overlay during mousemove detection', () => {
204
+ const overlayPointerEvents = [];
205
+ document.elementFromPoint.mockImplementation(() => {
206
+ // Capture overlay pointer events state during elementFromPoint call
207
+ const overlay = document.body.querySelector('[data-syntro-anchor-picker]');
208
+ if (overlay) {
209
+ overlayPointerEvents.push(overlay.style.pointerEvents);
210
+ }
211
+ return mockElementAtPoint;
212
+ });
213
+ const { unmount } = render(_jsx(AnchorPicker, { isActive: true, onPick: vi.fn(), onCancel: vi.fn() }));
214
+ act(() => {
215
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: 250, clientY: 150, bubbles: true }));
216
+ });
217
+ // During elementFromPoint, overlay pointerEvents should be 'none'
218
+ expect(overlayPointerEvents).toContain('none');
219
+ // After the call, it should be restored to 'auto'
220
+ const overlay = document.body.querySelector('[data-syntro-anchor-picker]');
221
+ expect(overlay.style.pointerEvents).toBe('auto');
222
+ unmount();
223
+ });
224
+ });
@@ -155,4 +155,106 @@ describe('ConditionStatusLine', () => {
155
155
  expect(label.className).toContain('se-text-text-secondary');
156
156
  expect(label.className).not.toContain('se-text-text-tertiary');
157
157
  });
158
+ it('renders progress bar for single condition with progress data', () => {
159
+ const status = {
160
+ visible: false,
161
+ isFallback: false,
162
+ conditions: [
163
+ {
164
+ type: 'event_count',
165
+ passed: false,
166
+ formatted: {
167
+ label: 'views >= 5',
168
+ instruction: 'View pages 5+ times',
169
+ shortLabel: 'View 5+ times',
170
+ progress: { current: 2, target: 5, operator: 'gte' },
171
+ },
172
+ },
173
+ ],
174
+ };
175
+ const { container } = render(_jsx(ConditionStatusLine, { status: status }));
176
+ // Progress bar has a title with current/target
177
+ const progressBar = container.querySelector('[title]');
178
+ expect(progressBar).toBeTruthy();
179
+ expect(progressBar.title).toContain('2/5');
180
+ // Also shows numeric progress
181
+ expect(screen.getByText('2/5')).toBeTruthy();
182
+ });
183
+ it('renders progress bar at 100% with green color', () => {
184
+ const status = {
185
+ visible: true,
186
+ isFallback: false,
187
+ conditions: [
188
+ {
189
+ type: 'event_count',
190
+ passed: true,
191
+ formatted: {
192
+ label: 'clicks >= 3',
193
+ instruction: 'Click 3+ times',
194
+ shortLabel: '3+ clicks',
195
+ progress: { current: 5, target: 3, operator: 'gte' },
196
+ },
197
+ },
198
+ ],
199
+ };
200
+ const { container } = render(_jsx(ConditionStatusLine, { status: status }));
201
+ const progressBar = container.querySelector('[title]');
202
+ expect(progressBar).toBeTruthy();
203
+ // 100% cap
204
+ expect(progressBar.title).toContain('100%');
205
+ // The inner bar should have green background
206
+ const innerBar = progressBar.firstElementChild;
207
+ expect(innerBar.className).toContain('se-bg-green-4');
208
+ });
209
+ it('renders progress bar with blue color when not at 100%', () => {
210
+ const status = {
211
+ visible: false,
212
+ isFallback: false,
213
+ conditions: [
214
+ {
215
+ type: 'event_count',
216
+ passed: false,
217
+ formatted: {
218
+ label: 'clicks >= 10',
219
+ instruction: 'Click 10+ times',
220
+ shortLabel: '10+ clicks',
221
+ progress: { current: 3, target: 10, operator: 'gte' },
222
+ },
223
+ },
224
+ ],
225
+ };
226
+ const { container } = render(_jsx(ConditionStatusLine, { status: status }));
227
+ const progressBar = container.querySelector('[title]');
228
+ const innerBar = progressBar.firstElementChild;
229
+ expect(innerBar.className).toContain('se-bg-blue-4');
230
+ expect(innerBar.className).not.toContain('se-bg-green-4');
231
+ });
232
+ it('renders progress bar in expanded multi-condition rows', () => {
233
+ const status = {
234
+ visible: false,
235
+ isFallback: false,
236
+ conditions: [
237
+ {
238
+ type: 'page_url',
239
+ passed: true,
240
+ formatted: { label: '/p', instruction: 'A', shortLabel: 'A' },
241
+ },
242
+ {
243
+ type: 'event_count',
244
+ passed: false,
245
+ formatted: {
246
+ label: 'views >= 5',
247
+ instruction: 'B',
248
+ shortLabel: 'B',
249
+ progress: { current: 2, target: 5, operator: 'gte' },
250
+ },
251
+ },
252
+ ],
253
+ };
254
+ render(_jsx(ConditionStatusLine, { status: status }));
255
+ // Expand
256
+ fireEvent.click(screen.getByText('1 of 2 conditions met'));
257
+ // Progress bar should appear in the expanded section
258
+ expect(screen.getByText('2/5')).toBeTruthy();
259
+ });
158
260
  });
@@ -1,18 +1,70 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render } from '@testing-library/react';
3
- import { describe, expect, it } from 'vitest';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
4
  import { DetectionBadge } from '../components/DetectionBadge';
5
5
  describe('DetectionBadge', () => {
6
- it('shows green dot class when found is true', () => {
6
+ it('displays "GoTo" text', () => {
7
+ const { container } = render(_jsx(DetectionBadge, { found: true }));
8
+ expect(container.textContent).toBe('GoTo');
9
+ });
10
+ it('uses green text color when found is true', () => {
7
11
  const { container } = render(_jsx(DetectionBadge, { found: true }));
8
12
  const badge = container.firstElementChild;
9
- expect(badge.className).toContain('se-bg-green-4');
13
+ expect(badge.className).toContain('se-text-green-4');
10
14
  expect(badge.title).toBe('Found on this page');
11
15
  });
12
- it('shows gray dot class when found is false', () => {
16
+ it('uses muted text color when found is false', () => {
13
17
  const { container } = render(_jsx(DetectionBadge, { found: false }));
14
18
  const badge = container.firstElementChild;
15
- expect(badge.className).toContain('se-bg-text-tertiary');
19
+ expect(badge.className).toContain('se-text-text-tertiary');
16
20
  expect(badge.title).toBe('Not found on this page');
17
21
  });
22
+ it('renders a <span> when no onClick is provided', () => {
23
+ const { container } = render(_jsx(DetectionBadge, { found: true }));
24
+ const badge = container.firstElementChild;
25
+ expect(badge.tagName).toBe('SPAN');
26
+ });
27
+ it('renders a <button> when onClick is provided', () => {
28
+ const { container } = render(_jsx(DetectionBadge, { found: true, onClick: () => { } }));
29
+ const badge = container.firstElementChild;
30
+ expect(badge.tagName).toBe('BUTTON');
31
+ });
32
+ it('calls onClick when clicked', () => {
33
+ const onClick = vi.fn();
34
+ const { container } = render(_jsx(DetectionBadge, { found: true, onClick: onClick }));
35
+ const badge = container.firstElementChild;
36
+ fireEvent.click(badge);
37
+ expect(onClick).toHaveBeenCalledTimes(1);
38
+ });
39
+ it('calls stopPropagation so clicks do not bubble to parent card', () => {
40
+ const onClick = vi.fn();
41
+ const parentClick = vi.fn();
42
+ const { container } = render(
43
+ // biome-ignore lint/a11y/useKeyWithClickEvents: test wrapper
44
+ _jsx("div", { onClick: parentClick, children: _jsx(DetectionBadge, { found: false, onClick: onClick }) }));
45
+ const badge = container.querySelector('button');
46
+ fireEvent.click(badge);
47
+ expect(onClick).toHaveBeenCalledTimes(1);
48
+ expect(parentClick).not.toHaveBeenCalled();
49
+ });
50
+ it('shows "Click to scroll to element" title when onClick + found', () => {
51
+ const { container } = render(_jsx(DetectionBadge, { found: true, onClick: () => { } }));
52
+ const badge = container.firstElementChild;
53
+ expect(badge.title).toBe('Click to scroll to element');
54
+ });
55
+ it('shows "Click to navigate to page" title when onClick + not found', () => {
56
+ const { container } = render(_jsx(DetectionBadge, { found: false, onClick: () => { } }));
57
+ const badge = container.firstElementChild;
58
+ expect(badge.title).toBe('Click to navigate to page');
59
+ });
60
+ it('has green indicator dot when found', () => {
61
+ const { container } = render(_jsx(DetectionBadge, { found: true }));
62
+ const dot = container.querySelector('[data-indicator]');
63
+ expect(dot.className).toContain('se-bg-green-4');
64
+ });
65
+ it('has muted indicator dot when not found', () => {
66
+ const { container } = render(_jsx(DetectionBadge, { found: false }));
67
+ const dot = container.querySelector('[data-indicator]');
68
+ expect(dot.className).toContain('se-bg-text-tertiary');
69
+ });
18
70
  });
@@ -25,4 +25,22 @@ describe('DismissedSection', () => {
25
25
  fireEvent.click(getByText('Dismissed (1)'));
26
26
  expect(queryByText('content')).toBeNull();
27
27
  });
28
+ it('expands on Enter keydown', () => {
29
+ const { getByText, queryByText } = render(_jsx(DismissedSection, { count: 2, children: _jsx("span", { children: "keyboard content" }) }));
30
+ const button = getByText('Dismissed (2)').closest('[role="button"]');
31
+ fireEvent.keyDown(button, { key: 'Enter' });
32
+ expect(queryByText('keyboard content')).toBeTruthy();
33
+ });
34
+ it('expands on Space keydown', () => {
35
+ const { getByText, queryByText } = render(_jsx(DismissedSection, { count: 2, children: _jsx("span", { children: "space content" }) }));
36
+ const button = getByText('Dismissed (2)').closest('[role="button"]');
37
+ fireEvent.keyDown(button, { key: ' ' });
38
+ expect(queryByText('space content')).toBeTruthy();
39
+ });
40
+ it('does not expand on other key presses', () => {
41
+ const { getByText, queryByText } = render(_jsx(DismissedSection, { count: 2, children: _jsx("span", { children: "other key content" }) }));
42
+ const button = getByText('Dismissed (2)').closest('[role="button"]');
43
+ fireEvent.keyDown(button, { key: 'Tab' });
44
+ expect(queryByText('other key content')).toBeNull();
45
+ });
28
46
  });
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { render } from '@testing-library/react';
3
- import { describe, expect, it } from 'vitest';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+ import { describe, expect, it, vi } from 'vitest';
4
4
  import { EditorCard } from '../components/EditorCard';
5
5
  describe('EditorCard', () => {
6
6
  it('renders children', () => {
@@ -22,4 +22,63 @@ describe('EditorCard', () => {
22
22
  const card = container.firstElementChild;
23
23
  expect(card.className).toContain('se-border-border-primary');
24
24
  });
25
+ it('calls onClick when Enter key is pressed', () => {
26
+ const onClick = vi.fn();
27
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", onClick: onClick, children: "child" }));
28
+ const card = container.firstElementChild;
29
+ fireEvent.keyDown(card, { key: 'Enter' });
30
+ expect(onClick).toHaveBeenCalledTimes(1);
31
+ });
32
+ it('calls onClick when Space key is pressed', () => {
33
+ const onClick = vi.fn();
34
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", onClick: onClick, children: "child" }));
35
+ const card = container.firstElementChild;
36
+ fireEvent.keyDown(card, { key: ' ' });
37
+ expect(onClick).toHaveBeenCalledTimes(1);
38
+ });
39
+ it('does not call onClick on other key presses', () => {
40
+ const onClick = vi.fn();
41
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", onClick: onClick, children: "child" }));
42
+ const card = container.firstElementChild;
43
+ fireEvent.keyDown(card, { key: 'Tab' });
44
+ expect(onClick).not.toHaveBeenCalled();
45
+ });
46
+ it('has role="button" and tabIndex when onClick is provided', () => {
47
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", onClick: () => { }, children: "child" }));
48
+ const card = container.firstElementChild;
49
+ expect(card.getAttribute('role')).toBe('button');
50
+ expect(card.getAttribute('tabindex')).toBe('0');
51
+ });
52
+ it('does not have role or tabIndex when onClick is not provided', () => {
53
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", children: "child" }));
54
+ const card = container.firstElementChild;
55
+ expect(card.getAttribute('role')).toBeNull();
56
+ expect(card.getAttribute('tabindex')).toBeNull();
57
+ });
58
+ it('applies cursor-pointer and hover classes when onClick is provided', () => {
59
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", onClick: () => { }, children: "child" }));
60
+ const card = container.firstElementChild;
61
+ expect(card.className).toContain('se-cursor-pointer');
62
+ expect(card.className).toContain('hover:se-bg-sidebar-hover');
63
+ });
64
+ it('does not apply cursor-pointer when onClick is not provided', () => {
65
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", children: "child" }));
66
+ const card = container.firstElementChild;
67
+ expect(card.className).not.toContain('se-cursor-pointer');
68
+ });
69
+ it('calls onMouseEnter and onMouseLeave', () => {
70
+ const onMouseEnter = vi.fn();
71
+ const onMouseLeave = vi.fn();
72
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, children: "child" }));
73
+ const card = container.firstElementChild;
74
+ fireEvent.mouseEnter(card);
75
+ expect(onMouseEnter).toHaveBeenCalledTimes(1);
76
+ fireEvent.mouseLeave(card);
77
+ expect(onMouseLeave).toHaveBeenCalledTimes(1);
78
+ });
79
+ it('merges custom className', () => {
80
+ const { container } = render(_jsx(EditorCard, { itemKey: "k", className: "custom-class", children: "child" }));
81
+ const card = container.firstElementChild;
82
+ expect(card.className).toContain('custom-class');
83
+ });
25
84
  });