@memberjunction/react-test-harness 2.70.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +735 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +250 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/assertion-helpers.d.ts +37 -0
- package/dist/lib/assertion-helpers.d.ts.map +1 -0
- package/dist/lib/assertion-helpers.js +108 -0
- package/dist/lib/assertion-helpers.js.map +1 -0
- package/dist/lib/browser-context.d.ts +33 -0
- package/dist/lib/browser-context.d.ts.map +1 -0
- package/dist/lib/browser-context.js +83 -0
- package/dist/lib/browser-context.js.map +1 -0
- package/dist/lib/component-runner.d.ts +38 -0
- package/dist/lib/component-runner.d.ts.map +1 -0
- package/dist/lib/component-runner.js +177 -0
- package/dist/lib/component-runner.js.map +1 -0
- package/dist/lib/test-harness.d.ts +55 -0
- package/dist/lib/test-harness.d.ts.map +1 -0
- package/dist/lib/test-harness.js +185 -0
- package/dist/lib/test-harness.js.map +1 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
# @memberjunction/react-test-harness
|
|
2
|
+
|
|
3
|
+
A powerful test harness for React components using Playwright, designed specifically for MemberJunction's React runtime components.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This package provides a comprehensive testing solution for React components, allowing you to:
|
|
8
|
+
- Load and execute React components in a real browser environment
|
|
9
|
+
- Run assertions on rendered output
|
|
10
|
+
- Execute tests via CLI or programmatically
|
|
11
|
+
- Capture screenshots and console output
|
|
12
|
+
- Run in headless or headed mode for debugging
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @memberjunction/react-test-harness
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## CLI Usage
|
|
21
|
+
|
|
22
|
+
### Run a Single Component
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Basic usage
|
|
26
|
+
mj-react-test run MyComponent.jsx
|
|
27
|
+
|
|
28
|
+
# With props
|
|
29
|
+
mj-react-test run MyComponent.jsx --props '{"title":"Hello","count":42}'
|
|
30
|
+
|
|
31
|
+
# With screenshot
|
|
32
|
+
mj-react-test run MyComponent.jsx --screenshot ./output.png
|
|
33
|
+
|
|
34
|
+
# In headed mode (visible browser)
|
|
35
|
+
mj-react-test run MyComponent.jsx --headed
|
|
36
|
+
|
|
37
|
+
# With debug output
|
|
38
|
+
mj-react-test run MyComponent.jsx --debug
|
|
39
|
+
|
|
40
|
+
# Wait for specific selector
|
|
41
|
+
mj-react-test run MyComponent.jsx --selector "#loaded-content" --timeout 5000
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Run Test Files
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Run a test file with multiple test cases
|
|
48
|
+
mj-react-test test my-tests.js
|
|
49
|
+
|
|
50
|
+
# With options
|
|
51
|
+
mj-react-test test my-tests.js --headed --debug
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Create Example Files
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Create example component and test files
|
|
58
|
+
mj-react-test create-example
|
|
59
|
+
|
|
60
|
+
# Create in specific directory
|
|
61
|
+
mj-react-test create-example --dir ./my-tests
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Programmatic Usage via TypeScript/JavaScript
|
|
65
|
+
|
|
66
|
+
The test harness is designed to be used as a library in your TypeScript/JavaScript code, not just via CLI. All classes and types are fully exported for programmatic use.
|
|
67
|
+
|
|
68
|
+
### Importing Classes and Types
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Import main classes
|
|
72
|
+
import {
|
|
73
|
+
ReactTestHarness,
|
|
74
|
+
BrowserManager,
|
|
75
|
+
ComponentRunner,
|
|
76
|
+
AssertionHelpers,
|
|
77
|
+
FluentMatcher
|
|
78
|
+
} from '@memberjunction/react-test-harness';
|
|
79
|
+
|
|
80
|
+
// Import types for TypeScript
|
|
81
|
+
import type {
|
|
82
|
+
TestHarnessOptions,
|
|
83
|
+
ComponentExecutionResult,
|
|
84
|
+
ComponentExecutionOptions,
|
|
85
|
+
BrowserContextOptions,
|
|
86
|
+
TestCase,
|
|
87
|
+
TestSummary
|
|
88
|
+
} from '@memberjunction/react-test-harness';
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Basic Component Testing
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { ReactTestHarness } from '@memberjunction/react-test-harness';
|
|
95
|
+
|
|
96
|
+
async function testMyComponent() {
|
|
97
|
+
const harness = new ReactTestHarness({
|
|
98
|
+
headless: true,
|
|
99
|
+
debug: false
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await harness.initialize();
|
|
104
|
+
|
|
105
|
+
// Test component code directly
|
|
106
|
+
const result = await harness.testComponent(`
|
|
107
|
+
const Component = ({ message }) => {
|
|
108
|
+
return <div className="greeting">{message}</div>;
|
|
109
|
+
};
|
|
110
|
+
`, { message: 'Hello World' });
|
|
111
|
+
|
|
112
|
+
console.log('Success:', result.success);
|
|
113
|
+
console.log('HTML:', result.html);
|
|
114
|
+
console.log('Console logs:', result.console);
|
|
115
|
+
|
|
116
|
+
return result;
|
|
117
|
+
} finally {
|
|
118
|
+
await harness.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Integration into Jest/Mocha/Vitest
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { ReactTestHarness, AssertionHelpers } from '@memberjunction/react-test-harness';
|
|
127
|
+
import { describe, it, beforeAll, afterAll } from 'vitest';
|
|
128
|
+
|
|
129
|
+
describe('My React Components', () => {
|
|
130
|
+
let harness: ReactTestHarness;
|
|
131
|
+
|
|
132
|
+
beforeAll(async () => {
|
|
133
|
+
harness = new ReactTestHarness({ headless: true });
|
|
134
|
+
await harness.initialize();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
afterAll(async () => {
|
|
138
|
+
await harness.close();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should render greeting component', async () => {
|
|
142
|
+
const result = await harness.testComponent(`
|
|
143
|
+
const Component = ({ name }) => <h1>Hello, {name}!</h1>;
|
|
144
|
+
`, { name: 'World' });
|
|
145
|
+
|
|
146
|
+
AssertionHelpers.assertSuccess(result);
|
|
147
|
+
AssertionHelpers.assertContainsText(result.html, 'Hello, World!');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('should handle click events', async () => {
|
|
151
|
+
const result = await harness.testComponent(`
|
|
152
|
+
const Component = () => {
|
|
153
|
+
const [count, setCount] = React.useState(0);
|
|
154
|
+
return (
|
|
155
|
+
<button onClick={() => setCount(count + 1)}>
|
|
156
|
+
Count: {count}
|
|
157
|
+
</button>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
`);
|
|
161
|
+
|
|
162
|
+
AssertionHelpers.assertContainsText(result.html, 'Count: 0');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Advanced Class Usage
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import {
|
|
171
|
+
ReactTestHarness,
|
|
172
|
+
BrowserManager,
|
|
173
|
+
ComponentRunner,
|
|
174
|
+
AssertionHelpers
|
|
175
|
+
} from '@memberjunction/react-test-harness';
|
|
176
|
+
|
|
177
|
+
class ComponentTestSuite {
|
|
178
|
+
private harness: ReactTestHarness;
|
|
179
|
+
private browserManager: BrowserManager;
|
|
180
|
+
private componentRunner: ComponentRunner;
|
|
181
|
+
|
|
182
|
+
constructor() {
|
|
183
|
+
// You can also use the underlying classes directly
|
|
184
|
+
this.browserManager = new BrowserManager({
|
|
185
|
+
viewport: { width: 1920, height: 1080 },
|
|
186
|
+
headless: true
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
this.componentRunner = new ComponentRunner(this.browserManager);
|
|
190
|
+
|
|
191
|
+
// Or use the high-level harness
|
|
192
|
+
this.harness = new ReactTestHarness({
|
|
193
|
+
headless: true,
|
|
194
|
+
debug: true
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async initialize() {
|
|
199
|
+
await this.harness.initialize();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async testComponent(code: string, props?: any) {
|
|
203
|
+
const result = await this.harness.testComponent(code, props);
|
|
204
|
+
|
|
205
|
+
// Use static assertion methods
|
|
206
|
+
AssertionHelpers.assertSuccess(result);
|
|
207
|
+
|
|
208
|
+
// Or create a fluent matcher
|
|
209
|
+
const matcher = AssertionHelpers.createMatcher(result.html);
|
|
210
|
+
matcher.toContainText('Expected text');
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async cleanup() {
|
|
216
|
+
await this.harness.close();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Usage
|
|
221
|
+
const suite = new ComponentTestSuite();
|
|
222
|
+
await suite.initialize();
|
|
223
|
+
await suite.testComponent(`const Component = () => <div>Test</div>;`);
|
|
224
|
+
await suite.cleanup();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Test Component Files
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
const result = await harness.testComponentFromFile(
|
|
231
|
+
'./MyComponent.jsx',
|
|
232
|
+
{ title: 'Test', value: 123 },
|
|
233
|
+
{
|
|
234
|
+
waitForSelector: '.loaded',
|
|
235
|
+
timeout: 10000
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Running Multiple Tests
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
const harness = new ReactTestHarness({ debug: true });
|
|
244
|
+
|
|
245
|
+
await harness.runTest('Component renders correctly', async () => {
|
|
246
|
+
const result = await harness.testComponent(`
|
|
247
|
+
const Component = () => <h1>Test</h1>;
|
|
248
|
+
`);
|
|
249
|
+
|
|
250
|
+
const { AssertionHelpers } = harness;
|
|
251
|
+
AssertionHelpers.assertSuccess(result);
|
|
252
|
+
AssertionHelpers.assertContainsText(result.html, 'Test');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Run multiple tests
|
|
256
|
+
const summary = await harness.runTests([
|
|
257
|
+
{
|
|
258
|
+
name: 'Has correct elements',
|
|
259
|
+
fn: async () => {
|
|
260
|
+
const result = await harness.testComponent(`
|
|
261
|
+
const Component = () => (
|
|
262
|
+
<div>
|
|
263
|
+
<h1 id="title">Title</h1>
|
|
264
|
+
<button className="action">Click</button>
|
|
265
|
+
</div>
|
|
266
|
+
);
|
|
267
|
+
`);
|
|
268
|
+
|
|
269
|
+
const matcher = harness.createMatcher(result.html);
|
|
270
|
+
matcher.toHaveElement('#title');
|
|
271
|
+
matcher.toHaveElement('.action');
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
name: 'Handles props correctly',
|
|
276
|
+
fn: async () => {
|
|
277
|
+
const result = await harness.testComponent(`
|
|
278
|
+
const Component = ({ items }) => (
|
|
279
|
+
<ul>
|
|
280
|
+
{items.map((item, i) => <li key={i}>{item}</li>)}
|
|
281
|
+
</ul>
|
|
282
|
+
);
|
|
283
|
+
`, { items: ['A', 'B', 'C'] });
|
|
284
|
+
|
|
285
|
+
const { AssertionHelpers } = harness;
|
|
286
|
+
AssertionHelpers.assertElementCount(result.html, 'li', 3);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
console.log(`Tests passed: ${summary.passed}/${summary.total}`);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Complete API Reference
|
|
295
|
+
|
|
296
|
+
### ReactTestHarness Class
|
|
297
|
+
|
|
298
|
+
The main class for testing React components.
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
class ReactTestHarness {
|
|
302
|
+
constructor(options?: TestHarnessOptions);
|
|
303
|
+
|
|
304
|
+
// Lifecycle methods
|
|
305
|
+
async initialize(): Promise<void>;
|
|
306
|
+
async close(): Promise<void>;
|
|
307
|
+
|
|
308
|
+
// Component testing methods
|
|
309
|
+
async testComponent(
|
|
310
|
+
componentCode: string,
|
|
311
|
+
props?: Record<string, any>,
|
|
312
|
+
options?: Partial<ComponentExecutionOptions>
|
|
313
|
+
): Promise<ComponentExecutionResult>;
|
|
314
|
+
|
|
315
|
+
async testComponentFromFile(
|
|
316
|
+
filePath: string,
|
|
317
|
+
props?: Record<string, any>,
|
|
318
|
+
options?: Partial<ComponentExecutionOptions>
|
|
319
|
+
): Promise<ComponentExecutionResult>;
|
|
320
|
+
|
|
321
|
+
// Test running methods
|
|
322
|
+
async runTest(name: string, fn: () => Promise<void>): Promise<void>;
|
|
323
|
+
async runTests(tests: TestCase[]): Promise<TestSummary>;
|
|
324
|
+
|
|
325
|
+
// Utility methods
|
|
326
|
+
getAssertionHelpers(): typeof AssertionHelpers;
|
|
327
|
+
createMatcher(html: string): FluentMatcher;
|
|
328
|
+
async screenshot(path?: string): Promise<Buffer>;
|
|
329
|
+
async evaluateInPage<T>(fn: () => T): Promise<T>;
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### BrowserManager Class
|
|
334
|
+
|
|
335
|
+
Manages the Playwright browser instance.
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
class BrowserManager {
|
|
339
|
+
constructor(options?: BrowserContextOptions);
|
|
340
|
+
|
|
341
|
+
async initialize(): Promise<void>;
|
|
342
|
+
async close(): Promise<void>;
|
|
343
|
+
async getPage(): Promise<Page>;
|
|
344
|
+
async navigate(url: string): Promise<void>;
|
|
345
|
+
async evaluateInPage<T>(fn: () => T): Promise<T>;
|
|
346
|
+
async screenshot(path?: string): Promise<Buffer>;
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### ComponentRunner Class
|
|
351
|
+
|
|
352
|
+
Executes React components in the browser.
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
class ComponentRunner {
|
|
356
|
+
constructor(browserManager: BrowserManager);
|
|
357
|
+
|
|
358
|
+
async executeComponent(options: ComponentExecutionOptions): Promise<ComponentExecutionResult>;
|
|
359
|
+
async executeComponentFromFile(
|
|
360
|
+
filePath: string,
|
|
361
|
+
props?: Record<string, any>,
|
|
362
|
+
options?: Partial<ComponentExecutionOptions>
|
|
363
|
+
): Promise<ComponentExecutionResult>;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### AssertionHelpers Static Class
|
|
368
|
+
|
|
369
|
+
Provides assertion methods for testing.
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
class AssertionHelpers {
|
|
373
|
+
// Result assertions
|
|
374
|
+
static assertSuccess(result: ComponentExecutionResult): void;
|
|
375
|
+
static assertNoErrors(result: ComponentExecutionResult): void;
|
|
376
|
+
static assertNoConsoleErrors(console: Array<{ type: string; text: string }>): void;
|
|
377
|
+
|
|
378
|
+
// Content assertions
|
|
379
|
+
static assertContainsText(html: string, text: string): void;
|
|
380
|
+
static assertNotContainsText(html: string, text: string): void;
|
|
381
|
+
static assertHasElement(html: string, selector: string): void;
|
|
382
|
+
static assertElementCount(html: string, tagName: string, expectedCount: number): void;
|
|
383
|
+
|
|
384
|
+
// Utility methods
|
|
385
|
+
static containsText(html: string, text: string): boolean;
|
|
386
|
+
static hasElement(html: string, selector: string): boolean;
|
|
387
|
+
static countElements(html: string, tagName: string): number;
|
|
388
|
+
static hasAttribute(html: string, selector: string, attribute: string, value?: string): boolean;
|
|
389
|
+
|
|
390
|
+
// Fluent matcher creation
|
|
391
|
+
static createMatcher(html: string): FluentMatcher;
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### FluentMatcher Interface
|
|
396
|
+
|
|
397
|
+
Provides fluent assertions for better readability.
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
interface FluentMatcher {
|
|
401
|
+
toContainText(text: string): void;
|
|
402
|
+
toHaveElement(selector: string): void;
|
|
403
|
+
toHaveElementCount(tagName: string, count: number): void;
|
|
404
|
+
toHaveAttribute(selector: string, attribute: string, value?: string): void;
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## Usage Examples for TypeScript Projects
|
|
409
|
+
|
|
410
|
+
### Creating a Reusable Test Utility
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
// test-utils.ts
|
|
414
|
+
import { ReactTestHarness, AssertionHelpers } from '@memberjunction/react-test-harness';
|
|
415
|
+
import type { ComponentExecutionResult } from '@memberjunction/react-test-harness';
|
|
416
|
+
|
|
417
|
+
export class ReactComponentTester {
|
|
418
|
+
private harness: ReactTestHarness;
|
|
419
|
+
|
|
420
|
+
constructor() {
|
|
421
|
+
this.harness = new ReactTestHarness({
|
|
422
|
+
headless: process.env.HEADED !== 'true',
|
|
423
|
+
debug: process.env.DEBUG === 'true'
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async setup() {
|
|
428
|
+
await this.harness.initialize();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async teardown() {
|
|
432
|
+
await this.harness.close();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async testSkipComponent(
|
|
436
|
+
componentCode: string,
|
|
437
|
+
data: any,
|
|
438
|
+
userState?: any,
|
|
439
|
+
callbacks?: any,
|
|
440
|
+
utilities?: any,
|
|
441
|
+
styles?: any
|
|
442
|
+
): Promise<ComponentExecutionResult> {
|
|
443
|
+
// Test with Skip-style props structure
|
|
444
|
+
const props = { data, userState, callbacks, utilities, styles };
|
|
445
|
+
return this.harness.testComponent(componentCode, props);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
expectSuccess(result: ComponentExecutionResult) {
|
|
449
|
+
AssertionHelpers.assertSuccess(result);
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
expectText(result: ComponentExecutionResult, text: string) {
|
|
454
|
+
AssertionHelpers.assertContainsText(result.html, text);
|
|
455
|
+
return this;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
expectNoText(result: ComponentExecutionResult, text: string) {
|
|
459
|
+
AssertionHelpers.assertNotContainsText(result.html, text);
|
|
460
|
+
return this;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Usage in tests
|
|
465
|
+
const tester = new ReactComponentTester();
|
|
466
|
+
await tester.setup();
|
|
467
|
+
|
|
468
|
+
const result = await tester.testSkipComponent(
|
|
469
|
+
skipComponentCode,
|
|
470
|
+
{ title: 'Test', items: [] },
|
|
471
|
+
{ viewMode: 'grid' }
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
tester
|
|
475
|
+
.expectSuccess(result)
|
|
476
|
+
.expectText(result, 'Test')
|
|
477
|
+
.expectNoText(result, 'Error');
|
|
478
|
+
|
|
479
|
+
await tester.teardown();
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Testing MemberJunction Skip Components
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
import { ReactTestHarness } from '@memberjunction/react-test-harness';
|
|
486
|
+
import type {
|
|
487
|
+
SkipComponentRootSpec,
|
|
488
|
+
SkipComponentCallbacks,
|
|
489
|
+
SkipComponentStyles
|
|
490
|
+
} from '@memberjunction/skip-types';
|
|
491
|
+
|
|
492
|
+
async function testSkipComponent(spec: SkipComponentRootSpec) {
|
|
493
|
+
const harness = new ReactTestHarness({ headless: true });
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
await harness.initialize();
|
|
497
|
+
|
|
498
|
+
// Create Skip-compatible props
|
|
499
|
+
const props = {
|
|
500
|
+
data: spec.data || {},
|
|
501
|
+
userState: spec.userState || {},
|
|
502
|
+
callbacks: {
|
|
503
|
+
RefreshData: () => console.log('Refresh requested'),
|
|
504
|
+
UpdateUserState: (state: any) => console.log('State update:', state),
|
|
505
|
+
OpenEntityRecord: (entity: string, id: string) => console.log('Open:', entity, id),
|
|
506
|
+
NotifyEvent: (event: string, data: any) => console.log('Event:', event, data)
|
|
507
|
+
} as SkipComponentCallbacks,
|
|
508
|
+
utilities: spec.utilities || {},
|
|
509
|
+
styles: spec.styles || {} as SkipComponentStyles
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const result = await harness.testComponent(spec.componentCode, props);
|
|
513
|
+
|
|
514
|
+
if (!result.success) {
|
|
515
|
+
throw new Error(`Component failed: ${result.error}`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return result;
|
|
519
|
+
} finally {
|
|
520
|
+
await harness.close();
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### CI/CD Integration
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
// ci-test-runner.ts
|
|
529
|
+
import { ReactTestHarness } from '@memberjunction/react-test-harness';
|
|
530
|
+
import * as fs from 'fs';
|
|
531
|
+
import * as path from 'path';
|
|
532
|
+
|
|
533
|
+
export async function runComponentTests(testDir: string) {
|
|
534
|
+
const harness = new ReactTestHarness({
|
|
535
|
+
headless: true,
|
|
536
|
+
screenshotOnError: true,
|
|
537
|
+
screenshotPath: './test-failures/'
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
const results = {
|
|
541
|
+
total: 0,
|
|
542
|
+
passed: 0,
|
|
543
|
+
failed: 0,
|
|
544
|
+
failures: [] as Array<{ component: string; error: string }>
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
await harness.initialize();
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const files = fs.readdirSync(testDir)
|
|
551
|
+
.filter(f => f.endsWith('.jsx') || f.endsWith('.tsx'));
|
|
552
|
+
|
|
553
|
+
for (const file of files) {
|
|
554
|
+
results.total++;
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const result = await harness.testComponentFromFile(
|
|
558
|
+
path.join(testDir, file)
|
|
559
|
+
);
|
|
560
|
+
|
|
561
|
+
if (result.success) {
|
|
562
|
+
results.passed++;
|
|
563
|
+
} else {
|
|
564
|
+
results.failed++;
|
|
565
|
+
results.failures.push({
|
|
566
|
+
component: file,
|
|
567
|
+
error: result.error || 'Unknown error'
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
results.failed++;
|
|
572
|
+
results.failures.push({
|
|
573
|
+
component: file,
|
|
574
|
+
error: String(error)
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
} finally {
|
|
579
|
+
await harness.close();
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return results;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Run in CI
|
|
586
|
+
const results = await runComponentTests('./components');
|
|
587
|
+
console.log(`Tests: ${results.passed}/${results.total} passed`);
|
|
588
|
+
|
|
589
|
+
if (results.failed > 0) {
|
|
590
|
+
console.error('Failures:', results.failures);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
## Component Execution Options
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
interface ComponentExecutionOptions {
|
|
599
|
+
componentCode: string;
|
|
600
|
+
props?: Record<string, any>;
|
|
601
|
+
setupCode?: string; // Additional setup code
|
|
602
|
+
timeout?: number; // Default: 30000ms
|
|
603
|
+
waitForSelector?: string; // Wait for element before capture
|
|
604
|
+
waitForLoadState?: 'load' | 'domcontentloaded' | 'networkidle';
|
|
605
|
+
}
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
## Test Harness Options
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
interface TestHarnessOptions {
|
|
612
|
+
headless?: boolean; // Default: true
|
|
613
|
+
viewport?: { // Default: 1280x720
|
|
614
|
+
width: number;
|
|
615
|
+
height: number;
|
|
616
|
+
};
|
|
617
|
+
debug?: boolean; // Default: false
|
|
618
|
+
screenshotOnError?: boolean; // Default: true
|
|
619
|
+
screenshotPath?: string; // Default: './error-screenshot.png'
|
|
620
|
+
userAgent?: string;
|
|
621
|
+
deviceScaleFactor?: number;
|
|
622
|
+
locale?: string;
|
|
623
|
+
timezoneId?: string;
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## Writing Test Files
|
|
628
|
+
|
|
629
|
+
Test files should export a default async function:
|
|
630
|
+
|
|
631
|
+
```javascript
|
|
632
|
+
// my-component.test.js
|
|
633
|
+
export default async function runTests(harness) {
|
|
634
|
+
const { AssertionHelpers } = harness;
|
|
635
|
+
|
|
636
|
+
await harness.runTest('Component renders', async () => {
|
|
637
|
+
const result = await harness.testComponentFromFile('./MyComponent.jsx');
|
|
638
|
+
AssertionHelpers.assertSuccess(result);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
await harness.runTest('Component handles props', async () => {
|
|
642
|
+
const result = await harness.testComponentFromFile(
|
|
643
|
+
'./MyComponent.jsx',
|
|
644
|
+
{ value: 100 }
|
|
645
|
+
);
|
|
646
|
+
AssertionHelpers.assertContainsText(result.html, '100');
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
## Advanced Usage
|
|
652
|
+
|
|
653
|
+
### Custom Browser Context
|
|
654
|
+
|
|
655
|
+
```typescript
|
|
656
|
+
import { BrowserManager } from '@memberjunction/react-test-harness';
|
|
657
|
+
|
|
658
|
+
const browser = new BrowserManager({
|
|
659
|
+
viewport: { width: 1920, height: 1080 },
|
|
660
|
+
locale: 'en-US',
|
|
661
|
+
timezoneId: 'America/New_York'
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
await browser.initialize();
|
|
665
|
+
const page = await browser.getPage();
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
### Direct Page Evaluation
|
|
669
|
+
|
|
670
|
+
```typescript
|
|
671
|
+
const harness = new ReactTestHarness();
|
|
672
|
+
await harness.initialize();
|
|
673
|
+
|
|
674
|
+
// Evaluate JavaScript in the page context
|
|
675
|
+
const result = await harness.evaluateInPage(() => {
|
|
676
|
+
return document.querySelector('h1')?.textContent;
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Take screenshots
|
|
680
|
+
const screenshot = await harness.screenshot('./output.png');
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
## Best Practices
|
|
684
|
+
|
|
685
|
+
1. **Always close the harness** after tests to free resources:
|
|
686
|
+
```typescript
|
|
687
|
+
try {
|
|
688
|
+
// Run tests
|
|
689
|
+
} finally {
|
|
690
|
+
await harness.close();
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
2. **Use waitForSelector** for dynamic content:
|
|
695
|
+
```typescript
|
|
696
|
+
const result = await harness.testComponent(componentCode, props, {
|
|
697
|
+
waitForSelector: '.async-content',
|
|
698
|
+
timeout: 5000
|
|
699
|
+
});
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
3. **Enable debug mode** during development:
|
|
703
|
+
```typescript
|
|
704
|
+
const harness = new ReactTestHarness({ debug: true });
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
4. **Group related tests** for better organization:
|
|
708
|
+
```typescript
|
|
709
|
+
await harness.runTests([
|
|
710
|
+
{ name: 'Feature A - Test 1', fn: async () => { /* ... */ } },
|
|
711
|
+
{ name: 'Feature A - Test 2', fn: async () => { /* ... */ } },
|
|
712
|
+
{ name: 'Feature B - Test 1', fn: async () => { /* ... */ } },
|
|
713
|
+
]);
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
## Troubleshooting
|
|
717
|
+
|
|
718
|
+
### Component Not Rendering
|
|
719
|
+
- Ensure your component is named `Component` or modify the execution template
|
|
720
|
+
- Check for syntax errors in your component code
|
|
721
|
+
- Enable debug mode to see console output
|
|
722
|
+
|
|
723
|
+
### Timeout Errors
|
|
724
|
+
- Increase timeout value: `--timeout 60000`
|
|
725
|
+
- Use `waitForLoadState: 'networkidle'` for components that load external resources
|
|
726
|
+
- Check if the selector in `waitForSelector` actually exists
|
|
727
|
+
|
|
728
|
+
### Screenshot Issues
|
|
729
|
+
- Ensure the screenshot path directory exists
|
|
730
|
+
- Use absolute paths for consistent results
|
|
731
|
+
- Check file permissions
|
|
732
|
+
|
|
733
|
+
## License
|
|
734
|
+
|
|
735
|
+
ISC
|