@syntrologie/adapt-nav 2.4.1 → 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.
- package/dist/NavWidget.d.ts +3 -3
- package/dist/NavWidget.d.ts.map +1 -1
- package/dist/NavWidget.js +47 -81
- package/dist/cdn.d.ts.map +1 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +50 -20
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -4
- package/dist/schema.d.ts +554 -95
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -8
- package/dist/summarize.d.ts +2 -2
- package/dist/summarize.d.ts.map +1 -1
- package/dist/summarize.js +5 -5
- package/dist/types.d.ts +7 -49
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/node_modules/@syntrologie/sdk-contracts/dist/index.d.ts +105 -2
- package/node_modules/@syntrologie/sdk-contracts/dist/index.js +5 -3
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.d.ts +798 -1
- package/node_modules/@syntrologie/sdk-contracts/dist/schemas.js +21 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/AnchorPicker.test.js +224 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +102 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DetectionBadge.test.js +58 -6
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +18 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +61 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +478 -7
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +54 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/selectorGenerator.test.js +257 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/useTriggerWhenStatus.test.js +1015 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts +4 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +2 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts +2 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/DetectionBadge.js +20 -3
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts +10 -8
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorPanelShell.js +350 -87
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts +3 -3
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/TriggerJourney.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +5 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts +24 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useTriggerWhenStatus.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/{useShowWhenStatus.js → useTriggerWhenStatus.js} +18 -15
- package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +3 -3
- package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/index.js +1 -1
- package/package.json +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/hooks/useShowWhenStatus.d.ts +0 -24
- 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
|
|
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 @@
|
|
|
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
|
+
});
|
package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js
CHANGED
|
@@ -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('
|
|
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-
|
|
13
|
+
expect(badge.className).toContain('se-text-green-4');
|
|
10
14
|
expect(badge.title).toBe('Found on this page');
|
|
11
15
|
});
|
|
12
|
-
it('
|
|
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-
|
|
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
|
});
|