@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/CHANGELOG.md +6 -64
- package/README.md +463 -0
- package/lib/index.js +255 -23
- package/lib/index.js.map +1 -1
- package/lib/keyboard.js +173 -0
- package/lib/keyboard.js.map +1 -0
- package/lib/web-components.js +248 -0
- package/lib/web-components.js.map +1 -0
- package/package.json +13 -5
- package/src/__tests__/index.test.js +88 -41
- package/src/__tests__/keyboard.test.js +116 -0
- package/src/index.js +132 -11
- package/src/keyboard.js +126 -0
- package/src/web-components.js +200 -0
package/src/index.js
CHANGED
|
@@ -1,18 +1,139 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import
|
|
2
|
+
import PropTypes from 'prop-types';
|
|
3
|
+
import { render } from '@testing-library/react';
|
|
4
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Default MUI theme for testing
|
|
8
|
+
*/
|
|
9
|
+
const defaultTheme = createTheme();
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
+
|
package/src/keyboard.js
ADDED
|
@@ -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
|
+
}
|