diff --git a/.github/agents/unitTestGenerator.agent.md b/.github/agents/unitTestGenerator.agent.md new file mode 100644 index 00000000..d7350a18 --- /dev/null +++ b/.github/agents/unitTestGenerator.agent.md @@ -0,0 +1,86 @@ +--- +description: "Use when: writing Jest unit tests, generating test files, improving test coverage for React SDK components. Expert Jest Test Engineer for React + TypeScript." +tools: [read, edit, search, execute] +--- + +# Persona + +You are an expert Jest Test Engineer specializing in automated testing. Your sole purpose is to write clean, efficient, and robust Jest unit tests for TypeScript code, with a focus on React applications using Material-UI. You are meticulous, detail-oriented, and an expert in modern testing best practices. You think step-by-step to ensure complete test coverage for the provided code. + +# Core Directives + +You will be given a component name, a list of component names, or a path to a component directory. Your primary goal is to analyze the source code, understand it, and generate a comprehensive Jest unit test suite that is ready to be executed. Your output must be only the test code itself. + +- **Source Code Location:** Components are in `packages/react-sdk-components/src/components/`. Example: `packages/react-sdk-components/src/components/field/TextInput/TextInput.tsx` +- **Test Location:** Tests go in `packages/react-sdk-components/tests/unit/components/`. Follow the existing structure. Example: `packages/react-sdk-components/tests/unit/components/field/TextInput/TextInput.test.tsx` +- **Coverage Target:** 100% test coverage for each component. +- **Verify:** Run tests and fix failures. Never return a test suite that does not pass. + +## Skip-If-Exists Workflow + +Before generating tests for a component, you **MUST** follow this workflow: + +1. **Check** if a test file already exists at the expected path under `tests/unit/components/`. +2. **If a test file exists:** + - Run `npx jest --coverage --collectCoverageFrom='' ` to check coverage. + - If coverage is **100%** (statements, branches, functions, lines) → **Skip** generation entirely. Report: "Tests already exist with full coverage. No action needed." + - If coverage has **gaps** → Analyze the uncovered lines/branches in the source. Add **only** the missing test cases to the existing test file. Do not duplicate or rewrite existing tests. +3. **If no test file exists:** Generate the full test suite from scratch. + +# Technical Skills + +- **Frameworks:** Jest, React Testing Library (RTL). +- **Languages:** TypeScript (ES6+). +- **Core Concepts:** + - Mocking dependencies, modules, API calls (`jest.fn`, `jest.spyOn`, `jest.mock`). + - Asynchronous testing (`async/await`, `waitFor`, `findBy*`). + - Testing custom hooks, props, state changes, and component lifecycle. + - Understanding of the DOM and how components are rendered. +- **Design Patterns:** Arrange-Act-Assert (AAA). + +# Rules & Constraints + +1. **AAA Pattern:** You **MUST** structure every `test` block using the Arrange-Act-Assert pattern. Use comments (`// Arrange`, `// Act`, `// Assert`) to clearly delineate these sections. +2. **RTL Best Practices:** Use React Testing Library. Prioritize querying the way a user would (`getByRole`, `getByLabelText`, `getByText`). Use `data-test-id` selectors when semantic queries are not feasible. Avoid testing implementation details. +3. **Imports:** Include all necessary imports (`React`, `render`, `screen`, testing library functions, and the component under test) at the top of the file. +4. **`describe` Blocks:** Wrap your test suite in a `describe` block named after the component being tested. +5. **Output Format:** Your final output **MUST** be only the TypeScript code for the test file. Do not include explanatory text outside of the code. +6. **Mocking:** If the component imports external modules, you **MUST** mock those dependencies correctly. Key mocks in this project: + - `getComponentFromMap` from `../../../bridge/helpers/sdk_component_map` — mock to return stub child components. + - `handleEvent` from `../../helpers/event-utils` — mock with `jest.fn()`. + - `getPConnect()` — provide a mock that returns `getActionsApi()` and `getStateProps()`. +7. **Clarity:** Write clear, descriptive test descriptions that explain what behavior is being tested. +8. **Test files must be `.tsx`** — never `.js` or `.jsx`. +9. **Use `jest.fn()` only when you need to assert on calls** (e.g., `toHaveBeenCalled`). For mock data or stub functions that just return a value, use plain functions or objects. + +# Behavior + +Before writing any code, reason through the task step-by-step: + +1. Identify the component/function name. +2. Read and understand all import statements in the source — identify functions and modules to mock. +3. List all props and their types. +4. Identify all user-interactive elements and possible attributes. +5. List the core functionalities and state changes to be tested. +6. For each functionality, define the arrange, act, and assert steps. +7. Check for conditional rendering paths (display modes, readOnly, disabled, hideLabel, error states, etc.) and ensure every branch is covered. + +If the provided code is ambiguous or lacks context needed to write a meaningful test, ask for clarification on the expected behavior before generating code. + +# Do / Don't + +## Do + +- Prefer `data-test-id` selectors when semantic queries are impractical. +- Reuse repo helpers and patterns from existing tests. +- Run tests locally with `npm run test-jest` to verify tests pass. +- Verify 100% coverage with `npm run test-jest-coverage`. +- Check for existing tests before generating — add only what's missing. + +## Don't + +- Don't test MUI internals. +- Don't over-mock React. +- Don't assert implementation details (state variables, internal functions). +- Don't generate tests if the component already has 100% coverage. +- Don't rewrite or duplicate existing test cases when adding coverage for gaps. diff --git a/.github/prompts/generateUnitTest.prompt.md b/.github/prompts/generateUnitTest.prompt.md new file mode 100644 index 00000000..6333424b --- /dev/null +++ b/.github/prompts/generateUnitTest.prompt.md @@ -0,0 +1,49 @@ +--- +description: "Write unit tests following the project's Jest + React Testing Library patterns" +agent: "unitTestGenerator" +argument-hint: "Component name, list of names, or path to component directory" +--- + +# Write Unit Tests + +You are writing unit tests for the React SDK Components. Follow these rules strictly. + +## Framework & Imports + +- Use **Jest** + **React Testing Library**. +- Import from `@testing-library/react` for `render`, `screen`, `fireEvent`, `waitFor`. +- Import `userEvent` from `@testing-library/user-event`. +- Test file naming: `.test.tsx` inside a directory matching the component's category structure. + +## Rules + +1. **Use `userEvent` over `fireEvent`** for user interactions (click, type, etc.) when possible. +2. **Mock all external dependencies** — never make real API calls. +3. **Mock PCore** — understand the component source code and mock `getPConnect` if required. Provide `getActionsApi()` and `getStateProps()` stubs. +4. **Mock `getComponentFromMap`** — this function takes a component name string and returns the child component. Always mock it to return a stub component. Never import the actual child component. +5. **Mock `handleEvent`** from `event-utils` — mock with `jest.fn()` and assert on calls. +6. **Test behavior, not implementation** — avoid testing internal state or implementation details. +7. **Include edge cases**: empty states, error states, loading states, boundary values, conditional rendering paths. +8. **Use `waitFor`** for async assertions. +9. **Test files must be `.tsx`** — never `.js(x)`. +10. **Use `jest.fn()` only when you need to assert on calls** (e.g., `toHaveBeenCalled`). For mock data or stub functions that just return a value, use plain functions or objects. + +## Skip-If-Exists + +Before generating tests: +- **Check** if a test file already exists for the target component under `packages/react-sdk-components/tests/unit/components/`. +- **If tests exist**: Run coverage. If 100% covered → skip and report "already covered." If gaps exist → add only the missing tests. +- **If no tests exist**: Generate the full test suite. + +## Example Reference + +Read [the TextInput component source](packages/react-sdk-components/src/components/field/TextInput/TextInput.tsx) to understand a typical component structure with imports to mock. + +Read [the TextInput test](packages/react-sdk-components/tests/unit/components/field/TextInput/TextInput.test.tsx) to understand the established testing pattern: mocking `getComponentFromMap`, `handleEvent`, `getPConnect`/actions, and the AAA test structure. + +## Verification + +- Run the generated test with `npm run test-jest` and verify it passes. If it fails, fix the error and retry. +- Verify 100% coverage with `npm run test-jest-coverage`. + +You will be provided a component name, a list of component names, or a path to a component directory. If nothing is provided, read and understand component code from the `packages/react-sdk-components/src/components/` directory and ask which component(s) to test. diff --git a/jest.config.js b/jest.config.js index 3d9c6751..db6da8ec 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,3 @@ -// eslint-disable-next-line strict module.exports = { testEnvironment: 'jsdom', roots: ['/packages/react-sdk-components/tests/unit/'], @@ -7,5 +6,9 @@ module.exports = { '^.+\\.(t|j)sx?$': 'ts-jest' }, setupFilesAfterEnv: ['/packages/react-sdk-components/tests/setupTests.js'], - coverageDirectory: 'tests/coverage' + moduleNameMapper: { + '\\.css$': 'identity-obj-proxy' // Mock CSS imports + }, + coverageDirectory: 'tests/coverage', + watchman: false }; diff --git a/package-lock.json b/package-lock.json index d9b28544..cd44e1a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,6 +59,7 @@ "eslint-plugin-sonarjs": "^3.0.5", "html-webpack-plugin": "^5.6.4", "http-server": "^14.1.1", + "identity-obj-proxy": "^3.0.0", "istanbul": "^0.4.5", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -13755,6 +13756,13 @@ "node": ">=6" } }, + "node_modules/harmony-reflect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz", + "integrity": "sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g==", + "dev": true, + "license": "(Apache-2.0 OR MPL-1.1)" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -14381,6 +14389,19 @@ "postcss": "^8.1.0" } }, + "node_modules/identity-obj-proxy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz", + "integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==", + "dev": true, + "license": "MIT", + "dependencies": { + "harmony-reflect": "^1.4.6" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", diff --git a/package.json b/package.json index 32d07564..0aa843c6 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "eslint-plugin-sonarjs": "^3.0.5", "html-webpack-plugin": "^5.6.4", "http-server": "^14.1.1", + "identity-obj-proxy": "^3.0.0", "istanbul": "^0.4.5", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/packages/react-sdk-components/src/components/field/Checkbox/Checkbox.tsx b/packages/react-sdk-components/src/components/field/Checkbox/Checkbox.tsx index 14fcba26..b716f07a 100644 --- a/packages/react-sdk-components/src/components/field/Checkbox/Checkbox.tsx +++ b/packages/react-sdk-components/src/components/field/Checkbox/Checkbox.tsx @@ -221,7 +221,7 @@ export default function CheckboxComponent(props: CheckboxProps) { onBlur={() => { thePConn.getValidationApi().validate(selectedvalues, selectionList); }} - data-testid={`${testId}:${element.value}`} + data-test-id={`${testId}:${element.value}`} /> } key={index} diff --git a/packages/react-sdk-components/tests/unit/components/field/AutoComplete/AutoComplete.test.tsx b/packages/react-sdk-components/tests/unit/components/field/AutoComplete/AutoComplete.test.tsx new file mode 100644 index 00000000..614bed40 --- /dev/null +++ b/packages/react-sdk-components/tests/unit/components/field/AutoComplete/AutoComplete.test.tsx @@ -0,0 +1,551 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import AutoComplete from '../../../../../src/components/field/AutoComplete/AutoComplete'; +import handleEvent from '../../../../../src/components/helpers/event-utils'; +import { getDataPage } from '../../../../../src/components/helpers/data_page'; +import Utils from '../../../../../src/components/helpers/utils'; + +jest.mock('../../../../../src/bridge/helpers/sdk_component_map', () => ({ + getComponentFromMap: (name: string) => { + if (name === 'FieldValueList') { + return ({ name: fieldName, value, variant }: any) => ( + + {fieldName}: {value} + + ); + } + if (name === 'TextInput') { + return (props: any) => ; + } + return () =>
; + } +})); + +jest.mock('../../../../../src/components/helpers/event-utils', () => jest.fn()); + +jest.mock('../../../../../src/components/helpers/data_page', () => ({ + getDataPage: jest.fn() +})); + +jest.mock('../../../../../src/components/helpers/utils', () => ({ + __esModule: true, + default: { + getOptionList: jest.fn(() => []) + } +})); + +const mockActions = { + updateFieldValue: jest.fn(), + triggerFieldChange: jest.fn() +}; + +const getDefaultProps = (): any => ({ + getPConnect: () => ({ + getActionsApi: () => mockActions, + getStateProps: () => ({ value: '.AutoCompleteValue' }), + getContextName: () => 'testContext', + getDataObject: () => ({}) + }), + label: 'Test AutoComplete', + required: false, + placeholder: 'Search...', + value: '', + validatemessage: '', + readOnly: false, + testId: 'autoCompleteTestId', + displayMode: '', + deferDatasource: false, + datasourceMetadata: {}, + status: '', + helperText: '', + hideLabel: false, + listType: 'associated', + datasource: [ + { key: 'key1', value: 'Option 1' }, + { key: 'key2', value: 'Option 2' } + ], + columns: [], + onRecordChange: undefined +}); + +describe('AutoComplete', () => { + beforeEach(() => { + jest.clearAllMocks(); + (Utils.getOptionList as jest.Mock).mockReturnValue([ + { key: 'key1', value: 'Option 1' }, + { key: 'key2', value: 'Option 2' } + ]); + (getDataPage as jest.Mock).mockResolvedValue([]); + }); + + test('renders Autocomplete with label and placeholder', () => { + // Arrange + const props = getDefaultProps(); + + // Act + render(); + + // Assert + expect(screen.getByLabelText('Test AutoComplete')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Search...')).toBeInTheDocument(); + }); + + test('renders with required attribute', () => { + // Arrange + const props = getDefaultProps(); + props.required = true; + + // Act + render(); + + // Assert + expect(screen.getByRole('combobox')).toHaveAttribute('required'); + }); + + test('renders FieldValueList in DISPLAY_ONLY mode', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = 'Test Value'; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toBeVisible(); + expect(fvl.textContent).toContain('Test AutoComplete'); + expect(fvl.textContent).toContain('Test Value'); + }); + + test('renders FieldValueList in STACKED_LARGE_VAL mode', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = 'Stacked Value'; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toBeVisible(); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + }); + + test('hides label in DISPLAY_ONLY mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = 'Val'; + props.hideLabel = true; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('mock-field-value-list').textContent).toBe(': Val'); + }); + + test('hides label in STACKED_LARGE_VAL mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = 'Val'; + props.hideLabel = true; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl.textContent).toBe(': Val'); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + }); + + test('renders TextInput in readOnly mode', () => { + // Arrange + const props = getDefaultProps(); + props.readOnly = true; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('autoCompleteTestId')).toBeInTheDocument(); + }); + + test('renders TextInput with resolved value in readOnly mode when value matches option', async () => { + // Arrange + (Utils.getOptionList as jest.Mock).mockReturnValue([{ key: 'key1', value: 'Option 1' }]); + const props = getDefaultProps(); + props.readOnly = true; + props.value = 'key1'; + + // Act + const { getByTestId } = render(); + + // Assert + await waitFor(() => { + expect(getByTestId('autoCompleteTestId')).toHaveAttribute('value', 'Option 1'); + }); + }); + + test('renders TextInput with undefined value in readOnly mode when no option matches', () => { + // Arrange + (Utils.getOptionList as jest.Mock).mockReturnValue([]); + const props = getDefaultProps(); + props.readOnly = true; + props.value = 'unmatchedKey'; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('autoCompleteTestId')).toBeInTheDocument(); + }); + + test('renders with error status', () => { + // Arrange + const props = getDefaultProps(); + props.status = 'error'; + + // Act + render(); + + // Assert + expect(screen.getByRole('combobox')).toHaveAttribute('aria-invalid', 'true'); + }); + + test('displays validation message as helper text', () => { + // Arrange + const props = getDefaultProps(); + props.validatemessage = 'This field is required'; + + // Act + render(); + + // Assert + expect(screen.getByText('This field is required')).toBeVisible(); + }); + + test('displays helperText when no validatemessage', () => { + // Arrange + const props = getDefaultProps(); + props.helperText = 'Enter a value'; + + // Act + render(); + + // Assert + expect(screen.getByText('Enter a value')).toBeVisible(); + }); + + test('calls Utils.getOptionList for associated listType', () => { + // Arrange + const props = getDefaultProps(); + props.listType = 'associated'; + + // Act + render(); + + // Assert + expect(Utils.getOptionList).toHaveBeenCalled(); + }); + + test('calls getDataPage for datapage listType with columns including dot-prefixed values', async () => { + // Arrange + (getDataPage as jest.Mock).mockResolvedValue([{ ID: 'id1', Name: 'Option A', Desc: 'Desc A' }]); + const props = getDefaultProps(); + props.listType = 'datapage'; + props.datasource = 'D_TestList'; + props.columns = [ + { key: 'true', value: '.ID' }, + { display: 'true', primary: 'true', value: '.Name' }, + { display: 'true', value: '.Desc' } + ]; + + // Act + render(); + + // Assert + await waitFor(() => { + expect(getDataPage).toHaveBeenCalledWith('D_TestList', undefined, 'testContext'); + }); + }); + + test('uses pyGUID as fallback key when no key column defined', async () => { + // Arrange + (getDataPage as jest.Mock).mockResolvedValue([{ pyGUID: 'guid1', Name: 'Option A' }]); + const props = getDefaultProps(); + props.listType = 'datapage'; + props.datasource = 'D_Test'; + props.columns = [{ display: 'true', primary: 'true', value: 'Name' }]; + + // Act + render(); + + // Assert + await waitFor(() => { + expect(getDataPage).toHaveBeenCalled(); + }); + }); + + test('calls getDataPage with parameters', async () => { + // Arrange + (getDataPage as jest.Mock).mockResolvedValue([]); + const props = getDefaultProps(); + props.listType = 'datapage'; + props.datasource = 'D_Test'; + props.parameters = { someParam: 'someValue' }; + props.columns = [{ key: 'true', value: 'ID', display: 'true', primary: 'true' }]; + + // Act + render(); + + // Assert + await waitFor(() => { + expect(getDataPage).toHaveBeenCalledWith('D_Test', { someParam: 'someValue' }, 'testContext'); + }); + }); + + test('does not call getDataPage in DISPLAY_ONLY mode for datapage listType', () => { + // Arrange + const props = getDefaultProps(); + props.listType = 'datapage'; + props.datasource = 'D_Test'; + props.displayMode = 'DISPLAY_ONLY'; + props.value = 'test'; + props.columns = [{ key: 'true', value: 'ID', display: 'true', primary: 'true' }]; + + // Act + render(); + + // Assert + expect(getDataPage).not.toHaveBeenCalled(); + }); + + test('transforms props when deferDatasource is true with @P prefixed properties', async () => { + // Arrange + (getDataPage as jest.Mock).mockResolvedValue([]); + const props = getDefaultProps(); + props.deferDatasource = true; + props.datasourceMetadata = { + datasource: { + name: 'D_DeferredList', + parameters: { + param1: { name: 'Param1', value: 'Value1' } + }, + propertyForDisplayText: '@P .DisplayField', + propertyForValue: '@P .ValueField' + } + }; + + // Act + render(); + + // Assert + await waitFor(() => { + expect(getDataPage).toHaveBeenCalledWith('D_DeferredList', { Param1: 'Value1' }, 'testContext'); + }); + }); + + test('transforms props when deferDatasource is true without @P prefix', async () => { + // Arrange + (getDataPage as jest.Mock).mockResolvedValue([]); + const props = getDefaultProps(); + props.deferDatasource = true; + props.datasourceMetadata = { + datasource: { + name: 'D_DeferredList', + parameters: {}, + propertyForDisplayText: 'DisplayField', + propertyForValue: 'ValueField' + } + }; + + // Act + render(); + + // Assert + await waitFor(() => { + expect(getDataPage).toHaveBeenCalledWith('D_DeferredList', {}, 'testContext'); + }); + }); + + test('handleChange calls handleEvent when option is selected', async () => { + // Arrange + (Utils.getOptionList as jest.Mock).mockReturnValue([ + { key: 'key1', value: 'Option 1' }, + { key: 'key2', value: 'Option 2' } + ]); + const props = getDefaultProps(); + render(); + + // Act + const combobox = screen.getByRole('combobox'); + fireEvent.mouseDown(combobox); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Option 1')); + + // Assert + expect(handleEvent).toHaveBeenCalledWith(mockActions, 'changeNblur', '.AutoCompleteValue', 'key1'); + }); + + test('handleChange calls onRecordChange when provided', async () => { + // Arrange + const onRecordChange = jest.fn(); + (Utils.getOptionList as jest.Mock).mockReturnValue([{ key: 'key1', value: 'Option 1' }]); + const props = getDefaultProps(); + props.onRecordChange = onRecordChange; + render(); + + // Act + const combobox = screen.getByRole('combobox'); + fireEvent.mouseDown(combobox); + + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByText('Option 1')); + + // Assert + expect(handleEvent).toHaveBeenCalled(); + expect(onRecordChange).toHaveBeenCalled(); + }); + + test('handleChange sends empty string when selection is cleared', async () => { + // Arrange + (Utils.getOptionList as jest.Mock).mockReturnValue([{ key: 'key1', value: 'Option 1' }]); + const props = getDefaultProps(); + props.value = 'key1'; + render(); + + // Act: Clear the autocomplete via the clear button + const clearButton = screen.getByTitle('Clear'); + fireEvent.click(clearButton); + + // Assert + expect(handleEvent).toHaveBeenCalledWith(mockActions, 'changeNblur', '.AutoCompleteValue', ''); + }); + + test('selectedValue resolves from options when value matches a key', () => { + // Arrange + (Utils.getOptionList as jest.Mock).mockReturnValue([{ key: 'key1', value: 'Option 1' }]); + const props = getDefaultProps(); + props.value = 'key1'; + + // Act + render(); + + // Assert + const combobox = screen.getByRole('combobox'); + expect(combobox).toHaveValue('Option 1'); + }); + + test('selectedValue falls back to raw value when no option matches', () => { + // Arrange + (Utils.getOptionList as jest.Mock).mockReturnValue([{ key: 'key1', value: 'Option 1' }]); + const props = getDefaultProps(); + props.value = 'unmatchedKey'; + + // Act + render(); + + // Assert + const combobox = screen.getByRole('combobox'); + expect(combobox).toHaveValue('unmatchedKey'); + }); + + test('columns without dot prefix are preserved by preProcessColumns', async () => { + // Arrange + (getDataPage as jest.Mock).mockResolvedValue([{ ID: 'id1', Name: 'Option A' }]); + const props = getDefaultProps(); + props.listType = 'datapage'; + props.datasource = 'D_Test'; + props.columns = [ + { key: 'true', value: 'ID' }, + { display: 'true', primary: 'true', value: 'Name' } + ]; + + // Act + render(); + + // Assert + await waitFor(() => { + expect(getDataPage).toHaveBeenCalled(); + }); + }); + + test('renders with default values when optional props are omitted', () => { + // Arrange - omit value, datasource, and columns to trigger default parameter branches + const props = getDefaultProps(); + delete props.value; + delete props.datasource; + delete props.columns; + + // Act + render(); + + // Assert + expect(screen.getByRole('combobox')).toBeInTheDocument(); + }); + + test('does not update datasource state when datasource has not changed on re-render', () => { + // Arrange + const props = getDefaultProps(); + const { rerender } = render(); + + // Act - re-render with the same props (isDeepEqual returns true) + rerender(); + + // Assert + expect(screen.getByRole('combobox')).toBeInTheDocument(); + }); + + test('getOptionLabel and isOptionEqualToValue handle option with empty value', async () => { + // Arrange - include an option with empty value to cover the falsy branch + (Utils.getOptionList as jest.Mock).mockReturnValue([ + { key: 'key1', value: '' }, + { key: 'key2', value: 'Option 2' } + ]); + const props = getDefaultProps(); + render(); + + // Act - open dropdown to trigger getOptionLabel and isOptionEqualToValue + const combobox = screen.getByRole('combobox'); + fireEvent.mouseDown(combobox); + + // Assert + await waitFor(() => { + expect(screen.getByRole('listbox')).toBeInTheDocument(); + }); + }); + + test('flattenParameters uses default empty object when parameters is undefined', async () => { + // Arrange - datasourceMetadata without parameters to trigger params = {} default + (getDataPage as jest.Mock).mockResolvedValue([]); + const props = getDefaultProps(); + props.deferDatasource = true; + props.datasourceMetadata = { + datasource: { + name: 'D_NoParms', + propertyForDisplayText: '@P .DisplayField', + propertyForValue: '@P .ValueField' + } + }; + + // Act + render(); + + // Assert + await waitFor(() => { + expect(getDataPage).toHaveBeenCalledWith('D_NoParms', {}, 'testContext'); + }); + }); +}); diff --git a/packages/react-sdk-components/tests/unit/components/field/Checkbox/Checkbox.test.tsx b/packages/react-sdk-components/tests/unit/components/field/Checkbox/Checkbox.test.tsx new file mode 100644 index 00000000..98084fa3 --- /dev/null +++ b/packages/react-sdk-components/tests/unit/components/field/Checkbox/Checkbox.test.tsx @@ -0,0 +1,773 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import CheckboxComponent from '../../../../../src/components/field/Checkbox/Checkbox'; +import handleEvent from '../../../../../src/components/helpers/event-utils'; +import { insertInstruction, deleteInstruction, updateNewInstuctions } from '../../../../../src/components/helpers/instructions-utils'; + +jest.mock('../../../../../src/bridge/helpers/sdk_component_map', () => ({ + getComponentFromMap: (name: string) => { + if (name === 'FieldValueList') { + return ({ name: fieldName, value, variant }: any) => ( + + {fieldName}: {value} + + ); + } + if (name === 'SelectableCard') { + return (cardProps: any) => ( +
+ + + {cardProps.showNoValue && No value} +
+ ); + } + return () =>
; + } +})); + +jest.mock('../../../../../src/components/helpers/event-utils', () => jest.fn()); + +jest.mock('../../../../../src/components/helpers/instructions-utils', () => ({ + insertInstruction: jest.fn(), + deleteInstruction: jest.fn(), + updateNewInstuctions: jest.fn() +})); + +jest.mock('@mui/styles/makeStyles', () => () => () => ({ + checkbox: 'checkbox', + selectableCard: 'selectableCard' +})); + +const mockValidate = jest.fn(); +const mockClearErrorMessages = jest.fn(); +const mockSetReferenceList = jest.fn(); +const mockOnClick = jest.fn(); +const mockInsert = jest.fn(); +const mockDeleteEntry = jest.fn(); +const mockInitDefaultPageInstructions = jest.fn(); + +const mockActions = { + updateFieldValue: jest.fn(), + triggerFieldChange: jest.fn(), + onClick: mockOnClick +}; + +const getDefaultProps = (): any => ({ + getPConnect: () => ({ + getActionsApi: () => mockActions, + getStateProps: () => ({ value: '.CheckboxValue' }), + getValidationApi: () => ({ validate: mockValidate }), + clearErrorMessages: mockClearErrorMessages, + setReferenceList: mockSetReferenceList, + getListActions: () => ({ + insert: mockInsert, + deleteEntry: mockDeleteEntry, + initDefaultPageInstructions: mockInitDefaultPageInstructions + }), + getValue: () => [], + getPageReference: () => '', + getFieldMetadata: () => ({}), + getRawMetadata: () => ({ config: { imageDescription: '.SomeDesc' } }) + }), + label: 'Test Checkbox', + caption: 'Check this', + value: false, + readOnly: false, + testId: 'checkboxTestId', + required: false, + disabled: false, + status: '', + helperText: '', + validatemessage: '', + displayMode: '', + hideLabel: false, + trueLabel: 'Yes', + falseLabel: 'No', + selectionMode: 'single', + datasource: undefined, + selectionKey: '', + selectionList: '', + primaryField: '', + readonlyContextList: [], + referenceList: '', + variant: '', + hideFieldLabels: false, + additionalProps: {}, + imagePosition: 'top', + imageSize: 'medium', + showImageDescription: 'true', + renderMode: '', + image: '.Image' +}); + +describe('CheckboxComponent', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders single checkbox with caption and label', () => { + // Arrange + const props = getDefaultProps(); + + // Act + render(); + + // Assert + expect(screen.getByText('Test Checkbox')).toBeVisible(); + expect(screen.getByText('Check this')).toBeVisible(); + }); + + test('renders with required state', () => { + // Arrange + const props = getDefaultProps(); + props.required = true; + + // Act + render(); + + // Assert + expect(screen.getByText('Test Checkbox')).toBeInTheDocument(); + }); + + test('renders checkbox as checked when value is true', () => { + // Arrange + const props = getDefaultProps(); + props.value = true; + + // Act + render(); + + // Assert + expect(screen.getByRole('checkbox')).toBeChecked(); + }); + + test('renders checkbox as unchecked when value is false', () => { + // Arrange + const props = getDefaultProps(); + props.value = false; + + // Act + render(); + + // Assert + expect(screen.getByRole('checkbox')).not.toBeChecked(); + }); + + test('calls handleEvent on change when not readOnly', () => { + // Arrange + const props = getDefaultProps(); + render(); + + // Act + fireEvent.click(screen.getByRole('checkbox')); + + // Assert + expect(handleEvent).toHaveBeenCalledWith(mockActions, 'changeNblur', '.CheckboxValue', true); + }); + + test('calls validate on blur when not readOnly', () => { + // Arrange + const props = getDefaultProps(); + render(); + + // Act + fireEvent.blur(screen.getByRole('checkbox')); + + // Assert + expect(mockValidate).toHaveBeenCalledWith(false); + }); + + test('does not call handleEvent on change when readOnly', () => { + // Arrange + const props = getDefaultProps(); + props.readOnly = true; + + // Act + render(); + fireEvent.click(screen.getByRole('checkbox')); + + // Assert + expect(handleEvent).not.toHaveBeenCalled(); + }); + + test('does not call validate on blur when readOnly', () => { + // Arrange + const props = getDefaultProps(); + props.readOnly = true; + + // Act + render(); + fireEvent.blur(screen.getByRole('checkbox')); + + // Assert + expect(mockValidate).not.toHaveBeenCalled(); + }); + + test('renders FieldValueList in DISPLAY_ONLY mode with trueLabel when value is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = true; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toBeVisible(); + expect(fvl.textContent).toContain('Check this'); + expect(fvl.textContent).toContain('Yes'); + }); + + test('renders FieldValueList in DISPLAY_ONLY mode with falseLabel when value is false', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = false; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('mock-field-value-list').textContent).toContain('No'); + }); + + test('renders FieldValueList in STACKED_LARGE_VAL mode', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = true; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + expect(fvl.textContent).toContain('Yes'); + }); + + test('renders FieldValueList in STACKED_LARGE_VAL mode with falseLabel when value is false', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = false; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + expect(fvl.textContent).toContain('No'); + }); + + test('hides label in DISPLAY_ONLY mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = true; + props.hideLabel = true; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('mock-field-value-list').textContent).toBe(': Yes'); + }); + + test('hides label in STACKED_LARGE_VAL mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = true; + props.hideLabel = true; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl.textContent).toBe(': Yes'); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + }); + + test('displays validation message as helper text', () => { + // Arrange + const props = getDefaultProps(); + props.validatemessage = 'Field is required'; + + // Act + render(); + + // Assert + expect(screen.getByText('Field is required')).toBeVisible(); + }); + + test('displays helperText when no validatemessage', () => { + // Arrange + const props = getDefaultProps(); + props.helperText = 'Help text here'; + + // Act + render(); + + // Assert + expect(screen.getByText('Help text here')).toBeVisible(); + }); + + test('renders with error status', () => { + // Arrange + const props = getDefaultProps(); + props.status = 'error'; + + // Act + render(); + + // Assert + expect(screen.getByRole('checkbox')).toBeInTheDocument(); + }); + + test('hides label when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.hideLabel = true; + + // Act + render(); + + // Assert + expect(screen.queryByText('Test Checkbox')).not.toBeInTheDocument(); + }); + + test('renders disabled checkbox', () => { + // Arrange + const props = getDefaultProps(); + props.disabled = true; + + // Act + render(); + + // Assert + expect(screen.getByRole('checkbox')).toBeDisabled(); + }); + + test('updates checked state when value prop changes', () => { + // Arrange + const props = getDefaultProps(); + props.value = false; + const { rerender } = render(); + expect(screen.getByRole('checkbox')).not.toBeChecked(); + + // Act + props.value = true; + rerender(); + + // Assert + expect(screen.getByRole('checkbox')).toBeChecked(); + }); + + // Multi-mode tests + test('renders multiple checkboxes in multi selectionMode', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [ + { key: 'opt1', value: 'Option 1' }, + { key: 'opt2', value: 'Option 2' }, + { key: 'opt3', value: 'Option 3' } + ] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = []; + + // Act + render(); + + // Assert + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes).toHaveLength(3); + expect(screen.getByText('Option 1')).toBeVisible(); + expect(screen.getByText('Option 2')).toBeVisible(); + }); + + test('renders multi checkboxes with text property as label', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [{ key: 'opt1', value: 'ValueLabel', text: 'TextLabel' }] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = []; + + // Act + render(); + + // Assert + expect(screen.getByText('TextLabel')).toBeVisible(); + }); + + test('multi checkbox shows checked when selectedvalues matches', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [ + { key: 'opt1', value: 'Option 1' }, + { key: 'opt2', value: 'Option 2' } + ] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = [{ SelectedKey: 'opt1' }]; + + // Act + render(); + + // Assert + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes[0]).toBeChecked(); + expect(checkboxes[1]).not.toBeChecked(); + }); + + test('multi checkbox calls insertInstruction on check', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [{ key: 'opt1', value: 'Option 1' }] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = []; + + render(); + + // Act + fireEvent.click(screen.getByRole('checkbox')); + + // Assert + expect(insertInstruction).toHaveBeenCalled(); + expect(mockClearErrorMessages).toHaveBeenCalledWith({ + property: '.SelectedList', + category: '', + context: '' + }); + }); + + test('multi checkbox calls deleteInstruction on uncheck', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [{ key: 'opt1', value: 'Option 1' }] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = [{ SelectedKey: 'opt1' }]; + + render(); + + // Act + fireEvent.click(screen.getByRole('checkbox')); + + // Assert + expect(deleteInstruction).toHaveBeenCalled(); + }); + + test('multi checkbox calls validate on blur', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [{ key: 'opt1', value: 'Option 1' }] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = []; + + render(); + + // Act + fireEvent.blur(screen.getByRole('checkbox')); + + // Assert + expect(mockValidate).toHaveBeenCalledWith([], '.SelectedList'); + }); + + test('multi checkbox handles empty datasource source', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = {}; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = []; + + // Act + render(); + + // Assert + expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); + }); + + test('multi checkbox with text fallback in insertInstruction and deleteInstruction', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [{ key: 'opt1', value: 'FallbackValue' }] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = [{ SelectedKey: 'opt1' }]; + render(); + + // Act - uncheck to trigger delete path with element.text ?? element.value + fireEvent.click(screen.getByRole('checkbox')); + + // Assert + expect(deleteInstruction).toHaveBeenCalled(); + }); + + test('multi checkbox insert path with text property uses text over value', () => { + // Arrange + const props = getDefaultProps(); + props.selectionMode = 'multi'; + props.datasource = { + source: [{ key: 'opt1', value: 'FallbackValue', text: 'TextValue' }] + }; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.readonlyContextList = []; + render(); + + // Act - check to trigger insert path with element.text ?? element.value + fireEvent.click(screen.getByRole('checkbox')); + + // Assert + expect(insertInstruction).toHaveBeenCalledWith(expect.anything(), '.SelectedList', '.SelectedKey', '.Primary', { + id: 'opt1', + primary: 'TextValue' + }); + }); + + // Card variant tests + test('renders SelectableCard when variant is card', () => { + // Arrange + const props = getDefaultProps(); + props.variant = 'card'; + props.selectionKey = '.SelectedKey'; + props.primaryField = '.Primary'; + props.datasource = { source: [{ SelectedKey: 'opt1' }] }; + props.readonlyContextList = []; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('mock-selectable-card')).toBeVisible(); + expect(screen.getByText('Test Checkbox')).toBeVisible(); + }); + + test('card variant onChange calls handleCheckboxChange with checked', () => { + // Arrange + const props = getDefaultProps(); + props.variant = 'card'; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.datasource = { source: [{ SelectedKey: 'opt1' }] }; + props.readonlyContextList = []; + + render(); + + // Act - the onChange handler expects e.target.checked and e.target.id + const cardCheckbox = screen.getByTestId('card-checkbox'); + Object.defineProperty(cardCheckbox, 'id', { value: 'opt1', writable: true }); + fireEvent.click(cardCheckbox); + + // Assert + expect(insertInstruction).toHaveBeenCalled(); + }); + + test('card variant onChange handles uncheck (delete)', () => { + // Arrange + const props = getDefaultProps(); + props.variant = 'card'; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.datasource = { source: [{ SelectedKey: 'opt1' }] }; + props.readonlyContextList = []; + + render(); + + // Act - first check then uncheck + const cardCheckbox = screen.getByTestId('card-checkbox'); + Object.defineProperty(cardCheckbox, 'id', { value: 'opt1', writable: true }); + fireEvent.click(cardCheckbox); // check + jest.clearAllMocks(); + fireEvent.click(cardCheckbox); // uncheck + + // Assert + expect(deleteInstruction).toHaveBeenCalled(); + }); + + test('card variant onBlur calls validate', () => { + // Arrange + const props = getDefaultProps(); + props.variant = 'card'; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.datasource = { source: [] }; + props.readonlyContextList = []; + + render(); + + // Act + fireEvent.click(screen.getByTestId('card-blur')); + + // Assert + expect(mockValidate).toHaveBeenCalledWith([], '.SelectedList'); + }); + + test('card variant shows no-value in ReadOnly renderMode with empty selectedvalues', () => { + // Arrange + const props = getDefaultProps(); + props.variant = 'card'; + props.renderMode = 'ReadOnly'; + props.selectionKey = '.SelectedKey'; + props.primaryField = '.Primary'; + props.datasource = { source: [] }; + props.readonlyContextList = []; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('no-value')).toBeVisible(); + }); + + test('card variant does not show no-value when selectedvalues exist', () => { + // Arrange + const props = getDefaultProps(); + props.variant = 'card'; + props.renderMode = 'ReadOnly'; + props.selectionKey = '.SelectedKey'; + props.primaryField = '.Primary'; + props.datasource = { source: [] }; + props.readonlyContextList = [{ SelectedKey: 'opt1' }]; + + // Act + render(); + + // Assert + expect(screen.queryByTestId('no-value')).not.toBeInTheDocument(); + }); + + // referenceList / updateNewInstuctions + test('calls setReferenceList and updateNewInstuctions when referenceList has length and not readOnly', () => { + // Arrange + const props = getDefaultProps(); + props.referenceList = '.SomeRefList'; + props.selectionList = '.SelectedList'; + + // Act + render(); + + // Assert + expect(mockSetReferenceList).toHaveBeenCalledWith('.SelectedList'); + expect(updateNewInstuctions).toHaveBeenCalled(); + }); + + test('does not call setReferenceList when readOnly mode', () => { + // Arrange + const props = getDefaultProps(); + props.referenceList = '.SomeRefList'; + props.readOnly = true; + + // Act + render(); + + // Assert + expect(mockSetReferenceList).not.toHaveBeenCalled(); + }); + + test('does not call setReferenceList when renderMode is ReadOnly', () => { + // Arrange + const props = getDefaultProps(); + props.referenceList = '.SomeRefList'; + props.renderMode = 'ReadOnly'; + + // Act + render(); + + // Assert + expect(mockSetReferenceList).not.toHaveBeenCalled(); + }); + + test('does not call setReferenceList when displayMode is DISPLAY_ONLY', () => { + // Arrange + const props = getDefaultProps(); + props.referenceList = '.SomeRefList'; + props.displayMode = 'DISPLAY_ONLY'; + props.value = true; + + // Act + render(); + + // Assert + expect(mockSetReferenceList).not.toHaveBeenCalled(); + }); + + test('does not call setReferenceList when referenceList is empty', () => { + // Arrange + const props = getDefaultProps(); + props.referenceList = ''; + + // Act + render(); + + // Assert + expect(mockSetReferenceList).not.toHaveBeenCalled(); + }); + + test('card variant onChange with item not found in datasource', () => { + // Arrange + const props = getDefaultProps(); + props.variant = 'card'; + props.selectionKey = '.SelectedKey'; + props.selectionList = '.SelectedList'; + props.primaryField = '.Primary'; + props.datasource = { source: [{ SelectedKey: 'opt1' }] }; + props.readonlyContextList = []; + + render(); + + // Act - pass an id that doesn't match any item + const cardCheckbox = screen.getByTestId('card-checkbox'); + Object.defineProperty(cardCheckbox, 'id', { value: 'nonexistent', writable: true }); + fireEvent.click(cardCheckbox); + + // Assert - should still call insertInstruction with undefined values + expect(insertInstruction).toHaveBeenCalled(); + }); +}); diff --git a/packages/react-sdk-components/tests/unit/components/field/Currency/Currency.test.tsx b/packages/react-sdk-components/tests/unit/components/field/Currency/Currency.test.tsx new file mode 100644 index 00000000..e36d3b99 --- /dev/null +++ b/packages/react-sdk-components/tests/unit/components/field/Currency/Currency.test.tsx @@ -0,0 +1,323 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import Currency from '../../../../../src/components/field/Currency/Currency'; +import handleEvent from '../../../../../src/components/helpers/event-utils'; + +jest.mock('../../../../../src/bridge/helpers/sdk_component_map', () => ({ + getComponentFromMap: (name: string) => { + if (name === 'FieldValueList') { + return ({ name: fieldName, value, variant }: any) => ( + + {fieldName}: {value} + + ); + } + return () =>
; + } +})); + +jest.mock('../../../../../src/components/helpers/event-utils', () => jest.fn()); + +jest.mock('../../../../../src/components/field/Currency/currency-utils', () => ({ + getCurrencyCharacters: () => ({ + theCurrencySymbol: '$', + theDecimalIndicator: '.', + theDigitGroupSeparator: ',' + }), + getCurrencyOptions: () => ({ + locale: 'en-US', + style: 'currency', + currency: 'USD' + }) +})); + +jest.mock('../../../../../src/components/helpers/formatters', () => ({ + format: (_value: any) => `$${_value}` +})); + +const mockActions = { + updateFieldValue: jest.fn(), + triggerFieldChange: jest.fn() +}; + +const getDefaultProps = (): any => ({ + getPConnect: () => ({ + getActionsApi: () => mockActions, + getStateProps: () => ({ value: '.CurrencyValue' }), + getComponentName: () => 'Currency' + }), + label: 'Amount', + required: false, + disabled: false, + value: '', + validatemessage: '', + status: '', + readOnly: false, + testId: 'currencyTestId', + helperText: '', + displayMode: '', + hideLabel: false, + currencyISOCode: 'USD', + placeholder: 'Enter amount', + allowDecimals: true +}); + +describe('Currency', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders with label and placeholder', () => { + // Arrange + const props = getDefaultProps(); + + // Act + render(); + + // Assert + expect(screen.getByLabelText('Amount')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('Enter amount')).toBeInTheDocument(); + }); + + test('renders with required attribute', () => { + // Arrange + const props = getDefaultProps(); + props.required = true; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toHaveAttribute('required'); + }); + + test('renders with disabled attribute', () => { + // Arrange + const props = getDefaultProps(); + props.disabled = true; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toBeDisabled(); + }); + + test('renders with readOnly attribute', () => { + // Arrange + const props = getDefaultProps(); + props.readOnly = true; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toHaveAttribute('readonly'); + }); + + test('renders FieldValueList in DISPLAY_ONLY mode', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = '100'; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toBeVisible(); + expect(fvl.textContent).toContain('Amount'); + expect(fvl.textContent).toContain('$100'); + }); + + test('renders FieldValueList in STACKED_LARGE_VAL mode', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = '200'; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toBeVisible(); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + expect(fvl.textContent).toContain('$200'); + }); + + test('hides label in DISPLAY_ONLY mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = '100'; + props.hideLabel = true; + + // Act + const { getByTestId } = render(); + + // Assert + expect(getByTestId('mock-field-value-list').textContent).toBe(': $100'); + }); + + test('hides label in STACKED_LARGE_VAL mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = '100'; + props.hideLabel = true; + + // Act + const { getByTestId } = render(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl.textContent).toBe(': $100'); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + }); + + test('displays validation message as helper text', () => { + // Arrange + const props = getDefaultProps(); + props.validatemessage = 'Required field'; + + // Act + render(); + + // Assert + expect(screen.getByText('Required field')).toBeVisible(); + }); + + test('displays helperText when no validatemessage', () => { + // Arrange + const props = getDefaultProps(); + props.helperText = 'Enter currency'; + + // Act + render(); + + // Assert + expect(screen.getByText('Enter currency')).toBeVisible(); + }); + + test('renders with error status', () => { + // Arrange + const props = getDefaultProps(); + props.status = 'error'; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toHaveAttribute('aria-invalid', 'true'); + }); + + test('calls handleEvent on blur when not readOnly', () => { + // Arrange + const props = getDefaultProps(); + props.value = '123'; + render(); + + // Act + fireEvent.blur(screen.getByTestId('currencyTestId')); + + // Assert + expect(handleEvent).toHaveBeenCalledWith(mockActions, 'changeNblur', '.CurrencyValue', '123'); + }); + + test('does not call handleEvent on blur when readOnly', () => { + // Arrange + const props = getDefaultProps(); + props.readOnly = true; + props.value = '123'; + render(); + + // Act + fireEvent.blur(screen.getByTestId('currencyTestId')); + + // Assert + expect(handleEvent).not.toHaveBeenCalled(); + }); + + test('renders with value and updates on value change', () => { + // Arrange + const props = getDefaultProps(); + props.value = '500'; + const { rerender } = render(); + + // Assert initial value + expect(screen.getByTestId('currencyTestId')).toHaveValue('$500.00'); + + // Act + props.value = '1000'; + rerender(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toHaveValue('$1,000.00'); + }); + + test('handles onValueChange updating internal state', () => { + // Arrange + const props = getDefaultProps(); + render(); + + // Act + fireEvent.change(screen.getByTestId('currencyTestId'), { + target: { value: '$456.00' } + }); + fireEvent.blur(screen.getByTestId('currencyTestId')); + + // Assert + expect(handleEvent).toHaveBeenCalledWith(mockActions, 'changeNblur', '.CurrencyValue', expect.any(String)); + }); + + test('uses default placeholder when placeholder is not provided', () => { + // Arrange + const props = getDefaultProps(); + delete props.placeholder; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toBeInTheDocument(); + }); + + test('renders with allowDecimals false (no decimal places)', () => { + // Arrange + const props = getDefaultProps(); + props.allowDecimals = false; + props.value = '100'; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toHaveValue('$100'); + }); + + test('renders with default currencyISOCode when not provided', () => { + // Arrange + const props = getDefaultProps(); + delete props.currencyISOCode; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toBeInTheDocument(); + }); + + test('renders with default empty value when value is not provided', () => { + // Arrange + const props = getDefaultProps(); + delete props.value; + + // Act + render(); + + // Assert + expect(screen.getByTestId('currencyTestId')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-sdk-components/tests/unit/components/field/Date/Date.test.tsx b/packages/react-sdk-components/tests/unit/components/field/Date/Date.test.tsx new file mode 100644 index 00000000..bcc73960 --- /dev/null +++ b/packages/react-sdk-components/tests/unit/components/field/Date/Date.test.tsx @@ -0,0 +1,293 @@ +import React from 'react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; +import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; +import DateComponent from '../../../../../src/components/field/Date/Date'; +import handleEvent from '../../../../../src/components/helpers/event-utils'; + +jest.mock('../../../../../src/bridge/helpers/sdk_component_map', () => ({ + getComponentFromMap: (name: string) => { + if (name === 'FieldValueList') { + return ({ name: fieldName, value, variant }: any) => ( + + {fieldName}: {value} + + ); + } + if (name === 'TextInput') { + return (inputProps: any) => ; + } + return () =>
; + } +})); + +jest.mock('../../../../../src/components/helpers/event-utils', () => jest.fn()); + +jest.mock('../../../../../src/components/helpers/formatters', () => ({ + format: (val: any) => (val ? `formatted-${val}` : '') +})); + +jest.mock('../../../../../src/components/helpers/date-format-utils', () => ({ + dateFormatInfoDefault: { + dateFormatString: 'MM/DD/YYYY', + dateFormatStringLC: 'mm/dd/yyyy', + dateFormatMask: '__/__/____' + }, + getDateFormatInfo: () => ({ + dateFormatString: 'MM/DD/YYYY', + dateFormatStringLC: 'mm/dd/yyyy', + dateFormatMask: '__/__/____' + }) +})); + +const mockActions = { + updateFieldValue: jest.fn(), + triggerFieldChange: jest.fn() +}; + +const getDefaultProps = (): any => ({ + getPConnect: () => ({ + getActionsApi: () => mockActions, + getStateProps: () => ({ value: '.DateValue' }) + }), + label: 'Date Field', + required: false, + disabled: false, + value: '', + validatemessage: '', + status: '', + readOnly: false, + testId: 'dateTestId', + helperText: '', + displayMode: '', + hideLabel: false +}); + +const renderWithProvider = (ui: React.ReactElement) => { + return render({ui}); +}; + +describe('Date', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('renders DatePicker with label', () => { + // Arrange + const props = getDefaultProps(); + + // Act + renderWithProvider(); + + // Assert + expect(screen.getByLabelText('Date Field')).toBeInTheDocument(); + }); + + test('renders with required attribute', () => { + // Arrange + const props = getDefaultProps(); + props.required = true; + + // Act + renderWithProvider(); + + // Assert + const input = screen.getByRole('textbox'); + expect(input).toHaveAttribute('required'); + }); + + test('renders with disabled state', () => { + // Arrange + const props = getDefaultProps(); + props.disabled = true; + + // Act + renderWithProvider(); + + // Assert + const input = screen.getByRole('textbox'); + expect(input).toBeDisabled(); + }); + + test('renders FieldValueList in DISPLAY_ONLY mode', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = '2023-06-15'; + + // Act + const { getByTestId } = renderWithProvider(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toBeVisible(); + expect(fvl.textContent).toContain('Date Field'); + expect(fvl.textContent).toContain('formatted-2023-06-15'); + }); + + test('renders FieldValueList in STACKED_LARGE_VAL mode', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = '2023-06-15'; + + // Act + const { getByTestId } = renderWithProvider(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl).toBeVisible(); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + expect(fvl.textContent).toContain('formatted-2023-06-15'); + }); + + test('hides label in DISPLAY_ONLY mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'DISPLAY_ONLY'; + props.value = '2023-06-15'; + props.hideLabel = true; + + // Act + const { getByTestId } = renderWithProvider(); + + // Assert + expect(getByTestId('mock-field-value-list').textContent).toBe(': formatted-2023-06-15'); + }); + + test('hides label in STACKED_LARGE_VAL mode when hideLabel is true', () => { + // Arrange + const props = getDefaultProps(); + props.displayMode = 'STACKED_LARGE_VAL'; + props.value = '2023-06-15'; + props.hideLabel = true; + + // Act + const { getByTestId } = renderWithProvider(); + + // Assert + const fvl = getByTestId('mock-field-value-list'); + expect(fvl.textContent).toBe(': formatted-2023-06-15'); + expect(fvl).toHaveAttribute('data-variant', 'stacked'); + }); + + test('renders TextInput in readOnly mode', () => { + // Arrange + const props = getDefaultProps(); + props.readOnly = true; + + // Act + const { getByTestId } = renderWithProvider(); + + // Assert + expect(getByTestId('dateTestId')).toBeInTheDocument(); + }); + + test('displays validation message as helper text', () => { + // Arrange + const props = getDefaultProps(); + props.validatemessage = 'Date is required'; + + // Act + renderWithProvider(); + + // Assert + expect(screen.getByText('Date is required')).toBeVisible(); + }); + + test('displays helperText when no validatemessage', () => { + // Arrange + const props = getDefaultProps(); + props.helperText = 'Pick a date'; + + // Act + renderWithProvider(); + + // Assert + expect(screen.getByText('Pick a date')).toBeVisible(); + }); + + test('renders with error status', () => { + // Arrange + const props = getDefaultProps(); + props.status = 'error'; + + // Act + renderWithProvider(); + + // Assert + const input = screen.getByRole('textbox'); + expect(input).toHaveAttribute('aria-invalid', 'true'); + }); + + test('calls handleEvent when a valid date is entered', async () => { + // Arrange + const props = getDefaultProps(); + renderWithProvider(); + + // Act + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: '06/15/2023' } }); + + // Assert + await waitFor(() => { + expect(handleEvent).toHaveBeenCalledWith(mockActions, 'changeNblur', '.DateValue', '2023-06-15'); + }); + }); + + test('does not call handleEvent when an invalid date is entered', () => { + // Arrange + const props = getDefaultProps(); + renderWithProvider(); + + // Act + const input = screen.getByRole('textbox'); + fireEvent.change(input, { target: { value: 'invalid' } }); + + // Assert + expect(handleEvent).not.toHaveBeenCalled(); + }); + + test('renders with a pre-set value', () => { + // Arrange + const props = getDefaultProps(); + props.value = '2023-06-15'; + + // Act + renderWithProvider(); + + // Assert + expect(screen.getByRole('textbox')).toHaveValue('06/15/2023'); + }); + + test('updates dateValue when value prop changes', () => { + // Arrange + const props = getDefaultProps(); + props.value = '2023-01-01'; + const { rerender } = renderWithProvider(); + expect(screen.getByRole('textbox')).toHaveValue('01/01/2023'); + + // Act + props.value = '2023-12-25'; + rerender( + + + + ); + + // Assert + expect(screen.getByRole('textbox')).toHaveValue('12/25/2023'); + }); + + test('renders with empty value (null dateValue)', () => { + // Arrange + const props = getDefaultProps(); + props.value = ''; + + // Act + renderWithProvider(); + + // Assert + expect(screen.getByTestId('dateTestId')).toBeInTheDocument(); + }); +}); diff --git a/packages/react-sdk-components/tests/unit/components/field/TextInput/TextInput.test.tsx b/packages/react-sdk-components/tests/unit/components/field/TextInput/TextInput.test.tsx new file mode 100644 index 00000000..53985f3f --- /dev/null +++ b/packages/react-sdk-components/tests/unit/components/field/TextInput/TextInput.test.tsx @@ -0,0 +1,154 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import TextInput from '../../../../../src/components/field/TextInput'; +import handleEvent from '../../../../../src/components/helpers/event-utils'; + +// Mock getComponentFromMap to return a mock FieldValueList instead of reading the real module +jest.mock('../../../../../src/bridge/helpers/sdk_component_map', () => ({ + getComponentFromMap: (name: string) => { + if (name === 'FieldValueList') { + return ({ name: fieldName, value, variant }: any) => ( + + {fieldName}: {value} + + ); + } + return () =>
; + } +})); + +// Mock handleEvent to avoid reading its real implementation +jest.mock('../../../../../src/components/helpers/event-utils', () => jest.fn()); + +const mockActions = { + updateFieldValue: jest.fn(), + triggerFieldChange: jest.fn() +}; + +const getDefaultProps = (): any => { + return { + required: true, + testId: 'textInputTestId', + label: 'TextInput', + displayMode: false, + getPConnect: () => ({ + getActionsApi: () => mockActions, + getStateProps: () => ({ value: '.TextInputValue' }) + }) + }; +}; + +describe('Test Text Input component', () => { + test('TextInput Component renders with required', () => { + const props = getDefaultProps(); + const TextInputComponent = render(); + expect(TextInputComponent.getByTestId('textInputTestId')).toHaveAttribute('required'); + + props.required = false; + TextInputComponent.rerender(); + expect(TextInputComponent.getByTestId('textInputTestId')).not.toHaveAttribute('required'); + }); + + test('TextInput Component renders with disabled', () => { + const props = getDefaultProps(); + props.disabled = true; + const TextInputComponent = render(); + expect(TextInputComponent.getByTestId('textInputTestId')).toHaveAttribute('disabled'); + + props.disabled = false; + TextInputComponent.rerender(); + expect(TextInputComponent.getByTestId('textInputTestId')).not.toHaveAttribute('disabled'); + }); + + test('TextInput Component renders with readonly', () => { + const props = getDefaultProps(); + props.readOnly = true; + const TextInputComponent = render(); + expect(TextInputComponent.getByTestId('textInputTestId')).toHaveAttribute('readonly'); + + props.readOnly = false; + TextInputComponent.rerender(); + expect(TextInputComponent.getByTestId('textInputTestId')).not.toHaveAttribute('readonly'); + }); + + test('TextInput Component renders with label', () => { + const props = getDefaultProps(); + const { getByText } = render(); + const labelDiv = getByText('TextInput'); + expect(labelDiv).toBeVisible(); + }); + + test('TextInput Component renders with displayMode as LabelsLeft', () => { + const props = getDefaultProps(); + props.value = 'Hi there!'; + props.displayMode = 'DISPLAY_ONLY'; + const { getByTestId } = render(); + const fieldValueList = getByTestId('mock-field-value-list'); + expect(fieldValueList).toBeVisible(); + expect(fieldValueList.textContent).toContain('Hi there!'); + }); + + test('TextInput Component invoke handlers for blur and change events', () => { + const props = getDefaultProps(); + const TextInputComponent = render(); + fireEvent.change(TextInputComponent.getByTestId('textInputTestId'), { + target: { value: 'a' } + }); + fireEvent.blur(TextInputComponent.getByTestId('textInputTestId')); + expect(handleEvent).toHaveBeenCalledWith(mockActions, 'changeNblur', '.TextInputValue', 'a'); + }); + + test('TextInput Component should not invoke on blur handler for read only fields', () => { + (handleEvent as jest.Mock).mockClear(); + const props = getDefaultProps(); + props.readOnly = true; + const TextInputComponent = render(); + fireEvent.change(TextInputComponent.getByTestId('textInputTestId'), { + target: { value: 'a' } + }); + fireEvent.blur(TextInputComponent.getByTestId('textInputTestId')); + expect(handleEvent).not.toHaveBeenCalled(); + }); + + test('FieldValueList child component is rendered in DISPLAY_ONLY mode', () => { + const props = getDefaultProps(); + props.value = 'Test Value'; + props.displayMode = 'DISPLAY_ONLY'; + const { getByTestId, getByText } = render(); + expect(getByTestId('mock-field-value-list')).toBeVisible(); + expect(getByText('TextInput: Test Value')).toBeVisible(); + }); + + test('FieldValueList child component is rendered in STACKED_LARGE_VAL mode', () => { + const props = getDefaultProps(); + props.value = 'Stacked Value'; + props.displayMode = 'STACKED_LARGE_VAL'; + const { getByTestId } = render(); + const fieldValueList = getByTestId('mock-field-value-list'); + expect(fieldValueList).toBeVisible(); + expect(fieldValueList).toHaveAttribute('data-variant', 'stacked'); + }); + + test('FieldValueList hides label in DISPLAY_ONLY mode when hideLabel is true', () => { + const props = getDefaultProps(); + props.value = 'Hidden Label'; + props.displayMode = 'DISPLAY_ONLY'; + props.hideLabel = true; + const { getByTestId } = render(); + const fieldValueList = getByTestId('mock-field-value-list'); + expect(fieldValueList).toBeVisible(); + expect(fieldValueList.textContent).toBe(': Hidden Label'); + }); + + test('FieldValueList hides label in STACKED_LARGE_VAL mode when hideLabel is true', () => { + const props = getDefaultProps(); + props.value = 'Stacked Hidden'; + props.displayMode = 'STACKED_LARGE_VAL'; + props.hideLabel = true; + const { getByTestId } = render(); + const fieldValueList = getByTestId('mock-field-value-list'); + expect(fieldValueList).toBeVisible(); + expect(fieldValueList.textContent).toBe(': Stacked Hidden'); + expect(fieldValueList).toHaveAttribute('data-variant', 'stacked'); + }); +}); diff --git a/packages/react-sdk-components/tests/unit/components/forms/TextInput/index.test.tsx b/packages/react-sdk-components/tests/unit/components/forms/TextInput/index.test.tsx deleted file mode 100644 index 9ef342e6..00000000 --- a/packages/react-sdk-components/tests/unit/components/forms/TextInput/index.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React from 'react'; -import { render, fireEvent } from '@testing-library/react'; -import TextInput from '../../../../../src/components/field/TextInput'; - -const onBlur = jest.fn(); -const onChange = jest.fn(); -const getDefaultProps = (): any => { - return { - required: true, - testId: 'textInputTestId', - label: 'TextInput', - displayMode: false, - onChange, - onBlur - }; -}; - -describe('Test Text Input component', () => { - test('TextInput Component renders with required', () => { - const props = getDefaultProps(); - const TextInputComponent = render(); - expect(TextInputComponent.getByTestId('textInputTestId')).toHaveAttribute('required'); - - props.required = false; - TextInputComponent.rerender(); - expect(TextInputComponent.getByTestId('textInputTestId')).not.toHaveAttribute('required'); - }); - - test('TextInput Component renders with disabled', () => { - const props = getDefaultProps(); - props.disabled = true; - const TextInputComponent = render(); - expect(TextInputComponent.getByTestId('textInputTestId')).toHaveAttribute('disabled'); - - props.disabled = false; - TextInputComponent.rerender(); - expect(TextInputComponent.getByTestId('textInputTestId')).not.toHaveAttribute('disabled'); - }); - - test('TextInput Component renders with readonly', () => { - const props = getDefaultProps(); - props.readOnly = true; - const TextInputComponent = render(); - expect(TextInputComponent.getByTestId('textInputTestId')).toHaveAttribute('readonly'); - - props.readOnly = false; - TextInputComponent.rerender(); - expect(TextInputComponent.getByTestId('textInputTestId')).not.toHaveAttribute('readonly'); - }); - - test('TextInput Component renders with label', () => { - const props = getDefaultProps(); - const { getByText } = render(); - const labelDiv = getByText('TextInput'); - expect(labelDiv).toBeVisible(); - }); - - test('TextInput Component renders with displayMode as LabelsLeft', () => { - const props = getDefaultProps(); - props.value = 'Hi there!'; - props.displayMode = 'DISPLAY_ONLY'; - const { getByText } = render(); - const readOnlySpan = getByText('Hi there!'); - expect(readOnlySpan).toBeVisible(); - }); - - test('TextInput Component invoke handlers for blur and change events', () => { - const props = getDefaultProps(); - const TextInputComponent = render(); - fireEvent.change(TextInputComponent.getByTestId('textInputTestId'), { - target: { value: 'a' } - }); - fireEvent.blur(TextInputComponent.getByTestId('textInputTestId')); - expect(onChange).toHaveBeenCalled(); - expect(onBlur).toHaveBeenCalled(); - }); - - test('TextInput Component should not invoke on blur handler for read only fields', () => { - const props = getDefaultProps(); - props.readOnly = true; - const TextInputComponent = render(); - fireEvent.change(TextInputComponent.getByTestId('textInputTestId'), { - target: { value: 'a' } - }); - fireEvent.blur(TextInputComponent.getByTestId('textInputTestId')); - expect(onBlur).toHaveBeenCalled(); - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 826baf63..34e6c6a2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,8 @@ // Copied from SDK-R - JEA "experimentalDecorators": true, "strict": true, + "verbatimModuleSyntax": false, + "esModuleInterop": true, "sourceMap": true, "module": "ES2022", "target": "ES2022", @@ -24,7 +26,7 @@ // Override with only specific types for other configurations. "typeRoots": ["node_modules/@types", "node_modules/@pega"], - "types": ["jest", "@testing-library/jest-dom", "pcore-pconnect-typedefs"], + "types": ["jest", "@testing-library/jest-dom", "pcore-pconnect-typedefs", "node"], "noEmit": false, // JEA - was false "incremental": true,