@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 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