@pie-lib/test-utils 0.22.1 → 0.22.2-next.164

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/src/index.js CHANGED
@@ -1,18 +1,139 @@
1
1
  import * as React from 'react';
2
- import { shallow } from 'enzyme';
2
+ import PropTypes from 'prop-types';
3
+ import { render } from '@testing-library/react';
4
+ import { ThemeProvider, createTheme } from '@mui/material/styles';
3
5
 
4
- export function shallowChild(Component, defaultProps = {}, nestLevel) {
5
- return function innerRender(props = {}) {
6
- let rendered = shallow(<Component {...defaultProps} {...props} />);
6
+ /**
7
+ * Default MUI theme for testing
8
+ */
9
+ const defaultTheme = createTheme();
7
10
 
8
- if (nestLevel) {
9
- let repeat = nestLevel;
11
+ /**
12
+ * Render a component with MUI ThemeProvider
13
+ *
14
+ * @param {React.ReactElement} ui - The component to render
15
+ * @param {Object} options - Render options
16
+ * @param {Object} options.theme - Custom MUI theme (optional)
17
+ * @param {Object} options.renderOptions - Additional options passed to RTL render
18
+ * @returns {Object} RTL render result
19
+ *
20
+ * @example
21
+ * const { getByRole } = renderWithTheme(<Button>Click me</Button>);
22
+ * expect(getByRole('button')).toBeInTheDocument();
23
+ */
24
+ export function renderWithTheme(ui, options = {}) {
25
+ const { theme = defaultTheme, ...renderOptions } = options;
10
26
 
11
- while (repeat--) {
12
- rendered = rendered.first().shallow();
13
- }
14
- }
27
+ function Wrapper({ children }) {
28
+ return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
29
+ }
30
+ Wrapper.propTypes = {
31
+ children: PropTypes.node,
32
+ };
33
+
34
+ return render(ui, { wrapper: Wrapper, ...renderOptions });
35
+ }
36
+
37
+ /**
38
+ * Render a component with multiple providers (Theme, etc.)
39
+ * Useful when you need to wrap components with various context providers
40
+ *
41
+ * @param {React.ReactElement} ui - The component to render
42
+ * @param {Object} options - Render options
43
+ * @param {Object} options.theme - Custom MUI theme (optional)
44
+ * @param {Array<React.ComponentType>} options.providers - Additional providers to wrap
45
+ * @param {Object} options.renderOptions - Additional options passed to RTL render
46
+ * @returns {Object} RTL render result
47
+ *
48
+ * @example
49
+ * const { getByText } = renderWithProviders(
50
+ * <MyComponent />,
51
+ * { providers: [ReduxProvider, RouterProvider] }
52
+ * );
53
+ */
54
+ export function renderWithProviders(ui, options = {}) {
55
+ const { theme = defaultTheme, providers = [], ...renderOptions } = options;
15
56
 
16
- return rendered;
57
+ function Wrapper({ children }) {
58
+ let wrapped = <ThemeProvider theme={theme}>{children}</ThemeProvider>;
59
+
60
+ // Wrap with additional providers (from innermost to outermost)
61
+ providers.forEach((Provider) => {
62
+ wrapped = <Provider>{wrapped}</Provider>;
63
+ });
64
+
65
+ return wrapped;
66
+ }
67
+ Wrapper.propTypes = {
68
+ children: PropTypes.node,
17
69
  };
70
+
71
+ return render(ui, { wrapper: Wrapper, ...renderOptions });
72
+ }
73
+
74
+ /**
75
+ * Create a custom theme for testing
76
+ * Useful for testing components with specific theme configurations
77
+ *
78
+ * @param {Object} themeOptions - MUI theme options
79
+ * @returns {Object} MUI theme
80
+ *
81
+ * @example
82
+ * const darkTheme = createTestTheme({ palette: { mode: 'dark' } });
83
+ * renderWithTheme(<Component />, { theme: darkTheme });
84
+ */
85
+ export function createTestTheme(themeOptions = {}) {
86
+ return createTheme(themeOptions);
18
87
  }
88
+
89
+ /**
90
+ * Wait for an element to be removed from the DOM
91
+ * Wrapper around waitForElementToBeRemoved for convenience
92
+ *
93
+ * @example
94
+ * await waitForRemoval(() => screen.queryByText('Loading...'));
95
+ */
96
+ export { waitForElementToBeRemoved as waitForRemoval } from '@testing-library/react';
97
+
98
+ /**
99
+ * Re-export all of @testing-library/react for convenience
100
+ * This allows consumers to import everything from one place
101
+ */
102
+ export * from '@testing-library/react';
103
+
104
+ /**
105
+ * Re-export userEvent as a named export for convenience
106
+ */
107
+ export { default as userEvent } from '@testing-library/user-event';
108
+
109
+ /**
110
+ * Re-export jest-dom matchers (they're automatically added in jest.setup.js,
111
+ * but we export them here for TypeScript support)
112
+ */
113
+ export * from '@testing-library/jest-dom';
114
+
115
+ /**
116
+ * Keyboard helpers for testing keyboard interactions
117
+ * Especially useful for components checking event.keyCode
118
+ */
119
+ export {
120
+ Keys,
121
+ pressKey,
122
+ typeAndSubmit,
123
+ clearAndType,
124
+ navigateWithKeys,
125
+ } from './keyboard';
126
+
127
+ /**
128
+ * Web component testing utilities
129
+ * For testing light DOM custom elements (no Shadow DOM)
130
+ */
131
+ export {
132
+ waitForCustomElement,
133
+ renderWebComponent,
134
+ dispatchCustomEvent,
135
+ waitForEvent,
136
+ isCustomElementDefined,
137
+ createCustomElement,
138
+ } from './web-components';
139
+
@@ -0,0 +1,126 @@
1
+ import userEvent from '@testing-library/user-event';
2
+ import { fireEvent } from '@testing-library/react';
3
+
4
+ /**
5
+ * Common keyboard key codes
6
+ * Useful for legacy components that check event.keyCode
7
+ *
8
+ * @example
9
+ * pressKey(input, Keys.ENTER);
10
+ * pressKey(input, Keys.ESCAPE);
11
+ */
12
+ export const Keys = {
13
+ ENTER: 13,
14
+ ESCAPE: 27,
15
+ SPACE: 32,
16
+ ARROW_LEFT: 37,
17
+ ARROW_UP: 38,
18
+ ARROW_RIGHT: 39,
19
+ ARROW_DOWN: 40,
20
+ TAB: 9,
21
+ BACKSPACE: 8,
22
+ DELETE: 46,
23
+ HOME: 36,
24
+ END: 35,
25
+ PAGE_UP: 33,
26
+ PAGE_DOWN: 34,
27
+ };
28
+
29
+ /**
30
+ * Simulate keyboard event with keyCode
31
+ * Useful for legacy components checking event.keyCode
32
+ *
33
+ * userEvent.type() with special keys like {Enter} doesn't work well with
34
+ * components that check event.keyCode. Use this helper instead.
35
+ *
36
+ * @param {HTMLElement} element - Target element
37
+ * @param {number} keyCode - Key code (use Keys.ENTER, Keys.ESCAPE, etc.)
38
+ * @param {string} type - Event type (keydown, keyup, keypress)
39
+ * @param {Object} options - Additional event properties
40
+ *
41
+ * @example
42
+ * // Press Enter on an input
43
+ * const input = screen.getByRole('textbox');
44
+ * pressKey(input, Keys.ENTER);
45
+ *
46
+ * @example
47
+ * // Press Escape with keyup event
48
+ * pressKey(dialog, Keys.ESCAPE, 'keyup');
49
+ *
50
+ * @example
51
+ * // Press with additional properties
52
+ * pressKey(input, Keys.ENTER, 'keydown', { ctrlKey: true });
53
+ */
54
+ export function pressKey(element, keyCode, type = 'keydown', options = {}) {
55
+ const event = new KeyboardEvent(type, {
56
+ keyCode,
57
+ which: keyCode,
58
+ bubbles: true,
59
+ cancelable: true,
60
+ ...options,
61
+ });
62
+
63
+ fireEvent(element, event);
64
+ }
65
+
66
+ /**
67
+ * Type text and then press Enter
68
+ * Common pattern for form submissions
69
+ *
70
+ * @param {HTMLElement} element - Input element
71
+ * @param {string} text - Text to type
72
+ *
73
+ * @example
74
+ * const input = screen.getByRole('textbox');
75
+ * await typeAndSubmit(input, 'hello world');
76
+ * expect(onSubmit).toHaveBeenCalledWith('hello world');
77
+ */
78
+ export async function typeAndSubmit(element, text) {
79
+ const user = userEvent.setup();
80
+ await user.type(element, text);
81
+ pressKey(element, Keys.ENTER);
82
+ }
83
+
84
+ /**
85
+ * Clear input and type new text
86
+ * Common pattern for updating form fields
87
+ *
88
+ * @param {HTMLElement} element - Input element
89
+ * @param {string} text - New text to type
90
+ *
91
+ * @example
92
+ * const input = screen.getByRole('textbox');
93
+ * await clearAndType(input, 'new value');
94
+ */
95
+ export async function clearAndType(element, text) {
96
+ const user = userEvent.setup();
97
+ await user.clear(element);
98
+ await user.type(element, text);
99
+ }
100
+
101
+ /**
102
+ * Simulate keyboard navigation
103
+ * Press arrow keys to navigate through a list
104
+ *
105
+ * @param {HTMLElement} element - List or container element
106
+ * @param {number} steps - Number of steps to navigate (positive = down/right, negative = up/left)
107
+ * @param {string} direction - 'vertical' or 'horizontal'
108
+ *
109
+ * @example
110
+ * // Navigate down 3 items in a list
111
+ * navigateWithKeys(listbox, 3, 'vertical');
112
+ *
113
+ * @example
114
+ * // Navigate left 2 items
115
+ * navigateWithKeys(tabs, -2, 'horizontal');
116
+ */
117
+ export function navigateWithKeys(element, steps, direction = 'vertical') {
118
+ const key = direction === 'vertical'
119
+ ? (steps > 0 ? Keys.ARROW_DOWN : Keys.ARROW_UP)
120
+ : (steps > 0 ? Keys.ARROW_RIGHT : Keys.ARROW_LEFT);
121
+
122
+ const count = Math.abs(steps);
123
+ for (let i = 0; i < count; i++) {
124
+ pressKey(element, key);
125
+ }
126
+ }
@@ -0,0 +1,200 @@
1
+ // Note: These helpers are for light DOM web components (no Shadow DOM)
2
+ // Standard React Testing Library queries work directly on these components
3
+
4
+ /**
5
+ * Wait for a custom element to be defined
6
+ * Custom elements are registered asynchronously
7
+ *
8
+ * @param {string} tagName - Custom element tag name (e.g., 'my-component')
9
+ * @param {number} timeout - Timeout in milliseconds
10
+ * @returns {Promise<void>}
11
+ *
12
+ * @example
13
+ * await waitForCustomElement('pie-chart');
14
+ * const chart = document.createElement('pie-chart');
15
+ *
16
+ * @example
17
+ * await waitForCustomElement('my-component', 5000);
18
+ */
19
+ export async function waitForCustomElement(tagName, timeout = 3000) {
20
+ if (customElements.get(tagName)) {
21
+ return;
22
+ }
23
+
24
+ return new Promise((resolve, reject) => {
25
+ const timer = setTimeout(() => {
26
+ reject(
27
+ new Error(
28
+ `Custom element '${tagName}' not defined within ${timeout}ms. ` +
29
+ 'Make sure the element is registered with customElements.define().'
30
+ )
31
+ );
32
+ }, timeout);
33
+
34
+ customElements.whenDefined(tagName).then(() => {
35
+ clearTimeout(timer);
36
+ resolve();
37
+ });
38
+ });
39
+ }
40
+
41
+ /**
42
+ * Render a web component and wait for it to be ready
43
+ * Handles the full lifecycle: wait for definition, create, append, wait for render
44
+ *
45
+ * @param {string} tagName - Custom element tag name
46
+ * @param {Object} attributes - Attributes to set on the element
47
+ * @param {Object} properties - Properties to set on the element
48
+ * @param {HTMLElement} container - Container to append to (defaults to document.body)
49
+ * @returns {Promise<HTMLElement>} The custom element
50
+ *
51
+ * @example
52
+ * const chart = await renderWebComponent('pie-chart', {
53
+ * type: 'bar',
54
+ * 'data-testid': 'my-chart'
55
+ * });
56
+ *
57
+ * @example
58
+ * const button = await renderWebComponent('custom-button',
59
+ * { 'aria-label': 'Submit' },
60
+ * { onClick: jest.fn() }
61
+ * );
62
+ */
63
+ export async function renderWebComponent(
64
+ tagName,
65
+ attributes = {},
66
+ properties = {},
67
+ container = document.body
68
+ ) {
69
+ await waitForCustomElement(tagName);
70
+
71
+ const element = document.createElement(tagName);
72
+
73
+ // Set attributes (strings)
74
+ Object.entries(attributes).forEach(([key, value]) => {
75
+ element.setAttribute(key, value);
76
+ });
77
+
78
+ // Set properties (objects, functions, etc.)
79
+ Object.entries(properties).forEach(([key, value]) => {
80
+ element[key] = value;
81
+ });
82
+
83
+ container.appendChild(element);
84
+
85
+ // Wait for component to render (custom elements may be async)
86
+ await new Promise((resolve) => setTimeout(resolve, 0));
87
+
88
+ return element;
89
+ }
90
+
91
+ /**
92
+ * Dispatch a custom event on an element
93
+ * Web components often use custom events for communication
94
+ *
95
+ * @param {HTMLElement} element - Element to dispatch event from
96
+ * @param {string} eventName - Event name (e.g., 'change', 'custom-event')
97
+ * @param {*} detail - Event detail data
98
+ * @param {Object} options - Event options (bubbles, composed, etc.)
99
+ *
100
+ * @example
101
+ * dispatchCustomEvent(chart, 'data-changed', { value: [1, 2, 3] });
102
+ *
103
+ * @example
104
+ * dispatchCustomEvent(button, 'custom-click', null, { bubbles: false });
105
+ */
106
+ export function dispatchCustomEvent(
107
+ element,
108
+ eventName,
109
+ detail = null,
110
+ options = {}
111
+ ) {
112
+ const event = new CustomEvent(eventName, {
113
+ detail,
114
+ bubbles: true,
115
+ composed: true, // Allow event to cross shadow DOM boundary
116
+ ...options,
117
+ });
118
+
119
+ element.dispatchEvent(event);
120
+ return event;
121
+ }
122
+
123
+ /**
124
+ * Listen for a custom event and return a promise that resolves when fired
125
+ * Useful for testing event emissions
126
+ *
127
+ * @param {HTMLElement} element - Element to listen to
128
+ * @param {string} eventName - Event name to wait for
129
+ * @param {number} timeout - Timeout in milliseconds
130
+ * @returns {Promise<CustomEvent>} Promise that resolves with the event
131
+ *
132
+ * @example
133
+ * const promise = waitForEvent(chart, 'data-loaded');
134
+ * chart.loadData();
135
+ * const event = await promise;
136
+ * expect(event.detail).toEqual({ loaded: true });
137
+ *
138
+ * @example
139
+ * await waitForEvent(component, 'ready', 5000);
140
+ */
141
+ export function waitForEvent(element, eventName, timeout = 3000) {
142
+ return new Promise((resolve, reject) => {
143
+ const timer = setTimeout(() => {
144
+ element.removeEventListener(eventName, handler);
145
+ reject(
146
+ new Error(`Event '${eventName}' not fired within ${timeout}ms`)
147
+ );
148
+ }, timeout);
149
+
150
+ const handler = (event) => {
151
+ clearTimeout(timer);
152
+ element.removeEventListener(eventName, handler);
153
+ resolve(event);
154
+ };
155
+
156
+ element.addEventListener(eventName, handler);
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Check if a custom element is defined
162
+ * Useful for verifying element registration
163
+ *
164
+ * @param {string} tagName - Custom element tag name
165
+ * @returns {boolean} True if element is defined
166
+ *
167
+ * @example
168
+ * if (isCustomElementDefined('pie-chart')) {
169
+ * // Element is ready to use
170
+ * }
171
+ */
172
+ export function isCustomElementDefined(tagName) {
173
+ return typeof customElements !== 'undefined' && customElements.get(tagName) !== undefined;
174
+ }
175
+
176
+ /**
177
+ * Helper to create and configure a custom element
178
+ * For light DOM web components that render React
179
+ *
180
+ * @param {string} tagName - Custom element tag name
181
+ * @param {Object} props - Props to pass to the element
182
+ * @returns {HTMLElement} The custom element
183
+ *
184
+ * @example
185
+ * const chart = createCustomElement('pie-chart', {
186
+ * data: [1, 2, 3],
187
+ * type: 'bar'
188
+ * });
189
+ * document.body.appendChild(chart);
190
+ */
191
+ export function createCustomElement(tagName, props = {}) {
192
+ const element = document.createElement(tagName);
193
+
194
+ // Set properties directly on the element
195
+ Object.entries(props).forEach(([key, value]) => {
196
+ element[key] = value;
197
+ });
198
+
199
+ return element;
200
+ }