@capillarytech/creatives-library 8.0.236-alpha.4 → 8.0.236-alpha.6
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/package.json +1 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +405 -24
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +2 -2
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +33 -20
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +45 -14
- package/v2Components/TemplatePreview/_templatePreview.scss +5 -5
- package/v2Containers/BeePopupEditor/index.js +14 -2
- package/v2Containers/CreativesContainer/index.js +31 -23
- package/v2Containers/InApp/constants.js +1 -0
- package/v2Containers/InApp/index.js +77 -31
- package/v2Containers/InappAdvance/index.js +95 -28
package/package.json
CHANGED
|
@@ -10,22 +10,32 @@ import '@testing-library/jest-dom';
|
|
|
10
10
|
import { IntlProvider } from 'react-intl';
|
|
11
11
|
import HTMLEditor from '../HTMLEditor';
|
|
12
12
|
|
|
13
|
+
// Options to control CodeEditorPane mock behavior
|
|
14
|
+
const mockCodeEditorOptions = {
|
|
15
|
+
includeInsertText: true,
|
|
16
|
+
insertTextThrows: false,
|
|
17
|
+
setRef: true,
|
|
18
|
+
navigateToLineThrows: false,
|
|
19
|
+
includeNavigateToLine: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
|
|
13
23
|
// Mock useLayoutEffect to behave like useEffect in tests
|
|
14
24
|
React.useLayoutEffect = React.useEffect;
|
|
15
25
|
|
|
16
26
|
// Mock browser APIs
|
|
17
27
|
global.IntersectionObserver = class IntersectionObserver {
|
|
18
|
-
constructor() {}
|
|
19
|
-
disconnect() {}
|
|
20
|
-
observe() {}
|
|
21
|
-
unobserve() {}
|
|
28
|
+
constructor() { }
|
|
29
|
+
disconnect() { }
|
|
30
|
+
observe() { }
|
|
31
|
+
unobserve() { }
|
|
22
32
|
};
|
|
23
33
|
|
|
24
34
|
global.ResizeObserver = class ResizeObserver {
|
|
25
|
-
constructor() {}
|
|
26
|
-
disconnect() {}
|
|
27
|
-
observe() {}
|
|
28
|
-
unobserve() {}
|
|
35
|
+
constructor() { }
|
|
36
|
+
disconnect() { }
|
|
37
|
+
observe() { }
|
|
38
|
+
unobserve() { }
|
|
29
39
|
};
|
|
30
40
|
|
|
31
41
|
// Setup window.matchMedia mock
|
|
@@ -132,19 +142,42 @@ jest.mock('../components/SplitContainer', () => {
|
|
|
132
142
|
};
|
|
133
143
|
});
|
|
134
144
|
|
|
145
|
+
|
|
135
146
|
jest.mock('../components/CodeEditorPane', () => {
|
|
136
147
|
const React = require('react');
|
|
137
148
|
return React.forwardRef(function MockCodeEditorPane(props, ref) {
|
|
138
149
|
const [value, setValue] = React.useState('');
|
|
139
150
|
|
|
140
|
-
React.useImperativeHandle(ref, () =>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
React.useImperativeHandle(ref, () => {
|
|
152
|
+
if (!mockCodeEditorOptions.setRef) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const methods = {
|
|
157
|
+
focus: jest.fn(),
|
|
158
|
+
getCursor: jest.fn(() => 0),
|
|
159
|
+
getValue: jest.fn(() => value),
|
|
160
|
+
setValue: jest.fn((newValue) => setValue(newValue)),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (mockCodeEditorOptions.includeNavigateToLine) {
|
|
164
|
+
methods.navigateToLine = jest.fn(() => {
|
|
165
|
+
if (mockCodeEditorOptions.navigateToLineThrows) {
|
|
166
|
+
throw new Error('Navigation failed');
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (mockCodeEditorOptions.includeInsertText) {
|
|
172
|
+
methods.insertText = jest.fn(() => {
|
|
173
|
+
if (mockCodeEditorOptions.insertTextThrows) {
|
|
174
|
+
throw new Error('Insert failed');
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return methods;
|
|
180
|
+
});
|
|
148
181
|
|
|
149
182
|
return (
|
|
150
183
|
<div data-testid="code-editor-pane">
|
|
@@ -158,6 +191,12 @@ jest.mock('../components/CodeEditorPane', () => {
|
|
|
158
191
|
}}
|
|
159
192
|
data-testid="editor-textarea"
|
|
160
193
|
/>
|
|
194
|
+
<button
|
|
195
|
+
onClick={() => props.onContextChange && props.onContextChange('test-context')}
|
|
196
|
+
data-testid="trigger-context-change"
|
|
197
|
+
>
|
|
198
|
+
Trigger Context Change
|
|
199
|
+
</button>
|
|
161
200
|
</div>
|
|
162
201
|
);
|
|
163
202
|
});
|
|
@@ -480,7 +519,7 @@ describe('HTMLEditor', () => {
|
|
|
480
519
|
// This test covers lines 104-109 in HTMLEditor.js
|
|
481
520
|
const mockUseInAppContent = require('../hooks/useInAppContent').useInAppContent;
|
|
482
521
|
const originalUseInAppContent = jest.requireActual('../hooks/useInAppContent').useInAppContent;
|
|
483
|
-
|
|
522
|
+
|
|
484
523
|
let capturedInitialContent = null;
|
|
485
524
|
jest.spyOn(require('../hooks/useInAppContent'), 'useInAppContent').mockImplementation((initialContent) => {
|
|
486
525
|
capturedInitialContent = initialContent;
|
|
@@ -542,7 +581,7 @@ describe('HTMLEditor', () => {
|
|
|
542
581
|
|
|
543
582
|
const mockUseInAppContent = require('../hooks/useInAppContent').useInAppContent;
|
|
544
583
|
const originalUseInAppContent = jest.requireActual('../hooks/useInAppContent').useInAppContent;
|
|
545
|
-
|
|
584
|
+
|
|
546
585
|
let capturedInitialContent = null;
|
|
547
586
|
jest.spyOn(require('../hooks/useInAppContent'), 'useInAppContent').mockImplementation((initialContent) => {
|
|
548
587
|
capturedInitialContent = initialContent;
|
|
@@ -930,7 +969,7 @@ describe('HTMLEditor', () => {
|
|
|
930
969
|
describe('Error Handling', () => {
|
|
931
970
|
it('handles missing editorRef gracefully', () => {
|
|
932
971
|
// Mock console.warn to avoid noise in tests
|
|
933
|
-
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
972
|
+
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => { });
|
|
934
973
|
|
|
935
974
|
render(
|
|
936
975
|
<TestWrapper>
|
|
@@ -1399,8 +1438,8 @@ describe('HTMLEditor', () => {
|
|
|
1399
1438
|
<div className="html-editor html-editor--email">
|
|
1400
1439
|
<CustomToolbar
|
|
1401
1440
|
onLabelInsert={handleLabelInsert}
|
|
1402
|
-
onToggleFullscreen={() => {}}
|
|
1403
|
-
onSave={() => {}}
|
|
1441
|
+
onToggleFullscreen={() => { }}
|
|
1442
|
+
onSave={() => { }}
|
|
1404
1443
|
/>
|
|
1405
1444
|
<div data-testid="split-container">
|
|
1406
1445
|
<div data-testid="code-editor-pane" ref={editorRef}>
|
|
@@ -1970,7 +2009,7 @@ describe('HTMLEditor', () => {
|
|
|
1970
2009
|
// Wait for component to render - might be in loading state
|
|
1971
2010
|
const toolbar = screen.queryByTestId('editor-toolbar');
|
|
1972
2011
|
const loading = screen.queryByText('Initializing HTML Editor...');
|
|
1973
|
-
|
|
2012
|
+
|
|
1974
2013
|
// Component should render (either loaded or loading)
|
|
1975
2014
|
expect(toolbar || loading).toBeTruthy();
|
|
1976
2015
|
});
|
|
@@ -1999,7 +2038,7 @@ describe('HTMLEditor', () => {
|
|
|
1999
2038
|
// Wait for component to render
|
|
2000
2039
|
const deviceToggle = screen.queryByTestId('device-toggle');
|
|
2001
2040
|
const loading = screen.queryByText('Initializing HTML Editor...');
|
|
2002
|
-
|
|
2041
|
+
|
|
2003
2042
|
// Component should render (either loaded or loading)
|
|
2004
2043
|
expect(deviceToggle || loading).toBeTruthy();
|
|
2005
2044
|
});
|
|
@@ -2075,7 +2114,7 @@ describe('HTMLEditor', () => {
|
|
|
2075
2114
|
const insertButton = screen.queryByText('Insert Label (Null Position)');
|
|
2076
2115
|
if (insertButton) {
|
|
2077
2116
|
fireEvent.click(insertButton);
|
|
2078
|
-
|
|
2117
|
+
|
|
2079
2118
|
// Should attempt to insert via editor ref
|
|
2080
2119
|
// The mock editor has insertText method, so it should work
|
|
2081
2120
|
expect(CapNotification.success).toHaveBeenCalled();
|
|
@@ -2314,6 +2353,8 @@ describe('HTMLEditor', () => {
|
|
|
2314
2353
|
});
|
|
2315
2354
|
|
|
2316
2355
|
it('focuses editor when navigateToLine is not available', () => {
|
|
2356
|
+
mockCodeEditorOptions.includeNavigateToLine = false;
|
|
2357
|
+
|
|
2317
2358
|
mockUseValidationImpl.mockReturnValueOnce({
|
|
2318
2359
|
isValidating: false,
|
|
2319
2360
|
getAllIssues: () => [{ message: 'Test error', line: 5, column: 10, severity: 'error' }],
|
|
@@ -2337,5 +2378,345 @@ describe('HTMLEditor', () => {
|
|
|
2337
2378
|
expect(codeEditorPane || loading).toBeTruthy();
|
|
2338
2379
|
});
|
|
2339
2380
|
});
|
|
2381
|
+
|
|
2382
|
+
|
|
2383
|
+
describe('Additional Coverage Tests', () => {
|
|
2384
|
+
const CapNotification = require('@capillarytech/cap-ui-library/CapNotification');
|
|
2385
|
+
|
|
2386
|
+
beforeEach(() => {
|
|
2387
|
+
// Reset options to default
|
|
2388
|
+
mockCodeEditorOptions.includeInsertText = true;
|
|
2389
|
+
mockCodeEditorOptions.insertTextThrows = false;
|
|
2390
|
+
mockCodeEditorOptions.setRef = true;
|
|
2391
|
+
mockCodeEditorOptions.navigateToLineThrows = false;
|
|
2392
|
+
mockCodeEditorOptions.includeNavigateToLine = true;
|
|
2393
|
+
|
|
2394
|
+
// Clear specific mocks instead of all to avoid breaking other mocks
|
|
2395
|
+
CapNotification.warning.mockClear();
|
|
2396
|
+
CapNotification.error.mockClear();
|
|
2397
|
+
CapNotification.success.mockClear();
|
|
2398
|
+
|
|
2399
|
+
// Restore hooks that might have been corrupted by Loading State Coverage tests
|
|
2400
|
+
// This is necessary because Loading State Coverage tests modify the require cache
|
|
2401
|
+
// and do not restore the original mocks
|
|
2402
|
+
require('../hooks/useEditorContent').useEditorContent = () => ({
|
|
2403
|
+
content: '<p>Test content</p>',
|
|
2404
|
+
updateContent: jest.fn(),
|
|
2405
|
+
saveContent: jest.fn(),
|
|
2406
|
+
markAsSaved: jest.fn(),
|
|
2407
|
+
isLoading: false,
|
|
2408
|
+
isDirty: false,
|
|
2409
|
+
hasContent: true,
|
|
2410
|
+
getContentSize: jest.fn(() => 20)
|
|
2411
|
+
});
|
|
2412
|
+
|
|
2413
|
+
require('../hooks/useInAppContent').useInAppContent = () => ({
|
|
2414
|
+
content: '<p>Android content</p>',
|
|
2415
|
+
deviceContent: {
|
|
2416
|
+
android: '<p>Android content</p>',
|
|
2417
|
+
ios: '<p>iOS content</p>'
|
|
2418
|
+
},
|
|
2419
|
+
activeDevice: 'android',
|
|
2420
|
+
keepContentSame: false,
|
|
2421
|
+
updateContent: jest.fn(),
|
|
2422
|
+
saveContent: jest.fn(),
|
|
2423
|
+
markAsSaved: jest.fn(),
|
|
2424
|
+
switchDevice: jest.fn(),
|
|
2425
|
+
toggleContentSync: jest.fn(),
|
|
2426
|
+
getDeviceContent: (device) => `<p>${device} content</p>`,
|
|
2427
|
+
setDeviceContent: jest.fn(),
|
|
2428
|
+
getContentSize: () => 20,
|
|
2429
|
+
isLoading: false,
|
|
2430
|
+
isDirty: false,
|
|
2431
|
+
hasContent: true
|
|
2432
|
+
});
|
|
2433
|
+
|
|
2434
|
+
require('../hooks/useLayoutState').useLayoutState = () => ({
|
|
2435
|
+
splitSizes: [50, 50],
|
|
2436
|
+
splitSize: 50,
|
|
2437
|
+
viewMode: 'desktop',
|
|
2438
|
+
mobileWidth: 375,
|
|
2439
|
+
isFullscreen: false,
|
|
2440
|
+
isResizing: false,
|
|
2441
|
+
updateSplitSizes: jest.fn(),
|
|
2442
|
+
setSplitSize: jest.fn(),
|
|
2443
|
+
setViewMode: jest.fn(),
|
|
2444
|
+
toggleViewMode: jest.fn(),
|
|
2445
|
+
setMobileWidth: jest.fn(),
|
|
2446
|
+
toggleFullscreen: jest.fn(),
|
|
2447
|
+
resetLayout: jest.fn(),
|
|
2448
|
+
setResizingState: jest.fn(),
|
|
2449
|
+
handleResize: jest.fn(),
|
|
2450
|
+
handleKeyboardShortcut: jest.fn(),
|
|
2451
|
+
isMobileView: false,
|
|
2452
|
+
isDesktopView: true,
|
|
2453
|
+
minPaneSize: 20,
|
|
2454
|
+
maxPaneSize: 80,
|
|
2455
|
+
gutterSize: 10
|
|
2456
|
+
});
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
describe('handleContextChange', () => {
|
|
2460
|
+
it('calls onContextChange prop when provided', () => {
|
|
2461
|
+
const onContextChange = jest.fn();
|
|
2462
|
+
const globalActions = { fetchSchemaForEntity: jest.fn() };
|
|
2463
|
+
|
|
2464
|
+
render(
|
|
2465
|
+
<TestWrapper>
|
|
2466
|
+
<HTMLEditor
|
|
2467
|
+
{...defaultProps}
|
|
2468
|
+
onContextChange={onContextChange}
|
|
2469
|
+
globalActions={globalActions}
|
|
2470
|
+
/>
|
|
2471
|
+
</TestWrapper>
|
|
2472
|
+
);
|
|
2473
|
+
|
|
2474
|
+
act(() => {
|
|
2475
|
+
jest.runAllTimers();
|
|
2476
|
+
});
|
|
2477
|
+
|
|
2478
|
+
fireEvent.click(screen.getByTestId('trigger-context-change'));
|
|
2479
|
+
|
|
2480
|
+
expect(onContextChange).toHaveBeenCalledWith('test-context');
|
|
2481
|
+
expect(globalActions.fetchSchemaForEntity).not.toHaveBeenCalled();
|
|
2482
|
+
});
|
|
2483
|
+
|
|
2484
|
+
it('calls globalActions.fetchSchemaForEntity when onContextChange is NOT provided', () => {
|
|
2485
|
+
const globalActions = { fetchSchemaForEntity: jest.fn() };
|
|
2486
|
+
const location = { query: { type: 'embedded' } };
|
|
2487
|
+
|
|
2488
|
+
render(
|
|
2489
|
+
<TestWrapper>
|
|
2490
|
+
<HTMLEditor
|
|
2491
|
+
{...defaultProps}
|
|
2492
|
+
globalActions={globalActions}
|
|
2493
|
+
location={location}
|
|
2494
|
+
variant="email"
|
|
2495
|
+
/>
|
|
2496
|
+
</TestWrapper>
|
|
2497
|
+
);
|
|
2498
|
+
|
|
2499
|
+
act(() => {
|
|
2500
|
+
jest.runAllTimers();
|
|
2501
|
+
});
|
|
2502
|
+
|
|
2503
|
+
fireEvent.click(screen.getByTestId('trigger-context-change'));
|
|
2504
|
+
|
|
2505
|
+
expect(globalActions.fetchSchemaForEntity).toHaveBeenCalledWith({
|
|
2506
|
+
layout: 'EMAIL',
|
|
2507
|
+
type: 'TAG',
|
|
2508
|
+
context: 'test-context',
|
|
2509
|
+
embedded: 'embedded',
|
|
2510
|
+
});
|
|
2511
|
+
});
|
|
2512
|
+
|
|
2513
|
+
it('handles INAPP variant in handleContextChange', () => {
|
|
2514
|
+
const globalActions = { fetchSchemaForEntity: jest.fn() };
|
|
2515
|
+
const location = { query: { type: 'full' } };
|
|
2516
|
+
|
|
2517
|
+
render(
|
|
2518
|
+
<TestWrapper>
|
|
2519
|
+
<HTMLEditor
|
|
2520
|
+
{...defaultProps}
|
|
2521
|
+
globalActions={globalActions}
|
|
2522
|
+
location={location}
|
|
2523
|
+
variant="inapp"
|
|
2524
|
+
/>
|
|
2525
|
+
</TestWrapper>
|
|
2526
|
+
);
|
|
2527
|
+
|
|
2528
|
+
act(() => {
|
|
2529
|
+
jest.runAllTimers();
|
|
2530
|
+
});
|
|
2531
|
+
|
|
2532
|
+
fireEvent.click(screen.getByTestId('trigger-context-change'));
|
|
2533
|
+
|
|
2534
|
+
expect(globalActions.fetchSchemaForEntity).toHaveBeenCalledWith({
|
|
2535
|
+
layout: 'SMS', // INAPP uses SMS layout
|
|
2536
|
+
type: 'TAG',
|
|
2537
|
+
context: 'test-context',
|
|
2538
|
+
embedded: 'full',
|
|
2539
|
+
});
|
|
2540
|
+
});
|
|
2541
|
+
|
|
2542
|
+
it('handles missing globalActions or location', () => {
|
|
2543
|
+
const globalActions = { fetchSchemaForEntity: jest.fn() };
|
|
2544
|
+
|
|
2545
|
+
// Case 1: No globalActions
|
|
2546
|
+
const { unmount } = render(
|
|
2547
|
+
<TestWrapper>
|
|
2548
|
+
<HTMLEditor
|
|
2549
|
+
{...defaultProps}
|
|
2550
|
+
globalActions={null}
|
|
2551
|
+
location={{}}
|
|
2552
|
+
/>
|
|
2553
|
+
</TestWrapper>
|
|
2554
|
+
);
|
|
2555
|
+
|
|
2556
|
+
act(() => {
|
|
2557
|
+
jest.runAllTimers();
|
|
2558
|
+
});
|
|
2559
|
+
|
|
2560
|
+
fireEvent.click(screen.getByTestId('trigger-context-change'));
|
|
2561
|
+
expect(globalActions.fetchSchemaForEntity).not.toHaveBeenCalled();
|
|
2562
|
+
unmount();
|
|
2563
|
+
|
|
2564
|
+
// Case 2: No location
|
|
2565
|
+
render(
|
|
2566
|
+
<TestWrapper>
|
|
2567
|
+
<HTMLEditor
|
|
2568
|
+
{...defaultProps}
|
|
2569
|
+
globalActions={globalActions}
|
|
2570
|
+
location={null}
|
|
2571
|
+
/>
|
|
2572
|
+
</TestWrapper>
|
|
2573
|
+
);
|
|
2574
|
+
|
|
2575
|
+
act(() => {
|
|
2576
|
+
jest.runAllTimers();
|
|
2577
|
+
});
|
|
2578
|
+
|
|
2579
|
+
fireEvent.click(screen.getByTestId('trigger-context-change'));
|
|
2580
|
+
expect(globalActions.fetchSchemaForEntity).not.toHaveBeenCalled();
|
|
2581
|
+
});
|
|
2582
|
+
});
|
|
2583
|
+
|
|
2584
|
+
describe('handleLabelInsert', () => {
|
|
2585
|
+
it('shows warning when editor is not ready (position null, no editor)', () => {
|
|
2586
|
+
mockCodeEditorOptions.setRef = false;
|
|
2587
|
+
|
|
2588
|
+
render(
|
|
2589
|
+
<TestWrapper>
|
|
2590
|
+
<HTMLEditor {...defaultProps} />
|
|
2591
|
+
</TestWrapper>
|
|
2592
|
+
);
|
|
2593
|
+
|
|
2594
|
+
act(() => {
|
|
2595
|
+
jest.runAllTimers();
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
// Click the button that passes null position
|
|
2599
|
+
const insertButton = screen.getByText('Insert Label (Null Position)');
|
|
2600
|
+
fireEvent.click(insertButton);
|
|
2601
|
+
|
|
2602
|
+
expect(CapNotification.warning).toHaveBeenCalledWith(
|
|
2603
|
+
expect.objectContaining({
|
|
2604
|
+
message: 'Failed to insert label',
|
|
2605
|
+
description: 'Editor is not ready. Please try again.',
|
|
2606
|
+
})
|
|
2607
|
+
);
|
|
2608
|
+
});
|
|
2609
|
+
|
|
2610
|
+
it('shows error when editor method insertText is not available', () => {
|
|
2611
|
+
mockCodeEditorOptions.includeInsertText = false;
|
|
2612
|
+
|
|
2613
|
+
render(
|
|
2614
|
+
<TestWrapper>
|
|
2615
|
+
<HTMLEditor {...defaultProps} />
|
|
2616
|
+
</TestWrapper>
|
|
2617
|
+
);
|
|
2618
|
+
|
|
2619
|
+
act(() => {
|
|
2620
|
+
jest.runAllTimers();
|
|
2621
|
+
});
|
|
2622
|
+
|
|
2623
|
+
const insertButton = screen.getByText('Insert Label (Null Position)');
|
|
2624
|
+
fireEvent.click(insertButton);
|
|
2625
|
+
|
|
2626
|
+
// Should show error when method is missing
|
|
2627
|
+
expect(CapNotification.error).toHaveBeenCalledWith(
|
|
2628
|
+
expect.objectContaining({
|
|
2629
|
+
message: 'Failed to insert label',
|
|
2630
|
+
})
|
|
2631
|
+
);
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
it('successfully inserts label when position is null', () => {
|
|
2635
|
+
mockCodeEditorOptions.includeInsertText = true;
|
|
2636
|
+
|
|
2637
|
+
render(
|
|
2638
|
+
<TestWrapper>
|
|
2639
|
+
<HTMLEditor {...defaultProps} />
|
|
2640
|
+
</TestWrapper>
|
|
2641
|
+
);
|
|
2642
|
+
|
|
2643
|
+
act(() => {
|
|
2644
|
+
jest.runAllTimers();
|
|
2645
|
+
});
|
|
2646
|
+
|
|
2647
|
+
const insertButton = screen.getByText('Insert Label (Null Position)');
|
|
2648
|
+
fireEvent.click(insertButton);
|
|
2649
|
+
|
|
2650
|
+
expect(CapNotification.success).toHaveBeenCalled();
|
|
2651
|
+
});
|
|
2652
|
+
|
|
2653
|
+
it('shows error when insertText throws', () => {
|
|
2654
|
+
mockCodeEditorOptions.includeInsertText = true;
|
|
2655
|
+
mockCodeEditorOptions.insertTextThrows = true;
|
|
2656
|
+
|
|
2657
|
+
render(
|
|
2658
|
+
<TestWrapper>
|
|
2659
|
+
<HTMLEditor {...defaultProps} />
|
|
2660
|
+
</TestWrapper>
|
|
2661
|
+
);
|
|
2662
|
+
|
|
2663
|
+
act(() => {
|
|
2664
|
+
jest.runAllTimers();
|
|
2665
|
+
});
|
|
2666
|
+
|
|
2667
|
+
const insertButton = screen.getByText('Insert Label (Null Position)');
|
|
2668
|
+
fireEvent.click(insertButton);
|
|
2669
|
+
|
|
2670
|
+
expect(CapNotification.error).toHaveBeenCalledWith(expect.objectContaining({
|
|
2671
|
+
description: 'Insert failed'
|
|
2672
|
+
}));
|
|
2673
|
+
});
|
|
2674
|
+
|
|
2675
|
+
it('shows success notification when position is provided (handled by CodeEditorPane)', () => {
|
|
2676
|
+
render(
|
|
2677
|
+
<TestWrapper>
|
|
2678
|
+
<HTMLEditor {...defaultProps} />
|
|
2679
|
+
</TestWrapper>
|
|
2680
|
+
);
|
|
2681
|
+
|
|
2682
|
+
act(() => {
|
|
2683
|
+
jest.runAllTimers();
|
|
2684
|
+
});
|
|
2685
|
+
|
|
2686
|
+
// Click the button that passes a position
|
|
2687
|
+
const insertButton = screen.getByText('Insert Label');
|
|
2688
|
+
fireEvent.click(insertButton);
|
|
2689
|
+
|
|
2690
|
+
expect(CapNotification.success).toHaveBeenCalled();
|
|
2691
|
+
});
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
describe('handleValidationErrorClick', () => {
|
|
2695
|
+
it('handles error when navigateToLine throws', () => {
|
|
2696
|
+
mockCodeEditorOptions.navigateToLineThrows = true;
|
|
2697
|
+
|
|
2698
|
+
mockUseValidationImpl.mockReturnValueOnce({
|
|
2699
|
+
isValidating: false,
|
|
2700
|
+
getAllIssues: () => [{ message: 'Test error', line: 5, column: 10, severity: 'error' }],
|
|
2701
|
+
isClean: () => false,
|
|
2702
|
+
summary: { totalErrors: 1, totalWarnings: 0 }
|
|
2703
|
+
});
|
|
2704
|
+
|
|
2705
|
+
render(
|
|
2706
|
+
<TestWrapper>
|
|
2707
|
+
<HTMLEditor {...defaultProps} />
|
|
2708
|
+
</TestWrapper>
|
|
2709
|
+
);
|
|
2710
|
+
|
|
2711
|
+
act(() => {
|
|
2712
|
+
jest.runAllTimers();
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2715
|
+
// Click error button
|
|
2716
|
+
const errorButton = screen.getByText('Error at Line 5');
|
|
2717
|
+
fireEvent.click(errorButton);
|
|
2718
|
+
});
|
|
2719
|
+
});
|
|
2720
|
+
});
|
|
2340
2721
|
});
|
|
2341
2722
|
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
overflow: auto;
|
|
41
41
|
position: relative;
|
|
42
42
|
height: 100%;
|
|
43
|
-
max-height:
|
|
43
|
+
max-height: 34.25rem;
|
|
44
44
|
background-color: $CAP_G01;
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
.codemirror-wrapper {
|
|
151
151
|
position: relative;
|
|
152
152
|
height: 100%;
|
|
153
|
-
max-height:
|
|
153
|
+
max-height: 34.25rem;
|
|
154
154
|
background-color: $CAP_G01;
|
|
155
155
|
border-radius: 0;
|
|
156
156
|
overflow: hidden;
|
|
@@ -926,20 +926,24 @@ describe('useInAppContent', () => {
|
|
|
926
926
|
|
|
927
927
|
describe('setDeviceContent State Updates (L95-L109)', () => {
|
|
928
928
|
it('updates Android content when newAndroidContent differs from current', () => {
|
|
929
|
+
// With the fix, initialContent effect only runs on first load or when transitioning from empty to content
|
|
930
|
+
// After initial load, changing initialContent won't update content (prevents overriding user edits)
|
|
929
931
|
const initialContent = {
|
|
930
|
-
[DEVICE_TYPES.ANDROID]: '
|
|
931
|
-
[DEVICE_TYPES.IOS]: '
|
|
932
|
+
[DEVICE_TYPES.ANDROID]: '', // Start with empty
|
|
933
|
+
[DEVICE_TYPES.IOS]: '',
|
|
932
934
|
};
|
|
933
935
|
|
|
934
936
|
const { rerender } = render(
|
|
935
937
|
<TestComponent initialContent={initialContent} />
|
|
936
938
|
);
|
|
937
939
|
|
|
938
|
-
|
|
940
|
+
// Initially empty
|
|
941
|
+
expect(screen.getByTestId('android-content')).toHaveTextContent('');
|
|
939
942
|
|
|
943
|
+
// Transition from empty to meaningful content - this SHOULD update
|
|
940
944
|
const updatedContent = {
|
|
941
945
|
[DEVICE_TYPES.ANDROID]: 'updated android',
|
|
942
|
-
[DEVICE_TYPES.IOS]: '
|
|
946
|
+
[DEVICE_TYPES.IOS]: '',
|
|
943
947
|
};
|
|
944
948
|
|
|
945
949
|
rerender(<TestComponent initialContent={updatedContent} />);
|
|
@@ -948,19 +952,21 @@ describe('useInAppContent', () => {
|
|
|
948
952
|
});
|
|
949
953
|
|
|
950
954
|
it('updates iOS content when newIosContent differs from current', () => {
|
|
955
|
+
// With the fix, initialContent effect only runs on first load or when transitioning from empty to content
|
|
951
956
|
const initialContent = {
|
|
952
|
-
[DEVICE_TYPES.ANDROID]: '
|
|
953
|
-
[DEVICE_TYPES.IOS]: '
|
|
957
|
+
[DEVICE_TYPES.ANDROID]: '',
|
|
958
|
+
[DEVICE_TYPES.IOS]: '', // Start with empty
|
|
954
959
|
};
|
|
955
960
|
|
|
956
961
|
const { rerender } = render(
|
|
957
962
|
<TestComponent initialContent={initialContent} />
|
|
958
963
|
);
|
|
959
964
|
|
|
960
|
-
expect(screen.getByTestId('ios-content')).toHaveTextContent('
|
|
965
|
+
expect(screen.getByTestId('ios-content')).toHaveTextContent('');
|
|
961
966
|
|
|
967
|
+
// Transition from empty to meaningful content - this SHOULD update
|
|
962
968
|
const updatedContent = {
|
|
963
|
-
[DEVICE_TYPES.ANDROID]: '
|
|
969
|
+
[DEVICE_TYPES.ANDROID]: '',
|
|
964
970
|
[DEVICE_TYPES.IOS]: 'updated ios',
|
|
965
971
|
};
|
|
966
972
|
|
|
@@ -970,18 +976,20 @@ describe('useInAppContent', () => {
|
|
|
970
976
|
});
|
|
971
977
|
|
|
972
978
|
it('updates both Android and iOS content when both differ', () => {
|
|
979
|
+
// With the fix, initialContent effect only runs on first load or when transitioning from empty to content
|
|
973
980
|
const initialContent = {
|
|
974
|
-
[DEVICE_TYPES.ANDROID]: '
|
|
975
|
-
[DEVICE_TYPES.IOS]: '
|
|
981
|
+
[DEVICE_TYPES.ANDROID]: '',
|
|
982
|
+
[DEVICE_TYPES.IOS]: '',
|
|
976
983
|
};
|
|
977
984
|
|
|
978
985
|
const { rerender } = render(
|
|
979
986
|
<TestComponent initialContent={initialContent} />
|
|
980
987
|
);
|
|
981
988
|
|
|
982
|
-
expect(screen.getByTestId('android-content')).toHaveTextContent('
|
|
983
|
-
expect(screen.getByTestId('ios-content')).toHaveTextContent('
|
|
989
|
+
expect(screen.getByTestId('android-content')).toHaveTextContent('');
|
|
990
|
+
expect(screen.getByTestId('ios-content')).toHaveTextContent('');
|
|
984
991
|
|
|
992
|
+
// Transition from empty to meaningful content - this SHOULD update
|
|
985
993
|
const updatedContent = {
|
|
986
994
|
[DEVICE_TYPES.ANDROID]: 'updated android',
|
|
987
995
|
[DEVICE_TYPES.IOS]: 'updated ios',
|
|
@@ -1016,6 +1024,8 @@ describe('useInAppContent', () => {
|
|
|
1016
1024
|
});
|
|
1017
1025
|
|
|
1018
1026
|
it('handles undefined newAndroidContent gracefully', () => {
|
|
1027
|
+
// With the fix, after initial load, rerendering with different initialContent won't update
|
|
1028
|
+
// This test now verifies that content persists even when initialContent changes
|
|
1019
1029
|
const initialContent = {
|
|
1020
1030
|
[DEVICE_TYPES.ANDROID]: 'initial android',
|
|
1021
1031
|
[DEVICE_TYPES.IOS]: 'initial ios',
|
|
@@ -1026,17 +1036,18 @@ describe('useInAppContent', () => {
|
|
|
1026
1036
|
);
|
|
1027
1037
|
|
|
1028
1038
|
const updatedContent = {
|
|
1029
|
-
[DEVICE_TYPES.IOS]: 'updated ios',
|
|
1039
|
+
[DEVICE_TYPES.IOS]: 'updated ios', // Android is undefined
|
|
1030
1040
|
};
|
|
1031
1041
|
|
|
1032
1042
|
rerender(<TestComponent initialContent={updatedContent} />);
|
|
1033
1043
|
|
|
1034
|
-
//
|
|
1044
|
+
// With the fix, content should NOT change after initial load (prevents overriding user edits)
|
|
1035
1045
|
expect(screen.getByTestId('android-content')).toHaveTextContent('initial android');
|
|
1036
|
-
expect(screen.getByTestId('ios-content')).toHaveTextContent('
|
|
1046
|
+
expect(screen.getByTestId('ios-content')).toHaveTextContent('initial ios');
|
|
1037
1047
|
});
|
|
1038
1048
|
|
|
1039
1049
|
it('handles undefined newIosContent gracefully', () => {
|
|
1050
|
+
// With the fix, after initial load, rerendering with different initialContent won't update
|
|
1040
1051
|
const initialContent = {
|
|
1041
1052
|
[DEVICE_TYPES.ANDROID]: 'initial android',
|
|
1042
1053
|
[DEVICE_TYPES.IOS]: 'initial ios',
|
|
@@ -1047,26 +1058,28 @@ describe('useInAppContent', () => {
|
|
|
1047
1058
|
);
|
|
1048
1059
|
|
|
1049
1060
|
const updatedContent = {
|
|
1050
|
-
[DEVICE_TYPES.ANDROID]: 'updated android',
|
|
1061
|
+
[DEVICE_TYPES.ANDROID]: 'updated android', // iOS is undefined
|
|
1051
1062
|
};
|
|
1052
1063
|
|
|
1053
1064
|
rerender(<TestComponent initialContent={updatedContent} />);
|
|
1054
1065
|
|
|
1055
|
-
//
|
|
1056
|
-
expect(screen.getByTestId('android-content')).toHaveTextContent('
|
|
1066
|
+
// With the fix, content should NOT change after initial load (prevents overriding user edits)
|
|
1067
|
+
expect(screen.getByTestId('android-content')).toHaveTextContent('initial android');
|
|
1057
1068
|
expect(screen.getByTestId('ios-content')).toHaveTextContent('initial ios');
|
|
1058
1069
|
});
|
|
1059
1070
|
|
|
1060
1071
|
it('returns updated content object when changes are made', () => {
|
|
1072
|
+
// Test that the transition from empty to content works correctly
|
|
1061
1073
|
const initialContent = {
|
|
1062
|
-
[DEVICE_TYPES.ANDROID]: '
|
|
1063
|
-
[DEVICE_TYPES.IOS]: '
|
|
1074
|
+
[DEVICE_TYPES.ANDROID]: '',
|
|
1075
|
+
[DEVICE_TYPES.IOS]: '',
|
|
1064
1076
|
};
|
|
1065
1077
|
|
|
1066
1078
|
const { rerender } = render(
|
|
1067
1079
|
<TestComponent initialContent={initialContent} />
|
|
1068
1080
|
);
|
|
1069
1081
|
|
|
1082
|
+
// Transition from empty to content - this SHOULD update
|
|
1070
1083
|
const updatedContent = {
|
|
1071
1084
|
[DEVICE_TYPES.ANDROID]: 'updated',
|
|
1072
1085
|
[DEVICE_TYPES.IOS]: 'updated',
|