@hubspot/ui-extensions 0.11.1 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/dist/__synced__/experimental/types.synced.d.ts +3 -4
  2. package/dist/__synced__/remoteComponents.synced.d.ts +152 -70
  3. package/dist/__synced__/remoteComponents.synced.js +98 -0
  4. package/dist/__synced__/types/components/button.synced.d.ts +6 -0
  5. package/dist/__tests__/crm/hooks/useAssociations.spec.js +33 -29
  6. package/dist/__tests__/crm/hooks/useCrmProperties.spec.js +19 -18
  7. package/dist/__tests__/crm/utils/fetchAssociations.spec.js +8 -7
  8. package/dist/__tests__/crm/utils/fetchCrmProperties.spec.js +34 -33
  9. package/dist/crm/index.d.ts +1 -1
  10. package/dist/crm/index.js +1 -1
  11. package/dist/experimental/index.d.ts +1 -1
  12. package/dist/experimental/index.js +1 -1
  13. package/dist/experimental/testing/__tests__/debug.spec.d.ts +1 -0
  14. package/dist/experimental/testing/__tests__/debug.spec.js +43 -0
  15. package/dist/experimental/testing/__tests__/find.spec.d.ts +1 -0
  16. package/dist/experimental/testing/__tests__/find.spec.js +33 -0
  17. package/dist/experimental/testing/__tests__/findAll.spec.d.ts +1 -0
  18. package/dist/experimental/testing/__tests__/findAll.spec.js +12 -0
  19. package/dist/experimental/testing/__tests__/findAllChildren.spec.d.ts +1 -0
  20. package/dist/experimental/testing/__tests__/findAllChildren.spec.js +48 -0
  21. package/dist/experimental/testing/__tests__/findChild.spec.d.ts +1 -0
  22. package/dist/experimental/testing/__tests__/findChild.spec.js +29 -0
  23. package/dist/experimental/testing/__tests__/fragments.spec.d.ts +1 -0
  24. package/dist/experimental/testing/__tests__/fragments.spec.js +59 -0
  25. package/dist/experimental/testing/__tests__/invalid-components.spec.d.ts +1 -0
  26. package/dist/experimental/testing/__tests__/invalid-components.spec.js +88 -0
  27. package/dist/experimental/testing/__tests__/isMatch.spec.d.ts +1 -0
  28. package/dist/experimental/testing/__tests__/isMatch.spec.js +60 -0
  29. package/dist/experimental/testing/__tests__/maybeFind.spec.d.ts +1 -0
  30. package/dist/experimental/testing/__tests__/maybeFind.spec.js +58 -0
  31. package/dist/experimental/testing/__tests__/maybeFindChild.spec.d.ts +1 -0
  32. package/dist/experimental/testing/__tests__/maybeFindChild.spec.js +65 -0
  33. package/dist/experimental/testing/__tests__/trigger.spec.d.ts +1 -0
  34. package/dist/experimental/testing/__tests__/trigger.spec.js +40 -0
  35. package/dist/experimental/testing/__tests__/type-utils.spec.d.ts +1 -0
  36. package/dist/experimental/testing/__tests__/type-utils.spec.js +163 -0
  37. package/dist/experimental/testing/__tests__/waitFor.spec.d.ts +1 -0
  38. package/dist/experimental/testing/__tests__/waitFor.spec.js +55 -0
  39. package/dist/experimental/testing/index.d.ts +3 -0
  40. package/dist/experimental/testing/index.js +3 -0
  41. package/dist/experimental/testing/internal/convert.d.ts +10 -0
  42. package/dist/experimental/testing/internal/convert.js +131 -0
  43. package/dist/experimental/testing/internal/debug.d.ts +1 -1
  44. package/dist/experimental/testing/internal/debug.js +10 -1
  45. package/dist/experimental/testing/internal/document.d.ts +14 -0
  46. package/dist/experimental/testing/internal/document.js +37 -0
  47. package/dist/experimental/testing/internal/errors.d.ts +12 -0
  48. package/dist/experimental/testing/internal/errors.js +18 -0
  49. package/dist/experimental/testing/internal/match.d.ts +19 -0
  50. package/dist/experimental/testing/internal/match.js +42 -0
  51. package/dist/experimental/testing/internal/query.js +1 -19
  52. package/dist/experimental/testing/internal/utils/promise-utils.d.ts +14 -0
  53. package/dist/experimental/testing/internal/utils/promise-utils.js +14 -0
  54. package/dist/experimental/testing/render.d.ts +9 -0
  55. package/dist/experimental/testing/render.js +155 -0
  56. package/dist/experimental/testing/types.d.ts +1 -0
  57. package/dist/pages/home/index.d.ts +1 -1
  58. package/dist/pages/home/index.js +1 -1
  59. package/package.json +11 -13
  60. package/dist/__synced__/appHomeRemoteComponents.synced.d.ts +0 -28
  61. package/dist/__synced__/appHomeRemoteComponents.synced.js +0 -21
  62. package/dist/__synced__/crmRemoteComponents.synced.d.ts +0 -66
  63. package/dist/__synced__/crmRemoteComponents.synced.js +0 -15
  64. package/dist/__synced__/experimentalRemoteComponents.synced.d.ts +0 -94
  65. package/dist/__synced__/experimentalRemoteComponents.synced.js +0 -56
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Alert, Button } from '../../../index';
4
+ import { render } from '../index';
5
+ describe('isMatch()', () => {
6
+ it('should allow assertions against a mix of text and element nodes', () => {
7
+ const { getRootNode, isMatch } = render(_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", children: "Click me!" }), _jsx(Alert, { title: "My Alert" }), "Hello"] }));
8
+ const rootNode = getRootNode();
9
+ const children = rootNode.children;
10
+ const buttonNode = children[0];
11
+ const alertNode = children[1];
12
+ const textNode = children[2];
13
+ if (isMatch(buttonNode, Button)) {
14
+ expect(buttonNode.props.variant).toEqual('primary');
15
+ }
16
+ else {
17
+ throw new Error('Expected button node');
18
+ }
19
+ expect(isMatch(alertNode, Button)).toBe(false);
20
+ expect(isMatch(textNode, Button)).toBe(false);
21
+ });
22
+ it('should allow a custom matcher function to be provided', () => {
23
+ const { getRootNode, isMatch } = render(_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", children: "Primary" }), _jsx(Button, { variant: "secondary", children: "Secondary" }), _jsx(Alert, { title: "My Alert" }), "Hello"] }));
24
+ const rootNode = getRootNode();
25
+ const children = rootNode.children;
26
+ const primaryButtonNode = children[0];
27
+ const secondaryButtonNode = children[1];
28
+ const alertNode = children[2];
29
+ const textNode = children[3];
30
+ const matcher = (node) => node.props.variant === 'primary';
31
+ if (isMatch(primaryButtonNode, Button, matcher)) {
32
+ expect(primaryButtonNode.props.variant).toEqual('primary');
33
+ }
34
+ else {
35
+ throw new Error('Expected button node');
36
+ }
37
+ expect(isMatch(secondaryButtonNode, Button, matcher)).toBe(false);
38
+ expect(isMatch(alertNode, Button, matcher)).toBe(false);
39
+ expect(isMatch(textNode, Button, matcher)).toBe(false);
40
+ });
41
+ it('should allow a partial props object to be provided for matching', () => {
42
+ const { getRootNode, isMatch } = render(_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", children: "Primary" }), _jsx(Button, { variant: "secondary", children: "Secondary" }), _jsx(Alert, { title: "My Alert" }), "Hello"] }));
43
+ const rootNode = getRootNode();
44
+ const children = rootNode.children;
45
+ const primaryButtonNode = children[0];
46
+ const secondaryButtonNode = children[1];
47
+ const alertNode = children[2];
48
+ const textNode = children[3];
49
+ const matcher = { variant: 'primary' };
50
+ if (isMatch(primaryButtonNode, Button, matcher)) {
51
+ expect(primaryButtonNode.props.variant).toEqual('primary');
52
+ }
53
+ else {
54
+ throw new Error('Expected button node');
55
+ }
56
+ expect(isMatch(secondaryButtonNode, Button, matcher)).toBe(false);
57
+ expect(isMatch(alertNode, Button, matcher)).toBe(false);
58
+ expect(isMatch(textNode, Button, matcher)).toBe(false);
59
+ });
60
+ });
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Alert, Button, ButtonRow, Text } from '../../../index';
4
+ import { render } from '../index';
5
+ describe('maybeFind()', () => {
6
+ it('should return the element when found', () => {
7
+ const { maybeFind } = render(_jsxs(_Fragment, { children: [_jsx(ButtonRow, { children: _jsx(Button, { variant: "primary", children: "Button 1" }) }), _jsx(Alert, { title: "My Alert" })] }));
8
+ const button = maybeFind(Button);
9
+ expect(button).toBeDefined();
10
+ expect(button?.props).toEqual({ variant: 'primary' });
11
+ });
12
+ it('should return null when element is not found', () => {
13
+ const { maybeFind } = render(_jsxs(_Fragment, { children: [_jsx(ButtonRow, { children: _jsx(Button, { variant: "primary", children: "Button 1" }) }), _jsx(Alert, { title: "My Alert" })] }));
14
+ const text = maybeFind(Text);
15
+ expect(text).toBeNull();
16
+ });
17
+ it('should return the first matching element when multiple elements match', () => {
18
+ const { maybeFind } = render(_jsxs(_Fragment, { children: [_jsx(Alert, { title: "First Alert" }), _jsx(Alert, { title: "Second Alert" }), _jsx(Alert, { title: "Third Alert" })] }));
19
+ const alert = maybeFind(Alert);
20
+ expect(alert).toBeDefined();
21
+ expect(alert?.props).toMatchObject({ title: 'First Alert' });
22
+ });
23
+ it('should support matcher function', () => {
24
+ const { maybeFind } = render(_jsx(_Fragment, { children: _jsxs(ButtonRow, { children: [_jsx(Button, { variant: "secondary", children: "Button 1" }), _jsx(Button, { variant: "primary", children: "Button 2" })] }) }));
25
+ const primaryButton = maybeFind(Button, (node) => node.props.variant === 'primary');
26
+ expect(primaryButton).toBeDefined();
27
+ expect(primaryButton?.props).toMatchObject({ variant: 'primary' });
28
+ });
29
+ it('should return null when matcher function does not match any elements', () => {
30
+ const { maybeFind } = render(_jsx(_Fragment, { children: _jsxs(ButtonRow, { children: [_jsx(Button, { variant: "secondary", children: "Button 1" }), _jsx(Button, { variant: "secondary", children: "Button 2" })] }) }));
31
+ const primaryButton = maybeFind(Button, (node) => node.props.variant === 'primary');
32
+ expect(primaryButton).toBeNull();
33
+ });
34
+ it('should support matcher object', () => {
35
+ const { maybeFind } = render(_jsx(_Fragment, { children: _jsxs(ButtonRow, { children: [_jsx(Button, { variant: "secondary", children: "Button 1" }), _jsx(Button, { variant: "primary", children: "Button 2" })] }) }));
36
+ const primaryButton = maybeFind(Button, { variant: 'primary' });
37
+ expect(primaryButton).toBeDefined();
38
+ expect(primaryButton?.props).toMatchObject({ variant: 'primary' });
39
+ });
40
+ it('should return null when matcher object does not match any elements', () => {
41
+ const { maybeFind } = render(_jsx(_Fragment, { children: _jsxs(ButtonRow, { children: [_jsx(Button, { variant: "secondary", children: "Button 1" }), _jsx(Button, { variant: "secondary", children: "Button 2" })] }) }));
42
+ const primaryButton = maybeFind(Button, { variant: 'primary' });
43
+ expect(primaryButton).toBeNull();
44
+ });
45
+ it('should work on nested element nodes', () => {
46
+ const { find } = render(_jsx(_Fragment, { children: _jsx(ButtonRow, { children: _jsx(Button, { variant: "primary", children: "Button 1" }) }) }));
47
+ const buttonRow = find(ButtonRow);
48
+ const nestedButton = buttonRow.maybeFind(Button);
49
+ expect(nestedButton).toBeDefined();
50
+ expect(nestedButton?.props).toMatchObject({ variant: 'primary' });
51
+ });
52
+ it('should return null on nested element when not found', () => {
53
+ const { find } = render(_jsx(_Fragment, { children: _jsx(ButtonRow, { children: _jsx(Button, { variant: "secondary", children: "Button 1" }) }) }));
54
+ const buttonRow = find(ButtonRow);
55
+ const text = buttonRow.maybeFind(Text);
56
+ expect(text).toBeNull();
57
+ });
58
+ });
@@ -0,0 +1,65 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Alert, Button, ButtonRow, Text } from '../../../index';
4
+ import { render } from '../index';
5
+ describe('maybeFindChild()', () => {
6
+ it('should return direct child from the root node', () => {
7
+ const { maybeFindChild } = render(_jsxs(_Fragment, { children: [_jsx(ButtonRow, { children: _jsx(Button, { variant: "primary", children: "Button 1" }) }), _jsx(Alert, { title: "My Alert" })] }));
8
+ const alert = maybeFindChild(Alert);
9
+ expect(alert).toBeDefined();
10
+ expect(alert?.props).toEqual({ title: 'My Alert' });
11
+ });
12
+ it('should return null when direct child is not found', () => {
13
+ const { maybeFindChild } = render(_jsx(_Fragment, { children: _jsx(ButtonRow, { children: _jsx(Button, { variant: "primary", children: "Button 1" }) }) }));
14
+ const text = maybeFindChild(Text);
15
+ expect(text).toBeNull();
16
+ });
17
+ it('should only find direct children, not nested descendants', () => {
18
+ const { maybeFindChild } = render(_jsxs(_Fragment, { children: [_jsx(ButtonRow, { children: _jsx(Button, { variant: "primary", children: "Button 1" }) }), _jsx(Alert, { title: "My Alert" })] }));
19
+ const button = maybeFindChild(Button);
20
+ expect(button).toBeNull();
21
+ });
22
+ it('should return first direct child when multiple children match', () => {
23
+ const { maybeFindChild } = render(_jsxs(_Fragment, { children: [_jsx(Alert, { title: "First Alert" }), _jsx(Alert, { title: "Second Alert" }), _jsx(Alert, { title: "Third Alert" })] }));
24
+ const alert = maybeFindChild(Alert);
25
+ expect(alert).toBeDefined();
26
+ expect(alert?.props).toMatchObject({ title: 'First Alert' });
27
+ });
28
+ it('should support matcher function', () => {
29
+ const { maybeFindChild } = render(_jsxs(_Fragment, { children: [_jsx(Alert, { title: "First Alert" }), _jsx(Alert, { title: "Second Alert" })] }));
30
+ const secondAlert = maybeFindChild(Alert, (node) => node.props.title === 'Second Alert');
31
+ expect(secondAlert).toBeDefined();
32
+ expect(secondAlert?.props).toMatchObject({ title: 'Second Alert' });
33
+ });
34
+ it('should return null when matcher function does not match any children', () => {
35
+ const { maybeFindChild } = render(_jsxs(_Fragment, { children: [_jsx(Alert, { title: "First Alert" }), _jsx(Alert, { title: "Second Alert" })] }));
36
+ const thirdAlert = maybeFindChild(Alert, (node) => node.props.title === 'Third Alert');
37
+ expect(thirdAlert).toBeNull();
38
+ });
39
+ it('should support matcher object', () => {
40
+ const { maybeFindChild } = render(_jsxs(_Fragment, { children: [_jsx(Alert, { title: "First Alert" }), _jsx(Alert, { title: "Second Alert" })] }));
41
+ const secondAlert = maybeFindChild(Alert, { title: 'Second Alert' });
42
+ expect(secondAlert).toBeDefined();
43
+ expect(secondAlert?.props).toMatchObject({ title: 'Second Alert' });
44
+ });
45
+ it('should return null when matcher object does not match any children', () => {
46
+ const { maybeFindChild } = render(_jsxs(_Fragment, { children: [_jsx(Alert, { title: "First Alert" }), _jsx(Alert, { title: "Second Alert" })] }));
47
+ const thirdAlert = maybeFindChild(Alert, { title: 'Third Alert' });
48
+ expect(thirdAlert).toBeNull();
49
+ });
50
+ it('should work on nested element nodes', () => {
51
+ const { find } = render(_jsx(_Fragment, { children: _jsxs(ButtonRow, { children: [_jsx(Button, { variant: "primary", children: "Button 1" }), _jsx(Button, { variant: "secondary", children: "Button 2" })] }) }));
52
+ const buttonRow = find(ButtonRow);
53
+ const primaryButton = buttonRow.maybeFindChild(Button, {
54
+ variant: 'primary',
55
+ });
56
+ expect(primaryButton).toBeDefined();
57
+ expect(primaryButton?.props).toMatchObject({ variant: 'primary' });
58
+ });
59
+ it('should return null on nested element when child not found', () => {
60
+ const { find } = render(_jsx(_Fragment, { children: _jsx(ButtonRow, { children: _jsx(Button, { variant: "secondary", children: "Button 1" }) }) }));
61
+ const buttonRow = find(ButtonRow);
62
+ const text = buttonRow.maybeFindChild(Text);
63
+ expect(text).toBeNull();
64
+ });
65
+ });
@@ -0,0 +1,40 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { Button } from '../../../index';
5
+ import { render } from '../index';
6
+ import { InvalidEventFunctionError, MissingEventFunctionError, } from '../internal/errors';
7
+ describe('trigger()', () => {
8
+ it('should allow events to be triggered', () => {
9
+ function Counter() {
10
+ const [count, setCount] = useState(0);
11
+ const handleClick = () => {
12
+ setCount(count + 1);
13
+ };
14
+ return (_jsxs(Button, { variant: "primary", onClick: handleClick, children: ["Clicked ", count, " times"] }));
15
+ }
16
+ const { find } = render(_jsx(Counter, {}));
17
+ expect(find(Button).text).toEqual('Clicked 0 times');
18
+ find(Button).trigger('onClick');
19
+ expect(find(Button).text).toEqual('Clicked 1 times');
20
+ });
21
+ it('should throw the correct error when a triggered event function is null', () => {
22
+ function Counter() {
23
+ return _jsx(Button, { variant: "primary", children: "Click me!" });
24
+ }
25
+ const { find } = render(_jsx(Counter, {}));
26
+ expect(() => {
27
+ find(Button).trigger('onClick');
28
+ }).toThrow(MissingEventFunctionError);
29
+ });
30
+ it('should throw the correct error when a triggered event function is not a function but also not null', () => {
31
+ function Counter() {
32
+ const handleClick = {};
33
+ return (_jsx(Button, { variant: "primary", onClick: handleClick, children: "Click me!" }));
34
+ }
35
+ const { find } = render(_jsx(Counter, {}));
36
+ expect(() => {
37
+ find(Button).trigger('onClick');
38
+ }).toThrow(InvalidEventFunctionError);
39
+ });
40
+ });
@@ -0,0 +1,163 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Button, ButtonRow, Text } from '../../../index';
4
+ import { render } from '../index';
5
+ import { isRenderedElementNode, isRenderedFragmentNode, isRenderedRootNode, isRenderedTextNode, } from '../type-utils';
6
+ import { RenderedNodeType } from '../types';
7
+ const TestComponent = () => (_jsx(Button, { overlay: _jsx(Text, { children: "Hello" }), children: "Text content" }));
8
+ const setupTestNodes = () => {
9
+ const { find, getRootNode } = render(_jsx(TestComponent, {}));
10
+ const elementNode = find(Button);
11
+ const rootNode = getRootNode();
12
+ const textNode = find(Button).children[0];
13
+ const fragmentNode = find(Button).props.overlay;
14
+ return {
15
+ elementNode,
16
+ textNode,
17
+ rootNode,
18
+ fragmentNode,
19
+ };
20
+ };
21
+ describe('type guard functions', () => {
22
+ describe('isRenderedElementNode()', () => {
23
+ it('should return true for an element node', () => {
24
+ const { elementNode } = setupTestNodes();
25
+ expect(isRenderedElementNode(elementNode)).toBe(true);
26
+ expect(elementNode.nodeType).toBe(RenderedNodeType.Element);
27
+ });
28
+ it('should return false for a text node', () => {
29
+ const { textNode } = setupTestNodes();
30
+ expect(isRenderedElementNode(textNode)).toBe(false);
31
+ });
32
+ it('should return false for a root node', () => {
33
+ const { rootNode } = setupTestNodes();
34
+ expect(isRenderedElementNode(rootNode)).toBe(false);
35
+ });
36
+ it('should return false for a fragment node', () => {
37
+ const { fragmentNode } = setupTestNodes();
38
+ expect(isRenderedElementNode(fragmentNode)).toBe(false);
39
+ });
40
+ it('should return false for null', () => {
41
+ expect(isRenderedElementNode(null)).toBe(false);
42
+ });
43
+ it('should return false for undefined', () => {
44
+ expect(isRenderedElementNode(undefined)).toBe(false);
45
+ });
46
+ });
47
+ describe('isRenderedTextNode()', () => {
48
+ it('should return true for a text node', () => {
49
+ const { textNode } = setupTestNodes();
50
+ expect(isRenderedTextNode(textNode)).toBe(true);
51
+ expect(textNode.nodeType).toBe(RenderedNodeType.Text);
52
+ });
53
+ it('should return false for an element node', () => {
54
+ const { elementNode } = setupTestNodes();
55
+ expect(isRenderedTextNode(elementNode)).toBe(false);
56
+ });
57
+ it('should return false for a root node', () => {
58
+ const { rootNode } = setupTestNodes();
59
+ expect(isRenderedTextNode(rootNode)).toBe(false);
60
+ });
61
+ it('should return false for a fragment node', () => {
62
+ const { fragmentNode } = setupTestNodes();
63
+ expect(isRenderedTextNode(fragmentNode)).toBe(false);
64
+ });
65
+ it('should return false for null', () => {
66
+ expect(isRenderedTextNode(null)).toBe(false);
67
+ });
68
+ it('should return false for undefined', () => {
69
+ expect(isRenderedTextNode(undefined)).toBe(false);
70
+ });
71
+ });
72
+ describe('isRenderedRootNode()', () => {
73
+ it('should return true for a root node', () => {
74
+ const { rootNode } = setupTestNodes();
75
+ expect(isRenderedRootNode(rootNode)).toBe(true);
76
+ expect(rootNode.nodeType).toBe(RenderedNodeType.Root);
77
+ });
78
+ it('should return false for an element node', () => {
79
+ const { elementNode } = setupTestNodes();
80
+ expect(isRenderedRootNode(elementNode)).toBe(false);
81
+ });
82
+ it('should return false for a text node', () => {
83
+ const { textNode } = setupTestNodes();
84
+ expect(isRenderedRootNode(textNode)).toBe(false);
85
+ });
86
+ it('should return false for a fragment node', () => {
87
+ const { fragmentNode } = setupTestNodes();
88
+ expect(isRenderedRootNode(fragmentNode)).toBe(false);
89
+ });
90
+ it('should return false for null', () => {
91
+ expect(isRenderedRootNode(null)).toBe(false);
92
+ });
93
+ it('should return false for undefined', () => {
94
+ expect(isRenderedRootNode(undefined)).toBe(false);
95
+ });
96
+ });
97
+ describe('isRenderedFragmentNode()', () => {
98
+ it('should return true for a fragment node', () => {
99
+ const { fragmentNode } = setupTestNodes();
100
+ expect(isRenderedFragmentNode(fragmentNode)).toBe(true);
101
+ expect(fragmentNode.nodeType).toBe(RenderedNodeType.Fragment);
102
+ });
103
+ it('should return false for an element node', () => {
104
+ const { elementNode } = setupTestNodes();
105
+ expect(isRenderedFragmentNode(elementNode)).toBe(false);
106
+ });
107
+ it('should return false for a text node', () => {
108
+ const { textNode } = setupTestNodes();
109
+ expect(isRenderedFragmentNode(textNode)).toBe(false);
110
+ });
111
+ it('should return false for a root node', () => {
112
+ const { rootNode } = setupTestNodes();
113
+ expect(isRenderedFragmentNode(rootNode)).toBe(false);
114
+ });
115
+ it('should return false for null', () => {
116
+ expect(isRenderedFragmentNode(null)).toBe(false);
117
+ });
118
+ it('should return false for undefined', () => {
119
+ expect(isRenderedFragmentNode(undefined)).toBe(false);
120
+ });
121
+ });
122
+ describe('type narrowing', () => {
123
+ it('should correctly narrow types for element nodes', () => {
124
+ const { find } = render(_jsx(ButtonRow, { children: _jsx(Button, { variant: "primary", children: "Click me" }) }));
125
+ const buttonRow = find(ButtonRow);
126
+ const firstChild = buttonRow.children[0];
127
+ if (isRenderedElementNode(firstChild)) {
128
+ expect(firstChild.name).toBe('Button');
129
+ expect(firstChild.props.variant).toBe('primary');
130
+ }
131
+ else {
132
+ throw new Error('Expected element node');
133
+ }
134
+ });
135
+ it('should correctly narrow types for text nodes', () => {
136
+ const { textNode } = setupTestNodes();
137
+ if (isRenderedTextNode(textNode)) {
138
+ expect(textNode.text).toBe('Text content');
139
+ }
140
+ else {
141
+ throw new Error('Expected text node');
142
+ }
143
+ });
144
+ it('should correctly narrow types for root nodes', () => {
145
+ const { rootNode } = setupTestNodes();
146
+ if (isRenderedRootNode(rootNode)) {
147
+ expect(rootNode.children.length).toBeGreaterThan(0);
148
+ }
149
+ else {
150
+ throw new Error('Expected root node');
151
+ }
152
+ });
153
+ it('should correctly narrow types for fragment nodes', () => {
154
+ const { fragmentNode } = setupTestNodes();
155
+ if (isRenderedFragmentNode(fragmentNode)) {
156
+ expect(fragmentNode.children.length).toBe(1);
157
+ }
158
+ else {
159
+ throw new Error('Expected fragment node');
160
+ }
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,55 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import { Button } from '../../../index';
5
+ import { render } from '../index';
6
+ import { WaitForTimeoutError } from '../internal/errors';
7
+ describe('waitFor()', () => {
8
+ beforeEach(() => {
9
+ // tell vitest we use mocked time
10
+ vi.useFakeTimers();
11
+ });
12
+ afterEach(() => {
13
+ // restoring date after each test run
14
+ vi.useRealTimers();
15
+ });
16
+ it('should allow for waiting for asynchronous updates', async () => {
17
+ function AsyncCounter() {
18
+ const [count, setCount] = useState(0);
19
+ useEffect(() => {
20
+ setTimeout(() => {
21
+ setCount((currentCount) => currentCount + 1);
22
+ }, 10);
23
+ }, []);
24
+ return _jsxs(Button, { variant: "primary", children: ["Count: ", count] });
25
+ }
26
+ const { find, waitFor } = render(_jsx(AsyncCounter, {}));
27
+ expect(find(Button).text).toEqual('Count: 0');
28
+ const waitForPromise = waitFor(() => {
29
+ expect(find(Button).text).toEqual('Count: 1');
30
+ });
31
+ vi.advanceTimersByTimeAsync(100);
32
+ await waitForPromise;
33
+ });
34
+ it('should allow for waiting for asynchronous updates with a timeout', async () => {
35
+ vi.useFakeTimers();
36
+ function AsyncCounter() {
37
+ const [count, setCount] = useState(0);
38
+ useEffect(() => {
39
+ setTimeout(() => {
40
+ setCount((currentCount) => currentCount + 1);
41
+ }, 5000);
42
+ }, []);
43
+ return _jsxs(Button, { variant: "primary", children: ["Count: ", count] });
44
+ }
45
+ const { find, waitFor } = render(_jsx(AsyncCounter, {}));
46
+ expect(find(Button).text).toEqual('Count: 0');
47
+ const waitForPromise = waitFor(() => {
48
+ expect(find(Button).text).toEqual('Count: 1');
49
+ });
50
+ // The default timeout is 1000ms, so we advance the timers by 5000ms to ensure the timeout is triggered.
51
+ vi.advanceTimersByTimeAsync(5000);
52
+ await expect(waitForPromise).rejects.toThrow(WaitForTimeoutError);
53
+ });
54
+ // TODO: Add a test for waiting for asynchronous updates with a custom timeout
55
+ });
@@ -0,0 +1,3 @@
1
+ export * from './type-utils';
2
+ export * from './render';
3
+ export * from './types';
@@ -0,0 +1,3 @@
1
+ export * from './type-utils';
2
+ export * from './render';
3
+ export * from './types';
@@ -0,0 +1,10 @@
1
+ import { type RemoteRoot } from '@remote-ui/core';
2
+ import type { RenderedRootNodeInternal } from './types-internal';
3
+ /**
4
+ * Converts a remote root node to a rendered root node. After conversion, the rendered root node will
5
+ * have its children set to the converted children.
6
+ *
7
+ * @param rootNode The rendered root node to convert.
8
+ * @param remoteRoot The remote root node to convert.
9
+ */
10
+ export declare const convertRemoteRoot: (rootNode: RenderedRootNodeInternal, remoteRoot: RemoteRoot) => void;
@@ -0,0 +1,131 @@
1
+ import { KIND_COMPONENT, KIND_FRAGMENT, KIND_TEXT, } from '@remote-ui/core';
2
+ import { createElementNode } from './element';
3
+ import { InvalidFragmentPropArrayError } from './errors';
4
+ import { createFragmentNode } from './fragment';
5
+ import { createTextNode } from './text';
6
+ import { isRenderedTextNode } from '../type-utils';
7
+ import { __hubSpotComponentRegistry } from '../../../__synced__/remoteComponents.synced';
8
+ import { EMPTY_CHILDREN } from './constants';
9
+ /* eslint-disable @typescript-eslint/no-use-before-define */
10
+ /**
11
+ * Converts a remote props object to a props object that can be attached to a rendered element node.
12
+ *
13
+ * @param document The document to use for creating new nodes.
14
+ * @param componentName The name of the component to convert the props for.
15
+ * @param remoteProps The remote props object to convert.
16
+ * @returns The rendered props object.
17
+ */
18
+ const convertRemoteProps = (document, componentName, remoteProps) => {
19
+ if (typeof remoteProps !== 'object' || remoteProps == null) {
20
+ return {};
21
+ }
22
+ const convertedProps = {};
23
+ for (const [propName, remotePropValue] of Object.entries(remoteProps)) {
24
+ if (propName === 'children') {
25
+ // Skip over `children` props since we created nested nodes in our tree for children
26
+ continue;
27
+ }
28
+ if (__hubSpotComponentRegistry.isComponentFragmentProp(componentName, propName)) {
29
+ if (Array.isArray(remotePropValue)) {
30
+ throw new InvalidFragmentPropArrayError(componentName, propName);
31
+ }
32
+ const fragmentNode = createFragmentNode(document);
33
+ if (remotePropValue == null) {
34
+ fragmentNode.children = EMPTY_CHILDREN;
35
+ }
36
+ else if (typeof remotePropValue === 'string') {
37
+ const text = remotePropValue;
38
+ fragmentNode.text = text;
39
+ fragmentNode.children = [createTextNode(text)];
40
+ }
41
+ else if (remotePropValue.kind === KIND_FRAGMENT) {
42
+ convertRemoteChildren(fragmentNode, remotePropValue);
43
+ }
44
+ convertedProps[propName] = fragmentNode;
45
+ }
46
+ else {
47
+ convertedProps[propName] = remotePropValue;
48
+ }
49
+ }
50
+ return convertedProps;
51
+ };
52
+ /**
53
+ * Converts a remote child node to a rendered child node.
54
+ *
55
+ * @param document The document to use for creating new nodes.
56
+ * @param remoteChild The remote child node to convert.
57
+ *
58
+ * @returns The rendered child node.
59
+ */
60
+ const convertChildNode = (document, remoteChild) => {
61
+ if (remoteChild.kind === KIND_TEXT) {
62
+ return createTextNode(remoteChild);
63
+ }
64
+ else if (remoteChild.kind === KIND_COMPONENT) {
65
+ const name = remoteChild.type;
66
+ const props = convertRemoteProps(document, name, remoteChild.props);
67
+ const elementNode = createElementNode(document, name, props);
68
+ if (!__hubSpotComponentRegistry.isAllowedComponentName(name)) {
69
+ document.addInvalidComponentName(name);
70
+ }
71
+ convertRemoteChildren(elementNode, remoteChild);
72
+ return elementNode;
73
+ }
74
+ else {
75
+ throw new Error(`Illegal State. Unknown remote child. (${JSON.stringify(remoteChild)})`);
76
+ }
77
+ };
78
+ /**
79
+ * Converts the children a remote parent node to a rendered parent node. After conversion, the rendered parent node will
80
+ * have its children set to the converted children.
81
+ *
82
+ * @param parentNode The rendered parent node to convert.
83
+ * @param remoteParentNode The remote parent node to convert.
84
+ */
85
+ const convertRemoteChildren = (parentNode, remoteParentNode) => {
86
+ const { document } = parentNode;
87
+ const remoteChildren = remoteParentNode.children;
88
+ if (remoteChildren == null) {
89
+ return;
90
+ }
91
+ if (typeof remoteChildren === 'string') {
92
+ const text = remoteChildren;
93
+ parentNode.text = text;
94
+ parentNode.children = [createTextNode(text)];
95
+ return;
96
+ }
97
+ let text = null;
98
+ const children = [];
99
+ let lastRenderedChild;
100
+ for (const remoteChild of remoteChildren) {
101
+ if (remoteChild.kind === KIND_TEXT) {
102
+ const currentChildText = remoteChild.text;
103
+ if (text == null) {
104
+ text = currentChildText;
105
+ }
106
+ else {
107
+ text += currentChildText;
108
+ }
109
+ if (lastRenderedChild && isRenderedTextNode(lastRenderedChild)) {
110
+ // We normalize text nodes by combining consecutive text nodes into a single text node.
111
+ lastRenderedChild.text += remoteChild.text;
112
+ continue;
113
+ }
114
+ }
115
+ const renderedChild = convertChildNode(document, remoteChild);
116
+ children.push(renderedChild);
117
+ lastRenderedChild = renderedChild;
118
+ }
119
+ parentNode.text = text;
120
+ parentNode.children = children;
121
+ };
122
+ /**
123
+ * Converts a remote root node to a rendered root node. After conversion, the rendered root node will
124
+ * have its children set to the converted children.
125
+ *
126
+ * @param rootNode The rendered root node to convert.
127
+ * @param remoteRoot The remote root node to convert.
128
+ */
129
+ export const convertRemoteRoot = (rootNode, remoteRoot) => {
130
+ convertRemoteChildren(rootNode, remoteRoot);
131
+ };
@@ -5,4 +5,4 @@ import type { RenderedParentNode } from '../types';
5
5
  * @param parentNode The node to log.
6
6
  * @param label Optional label to display before the node tree.
7
7
  */
8
- export declare const debugLog: (parentNode: RenderedParentNode, label?: string) => never;
8
+ export declare const debugLog: (parentNode: RenderedParentNode, label?: string) => void;
@@ -1,3 +1,4 @@
1
+ import { printNode } from './print';
1
2
  /* eslint-disable @typescript-eslint/no-unused-vars */
2
3
  /**
3
4
  * Logs a rendered node tree to the console for debugging purposes.
@@ -6,5 +7,13 @@
6
7
  * @param label Optional label to display before the node tree.
7
8
  */
8
9
  export const debugLog = (parentNode, label) => {
9
- throw new Error('Not implemented');
10
+ const componentTreeString = printNode(parentNode);
11
+ const separator = ''.padEnd(label?.length ?? 20, '=');
12
+ const logLines = [separator];
13
+ if (label) {
14
+ logLines.push(label);
15
+ }
16
+ logLines.push(componentTreeString);
17
+ logLines.push(separator);
18
+ console.log(logLines.join('\n'));
10
19
  };
@@ -0,0 +1,14 @@
1
+ import type { RenderedDocumentInternal, RenderedRootNodeInternal } from './types-internal';
2
+ export interface CreateDocumentOptions {
3
+ /**
4
+ * A function that can be called to get the latest rendered root node.
5
+ */
6
+ getLatestRootNode: () => RenderedRootNodeInternal;
7
+ }
8
+ /**
9
+ * Creates a rendered document.
10
+ *
11
+ * @param options Options for creating the document.
12
+ * @returns A rendered document.
13
+ */
14
+ export declare const createDocument: (options: CreateDocumentOptions) => RenderedDocumentInternal;