@pega/custom-dx-components 26.1.11 → 26.1.12-alpha.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/package.json +48 -64
- package/src/ai/README.md +44 -0
- package/src/ai/catalog/README.md +133 -0
- package/src/ai/catalog/best-practices/README.md +24 -0
- package/src/ai/catalog/best-practices/accessibility-for-async-components.md +53 -0
- package/src/ai/catalog/best-practices/error-handling-and-resiliency.md +116 -0
- package/src/ai/catalog/best-practices/genai-assistant-integration-patterns.md +49 -0
- package/src/ai/catalog/best-practices/performance-and-efficiency.md +109 -0
- package/src/ai/catalog/best-practices/security-considerations.md +93 -0
- package/src/ai/catalog/best-practices/testing-pconnect-components.md +245 -0
- package/src/ai/catalog/best-practices/troubleshooting-common-pitfalls.md +132 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/SKILL.md +44 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/README.md +39 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/approval-workflow-decision.md +47 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/attachment-upload.md +45 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/bulk-action-submit.md +50 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/confirm-before-destructive-action.md +43 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/create-and-stage-new-case.md +45 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/graceful-submit-failure.md +39 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/modal-assignment-submit.md +58 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/multi-step-wizard.md +44 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/resolve-or-ignore-duplicate.md +47 -0
- package/src/ai/catalog/pconnect-assignment-lifecycle/cookbooks/safe-case-deletion.md +36 -0
- package/src/ai/catalog/pconnect-case-collaboration/SKILL.md +42 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/README.md +36 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/look-up-user-details.md +43 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/manage-case-attachments.md +51 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/manage-case-participants.md +52 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/participant-and-follower.md +40 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/post-and-manage-feed.md +50 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/relate-cases.md +35 -0
- package/src/ai/catalog/pconnect-case-collaboration/cookbooks/tag-case-and-find-related.md +44 -0
- package/src/ai/catalog/pconnect-data-subscriptions/SKILL.md +42 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/README.md +35 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/data-page-subscription-cleanup.md +41 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/publish-and-subscribe-events.md +40 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/pubsub-cleanup.md +38 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/query-paginated-list-data.md +44 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/realtime-case-subscription.md +46 -0
- package/src/ai/catalog/pconnect-data-subscriptions/cookbooks/refresh-and-update-data.md +41 -0
- package/src/ai/catalog/pconnect-forms-and-fields/SKILL.md +44 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/README.md +36 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/autosave-tracking.md +45 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/cascading-dependent-fields.md +51 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/collect-form-changes-for-manual-save.md +45 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/custom-field-validators.md +44 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/discard-changes-and-reset-form.md +41 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/full-form-validation.md +38 -0
- package/src/ai/catalog/pconnect-forms-and-fields/cookbooks/persist-widget-ui-state.md +41 -0
- package/src/ai/catalog/pconnect-lists-and-views/SKILL.md +44 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/README.md +35 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/activate-and-switch-container-items.md +52 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/container-item-lifecycle.md +61 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/insert-delete-list-rows.md +40 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/reorder-list-rows.md +39 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/repeating-row-edit.md +39 -0
- package/src/ai/catalog/pconnect-lists-and-views/cookbooks/view-personalization.md +60 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/SKILL.md +45 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/README.md +35 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/agent-conversation-for-tasks.md +54 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/bookmarkable-filter-state.md +40 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/deep-link-semantic-url.md +39 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/genai-assistant-conversation.md +43 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/locale-switch-and-relocalize.md +38 -0
- package/src/ai/catalog/pconnect-navigation-locale-genai/cookbooks/persist-operator-preferences.md +40 -0
- package/src/ai/catalog/references/architecture-overview.md +52 -0
- package/src/ai/catalog/references/pconnect-actions-api.md +948 -0
- package/src/ai/catalog/references/pconnect-case-info-data-info-localization.md +618 -0
- package/src/ai/catalog/references/pconnect-case-validation-utilities.md +238 -0
- package/src/ai/catalog/references/pconnect-container-manager.md +215 -0
- package/src/ai/catalog/references/pconnect-list-actions.md +250 -0
- package/src/ai/catalog/references/pconnect-misc-suggestions-creation.md +231 -0
- package/src/ai/catalog/references/pconnect-reference-context-component.md +471 -0
- package/src/ai/catalog/references/pconnect-state-config.md +316 -0
- package/src/ai/catalog/references/pconnect-validation-api.md +45 -0
- package/src/ai/catalog/references/pcore-analytics-genai-details.md +50 -0
- package/src/ai/catalog/references/pcore-bootstrap-init-details.md +121 -0
- package/src/ai/catalog/references/pcore-bootstrap-registry-routing.md +253 -0
- package/src/ai/catalog/references/pcore-case-collaboration-details.md +191 -0
- package/src/ai/catalog/references/pcore-case-collaboration-genai.md +264 -0
- package/src/ai/catalog/references/pcore-environment-info-details.md +96 -0
- package/src/ai/catalog/references/pcore-environment-locale-namespace.md +175 -0
- package/src/ai/catalog/references/pcore-messaging-details.md +99 -0
- package/src/ai/catalog/references/pcore-misc-details.md +207 -0
- package/src/ai/catalog/references/pcore-misc-utilities.md +325 -0
- package/src/ai/catalog/references/pcore-rest-data-details.md +164 -0
- package/src/ai/catalog/references/pcore-rest-data-messaging.md +308 -0
- package/src/ai/catalog/references/pcore-store-context-details.md +40 -0
- package/src/ai/catalog/references/pcore-store-context-state.md +130 -0
- package/src/ai/index.js +154 -0
- package/src/ai/project/.claude/skills/boolean/SKILL.md +43 -0
- package/src/ai/project/.claude/skills/case/SKILL.md +58 -0
- package/src/ai/project/.claude/skills/create-dx-component/SKILL.md +98 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/PConnProps.d.ts +24 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/create-nonce.ts +5 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/event-utils.ts +18 -0
- package/src/ai/project/.claude/skills/create-dx-component/assets/styles.ts +7 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/README.md +28 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/build-a-data-widget.md +281 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/field-with-limits-and-validation.md +156 -0
- package/src/ai/project/.claude/skills/create-dx-component/cookbooks/status-badge-field.md +97 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/accessibility.md +71 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/choosing-type-and-subtype.md +79 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/cli-workflow.md +125 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/component-patterns.md +241 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/config-schema.md +296 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/cosmos-components.md +137 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/field-behavior.md +155 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/field-essentials.md +90 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/field-subtypes-extra.md +68 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/localization.md +30 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/pcore-pconnect-apis.md +77 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/presentation-essentials.md +57 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/props-contract.md +182 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/state-matrix.md +102 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/storybook-pattern.md +262 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/styles-pattern.md +54 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/template-essentials.md +87 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/template-patterns.md +131 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/test-pattern.md +215 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/ui-gallery-reuse.md +93 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/widget-essentials.md +84 -0
- package/src/ai/project/.claude/skills/create-dx-component/references/widget-patterns.md +164 -0
- package/src/ai/project/.claude/skills/currency/SKILL.md +46 -0
- package/src/ai/project/.claude/skills/date/SKILL.md +46 -0
- package/src/ai/project/.claude/skills/decimal/SKILL.md +49 -0
- package/src/ai/project/.claude/skills/details/SKILL.md +60 -0
- package/src/ai/project/.claude/skills/email/SKILL.md +42 -0
- package/src/ai/project/.claude/skills/field/SKILL.md +68 -0
- package/src/ai/project/.claude/skills/form/SKILL.md +51 -0
- package/src/ai/project/.claude/skills/generate-component-set/SKILL.md +80 -0
- package/src/ai/project/.claude/skills/generate-component-set/component-manifest.json +161 -0
- package/src/ai/project/.claude/skills/identify-components/SKILL.md +76 -0
- package/src/ai/project/.claude/skills/integer/SKILL.md +45 -0
- package/src/ai/project/.claude/skills/page/SKILL.md +60 -0
- package/src/ai/project/.claude/skills/page-widget/SKILL.md +55 -0
- package/src/ai/project/.claude/skills/percentage/SKILL.md +52 -0
- package/src/ai/project/.claude/skills/phone/SKILL.md +44 -0
- package/src/ai/project/.claude/skills/picklist/SKILL.md +50 -0
- package/src/ai/project/.claude/skills/plan-dx-component/SKILL.md +51 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/design-system-mapping.md +108 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/intake.md +146 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/plan-template.md +76 -0
- package/src/ai/project/.claude/skills/plan-dx-component/references/requirements-template.md +88 -0
- package/src/ai/project/.claude/skills/presentation/SKILL.md +61 -0
- package/src/ai/project/.claude/skills/publish-library/SKILL.md +106 -0
- package/src/ai/project/.claude/skills/radiobuttons/SKILL.md +51 -0
- package/src/ai/project/.claude/skills/review-component/SKILL.md +49 -0
- package/src/ai/project/.claude/skills/review-component/references/checklist.md +77 -0
- package/src/ai/project/.claude/skills/template/SKILL.md +60 -0
- package/src/ai/project/.claude/skills/text/SKILL.md +45 -0
- package/src/ai/project/.claude/skills/update-dx-component/SKILL.md +56 -0
- package/src/ai/project/.claude/skills/update-dx-component/references/maintenance.md +43 -0
- package/src/ai/project/.claude/skills/url/SKILL.md +46 -0
- package/src/ai/project/.claude/skills/validate-component/SKILL.md +78 -0
- package/src/ai/project/.claude/skills/validate-component/references/runtime-troubleshooting.md +56 -0
- package/src/ai/project/.claude/skills/validate-component/scripts/check-component.mjs +327 -0
- package/src/ai/project/.claude/skills/widget/SKILL.md +59 -0
- package/src/ai/project/.gemini/settings.json +5 -0
- package/src/ai/project/AGENTS.md +74 -0
- package/src/ai/readme-section.md +91 -0
- package/src/cli.js +48 -7
- package/src/constants.js +54 -53
- package/src/internal-tests.js +27 -0
- package/src/tasks/ai-toggle/index.js +46 -0
- package/src/tasks/authenticate/helper.js +38 -38
- package/src/tasks/authenticate/index.js +17 -15
- package/src/tasks/build-all/helper.js +4 -4
- package/src/tasks/build-all/index.js +7 -7
- package/src/tasks/build-comp/helper.js +17 -17
- package/src/tasks/build-comp/index.js +10 -10
- package/src/tasks/build-jest-assets/index.js +7 -9
- package/src/tasks/build-lib/helper.js +8 -8
- package/src/tasks/build-lib/index.js +8 -8
- package/src/tasks/bundle/builder/index.js +9 -12
- package/src/tasks/bundle/builder/modules.js +8 -9
- package/src/tasks/bundle/builder/paths.js +2 -19
- package/src/tasks/bundle/builder/webpack.config.js +60 -77
- package/src/tasks/bundle/index.js +8 -8
- package/src/tasks/create/index.js +35 -35
- package/src/tasks/create/templates/Constellation/Field/Boolean/index.tsx.mustache +1 -36
- package/src/tasks/create/templates/Constellation/Field/Date/date.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/Date/index.tsx.mustache +2 -27
- package/src/tasks/create/templates/Constellation/Field/DateTime/date-utils.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/DateTime/date.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/DateTime/index.tsx.mustache +2 -35
- package/src/tasks/create/templates/Constellation/Field/Decimal/index.tsx.mustache +3 -36
- package/src/tasks/create/templates/Constellation/Field/Decimal-Currency/index.tsx.mustache +4 -39
- package/src/tasks/create/templates/Constellation/Field/Decimal-Percentage/index.tsx.mustache +4 -53
- package/src/tasks/create/templates/Constellation/Field/Icon-Button-URL/index.tsx.mustache +5 -5
- package/src/tasks/create/templates/Constellation/Field/Integer/index.tsx.mustache +3 -36
- package/src/tasks/create/templates/Constellation/Field/Picklist/index.tsx.mustache +7 -42
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/component-constants.ts +1 -1
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/date.ts.mustache +0 -2
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/embed-utils.ts.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/index.tsx.mustache +11 -11
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/repeat-utils.ts.mustache +3 -13
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/textformat-utils.tsx.mustache +2 -3
- package/src/tasks/create/templates/Constellation/Field/Picklist-Autocomplete/utils.ts.mustache +28 -28
- package/src/tasks/create/templates/Constellation/Field/Picklist-RadioButtons/index.tsx.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Field/Picklist-RadioButtons/textformat-utils.tsx.mustache +2 -3
- package/src/tasks/create/templates/Constellation/Field/Text/index.tsx.mustache +4 -33
- package/src/tasks/create/templates/Constellation/Field/Text-Email/index.tsx.mustache +3 -33
- package/src/tasks/create/templates/Constellation/Field/Text-Input/index.tsx.mustache +3 -31
- package/src/tasks/create/templates/Constellation/Field/Text-Paragraph/index.tsx.mustache +3 -31
- package/src/tasks/create/templates/Constellation/Field/Text-Phone/index.tsx.mustache +3 -35
- package/src/tasks/create/templates/Constellation/Field/Text-URL/index.tsx.mustache +3 -33
- package/src/tasks/create/templates/Constellation/Field/Text-URL/text-url.tsx.mustache +2 -3
- package/src/tasks/create/templates/Constellation/Field/TimeofDay/index.tsx.mustache +2 -35
- package/src/tasks/create/templates/Constellation/Field/TimeofDay/time-of-day.ts.mustache +2 -2
- package/src/tasks/create/templates/Constellation/Template/DETAILS/Avatar.tsx.mustache +19 -15
- package/src/tasks/create/templates/Constellation/Template/DETAILS/Operator.tsx.mustache +6 -6
- package/src/tasks/create/templates/Constellation/Template/DETAILS/demo.stories.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Template/DETAILS/index.tsx.mustache +4 -8
- package/src/tasks/create/templates/Constellation/Template/DETAILS/mock.ts.mustache +9 -9
- package/src/tasks/create/templates/Constellation/Template/DETAILS/utils.js.mustache +4 -8
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/Avatar.tsx.mustache +19 -15
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/Operator.tsx.mustache +6 -6
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/demo.stories.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/index.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/mock.ts.mustache +9 -9
- package/src/tasks/create/templates/Constellation/Template/DETAILS-region/utils.ts.mustache +4 -8
- package/src/tasks/create/templates/Constellation/Template/FORM/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Constellation/Template/FORM-region/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Constellation/Template/PAGE/utils.ts.mustache +4 -9
- package/src/tasks/create/templates/Constellation/Widget/CASE/demo.test.tsx.mustache +3 -3
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/Avatar.tsx.mustache +19 -14
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/Operator.tsx.mustache +6 -6
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/index.tsx.mustache +1 -1
- package/src/tasks/create/templates/Constellation/Widget/PAGE-CASE/mock.ts.mustache +1 -1
- package/src/tasks/create/templates/Launchpad/Field/Icon-Button-URL/index.tsx.mustache +5 -5
- package/src/tasks/create/templates/Launchpad/Field/Text-Phone/index.tsx.mustache +3 -35
- package/src/tasks/create/templates/Launchpad/Field/__components.md +1 -1
- package/src/tasks/create/templates/Launchpad/Template/FORM/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Launchpad/Template/FORM-region/demo.stories.tsx.mustache +8 -8
- package/src/tasks/create/templates/Launchpad/Widget/CASE/demo.test.tsx.mustache +3 -3
- package/src/tasks/create/templates/Launchpad/Widget/PAGE-CASE/index.tsx.mustache +1 -1
- package/src/tasks/create/templates/Launchpad/Widget/PAGE-CASE/mock.ts.mustache +1 -1
- package/src/tasks/create-all/index.js +42 -41
- package/src/tasks/create-lib/helper.js +13 -13
- package/src/tasks/create-lib/index.js +5 -5
- package/src/tasks/create-version/helper.js +10 -10
- package/src/tasks/create-version/index.js +3 -4
- package/src/tasks/delete/helper.js +20 -19
- package/src/tasks/delete/index.js +2 -2
- package/src/tasks/delete-all/helper.js +26 -25
- package/src/tasks/delete-all/index.js +2 -2
- package/src/tasks/delete-lib/helper.js +6 -6
- package/src/tasks/delete-lib/index.js +6 -6
- package/src/tasks/delete-pub/helper.js +18 -18
- package/src/tasks/delete-pub/index.js +8 -8
- package/src/tasks/delete-version/index.js +2 -2
- package/src/tasks/dev-server/index.js +1 -1
- package/src/tasks/export-archive/helper.js +21 -21
- package/src/tasks/export-archive/index.js +27 -40
- package/src/tasks/export-rc/helper.js +34 -34
- package/src/tasks/export-rc/index.js +21 -31
- package/src/tasks/import-archive/helper.js +39 -39
- package/src/tasks/import-archive/index.js +69 -89
- package/src/tasks/import-comp/helper.js +8 -8
- package/src/tasks/import-comp/index.js +30 -42
- package/src/tasks/index.js +5 -0
- package/src/tasks/init/Node.gitignore +149 -0
- package/src/tasks/init/helper.js +15 -8
- package/src/tasks/init/index.js +115 -35
- package/src/tasks/init/template/.prettierrc.json +4 -1
- package/src/tasks/init/template/.storybook/main.ts +1 -4
- package/src/tasks/init/template/.storybook/manager.ts +1 -1
- package/src/tasks/init/template/.storybook/preview.tsx +2 -2
- package/src/tasks/init/template/.storybook/theme.js +1 -1
- package/src/tasks/init/template/.vscode/extensions.json +0 -1
- package/src/tasks/init/template/README.md +2 -2
- package/src/tasks/init/template/eslint.config.mjs +13 -155
- package/src/tasks/init/template/jest.config.cjs +2 -0
- package/src/tasks/init/template/package-lock.json +26864 -0
- package/src/tasks/init/template/package.json.mustache +19 -74
- package/src/tasks/init/template/setupTests.ts +10 -4
- package/src/tasks/init/template/tsconfig.json +14 -6
- package/src/tasks/internal-test/index.js +21 -0
- package/src/tasks/internal-test-full/index.js +15 -0
- package/src/tasks/internal-test-functional/index.js +15 -0
- package/src/tasks/internal-test-unit/index.js +13 -0
- package/src/tasks/linter/index.js +2 -2
- package/src/tasks/list/helper.js +41 -40
- package/src/tasks/list/index.js +3 -3
- package/src/tasks/list-lib/helper.js +5 -5
- package/src/tasks/list-lib/index.js +8 -8
- package/src/tasks/list-pub/helper.js +23 -16
- package/src/tasks/list-pub/index.js +24 -21
- package/src/tasks/move/helper.js +8 -8
- package/src/tasks/move/index.js +19 -23
- package/src/tasks/promote-webpack/index.js +4 -4
- package/src/tasks/promote-webpack/webpack.config.js +62 -93
- package/src/tasks/publish/helper.js +21 -21
- package/src/tasks/publish/index.js +16 -16
- package/src/tasks/publish-all/helper.js +20 -20
- package/src/tasks/publish-all/index.js +11 -12
- package/src/tasks/publish-archive/helper.js +9 -9
- package/src/tasks/publish-archive/index.js +12 -14
- package/src/tasks/publish-lib/helper.js +3 -3
- package/src/tasks/publish-lib/index.js +28 -34
- package/src/tasks/quarantine/index.js +8 -8
- package/src/tasks/rename/index.js +26 -30
- package/src/tasks/rename-lib/index.js +21 -25
- package/src/tasks/restore/index.js +8 -8
- package/src/tasks/restore-config/index.js +3 -5
- package/src/tasks/set-perm/index.js +8 -8
- package/src/tasks/show-help/index.js +17 -9
- package/src/tasks/show-status/index.js +2 -2
- package/src/tasks/store/index.js +8 -8
- package/src/tasks/switch-lib/helper.js +7 -7
- package/src/tasks/switch-lib/index.js +7 -7
- package/src/tasks/switch-version/helper.js +6 -6
- package/src/tasks/switch-version/index.js +7 -7
- package/src/tasks/update/index.js +1 -1
- package/src/tasks/upgrade/index.js +7 -7
- package/src/tasks/validator/index.js +49 -203
- package/src/util.js +169 -186
- package/src/rules/no-pcore-pconnect-in-presentation.js +0 -103
- package/src/tasks/init/template/.stylelintrc.json +0 -4
- package/src/tasks/init/template/babel.config.cjs +0 -22
- package/src/tasks/init/template/cspell.json +0 -14
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: GenAI Assistant Integration Patterns
|
|
3
|
+
description: Conversation lifecycle, state rehydration, streaming, feedback, and safety-gate patterns for building custom components on top of GenAIAssistantUtils and GenAIUtils.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# GenAI Assistant Integration Patterns
|
|
7
|
+
|
|
8
|
+
> Part of [Cross-Cutting Best Practices](./README.md).
|
|
9
|
+
|
|
10
|
+
`GenAIAssistantUtils` and `GenAIUtils` (see [pcore-analytics-genai-details.md](../references/pcore-analytics-genai-details.md) and [pcore-case-collaboration-genai.md](../references/pcore-case-collaboration-genai.md)) let a custom component start and continue a conversation with a GenAI Assistant or Agent, and rewrite text. This guide covers the integration patterns specific to these APIs — for content-safety concerns (prompt injection, sanitizing model output), see [Security Considerations](./security-considerations.md); for the general async-state and mocking patterns, see [Testing](./testing-pconnect-components.md#testing-genai-assistant-conversations) and [Error Handling & Resiliency](./error-handling-and-resiliency.md).
|
|
11
|
+
|
|
12
|
+
## Gate on `isGenAIEnabled()` Before Rendering Any GenAI Affordance
|
|
13
|
+
|
|
14
|
+
Not every environment has GenAI enabled. Check `PCore.getEnvironmentInfo().isGenAIEnabled()` before rendering a GenAI-dependent button, panel, or suggestion — and treat "disabled" as an expected, silent state (simply don't render the affordance), not an error condition to surface to the user:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
const genAIEnabled = PCore.getEnvironmentInfo().isGenAIEnabled();
|
|
18
|
+
|
|
19
|
+
if (!genAIEnabled) return null; // no GenAI affordance in this environment — not an error
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Rehydrate Conversation History Instead of Trusting Local State Alone
|
|
23
|
+
|
|
24
|
+
`createConversation`/`createConversationForAgent` start a conversation that's tracked server-side; `getMessages`/`getMessagesForAgent` and the paginated `getConversations`/`getConversationsForAgent` retrieve what's already there. Local component state built up as the user sends/receives messages in the current session is a convenient cache, but it is not the source of truth — on mount (or when reopening a previously started conversation), call `getMessages(assistantID, conversationID, context)` to rehydrate rather than assuming a fresh component instance starts with no history (see [Troubleshooting: a GenAI Assistant conversation's history is empty after a page reload](./troubleshooting-common-pitfalls.md#a-genai-assistant-conversations-history-is-empty-after-a-page-reload)).
|
|
25
|
+
|
|
26
|
+
## Render Incrementally When `enableStreaming` Is Used
|
|
27
|
+
|
|
28
|
+
`sendMessage` supports an `enableStreaming` option for a more responsive, incremental reply rather than waiting for the full response. If a component uses streaming, render the partial response as it arrives (with its own visible "in progress" indicator distinct from a fully-loaded message) rather than buffering the whole thing client-side and only rendering once — buffering defeats the purpose of requesting streaming in the first place and leaves the user staring at nothing for longer than necessary.
|
|
29
|
+
|
|
30
|
+
## Handle `GenAIUtils.rewriteText`'s Discriminated Result Explicitly
|
|
31
|
+
|
|
32
|
+
`rewriteText({ originalText, rewriteAction })` resolves an object shaped as either `{ success: true, rewrittenText }` or a failure shape — it does not reject on a model-side failure the way a network error would. Check `result.success` explicitly and handle both branches (see [Testing: testing GenAI Assistant conversations](./testing-pconnect-components.md#testing-genai-assistant-conversations)); a component that only reads `result.rewrittenText` without checking `success` first will render `undefined` on failure instead of a clear fallback message.
|
|
33
|
+
|
|
34
|
+
## Use the Like/Unlike Feedback Loop, Don't Build a Parallel One
|
|
35
|
+
|
|
36
|
+
`likeAgentMessage`/`unLikeAgentMessage(agentID, conversationID, messageID, context, feedbackText?)` are the documented mechanism for capturing a user's reaction to an Agent's message, including optional free-text feedback on removal of a like. Route thumbs-up/thumbs-down UI through these calls rather than inventing a separate feedback-collection mechanism — this keeps feedback attached to the same conversation/message record the platform already tracks, rather than living in a disconnected system nobody correlates back to the conversation.
|
|
37
|
+
|
|
38
|
+
## Treat a Long-Running Conversation Call Like Any Other Async Operation
|
|
39
|
+
|
|
40
|
+
Nothing about `sendMessage`/`sendMessageForAgent` exempts it from the general patterns elsewhere in this folder: it's a `Promise` that can reject (see [Error Handling & Resiliency](./error-handling-and-resiliency.md)), a user can trigger a new message before a previous one resolves (consider whether to disable the input or track/cancel the stale one, per [Performance & Efficiency: cancel requests that are no longer needed](./performance-and-efficiency.md#cancel-requests-that-are-no-longer-needed)), and its pending/success/error states need the same live-region treatment as any other async UI (see [Accessibility for Async, API-Driven Components](./accessibility-for-async-components.md)).
|
|
41
|
+
|
|
42
|
+
## Checklist
|
|
43
|
+
|
|
44
|
+
- Check `PCore.getEnvironmentInfo().isGenAIEnabled()` before rendering any GenAI-dependent UI; treat "disabled" as a silent no-render, not an error.
|
|
45
|
+
- Rehydrate conversation history via `getMessages`/`getConversations` on mount rather than relying only on in-session local state.
|
|
46
|
+
- Render streamed responses incrementally, not buffered until fully complete, when `enableStreaming` is used.
|
|
47
|
+
- Explicitly branch on `rewriteText`'s `success` field rather than assuming the happy-path shape.
|
|
48
|
+
- Route message feedback through `likeAgentMessage`/`unLikeAgentMessage` instead of a separate, disconnected mechanism.
|
|
49
|
+
- Apply the same async-state, cancellation, and accessibility patterns used elsewhere in these guides to conversation calls — they aren't a special case.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Performance & Efficiency
|
|
3
|
+
description: Debouncing, buffering, caching/invalidation, request cancellation, and subscription hygiene patterns for components that call PCore/PConnect APIs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Performance & Efficiency
|
|
7
|
+
|
|
8
|
+
> Part of [Cross-Cutting Best Practices](./README.md).
|
|
9
|
+
|
|
10
|
+
Most PCore/PConnect calls cross a network boundary, and several return values the framework already caches for you. The patterns below keep a custom component from doing unnecessary work — its own or the server's.
|
|
11
|
+
|
|
12
|
+
## Debounce or Buffer User-Triggered Calls
|
|
13
|
+
|
|
14
|
+
A field wired directly to `onChange` → REST/data-page call fires once per keystroke, which wastes network round trips and can make results arrive out of order (see [Troubleshooting: an older async response overwrites a newer one](./troubleshooting-common-pitfalls.md#an-older-async-response-overwrites-a-newer-one-a-fast-typed-search-shows-results-for-an-earlier-query)). Two documented options:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
// Option 1: a debounced Observable — react only after input goes quiet.
|
|
18
|
+
const debouncedSubject = PCore.getAsynchronousUtils().getDebouncedSubject(250);
|
|
19
|
+
const subscription = debouncedSubject.subscribe((value) => runSearch(value));
|
|
20
|
+
// ...on each keystroke: debouncedSubject.next(currentValue);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// Option 2: buffered/batched fetches built into a live DataApi instance (typeahead-style fields).
|
|
25
|
+
const dataApiInstance = await PCore.getDataApi().init(dataConfig, contextName);
|
|
26
|
+
dataApiInstance.registerForBufferedCall({ waitTime: 50 });
|
|
27
|
+
const results = await dataApiInstance.fetchData(searchTerm);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
See [pcore-misc-details.md](../references/pcore-misc-details.md) and [pcore-rest-data-details.md](../references/pcore-rest-data-details.md) for full signatures.
|
|
31
|
+
|
|
32
|
+
## Cache Data Pages, But Invalidate Deliberately
|
|
33
|
+
|
|
34
|
+
Data page reads (`DataPageUtils.getDataAsync`/`getPageDataAsync`, `DataApi.init`) are cached by the framework — calling them again with the same parameters typically reuses the cached result instead of re-fetching. That's a performance win, but it means a stale read is a common bug after a write:
|
|
35
|
+
|
|
36
|
+
- After a save/update that should be reflected on next read, call `DataPageUtils.markDataInstanceDirty(dataPageName, parameters)` or re-fetch with `{ invalidateCache: true }`.
|
|
37
|
+
- When a `DataApi` instance is scoped to a context that's going away (e.g. a container item closing), call `DataApi.clearContextedCache(context)` so the next open of that context doesn't reuse a stale cached instance.
|
|
38
|
+
- For small, component-local values (not full data page results), `NavigationUtils.getComponentCache`/`setComponentCache`/`resetComponentCache` (see [pcore-misc-details.md](../references/pcore-misc-details.md)) offer a lighter-weight, shorter-lived cache than a data page round trip.
|
|
39
|
+
|
|
40
|
+
Caching is only a net win if invalidation is deliberate — don't cache a value and then forget which write paths should invalidate it.
|
|
41
|
+
|
|
42
|
+
## Cancel Requests That Are No Longer Needed
|
|
43
|
+
|
|
44
|
+
If a component can trigger a new request before a previous one resolves (a search box, a fast-changing filter, an unmounting component), cancel the stale request instead of letting it complete and race the newer one:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
const { invokeRestApi, getCancelTokenSource, isRequestCanceled } = PCore.getRestClient();
|
|
48
|
+
const cancelTokenSource = getCancelTokenSource();
|
|
49
|
+
|
|
50
|
+
invokeRestApi(routeKey, { ...payload, cancelTokenSource }).catch((error) => {
|
|
51
|
+
if (isRequestCanceled(error)) return; // expected — a newer request superseded this one
|
|
52
|
+
handleRealError(error);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// When a newer request starts, or the component unmounts:
|
|
56
|
+
cancelTokenSource.cancel();
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This isn't only about correctness (see [Error Handling & Resiliency: distinguish cancellation from failure](./error-handling-and-resiliency.md#distinguish-cancellation-from-failure)) — it also frees the response payload the server would otherwise still compute and send for a result nobody will use.
|
|
60
|
+
|
|
61
|
+
## Avoid Redundant Subscriptions
|
|
62
|
+
|
|
63
|
+
If multiple sibling components need the same data page or the same pub/sub event, prefer a single subscription owned by a shared ancestor/context over each sibling independently calling `subscribeToDataPageUpdates`/`PubSubUtils.subscribe`. Duplicate subscriptions mean duplicate network/server work for identical data, and duplicate cleanup surface area for the same bug class described in the troubleshooting guide's [subscription cleanup entry](./troubleshooting-common-pitfalls.md#subscription-callback-keeps-firing-after-the-component-unmounts).
|
|
64
|
+
|
|
65
|
+
## Minimize Store Reads in the Render Path
|
|
66
|
+
|
|
67
|
+
`StateUtils.getSharedState`/`ContextTreeManager.getFieldsList` (see [pcore-store-context-details.md](../references/pcore-store-context-details.md)) can return large structures. Read only the narrow piece of state a component actually renders, rather than pulling a broad object and destructuring it afterward — a narrower read makes it easier to reason about what a re-render actually depends on, and avoids re-rendering on changes to sibling state the component never displays.
|
|
68
|
+
|
|
69
|
+
## Batch Related List Operations
|
|
70
|
+
|
|
71
|
+
When applying the same change to many rows of a repeating structure, prefer `ListActions`' batch-capable operations (see [pconnect-list-actions.md](../references/pconnect-list-actions.md)) over looping and calling a single-row update once per row — a loop of individual calls multiplies both client-side dispatches and, depending on the operation, server round trips.
|
|
72
|
+
|
|
73
|
+
## Parallelize Independent Calls Instead of Awaiting Them One at a Time
|
|
74
|
+
|
|
75
|
+
If a component needs the results of two or more calls that don't depend on each other's output (for example, a data page lookup and a separate `AnalyticsUtils` call), issue them together with `Promise.all` rather than `await`-ing each in sequence. Sequential awaits of independent calls create an artificial waterfall — the total wait time becomes the sum of both round trips instead of the slower of the two:
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
// Avoid: the second call doesn't start until the first one finishes, for no reason.
|
|
79
|
+
const lookupResult = await PCore.getDataPageUtils().getDataAsync(dataPage, contextName);
|
|
80
|
+
const insightIDs = await PCore.getAnalyticsUtils().getInsightIDs();
|
|
81
|
+
|
|
82
|
+
// Prefer: both requests are in flight at the same time.
|
|
83
|
+
const [lookupResult, insightIDs] = await Promise.all([
|
|
84
|
+
PCore.getDataPageUtils().getDataAsync(dataPage, contextName),
|
|
85
|
+
PCore.getAnalyticsUtils().getInsightIDs()
|
|
86
|
+
]);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Only sequence calls when the second genuinely depends on the first's resolved value (for example, using a fetched record's ID as a parameter to the next call).
|
|
90
|
+
|
|
91
|
+
## Virtualize Long Repeating Structures
|
|
92
|
+
|
|
93
|
+
Rendering every row of a large repeating structure or list view at once means the browser builds and lays out DOM nodes for rows the user can't currently see. For lists that can grow into the hundreds of rows, render only the visible window (plus a small overscan buffer) instead of the full data set — check whether the presentation component already used for the list (in `cosmos-core`/`dx-components`) has built-in virtualization support before building a custom windowing solution.
|
|
94
|
+
|
|
95
|
+
## Lazy-Load Assets That Aren't Needed on Initial Render
|
|
96
|
+
|
|
97
|
+
`AssetLoader.loadAssets(assets, options)`/`getSvcImage(key)`/`getSvcImageUrl(key)` (see [pcore-misc-details.md](../references/pcore-misc-details.md)) fetch assets on demand rather than requiring them to be bundled/loaded upfront. Prefer loading an image, icon set, or other optional asset only when the view that displays it actually renders (or becomes visible), instead of loading every asset a component might ever need at mount time — this matters most for content behind a tab, an accordion panel, or a modal that many users never open.
|
|
98
|
+
|
|
99
|
+
## Checklist
|
|
100
|
+
|
|
101
|
+
- Debounce or buffer any call wired to a fast-firing user input (typing, dragging, rapid toggles).
|
|
102
|
+
- After a write that affects a cached data page, invalidate it deliberately (`markDataInstanceDirty`/`invalidateCache`) rather than assuming the next read will be fresh.
|
|
103
|
+
- Cancel a request that a newer one has superseded, and check `isRequestCanceled` before treating its rejection as a real error.
|
|
104
|
+
- Share one subscription across siblings that need the same data instead of each subscribing independently.
|
|
105
|
+
- Read only the state slice a component renders, not a broad object it partially uses.
|
|
106
|
+
- Batch repeating-row operations instead of looping a single-row call.
|
|
107
|
+
- Run independent calls with `Promise.all` instead of awaiting them one after another.
|
|
108
|
+
- Virtualize (render only the visible window of) large repeating structures instead of every row at once.
|
|
109
|
+
- Lazy-load optional assets (`AssetLoader`) when the view that needs them renders, not unconditionally at mount.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Security Considerations
|
|
3
|
+
description: OWASP-aligned guidance for safely rendering, uploading, linking, authenticating, and logging data obtained through PCore/PConnect APIs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Security Considerations
|
|
7
|
+
|
|
8
|
+
> Part of [Cross-Cutting Best Practices](./README.md).
|
|
9
|
+
|
|
10
|
+
The APIs cataloged in these skills return content that can originate from other users, an AI model, or the current URL — none of it should be treated as trusted by default. The items below call out where that matters, organized roughly by OWASP Top 10 category.
|
|
11
|
+
|
|
12
|
+
| OWASP category | Where it applies here |
|
|
13
|
+
| --------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
14
|
+
| A01: Broken Access Control | Client-side checks as a security boundary; trusting client-computed permissions |
|
|
15
|
+
| A03: Injection (incl. XSS) | Rendering feed/comment/GenAI content; prompt injection into GenAI Assistant calls |
|
|
16
|
+
| A05: Security Misconfiguration | Manually managing auth headers/tokens instead of using the provided REST client |
|
|
17
|
+
| A08: Software and Data Integrity Failures | Trusting uploaded/downloaded attachment content without a type check |
|
|
18
|
+
| A09: Security Logging and Monitoring Failures | Logging raw error payloads or field values that may contain PII/tokens |
|
|
19
|
+
|
|
20
|
+
## Sanitize Free-Text Content Before Rendering It as HTML
|
|
21
|
+
|
|
22
|
+
Case feed posts/comments (`FeedUtils`), correspondence, and GenAI Assistant replies (`GenAIAssistantUtils.sendMessage`/`sendMessageForAgent`) are free text written by another user or generated by a model — not markup you authored. **NEVER** render this content with `dangerouslySetInnerHTML`/`innerHTML` without first sanitizing it through a library such as `dompurify`. Treat an AI-generated response exactly like user-generated content for this purpose: a model can be prompted (directly or indirectly, via injected content it summarizes) into emitting markup or script, and it is not a trusted source of pre-sanitized HTML.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import DOMPurify from 'dompurify';
|
|
26
|
+
|
|
27
|
+
const safeHtml = DOMPurify.sanitize(feedPost.message);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If the content is only ever rendered as plain text (no formatting needed), render it as text content instead of HTML and skip the sanitization step entirely — that's a stronger guarantee than sanitizing and still using an HTML sink.
|
|
31
|
+
|
|
32
|
+
## Treat Feed Content Forwarded Into a GenAI Prompt as Untrusted Input Too
|
|
33
|
+
|
|
34
|
+
If a component builds a GenAI Assistant message (`sendMessage`/`sendMessageForAgent`) that incorporates case feed text, attachment names, or other user-authored content, remember that content can itself contain text crafted to influence the model ("prompt injection") — for example, embedded instructions telling the assistant to ignore prior context. Don't concatenate raw user content directly into a system-level instruction string; keep user-authored content clearly delimited as data being discussed, not as instructions to the assistant, and don't let a model's response alone gate a subsequent privileged action (like a submit or delete) without the normal validation/confirmation flow.
|
|
35
|
+
|
|
36
|
+
## Client-Side Checks Are UX, Never the Security Boundary
|
|
37
|
+
|
|
38
|
+
Hiding a button, disabling an action, or skipping a client-side field because a role/permission check failed is a UX affordance, not access control. The server (case type stage, access control rules) is the actual authority — never assume that because a component doesn't render a "Delete" button, the corresponding `ActionsApi.deleteWork` call is unreachable, and never treat a client-computed permission flag (from cached state, a prior response, or a stale subscription) as authoritative for anything higher-stakes than what to render. Fail closed: if a permission/config value that gates a sensitive action hasn't loaded yet or failed to load, treat it as "not permitted" rather than defaulting to showing the action.
|
|
39
|
+
|
|
40
|
+
## Only Build Deep Links From a Trusted `routeKey`
|
|
41
|
+
|
|
42
|
+
When constructing a shareable link with `SemanticUrlUtils.getResolvedSemanticURL(routeKey, payload, params, locale?)` (see [deep-link-semantic-url](../pconnect-navigation-locale-genai/cookbooks/deep-link-semantic-url.md)), source `routeKey` from `PCore.getSemanticUrlUtils().getActions()` — never from a raw query string, hash fragment, or other user-controllable input. Passing an attacker-influenced value as a route key risks resolving to an unintended destination (an open-redirect-style issue) rather than the case/view you meant to link to.
|
|
43
|
+
|
|
44
|
+
## Treat Client-Side Upload Checks as UX Only, Not a Security Control
|
|
45
|
+
|
|
46
|
+
`AttachmentUtils.uploadAttachment` performs server-side size/type rejection — that is the authoritative check. A client-side file-type or size pre-check (to show a fast error before the network round trip) is fine for UX, but never rely on it as the actual security boundary: always still handle the rejection from `uploadAttachment` itself, and never assume a file "passed" validation just because the client-side check didn't flag it.
|
|
47
|
+
|
|
48
|
+
## Don't Trust Downloaded Attachment Content Either
|
|
49
|
+
|
|
50
|
+
`AttachmentUtils.downloadAttachment`/`streamDownloadAttachment` return binary content another user uploaded. If a component renders that content inline (previewing an SVG, HTML, or text file, for example) rather than triggering a normal file download, apply the same rule as feed content: an SVG can carry an embedded `<script>`, and an HTML file can carry arbitrary markup. Prefer the browser's native download/save behavior for attachment content, and if inline preview is genuinely required, sanitize it first and never render it as same-origin HTML without isolation (for example, a sandboxed `<iframe>`).
|
|
51
|
+
|
|
52
|
+
## Let the Provided REST Client Own Authentication
|
|
53
|
+
|
|
54
|
+
`RestClient` (see [pcore-rest-data-details.md](../references/pcore-rest-data-details.md)) already attaches the operator's authenticated session/auth headers to `invokeRestApi`/`invokeCustomRestApi` calls, and exposes `getHeaderProcessor()` for the supported way to add custom headers. Never manually read, store, or forward an auth token from component code (e.g. into `localStorage`, a query string, or a custom header built by hand) — that both duplicates a security-relevant responsibility the platform already owns and creates a new place a token can leak (browser storage is readable by any script on the page, and tokens in URLs end up in server access logs and browser history).
|
|
55
|
+
|
|
56
|
+
## Don't Leak Sensitive Details Through Error Handling
|
|
57
|
+
|
|
58
|
+
When a `Promise`-returning PCore/PConnect method rejects, `ErrorHandler`/`FormUtils` (see [pcore-misc-details.md](../references/pcore-misc-details.md)) let you show a message to the user and/or log the failure. Show the user a clear, actionable, generic message via `ErrorHandler.setGenericFailedMessage`/the component's own error state — never the raw rejection object, a stack trace, or an internal error code that could reveal implementation details. If you log the error for diagnostics, make sure the logged payload doesn't include field values that could contain PII or auth tokens; log identifiers (case ID, context name) instead of full field state.
|
|
59
|
+
|
|
60
|
+
## Unsubscribe When a Component Loses Access, Not Just to Avoid Leaks
|
|
61
|
+
|
|
62
|
+
`PubSubUtils.subscribe`/`MessagingServiceManager.subscribe` keep delivering events to a callback until you unsubscribe. Beyond the memory-leak concern already covered in the [data-page-subscription-cleanup](../pconnect-data-subscriptions/cookbooks/data-page-subscription-cleanup.md) and [pubsub-cleanup](../pconnect-data-subscriptions/cookbooks/pubsub-cleanup.md) recipes, unsubscribing promptly on unmount also matters from a least-privilege standpoint — a component that has navigated away from a case shouldn't keep receiving that case's pushed updates in memory for longer than it's displayed.
|
|
63
|
+
|
|
64
|
+
## Treat User-Supplied Values as Untrusted Before Passing Them as API Parameters
|
|
65
|
+
|
|
66
|
+
Search boxes, list filters, and other user-editable inputs sometimes feed directly into a data page or REST call parameter (`DataApiUtils`, `RestClient` — see [pcore-rest-data-details.md](../references/pcore-rest-data-details.md)). The server enforces access control on the resulting request, but still validate/constrain the shape of user input client-side before sending it (for example, an expected numeric ID field should not accept arbitrary text, and an expected enum value should be checked against an allow-list rather than passed through) — this reduces malformed-request noise and avoids accidentally surfacing another record's error details in a client-visible error message.
|
|
67
|
+
|
|
68
|
+
## Practice Data Minimization When Forwarding Content to a GenAI Model
|
|
69
|
+
|
|
70
|
+
`GenAIAssistantUtils.sendMessage`/`sendMessageForAgent` and `GenAIUtils.rewriteText` (see [pcore-analytics-genai-details.md](../references/pcore-analytics-genai-details.md)) send whatever text you pass to a model, which may be hosted by a third party outside the case's normal data boundary. Don't forward more of the case than the task actually needs — for example, summarizing one field's free text doesn't require serializing the entire case payload into the prompt. Passing an entire case (including fields the current user shouldn't be exfiltrating verbatim into a model's context, such as another party's contact details embedded in an unrelated field) is a data-minimization problem independent of whether the model itself is trustworthy.
|
|
71
|
+
|
|
72
|
+
## Don't Assume a Case/Assignment ID From a URL Is Valid for the Current User
|
|
73
|
+
|
|
74
|
+
A deep link, browser history entry, or bookmarked route can carry a case ID, assignment handle, or `routeKey` that the current user is no longer (or was never) authorized to open. The server-side access check on the resulting request is the real control, but the component should treat "the ID resolved to a permission/not-found error" as an expected, handled outcome — not a bug — and show a clear "you don't have access to this" or "not found" state rather than a raw error, a blank screen, or (worse) a partially-rendered view built from a previous, unrelated case still held in memory.
|
|
75
|
+
|
|
76
|
+
## Be Deliberate About What `EnvironmentInfo` Details You Surface
|
|
77
|
+
|
|
78
|
+
`PCore.getEnvironmentInfo()` (see [pcore-environment-info-details.md](../references/pcore-environment-info-details.md)) exposes details about the running application/environment. Avoid logging or otherwise surfacing its full contents in a way that ends up in client-visible debug output, error reports, or telemetry sent to a third party — prefer reading and forwarding only the specific field a feature needs (e.g. the current locale) over passing the whole object along.
|
|
79
|
+
|
|
80
|
+
## Checklist
|
|
81
|
+
|
|
82
|
+
- Sanitize (or render as plain text) any feed post, comment, or GenAI Assistant reply before putting it in an HTML sink.
|
|
83
|
+
- Never let raw user-authored content become part of a GenAI system instruction — keep it clearly framed as data, not instructions.
|
|
84
|
+
- Never treat a hidden/disabled UI element, or a client-cached permission flag, as the actual access-control boundary — fail closed on unknown/unloaded permission state.
|
|
85
|
+
- Source deep-link `routeKey` values from `getActions()`, never from user-controllable URL input.
|
|
86
|
+
- Treat client-side upload/download type checks as UX only; the server's validation is authoritative, and previewed attachment content needs the same sanitization as any other untrusted HTML.
|
|
87
|
+
- Never hand-manage auth tokens/headers — use `RestClient`'s built-in mechanisms.
|
|
88
|
+
- Show users a generic, actionable error message; log identifiers, not raw field values or tokens.
|
|
89
|
+
- Unsubscribe on unmount/navigation-away, not only to avoid leaks but to stop receiving updates a component no longer has a reason to see.
|
|
90
|
+
- Validate/allow-list user-supplied values before they become REST or data page parameters.
|
|
91
|
+
- Forward only the case content a GenAI call actually needs — don't serialize an entire case into a prompt when one field would do.
|
|
92
|
+
- Handle a permission/not-found error from a URL-sourced case or assignment ID as an expected outcome, with a clear message — not a raw error or a stale view left over from a previous case.
|
|
93
|
+
- Read only the specific `EnvironmentInfo` field a feature needs rather than logging or forwarding the whole object.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Testing Components That Use PCore/PConnect
|
|
3
|
+
description: How to mock PCore and PConnect in Jest + React Testing Library tests, covering type safety, the four API-driven UI states, cancellation, debouncing, and cleanup — without over-mocking or coupling tests to internal implementation.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Testing Components That Use PCore/PConnect
|
|
7
|
+
|
|
8
|
+
> Part of [Cross-Cutting Best Practices](./README.md).
|
|
9
|
+
|
|
10
|
+
A component that calls `PCore.*` or `pConn.*` needs those calls mocked in a unit test — `PCore` and `PConnect` are runtime globals provided by the Constellation shell, never available in a plain Jest environment. This guide covers how to mock them correctly, and how to test the behaviors that are specific to API-driven components (loading/error states, cancellation, debouncing, cleanup).
|
|
11
|
+
|
|
12
|
+
## Only Mock What the Component Actually Uses
|
|
13
|
+
|
|
14
|
+
Do not create a large, all-purpose mock of `PCore` or `pConn` shared across every test. Mock only the methods the component under test actually calls, scoped to that test file:
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
globalThis.PCore = {
|
|
19
|
+
getEnvironmentInfo: () => ({ getLocale: () => 'en-US' }),
|
|
20
|
+
getAttachmentUtils: () => ({ uploadAttachment: jest.fn() })
|
|
21
|
+
// Add more only as the component grows to call more of PCore.
|
|
22
|
+
} as unknown as typeof PCore;
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**NEVER** add `PCore`/`pConn` mocks to a shared `setupTests.ts` or `setupGlobals.ts` file. A shared global mock hides which methods a given component actually depends on, and one test's mock shape silently leaks into unrelated tests. Each test file owns its own mock.
|
|
27
|
+
|
|
28
|
+
## Mocking `pConn` (PConnect)
|
|
29
|
+
|
|
30
|
+
`pConn` is normally obtained via a `getPConnect` prop. Mock the prop function to return an object literal with only the methods the component calls:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
const mockPConnect = {
|
|
34
|
+
getValue: jest.fn().mockReturnValue(''),
|
|
35
|
+
getActionsApi: jest.fn().mockReturnValue({
|
|
36
|
+
updateFieldValue: jest.fn()
|
|
37
|
+
})
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
render(<MyComponent getPConnect={() => mockPConnect} />);
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For a method the component calls conditionally (`pConn.someMethod?.()`), still provide it in the mock when the test exercises that branch — an optional-chained call that resolves to `undefined` will silently skip the branch instead of failing, which can mask a broken test.
|
|
44
|
+
|
|
45
|
+
## Keep Mocks Type-Safe, Not `any`
|
|
46
|
+
|
|
47
|
+
A mock typed as `any` compiles no matter how far it drifts from the real API — it will happily accept a call with the wrong argument count or a typo'd method name. Cast the mock object to the real type instead of typing it as `any`, so a genuine signature mismatch is caught at compile time rather than discovered at runtime:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
const mockPConnect: Partial<PConnect> = {
|
|
51
|
+
getValue: jest.fn().mockReturnValue('')
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
render(<MyComponent getPConnect={() => mockPConnect as PConnect} />);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
This is also why a full, shared, all-purpose mock is a trap even when it's convenient: a giant object that "implements everything" tends to get typed as `any`/`unknown` to avoid maintenance overhead, which silently defeats this safety net. Small, per-test, precisely typed mocks keep the type checker useful.
|
|
58
|
+
|
|
59
|
+
## Test the Four States of an API-Driven Component
|
|
60
|
+
|
|
61
|
+
Any component whose rendered output depends on a `Promise`-returning `PCore`/`pConn` call has (at least) four distinct states worth a test each — it's easy to only cover the success path and ship the other three unverified:
|
|
62
|
+
|
|
63
|
+
| State | What to assert |
|
|
64
|
+
| ------- | -------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| Loading | A loading indicator/skeleton is shown while the promise is still pending. |
|
|
66
|
+
| Success | The resolved data renders correctly, and the loading indicator is gone. |
|
|
67
|
+
| Empty | A resolved-but-empty result (e.g. `[]` or `null`) renders an empty state, not a blank screen or a crash. |
|
|
68
|
+
| Error | A rejected promise renders a visible error state — never a silently blank component. |
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
it('renders an empty state when the data page returns no rows', async () => {
|
|
72
|
+
const getListDataView = jest.fn().mockResolvedValue({ data: [] });
|
|
73
|
+
globalThis.PCore = { getDataApiUtils: () => ({ getListDataView }) } as unknown as typeof PCore;
|
|
74
|
+
|
|
75
|
+
render(<MyListComponent getPConnect={() => mockPConnect} />);
|
|
76
|
+
|
|
77
|
+
expect(await screen.findByText(/no results/i)).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Testing Promise-Returning Methods
|
|
82
|
+
|
|
83
|
+
Most PCore/PConnect methods that hit the network return a `Promise`. Mock both the success and rejection path, and assert on the resulting UI state rather than on the mock call itself:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
it('shows an error state when the upload fails', async () => {
|
|
87
|
+
const uploadAttachment = jest.fn().mockRejectedValue(new Error('upload failed'));
|
|
88
|
+
globalThis.PCore = { getAttachmentUtils: () => ({ uploadAttachment }) } as unknown as typeof PCore;
|
|
89
|
+
|
|
90
|
+
render(<AttachmentUploader />);
|
|
91
|
+
await userEvent.upload(screen.getByLabelText(/choose file/i), someFile);
|
|
92
|
+
|
|
93
|
+
expect(await screen.findByRole('alert')).toHaveTextContent(/upload failed/i);
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Testing Cancellation-Aware Requests
|
|
98
|
+
|
|
99
|
+
If a component uses `RestClient.getCancelTokenSource()`/`isRequestCanceled()` (see [pcore-rest-data-details.md](../references/pcore-rest-data-details.md)) to abort an in-flight request — for example, when a search term changes before the previous search resolved — test that a canceled request's resolution is ignored rather than shown as an error:
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
it('ignores a canceled request instead of showing an error', async () => {
|
|
103
|
+
const cancel = jest.fn();
|
|
104
|
+
const invokeRestApi = jest.fn().mockRejectedValue(new Error('canceled'));
|
|
105
|
+
globalThis.PCore = {
|
|
106
|
+
getRestClient: () => ({
|
|
107
|
+
getCancelTokenSource: () => ({ token: {}, cancel }),
|
|
108
|
+
invokeRestApi,
|
|
109
|
+
isRequestCanceled: () => true
|
|
110
|
+
})
|
|
111
|
+
} as unknown as typeof PCore;
|
|
112
|
+
|
|
113
|
+
const { rerender } = render(<SearchField getPConnect={() => mockPConnect} query="a" />);
|
|
114
|
+
rerender(<SearchField getPConnect={() => mockPConnect} query="ab" />);
|
|
115
|
+
|
|
116
|
+
expect(cancel).toHaveBeenCalled();
|
|
117
|
+
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Testing Debounced or Buffered Input
|
|
122
|
+
|
|
123
|
+
If a component uses `AsynchronousUtils.getDebouncedSubject(delay)` or `DataApi.registerForBufferedCall({ waitTime })` (see [pcore-misc-details.md](../references/pcore-misc-details.md) and [pcore-rest-data-details.md](../references/pcore-rest-data-details.md)) to avoid firing a request on every keystroke, use fake timers to assert the call is debounced rather than fired once per keystroke:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
it('fires one lookup after typing stops, not one per keystroke', async () => {
|
|
127
|
+
jest.useFakeTimers();
|
|
128
|
+
const fetchData = jest.fn().mockResolvedValue([]);
|
|
129
|
+
// ...render, then simulate rapid input...
|
|
130
|
+
jest.advanceTimersByTime(300);
|
|
131
|
+
|
|
132
|
+
expect(fetchData).toHaveBeenCalledTimes(1);
|
|
133
|
+
jest.useRealTimers();
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Testing Subscribe/Unsubscribe Cleanup
|
|
138
|
+
|
|
139
|
+
Any component that subscribes to a data page, `PubSubUtils`, or `MessagingServiceManager` should have its unmount behavior tested — a missed unsubscribe is a real bug (see [Troubleshooting: subscription keeps firing after unmount](./troubleshooting-common-pitfalls.md#subscription-callback-keeps-firing-after-the-component-unmounts)), and it is straightforward to verify:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
it('unsubscribes on unmount', () => {
|
|
143
|
+
const subscribe = jest.fn().mockReturnValue('sub-1'); // MessagingServiceManager.subscribe returns the subscription id
|
|
144
|
+
const unsubscribe = jest.fn();
|
|
145
|
+
globalThis.PCore = { getMessagingServiceManager: () => ({ subscribe, unsubscribe }) } as unknown as typeof PCore;
|
|
146
|
+
|
|
147
|
+
const { unmount } = render(<MyComponent getPConnect={() => mockPConnect} />);
|
|
148
|
+
unmount();
|
|
149
|
+
|
|
150
|
+
expect(unsubscribe).toHaveBeenCalledWith('sub-1');
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
`PubSubUtils.subscribe` returns nothing: it unsubscribes by event type and subscriber name, so assert `unsubscribe` was called with `(eventType, name)` instead.
|
|
155
|
+
|
|
156
|
+
## Avoid Testing Implementation Details
|
|
157
|
+
|
|
158
|
+
Assert on what the user would see or experience (rendered text, roles, an emitted callback prop), not on private internals. A test that asserts "component calls `getValue` exactly once" is brittle — it breaks on a harmless refactor (e.g. reading the value once and memoizing it vs. reading it twice) without the actual behavior changing. The one legitimate exception is cleanup/lifecycle behavior (unsubscribe, cancel) where the call itself **is** the behavior under test — there, asserting the mock was called is correct, not an implementation-detail test.
|
|
159
|
+
|
|
160
|
+
## Guard Against Mock/Reality Drift
|
|
161
|
+
|
|
162
|
+
A mock that always resolves happily gives false confidence once the real method's signature or behavior changes upstream. Periodically re-check a component's mocks against the method's actual documented signature in `references/` — an outdated mock (missing a newly required parameter, or resolving a shape the real method no longer returns) can keep a test green long after the component itself is broken against the real API.
|
|
163
|
+
|
|
164
|
+
## Prefer `findBy`/`waitFor` Over Manual Timers for Async Assertions
|
|
165
|
+
|
|
166
|
+
Once a mock resolves a `Promise`, the resulting state update is asynchronous — asserting on it immediately (before the microtask queue flushes) is a common source of flaky tests. Use RTL's `findBy*` queries (which internally retry until timeout) or `waitFor` instead of an arbitrary `setTimeout`/manual `await Promise.resolve()` chain:
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Avoid: a magic delay that's either too short (flaky) or too long (slow suite).
|
|
170
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
171
|
+
expect(screen.getByText(/loaded/i)).toBeInTheDocument();
|
|
172
|
+
|
|
173
|
+
// Prefer: retries automatically until the assertion passes or times out.
|
|
174
|
+
expect(await screen.findByText(/loaded/i)).toBeInTheDocument();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
The one exception is a debounced/buffered call under fake timers (see above) — there, `jest.advanceTimersByTime` is the correct, deterministic way to advance past the delay, not a substitute for `findBy`/`waitFor` on the resulting UI update.
|
|
178
|
+
|
|
179
|
+
## Testing Locale-Dependent Rendering
|
|
180
|
+
|
|
181
|
+
If a component reads `pConn.getLocalizationService().getLocalizedText(...)`/`getLocalizedMessage(...)` (see [pcore-environment-info-details.md](../references/pcore-environment-info-details.md)) or calls `PCore.switchLocale(locale)`, mock the localization service to return a distinguishable value per key rather than echoing the key back unchanged — an identity mock (`getLocalizedText: (key) => key`) can hide a bug where the component accidentally renders the raw key instead of the resolved value in production:
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const mockPConnect = {
|
|
185
|
+
getLocalizationService: () => ({
|
|
186
|
+
getLocalizedText: (key: string) => `[localized]${key}`
|
|
187
|
+
})
|
|
188
|
+
};
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
If the component also needs to react to a locale switch (see [Troubleshooting: locale switched but labels didn't update](./troubleshooting-common-pitfalls.md#locale-switched-but-already-rendered-labels-didnt-update)), add a test that re-invokes the localization mock after simulating the switch and asserts the rendered text actually changed — not just that the switch function was called.
|
|
192
|
+
|
|
193
|
+
## Testing GenAI Assistant Conversations
|
|
194
|
+
|
|
195
|
+
`GenAIAssistantUtils.sendMessage`/`sendMessageForAgent` (see [pcore-analytics-genai-details.md](../references/pcore-analytics-genai-details.md)) resolve a `Promise`, and `GenAIUtils.rewriteText` resolves a discriminated union (`{ success: true, rewrittenText }` or `{ success: false, ... }`). Test both branches of the union explicitly — a mock that only ever resolves the success shape hides a missing/incorrect handler for the failure shape:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
it('shows a fallback message when text rewriting reports failure', async () => {
|
|
199
|
+
const rewriteText = jest.fn().mockResolvedValue({ success: false, message: 'unavailable' });
|
|
200
|
+
globalThis.PCore = {
|
|
201
|
+
getEnvironmentInfo: () => ({ isGenAIEnabled: () => true }),
|
|
202
|
+
getGenAIUtils: () => ({ rewriteText })
|
|
203
|
+
} as unknown as typeof PCore;
|
|
204
|
+
|
|
205
|
+
render(<RewriteButton getPConnect={() => mockPConnect} />);
|
|
206
|
+
await userEvent.click(screen.getByRole('button', { name: /rewrite/i }));
|
|
207
|
+
|
|
208
|
+
expect(await screen.findByText(/unavailable/i)).toBeInTheDocument();
|
|
209
|
+
});
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Also test the gate itself: when `isEnvironmentInfo().isGenAIEnabled()` returns `false`, assert the component doesn't render the GenAI-dependent affordance at all, rather than rendering it and letting the call fail.
|
|
213
|
+
|
|
214
|
+
## Build Small, Local Test Data Factories Instead of One Shared Fixture
|
|
215
|
+
|
|
216
|
+
A single large shared fixture object (for example, one exported `mockCaseData` used across many test files) tends to accumulate fields for whichever test needed them first, and every later test either depends on unrelated fields it doesn't understand or has to override several to get a clean state. Prefer a small factory function local to (or near) the test file that returns a minimal valid object, with overrides for the specific field a test cares about:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const buildCaseInfo = (overrides: Partial<CaseInfo> = {}): CaseInfo => ({
|
|
220
|
+
ID: 'WORK-1',
|
|
221
|
+
caseTypeName: 'Case',
|
|
222
|
+
status: 'Open',
|
|
223
|
+
...overrides
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('shows a resolved badge when the case is closed', () => {
|
|
227
|
+
render(<CaseStatusBadge caseInfo={buildCaseInfo({ status: 'Resolved' })} />);
|
|
228
|
+
expect(screen.getByText(/resolved/i)).toBeInTheDocument();
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Checklist
|
|
233
|
+
|
|
234
|
+
- Mock only the methods the component under test calls — no shared, all-purpose global mock.
|
|
235
|
+
- Type mocks against the real interface (`Partial<PConnect>`/`typeof PCore`) instead of `any`, so drift is caught at compile time.
|
|
236
|
+
- Cover all four states for any API-driven view: loading, success, empty, and error.
|
|
237
|
+
- If the component cancels stale requests, test that a canceled/superseded response doesn't produce a visible error.
|
|
238
|
+
- If the component debounces/buffers input, use fake timers to assert a single call, not one per keystroke.
|
|
239
|
+
- If the component subscribes to anything, add a test asserting the unsubscribe function is called on unmount.
|
|
240
|
+
- Assert on rendered output (`screen.getByRole`/`getByText`) rather than on internal state or an incidental mock call.
|
|
241
|
+
- Never make a real network call from a unit test — every `PCore`/`pConn` method the component touches must be mocked.
|
|
242
|
+
- Use `findBy*`/`waitFor` for async assertions instead of arbitrary `setTimeout` delays.
|
|
243
|
+
- Mock localization with a distinguishable value per key, not an identity echo, so a raw-key rendering bug can't hide.
|
|
244
|
+
- For a GenAI-gated affordance, test both the enabled/disabled gate and the success/failure branches of any discriminated-union result.
|
|
245
|
+
- Prefer small, local test-data factory functions over one large shared fixture that accumulates unrelated fields.
|