@memberjunction/react-test-harness 3.4.0 → 4.1.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.
Files changed (59) hide show
  1. package/README.md +130 -914
  2. package/dist/cli/index.js +38 -66
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/index.d.ts +6 -6
  5. package/dist/index.js +7 -17
  6. package/dist/index.js.map +1 -1
  7. package/dist/lib/assertion-helpers.js +1 -5
  8. package/dist/lib/assertion-helpers.js.map +1 -1
  9. package/dist/lib/browser-context.d.ts +0 -1
  10. package/dist/lib/browser-context.d.ts.map +1 -1
  11. package/dist/lib/browser-context.js +3 -7
  12. package/dist/lib/browser-context.js.map +1 -1
  13. package/dist/lib/component-linter.d.ts +1 -1
  14. package/dist/lib/component-linter.js +8759 -8789
  15. package/dist/lib/component-linter.js.map +1 -1
  16. package/dist/lib/component-runner.d.ts +2 -3
  17. package/dist/lib/component-runner.d.ts.map +1 -1
  18. package/dist/lib/component-runner.js +43 -70
  19. package/dist/lib/component-runner.js.map +1 -1
  20. package/dist/lib/constraint-validators/base-constraint-validator.d.ts +2 -2
  21. package/dist/lib/constraint-validators/base-constraint-validator.js +4 -8
  22. package/dist/lib/constraint-validators/base-constraint-validator.js.map +1 -1
  23. package/dist/lib/constraint-validators/index.d.ts +5 -5
  24. package/dist/lib/constraint-validators/index.js +5 -21
  25. package/dist/lib/constraint-validators/index.js.map +1 -1
  26. package/dist/lib/constraint-validators/required-when-validator.d.ts +2 -2
  27. package/dist/lib/constraint-validators/required-when-validator.js +6 -9
  28. package/dist/lib/constraint-validators/required-when-validator.js.map +1 -1
  29. package/dist/lib/constraint-validators/sql-where-clause-validator.d.ts +2 -2
  30. package/dist/lib/constraint-validators/sql-where-clause-validator.d.ts.map +1 -1
  31. package/dist/lib/constraint-validators/sql-where-clause-validator.js +28 -29
  32. package/dist/lib/constraint-validators/sql-where-clause-validator.js.map +1 -1
  33. package/dist/lib/constraint-validators/subset-of-entity-fields-validator.d.ts +2 -2
  34. package/dist/lib/constraint-validators/subset-of-entity-fields-validator.js +6 -9
  35. package/dist/lib/constraint-validators/subset-of-entity-fields-validator.js.map +1 -1
  36. package/dist/lib/constraint-validators/validation-context.d.ts +2 -2
  37. package/dist/lib/constraint-validators/validation-context.js +1 -2
  38. package/dist/lib/constraint-validators/validation-context.js.map +1 -1
  39. package/dist/lib/control-flow-analyzer.js +4 -31
  40. package/dist/lib/control-flow-analyzer.js.map +1 -1
  41. package/dist/lib/library-lint-cache.js +4 -8
  42. package/dist/lib/library-lint-cache.js.map +1 -1
  43. package/dist/lib/linter-test-tool.js +10 -37
  44. package/dist/lib/linter-test-tool.js.map +1 -1
  45. package/dist/lib/prop-value-extractor.js +2 -29
  46. package/dist/lib/prop-value-extractor.js.map +1 -1
  47. package/dist/lib/styles-type-analyzer.js +6 -33
  48. package/dist/lib/styles-type-analyzer.js.map +1 -1
  49. package/dist/lib/test-harness.d.ts +4 -5
  50. package/dist/lib/test-harness.d.ts.map +1 -1
  51. package/dist/lib/test-harness.js +11 -38
  52. package/dist/lib/test-harness.js.map +1 -1
  53. package/dist/lib/type-context.d.ts.map +1 -1
  54. package/dist/lib/type-context.js +7 -14
  55. package/dist/lib/type-context.js.map +1 -1
  56. package/dist/lib/type-inference-engine.d.ts +1 -1
  57. package/dist/lib/type-inference-engine.js +46 -77
  58. package/dist/lib/type-inference-engine.js.map +1 -1
  59. package/package.json +21 -21
package/README.md CHANGED
@@ -1,979 +1,195 @@
1
1
  # @memberjunction/react-test-harness
2
2
 
3
- A powerful test harness for React components using Playwright, designed specifically for MemberJunction's React runtime components with support for dynamic library configurations.
3
+ Automated test harness for MemberJunction React components using Playwright. Provides static analysis (linting), constraint validation, browser-based rendering tests, and a CLI for running test suites against dynamically compiled components.
4
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
- - Dynamically configure external libraries for testing different scenarios
10
- - Run assertions on rendered output
11
- - Execute tests via CLI or programmatically
12
- - Capture screenshots and console output
13
- - Run in headless or headed mode for debugging
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install @memberjunction/react-test-harness
19
- ```
20
-
21
- ## CLI Usage
5
+ ## Architecture
22
6
 
23
- ### Run a Single Component
7
+ ```mermaid
8
+ graph TD
9
+ subgraph "@memberjunction/react-test-harness"
10
+ A[TestHarness] --> B[ComponentLinter]
11
+ A --> C[ComponentRunner]
12
+ A --> D[BrowserContext]
24
13
 
25
- ```bash
26
- # Basic usage
27
- mj-react-test run MyComponent.jsx
28
-
29
- # With props
30
- mj-react-test run MyComponent.jsx --props '{"title":"Hello","count":42}'
31
-
32
- # With screenshot
33
- mj-react-test run MyComponent.jsx --screenshot ./output.png
14
+ B --> E[Type Inference Engine]
15
+ B --> F[Control Flow Analyzer]
16
+ B --> G[Prop Value Extractor]
17
+ B --> H[Styles Type Analyzer]
34
18
 
35
- # In headed mode (visible browser)
36
- mj-react-test run MyComponent.jsx --headed
19
+ subgraph "Constraint Validators"
20
+ I[BaseConstraintValidator]
21
+ I --> J[RequiredWhenValidator]
22
+ I --> K[SQLWhereClauseValidator]
23
+ I --> L[SubsetOfEntityFieldsValidator]
24
+ end
37
25
 
38
- # With debug output
39
- mj-react-test run MyComponent.jsx --debug
40
-
41
- # Wait for specific selector
42
- mj-react-test run MyComponent.jsx --selector "#loaded-content" --timeout 5000
43
- ```
26
+ C --> D
27
+ D --> M["Playwright Browser"]
44
28
 
45
- ### Run Test Files
46
-
47
- ```bash
48
- # Run a test file with multiple test cases
49
- mj-react-test test my-tests.js
50
-
51
- # With options
52
- mj-react-test test my-tests.js --headed --debug
53
- ```
29
+ N[CLI] --> A
30
+ end
54
31
 
55
- ### Create Example Files
32
+ subgraph "Dependencies"
33
+ O["@memberjunction/react-runtime<br/>(Component Compilation)"]
34
+ P["Babel Parser<br/>(AST Analysis)"]
35
+ end
56
36
 
57
- ```bash
58
- # Create example component and test files
59
- mj-react-test create-example
37
+ B --> P
38
+ C --> O
60
39
 
61
- # Create in specific directory
62
- mj-react-test create-example --dir ./my-tests
40
+ style A fill:#2d6a9f,stroke:#1a4971,color:#fff
41
+ style B fill:#2d8659,stroke:#1a5c3a,color:#fff
42
+ style C fill:#2d8659,stroke:#1a5c3a,color:#fff
43
+ style D fill:#7c5295,stroke:#563a6b,color:#fff
44
+ style E fill:#b8762f,stroke:#8a5722,color:#fff
45
+ style I fill:#7c5295,stroke:#563a6b,color:#fff
46
+ style N fill:#2d6a9f,stroke:#1a4971,color:#fff
63
47
  ```
64
48
 
65
- ## Dynamic Library Configuration (New)
66
-
67
- The test harness now supports dynamic library configuration, allowing you to test components with different sets of external libraries.
49
+ ## Overview
68
50
 
69
- ### Basic Library Configuration
51
+ This package provides comprehensive testing for MemberJunction's dynamically compiled React components. It combines static analysis (without executing) and runtime testing (via Playwright) to validate components before deployment.
70
52
 
71
- ```typescript
72
- import { ReactTestHarness } from '@memberjunction/react-test-harness';
73
- import type { LibraryConfiguration } from '@memberjunction/react-runtime';
74
-
75
- const customLibraryConfig: LibraryConfiguration = {
76
- libraries: [
77
- // Runtime libraries (always needed)
78
- {
79
- id: 'react',
80
- name: 'React',
81
- category: 'runtime',
82
- globalVariable: 'React',
83
- version: '18',
84
- cdnUrl: 'https://unpkg.com/react@18/umd/react.development.js',
85
- isEnabled: true,
86
- isCore: true,
87
- isRuntimeOnly: true
88
- },
89
- // Component libraries (available to components)
90
- {
91
- id: 'lodash',
92
- name: 'lodash',
93
- displayName: 'Lodash',
94
- category: 'utility',
95
- globalVariable: '_',
96
- version: '4.17.21',
97
- cdnUrl: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js',
98
- isEnabled: true,
99
- isCore: false
100
- },
101
- {
102
- id: 'chart-js',
103
- name: 'Chart',
104
- displayName: 'Chart.js',
105
- category: 'charting',
106
- globalVariable: 'Chart',
107
- version: '4.4.0',
108
- cdnUrl: 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js',
109
- isEnabled: true,
110
- isCore: false
111
- }
112
- ],
113
- metadata: {
114
- version: '1.0.0',
115
- lastUpdated: '2024-01-01'
116
- }
117
- };
118
-
119
- // Test with custom libraries
120
- const harness = new ReactTestHarness({ headless: true });
121
- await harness.initialize();
122
-
123
- const result = await harness.testComponent(
124
- `const Component = () => {
125
- if (!_) return <div>Lodash not available</div>;
126
- const sorted = _.sortBy([3, 1, 2]);
127
- return <div>{sorted.join(', ')}</div>;
128
- }`,
129
- {},
130
- { libraryConfiguration: customLibraryConfig }
131
- );
132
- ```
53
+ **Key capabilities:**
133
54
 
134
- ### Testing Organization-Specific Libraries
55
+ - **Component Linting**: Static analysis of component source using Babel AST parsing
56
+ - **Type Inference**: Infers component prop types from source code and usage patterns
57
+ - **Constraint Validation**: Validates data constraints like SQL WHERE clauses, required-when conditions, and entity field subsets
58
+ - **Control Flow Analysis**: Detects unreachable code, missing returns, and complex control flow patterns
59
+ - **Browser Rendering**: Launches components in a real browser via Playwright for visual/functional testing
60
+ - **Prop Value Extraction**: Extracts and validates prop values from JSX source
61
+ - **Library Lint Caching**: Caches lint results for external libraries to improve performance
62
+ - **CLI Tool**: `mj-react-test` command for running test suites
135
63
 
136
- ```typescript
137
- // Test with minimal libraries
138
- const minimalConfig: LibraryConfiguration = {
139
- libraries: [
140
- // Only runtime essentials
141
- { id: 'react', category: 'runtime', isEnabled: true, isRuntimeOnly: true, ... },
142
- { id: 'react-dom', category: 'runtime', isEnabled: true, isRuntimeOnly: true, ... },
143
- { id: 'babel', category: 'runtime', isEnabled: true, isRuntimeOnly: true, ... },
144
- // Just one utility library
145
- { id: 'lodash', category: 'utility', isEnabled: true, ... }
146
- ],
147
- metadata: { version: '1.0.0', lastUpdated: '2024-01-01' }
148
- };
149
-
150
- // Test with full library suite
151
- const fullConfig: LibraryConfiguration = {
152
- libraries: [
153
- // All runtime libraries
154
- // All UI libraries (antd, react-bootstrap)
155
- // All charting libraries (chart.js, d3)
156
- // All utilities (lodash, dayjs)
157
- ],
158
- metadata: { version: '1.0.0', lastUpdated: '2024-01-01' }
159
- };
160
-
161
- // Test component behavior with different configurations
162
- const componentCode = `
163
- const Component = () => {
164
- const hasChart = typeof Chart !== 'undefined';
165
- const hasAntd = typeof antd !== 'undefined';
166
-
167
- return (
168
- <div>
169
- <p>Chart.js: {hasChart ? 'Available' : 'Not Available'}</p>
170
- <p>Ant Design: {hasAntd ? 'Available' : 'Not Available'}</p>
171
- </div>
172
- );
173
- };
174
- `;
175
-
176
- const minimalResult = await harness.testComponent(componentCode, {}, {
177
- libraryConfiguration: minimalConfig
178
- });
64
+ ## Installation
179
65
 
180
- const fullResult = await harness.testComponent(componentCode, {}, {
181
- libraryConfiguration: fullConfig
182
- });
66
+ ```bash
67
+ npm install @memberjunction/react-test-harness
183
68
  ```
184
69
 
185
- ## Programmatic Usage via TypeScript/JavaScript
186
-
187
- 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.
70
+ ## Usage
188
71
 
189
- ### Importing Classes and Types
72
+ ### CLI
190
73
 
191
- ```typescript
192
- // Import main classes
193
- import {
194
- ReactTestHarness,
195
- BrowserManager,
196
- ComponentRunner,
197
- AssertionHelpers,
198
- FluentMatcher
199
- } from '@memberjunction/react-test-harness';
74
+ ```bash
75
+ # Run the test harness
76
+ npx mj-react-test
200
77
 
201
- // Import types for TypeScript
202
- import type {
203
- TestHarnessOptions,
204
- ComponentExecutionResult,
205
- ComponentExecutionOptions,
206
- BrowserContextOptions,
207
- TestCase,
208
- TestSummary
209
- } from '@memberjunction/react-test-harness';
78
+ # Or if installed globally
79
+ mj-react-test
210
80
  ```
211
81
 
212
- ### Basic Component Testing
82
+ ### Programmatic API
213
83
 
214
84
  ```typescript
215
- import { ReactTestHarness } from '@memberjunction/react-test-harness';
216
-
217
- async function testMyComponent() {
218
- const harness = new ReactTestHarness({
219
- headless: true,
220
- debug: false
221
- });
222
-
223
- try {
224
- await harness.initialize();
225
-
226
- // Test component code directly
227
- const result = await harness.testComponent(`
228
- const Component = ({ message }) => {
229
- return <div className="greeting">{message}</div>;
230
- };
231
- `, { message: 'Hello World' });
232
-
233
- console.log('Success:', result.success);
234
- console.log('HTML:', result.html);
235
- console.log('Console logs:', result.console);
236
-
237
- return result;
238
- } finally {
239
- await harness.close();
240
- }
241
- }
242
- ```
85
+ import { TestHarness } from '@memberjunction/react-test-harness';
243
86
 
244
- ### Integration into Jest/Mocha/Vitest
87
+ const harness = new TestHarness();
245
88
 
246
- ```typescript
247
- import { ReactTestHarness, AssertionHelpers } from '@memberjunction/react-test-harness';
248
- import { describe, it, beforeAll, afterAll } from 'vitest';
249
-
250
- describe('My React Components', () => {
251
- let harness: ReactTestHarness;
252
-
253
- beforeAll(async () => {
254
- harness = new ReactTestHarness({ headless: true });
255
- await harness.initialize();
256
- });
257
-
258
- afterAll(async () => {
259
- await harness.close();
260
- });
261
-
262
- it('should render greeting component', async () => {
263
- const result = await harness.testComponent(`
264
- const Component = ({ name }) => <h1>Hello, {name}!</h1>;
265
- `, { name: 'World' });
266
-
267
- AssertionHelpers.assertSuccess(result);
268
- AssertionHelpers.assertContainsText(result.html, 'Hello, World!');
269
- });
270
-
271
- it('should handle click events', async () => {
272
- const result = await harness.testComponent(`
273
- const Component = () => {
274
- const [count, setCount] = React.useState(0);
275
- return (
276
- <button onClick={() => setCount(count + 1)}>
277
- Count: {count}
278
- </button>
279
- );
280
- };
281
- `);
282
-
283
- AssertionHelpers.assertContainsText(result.html, 'Count: 0');
284
- });
89
+ // Run all tests for a component
90
+ const results = await harness.RunTests(componentSource, {
91
+ libraries: libraryConfigs,
92
+ entityMetadata: entityInfo
285
93
  });
286
94
  ```
287
95
 
288
- ### Advanced Class Usage
289
-
290
- ```typescript
291
- import {
292
- ReactTestHarness,
293
- BrowserManager,
294
- ComponentRunner,
295
- AssertionHelpers
296
- } from '@memberjunction/react-test-harness';
297
-
298
- class ComponentTestSuite {
299
- private harness: ReactTestHarness;
300
- private browserManager: BrowserManager;
301
- private componentRunner: ComponentRunner;
302
-
303
- constructor() {
304
- // You can also use the underlying classes directly
305
- this.browserManager = new BrowserManager({
306
- viewport: { width: 1920, height: 1080 },
307
- headless: true
308
- });
309
-
310
- this.componentRunner = new ComponentRunner(this.browserManager);
311
-
312
- // Or use the high-level harness
313
- this.harness = new ReactTestHarness({
314
- headless: true,
315
- debug: true
316
- });
317
- }
318
-
319
- async initialize() {
320
- await this.harness.initialize();
321
- }
322
-
323
- async testComponent(code: string, props?: any) {
324
- const result = await this.harness.testComponent(code, props);
325
-
326
- // Use static assertion methods
327
- AssertionHelpers.assertSuccess(result);
328
-
329
- // Or create a fluent matcher
330
- const matcher = AssertionHelpers.createMatcher(result.html);
331
- matcher.toContainText('Expected text');
332
-
333
- return result;
334
- }
335
-
336
- async cleanup() {
337
- await this.harness.close();
338
- }
339
- }
340
-
341
- // Usage
342
- const suite = new ComponentTestSuite();
343
- await suite.initialize();
344
- await suite.testComponent(`const Component = () => <div>Test</div>;`);
345
- await suite.cleanup();
346
- ```
347
-
348
- ### Test Component Files
96
+ ### Component Linting
349
97
 
350
98
  ```typescript
351
- const result = await harness.testComponentFromFile(
352
- './MyComponent.jsx',
353
- { title: 'Test', value: 123 },
354
- {
355
- waitForSelector: '.loaded',
356
- timeout: 10000
357
- }
358
- );
359
- ```
360
-
361
- ### Running Multiple Tests
99
+ import { ComponentLinter } from '@memberjunction/react-test-harness';
362
100
 
363
- ```typescript
364
- const harness = new ReactTestHarness({ debug: true });
365
-
366
- await harness.runTest('Component renders correctly', async () => {
367
- const result = await harness.testComponent(`
368
- const Component = () => <h1>Test</h1>;
369
- `);
370
-
371
- const { AssertionHelpers } = harness;
372
- AssertionHelpers.assertSuccess(result);
373
- AssertionHelpers.assertContainsText(result.html, 'Test');
101
+ const linter = new ComponentLinter();
102
+ const lintResults = linter.Lint(componentSource, {
103
+ checkPropTypes: true,
104
+ checkControlFlow: true,
105
+ validateConstraints: true
374
106
  });
375
107
 
376
- // Run multiple tests
377
- const summary = await harness.runTests([
378
- {
379
- name: 'Has correct elements',
380
- fn: async () => {
381
- const result = await harness.testComponent(`
382
- const Component = () => (
383
- <div>
384
- <h1 id="title">Title</h1>
385
- <button className="action">Click</button>
386
- </div>
387
- );
388
- `);
389
-
390
- const matcher = harness.createMatcher(result.html);
391
- matcher.toHaveElement('#title');
392
- matcher.toHaveElement('.action');
393
- }
394
- },
395
- {
396
- name: 'Handles props correctly',
397
- fn: async () => {
398
- const result = await harness.testComponent(`
399
- const Component = ({ items }) => (
400
- <ul>
401
- {items.map((item, i) => <li key={i}>{item}</li>)}
402
- </ul>
403
- );
404
- `, { items: ['A', 'B', 'C'] });
405
-
406
- const { AssertionHelpers } = harness;
407
- AssertionHelpers.assertElementCount(result.html, 'li', 3);
408
- }
409
- }
410
- ]);
411
-
412
- console.log(`Tests passed: ${summary.passed}/${summary.total}`);
413
- ```
414
-
415
- ## Complete API Reference
416
-
417
- ### ReactTestHarness Class
418
-
419
- The main class for testing React components.
420
-
421
- ```typescript
422
- class ReactTestHarness {
423
- constructor(options?: TestHarnessOptions);
424
-
425
- // Lifecycle methods
426
- async initialize(): Promise<void>;
427
- async close(): Promise<void>;
428
-
429
- // Component testing methods
430
- async testComponent(
431
- componentCode: string,
432
- props?: Record<string, any>,
433
- options?: Partial<ComponentExecutionOptions>
434
- ): Promise<ComponentExecutionResult>;
435
-
436
- async testComponentFromFile(
437
- filePath: string,
438
- props?: Record<string, any>,
439
- options?: Partial<ComponentExecutionOptions>
440
- ): Promise<ComponentExecutionResult>;
441
-
442
- // Test running methods
443
- async runTest(name: string, fn: () => Promise<void>): Promise<void>;
444
- async runTests(tests: TestCase[]): Promise<TestSummary>;
445
-
446
- // Utility methods
447
- getAssertionHelpers(): typeof AssertionHelpers;
448
- createMatcher(html: string): FluentMatcher;
449
- async screenshot(path?: string): Promise<Buffer>;
450
- async evaluateInPage<T>(fn: () => T): Promise<T>;
451
- }
452
- ```
453
-
454
- ### BrowserManager Class
455
-
456
- Manages the Playwright browser instance.
457
-
458
- ```typescript
459
- class BrowserManager {
460
- constructor(options?: BrowserContextOptions);
461
-
462
- async initialize(): Promise<void>;
463
- async close(): Promise<void>;
464
- async getPage(): Promise<Page>;
465
- async navigate(url: string): Promise<void>;
466
- async evaluateInPage<T>(fn: () => T): Promise<T>;
467
- async screenshot(path?: string): Promise<Buffer>;
468
- }
469
- ```
470
-
471
- ### ComponentRunner Class
472
-
473
- Executes React components in the browser.
474
-
475
- ```typescript
476
- class ComponentRunner {
477
- constructor(browserManager: BrowserManager);
478
-
479
- async executeComponent(options: ComponentExecutionOptions): Promise<ComponentExecutionResult>;
480
- async executeComponentFromFile(
481
- filePath: string,
482
- props?: Record<string, any>,
483
- options?: Partial<ComponentExecutionOptions>
484
- ): Promise<ComponentExecutionResult>;
108
+ for (const issue of lintResults.issues) {
109
+ console.log(`${issue.severity}: ${issue.message} (line ${issue.line})`);
485
110
  }
486
111
  ```
487
112
 
488
- ### AssertionHelpers Static Class
489
-
490
- Provides assertion methods for testing.
113
+ ### Browser-Based Testing
491
114
 
492
115
  ```typescript
493
- class AssertionHelpers {
494
- // Result assertions
495
- static assertSuccess(result: ComponentExecutionResult): void;
496
- static assertNoErrors(result: ComponentExecutionResult): void;
497
- static assertNoConsoleErrors(console: Array<{ type: string; text: string }>): void;
498
-
499
- // Content assertions
500
- static assertContainsText(html: string, text: string): void;
501
- static assertNotContainsText(html: string, text: string): void;
502
- static assertHasElement(html: string, selector: string): void;
503
- static assertElementCount(html: string, tagName: string, expectedCount: number): void;
504
-
505
- // Utility methods
506
- static containsText(html: string, text: string): boolean;
507
- static hasElement(html: string, selector: string): boolean;
508
- static countElements(html: string, tagName: string): number;
509
- static hasAttribute(html: string, selector: string, attribute: string, value?: string): boolean;
510
-
511
- // Fluent matcher creation
512
- static createMatcher(html: string): FluentMatcher;
513
- }
514
- ```
515
-
516
- ### FluentMatcher Interface
116
+ import { ComponentRunner, BrowserContext } from '@memberjunction/react-test-harness';
517
117
 
518
- Provides fluent assertions for better readability.
118
+ const browser = await BrowserContext.Create();
119
+ const runner = new ComponentRunner(browser);
519
120
 
520
- ```typescript
521
- interface FluentMatcher {
522
- toContainText(text: string): void;
523
- toHaveElement(selector: string): void;
524
- toHaveElementCount(tagName: string, count: number): void;
525
- toHaveAttribute(selector: string, attribute: string, value?: string): void;
526
- }
527
- ```
528
-
529
- ## Parallel Testing
530
-
531
- ### Important: Test Harness Instance Limitations
532
-
533
- The ReactTestHarness uses a single browser page instance and is **NOT safe for parallel test execution** on the same instance. This is due to Playwright's internal limitations with `exposeFunction` and potential race conditions when multiple tests try to modify the same page context simultaneously.
534
-
535
- ### Sequential Testing (Single Instance)
536
-
537
- For sequential test execution, you can safely reuse a single harness instance:
538
-
539
- ```typescript
540
- const harness = new ReactTestHarness({ headless: true });
541
- await harness.initialize();
542
-
543
- // ✅ CORRECT - Sequential testing on same instance
544
- for (const test of tests) {
545
- const result = await harness.testComponent(test.code, test.props);
546
- // Each test runs one after another, no conflicts
547
- }
548
-
549
- await harness.close();
550
- ```
551
-
552
- ### Parallel Testing (Multiple Instances)
553
-
554
- For parallel test execution, you **MUST** create separate ReactTestHarness instances:
555
-
556
- ```typescript
557
- // ✅ CORRECT - Parallel testing with separate instances
558
- const results = await Promise.all(tests.map(async (test) => {
559
- const harness = new ReactTestHarness({ headless: true });
560
- await harness.initialize();
561
-
562
- try {
563
- return await harness.testComponent(test.code, test.props);
564
- } finally {
565
- await harness.close(); // Clean up each instance
566
- }
567
- }));
568
- ```
569
-
570
- ### Common Mistake to Avoid
571
-
572
- ```typescript
573
- // ❌ WRONG - DO NOT DO THIS
574
- const harness = new ReactTestHarness({ headless: true });
575
- await harness.initialize();
576
-
577
- // This will cause conflicts and errors!
578
- const results = await Promise.all(
579
- tests.map(test => harness.testComponent(test.code, test.props))
580
- );
581
- ```
582
-
583
- This approach will fail with errors like:
584
- - "Function '__mjGetEntityObject' has been already registered"
585
- - "Cannot read properties of undefined (reading 'addBinding')"
586
-
587
- ### Performance Considerations
588
-
589
- While creating multiple harness instances has some overhead (each launches its own browser context), the benefits of parallel execution typically outweigh this cost:
590
-
591
- - **Sequential (1 instance)**: Lower memory usage, but tests run one by one
592
- - **Parallel (N instances)**: Higher memory usage, but tests complete much faster
593
-
594
- ### Example: Test Runner with Configurable Parallelism
595
-
596
- ```typescript
597
- class TestRunner {
598
- async runTests(tests: TestCase[], parallel = false) {
599
- if (parallel) {
600
- // Create new instance for each test
601
- return Promise.all(tests.map(async (test) => {
602
- const harness = new ReactTestHarness({ headless: true });
603
- await harness.initialize();
604
- try {
605
- return await harness.testComponent(test.code, test.props);
606
- } finally {
607
- await harness.close();
608
- }
609
- }));
610
- } else {
611
- // Reuse single instance for all tests
612
- const harness = new ReactTestHarness({ headless: true });
613
- await harness.initialize();
614
-
615
- const results = [];
616
- for (const test of tests) {
617
- results.push(await harness.testComponent(test.code, test.props));
618
- }
619
-
620
- await harness.close();
621
- return results;
622
- }
623
- }
624
- }
625
- ```
626
-
627
- ## Usage Examples for TypeScript Projects
628
-
629
- ### Creating a Reusable Test Utility
630
-
631
- ```typescript
632
- // test-utils.ts
633
- import { ReactTestHarness, AssertionHelpers } from '@memberjunction/react-test-harness';
634
- import type { ComponentExecutionResult } from '@memberjunction/react-test-harness';
635
-
636
- export class ReactComponentTester {
637
- private harness: ReactTestHarness;
638
-
639
- constructor() {
640
- this.harness = new ReactTestHarness({
641
- headless: process.env.HEADED !== 'true',
642
- debug: process.env.DEBUG === 'true'
643
- });
644
- }
645
-
646
- async setup() {
647
- await this.harness.initialize();
648
- }
649
-
650
- async teardown() {
651
- await this.harness.close();
652
- }
653
-
654
- async testMJComponent(
655
- componentCode: string,
656
- data: any,
657
- userState?: any,
658
- callbacks?: any,
659
- utilities?: any,
660
- styles?: any
661
- ): Promise<ComponentExecutionResult> {
662
- // Test with MJ-style props structure
663
- const props = { data, userState, callbacks, utilities, styles };
664
- return this.harness.testComponent(componentCode, props);
665
- }
666
-
667
- expectSuccess(result: ComponentExecutionResult) {
668
- AssertionHelpers.assertSuccess(result);
669
- return this;
670
- }
671
-
672
- expectText(result: ComponentExecutionResult, text: string) {
673
- AssertionHelpers.assertContainsText(result.html, text);
674
- return this;
675
- }
676
-
677
- expectNoText(result: ComponentExecutionResult, text: string) {
678
- AssertionHelpers.assertNotContainsText(result.html, text);
679
- return this;
680
- }
681
- }
682
-
683
- // Usage in tests
684
- const tester = new ReactComponentTester();
685
- await tester.setup();
686
-
687
- const result = await tester.testMJComponent(
688
- componentCode,
689
- { title: 'Test', items: [] },
690
- { viewMode: 'grid' }
691
- );
692
-
693
- tester
694
- .expectSuccess(result)
695
- .expectText(result, 'Test')
696
- .expectNoText(result, 'Error');
697
-
698
- await tester.teardown();
699
- ```
700
-
701
- ### Testing with Different Library Configurations
702
-
703
- ```typescript
704
- import { ReactTestHarness } from '@memberjunction/react-test-harness';
705
- import type { LibraryConfiguration } from '@memberjunction/react-runtime';
706
-
707
- class LibraryCompatibilityTester {
708
- private harness: ReactTestHarness;
709
-
710
- constructor() {
711
- this.harness = new ReactTestHarness({ headless: true });
712
- }
713
-
714
- async testWithLibraries(
715
- componentCode: string,
716
- enabledLibraries: string[]
717
- ) {
718
- const config: LibraryConfiguration = {
719
- libraries: [
720
- // Always include runtime
721
- { id: 'react', category: 'runtime', isEnabled: true, isRuntimeOnly: true, ... },
722
- { id: 'react-dom', category: 'runtime', isEnabled: true, isRuntimeOnly: true, ... },
723
- { id: 'babel', category: 'runtime', isEnabled: true, isRuntimeOnly: true, ... },
724
- // Conditionally enable other libraries
725
- { id: 'lodash', category: 'utility', isEnabled: enabledLibraries.includes('lodash'), ... },
726
- { id: 'chart-js', category: 'charting', isEnabled: enabledLibraries.includes('chart-js'), ... },
727
- { id: 'antd', category: 'ui', isEnabled: enabledLibraries.includes('antd'), ... },
728
- ],
729
- metadata: { version: '1.0.0', lastUpdated: '2024-01-01' }
730
- };
731
-
732
- return this.harness.testComponent(componentCode, {}, {
733
- libraryConfiguration: config
734
- });
735
- }
736
- }
737
- ```
738
-
739
- ### CI/CD Integration
740
-
741
- ```typescript
742
- // ci-test-runner.ts
743
- import { ReactTestHarness } from '@memberjunction/react-test-harness';
744
- import * as fs from 'fs';
745
- import * as path from 'path';
746
-
747
- export async function runComponentTests(testDir: string) {
748
- const harness = new ReactTestHarness({
749
- headless: true,
750
- screenshotOnError: true,
751
- screenshotPath: './test-failures/'
752
- });
753
-
754
- const results = {
755
- total: 0,
756
- passed: 0,
757
- failed: 0,
758
- failures: [] as Array<{ component: string; error: string }>
759
- };
760
-
761
- await harness.initialize();
762
-
763
- try {
764
- const files = fs.readdirSync(testDir)
765
- .filter(f => f.endsWith('.jsx') || f.endsWith('.tsx'));
766
-
767
- for (const file of files) {
768
- results.total++;
769
-
770
- try {
771
- const result = await harness.testComponentFromFile(
772
- path.join(testDir, file)
773
- );
774
-
775
- if (result.success) {
776
- results.passed++;
777
- } else {
778
- results.failed++;
779
- results.failures.push({
780
- component: file,
781
- error: result.error || 'Unknown error'
782
- });
783
- }
784
- } catch (error) {
785
- results.failed++;
786
- results.failures.push({
787
- component: file,
788
- error: String(error)
789
- });
790
- }
791
- }
792
- } finally {
793
- await harness.close();
794
- }
795
-
796
- return results;
797
- }
121
+ const result = await runner.Render(compiledComponent, {
122
+ props: { data: testData },
123
+ timeout: 5000
124
+ });
798
125
 
799
- // Run in CI
800
- const results = await runComponentTests('./components');
801
- console.log(`Tests: ${results.passed}/${results.total} passed`);
126
+ console.log('Rendered successfully:', result.success);
127
+ console.log('Console errors:', result.consoleErrors);
802
128
 
803
- if (results.failed > 0) {
804
- console.error('Failures:', results.failures);
805
- process.exit(1);
806
- }
129
+ await browser.Close();
807
130
  ```
808
131
 
809
- ## Component Execution Options
132
+ ### Constraint Validation
810
133
 
811
134
  ```typescript
812
- interface ComponentExecutionOptions {
813
- componentSpec: ComponentSpec;
814
- props?: Record<string, any>;
815
- setupCode?: string; // Additional setup code
816
- timeout?: number; // Default: 30000ms
817
- waitForSelector?: string; // Wait for element before capture
818
- waitForLoadState?: 'load' | 'domcontentloaded' | 'networkidle';
819
- contextUser: UserInfo;
820
- libraryConfiguration?: LibraryConfiguration; // New: Custom library configuration
821
- }
822
- ```
823
-
824
- ## Test Harness Options
825
-
826
- ```typescript
827
- interface TestHarnessOptions {
828
- headless?: boolean; // Default: true
829
- viewport?: { // Default: 1280x720
830
- width: number;
831
- height: number;
832
- };
833
- debug?: boolean; // Default: false
834
- screenshotOnError?: boolean; // Default: true
835
- screenshotPath?: string; // Default: './error-screenshot.png'
836
- userAgent?: string;
837
- deviceScaleFactor?: number;
838
- locale?: string;
839
- timezoneId?: string;
840
- }
841
- ```
842
-
843
- ## Writing Test Files
844
-
845
- Test files should export a default async function:
135
+ import {
136
+ SQLWhereClauseValidator,
137
+ RequiredWhenValidator,
138
+ SubsetOfEntityFieldsValidator
139
+ } from '@memberjunction/react-test-harness';
846
140
 
847
- ```javascript
848
- // my-component.test.js
849
- export default async function runTests(harness) {
850
- const { AssertionHelpers } = harness;
141
+ // Validate a SQL WHERE clause
142
+ const sqlValidator = new SQLWhereClauseValidator();
143
+ const sqlResult = sqlValidator.Validate("Status = 'Active' AND Age > 18", context);
851
144
 
852
- await harness.runTest('Component renders', async () => {
853
- const result = await harness.testComponentFromFile('./MyComponent.jsx');
854
- AssertionHelpers.assertSuccess(result);
855
- });
145
+ // Validate required-when conditions
146
+ const reqValidator = new RequiredWhenValidator();
147
+ const reqResult = reqValidator.Validate("IsAdmin = true", context);
856
148
 
857
- await harness.runTest('Component handles props', async () => {
858
- const result = await harness.testComponentFromFile(
859
- './MyComponent.jsx',
860
- { value: 100 }
861
- );
862
- AssertionHelpers.assertContainsText(result.html, '100');
863
- });
864
- }
149
+ // Validate field subset
150
+ const subsetValidator = new SubsetOfEntityFieldsValidator();
151
+ const subsetResult = subsetValidator.Validate(["Name", "Email", "Status"], context);
865
152
  ```
866
153
 
867
- ## Advanced Usage
154
+ ## Components
868
155
 
869
- ### Custom Browser Context
156
+ | Component | Description |
157
+ |-----------|-------------|
158
+ | `TestHarness` | Main orchestrator for running all test types |
159
+ | `ComponentLinter` | Static analysis of component source code |
160
+ | `ComponentRunner` | Browser-based component rendering tests |
161
+ | `BrowserContext` | Manages Playwright browser lifecycle |
162
+ | `TypeInferenceEngine` | Infers prop types from source code |
163
+ | `ControlFlowAnalyzer` | Detects control flow issues |
164
+ | `PropValueExtractor` | Extracts and validates prop values |
165
+ | `StylesTypeAnalyzer` | Analyzes component style patterns |
166
+ | `LibraryLintCache` | Caches lint results for external libraries |
167
+ | `LinterTestTool` | Testing utilities for linter development |
870
168
 
871
- ```typescript
872
- import { BrowserManager } from '@memberjunction/react-test-harness';
169
+ ## Testing
873
170
 
874
- const browser = new BrowserManager({
875
- viewport: { width: 1920, height: 1080 },
876
- locale: 'en-US',
877
- timezoneId: 'America/New_York'
878
- });
879
-
880
- await browser.initialize();
881
- const page = await browser.getPage();
171
+ ```bash
172
+ npm test
173
+ npm run test:watch
882
174
  ```
883
175
 
884
- ### Direct Page Evaluation
885
-
886
- ```typescript
887
- const harness = new ReactTestHarness();
888
- await harness.initialize();
889
-
890
- // Evaluate JavaScript in the page context
891
- const result = await harness.evaluateInPage(() => {
892
- return document.querySelector('h1')?.textContent;
893
- });
176
+ Uses Vitest for unit testing.
894
177
 
895
- // Take screenshots
896
- const screenshot = await harness.screenshot('./output.png');
897
- ```
178
+ ## Dependencies
898
179
 
899
- ## Limitations
900
-
901
- Due to the architecture of the test harness (Node.js controlling a browser via Playwright), there are some important limitations to be aware of. See [docs/limitations.md](./docs/limitations.md) for details on:
902
-
903
- - Serialization requirements between Node.js and browser
904
- - BaseEntity method access limitations
905
- - Differences between test and production environments
906
-
907
- ## Best Practices
908
-
909
- 1. **Always close the harness** after tests to free resources:
910
- ```typescript
911
- try {
912
- // Run tests
913
- } finally {
914
- await harness.close();
915
- }
916
- ```
917
-
918
- 2. **Use waitForSelector** for dynamic content:
919
- ```typescript
920
- const result = await harness.testComponent(componentCode, props, {
921
- waitForSelector: '.async-content',
922
- timeout: 5000
923
- });
924
- ```
925
-
926
- 3. **Enable debug mode** during development:
927
- ```typescript
928
- const harness = new ReactTestHarness({ debug: true });
929
- ```
930
-
931
- 4. **Group related tests** for better organization:
932
- ```typescript
933
- await harness.runTests([
934
- { name: 'Feature A - Test 1', fn: async () => { /* ... */ } },
935
- { name: 'Feature A - Test 2', fn: async () => { /* ... */ } },
936
- { name: 'Feature B - Test 1', fn: async () => { /* ... */ } },
937
- ]);
938
- ```
939
-
940
- 5. **Test with different library configurations** to ensure compatibility:
941
- ```typescript
942
- // Test with minimal libraries
943
- const minimalResult = await harness.testComponent(code, props, {
944
- libraryConfiguration: minimalLibraryConfig
945
- });
946
-
947
- // Test with full libraries
948
- const fullResult = await harness.testComponent(code, props, {
949
- libraryConfiguration: fullLibraryConfig
950
- });
951
- ```
952
-
953
- ## Troubleshooting
954
-
955
- ### Component Not Rendering
956
- - Ensure your component is named `Component` or modify the execution template
957
- - Check for syntax errors in your component code
958
- - Enable debug mode to see console output
959
- - Verify required libraries are included in libraryConfiguration
960
-
961
- ### Timeout Errors
962
- - Increase timeout value: `--timeout 60000`
963
- - Use `waitForLoadState: 'networkidle'` for components that load external resources
964
- - Check if the selector in `waitForSelector` actually exists
965
-
966
- ### Screenshot Issues
967
- - Ensure the screenshot path directory exists
968
- - Use absolute paths for consistent results
969
- - Check file permissions
970
-
971
- ### Library Loading Issues
972
- - Verify CDN URLs are accessible
973
- - Check that globalVariable names match what components expect
974
- - Ensure runtime libraries (React, ReactDOM, Babel) are always included
975
- - Use isRuntimeOnly flag for libraries not exposed to components
180
+ | Package | Purpose |
181
+ |---------|---------|
182
+ | `@memberjunction/react-runtime` | Component compilation and registry |
183
+ | `@memberjunction/interactive-component-types` | Component type definitions |
184
+ | `@memberjunction/core` | Core MJ functionality |
185
+ | `@memberjunction/core-entities` | Entity types |
186
+ | `@babel/parser` | AST parsing for static analysis |
187
+ | `@babel/traverse` | AST traversal |
188
+ | `@playwright/test` | Browser automation |
189
+ | `commander` | CLI framework |
190
+ | `chalk` | Terminal styling |
191
+ | `node-sql-parser` | SQL WHERE clause validation |
976
192
 
977
193
  ## License
978
194
 
979
- ISC
195
+ ISC