@syntrologie/adapt-content 2.2.0-canary.9 → 2.2.0
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/reconciliation-guard.d.ts +29 -0
- package/dist/reconciliation-guard.d.ts.map +1 -0
- package/dist/reconciliation-guard.js +72 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +24 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/BeforeAfterToggle.test.js +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.js +158 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/DismissedSection.test.js +6 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorCard.test.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorHeader.test.js +4 -5
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.d.ts +2 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.d.ts.map +1 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.js +25 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ElementHighlight.test.js +22 -0
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/TriggerJourney.test.js +77 -14
- package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/formatConditionLabel.test.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts +1 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/AnchorPicker.js +4 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.js +4 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ConditionStatusLine.js +5 -5
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/DismissedSection.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditBackButton.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorCard.js +10 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorFooter.js +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorInput.js +5 -2
- 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 +4 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorSelect.js +5 -2
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/EditorTextarea.js +6 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/components/ElementHighlight.js +24 -12
- 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 +4 -4
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.d.ts.map +1 -1
- package/node_modules/@syntrologie/shared-editor-ui/dist/formatConditionLabel.js +12 -20
- package/node_modules/@syntrologie/shared-editor-ui/dist/index.d.ts +1 -1
- 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
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliation Guard - MutationObserver defense against React DOM removal.
|
|
3
|
+
*
|
|
4
|
+
* When the Syntrologie SDK inserts DOM nodes into a React-managed subtree
|
|
5
|
+
* (via content:insertHtml), React's reconciliation will silently remove them
|
|
6
|
+
* on the next render because they don't exist in React's virtual DOM.
|
|
7
|
+
*
|
|
8
|
+
* This guard watches for the removal of our inserted container and re-inserts
|
|
9
|
+
* it using a debounced retry mechanism with a maximum retry count to prevent
|
|
10
|
+
* infinite loops (e.g., in React StrictMode which double-invokes effects).
|
|
11
|
+
*/
|
|
12
|
+
export interface ReconciliationGuardOptions {
|
|
13
|
+
/** Maximum re-insertion attempts before giving up. Default: 3 */
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
/** Debounce interval in ms to coalesce rapid removals. Default: 50 */
|
|
16
|
+
debounceMs?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Watch for a container element being removed from the DOM by an external
|
|
20
|
+
* framework (React, Vue, etc.) and call `reinsertFn` to re-insert it.
|
|
21
|
+
*
|
|
22
|
+
* @param container The element we inserted (has data-syntro-action-id)
|
|
23
|
+
* @param anchor The anchor element our container is positioned relative to
|
|
24
|
+
* @param reinsertFn Called when the container is removed — should re-insert it
|
|
25
|
+
* @param opts Configuration for retry limits and debounce timing
|
|
26
|
+
* @returns Cleanup function that disconnects the observer
|
|
27
|
+
*/
|
|
28
|
+
export declare function guardAgainstReconciliation(container: HTMLElement, anchor: HTMLElement, reinsertFn: () => void, opts?: ReconciliationGuardOptions): () => void;
|
|
29
|
+
//# sourceMappingURL=reconciliation-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconciliation-guard.d.ts","sourceRoot":"","sources":["../src/reconciliation-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,0BAA0B;IACzC,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,EAAE,WAAW,EACtB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,MAAM,IAAI,EACtB,IAAI,CAAC,EAAE,0BAA0B,GAChC,MAAM,IAAI,CAoDZ"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliation Guard - MutationObserver defense against React DOM removal.
|
|
3
|
+
*
|
|
4
|
+
* When the Syntrologie SDK inserts DOM nodes into a React-managed subtree
|
|
5
|
+
* (via content:insertHtml), React's reconciliation will silently remove them
|
|
6
|
+
* on the next render because they don't exist in React's virtual DOM.
|
|
7
|
+
*
|
|
8
|
+
* This guard watches for the removal of our inserted container and re-inserts
|
|
9
|
+
* it using a debounced retry mechanism with a maximum retry count to prevent
|
|
10
|
+
* infinite loops (e.g., in React StrictMode which double-invokes effects).
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Watch for a container element being removed from the DOM by an external
|
|
14
|
+
* framework (React, Vue, etc.) and call `reinsertFn` to re-insert it.
|
|
15
|
+
*
|
|
16
|
+
* @param container The element we inserted (has data-syntro-action-id)
|
|
17
|
+
* @param anchor The anchor element our container is positioned relative to
|
|
18
|
+
* @param reinsertFn Called when the container is removed — should re-insert it
|
|
19
|
+
* @param opts Configuration for retry limits and debounce timing
|
|
20
|
+
* @returns Cleanup function that disconnects the observer
|
|
21
|
+
*/
|
|
22
|
+
export function guardAgainstReconciliation(container, anchor, reinsertFn, opts) {
|
|
23
|
+
const maxRetries = opts?.maxRetries ?? 3;
|
|
24
|
+
const debounceMs = opts?.debounceMs ?? 50;
|
|
25
|
+
// Find the nearest parent to observe. Prefer container's parent, then anchor's.
|
|
26
|
+
const observeTarget = container.parentElement ?? anchor.parentElement;
|
|
27
|
+
if (!observeTarget)
|
|
28
|
+
return () => { };
|
|
29
|
+
let retries = 0;
|
|
30
|
+
let debounceTimer = null;
|
|
31
|
+
let disconnected = false;
|
|
32
|
+
const observer = new MutationObserver((mutations) => {
|
|
33
|
+
if (disconnected)
|
|
34
|
+
return;
|
|
35
|
+
for (const mutation of mutations) {
|
|
36
|
+
for (const removed of mutation.removedNodes) {
|
|
37
|
+
// Check if the removed node is our container
|
|
38
|
+
if (removed !== container)
|
|
39
|
+
continue;
|
|
40
|
+
if (retries >= maxRetries) {
|
|
41
|
+
observer.disconnect();
|
|
42
|
+
disconnected = true;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Debounce to coalesce rapid React re-renders
|
|
46
|
+
if (debounceTimer)
|
|
47
|
+
clearTimeout(debounceTimer);
|
|
48
|
+
debounceTimer = setTimeout(() => {
|
|
49
|
+
if (disconnected)
|
|
50
|
+
return;
|
|
51
|
+
retries++;
|
|
52
|
+
try {
|
|
53
|
+
reinsertFn();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Re-insertion failed — stop trying
|
|
57
|
+
observer.disconnect();
|
|
58
|
+
disconnected = true;
|
|
59
|
+
}
|
|
60
|
+
}, debounceMs);
|
|
61
|
+
return; // Found our container, no need to check further
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
observer.observe(observeTarget, { childList: true, subtree: true });
|
|
66
|
+
return () => {
|
|
67
|
+
disconnected = true;
|
|
68
|
+
observer.disconnect();
|
|
69
|
+
if (debounceTimer)
|
|
70
|
+
clearTimeout(debounceTimer);
|
|
71
|
+
};
|
|
72
|
+
}
|
package/dist/runtime.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EACV,cAAc,EACd,cAAc,EAEd,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,aAAa,EACd,MAAM,SAAS,CAAC;AAMjB;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,cAAc,CAAC,gBAAgB,CA4F9D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CA+BxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE,cAAc,CAAC,aAAa,CAwDxD,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CA8B1D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,EAAE,cAAc,CAAC,iBAAiB,CA8BhE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,EAAE,cAAc,CAAC,cAAc,CA+C1D,CAAC;AAMF;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;EAOZ,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,OAAO;;;;;;;;;;;;;;;;;;;;;;;;CAMnB,CAAC"}
|
package/dist/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* DOM manipulation actions: insertHtml, setText, setAttr, addClass, removeClass, setStyle.
|
|
5
5
|
* These follow the hostPatcher snapshot pattern for safe reversibility.
|
|
6
6
|
*/
|
|
7
|
+
import { guardAgainstReconciliation } from './reconciliation-guard';
|
|
7
8
|
import { sanitizeHtml } from './sanitizer';
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// Executors
|
|
@@ -48,8 +49,31 @@ export const executeInsertHtml = async (action, context) => {
|
|
|
48
49
|
anchorId: action.anchorId,
|
|
49
50
|
position: action.position,
|
|
50
51
|
});
|
|
52
|
+
// Guard against React reconciliation removing our container.
|
|
53
|
+
// The reinsert function re-applies the same insertion strategy.
|
|
54
|
+
const reinsertFn = () => {
|
|
55
|
+
switch (action.position) {
|
|
56
|
+
case 'before':
|
|
57
|
+
anchorEl.insertAdjacentElement('beforebegin', container);
|
|
58
|
+
break;
|
|
59
|
+
case 'after':
|
|
60
|
+
anchorEl.insertAdjacentElement('afterend', container);
|
|
61
|
+
break;
|
|
62
|
+
case 'prepend':
|
|
63
|
+
anchorEl.insertBefore(container, anchorEl.firstChild);
|
|
64
|
+
break;
|
|
65
|
+
case 'append':
|
|
66
|
+
anchorEl.appendChild(container);
|
|
67
|
+
break;
|
|
68
|
+
case 'replace':
|
|
69
|
+
// Cannot re-insert for replace — anchor was already replaced
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const guardCleanup = guardAgainstReconciliation(container, anchorEl, reinsertFn);
|
|
51
74
|
return {
|
|
52
75
|
cleanup: () => {
|
|
76
|
+
guardCleanup();
|
|
53
77
|
if (action.position === 'replace' && originalContent !== null) {
|
|
54
78
|
// Restore original element
|
|
55
79
|
const restoredEl = document.createElement(anchorEl.tagName);
|
|
@@ -23,6 +23,7 @@ describe('BeforeAfterToggle', () => {
|
|
|
23
23
|
it('highlights active mode with blue class', () => {
|
|
24
24
|
const { getByText } = render(_jsx(BeforeAfterToggle, { mode: "before", onToggle: () => { } }));
|
|
25
25
|
expect(getByText('Before').className).toContain('se-text-blue-5');
|
|
26
|
+
expect(getByText('Before').className).toContain('se-bg-blue-5/30');
|
|
26
27
|
expect(getByText('After').className).toContain('se-text-text-secondary');
|
|
27
28
|
});
|
|
28
29
|
});
|
package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/ConditionStatusLine.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConditionStatusLine.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/ConditionStatusLine.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { ConditionStatusLine } from '../components/ConditionStatusLine';
|
|
5
|
+
describe('ConditionStatusLine', () => {
|
|
6
|
+
it('returns null when status is null', () => {
|
|
7
|
+
const { container } = render(_jsx(ConditionStatusLine, { status: null }));
|
|
8
|
+
expect(container.innerHTML).toBe('');
|
|
9
|
+
});
|
|
10
|
+
it('renders single condition inline', () => {
|
|
11
|
+
const status = {
|
|
12
|
+
visible: true,
|
|
13
|
+
isFallback: false,
|
|
14
|
+
conditions: [
|
|
15
|
+
{
|
|
16
|
+
type: 'page_url',
|
|
17
|
+
passed: true,
|
|
18
|
+
formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
render(_jsx(ConditionStatusLine, { status: status }));
|
|
23
|
+
expect(screen.getByText('page_url:')).toBeTruthy();
|
|
24
|
+
expect(screen.getByText('/pricing')).toBeTruthy();
|
|
25
|
+
});
|
|
26
|
+
it('uses at least 12px font size for condition rows', () => {
|
|
27
|
+
const status = {
|
|
28
|
+
visible: true,
|
|
29
|
+
isFallback: false,
|
|
30
|
+
conditions: [
|
|
31
|
+
{
|
|
32
|
+
type: 'page_url',
|
|
33
|
+
passed: true,
|
|
34
|
+
formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
const { container } = render(_jsx(ConditionStatusLine, { status: status }));
|
|
39
|
+
const row = container.firstElementChild;
|
|
40
|
+
expect(row.className).toContain('se-text-[12px]');
|
|
41
|
+
expect(row.className).not.toContain('se-text-[10px]');
|
|
42
|
+
});
|
|
43
|
+
it('uses secondary text color for readability', () => {
|
|
44
|
+
const status = {
|
|
45
|
+
visible: true,
|
|
46
|
+
isFallback: false,
|
|
47
|
+
conditions: [
|
|
48
|
+
{
|
|
49
|
+
type: 'page_url',
|
|
50
|
+
passed: true,
|
|
51
|
+
formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
const { container } = render(_jsx(ConditionStatusLine, { status: status }));
|
|
56
|
+
const row = container.firstElementChild;
|
|
57
|
+
expect(row.className).toContain('se-text-text-secondary');
|
|
58
|
+
});
|
|
59
|
+
it('uses medium font weight for small text', () => {
|
|
60
|
+
const status = {
|
|
61
|
+
visible: true,
|
|
62
|
+
isFallback: false,
|
|
63
|
+
conditions: [
|
|
64
|
+
{
|
|
65
|
+
type: 'page_url',
|
|
66
|
+
passed: true,
|
|
67
|
+
formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
};
|
|
71
|
+
const { container } = render(_jsx(ConditionStatusLine, { status: status }));
|
|
72
|
+
const row = container.firstElementChild;
|
|
73
|
+
expect(row.className).toContain('se-font-medium');
|
|
74
|
+
});
|
|
75
|
+
it('renders multi-condition summary with expand button', () => {
|
|
76
|
+
const status = {
|
|
77
|
+
visible: false,
|
|
78
|
+
isFallback: false,
|
|
79
|
+
conditions: [
|
|
80
|
+
{
|
|
81
|
+
type: 'page_url',
|
|
82
|
+
passed: true,
|
|
83
|
+
formatted: { label: '/p', instruction: 'A', shortLabel: 'A' },
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'event_count',
|
|
87
|
+
passed: false,
|
|
88
|
+
formatted: { label: 'views >= 2', instruction: 'B', shortLabel: 'B' },
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
render(_jsx(ConditionStatusLine, { status: status }));
|
|
93
|
+
expect(screen.getByText('1 of 2 conditions met')).toBeTruthy();
|
|
94
|
+
});
|
|
95
|
+
it('uses at least 11px for expand indicator', () => {
|
|
96
|
+
const status = {
|
|
97
|
+
visible: false,
|
|
98
|
+
isFallback: false,
|
|
99
|
+
conditions: [
|
|
100
|
+
{
|
|
101
|
+
type: 'a',
|
|
102
|
+
passed: true,
|
|
103
|
+
formatted: { label: 'a', instruction: 'A', shortLabel: 'A' },
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
type: 'b',
|
|
107
|
+
passed: false,
|
|
108
|
+
formatted: { label: 'b', instruction: 'B', shortLabel: 'B' },
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
const { container } = render(_jsx(ConditionStatusLine, { status: status }));
|
|
113
|
+
// The expand indicator is the last span in the button
|
|
114
|
+
const button = container.querySelector('button');
|
|
115
|
+
const indicator = button.querySelector('span:last-child');
|
|
116
|
+
expect(indicator.className).toContain('se-text-[11px]');
|
|
117
|
+
expect(indicator.className).not.toContain('se-text-[8px]');
|
|
118
|
+
});
|
|
119
|
+
it('expands to show individual condition rows on click', () => {
|
|
120
|
+
const status = {
|
|
121
|
+
visible: false,
|
|
122
|
+
isFallback: false,
|
|
123
|
+
conditions: [
|
|
124
|
+
{
|
|
125
|
+
type: 'page_url',
|
|
126
|
+
passed: true,
|
|
127
|
+
formatted: { label: '/p', instruction: 'A', shortLabel: 'A' },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
type: 'event_count',
|
|
131
|
+
passed: false,
|
|
132
|
+
formatted: { label: 'views >= 2', instruction: 'B', shortLabel: 'B' },
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
render(_jsx(ConditionStatusLine, { status: status }));
|
|
137
|
+
fireEvent.click(screen.getByText('1 of 2 conditions met'));
|
|
138
|
+
expect(screen.getByText('page_url:')).toBeTruthy();
|
|
139
|
+
expect(screen.getByText('event_count:')).toBeTruthy();
|
|
140
|
+
});
|
|
141
|
+
it('uses secondary color (not tertiary) for condition detail labels', () => {
|
|
142
|
+
const status = {
|
|
143
|
+
visible: true,
|
|
144
|
+
isFallback: false,
|
|
145
|
+
conditions: [
|
|
146
|
+
{
|
|
147
|
+
type: 'page_url',
|
|
148
|
+
passed: true,
|
|
149
|
+
formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
render(_jsx(ConditionStatusLine, { status: status }));
|
|
154
|
+
const label = screen.getByText('/pricing');
|
|
155
|
+
expect(label.className).toContain('se-text-text-secondary');
|
|
156
|
+
expect(label.className).not.toContain('se-text-text-tertiary');
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -12,6 +12,12 @@ describe('DismissedSection', () => {
|
|
|
12
12
|
fireEvent.click(getByText('Dismissed (2)'));
|
|
13
13
|
expect(queryByText('revealed')).toBeTruthy();
|
|
14
14
|
});
|
|
15
|
+
it('uses secondary text color for readability', () => {
|
|
16
|
+
const { getByText } = render(_jsx(DismissedSection, { count: 2, children: _jsx("span", { children: "child" }) }));
|
|
17
|
+
const button = getByText('Dismissed (2)').closest('[role="button"]');
|
|
18
|
+
expect(button.className).toContain('se-text-text-secondary');
|
|
19
|
+
expect(button.className).not.toContain('se-text-text-tertiary');
|
|
20
|
+
});
|
|
15
21
|
it('collapses again on second click', () => {
|
|
16
22
|
const { getByText, queryByText } = render(_jsx(DismissedSection, { count: 1, children: _jsx("span", { children: "content" }) }));
|
|
17
23
|
fireEvent.click(getByText('Dismissed (1)'));
|
|
@@ -15,7 +15,7 @@ describe('EditorCard', () => {
|
|
|
15
15
|
it('applies green border class when validated is true', () => {
|
|
16
16
|
const { container } = render(_jsx(EditorCard, { itemKey: "k", validated: true, children: "child" }));
|
|
17
17
|
const card = container.firstElementChild;
|
|
18
|
-
expect(card.className).toContain('se-border-green-4/
|
|
18
|
+
expect(card.className).toContain('se-border-green-4/50');
|
|
19
19
|
});
|
|
20
20
|
it('applies default border class when validated is false', () => {
|
|
21
21
|
const { container } = render(_jsx(EditorCard, { itemKey: "k", validated: false, children: "child" }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
4
|
import { EditorHeader } from '../components/EditorHeader';
|
|
5
5
|
describe('EditorHeader', () => {
|
|
@@ -15,10 +15,9 @@ describe('EditorHeader', () => {
|
|
|
15
15
|
const { queryByText } = render(_jsx(EditorHeader, { title: "T", onBack: () => { } }));
|
|
16
16
|
expect(queryByText('Sub')).toBeNull();
|
|
17
17
|
});
|
|
18
|
-
it('
|
|
18
|
+
it('accepts deprecated onBack prop without rendering a back button', () => {
|
|
19
19
|
const onBack = vi.fn();
|
|
20
|
-
const {
|
|
21
|
-
|
|
22
|
-
expect(onBack).toHaveBeenCalledOnce();
|
|
20
|
+
const { queryByText } = render(_jsx(EditorHeader, { title: "T", onBack: onBack }));
|
|
21
|
+
expect(queryByText('← Back')).toBeNull();
|
|
23
22
|
});
|
|
24
23
|
});
|
package/node_modules/@syntrologie/shared-editor-ui/dist/__tests__/EditorPanelShell.test.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EditorPanelShell.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/EditorPanelShell.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { EditorPanelShell } from '../components/EditorPanelShell';
|
|
5
|
+
// Mock ResizeObserver and pointer capture for jsdom
|
|
6
|
+
vi.stubGlobal('ResizeObserver', class {
|
|
7
|
+
observe() { }
|
|
8
|
+
unobserve() { }
|
|
9
|
+
disconnect() { }
|
|
10
|
+
});
|
|
11
|
+
Element.prototype.setPointerCapture = vi.fn();
|
|
12
|
+
Element.prototype.releasePointerCapture = vi.fn();
|
|
13
|
+
describe('EditorPanelShell', () => {
|
|
14
|
+
it('applies antialiased font smoothing to the panel', () => {
|
|
15
|
+
const { container } = render(_jsx(EditorPanelShell, { isOpen: true, onToggle: () => { }, position: "right", children: _jsx("div", { children: "Content" }) }));
|
|
16
|
+
const panel = container.querySelector('[data-syntro-editor-panel]');
|
|
17
|
+
expect(panel).toBeTruthy();
|
|
18
|
+
expect(panel.className).toContain('se-antialiased');
|
|
19
|
+
});
|
|
20
|
+
it('does not render panel when closed', () => {
|
|
21
|
+
const { container } = render(_jsx(EditorPanelShell, { isOpen: false, onToggle: () => { }, children: _jsx("div", { children: "Content" }) }));
|
|
22
|
+
const panel = container.querySelector('[data-syntro-editor-panel]');
|
|
23
|
+
expect(panel).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -92,6 +92,28 @@ describe('ElementHighlight', () => {
|
|
|
92
92
|
expect(label.textContent).toContain('300');
|
|
93
93
|
expect(label.textContent).toContain('150');
|
|
94
94
|
});
|
|
95
|
+
it('uses at least 11px font for highlight labels', () => {
|
|
96
|
+
render(_jsx(ElementHighlight, { element: mockElement, color: "rgba(99,102,241,0.6)", showDimensions: true }));
|
|
97
|
+
const label = document.body.querySelector('[data-syntro-highlight-label]');
|
|
98
|
+
expect(label).toBeTruthy();
|
|
99
|
+
// showDimensions uses the smaller size (11px); non-dimensions uses 12px
|
|
100
|
+
expect(label.style.fontSize).toBe('11px');
|
|
101
|
+
});
|
|
102
|
+
it('uses 12px font for non-dimensions labels', () => {
|
|
103
|
+
render(_jsx(ElementHighlight, { element: mockElement, color: "#ef4444", label: "Test Label" }));
|
|
104
|
+
const label = document.body.querySelector('[data-syntro-highlight-label]');
|
|
105
|
+
expect(label).toBeTruthy();
|
|
106
|
+
expect(label.style.fontSize).toBe('12px');
|
|
107
|
+
});
|
|
108
|
+
it('uses 12px font for remove button', () => {
|
|
109
|
+
const onRemove = vi.fn();
|
|
110
|
+
render(_jsx(ElementHighlight, { element: mockElement, color: "#ef4444", label: "Test", showRemove: true, onRemove: onRemove }));
|
|
111
|
+
const removeBtn = document.body.querySelector('[data-syntro-highlight-remove]');
|
|
112
|
+
expect(removeBtn).toBeTruthy();
|
|
113
|
+
expect(removeBtn.style.fontSize).toBe('12px');
|
|
114
|
+
expect(removeBtn.style.width).toBe('16px');
|
|
115
|
+
expect(removeBtn.style.height).toBe('16px');
|
|
116
|
+
});
|
|
95
117
|
it('does not show remove button when showRemove is false', () => {
|
|
96
118
|
render(_jsx(ElementHighlight, { element: mockElement, color: "#3b82f6", label: "Test" }));
|
|
97
119
|
const removeBtn = document.body.querySelector('[data-syntro-highlight-remove]');
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { describe, it, expect } from 'vitest';
|
|
3
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { TriggerJourney } from '../components/TriggerJourney';
|
|
5
5
|
describe('TriggerJourney', () => {
|
|
6
|
-
it('
|
|
7
|
-
|
|
8
|
-
expect(
|
|
6
|
+
it('renders "Always Present" when status is null', () => {
|
|
7
|
+
render(_jsx(TriggerJourney, { status: null }));
|
|
8
|
+
expect(screen.getByText('Always Present')).toBeTruthy();
|
|
9
9
|
});
|
|
10
|
-
it('
|
|
10
|
+
it('renders "Always Present" when conditions array is empty', () => {
|
|
11
11
|
const status = { visible: true, isFallback: true, conditions: [] };
|
|
12
|
-
|
|
13
|
-
expect(
|
|
12
|
+
render(_jsx(TriggerJourney, { status: status }));
|
|
13
|
+
expect(screen.getByText('Always Present')).toBeTruthy();
|
|
14
14
|
});
|
|
15
15
|
it('renders a single node for one condition', () => {
|
|
16
16
|
const status = {
|
|
17
17
|
visible: true,
|
|
18
18
|
isFallback: false,
|
|
19
|
-
conditions: [
|
|
19
|
+
conditions: [
|
|
20
|
+
{
|
|
20
21
|
type: 'page_url',
|
|
21
22
|
passed: true,
|
|
22
23
|
formatted: { label: '/pricing', instruction: 'Visit /pricing', shortLabel: '/pricing' },
|
|
23
|
-
}
|
|
24
|
+
},
|
|
25
|
+
],
|
|
24
26
|
};
|
|
25
27
|
render(_jsx(TriggerJourney, { status: status }));
|
|
26
28
|
expect(screen.getByText('/pricing')).toBeTruthy();
|
|
@@ -33,7 +35,11 @@ describe('TriggerJourney', () => {
|
|
|
33
35
|
{
|
|
34
36
|
type: 'page_url',
|
|
35
37
|
passed: true,
|
|
36
|
-
formatted: {
|
|
38
|
+
formatted: {
|
|
39
|
+
label: '/fine-arts/',
|
|
40
|
+
instruction: 'Visit a /fine-arts/ page',
|
|
41
|
+
shortLabel: '/fine-arts/',
|
|
42
|
+
},
|
|
37
43
|
},
|
|
38
44
|
{
|
|
39
45
|
type: 'event_count',
|
|
@@ -57,7 +63,8 @@ describe('TriggerJourney', () => {
|
|
|
57
63
|
const status = {
|
|
58
64
|
visible: false,
|
|
59
65
|
isFallback: false,
|
|
60
|
-
conditions: [
|
|
66
|
+
conditions: [
|
|
67
|
+
{
|
|
61
68
|
type: 'event_count',
|
|
62
69
|
passed: false,
|
|
63
70
|
formatted: {
|
|
@@ -66,7 +73,8 @@ describe('TriggerJourney', () => {
|
|
|
66
73
|
shortLabel: '3+ clicks',
|
|
67
74
|
progress: { current: 1, target: 3, operator: 'gte' },
|
|
68
75
|
},
|
|
69
|
-
}
|
|
76
|
+
},
|
|
77
|
+
],
|
|
70
78
|
};
|
|
71
79
|
render(_jsx(TriggerJourney, { status: status }));
|
|
72
80
|
expect(screen.getByText('1/3')).toBeTruthy();
|
|
@@ -109,15 +117,70 @@ describe('TriggerJourney', () => {
|
|
|
109
117
|
render(_jsx(TriggerJourney, { status: status }));
|
|
110
118
|
expect(screen.queryByText('All conditions met')).toBeNull();
|
|
111
119
|
});
|
|
120
|
+
it('uses secondary text color and medium weight for node labels', () => {
|
|
121
|
+
const status = {
|
|
122
|
+
visible: false,
|
|
123
|
+
isFallback: false,
|
|
124
|
+
conditions: [
|
|
125
|
+
{
|
|
126
|
+
type: 'page_url',
|
|
127
|
+
passed: false,
|
|
128
|
+
formatted: { label: '/p', instruction: 'Visit pricing', shortLabel: '/pricing' },
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
render(_jsx(TriggerJourney, { status: status }));
|
|
133
|
+
const label = screen.getByText('/pricing');
|
|
134
|
+
expect(label.className).toContain('se-text-text-secondary');
|
|
135
|
+
expect(label.className).not.toContain('se-text-text-tertiary');
|
|
136
|
+
expect(label.className).toContain('se-font-medium');
|
|
137
|
+
});
|
|
138
|
+
it('uses at least 11px font size for node labels', () => {
|
|
139
|
+
const status = {
|
|
140
|
+
visible: false,
|
|
141
|
+
isFallback: false,
|
|
142
|
+
conditions: [
|
|
143
|
+
{
|
|
144
|
+
type: 'page_url',
|
|
145
|
+
passed: false,
|
|
146
|
+
formatted: { label: '/p', instruction: 'Visit pricing', shortLabel: '/pricing' },
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
render(_jsx(TriggerJourney, { status: status }));
|
|
151
|
+
const label = screen.getByText('/pricing');
|
|
152
|
+
expect(label.className).toContain('se-text-[11px]');
|
|
153
|
+
});
|
|
154
|
+
it('uses secondary text color for "Always Present" label', () => {
|
|
155
|
+
render(_jsx(TriggerJourney, { status: null }));
|
|
156
|
+
const label = screen.getByText('Always Present');
|
|
157
|
+
expect(label.className).toContain('se-text-text-secondary');
|
|
158
|
+
expect(label.className).not.toContain('se-text-text-tertiary');
|
|
159
|
+
});
|
|
160
|
+
it('uses at least 12px for "All conditions met" text', () => {
|
|
161
|
+
const status = {
|
|
162
|
+
visible: true,
|
|
163
|
+
isFallback: false,
|
|
164
|
+
conditions: [
|
|
165
|
+
{ type: 'a', passed: true, formatted: { label: 'a', instruction: 'A', shortLabel: 'A' } },
|
|
166
|
+
{ type: 'b', passed: true, formatted: { label: 'b', instruction: 'B', shortLabel: 'B' } },
|
|
167
|
+
],
|
|
168
|
+
};
|
|
169
|
+
const { container } = render(_jsx(TriggerJourney, { status: status }));
|
|
170
|
+
const badge = screen.getByText('All conditions met').closest('div');
|
|
171
|
+
expect(badge.className).toContain('se-text-[12px]');
|
|
172
|
+
});
|
|
112
173
|
it('uses instruction as title attribute for tooltip', () => {
|
|
113
174
|
const status = {
|
|
114
175
|
visible: false,
|
|
115
176
|
isFallback: false,
|
|
116
|
-
conditions: [
|
|
177
|
+
conditions: [
|
|
178
|
+
{
|
|
117
179
|
type: 'page_url',
|
|
118
180
|
passed: false,
|
|
119
181
|
formatted: { label: '/p', instruction: 'Visit /pricing page', shortLabel: '/pricing' },
|
|
120
|
-
}
|
|
182
|
+
},
|
|
183
|
+
],
|
|
121
184
|
};
|
|
122
185
|
render(_jsx(TriggerJourney, { status: status }));
|
|
123
186
|
const label = screen.getByText('/pricing');
|
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
* Instructions / cancel UI should be rendered by the consumer in their own
|
|
16
16
|
* panel — this component only provides the page overlay.
|
|
17
17
|
*/
|
|
18
|
-
import React from 'react';
|
|
19
18
|
export interface PickedElement {
|
|
20
19
|
element: Element;
|
|
21
20
|
selector: string;
|
|
@@ -27,5 +26,5 @@ export interface AnchorPickerProps {
|
|
|
27
26
|
onCancel: () => void;
|
|
28
27
|
excludeSelector?: string;
|
|
29
28
|
}
|
|
30
|
-
export declare function AnchorPicker({ isActive, onPick, onCancel, excludeSelector, }: AnchorPickerProps):
|
|
29
|
+
export declare function AnchorPicker({ isActive, onPick, onCancel, excludeSelector, }: AnchorPickerProps): import("react").ReactPortal | null;
|
|
31
30
|
//# sourceMappingURL=AnchorPicker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnchorPicker.d.ts","sourceRoot":"","sources":["../../src/components/AnchorPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;
|
|
1
|
+
{"version":3,"file":"AnchorPicker.d.ts","sourceRoot":"","sources":["../../src/components/AnchorPicker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAWH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAKD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,MAAM,EACN,QAAQ,EACR,eAAuJ,GACxJ,EAAE,iBAAiB,sCAgMnB"}
|
|
@@ -103,7 +103,7 @@ export function AnchorPicker({ isActive, onPick, onCancel, excludeSelector = '[d
|
|
|
103
103
|
}, children: [_jsx("div", { style: {
|
|
104
104
|
position: 'absolute',
|
|
105
105
|
inset: 0,
|
|
106
|
-
background: 'rgba(0, 0, 0, 0.
|
|
106
|
+
background: 'rgba(0, 0, 0, 0.08)',
|
|
107
107
|
pointerEvents: 'none',
|
|
108
108
|
} }), hoveredElement && rect && (_jsx("div", { style: {
|
|
109
109
|
position: 'fixed',
|
|
@@ -114,7 +114,7 @@ export function AnchorPicker({ isActive, onPick, onCancel, excludeSelector = '[d
|
|
|
114
114
|
border: `2px solid ${HIGHLIGHT_COLOR}`,
|
|
115
115
|
backgroundColor: HIGHLIGHT_BG,
|
|
116
116
|
borderRadius: '4px',
|
|
117
|
-
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.
|
|
117
|
+
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.2)',
|
|
118
118
|
pointerEvents: 'none',
|
|
119
119
|
transition: 'all 0.1s ease-out',
|
|
120
120
|
} })), hoveredElement && rect && (_jsxs("div", { style: {
|
|
@@ -128,11 +128,11 @@ export function AnchorPicker({ isActive, onPick, onCancel, excludeSelector = '[d
|
|
|
128
128
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
|
129
129
|
zIndex: 1,
|
|
130
130
|
fontFamily: 'monospace',
|
|
131
|
-
fontSize: '
|
|
131
|
+
fontSize: '13px',
|
|
132
132
|
maxWidth: '300px',
|
|
133
133
|
pointerEvents: 'none',
|
|
134
134
|
}, children: [_jsx("div", { style: {
|
|
135
|
-
fontSize: '
|
|
135
|
+
fontSize: '12px',
|
|
136
136
|
textTransform: 'uppercase',
|
|
137
137
|
letterSpacing: '0.05em',
|
|
138
138
|
marginBottom: '4px',
|
package/node_modules/@syntrologie/shared-editor-ui/dist/components/BeforeAfterToggle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BeforeAfterToggle.d.ts","sourceRoot":"","sources":["../../src/components/BeforeAfterToggle.tsx"],"names":[],"mappings":"AAEA,UAAU,sBAAsB;IAC9B,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,sBAAsB,
|
|
1
|
+
{"version":3,"file":"BeforeAfterToggle.d.ts","sourceRoot":"","sources":["../../src/components/BeforeAfterToggle.tsx"],"names":[],"mappings":"AAEA,UAAU,sBAAsB;IAC9B,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,iBAAiB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CA6B3E"}
|