From abb84f1c384816fcd9b6e6921028bef25f744f04 Mon Sep 17 00:00:00 2001 From: Stephen Zhou Date: Thu, 16 Apr 2026 21:01:35 +0800 Subject: [PATCH] chore: enable noUncheckedIndexedAccess (#35178) --- .../migrate-no-unchecked-indexed-access.js | 28 + .../package.json | 20 + .../src/cli.ts | 45 + .../no-unchecked-indexed-access/migrate.ts | 1835 +++++++++++++++++ .../no-unchecked-indexed-access/normalize.ts | 51 + .../src/no-unchecked-indexed-access/run.ts | 325 +++ .../vite.config.ts | 17 + pnpm-lock.yaml | 16 + .../datasets/dataset-settings-flow.test.tsx | 2 +- .../datasets/document-management.test.tsx | 4 +- .../datasets/hit-testing-flow.test.tsx | 6 +- .../pipeline-datasource-flow.test.tsx | 8 +- web/__tests__/datasets/segment-crud.test.tsx | 2 +- .../document-detail-navigation-fix.test.tsx | 4 +- .../explore/explore-app-list-flow.test.tsx | 19 +- .../slash-command-modes.test.tsx | 4 +- .../plugins/plugin-data-utilities.test.ts | 14 +- .../plugins/plugin-page-shell-flow.test.tsx | 10 +- .../chunk-preview-formatting.test.ts | 22 +- .../input-field-crud-flow.test.ts | 10 +- .../rag-pipeline/test-run-flow.test.ts | 12 +- web/__tests__/real-browser-flicker.test.tsx | 39 +- .../tools/provider-list-shell-flow.test.tsx | 6 +- .../tools/tool-data-processing.test.ts | 38 +- web/app/account/oauth/authorize/page.tsx | 2 +- .../app-info/__tests__/index.spec.tsx | 12 +- .../app/annotation/__tests__/list.spec.tsx | 12 +- .../__tests__/index.spec.tsx | 8 +- .../__tests__/index.spec.tsx | 146 +- .../header-opts/__tests__/index.spec.tsx | 10 +- .../__tests__/index.spec.tsx | 13 +- .../add-member-or-group-pop.tsx | 2 +- .../app-publisher/__tests__/index.spec.tsx | 16 +- .../__tests__/version-info-modal.spec.tsx | 18 +- .../app/app-publisher/features-wrapper.tsx | 2 +- .../components/app/app-publisher/index.tsx | 2 +- .../app/configuration/__tests__/utils.spec.ts | 4 +- .../__tests__/advanced-prompt-input.spec.tsx | 2 +- .../config-prompt/__tests__/index.spec.tsx | 16 +- .../__tests__/simple-prompt-input.spec.tsx | 14 +- .../config-prompt/advanced-prompt-input.tsx | 4 +- .../app/configuration/config-prompt/index.tsx | 4 +- .../config-prompt/simple-prompt-input.tsx | 4 +- .../config-var/__tests__/index.spec.tsx | 28 +- .../__tests__/form-fields.spec.tsx | 6 +- .../config-modal/__tests__/index.spec.tsx | 4 +- .../config-string/__tests__/index.spec.tsx | 10 +- .../app/configuration/config-var/index.tsx | 6 +- .../config-vision/__tests__/index.spec.tsx | 8 +- .../__tests__/index.spec.tsx | 176 +- .../automatic/__tests__/result.spec.tsx | 10 +- .../dataset-config/__tests__/index.spec.tsx | 85 +- .../card-item/__tests__/index.spec.tsx | 20 +- .../context-var/__tests__/index.spec.tsx | 89 +- .../context-var/__tests__/var-picker.spec.tsx | 133 +- .../__tests__/config-content.spec.tsx | 11 +- .../params-config/__tests__/index.spec.tsx | 19 +- .../params-config/config-content.tsx | 4 +- .../params-config/weighted-score.tsx | 4 +- .../__tests__/retrieval-section.spec.tsx | 53 +- .../debug/__tests__/index.spec.tsx | 44 +- .../__tests__/context-provider.spec.tsx | 6 +- .../__tests__/debug-item.spec.tsx | 45 +- .../__tests__/index.spec.tsx | 85 +- .../model-parameter-trigger.tsx | 4 +- .../__tests__/index.spec.tsx | 40 +- .../__tests__/use-configuration-utils.spec.ts | 2 +- .../external-data-tool-modal-utils.spec.ts | 2 +- .../tools/external-data-tool-modal-utils.ts | 4 +- web/app/components/app/configuration/utils.ts | 2 +- .../__tests__/index.spec.tsx | 63 +- .../app-list/__tests__/index.spec.tsx | 40 +- .../__tests__/index.spec.tsx | 22 +- .../app/log/__tests__/filter.spec.tsx | 50 +- .../app/log/__tests__/list-utils.spec.ts | 6 +- web/app/components/app/log/index.tsx | 2 +- .../__tests__/app-chart-utils.spec.ts | 10 +- .../app/overview/__tests__/app-chart.spec.tsx | 14 +- .../embedded/__tests__/index.spec.tsx | 4 +- .../app/overview/embedded/index.tsx | 2 +- .../saved-items/__tests__/index.spec.tsx | 8 +- .../components/app/type-selector/index.tsx | 4 +- .../workflow-log/__tests__/filter.spec.tsx | 65 +- .../app/workflow-log/__tests__/list.spec.tsx | 168 +- web/app/components/app/workflow-log/index.tsx | 2 +- .../components/apps/__tests__/list.spec.tsx | 92 +- web/app/components/apps/app-card.tsx | 2 +- .../__tests__/use-apps-query-state.spec.tsx | 12 +- .../apps/hooks/use-dsl-drag-drop.ts | 4 +- web/app/components/apps/list.tsx | 2 +- .../base/amplitude/__tests__/utils.spec.ts | 2 +- .../app-icon-picker/__tests__/index.spec.tsx | 20 +- .../__tests__/audio.player.manager.spec.ts | 18 +- .../base/audio-btn/__tests__/audio.spec.ts | 120 +- .../base/audio-btn/__tests__/index.spec.tsx | 36 +- .../base/audio-gallery/AudioPlayer.tsx | 4 +- .../__tests__/AudioPlayer.spec.tsx | 48 +- web/app/components/base/block-input/index.tsx | 2 +- .../base/carousel/__tests__/index.spec.tsx | 20 +- .../base/chat/__tests__/utils.spec.ts | 40 +- .../__tests__/chat-wrapper.spec.tsx | 126 +- .../__tests__/header-in-mobile.spec.tsx | 96 +- .../__tests__/hooks.spec.tsx | 8 +- .../header/__tests__/index.spec.tsx | 29 +- .../base/chat/chat-with-history/hooks.tsx | 2 +- .../base/chat/chat/__tests__/hooks.spec.tsx | 234 +-- .../chat/answer/__tests__/operation.spec.tsx | 63 +- .../__tests__/suggested-questions.spec.tsx | 12 +- .../__tests__/human-input-form.spec.tsx | 20 +- .../human-input-content/content-item.tsx | 4 +- .../__tests__/operation.spec.tsx | 12 +- .../base/chat/chat/chat-input-area/index.tsx | 4 +- .../chat/citation/__tests__/popup.spec.tsx | 119 +- .../base/chat/chat/citation/index.tsx | 6 +- web/app/components/base/chat/chat/hooks.ts | 26 +- web/app/components/base/chat/chat/index.tsx | 2 +- .../chat/thought/__tests__/index.spec.tsx | 80 +- .../base/chat/chat/use-chat-layout.ts | 4 +- .../inputs-form/__tests__/content.spec.tsx | 16 +- .../base/chat/embedded-chatbot/theme/utils.ts | 2 +- web/app/components/base/chat/utils.ts | 12 +- .../checkbox-list/__tests__/index.spec.tsx | 46 +- .../base/chip/__tests__/index.spec.tsx | 66 +- .../calendar/__tests__/index.spec.tsx | 8 +- .../date-picker/__tests__/header.spec.tsx | 9 +- .../date-picker/__tests__/index.spec.tsx | 84 +- .../time-picker/__tests__/index.spec.tsx | 73 +- .../time-picker/__tests__/options.spec.tsx | 12 +- .../base/date-and-time-picker/utils/dayjs.ts | 4 +- .../emoji-picker/__tests__/Inner.spec.tsx | 22 +- .../emoji-picker/__tests__/index.spec.tsx | 12 +- .../__tests__/use-annotation-config.spec.ts | 10 +- .../__tests__/index.spec.tsx | 31 +- .../__tests__/setting-modal.spec.tsx | 12 +- .../moderation/__tests__/index.spec.tsx | 51 +- .../moderation-setting-modal.spec.tsx | 83 +- .../moderation/moderation-setting-modal.tsx | 4 +- .../__tests__/param-config-content.spec.tsx | 36 +- .../__tests__/voice-settings.spec.tsx | 10 +- .../file-uploader/__tests__/hooks.spec.ts | 14 +- .../file-uploader/__tests__/utils.spec.ts | 30 +- .../base/file-uploader/file-input.tsx | 4 +- .../__tests__/file-item.spec.tsx | 128 +- .../__tests__/file-image-item.spec.tsx | 56 +- .../components/base/file-uploader/hooks.ts | 6 +- .../components/base/file-uploader/utils.ts | 12 +- .../base/__tests__/base-form.spec.tsx | 36 +- .../field/__tests__/file-uploader.spec.tsx | 6 +- .../variable-or-constant-input.spec.tsx | 18 +- .../base/form/form-scenarios/base/index.tsx | 2 +- .../base/form/form-scenarios/demo/index.tsx | 2 +- .../base/form/hooks/use-check-validated.ts | 2 +- .../components/base/form/index.stories.tsx | 2 +- .../base/form/utils/secret-input/index.ts | 6 +- .../base/grid-mask/__tests__/index.spec.tsx | 32 +- .../base/icons/icon-gallery.stories.tsx | 4 +- web/app/components/base/icons/utils.ts | 6 +- .../image-gallery/__tests__/index.spec.tsx | 20 +- .../image-uploader/__tests__/hooks.spec.ts | 32 +- .../image-uploader/__tests__/utils.spec.ts | 2 +- .../components/base/image-uploader/hooks.ts | 12 +- .../base/image-uploader/image-preview.tsx | 2 +- .../base/input/__tests__/index.spec.tsx | 48 +- .../__tests__/code-block.spec.tsx | 58 +- .../markdown-blocks/__tests__/form.spec.tsx | 33 +- .../base/markdown-with-directive/index.tsx | 8 +- .../base/markdown/__tests__/index.spec.tsx | 8 +- .../__tests__/streamdown-wrapper.spec.tsx | 35 +- .../base/markdown/streamdown-wrapper.tsx | 8 +- web/app/components/base/mermaid/index.tsx | 4 +- .../__tests__/index.spec.tsx | 8 +- .../new-audio-button/__tests__/index.spec.tsx | 24 +- web/app/components/base/notion-icon/index.tsx | 2 +- .../base/notion-page-selector/base.tsx | 6 +- .../page-selector/__tests__/index.spec.tsx | 90 +- .../__tests__/use-page-selector-model.spec.ts | 8 +- .../page-selector/__tests__/utils.spec.ts | 18 +- .../page-selector/virtual-page-list.tsx | 4 +- .../base/pagination/__tests__/index.spec.tsx | 147 +- web/app/components/base/pagination/hook.ts | 6 +- .../base/param-item/index.stories.tsx | 8 +- .../__tests__/index.spec.tsx | 14 +- .../__tests__/progress-circle.spec.tsx | 20 +- .../prompt-editor/__tests__/hooks.spec.tsx | 14 +- .../prompt-editor/__tests__/utils.spec.ts | 34 +- .../base/prompt-editor/constants.tsx | 2 +- .../components/base/prompt-editor/hooks.ts | 10 +- .../plugins/__tests__/tree-view.spec.tsx | 4 +- .../__tests__/hooks.spec.tsx | 98 +- .../context-block-replacement-block.spec.tsx | 4 +- .../context-block/__tests__/index.spec.tsx | 2 +- ...r-message-block-replacement-block.spec.tsx | 12 +- .../__tests__/index.spec.tsx | 8 +- .../__tests__/component.spec.tsx | 14 +- .../__tests__/input-field.spec.tsx | 26 +- .../__tests__/pre-populate.spec.tsx | 6 +- .../hitl-input-block-replacement-block.tsx | 2 +- .../hitl-input-block/variable-block.tsx | 4 +- .../plugins/shortcuts-popup-plugin/index.tsx | 2 +- .../__tests__/index.spec.tsx | 4 +- .../__tests__/component.spec.tsx | 6 +- .../__tests__/index.spec.tsx | 8 +- ...-variable-block-replacement-block.spec.tsx | 14 +- .../workflow-variable-block/component.tsx | 12 +- .../components/base/prompt-editor/utils.ts | 20 +- .../prompt-log-modal/__tests__/index.spec.tsx | 20 +- .../components/base/prompt-log-modal/card.tsx | 2 +- .../base/prompt-log-modal/index.tsx | 2 +- .../base/select/__tests__/index.spec.tsx | 249 ++- .../base/select/__tests__/pure.spec.tsx | 30 +- .../base/svg-gallery/__tests__/index.spec.tsx | 12 +- .../base/svg/__tests__/index.spec.tsx | 4 +- .../base/tag-input/__tests__/index.spec.tsx | 20 +- .../tag-management/__tests__/panel.spec.tsx | 162 +- .../__tests__/selector.spec.tsx | 72 +- .../__tests__/VideoPlayer.spec.tsx | 25 +- .../base/voice-input/__tests__/index.spec.tsx | 40 +- .../billing-page/__tests__/index.spec.tsx | 10 +- .../billing/plan/__tests__/index.spec.tsx | 19 +- .../plan/assets/__tests__/index.spec.tsx | 41 +- .../pricing/plans/__tests__/index.spec.tsx | 14 +- .../cloud-plan-item/__tests__/index.spec.tsx | 57 +- .../__tests__/index.spec.tsx | 16 +- web/app/components/billing/utils/index.ts | 4 +- .../image-list/__tests__/index.spec.tsx | 160 +- .../image-previewer/__tests__/index.spec.tsx | 56 +- .../datasets/common/image-previewer/index.tsx | 22 +- .../image-uploader/__tests__/utils.spec.ts | 10 +- .../common/image-uploader/hooks/use-upload.ts | 8 +- .../datasets/common/image-uploader/utils.ts | 2 +- .../__tests__/index.spec.tsx | 44 +- .../common/retrieval-param-config/index.tsx | 4 +- .../__tests__/index.spec.tsx | 112 +- .../datasets/create/__tests__/index.spec.tsx | 197 +- .../__tests__/rule-detail.spec.tsx | 14 +- .../create/embedding-process/rule-detail.tsx | 2 +- .../file-preview/__tests__/index.spec.tsx | 104 +- .../hooks/__tests__/use-file-upload.spec.tsx | 8 +- web/app/components/datasets/create/index.tsx | 2 +- .../create/step-two/__tests__/index.spec.tsx | 189 +- .../__tests__/indexing-mode-section.spec.tsx | 28 +- .../components/__tests__/inputs.spec.tsx | 27 +- .../__tests__/use-segmentation-state.spec.ts | 8 +- .../create/step-two/hooks/unescape.ts | 2 +- .../__tests__/index.spec.tsx | 63 +- .../create/website/__tests__/base.spec.tsx | 70 +- .../base/__tests__/crawled-result.spec.tsx | 30 +- .../create/website/base/crawled-result.tsx | 2 +- .../firecrawl/__tests__/index.spec.tsx | 67 +- .../firecrawl/__tests__/options.spec.tsx | 63 +- .../jina-reader/__tests__/index.spec.tsx | 110 +- .../watercrawl/__tests__/index.spec.tsx | 113 +- .../watercrawl/__tests__/options.spec.tsx | 44 +- .../components/__tests__/operations.spec.tsx | 33 +- .../document-list/__tests__/index.spec.tsx | 81 +- .../__tests__/document-table-row.spec.tsx | 51 +- .../components/document-source-icon.tsx | 2 +- .../__tests__/index.spec.tsx | 51 +- .../__tests__/step-indicator.spec.tsx | 8 +- .../__tests__/index.spec.tsx | 249 ++- .../data-source-options/index.tsx | 2 +- .../__tests__/index.spec.tsx | 131 +- .../base/credential-selector/index.tsx | 2 +- .../__tests__/use-local-file-upload.spec.tsx | 11 +- .../data-source/online-documents/index.tsx | 4 +- .../page-selector/__tests__/index.spec.tsx | 288 ++- .../page-selector/__tests__/utils.spec.ts | 26 +- .../online-drive/__tests__/index.spec.tsx | 98 +- .../online-drive/__tests__/utils.spec.ts | 8 +- .../file-list/header/__tests__/index.spec.tsx | 103 +- .../breadcrumbs/__tests__/bucket.spec.tsx | 6 +- .../breadcrumbs/__tests__/index.spec.tsx | 282 ++- .../dropdown/__tests__/index.spec.tsx | 216 +- .../file-list/header/breadcrumbs/index.tsx | 2 +- .../online-drive/file-list/list/index.tsx | 2 +- .../online-drive/file-list/list/utils.ts | 10 +- .../data-source/online-drive/utils.ts | 10 +- .../base/__tests__/crawled-result.spec.tsx | 37 +- .../base/__tests__/index.spec.tsx | 137 +- .../website-crawl/base/crawled-result.tsx | 2 +- .../website-crawl/base/options/index.tsx | 2 +- .../__tests__/use-datasource-store.spec.ts | 2 +- .../hooks/use-datasource-actions.ts | 2 +- .../hooks/use-datasource-ui-state.ts | 2 +- .../documents/create-from-pipeline/index.tsx | 2 +- .../preview/__tests__/file-preview.spec.tsx | 10 +- .../preview/__tests__/web-preview.spec.tsx | 10 +- .../preview/chunk-preview.tsx | 6 +- .../process-documents/form.tsx | 2 +- .../processing/embedding-process/index.tsx | 2 +- .../__tests__/step-three-content.spec.tsx | 16 +- .../detail/__tests__/new-segment.spec.tsx | 51 +- .../__tests__/child-segment-detail.spec.tsx | 63 +- .../detail/completed/__tests__/index.spec.tsx | 154 +- .../__tests__/new-child-segment.spec.tsx | 34 +- .../__tests__/segment-detail.spec.tsx | 61 +- .../common/__tests__/action-buttons.spec.tsx | 33 +- .../common/__tests__/chunk-content.spec.tsx | 100 +- .../components/segment-list-content.tsx | 4 +- .../__tests__/use-child-segment-data.spec.ts | 8 +- .../detail/completed/segment-list.tsx | 2 +- .../__tests__/use-embedding-status.spec.tsx | 2 +- .../detail/metadata/__tests__/index.spec.tsx | 124 +- .../__tests__/metadata-field-list.spec.tsx | 16 +- .../components/metadata-field-list.tsx | 4 +- .../use-document-list-query-state.spec.tsx | 36 +- .../__tests__/index.spec.tsx | 46 +- .../__tests__/index.spec.tsx | 32 +- .../__tests__/index.spec.tsx | 56 +- .../create/ExternalApiSelection.tsx | 4 +- .../create/__tests__/index.spec.tsx | 203 +- .../hit-testing/__tests__/index.spec.tsx | 82 +- .../query-input/__tests__/index.spec.tsx | 59 +- web/app/components/datasets/list/datasets.tsx | 2 +- .../__tests__/modal.spec.tsx | 72 +- .../metadata/edit-metadata-batch/modal.tsx | 10 +- .../use-batch-edit-document-metadata.spec.ts | 8 +- .../__tests__/use-metadata-document.spec.ts | 2 +- .../hooks/use-batch-edit-document-metadata.ts | 4 +- .../metadata/hooks/use-metadata-document.ts | 8 +- .../dataset-metadata-drawer.spec.tsx | 84 +- .../__tests__/index.spec.tsx | 136 +- .../__tests__/info-group.spec.tsx | 38 +- .../preview/__tests__/container.spec.tsx | 47 +- .../rename-modal/__tests__/index.spec.tsx | 348 +++- .../chunk-structure/__tests__/hooks.spec.tsx | 42 +- .../__tests__/basic-info-section.spec.tsx | 96 +- .../hooks/__tests__/use-form-state.spec.ts | 2 +- .../index-method/__tests__/index.spec.tsx | 29 +- .../__tests__/index.spec.tsx | 72 +- .../settings/permission-selector/index.tsx | 12 +- web/app/components/develop/code.tsx | 2 +- .../components/develop/hooks/use-doc-toc.ts | 2 +- .../__tests__/secret-key-button.spec.tsx | 38 +- web/app/components/develop/tag.tsx | 2 +- .../create-app-modal/__tests__/index.spec.tsx | 42 +- .../__tests__/use-get-requirements.spec.ts | 16 +- .../try-app/app-info/use-get-requirements.ts | 6 +- .../try-app/preview/basic-app-preview.tsx | 2 +- .../actions/__tests__/index.spec.ts | 10 +- .../actions/__tests__/knowledge.spec.ts | 4 +- .../actions/__tests__/recent-store.spec.ts | 6 +- .../__tests__/direct-commands.spec.ts | 26 +- .../actions/commands/__tests__/go.spec.tsx | 16 +- .../commands/__tests__/language.spec.ts | 4 +- .../commands/__tests__/registry.spec.ts | 4 +- .../actions/commands/__tests__/theme.spec.ts | 2 +- .../goto-anything/command-selector.tsx | 2 +- .../use-goto-anything-results.spec.ts | 6 +- .../hooks/use-goto-anything-results.ts | 4 +- web/app/components/goto-anything/index.tsx | 2 +- .../__tests__/compliance.spec.tsx | 24 +- .../__tests__/index.spec.tsx | 12 +- .../account-setting/__tests__/index.spec.tsx | 156 +- .../__tests__/index.spec.tsx | 49 +- .../__tests__/item.spec.tsx | 20 +- .../__tests__/selector.spec.tsx | 11 +- .../__tests__/card.spec.tsx | 35 +- .../hooks/use-marketplace-all-plugins.ts | 4 +- .../header/account-setting/index.tsx | 2 +- .../members-page/__tests__/index.spec.tsx | 53 +- .../__tests__/hooks.spec.ts | 12 +- .../__tests__/utils.spec.ts | 2 +- .../model-provider-page/hooks.ts | 4 +- .../add-credential-in-load-balancing.spec.tsx | 11 +- ...itch-credential-in-load-balancing.spec.tsx | 67 +- .../__tests__/authorized-item.spec.tsx | 18 +- .../authorized/__tests__/index.spec.tsx | 14 +- .../hooks/__tests__/use-auth-service.spec.tsx | 16 +- .../__tests__/use-custom-models.spec.tsx | 4 +- .../__tests__/use-model-form-schemas.spec.tsx | 2 +- .../model-modal/__tests__/Form.spec.tsx | 159 +- .../model-modal/__tests__/index.spec.tsx | 16 +- .../__tests__/parameter-item.spec.tsx | 44 +- .../__tests__/popup-item.spec.tsx | 22 +- .../model-selector/__tests__/popup.spec.tsx | 56 +- .../model-selector/popup-item.tsx | 2 +- .../__tests__/model-list.spec.tsx | 106 +- .../model-load-balancing-modal.spec.tsx | 41 +- .../__tests__/usage-priority-section.spec.tsx | 16 +- .../use-activate-credential.spec.tsx | 4 +- .../model-load-balancing-configs.tsx | 2 +- .../model-load-balancing-modal.tsx | 2 +- .../account-setting/plugin-page/index.tsx | 2 +- web/app/components/header/app-nav/index.tsx | 2 +- .../app-selector/__tests__/index.spec.tsx | 11 +- .../nav/nav-selector/__tests__/index.spec.tsx | 8 +- .../plugins/__tests__/hooks.spec.ts | 8 +- web/app/components/plugins/card/index.tsx | 2 +- .../plugins/install-plugin/hooks.ts | 2 +- .../steps/__tests__/install-multi.spec.tsx | 70 +- .../steps/hooks/use-install-multi-state.ts | 2 +- .../install-bundle/steps/install.tsx | 6 +- .../install-bundle/steps/installed.tsx | 6 +- .../marketplace/__tests__/query.spec.tsx | 6 +- .../marketplace/__tests__/utils.spec.ts | 10 +- .../marketplace/list/list-with-collection.tsx | 2 +- .../search-box/__tests__/index.spec.tsx | 187 +- .../search-box/trigger/marketplace.tsx | 2 +- .../search-box/trigger/tool-selector.tsx | 2 +- .../marketplace/sort-dropdown/index.tsx | 2 +- .../__tests__/authorized-in-node.spec.tsx | 8 +- .../__tests__/plugin-auth-in-agent.spec.tsx | 16 +- .../authorized/__tests__/index.spec.tsx | 324 ++- .../hooks/__tests__/use-credential.spec.ts | 2 +- .../__tests__/agent-strategy-list.spec.tsx | 12 +- .../__tests__/detail-header.spec.tsx | 104 +- .../__tests__/endpoint-card.spec.tsx | 52 +- .../__tests__/endpoint-list.spec.tsx | 26 +- .../__tests__/endpoint-modal.spec.tsx | 22 +- .../__tests__/strategy-detail.spec.tsx | 40 +- .../app-selector/__tests__/index.spec.tsx | 340 +-- .../app-selector/app-picker.tsx | 2 +- .../hooks/use-app-inputs-form-schema.ts | 2 +- .../__tests__/tts-params-panel.spec.tsx | 172 +- .../__tests__/index.spec.tsx | 16 +- .../__tests__/subscription-card.spec.tsx | 12 +- .../create/__tests__/index.spec.tsx | 305 ++- .../__tests__/use-oauth-client-state.spec.ts | 8 +- .../create/hooks/use-common-modal-state.ts | 2 +- .../subscription-list/create/index.tsx | 6 +- .../reasoning-config-form.helpers.ts | 10 +- .../components/reasoning-config-form.tsx | 4 +- .../__tests__/use-tool-selector-state.spec.ts | 4 +- .../__tests__/event-detail-drawer.spec.tsx | 46 +- .../trigger/event-detail-drawer.tsx | 2 +- .../plugin-item/__tests__/action.spec.tsx | 112 +- .../components/plugins/plugin-item/index.tsx | 2 +- .../__tests__/category-filter.spec.tsx | 12 +- .../__tests__/index.spec.tsx | 141 +- .../filter-management/category-filter.tsx | 2 +- .../plugin-page/list/__tests__/index.spec.tsx | 138 +- .../plugin-tasks/__tests__/index.spec.tsx | 121 +- .../__tests__/plugin-section.spec.tsx | 22 +- .../__tests__/plugin-task-list.spec.tsx | 59 +- .../components/error-plugin-item.tsx | 6 +- .../plugins/plugin-page/use-uploader.ts | 2 +- .../__tests__/index.spec.tsx | 133 +- .../update-plugin/plugin-version-picker.tsx | 2 +- .../chunk-card-list/__tests__/index.spec.tsx | 210 +- .../input-field/__tests__/index.spec.tsx | 146 +- .../panel/input-field/editor/form/index.tsx | 2 +- .../field-list/__tests__/field-item.spec.tsx | 10 +- .../field-list/__tests__/hooks.spec.ts | 8 +- .../field-list/__tests__/index.spec.tsx | 197 +- .../panel/input-field/field-list/hooks.ts | 4 +- .../components/panel/input-field/index.tsx | 4 +- .../preparation/__tests__/hooks.spec.ts | 10 +- .../preparation/__tests__/index.spec.tsx | 134 +- .../__tests__/index.spec.tsx | 74 +- .../preparation/data-source-options/index.tsx | 2 +- .../document-processing/options.tsx | 2 +- .../panel/test-run/preparation/index.tsx | 4 +- .../test-run/result/__tests__/index.spec.tsx | 144 +- .../result-preview/__tests__/index.spec.tsx | 112 +- .../__tests__/index.spec.tsx | 140 +- .../hooks/__tests__/index.spec.ts | 14 +- .../hooks/__tests__/use-input-fields.spec.ts | 20 +- .../__tests__/use-nodes-sync-draft.spec.ts | 12 +- .../__tests__/use-pipeline-config.spec.ts | 6 +- .../__tests__/use-pipeline-template.spec.ts | 12 +- .../hooks/__tests__/use-pipeline.spec.ts | 6 +- .../use-rag-pipeline-search.spec.tsx | 6 +- .../rag-pipeline/hooks/use-pipeline.tsx | 4 +- .../store/__tests__/index.spec.ts | 28 +- .../utils/__tests__/index.spec.ts | 24 +- .../components/rag-pipeline/utils/nodes.ts | 6 +- .../text-generation-result-panel.spec.tsx | 26 +- .../hooks/use-text-generation-batch.ts | 4 +- .../workflow-stream-handlers.spec.ts | 12 +- .../text-generation/result/result-request.ts | 2 +- .../result/workflow-stream-handlers.ts | 12 +- .../csv-download/__tests__/index.spec.tsx | 10 +- .../run-once/__tests__/index.spec.tsx | 22 +- .../__tests__/index.spec.tsx | 66 +- .../__tests__/test-api.spec.tsx | 32 +- .../mcp/__tests__/mcp-server-modal.spec.tsx | 46 +- .../tools/mcp/__tests__/modal.spec.tsx | 75 +- .../mcp/detail/__tests__/content.spec.tsx | 73 +- .../components/tools/mcp/headers-input.tsx | 2 +- .../__tests__/use-mcp-modal-form.spec.ts | 8 +- .../tools/mcp/hooks/use-mcp-service-card.ts | 2 +- .../__tests__/authentication-section.spec.tsx | 34 +- .../__tests__/headers-section.spec.tsx | 97 +- .../tools/provider/__tests__/detail.spec.tsx | 84 +- web/app/components/tools/provider/detail.tsx | 2 +- .../utils/__tests__/to-form-schema.spec.ts | 60 +- .../components/tools/utils/to-form-schema.ts | 4 +- .../__tests__/configure-button.spec.tsx | 312 ++- .../__tests__/method-selector.spec.tsx | 60 +- .../__tests__/use-configure-button.spec.ts | 6 +- .../components/tools/workflow-tool/index.tsx | 4 +- .../workflow-app/components/workflow-main.tsx | 2 +- .../__tests__/use-workflow-template.spec.ts | 6 +- web/app/components/workflow-app/utils.ts | 2 +- .../__tests__/candidate-node-main.spec.tsx | 10 +- ...ustom-edge-linear-gradient-render.spec.tsx | 24 +- .../__tests__/edge-contextmenu.spec.tsx | 18 +- .../__tests__/workflow-history-store.spec.tsx | 8 +- .../__tests__/workflow-test-env.spec.tsx | 18 +- .../__tests__/index-bar.spec.tsx | 8 +- .../block-selector/__tests__/tabs.spec.tsx | 26 +- .../__tests__/view-type-select.spec.tsx | 8 +- .../workflow/block-selector/blocks.tsx | 2 +- .../workflow/block-selector/data-sources.tsx | 2 +- .../workflow/block-selector/index-bar.tsx | 8 +- .../rag-tool-recommendations/list.tsx | 10 +- .../block-selector/tool/action-item.tsx | 4 +- .../tool/tool-list-tree-view/list.tsx | 2 +- .../workflow/block-selector/tool/tool.tsx | 26 +- .../workflow/block-selector/tools.tsx | 10 +- .../trigger-plugin/action-item.tsx | 6 +- .../block-selector/trigger-plugin/item.tsx | 16 +- .../__tests__/collaboration-manager.test.ts | 8 +- .../core/__tests__/crdt-provider.test.ts | 2 +- .../collaboration/utils/user-color.ts | 2 +- .../workflow/comment/mention-input.tsx | 6 +- .../components/workflow/comment/thread.tsx | 2 +- web/app/components/workflow/features.tsx | 8 +- .../header/__tests__/run-mode.spec.tsx | 8 +- .../workflow/header/checklist/node-group.tsx | 2 +- .../header/checklist/plugin-group.tsx | 2 +- .../use-edges-interactions.helpers.spec.ts | 4 +- .../__tests__/use-node-data-update.spec.ts | 2 +- .../__tests__/use-nodes-interactions.spec.ts | 4 +- .../use-workflow-interactions.spec.tsx | 2 +- .../use-workflow-organize.helpers.spec.ts | 4 +- .../__tests__/use-workflow-organize.spec.tsx | 2 +- .../__tests__/use-workflow-variables.spec.ts | 8 +- .../workflow/hooks/use-checklist.ts | 88 +- .../workflow/hooks/use-edges-interactions.ts | 4 +- .../hooks/use-fetch-workflow-inspect-vars.ts | 12 +- .../components/workflow/hooks/use-helpline.ts | 16 +- .../hooks/use-inspect-vars-crud-common.ts | 8 +- .../workflow/hooks/use-nodes-interactions.ts | 46 +- .../workflow/hooks/use-workflow-comment.ts | 4 +- .../__tests__/use-workflow-agent-log.spec.ts | 10 +- .../use-workflow-node-finished.spec.ts | 2 +- ...kflow-node-human-input-form-filled.spec.ts | 2 +- ...flow-node-human-input-form-timeout.spec.ts | 2 +- ...workflow-node-human-input-required.spec.ts | 6 +- ...se-workflow-node-iteration-started.spec.ts | 2 +- .../use-workflow-node-loop-finished.spec.ts | 2 +- .../use-workflow-node-loop-started.spec.ts | 2 +- .../use-workflow-node-started.spec.ts | 4 +- .../use-workflow-agent-log.ts | 16 +- .../use-workflow-finished.ts | 4 +- ...-workflow-node-human-input-form-timeout.ts | 2 +- .../use-workflow-node-human-input-required.ts | 4 +- .../use-workflow-node-iteration-started.ts | 14 +- .../use-workflow-node-loop-started.ts | 14 +- .../use-workflow-node-started.ts | 12 +- .../components/workflow/hooks/use-workflow.ts | 4 +- web/app/components/workflow/index.tsx | 2 +- .../components/before-run-form/form-item.tsx | 16 +- .../_base/components/before-run-form/form.tsx | 4 +- .../components/before-run-form/helpers.ts | 2 +- .../code-editor/editor-support-vars.tsx | 2 +- .../_base/components/error-handle/utils.ts | 4 +- .../nodes/_base/components/file-type-item.tsx | 2 +- .../nodes/_base/components/next-step/line.tsx | 4 +- .../readonly-input-with-select-var.tsx | 2 +- .../components/support-var-input/index.tsx | 2 +- .../components/switch-plugin-version.tsx | 2 +- .../nodes/_base/components/variable-tag.tsx | 2 +- .../__tests__/output-var-list.spec.tsx | 10 +- .../var-reference-picker.trigger.spec.tsx | 16 +- .../var-reference-vars.helpers.spec.ts | 4 +- .../object-child-tree-panel/picker/index.tsx | 2 +- .../object-child-tree-panel/show/index.tsx | 2 +- .../components/variable/output-var-list.tsx | 10 +- .../nodes/_base/components/variable/utils.ts | 88 +- .../variable/var-full-path-panel.tsx | 6 +- .../_base/components/variable/var-list.tsx | 24 +- .../variable/var-reference-picker.helpers.ts | 4 +- .../variable/var-reference-picker.tsx | 10 +- .../variable/var-reference-vars.helpers.ts | 2 +- .../_base/components/workflow-panel/index.tsx | 2 +- .../workflow-panel/last-run/use-last-run.ts | 8 +- .../nodes/_base/hooks/use-one-step-run.ts | 16 +- .../nodes/_base/hooks/use-output-var-list.ts | 14 +- .../use-single-run-form-params.spec.ts | 6 +- .../workflow/nodes/agent/use-config.ts | 4 +- .../assigner/__tests__/integration.spec.tsx | 40 +- .../use-single-run-form-params.spec.ts | 6 +- .../assigner/components/var-list/index.tsx | 22 +- .../workflow/nodes/code/code-parser.ts | 4 +- .../workflow/nodes/data-source-empty/hooks.ts | 4 +- .../data-source/hooks/use-before-run-form.ts | 4 +- .../__tests__/integration.spec.tsx | 18 +- .../__tests__/panel.spec.tsx | 12 +- .../use-single-run-form-params.spec.ts | 4 +- .../nodes/end/__tests__/use-config.spec.ts | 2 +- .../components/workflow/nodes/end/node.tsx | 2 +- .../nodes/http/__tests__/integration.spec.tsx | 40 +- .../nodes/http/components/curl-parser.ts | 8 +- .../nodes/http/components/edit-body/index.tsx | 4 +- .../nodes/http/hooks/use-key-value-list.ts | 2 +- .../__tests__/human-input.spec.tsx | 59 +- .../__tests__/button-style-dropdown.spec.tsx | 10 +- .../__tests__/form-content-preview.spec.tsx | 16 +- .../components/__tests__/user-action.spec.tsx | 6 +- .../__tests__/method-item.spec.tsx | 24 +- .../delivery-method/recipient/email-input.tsx | 2 +- .../delivery-method/test-email-sender.tsx | 6 +- .../components/variable-in-markdown.tsx | 8 +- .../use-single-run-form-params.spec.ts | 2 +- .../components/condition-list/index.tsx | 2 +- .../nodes/if-else/use-config.helpers.ts | 2 +- web/app/components/workflow/nodes/index.tsx | 5 +- .../__tests__/use-interactions.spec.tsx | 2 +- .../use-single-run-form-params.spec.ts | 10 +- .../iteration/use-single-run-form-params.ts | 6 +- .../use-single-run-form-params.spec.ts | 6 +- .../__tests__/search-method-option.spec.tsx | 24 +- .../top-k-and-score-threshold.spec.tsx | 10 +- .../__tests__/use-config.spec.ts | 6 +- .../use-knowledge-metadata-config.spec.ts | 4 +- .../nodes/knowledge-retrieval/utils.ts | 4 +- .../__tests__/extract-input.spec.tsx | 6 +- .../__tests__/limit-config.spec.tsx | 6 +- .../components/filter-condition.tsx | 2 +- .../workflow/nodes/list-operator/panel.tsx | 2 +- .../nodes/list-operator/use-config.helpers.ts | 2 +- .../nodes/llm/__tests__/use-config.spec.ts | 4 +- .../nodes/llm/components/config-prompt.tsx | 10 +- .../visual-editor/hooks.ts | 74 +- .../__tests__/use-llm-prompt-config.spec.ts | 22 +- .../loop/__tests__/use-config.helpers.spec.ts | 10 +- .../loop/__tests__/use-interactions.spec.tsx | 2 +- .../use-single-run-form-params.spec.ts | 6 +- .../workflow/nodes/loop/use-config.helpers.ts | 4 +- .../use-single-run-form-params.helpers.ts | 8 +- .../use-single-run-form-params.spec.ts | 10 +- .../__tests__/import-from-tool.spec.tsx | 4 +- .../use-single-run-form-params.spec.ts | 10 +- .../components/class-list.tsx | 4 +- .../nodes/start/__tests__/use-config.spec.ts | 4 +- .../use-single-run-form-params.spec.ts | 8 +- .../nodes/start/components/var-list.tsx | 2 +- .../workflow/nodes/start/use-config.ts | 4 +- .../use-single-run-form-params.spec.ts | 6 +- .../nodes/tool/__tests__/panel.spec.tsx | 30 +- .../__tests__/index.spec.tsx | 8 +- .../__tests__/placeholder.spec.tsx | 16 +- .../tool-form/__tests__/index.spec.tsx | 8 +- .../tool-form/__tests__/item.spec.tsx | 26 +- .../use-single-run-form-params.spec.ts | 8 +- .../tool/hooks/use-single-run-form-params.ts | 4 +- .../nodes/tool/output-schema-utils.ts | 2 +- .../nodes/trigger-schedule/default.ts | 8 +- .../utils/__tests__/integration.spec.ts | 20 +- .../trigger-schedule/utils/cron-parser.ts | 6 +- .../utils/execution-time-calculator.ts | 32 +- .../__tests__/generic-table.spec.tsx | 14 +- .../__tests__/header-table.spec.tsx | 6 +- .../__tests__/parameter-table.spec.tsx | 4 +- .../components/parameter-table.tsx | 2 +- .../use-single-run-form-params.spec.ts | 4 +- .../__tests__/node-group-item.spec.tsx | 20 +- .../components/node-group-item.tsx | 4 +- .../variable-assigner/use-config.helpers.ts | 10 +- .../nodes/variable-assigner/use-config.ts | 8 +- .../components/workflow/note-node/index.tsx | 6 +- .../note-editor/toolbar/color-picker.tsx | 26 +- .../components/workflow/operator/index.tsx | 2 +- .../human-input-filled-form-list.spec.tsx | 6 +- .../__tests__/hooks/handle-resume.spec.ts | 34 +- .../__tests__/hooks/handle-send.spec.ts | 4 +- .../__tests__/hooks/opening-statement.spec.ts | 14 +- .../__tests__/hooks/sse-callbacks.spec.ts | 48 +- .../conversation-variable-modal.tsx | 4 +- .../workflow/panel/debug-and-preview/hooks.ts | 26 +- web/app/components/workflow/panel/index.tsx | 2 +- .../workflow/panel/inputs-panel.tsx | 2 +- .../panel/version-history-panel/index.tsx | 2 +- .../run/__tests__/tracing-panel.spec.tsx | 18 +- .../run/agent-log/agent-result-panel.tsx | 6 +- .../__tests__/loop-log-trigger.spec.tsx | 6 +- .../components/workflow/run/output-panel.tsx | 8 +- .../utils/format-log/__tests__/index.spec.ts | 4 +- .../utils/format-log/graph-to-log-struct.ts | 6 +- .../run/utils/format-log/iteration/index.ts | 2 +- .../workflow/selection-contextmenu.tsx | 14 +- .../__tests__/datasets-detail-store.spec.ts | 4 +- .../__tests__/inspect-vars-slice.spec.ts | 46 +- .../__tests__/inspect-vars-slice.spec.ts | 8 +- .../workflow/debug/inspect-vars-slice.ts | 6 +- .../workflow/update-dsl-modal.helpers.ts | 2 +- .../utils/__tests__/elk-layout.spec.ts | 14 +- .../utils/__tests__/node-navigation.spec.ts | 8 +- .../workflow/utils/__tests__/tool.spec.ts | 4 +- .../workflow/utils/__tests__/trigger.spec.ts | 6 +- .../utils/__tests__/workflow-init.spec.ts | 86 +- .../workflow/utils/__tests__/workflow.spec.ts | 8 +- web/app/components/workflow/utils/node.ts | 2 +- .../workflow/utils/workflow-init.ts | 38 +- .../variable-inspect/__tests__/group.spec.tsx | 8 +- .../variable-inspect/__tests__/panel.spec.tsx | 10 +- .../__tests__/value-content-sections.spec.tsx | 8 +- .../value-content-sections.tsx | 8 +- .../variable-inspect/value-content.tsx | 2 +- .../components/__tests__/zoom-in-out.spec.tsx | 4 +- .../nodes/loop/__tests__/hooks.spec.tsx | 2 +- .../components/note-node/index.tsx | 6 +- web/app/signin/invite-settings/page.tsx | 2 +- web/context/modal-context.test.tsx | 8 +- web/eslint-suppressions.json | 112 +- web/hooks/use-app-favicon.ts | 2 +- web/hooks/use-async-window-open.spec.ts | 6 +- web/hooks/use-query-params.spec.tsx | 30 +- web/i18n-config/index.ts | 2 +- web/i18n-config/language.ts | 2 +- web/knip.config.ts | 2 - web/plugins/dev-proxy/cookies.ts | 6 +- web/plugins/dev-proxy/server.spec.ts | 2 +- web/plugins/vite/utils.ts | 6 +- web/scripts/analyze-i18n-diff.ts | 4 +- web/scripts/gen-doc-paths.ts | 22 +- web/service/base.ts | 18 +- web/service/knowledge/use-create-dataset.ts | 2 +- web/service/use-plugins.ts | 6 +- web/tsconfig.json | 1 + web/utils/emoji.ts | 2 +- web/utils/error-parser.ts | 2 +- web/utils/format.ts | 14 +- web/utils/model-config.spec.ts | 8 +- web/utils/urlValidation.ts | 2 +- web/utils/var.ts | 4 +- 729 files changed, 14660 insertions(+), 7803 deletions(-) create mode 100755 packages/migrate-no-unchecked-indexed-access/bin/migrate-no-unchecked-indexed-access.js create mode 100644 packages/migrate-no-unchecked-indexed-access/package.json create mode 100644 packages/migrate-no-unchecked-indexed-access/src/cli.ts create mode 100644 packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts create mode 100644 packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts create mode 100644 packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts create mode 100644 packages/migrate-no-unchecked-indexed-access/vite.config.ts diff --git a/packages/migrate-no-unchecked-indexed-access/bin/migrate-no-unchecked-indexed-access.js b/packages/migrate-no-unchecked-indexed-access/bin/migrate-no-unchecked-indexed-access.js new file mode 100755 index 0000000000..2f2b0d72a9 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/bin/migrate-no-unchecked-indexed-access.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node + +import { spawnSync } from 'node:child_process' +import fs from 'node:fs' +import path from 'node:path' +import process from 'node:process' +import { fileURLToPath } from 'node:url' + +const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const entryFile = path.join(packageRoot, 'dist', 'cli.mjs') + +if (!fs.existsSync(entryFile)) + throw new Error(`Built CLI entry not found at ${entryFile}. Run "pnpm --filter migrate-no-unchecked-indexed-access build" first.`) + +const result = spawnSync( + process.execPath, + [entryFile, ...process.argv.slice(2)], + { + cwd: process.cwd(), + env: process.env, + stdio: 'inherit', + }, +) + +if (result.error) + throw result.error + +process.exit(result.status ?? 1) diff --git a/packages/migrate-no-unchecked-indexed-access/package.json b/packages/migrate-no-unchecked-indexed-access/package.json new file mode 100644 index 0000000000..781e88b0e8 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/package.json @@ -0,0 +1,20 @@ +{ + "name": "migrate-no-unchecked-indexed-access", + "private": true, + "version": "0.0.0-private", + "type": "module", + "bin": { + "migrate-no-unchecked-indexed-access": "./bin/migrate-no-unchecked-indexed-access.js" + }, + "scripts": { + "build": "vp pack" + }, + "dependencies": { + "typescript": "catalog:" + }, + "devDependencies": { + "@types/node": "catalog:", + "vite": "catalog:", + "vite-plus": "catalog:" + } +} diff --git a/packages/migrate-no-unchecked-indexed-access/src/cli.ts b/packages/migrate-no-unchecked-indexed-access/src/cli.ts new file mode 100644 index 0000000000..fa52510905 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/cli.ts @@ -0,0 +1,45 @@ +import process from 'node:process' +import { runBatchMigrationCommand } from './no-unchecked-indexed-access/run' + +function printUsage() { + console.log(`Usage: + migrate-no-unchecked-indexed-access [options] + +Options: + --project + --batch-size + --batch-iterations + --max-rounds + --verbose`) +} + +async function flushStandardStreams() { + await Promise.all([ + new Promise(resolve => process.stdout.write('', () => resolve())), + new Promise(resolve => process.stderr.write('', () => resolve())), + ]) +} + +async function main() { + const argv = process.argv.slice(2) + if (argv.includes('help') || argv.includes('--help') || argv.includes('-h')) { + printUsage() + return + } + + await runBatchMigrationCommand(argv) +} + +let exitCode = 0 + +try { + await main() + exitCode = process.exitCode ?? 0 +} +catch (error) { + console.error(error instanceof Error ? error.message : error) + exitCode = 1 +} + +await flushStandardStreams() +process.exit(exitCode) diff --git a/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts new file mode 100644 index 0000000000..6b79e214bb --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/migrate.ts @@ -0,0 +1,1835 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import ts from 'typescript' + +const SUPPORTED_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts']) +export const SUPPORTED_DIAGNOSTIC_CODES = new Set([2322, 2339, 2345, 2488, 2532, 2538, 2604, 2722, 2769, 2786, 7006, 18047, 18048]) +const DEFAULT_MAX_ITERATIONS = 10 +const ACCESS_DIAGNOSTIC_CODES = new Set([2339, 2532, 18047, 18048]) +const ASSIGNABILITY_DIAGNOSTIC_CODES = new Set([2322, 2345, 2769]) +const parsedConfigCache = new Map() + +type CliOptions = { + files: string[] + maxIterations: number + project: string + useFullProjectRoots?: boolean + verbose: boolean + write: boolean +} + +type TextEdit = { + end: number + expectedText?: string + replacement: string + start: number +} + +type EditTarget + = { expression: ts.Expression, kind: 'expression', sourceFile: ts.SourceFile } + | { end: number, kind: 'direct-edit', replacement: string, sourceFile: ts.SourceFile, start: number } + | { kind: 'shorthand-property', property: ts.ShorthandPropertyAssignment, sourceFile: ts.SourceFile } + +export function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = { + files: [], + maxIterations: DEFAULT_MAX_ITERATIONS, + project: 'tsconfig.json', + verbose: false, + write: false, + } + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i] + if (!arg) + continue + + if (arg === '--') + continue + + if (arg === '--write') { + options.write = true + continue + } + + if (arg === '--verbose') { + options.verbose = true + continue + } + + if (arg === '--project') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --project') + + options.project = value + i += 1 + continue + } + + if (arg === '--max-iterations') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --max-iterations') + + const parsed = Number(value) + if (!Number.isInteger(parsed) || parsed <= 0) + throw new Error(`Invalid --max-iterations value: ${value}`) + + options.maxIterations = parsed + i += 1 + continue + } + + if (arg === '--files') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --files') + + options.files.push(...splitFilesArgument(value)) + i += 1 + continue + } + + if (arg.startsWith('--')) + throw new Error(`Unknown option: ${arg}`) + + options.files.push(...splitFilesArgument(arg)) + } + + return options +} + +function splitFilesArgument(value: string): string[] { + return value + .split(',') + .map(item => item.trim()) + .filter(Boolean) +} + +function parseTsConfig(projectPath: string): ts.ParsedCommandLine { + const cached = parsedConfigCache.get(projectPath) + if (cached) + return cached + + const configFile = ts.readConfigFile(projectPath, ts.sys.readFile) + if (configFile.error) + throw new Error(formatDiagnostic(configFile.error)) + + const configDirectory = path.dirname(projectPath) + const parsedConfig = ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + configDirectory, + undefined, + projectPath, + ) + parsedConfigCache.set(projectPath, parsedConfig) + return parsedConfig +} + +function createMigrationProgram( + rootNames: string[], + parsedConfig: ts.ParsedCommandLine, + fileTexts: Map, + oldProgram?: ts.Program, +): ts.Program { + const compilerHost = ts.createCompilerHost(parsedConfig.options, true) + const originalGetSourceFile = compilerHost.getSourceFile.bind(compilerHost) + + compilerHost.readFile = (fileName) => { + return fileTexts.get(fileName) ?? ts.sys.readFile(fileName) + } + + compilerHost.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => { + const text = fileTexts.get(fileName) + if (text !== undefined) + return ts.createSourceFile(fileName, text, languageVersion, true) + + return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile) + } + + return ts.createProgram({ + oldProgram, + host: compilerHost, + options: parsedConfig.options, + projectReferences: parsedConfig.projectReferences, + rootNames, + }) +} + +function isTargetFile(fileName: string): boolean { + const extension = path.extname(fileName) + if (!SUPPORTED_EXTENSIONS.has(extension)) + return false + + if (fileName.endsWith('.d.ts')) + return false + + return !fileName.includes(`${path.sep}.next${path.sep}`) +} + +function normalizeFileName(fileName: string): string { + return path.resolve(fileName) +} + +function isDeclarationSupportFile(fileName: string): boolean { + return fileName.endsWith('.d.ts') +} + +function isSetupSupportFile(fileName: string): boolean { + const baseName = path.basename(fileName) + return baseName === 'vitest.setup.ts' + || baseName === 'vitest.setup.tsx' + || baseName === 'jest.setup.ts' + || baseName === 'jest.setup.tsx' + || baseName === 'setupTests.ts' + || baseName === 'setupTests.tsx' + || baseName === 'test.setup.ts' + || baseName === 'test.setup.tsx' +} + +function getMigrationRootNames( + parsedConfig: ts.ParsedCommandLine, + targetFiles: string[], +): string[] { + const rootNames = new Set(targetFiles) + + for (const fileName of parsedConfig.fileNames.map(normalizeFileName)) { + if (isDeclarationSupportFile(fileName) || isSetupSupportFile(fileName)) + rootNames.add(fileName) + } + + return Array.from(rootNames) +} + +function createFileMatcher(filePatterns: string[]): (fileName: string) => boolean { + if (filePatterns.length === 0) + return () => true + + const patterns = filePatterns.map(pattern => ({ + absolute: normalizeFileName(pattern), + raw: pattern.split(path.sep).join('/'), + })) + return (fileName: string) => { + const normalized = normalizeFileName(fileName) + const unixStyle = normalized.split(path.sep).join('/') + return patterns.some(pattern => normalized === pattern.absolute || unixStyle.endsWith(pattern.raw)) + } +} + +function formatDiagnostic(diagnostic: ts.Diagnostic): string { + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n') + if (!diagnostic.file || diagnostic.start === undefined) + return message + + const position = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start) + return `${diagnostic.file.fileName}:${position.line + 1}:${position.character + 1} TS${diagnostic.code}: ${message}` +} + +function ensureTrailingNonNullAssertion(expression: string): string { + const trimmedExpression = expression.trimEnd() + return trimmedExpression.endsWith('!') + ? trimmedExpression + : `${trimmedExpression}!` +} + +function hasOptionalChainDescendant(node: ts.Node): boolean { + let found = false + + const visit = (current: ts.Node) => { + if (found) + return + + if (ts.isOptionalChain(current)) { + found = true + return + } + + current.forEachChild(visit) + } + + visit(node) + return found +} + +function shouldPrintInlineNonNullAssertion(expression: ts.Expression): boolean { + return ts.isOptionalChain(expression) + || (ts.isParenthesizedExpression(expression) && hasOptionalChainDescendant(expression.expression)) +} + +function normalizeOptionalChainNonNullContinuations(text: string): string { + const sourceFile = ts.createSourceFile('normalize.tsx', text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) + const edits: TextEdit[] = [] + + const visit = (node: ts.Node) => { + if ( + ts.isNonNullExpression(node) + && ts.isParenthesizedExpression(node.expression) + && hasOptionalChainDescendant(node.expression.expression) + ) { + edits.push({ + end: node.getEnd(), + replacement: `${node.expression.expression.getText(sourceFile)}!`, + start: node.getStart(sourceFile), + }) + return + } + + node.forEachChild(visit) + } + + visit(sourceFile) + + if (edits.length === 0) + return text + + return applyEdits(text, edits).text +} + +function collapseRepeatedInlineComments(text: string): string { + return text + .split('\n') + .map((line) => { + const commentIndex = line.indexOf('//') + if (commentIndex < 0) + return line + + const prefix = line.slice(0, commentIndex).trimEnd() + const comment = line.slice(commentIndex + 2).trim() + const segments = comment + .split(/\s+\/\/\s+/) + .map(item => item.trim()) + .filter(Boolean) + + if (segments.length < 2) + return line + + const lastSegment = segments[segments.length - 1]! + const stableSegments = segments.slice(0, -1) + const repeatedSameComment = stableSegments.length > 0 + && stableSegments.every(segment => segment === segments[0]) + && (lastSegment === segments[0] || segments[0]!.startsWith(lastSegment) || lastSegment.startsWith(segments[0]!)) + + if (!repeatedSameComment) + return line.replace(/!{2,}$/g, '!') + + const normalizedComment = segments[0]!.replace(/!{2,}$/g, '!') + return prefix ? `${prefix} // ${normalizedComment}` : `// ${normalizedComment}` + }) + .join('\n') +} + +export function normalizeMalformedAssertions(text: string): string { + const normalizedText = text + .replace(/\n(\s*)! (\s*\/\/[^\n]*)\n/g, '! $2\n') + .replace(/\.not!+(?=[.(])/g, '.not') + .replace(/(\(|,\s*)([A-Za-z_$][\w$]*)\s*:\s*any\s*=>/g, '$1($2: any) =>') + .replace(/([,{]\s*)([A-Z_$][\w$]*)!=\{/g, '$1$2={') + .replace(/\b([A-Z_$][\w$]*)!!,/gi, '$1: $1!,') + .replace(/\b([A-Z_$][\w$]*)!!:/gi, '$1:') + .replace(/([,{]\s*)([A-Z_$][\w$]*)!:/gi, '$1$2:') + .replace(/\b(const|let|var)\s+\{([^=\n]+)\}\s*=\s*([^\n;]+)/g, (fullMatch, keyword: string, bindings: string, expression: string) => { + if (!bindings.includes('!')) + return fullMatch + + const normalizedBindings = bindings.replace(/!([,\s}:])/g, '$1') + return `${keyword} {${normalizedBindings}} = ${ensureTrailingNonNullAssertion(expression)}` + }) + + return collapseRepeatedInlineComments(normalizeOptionalChainNonNullContinuations(normalizedText)) +} + +function isExpressionTarget(target: EditTarget): target is Extract { + return target.kind === 'expression' +} + +function createExpressionTarget(expression: ts.Expression): EditTarget { + return { + expression, + kind: 'expression', + sourceFile: expression.getSourceFile(), + } +} + +function createShorthandPropertyTarget(property: ts.ShorthandPropertyAssignment): EditTarget { + return { + kind: 'shorthand-property', + property, + sourceFile: property.getSourceFile(), + } +} + +function createDirectEditTarget( + sourceFile: ts.SourceFile, + start: number, + end: number, + replacement: string, +): EditTarget { + return { + end, + kind: 'direct-edit', + replacement, + sourceFile, + start, + } +} + +function createIterableFallbackReplacement( + expression: ts.Expression, + sourceFile: ts.SourceFile, +): string { + return `(${expression.getText(sourceFile)} ?? [])` +} + +function createIterableFallbackTarget(expression: ts.Expression): EditTarget { + return createDirectEditTarget( + expression.getSourceFile(), + expression.getStart(expression.getSourceFile()), + expression.getEnd(), + createIterableFallbackReplacement(expression, expression.getSourceFile()), + ) +} + +function createArrayLiteralIterableFallbackTarget( + arrayLiteral: ts.ArrayLiteralExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const sourceFile = arrayLiteral.getSourceFile() + const start = arrayLiteral.getStart(sourceFile) + const end = arrayLiteral.getEnd() + const originalText = sourceFile.text.slice(start, end) + const edits: TextEdit[] = [] + + for (const element of arrayLiteral.elements) { + if (!ts.isSpreadElement(element)) + continue + + if (isAlreadyNonNull(element.expression)) + continue + + if (!typeIncludesUndefined(checker.getTypeAtLocation(element.expression))) + continue + + edits.push({ + end: element.expression.getEnd() - start, + replacement: createIterableFallbackReplacement(element.expression, sourceFile), + start: element.expression.getStart(sourceFile) - start, + }) + } + + if (edits.length === 0) + return undefined + + return createDirectEditTarget( + sourceFile, + start, + end, + applyEdits(originalText, edits).text, + ) +} + +function getTokenAtPosition(sourceFile: ts.SourceFile, position: number): ts.Node { + let current: ts.Node = sourceFile + + while (true) { + let next: ts.Node | undefined + current.forEachChild((child) => { + if (!next && position >= child.getFullStart() && position < child.getEnd()) + next = child + }) + + if (!next) + return current + + current = next + } +} + +function findAncestor( + node: ts.Node | undefined, + predicate: (candidate: ts.Node) => candidate is NodeType, +): NodeType | undefined { + let current = node + + while (current) { + if (predicate(current)) + return current + + current = current.parent + } + + return undefined +} + +function findTightestExpression(sourceFile: ts.SourceFile, start: number, end: number): ts.Expression | undefined { + let node: ts.Node | undefined = getTokenAtPosition(sourceFile, start) + + while (node) { + if (ts.isExpression(node)) { + const nodeStart = node.getStart(sourceFile) + const nodeEnd = node.getEnd() + if (nodeStart <= start && end <= nodeEnd) + return node + } + + node = node.parent + } + + return undefined +} + +function isAssignmentOperator(token: ts.SyntaxKind): boolean { + return token >= ts.SyntaxKind.FirstAssignment && token <= ts.SyntaxKind.LastAssignment +} + +function typeIncludesUndefined(type: ts.Type): boolean { + if ((type.flags & ts.TypeFlags.Undefined) !== 0) + return true + + if (!type.isUnion()) + return false + + return type.types.some(typeIncludesUndefined) +} + +function skipOuterExpressions(expression: ts.Expression): ts.Expression { + let current = expression + + while (ts.isParenthesizedExpression(current) || ts.isNonNullExpression(current)) + current = current.expression + + return current +} + +function isAlreadyNonNull(expression: ts.Expression): boolean { + let current = expression + + while (ts.isParenthesizedExpression(current)) + current = current.expression + + return ts.isNonNullExpression(current) +} + +function findAssignmentLikeCandidate( + token: ts.Node, + sourceFile: ts.SourceFile, + start: number, + end: number, +): ts.Expression | undefined { + let current: ts.Node | undefined = token + + while (current) { + if (ts.isVariableDeclaration(current) && current.initializer) + return current.initializer + + if (ts.isPropertyDeclaration(current) && current.initializer) + return current.initializer + + if (ts.isPropertyAssignment(current)) + return current.initializer + + if (ts.isShorthandPropertyAssignment(current)) + return current.name + + if (ts.isParameter(current) && current.initializer) + return current.initializer + + if (ts.isReturnStatement(current) && current.expression) + return current.expression + + if (ts.isBinaryExpression(current) && isAssignmentOperator(current.operatorToken.kind)) + return current.right + + if (ts.isJsxAttribute(current) && current.initializer && ts.isJsxExpression(current.initializer) && current.initializer.expression) + return current.initializer.expression + + if (ts.isJsxSpreadAttribute(current)) + return current.expression + + current = current.parent + } + + return findTightestExpression(sourceFile, start, end) +} + +function findArgumentCandidate( + token: ts.Node, + sourceFile: ts.SourceFile, + start: number, + end: number, +): ts.Expression | undefined { + let current: ts.Node | undefined = token + + while (current) { + if ((ts.isCallExpression(current) || ts.isNewExpression(current)) && current.arguments) { + const argument = current.arguments.find((item) => { + const itemStart = item.getStart(sourceFile) + const itemEnd = item.getEnd() + return itemStart <= start && end <= itemEnd + }) + if (argument) + return argument + } + + current = current.parent + } + + return findTightestExpression(sourceFile, start, end) +} + +function getExpressionFromJsxAttribute(attribute: ts.JsxAttribute): ts.Expression | undefined { + return attribute.initializer && ts.isJsxExpression(attribute.initializer) + ? attribute.initializer.expression + : undefined +} + +function findTargetFromExpression( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const referencedDeclarationTarget = findReferencedDeclarationInitializerTarget(expression, checker) + if (referencedDeclarationTarget) + return referencedDeclarationTarget + + const nestedTarget = findNestedContainerTarget(expression, checker) + if (nestedTarget) + return nestedTarget + + const innerExpression = skipOuterExpressions(expression) + if (ts.isConditionalExpression(innerExpression)) { + return findTargetFromExpression(innerExpression.whenTrue, checker) + ?? findTargetFromExpression(innerExpression.whenFalse, checker) + } + + if ( + ts.isBinaryExpression(innerExpression) + && ( + innerExpression.operatorToken.kind === ts.SyntaxKind.BarBarToken + || innerExpression.operatorToken.kind === ts.SyntaxKind.QuestionQuestionToken + || innerExpression.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken + ) + ) { + return findTargetFromExpression(innerExpression.left, checker) + ?? findTargetFromExpression(innerExpression.right, checker) + } + + if (ts.isArrowFunction(innerExpression) || ts.isFunctionExpression(innerExpression)) { + const functionTarget = findFunctionLikeReturnTarget(innerExpression, checker) + if (functionTarget) + return functionTarget + } + + if (ts.isPropertyAccessExpression(innerExpression)) { + const namedPropertyTarget = findNamedPropertyTarget(innerExpression.expression, innerExpression.name.text, checker) + if (namedPropertyTarget) + return namedPropertyTarget + } + + if (ts.isCallExpression(innerExpression)) { + const collectionCallbackTarget = findCollectionCallbackTarget(innerExpression, checker) + if (collectionCallbackTarget) + return collectionCallbackTarget + + const callbackArgumentTarget = findCallbackArgumentTarget(innerExpression, checker) + if (callbackArgumentTarget) + return callbackArgumentTarget + + const callExpressionTarget = findCallExpressionDeclarationTarget(innerExpression, checker) + if (callExpressionTarget) + return callExpressionTarget + } + + if (!typeIncludesUndefined(checker.getTypeAtLocation(expression))) + return undefined + + return createExpressionTarget(expression) +} + +function findJsxSpreadAttributeTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const spreadAttribute = findAncestor(token, ts.isJsxSpreadAttribute) + if (spreadAttribute) + return findTargetFromExpression(spreadAttribute.expression, checker) + + const openingLikeElement = findAncestor(token, node => + ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) + + if (!openingLikeElement) + return undefined + + for (const attribute of openingLikeElement.attributes.properties) { + if (!ts.isJsxSpreadAttribute(attribute)) + continue + + const target = findTargetFromExpression(attribute.expression, checker) + if (target) + return target + } + + return undefined +} + +function findShorthandPropertyTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const property = findAncestor(token, ts.isShorthandPropertyAssignment) + if (!property) + return undefined + + return typeIncludesUndefined(checker.getTypeAtLocation(property.name)) + ? createShorthandPropertyTarget(property) + : undefined +} + +function findPropertyAssignmentInitializerTarget( + token: ts.Node, + start: number, + checker: ts.TypeChecker, +): EditTarget | undefined { + const propertyAssignment = findAncestor(token, ts.isPropertyAssignment) + if (!propertyAssignment) + return undefined + + const propertyNameStart = propertyAssignment.name.getStart() + const propertyNameEnd = propertyAssignment.name.getEnd() + if (start < propertyNameStart || start >= propertyNameEnd) + return undefined + + const directTarget = findTargetFromExpression(propertyAssignment.initializer, checker) + if (directTarget) + return directTarget + + const nestedTarget = findNestedContainerTarget(propertyAssignment.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (!typeIncludesUndefined(checker.getTypeAtLocation(propertyAssignment.initializer))) + return undefined + + return createExpressionTarget(propertyAssignment.initializer) +} + +function findPropertyAccessExpressionTarget( + token: ts.Node, + start: number, +): EditTarget | undefined { + const propertyAccess = findAncestor(token, ts.isPropertyAccessExpression) + if (!propertyAccess) + return undefined + + if (start >= propertyAccess.name.getStart() && start < propertyAccess.name.getEnd()) + return createExpressionTarget(propertyAccess.expression) + + return undefined +} + +function findUndefinedAccessTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + let current: ts.Node | undefined = token + let bestTarget: EditTarget | undefined + + while (current) { + if (ts.isPropertyAccessExpression(current)) { + const expression = current.expression + if (typeIncludesUndefined(checker.getTypeAtLocation(expression)) && !isAlreadyNonNull(expression)) + bestTarget = createExpressionTarget(expression) + } + + if (ts.isElementAccessExpression(current)) { + const expression = current.expression + if (typeIncludesUndefined(checker.getTypeAtLocation(expression)) && !isAlreadyNonNull(expression)) + bestTarget = createExpressionTarget(expression) + } + + current = current.parent + } + + return bestTarget +} + +function findElementAccessArgumentTarget(token: ts.Node): EditTarget | undefined { + let current = token + let matchingElementAccess: ts.ElementAccessExpression | undefined + + while (current) { + if (ts.isElementAccessExpression(current) && current.argumentExpression) + matchingElementAccess = current + + current = current.parent + } + + if (!matchingElementAccess?.argumentExpression) + return undefined + + return createExpressionTarget(matchingElementAccess.argumentExpression) +} + +function findIterableTarget( + sourceFile: ts.SourceFile, + token: ts.Node, + start: number, + end: number, + checker: ts.TypeChecker, +): EditTarget | undefined { + const arrayLiteral = findAncestor(token, ts.isArrayLiteralExpression) + if (arrayLiteral) { + const arrayLiteralTarget = createArrayLiteralIterableFallbackTarget(arrayLiteral, checker) + if (arrayLiteralTarget) + return arrayLiteralTarget + } + + const spreadElement = findAncestor(token, ts.isSpreadElement) + if (spreadElement && !isAlreadyNonNull(spreadElement.expression)) + return createIterableFallbackTarget(spreadElement.expression) + + const variableDeclaration = findAncestor(token, ts.isVariableDeclaration) + if ( + variableDeclaration?.initializer + && typeIncludesUndefined(checker.getTypeAtLocation(variableDeclaration.initializer)) + && !isAlreadyNonNull(variableDeclaration.initializer) + ) { + return createExpressionTarget(variableDeclaration.initializer) + } + + const binaryExpression = findAncestor(token, ts.isBinaryExpression) + if ( + binaryExpression + && isAssignmentOperator(binaryExpression.operatorToken.kind) + && typeIncludesUndefined(checker.getTypeAtLocation(binaryExpression.right)) + && !isAlreadyNonNull(binaryExpression.right) + ) { + return createExpressionTarget(binaryExpression.right) + } + + return undefined +} + +function findImplicitAnyParameterTarget(token: ts.Node): EditTarget | undefined { + const parameter = findAncestor(token, ts.isParameter) + if (!parameter || parameter.type || !ts.isIdentifier(parameter.name)) + return undefined + + const sourceFile = parameter.getSourceFile() + const replacement = ts.isArrowFunction(parameter.parent) && parameter.parent.parameters.length === 1 + ? `(${parameter.name.getText(sourceFile)}: any)` + : `${parameter.name.getText(sourceFile)}: any` + + return createDirectEditTarget( + sourceFile, + parameter.getStart(sourceFile), + parameter.getEnd(), + replacement, + ) +} + +function getArrayPatternElementTypeText( + element: ts.ArrayBindingElement | ts.Expression, + checker: ts.TypeChecker, +): string { + if (ts.isOmittedExpression(element)) + return 'unknown' + + const targetNode = ts.isBindingElement(element) + ? element.name + : element + + const targetType = checker.getNonNullableType(checker.getTypeAtLocation(targetNode)) + const typeText = checker.typeToString(targetType) + return typeText === 'never' ? 'unknown' : typeText +} + +function createArrayDestructuringReplacement( + sourceFile: ts.SourceFile, + expression: ts.Expression, + elements: readonly (ts.ArrayBindingElement | ts.Expression)[], + checker: ts.TypeChecker, + options?: { + fallbackToEmptyArray?: boolean + }, +): string | undefined { + if (elements.length === 0) + return undefined + + const tupleTypes = elements.map(element => getArrayPatternElementTypeText(element, checker)) + const expressionText = options?.fallbackToEmptyArray + ? `(${expression.getText(sourceFile)} ?? [])` + : `(${expression.getText(sourceFile)})` + return `${expressionText} as [${tupleTypes.join(', ')}]` +} + +function findArrayDestructuringTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const binaryExpression = findAncestor(token, ts.isBinaryExpression) + if (binaryExpression && isAssignmentOperator(binaryExpression.operatorToken.kind) && ts.isArrayLiteralExpression(binaryExpression.left)) { + const replacement = createArrayDestructuringReplacement( + binaryExpression.getSourceFile(), + binaryExpression.right, + binaryExpression.left.elements, + checker, + { + fallbackToEmptyArray: typeIncludesUndefined(checker.getTypeAtLocation(binaryExpression.right)), + }, + ) + if (replacement) { + return createDirectEditTarget( + binaryExpression.getSourceFile(), + binaryExpression.right.getStart(binaryExpression.getSourceFile()), + binaryExpression.right.getEnd(), + replacement, + ) + } + } + + const variableDeclaration = findAncestor(token, ts.isVariableDeclaration) + if (variableDeclaration?.initializer && ts.isArrayBindingPattern(variableDeclaration.name)) { + const replacement = createArrayDestructuringReplacement( + variableDeclaration.getSourceFile(), + variableDeclaration.initializer, + variableDeclaration.name.elements, + checker, + { + fallbackToEmptyArray: typeIncludesUndefined(checker.getTypeAtLocation(variableDeclaration.initializer)), + }, + ) + if (replacement) { + return createDirectEditTarget( + variableDeclaration.getSourceFile(), + variableDeclaration.initializer.getStart(variableDeclaration.getSourceFile()), + variableDeclaration.initializer.getEnd(), + replacement, + ) + } + } + + return undefined +} + +function findVariableDeclarationInitializerTarget( + sourceFile: ts.SourceFile, + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const variableDeclaration = findAncestor(token, ts.isVariableDeclaration) + if (!variableDeclaration?.initializer) + return undefined + + const nestedTarget = findNestedContainerTarget(variableDeclaration.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (!typeIncludesUndefined(checker.getTypeAtLocation(variableDeclaration.initializer))) + return undefined + + return createExpressionTarget(variableDeclaration.initializer) +} + +function getResolvedValueDeclaration( + symbol: ts.Symbol | undefined, + checker: ts.TypeChecker, +): ts.Declaration | undefined { + if (!symbol) + return undefined + + const resolvedSymbol = symbol.flags & ts.SymbolFlags.Alias + ? checker.getAliasedSymbol(symbol) + : symbol + + return resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0] +} + +function getFunctionLikeDeclaration( + declaration: ts.Declaration, +): ts.FunctionLikeDeclarationBase | undefined { + if ( + ts.isFunctionDeclaration(declaration) + || ts.isMethodDeclaration(declaration) + || ts.isFunctionExpression(declaration) + || ts.isArrowFunction(declaration) + ) { + return declaration + } + + if ( + ts.isVariableDeclaration(declaration) + && declaration.initializer + && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer)) + ) { + return declaration.initializer + } + + return undefined +} + +function getPropertyNameText(name: ts.PropertyName | ts.BindingName): string | undefined { + if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) + return name.text + + return undefined +} + +function getCallExpressionPropertyAccess(callExpression: ts.CallExpression): ts.PropertyAccessExpression | undefined { + const callee = skipOuterExpressions(callExpression.expression) + return ts.isPropertyAccessExpression(callee) ? callee : undefined +} + +function getFunctionExpressionArgument(callExpression: ts.CallExpression, index = 0): ts.ArrowFunction | ts.FunctionExpression | undefined { + const callback = callExpression.arguments[index] + return callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) + ? callback + : undefined +} + +function findTargetInFunctionBody( + body: ts.ConciseBody, + resolveExpression: (expression: ts.Expression) => EditTarget | undefined, +): EditTarget | undefined { + if (ts.isBlock(body)) { + for (const expression of findReturnStatementExpressions(body)) { + const target = resolveExpression(expression) + if (target) + return target + } + + return undefined + } + + return resolveExpression(body) +} + +function getParameterCollectionExpression( + declaration: ts.ParameterDeclaration, +): ts.Expression | undefined { + const functionLikeDeclaration = declaration.parent + if ( + !(ts.isArrowFunction(functionLikeDeclaration) || ts.isFunctionExpression(functionLikeDeclaration)) + || !ts.isCallExpression(functionLikeDeclaration.parent) + || functionLikeDeclaration.parent.arguments[0] !== functionLikeDeclaration + ) { + return undefined + } + + const callee = getCallExpressionPropertyAccess(functionLikeDeclaration.parent) + return callee?.expression +} + +function findObjectLiteralNamedPropertyTarget( + objectLiteral: ts.ObjectLiteralExpression, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + for (const property of objectLiteral.properties) { + if (ts.isSpreadAssignment(property)) + continue + + if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) + return createShorthandPropertyTarget(property) + + if (ts.isPropertyAssignment(property)) { + const currentPropertyName = getPropertyNameText(property.name) + if (currentPropertyName !== propertyName) + continue + + return findTargetFromExpression(property.initializer, checker) + ?? createExpressionTarget(property.initializer) + } + } + + return undefined +} + +function findFunctionLikeNamedReturnTarget( + declaration: ts.FunctionLikeDeclarationBase, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + if (!declaration.body) + return undefined + + return findTargetInFunctionBody( + declaration.body, + expression => findNamedPropertyTarget(expression, propertyName, checker), + ) +} + +function findCollectionPropertyTarget( + expression: ts.Expression, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + + if (ts.isIdentifier(innerExpression)) + return findNamedPropertyTarget(innerExpression, propertyName, checker) + + if (!ts.isCallExpression(innerExpression)) + return undefined + + const callee = getCallExpressionPropertyAccess(innerExpression) + if (!callee) + return undefined + + if (callee.name.text === 'map' || callee.name.text === 'flatMap') { + const callback = getFunctionExpressionArgument(innerExpression) + if (!callback) + return undefined + + return findTargetInFunctionBody( + callback.body, + returnedExpression => findNamedPropertyTarget(returnedExpression, propertyName, checker), + ) + } + + if (callee.name.text === 'filter') + return findCollectionPropertyTarget(callee.expression, propertyName, checker) + + return undefined +} + +function findNamedPropertyTarget( + expression: ts.Expression, + propertyName: string, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + + if (ts.isObjectLiteralExpression(innerExpression)) + return findObjectLiteralNamedPropertyTarget(innerExpression, propertyName, checker) + + if (ts.isIdentifier(innerExpression)) { + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(innerExpression), checker) + if (!declaration) + return undefined + + if (ts.isParameter(declaration)) { + const collectionExpression = getParameterCollectionExpression(declaration) + if (collectionExpression) + return findCollectionPropertyTarget(collectionExpression, propertyName, checker) + } + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (functionLikeDeclaration) + return findFunctionLikeNamedReturnTarget(functionLikeDeclaration, propertyName, checker) + + if (ts.isVariableDeclaration(declaration) && declaration.initializer) + return findNamedPropertyTarget(declaration.initializer, propertyName, checker) + + return undefined + } + + if (ts.isCallExpression(innerExpression)) { + const collectionPropertyTarget = findCollectionPropertyTarget(innerExpression, propertyName, checker) + if (collectionPropertyTarget) + return collectionPropertyTarget + + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(skipOuterExpressions(innerExpression.expression)), checker) + if (!declaration) + return undefined + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (!functionLikeDeclaration) + return undefined + + return findFunctionLikeNamedReturnTarget(functionLikeDeclaration, propertyName, checker) + } + + return undefined +} + +function findReturnStatementExpressions(node: ts.Node): ts.Expression[] { + const expressions: ts.Expression[] = [] + + const visit = (current: ts.Node) => { + if ( + current !== node + && ( + ts.isArrowFunction(current) + || ts.isFunctionExpression(current) + || ts.isFunctionDeclaration(current) + || ts.isMethodDeclaration(current) + ) + ) { + return + } + + if (ts.isReturnStatement(current) && current.expression) + expressions.push(current.expression) + + current.forEachChild(visit) + } + + visit(node) + return expressions +} + +function findFunctionLikeReturnTarget( + declaration: ts.FunctionLikeDeclarationBase, + checker: ts.TypeChecker, +): EditTarget | undefined { + if (!declaration.body) + return undefined + + return findTargetInFunctionBody( + declaration.body, + expression => findTargetFromExpression(expression, checker), + ) +} + +function findCallExpressionDeclarationTarget( + callExpression: ts.CallExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(skipOuterExpressions(callExpression.expression)), checker) + if (!declaration) + return undefined + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (!functionLikeDeclaration) + return undefined + + return findFunctionLikeReturnTarget(functionLikeDeclaration, checker) +} + +function findCallbackArgumentTarget( + callExpression: ts.CallExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const callee = skipOuterExpressions(callExpression.expression) + const calleeName = ts.isIdentifier(callee) ? callee.text : getCallExpressionPropertyAccess(callExpression)?.name.text + + if (calleeName !== 'useCallback' && calleeName !== 'useMemo') + return undefined + + const callback = getFunctionExpressionArgument(callExpression) + if (!callback) + return undefined + + return findFunctionLikeReturnTarget(callback, checker) +} + +function findReferencedDeclarationInitializerTarget( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + if (!ts.isIdentifier(innerExpression)) + return undefined + + const declaration = getResolvedValueDeclaration(checker.getSymbolAtLocation(innerExpression), checker) + if (!declaration) + return undefined + + if (ts.isBindingElement(declaration)) { + const propertyName = declaration.propertyName + ? getPropertyNameText(declaration.propertyName) + : getPropertyNameText(declaration.name) + + const variableDeclaration = declaration.parent.parent + if (propertyName && ts.isVariableDeclaration(variableDeclaration) && variableDeclaration.initializer) { + const namedPropertyTarget = findNamedPropertyTarget(variableDeclaration.initializer, propertyName, checker) + if (namedPropertyTarget) + return namedPropertyTarget + } + } + + if (ts.isParameter(declaration)) { + const collectionExpression = getParameterCollectionExpression(declaration) + if (collectionExpression) { + const collectionTarget = findTargetFromExpression(collectionExpression, checker) + if (collectionTarget) + return collectionTarget + } + } + + const functionLikeDeclaration = getFunctionLikeDeclaration(declaration) + if (functionLikeDeclaration) { + const functionTarget = findFunctionLikeReturnTarget(functionLikeDeclaration, checker) + if (functionTarget) + return functionTarget + } + + if (!ts.isVariableDeclaration(declaration) || !declaration.initializer) + return undefined + + const collectionCallbackTarget = findCollectionCallbackTarget(declaration.initializer, checker) + if (collectionCallbackTarget) + return collectionCallbackTarget + + const initializerTarget = findTargetFromExpression(declaration.initializer, checker) + if (initializerTarget) + return initializerTarget + + const nestedTarget = findNestedContainerTarget(declaration.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (!typeIncludesUndefined(checker.getTypeAtLocation(declaration.initializer))) + return undefined + + return createExpressionTarget(declaration.initializer) +} + +function findCollectionCallbackTarget( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + if (!ts.isCallExpression(innerExpression)) + return undefined + + const callee = getCallExpressionPropertyAccess(innerExpression) + if (!callee) + return undefined + + if (callee.name.text !== 'map' && callee.name.text !== 'flatMap') + return undefined + + const callback = getFunctionExpressionArgument(innerExpression) + if (!callback) + return undefined + + return findFunctionLikeReturnTarget(callback, checker) +} + +function findJsxComponentDeclarationTarget( + token: ts.Node, + checker: ts.TypeChecker, +): EditTarget | undefined { + const openingLikeElement = findAncestor(token, node => + ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) + if (!openingLikeElement) + return undefined + + const tagName = openingLikeElement.tagName + if (!ts.isIdentifier(tagName)) + return undefined + + const symbol = checker.getSymbolAtLocation(tagName) + const declaration = symbol?.valueDeclaration + if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) + return undefined + + if (!typeIncludesUndefined(checker.getTypeAtLocation(declaration.initializer))) + return undefined + + return createExpressionTarget(declaration.initializer) +} + +function findObjectLiteralPropertyTarget( + objectLiteral: ts.ObjectLiteralExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + for (const property of objectLiteral.properties) { + if (ts.isSpreadAssignment(property)) { + const directTarget = findTargetFromExpression(property.expression, checker) + if (directTarget) + return directTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(property.expression))) + return createExpressionTarget(property.expression) + continue + } + + if (ts.isShorthandPropertyAssignment(property)) { + if (typeIncludesUndefined(checker.getTypeAtLocation(property.name))) + return createShorthandPropertyTarget(property) + continue + } + + if (ts.isPropertyAssignment(property)) { + const directTarget = findTargetFromExpression(property.initializer, checker) + if (directTarget) + return directTarget + + const nestedTarget = findNestedContainerTarget(property.initializer, checker) + if (nestedTarget) + return nestedTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(property.initializer))) + return createExpressionTarget(property.initializer) + } + } + + return undefined +} + +function findArrayLiteralElementTarget( + arrayLiteral: ts.ArrayLiteralExpression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const iterableFallbackTarget = createArrayLiteralIterableFallbackTarget(arrayLiteral, checker) + if (iterableFallbackTarget) + return iterableFallbackTarget + + for (const element of arrayLiteral.elements) { + if (ts.isSpreadElement(element)) { + const directTarget = findTargetFromExpression(element.expression, checker) + if (directTarget) + return directTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(element.expression))) + return createExpressionTarget(element.expression) + continue + } + + const directTarget = findTargetFromExpression(element, checker) + if (directTarget) + return directTarget + + const nestedTarget = findNestedContainerTarget(element, checker) + if (nestedTarget) + return nestedTarget + + if (typeIncludesUndefined(checker.getTypeAtLocation(element))) + return createExpressionTarget(element) + } + + for (let index = arrayLiteral.elements.length - 1; index >= 0; index -= 1) { + const element = arrayLiteral.elements[index] + if (!element) + continue + + if (ts.isSpreadElement(element)) + continue + + if (!isAlreadyNonNull(element)) + return createExpressionTarget(element) + } + + return undefined +} + +function findNestedContainerTarget( + expression: ts.Expression, + checker: ts.TypeChecker, +): EditTarget | undefined { + const innerExpression = skipOuterExpressions(expression) + if (ts.isObjectLiteralExpression(innerExpression)) + return findObjectLiteralPropertyTarget(innerExpression, checker) + + if (ts.isArrayLiteralExpression(innerExpression)) + return findArrayLiteralElementTarget(innerExpression, checker) + + return undefined +} + +function findAccessDiagnosticTarget( + sourceFile: ts.SourceFile, + token: ts.Node, + start: number, + end: number, + checker: ts.TypeChecker, +): EditTarget | undefined { + const directExpression = findTightestExpression(sourceFile, start, end) + if (directExpression) { + if (typeIncludesUndefined(checker.getTypeAtLocation(directExpression)) && !isAlreadyNonNull(directExpression)) + return createExpressionTarget(directExpression) + + const referencedDeclarationTarget = findReferencedDeclarationInitializerTarget(directExpression, checker) + if (referencedDeclarationTarget && isExpressionTarget(referencedDeclarationTarget) && !isAlreadyNonNull(referencedDeclarationTarget.expression)) + return referencedDeclarationTarget + } + + const bindingPatternTarget = findVariableDeclarationInitializerTarget(sourceFile, token, checker) + if (bindingPatternTarget && isExpressionTarget(bindingPatternTarget) && !isAlreadyNonNull(bindingPatternTarget.expression)) + return bindingPatternTarget + + const accessTarget = findUndefinedAccessTarget(token, checker) + if (accessTarget && isExpressionTarget(accessTarget) && !isAlreadyNonNull(accessTarget.expression)) + return accessTarget + + const propertyAccessTarget = findPropertyAccessExpressionTarget(token, start) + if (propertyAccessTarget && isExpressionTarget(propertyAccessTarget) && !isAlreadyNonNull(propertyAccessTarget.expression)) + return propertyAccessTarget + + return undefined +} + +function findDiagnosticCandidate( + sourceFile: ts.SourceFile, + token: ts.Node, + start: number, + end: number, + diagnosticCode: number, + checker: ts.TypeChecker, +): ts.Expression | undefined { + if (diagnosticCode === 2322) { + const directExpression = findTightestExpression(sourceFile, start, end) + if (directExpression && typeIncludesUndefined(checker.getTypeAtLocation(directExpression))) + return directExpression + + return findAssignmentLikeCandidate(token, sourceFile, start, end) + } + + if (diagnosticCode === 2345) + return findArgumentCandidate(token, sourceFile, start, end) + + if (diagnosticCode === 2722) { + const current = findTightestExpression(sourceFile, start, end) + if (current && ts.isCallExpression(current)) + return current.expression + + return findTightestExpression(sourceFile, start, end) + } + + return findTightestExpression(sourceFile, start, end) +} + +function resolveEditTarget( + sourceFile: ts.SourceFile, + diagnostic: ts.DiagnosticWithLocation, + checker: ts.TypeChecker, +): EditTarget | undefined { + const start = diagnostic.start + const end = diagnostic.start + diagnostic.length + const token = getTokenAtPosition(sourceFile, start) + + const shorthandTarget = findShorthandPropertyTarget(token, checker) + if (shorthandTarget) + return shorthandTarget + + const propertyAssignmentTarget = findPropertyAssignmentInitializerTarget(token, start, checker) + if (propertyAssignmentTarget) + return propertyAssignmentTarget + + const jsxSpreadTarget = findJsxSpreadAttributeTarget(token, checker) + if (jsxSpreadTarget && isExpressionTarget(jsxSpreadTarget) && !isAlreadyNonNull(jsxSpreadTarget.expression)) + return jsxSpreadTarget + + const jsxAttribute = findAncestor(token, ts.isJsxAttribute) + const jsxExpression = jsxAttribute ? getExpressionFromJsxAttribute(jsxAttribute) : undefined + + if ( + ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) + && jsxExpression + && typeIncludesUndefined(checker.getTypeAtLocation(jsxExpression)) + && !isAlreadyNonNull(jsxExpression) + ) { + return findTargetFromExpression(jsxExpression, checker) + ?? createExpressionTarget(jsxExpression) + } + + if (ACCESS_DIAGNOSTIC_CODES.has(diagnostic.code)) + return findAccessDiagnosticTarget(sourceFile, token, start, end, checker) + + if (diagnostic.code === 2322 || diagnostic.code === 2488) { + const arrayDestructuringTarget = findArrayDestructuringTarget(token, checker) + if (arrayDestructuringTarget) + return arrayDestructuringTarget + } + + if (diagnostic.code === 2538) { + const elementAccessTarget = findElementAccessArgumentTarget(token) + if (elementAccessTarget && isExpressionTarget(elementAccessTarget) && !isAlreadyNonNull(elementAccessTarget.expression)) + return elementAccessTarget + } + + if (diagnostic.code === 7006) + return findImplicitAnyParameterTarget(token) + + if (diagnostic.code === 2488) { + const iterableTarget = findIterableTarget(sourceFile, token, start, end, checker) + if (iterableTarget && (!isExpressionTarget(iterableTarget) || !isAlreadyNonNull(iterableTarget.expression))) + return iterableTarget + } + + if (diagnostic.code === 2604 || diagnostic.code === 2786) { + const jsxComponentTarget = findJsxComponentDeclarationTarget(token, checker) + if (jsxComponentTarget && isExpressionTarget(jsxComponentTarget) && !isAlreadyNonNull(jsxComponentTarget.expression)) + return jsxComponentTarget + } + + const candidate = findDiagnosticCandidate(sourceFile, token, start, end, diagnostic.code, checker) + + if (!candidate) { + return jsxExpression && !isAlreadyNonNull(jsxExpression) + ? createExpressionTarget(jsxExpression) + : undefined + } + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code)) { + if ( + diagnostic.code === 2345 + && typeIncludesUndefined(checker.getTypeAtLocation(candidate)) + && !isAlreadyNonNull(candidate) + && ( + ts.isIdentifier(candidate) + || ts.isElementAccessExpression(candidate) + || ts.isPropertyAccessExpression(candidate) + ) + ) { + return createExpressionTarget(candidate) + } + + const referencedDeclarationTarget = findReferencedDeclarationInitializerTarget(candidate, checker) + if (referencedDeclarationTarget && isExpressionTarget(referencedDeclarationTarget) && !isAlreadyNonNull(referencedDeclarationTarget.expression)) + return referencedDeclarationTarget + + const collectionCallbackTarget = findCollectionCallbackTarget(candidate, checker) + if (collectionCallbackTarget && isExpressionTarget(collectionCallbackTarget) && !isAlreadyNonNull(collectionCallbackTarget.expression)) + return collectionCallbackTarget + } + + const targetFromCandidate = findTargetFromExpression(candidate, checker) + if (targetFromCandidate && (!isExpressionTarget(targetFromCandidate) || !isAlreadyNonNull(targetFromCandidate.expression))) + return targetFromCandidate + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) && (ts.isArrowFunction(candidate) || ts.isFunctionExpression(candidate))) { + const functionTarget = findFunctionLikeReturnTarget(candidate, checker) + if (functionTarget && isExpressionTarget(functionTarget) && !isAlreadyNonNull(functionTarget.expression)) + return functionTarget + } + + const nestedContainerTarget = findNestedContainerTarget(candidate, checker) + if (nestedContainerTarget) + return nestedContainerTarget + + if (isAlreadyNonNull(candidate)) + return undefined + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) && ts.isObjectLiteralExpression(candidate)) { + const objectLiteralTarget = findObjectLiteralPropertyTarget(candidate, checker) + if (objectLiteralTarget) + return objectLiteralTarget + } + + if (diagnostic.code === 2322) { + const declarationInitializerTarget = findVariableDeclarationInitializerTarget(sourceFile, token, checker) + if (declarationInitializerTarget && isExpressionTarget(declarationInitializerTarget) && !isAlreadyNonNull(declarationInitializerTarget.expression)) + return declarationInitializerTarget + } + + if (ASSIGNABILITY_DIAGNOSTIC_CODES.has(diagnostic.code) && !typeIncludesUndefined(checker.getTypeAtLocation(candidate))) + return undefined + + return createExpressionTarget(candidate) +} + +function createEditForTarget( + target: EditTarget, + printer: ts.Printer, +): TextEdit { + const sourceFile = target.sourceFile + + if (target.kind === 'direct-edit') { + return { + end: target.end, + expectedText: sourceFile.text.slice(target.start, target.end), + replacement: target.replacement, + start: target.start, + } + } + + if (target.kind === 'shorthand-property') { + const name = target.property.name + const nonNullName = printer.printNode( + ts.EmitHint.Expression, + ts.factory.createNonNullExpression(name), + sourceFile, + ) + return { + end: target.property.getEnd(), + expectedText: sourceFile.text.slice(target.property.getStart(sourceFile), target.property.getEnd()), + replacement: `${name.getText(sourceFile)}: ${nonNullName}`, + start: target.property.getStart(sourceFile), + } + } + + const replacement = shouldPrintInlineNonNullAssertion(target.expression) + ? `${target.expression.getText(sourceFile)}!` + : printer.printNode( + ts.EmitHint.Expression, + ts.factory.createNonNullExpression(target.expression), + sourceFile, + ) + + return { + end: target.expression.getEnd(), + expectedText: sourceFile.text.slice(target.expression.getStart(sourceFile), target.expression.getEnd()), + replacement, + start: target.expression.getStart(sourceFile), + } +} + +function hasOverlap(existingEdits: TextEdit[], nextEdit: TextEdit): boolean { + return existingEdits.some(edit => nextEdit.start < edit.end && edit.start < nextEdit.end) +} + +function applyEdits(text: string, edits: TextEdit[]): { appliedEditCount: number, text: string } { + let currentText = text + let appliedEditCount = 0 + + for (const edit of edits.sort((left, right) => right.start - left.start)) { + if (edit.replacement.length > currentText.length * 4) + continue + + try { + currentText = `${currentText.slice(0, edit.start)}${edit.replacement}${currentText.slice(edit.end)}` + appliedEditCount += 1 + } + catch { + continue + } + } + + return { + appliedEditCount, + text: currentText, + } +} + +function isValidEditRange(text: string, edit: TextEdit): boolean { + return Number.isInteger(edit.start) + && Number.isInteger(edit.end) + && edit.start >= 0 + && edit.end >= edit.start + && edit.end <= text.length +} + +function filterApplicableEdits(text: string, edits: TextEdit[]): TextEdit[] { + return edits.filter(edit => isValidEditRange(text, edit) && (!edit.expectedText || text.slice(edit.start, edit.end) === edit.expectedText)) +} + +export async function runMigration(options: CliOptions) { + const projectPath = path.resolve(process.cwd(), options.project) + const parsedConfig = parseTsConfig(projectPath) + const matchesRequestedFile = createFileMatcher(options.files) + const targetFiles = parsedConfig.fileNames + .map(normalizeFileName) + .filter(isTargetFile) + .filter(matchesRequestedFile) + + if (targetFiles.length === 0) { + console.error('No matching TypeScript source files found.') + process.exitCode = 1 + return { converged: false, totalEdits: 0 } + } + + const fileTexts = new Map() + const printer = ts.createPrinter() + const migrationRootNames = options.useFullProjectRoots + ? parsedConfig.fileNames.map(normalizeFileName) + : getMigrationRootNames(parsedConfig, targetFiles) + + let totalEdits = 0 + let converged = false + let previousProgram: ts.Program | undefined + + for (let iteration = 1; iteration <= options.maxIterations; iteration += 1) { + const program = createMigrationProgram(migrationRootNames, parsedConfig, fileTexts, previousProgram) + const checker = program.getTypeChecker() + const editsByFile = new Map() + + for (const fileName of targetFiles) { + const sourceFile = program.getSourceFile(fileName) + if (!sourceFile) + continue + + const diagnostics = program + .getSemanticDiagnostics(sourceFile) + .filter((diagnostic): diagnostic is ts.DiagnosticWithLocation => { + return diagnostic.file !== undefined + && diagnostic.start !== undefined + && diagnostic.length !== undefined + && SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code) + }) + + if (options.verbose && diagnostics.length > 0) + console.log(`file ${path.relative(process.cwd(), fileName)}: ${diagnostics.length} supported diagnostic(s)`) + + for (const diagnostic of diagnostics) { + const target = resolveEditTarget(sourceFile, diagnostic, checker) + if (!target) { + if (options.verbose) + console.log(`unresolved ${formatDiagnostic(diagnostic)}`) + continue + } + + const editFileName = target.sourceFile.fileName + const edit = createEditForTarget(target, printer) + const existing = editsByFile.get(editFileName) ?? [] + if (hasOverlap(existing, edit)) + continue + + existing.push(edit) + editsByFile.set(editFileName, existing) + + if (options.verbose) { + const position = target.sourceFile.getLineAndCharacterOfPosition(edit.start) + console.log(`iter ${iteration}: ${path.relative(process.cwd(), editFileName)}:${position.line + 1}:${position.character + 1} -> add !`) + } + } + } + + if (editsByFile.size === 0) { + console.log(`No more supported diagnostics after ${iteration - 1} iteration(s).`) + converged = true + break + } + + let iterationEditCount = 0 + + for (const [fileName, edits] of editsByFile) { + const currentText = fileTexts.get(fileName) ?? await fs.readFile(fileName, 'utf8') + const applicableEdits = filterApplicableEdits(currentText, edits) + if (applicableEdits.length === 0) + continue + + const { appliedEditCount, text: editedText } = applyEdits(currentText, applicableEdits) + if (appliedEditCount === 0) + continue + + const nextText = normalizeMalformedAssertions(editedText) + if (nextText === currentText) { + if (options.verbose) { + const firstEdit = applicableEdits[0] + console.log(`iter ${iteration}: no-op after normalization for ${path.relative(process.cwd(), fileName)}:${firstEdit?.start ?? 0} ${JSON.stringify(firstEdit ? currentText.slice(firstEdit.start, firstEdit.end) : '')} -> ${JSON.stringify(firstEdit?.replacement ?? '')}`) + } + continue + } + + fileTexts.set(fileName, nextText) + iterationEditCount += appliedEditCount + } + + totalEdits += iterationEditCount + console.log(`Iteration ${iteration}: ${iterationEditCount} edit(s) across ${editsByFile.size} file(s).`) + previousProgram = program + } + + if (totalEdits === 0) { + console.log('No supported noUncheckedIndexedAccess-style diagnostics were migrated.') + return { converged, totalEdits } + } + + if (!options.write) { + if (!converged) + console.log(`Stopped after reaching --max-iterations=${options.maxIterations}.`) + + console.log(`Dry run complete. ${totalEdits} edit(s) are ready. Re-run with --write to apply them.`) + return { converged, totalEdits } + } + + const changedFiles = Array.from(fileTexts.entries()) + await Promise.all(changedFiles.map(async ([fileName, text]) => { + await fs.writeFile(fileName, text) + })) + + if (!converged) + console.log(`Stopped after reaching --max-iterations=${options.maxIterations}.`) + + console.log(`Wrote ${totalEdits} edit(s) to ${changedFiles.length} file(s).`) + return { converged, totalEdits } +} + +export async function runMigrationCommand(argv: string[]) { + await runMigration(parseArgs(argv)) +} diff --git a/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts new file mode 100644 index 0000000000..d3b88736fc --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/normalize.ts @@ -0,0 +1,51 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' +import { normalizeMalformedAssertions } from './migrate' + +const ROOT = process.cwd() +const EXTENSIONS = new Set(['.ts', '.tsx']) + +async function collectFiles(directory: string): Promise { + const entries = await fs.readdir(directory, { withFileTypes: true }) + const files: string[] = [] + + for (const entry of entries) { + if (entry.name === 'node_modules' || entry.name === '.next') + continue + + const absolutePath = path.join(directory, entry.name) + if (entry.isDirectory()) { + files.push(...await collectFiles(absolutePath)) + continue + } + + if (!EXTENSIONS.has(path.extname(entry.name))) + continue + + files.push(absolutePath) + } + + return files +} + +async function main() { + const files = await collectFiles(ROOT) + let changedFileCount = 0 + + await Promise.all(files.map(async (fileName) => { + const currentText = await fs.readFile(fileName, 'utf8') + const nextText = normalizeMalformedAssertions(currentText) + if (nextText === currentText) + return + + await fs.writeFile(fileName, nextText) + changedFileCount += 1 + })) + + console.log(`Normalized malformed assertion syntax in ${changedFileCount} file(s).`) +} + +export async function runNormalizeCommand(_argv: string[]) { + await main() +} diff --git a/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts new file mode 100644 index 0000000000..ad655e4f11 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/src/no-unchecked-indexed-access/run.ts @@ -0,0 +1,325 @@ +import { execFile } from 'node:child_process' +import { createHash } from 'node:crypto' +import fs from 'node:fs/promises' +import os from 'node:os' +import path from 'node:path' +import process from 'node:process' +import { promisify } from 'node:util' +import { runMigration, SUPPORTED_DIAGNOSTIC_CODES } from './migrate' + +const execFileAsync = promisify(execFile) +const DIAGNOSTIC_PATTERN = /^(.+?\.(?:ts|tsx))\((\d+),(\d+)\): error TS(\d+): (.+)$/ +const DEFAULT_BATCH_SIZE = 100 +const DEFAULT_BATCH_ITERATIONS = 5 +const DEFAULT_MAX_ROUNDS = 20 +const TYPECHECK_CACHE_DIR = path.join(os.tmpdir(), 'migrate-no-unchecked-indexed-access') + +type CliOptions = { + batchIterations: number + batchSize: number + maxRounds: number + project: string + verbose: boolean +} + +type DiagnosticEntry = { + code: number + fileName: string + line: number + message: string +} + +function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = { + batchIterations: DEFAULT_BATCH_ITERATIONS, + batchSize: DEFAULT_BATCH_SIZE, + maxRounds: DEFAULT_MAX_ROUNDS, + project: 'tsconfig.json', + verbose: false, + } + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i] + + if (arg === '--') + continue + + if (arg === '--verbose') { + options.verbose = true + continue + } + + if (arg === '--project') { + const value = argv[i + 1] + if (!value) + throw new Error('Missing value for --project') + + options.project = value + i += 1 + continue + } + + if (arg === '--batch-size') { + const value = Number(argv[i + 1]) + if (!Number.isInteger(value) || value <= 0) + throw new Error('Invalid value for --batch-size') + + options.batchSize = value + i += 1 + continue + } + + if (arg === '--batch-iterations') { + const value = Number(argv[i + 1]) + if (!Number.isInteger(value) || value <= 0) + throw new Error('Invalid value for --batch-iterations') + + options.batchIterations = value + i += 1 + continue + } + + if (arg === '--max-rounds') { + const value = Number(argv[i + 1]) + if (!Number.isInteger(value) || value <= 0) + throw new Error('Invalid value for --max-rounds') + + options.maxRounds = value + i += 1 + continue + } + + throw new Error(`Unknown option: ${arg}`) + } + + return options +} + +function getTypeCheckBuildInfoPath(projectPath: string): string { + const hash = createHash('sha1') + .update(projectPath) + .digest('hex') + .slice(0, 16) + + return path.join(TYPECHECK_CACHE_DIR, `${hash}.tsbuildinfo`) +} + +async function runTypeCheck( + project: string, + options?: { + incremental?: boolean + }, +): Promise<{ diagnostics: DiagnosticEntry[], exitCode: number, rawOutput: string }> { + const projectPath = path.resolve(process.cwd(), project) + const projectDirectory = path.dirname(projectPath) + const buildInfoPath = getTypeCheckBuildInfoPath(projectPath) + const incremental = options?.incremental ?? true + + await fs.mkdir(TYPECHECK_CACHE_DIR, { recursive: true }) + + const tscArgs = ['exec', 'tsc', '--noEmit', '--pretty', 'false'] + if (incremental) { + tscArgs.push('--incremental', '--tsBuildInfoFile', buildInfoPath) + } + else { + tscArgs.push('--incremental', 'false') + } + tscArgs.push('--project', projectPath) + + try { + const { stdout, stderr } = await execFileAsync('pnpm', tscArgs, { + cwd: projectDirectory, + env: { + ...process.env, + NODE_OPTIONS: process.env.NODE_OPTIONS ?? '--max-old-space-size=8192', + }, + maxBuffer: 1024 * 1024 * 32, + }) + + const rawOutput = `${stdout}${stderr}`.trim() + return { + diagnostics: parseDiagnostics(rawOutput, projectDirectory), + exitCode: 0, + rawOutput, + } + } + catch (error) { + const exitCode = typeof error === 'object' && error && 'code' in error && typeof error.code === 'number' + ? error.code + : 1 + const stdout = typeof error === 'object' && error && 'stdout' in error && typeof error.stdout === 'string' + ? error.stdout + : '' + const stderr = typeof error === 'object' && error && 'stderr' in error && typeof error.stderr === 'string' + ? error.stderr + : '' + const rawOutput = `${stdout}${stderr}`.trim() + + return { + diagnostics: parseDiagnostics(rawOutput, projectDirectory), + exitCode, + rawOutput, + } + } +} + +function parseDiagnostics(rawOutput: string, projectDirectory: string): DiagnosticEntry[] { + return rawOutput + .split('\n') + .map(line => line.trim()) + .flatMap((line) => { + const match = line.match(DIAGNOSTIC_PATTERN) + if (!match) + return [] + + return [{ + code: Number(match[4]), + fileName: path.resolve(projectDirectory, match[1]!), + line: Number(match[2]), + message: match[5] ?? '', + }] + }) +} + +function summarizeCodes(diagnostics: DiagnosticEntry[]): string { + const counts = new Map() + for (const diagnostic of diagnostics) + counts.set(diagnostic.code, (counts.get(diagnostic.code) ?? 0) + 1) + + return Array.from(counts.entries()) + .sort((left, right) => right[1] - left[1]) + .slice(0, 8) + .map(([code, count]) => `TS${code}:${count}`) + .join(', ') +} + +function chunk(items: T[], size: number): T[][] { + const batches: T[][] = [] + for (let i = 0; i < items.length; i += size) + batches.push(items.slice(i, i + size)) + + return batches +} + +async function runBatchMigration(options: CliOptions) { + for (let round = 1; round <= options.maxRounds; round += 1) { + const { diagnostics, exitCode, rawOutput } = await runTypeCheck(options.project) + if (exitCode === 0) { + const finalCheck = await runTypeCheck(options.project, { incremental: false }) + if (finalCheck.exitCode !== 0) { + const finalDiagnostics = finalCheck.diagnostics + console.log(`Final cold type check found ${finalDiagnostics.length} diagnostic(s). ${summarizeCodes(finalDiagnostics)}`) + + if (options.verbose) { + for (const diagnostic of finalDiagnostics.slice(0, 40)) + console.log(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`) + } + + const finalSupportedFiles = Array.from(new Set( + finalDiagnostics + .filter(diagnostic => SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code)) + .map(diagnostic => diagnostic.fileName), + )) + + if (finalSupportedFiles.length > 0) { + console.log(` Final pass batch: ${finalSupportedFiles.length} file(s)`) + let finalResult = await runMigration({ + files: finalSupportedFiles, + maxIterations: options.batchIterations, + project: options.project, + verbose: options.verbose, + write: true, + }) + + if (finalResult.totalEdits === 0) { + console.log(' No edits produced; retrying final pass with full project roots.') + finalResult = await runMigration({ + files: finalSupportedFiles, + maxIterations: options.batchIterations, + project: options.project, + useFullProjectRoots: true, + verbose: options.verbose, + write: true, + }) + } + + if (finalResult.totalEdits > 0) + continue + } + + if (finalCheck.rawOutput) + process.stderr.write(`${finalCheck.rawOutput}\n`) + process.exitCode = 1 + return + } + + console.log(`Type check passed after ${round - 1} migration round(s).`) + return + } + + const supportedDiagnostics = diagnostics.filter(diagnostic => SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code)) + const unsupportedDiagnostics = diagnostics.filter(diagnostic => !SUPPORTED_DIAGNOSTIC_CODES.has(diagnostic.code)) + const supportedFiles = Array.from(new Set(supportedDiagnostics.map(diagnostic => diagnostic.fileName))) + + console.log(`Round ${round}: ${diagnostics.length} diagnostic(s). ${summarizeCodes(diagnostics)}`) + + if (options.verbose) { + for (const diagnostic of diagnostics.slice(0, 40)) + console.log(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`) + } + + if (supportedFiles.length === 0) { + console.error('No supported diagnostics remain to migrate.') + if (unsupportedDiagnostics.length > 0) { + console.error('Remaining unsupported diagnostics:') + for (const diagnostic of unsupportedDiagnostics.slice(0, 40)) + console.error(`${path.relative(process.cwd(), diagnostic.fileName)}:${diagnostic.line} TS${diagnostic.code} ${diagnostic.message}`) + } + if (rawOutput) + process.stderr.write(`${rawOutput}\n`) + process.exitCode = 1 + return + } + + let roundEdits = 0 + const batches = chunk(supportedFiles, options.batchSize) + + for (const [index, batch] of batches.entries()) { + console.log(` Batch ${index + 1}/${batches.length}: ${batch.length} file(s)`) + let result = await runMigration({ + files: batch, + maxIterations: options.batchIterations, + project: options.project, + verbose: options.verbose, + write: true, + }) + + if (result.totalEdits === 0) { + console.log(' No edits produced; retrying batch with full project roots.') + result = await runMigration({ + files: batch, + maxIterations: options.batchIterations, + project: options.project, + useFullProjectRoots: true, + verbose: options.verbose, + write: true, + }) + } + + roundEdits += result.totalEdits + } + + if (roundEdits === 0) { + console.error('Migration script made no edits in this round; stopping to avoid an infinite loop.') + process.exitCode = 1 + return + } + } + + console.error(`Reached --max-rounds=${options.maxRounds} before type check passed.`) + process.exitCode = 1 +} + +export async function runBatchMigrationCommand(argv: string[]) { + await runBatchMigration(parseArgs(argv)) +} diff --git a/packages/migrate-no-unchecked-indexed-access/vite.config.ts b/packages/migrate-no-unchecked-indexed-access/vite.config.ts new file mode 100644 index 0000000000..ac4aed1a06 --- /dev/null +++ b/packages/migrate-no-unchecked-indexed-access/vite.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from 'vite-plus' + +export default defineConfig({ + pack: { + clean: true, + deps: { + neverBundle: ['typescript'], + }, + entry: ['src/cli.ts'], + format: ['esm'], + outDir: 'dist', + platform: 'node', + sourcemap: true, + target: 'node22', + treeshake: true, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d37f6b7977..cdf5001e2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -624,6 +624,22 @@ importers: specifier: 'catalog:' version: 0.1.2 + packages/migrate-no-unchecked-indexed-access: + dependencies: + typescript: + specifier: 'catalog:' + version: 6.0.2 + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 25.6.0 + vite: + specifier: npm:@voidzero-dev/vite-plus-core@0.1.18 + version: '@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3)' + vite-plus: + specifier: 'catalog:' + version: 0.1.18(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(@voidzero-dev/vite-plus-core@0.1.18(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3))(esbuild@0.27.2)(happy-dom@20.9.0)(jiti@2.6.1)(sass@1.98.0)(terser@5.46.1)(tsx@4.21.0)(typescript@6.0.2)(yaml@2.8.3) + sdks/nodejs-client: devDependencies: '@eslint/js': diff --git a/web/__tests__/datasets/dataset-settings-flow.test.tsx b/web/__tests__/datasets/dataset-settings-flow.test.tsx index b4a5e78326..7c53401b15 100644 --- a/web/__tests__/datasets/dataset-settings-flow.test.tsx +++ b/web/__tests__/datasets/dataset-settings-flow.test.tsx @@ -311,7 +311,7 @@ describe('Dataset Settings Flow - Cross-Module Configuration Cascade', () => { await result.current.handleSave() }) - const savedBody = mockUpdateDatasetSetting.mock.calls[0][0].body as Record + const savedBody = mockUpdateDatasetSetting.mock.calls[0]![0].body as Record expect(savedBody).not.toHaveProperty('partial_member_list') }) }) diff --git a/web/__tests__/datasets/document-management.test.tsx b/web/__tests__/datasets/document-management.test.tsx index f9d80520ed..96937332a3 100644 --- a/web/__tests__/datasets/document-management.test.tsx +++ b/web/__tests__/datasets/document-management.test.tsx @@ -109,7 +109,7 @@ describe('Document Management Flow', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.options.history).toBe('replace') expect(update.searchParams.get('keyword')).toBe('test') expect(update.searchParams.get('page')).toBe('2') @@ -123,7 +123,7 @@ describe('Document Management Flow', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.options.history).toBe('replace') expect(update.searchParams.toString()).toBe('') }) diff --git a/web/__tests__/datasets/hit-testing-flow.test.tsx b/web/__tests__/datasets/hit-testing-flow.test.tsx index 93d6f77d8f..54f85c5200 100644 --- a/web/__tests__/datasets/hit-testing-flow.test.tsx +++ b/web/__tests__/datasets/hit-testing-flow.test.tsx @@ -282,7 +282,7 @@ describe('Hit Testing Flow', () => { const response = createHitTestingResponse(5) for (let i = 1; i < response.records.length; i++) { - expect(response.records[i - 1].score).toBeGreaterThanOrEqual(response.records[i].score) + expect(response.records[i - 1]!.score).toBeGreaterThanOrEqual(response.records[i]!.score) } }) @@ -290,8 +290,8 @@ describe('Hit Testing Flow', () => { const response = createHitTestingResponse(1) const record = response.records[0] - expect(record.segment.document.name).toBeTruthy() - expect(record.segment.document.data_source_type).toBeTruthy() + expect(record!.segment.document.name).toBeTruthy() + expect(record!.segment.document.data_source_type).toBeTruthy() }) }) diff --git a/web/__tests__/datasets/pipeline-datasource-flow.test.tsx b/web/__tests__/datasets/pipeline-datasource-flow.test.tsx index dc140e8514..d005fda558 100644 --- a/web/__tests__/datasets/pipeline-datasource-flow.test.tsx +++ b/web/__tests__/datasets/pipeline-datasource-flow.test.tsx @@ -113,8 +113,8 @@ describe('Pipeline Data Source Store Composition - Cross-Slice Integration', () store.getState().setLocalFileList(files) expect(store.getState().localFileList).toHaveLength(3) - expect(store.getState().localFileList[0].fileID).toBe('f1') - expect(store.getState().localFileList[2].fileID).toBe('f3') + expect(store.getState().localFileList[0]!.fileID).toBe('f1') + expect(store.getState().localFileList[2]!.fileID).toBe('f3') }) it('should update preview ref when setting file list', () => { @@ -157,7 +157,7 @@ describe('Pipeline Data Source Store Composition - Cross-Slice Integration', () store.getState().setOnlineDocuments(pages) expect(store.getState().onlineDocuments).toHaveLength(2) - expect(store.getState().onlineDocuments[0].page_id).toBe('page-1') + expect(store.getState().onlineDocuments[0]!.page_id).toBe('page-1') }) it('should update preview ref when setting online documents', () => { @@ -391,7 +391,7 @@ describe('Pipeline Data Source Store Composition - Cross-Slice Integration', () store.getState().setLocalFileList(files) // Step 3: Select current file for preview - store.getState().setCurrentLocalFile(files[0].file) + store.getState().setCurrentLocalFile(files[0]!.file) // Verify all state is consistent expect(store.getState().currentCredentialId).toBe('upload-cred-1') diff --git a/web/__tests__/datasets/segment-crud.test.tsx b/web/__tests__/datasets/segment-crud.test.tsx index 9190e17395..fe21dd5079 100644 --- a/web/__tests__/datasets/segment-crud.test.tsx +++ b/web/__tests__/datasets/segment-crud.test.tsx @@ -289,7 +289,7 @@ describe('Segment CRUD Flow', () => { // Open detail modal act(() => { - modalResult.current.onClickCard(segments[0]) + modalResult.current.onClickCard(segments[0]!) }) // All states should be independent diff --git a/web/__tests__/document-detail-navigation-fix.test.tsx b/web/__tests__/document-detail-navigation-fix.test.tsx index 5cb115830e..c5641d2adf 100644 --- a/web/__tests__/document-detail-navigation-fix.test.tsx +++ b/web/__tests__/document-detail-navigation-fix.test.tsx @@ -146,7 +146,7 @@ describe('Document Detail Navigation Fix Verification', () => { fireEvent.click(screen.getByTestId('back-button-fixed')) // URLSearchParams will normalize the encoding, but preserve all parameters - const expectedCall = mockPush.mock.calls[0][0] + const expectedCall = mockPush.mock.calls[0]![0] expect(expectedCall).toMatch(/^\/datasets\/dataset-123\/documents\?/) expect(expectedCall).toMatch(/page=1/) expect(expectedCall).toMatch(/limit=50/) @@ -257,7 +257,7 @@ describe('Document Detail Navigation Fix Verification', () => { // Should still attempt navigation (URLSearchParams will clean up the parameters) expect(mockPush).toHaveBeenCalled() - const navigationPath = mockPush.mock.calls[0][0] + const navigationPath = mockPush.mock.calls[0]![0] expect(navigationPath).toMatch(/^\/datasets\/dataset-123\/documents/) console.log('✅ Malformed parameters handled gracefully:', navigationPath) diff --git a/web/__tests__/explore/explore-app-list-flow.test.tsx b/web/__tests__/explore/explore-app-list-flow.test.tsx index 40f2156c06..e2c7831018 100644 --- a/web/__tests__/explore/explore-app-list-flow.test.tsx +++ b/web/__tests__/explore/explore-app-list-flow.test.tsx @@ -175,16 +175,16 @@ describe('Explore App List Flow', () => { it('should display all apps when no category filter is applied', () => { renderAppList() - expect(screen.getByText('Writer Bot')).toBeInTheDocument() - expect(screen.getByText('Translator')).toBeInTheDocument() - expect(screen.getByText('Code Helper')).toBeInTheDocument() + expect(screen.getByText('Writer Bot'))!.toBeInTheDocument() + expect(screen.getByText('Translator'))!.toBeInTheDocument() + expect(screen.getByText('Code Helper'))!.toBeInTheDocument() }) it('should filter apps by selected category', () => { mockTabValue = 'Writing' renderAppList() - expect(screen.getByText('Writer Bot')).toBeInTheDocument() + expect(screen.getByText('Writer Bot'))!.toBeInTheDocument() expect(screen.queryByText('Translator')).not.toBeInTheDocument() expect(screen.queryByText('Code Helper')).not.toBeInTheDocument() }) @@ -196,7 +196,7 @@ describe('Explore App List Flow', () => { fireEvent.change(input, { target: { value: 'trans' } }) await waitFor(() => { - expect(screen.getByText('Translator')).toBeInTheDocument() + expect(screen.getByText('Translator'))!.toBeInTheDocument() expect(screen.queryByText('Writer Bot')).not.toBeInTheDocument() expect(screen.queryByText('Code Helper')).not.toBeInTheDocument() }) @@ -218,7 +218,7 @@ describe('Explore App List Flow', () => { renderAppList(true, onSuccess) // Step 2: Click add to workspace button - opens create modal - fireEvent.click(screen.getAllByText('explore.appCard.addToWorkspace')[0]) + fireEvent.click(screen.getAllByText('explore.appCard.addToWorkspace')[0]!) // Step 3: Confirm creation in modal fireEvent.click(await screen.findByTestId('confirm-create')) @@ -232,7 +232,8 @@ describe('Explore App List Flow', () => { expect(mockHandleImportDSL).toHaveBeenCalledTimes(1) // Step 6: DSL confirm modal appears and user confirms - expect(await screen.findByTestId('dsl-confirm-modal')).toBeInTheDocument() + // Step 6: DSL confirm modal appears and user confirms + expect(await screen.findByTestId('dsl-confirm-modal'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('dsl-confirm')) // Step 7: Flow completes successfully @@ -250,7 +251,7 @@ describe('Explore App List Flow', () => { mockExploreData = undefined const { unmount } = render(appListElement()) - expect(screen.getByRole('status')).toBeInTheDocument() + expect(screen.getByRole('status'))!.toBeInTheDocument() // Step 2: Data loads mockIsLoading = false @@ -262,7 +263,7 @@ describe('Explore App List Flow', () => { renderAppList() expect(screen.queryByRole('status')).not.toBeInTheDocument() - expect(screen.getByText('Alpha')).toBeInTheDocument() + expect(screen.getByText('Alpha'))!.toBeInTheDocument() }) }) diff --git a/web/__tests__/goto-anything/slash-command-modes.test.tsx b/web/__tests__/goto-anything/slash-command-modes.test.tsx index 38c965e383..bcaddfb27b 100644 --- a/web/__tests__/goto-anything/slash-command-modes.test.tsx +++ b/web/__tests__/goto-anything/slash-command-modes.test.tsx @@ -108,8 +108,8 @@ describe('Slash Command Dual-Mode System', () => { const results = await handler?.search('', 'en') expect(results).toHaveLength(2) - expect(results?.[0].title).toBe('Light Theme') - expect(results?.[1].title).toBe('Dark Theme') + expect(results?.[0]!.title).toBe('Light Theme') + expect(results?.[1]!.title).toBe('Dark Theme') }) it('should not have execute function for submenu mode', () => { diff --git a/web/__tests__/plugins/plugin-data-utilities.test.ts b/web/__tests__/plugins/plugin-data-utilities.test.ts index 068b0e3238..cfec48ca4d 100644 --- a/web/__tests__/plugins/plugin-data-utilities.test.ts +++ b/web/__tests__/plugins/plugin-data-utilities.test.ts @@ -145,15 +145,15 @@ describe('Plugin Data Utilities Integration', () => { validCategory: getValidCategoryKeys(p.category), })) - expect(results[0].validTags.length).toBeGreaterThan(0) - expect(results[0].validCategory).toBe('tool') + expect(results[0]!.validTags.length).toBeGreaterThan(0) + expect(results[0]!.validCategory).toBe('tool') - expect(results[1].validTags).toContain('image') - expect(results[1].validTags).toContain('design') - expect(results[1].validCategory).toBe('model') + expect(results[1]!.validTags).toContain('image') + expect(results[1]!.validTags).toContain('design') + expect(results[1]!.validCategory).toBe('model') - expect(results[2].validTags).toHaveLength(0) - expect(results[2].validCategory).toBe('extension') + expect(results[2]!.validTags).toHaveLength(0) + expect(results[2]!.validCategory).toBe('extension') }) }) }) diff --git a/web/__tests__/plugins/plugin-page-shell-flow.test.tsx b/web/__tests__/plugins/plugin-page-shell-flow.test.tsx index 5895a20eef..9202f647af 100644 --- a/web/__tests__/plugins/plugin-page-shell-flow.test.tsx +++ b/web/__tests__/plugins/plugin-page-shell-flow.test.tsx @@ -132,16 +132,16 @@ describe('Plugin Page Shell Flow', () => { it('switches from installed plugins to marketplace and syncs the active tab into the URL', async () => { const { onUrlUpdate } = renderPluginPage() - expect(screen.getByTestId('plugins-view')).toBeInTheDocument() + expect(screen.getByTestId('plugins-view'))!.toBeInTheDocument() expect(screen.queryByTestId('marketplace-view')).not.toBeInTheDocument() fireEvent.click(screen.getByTestId('tab-item-discover')) await waitFor(() => { - expect(screen.getByTestId('marketplace-view')).toBeInTheDocument() + expect(screen.getByTestId('marketplace-view'))!.toBeInTheDocument() }) - const tabUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const tabUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(tabUpdate.searchParams.get('tab')).toBe('discover') }) @@ -150,13 +150,13 @@ describe('Plugin Page Shell Flow', () => { await waitFor(() => { expect(mockFetchManifestFromMarketPlace).toHaveBeenCalledWith('langgenius%2Fplugin-demo') - expect(screen.getByTestId('install-from-marketplace-modal')).toBeInTheDocument() + expect(screen.getByTestId('install-from-marketplace-modal'))!.toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'close-install-modal' })) await waitFor(() => { - const clearUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const clearUpdate = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(clearUpdate.searchParams.has('package-ids')).toBe(false) }) }) diff --git a/web/__tests__/rag-pipeline/chunk-preview-formatting.test.ts b/web/__tests__/rag-pipeline/chunk-preview-formatting.test.ts index c4cafbc1c5..6c8dfb982f 100644 --- a/web/__tests__/rag-pipeline/chunk-preview-formatting.test.ts +++ b/web/__tests__/rag-pipeline/chunk-preview-formatting.test.ts @@ -38,9 +38,9 @@ describe('Chunk Preview Formatting', () => { expect(Array.isArray(result)).toBe(true) const chunks = result as Array<{ content: string, summary?: string }> expect(chunks).toHaveLength(2) - expect(chunks[0].content).toBe('Chunk 1 content') - expect(chunks[0].summary).toBe('Summary 1') - expect(chunks[1].content).toBe('Chunk 2 content') + expect(chunks[0]!.content).toBe('Chunk 1 content') + expect(chunks[0]!.summary).toBe('Summary 1') + expect(chunks[1]!.content).toBe('Chunk 2 content') }) it('should limit chunks to RAG_PIPELINE_PREVIEW_CHUNK_NUM', () => { @@ -84,9 +84,9 @@ describe('Chunk Preview Formatting', () => { expect(result.parent_mode).toBe('paragraph') expect(result.parent_child_chunks).toHaveLength(1) - expect(result.parent_child_chunks[0].parent_content).toBe('Parent paragraph') - expect(result.parent_child_chunks[0].parent_summary).toBe('Parent summary') - expect(result.parent_child_chunks[0].child_contents).toEqual(['Child 1', 'Child 2']) + expect(result.parent_child_chunks[0]!.parent_content).toBe('Parent paragraph') + expect(result.parent_child_chunks[0]!.parent_summary).toBe('Parent summary') + expect(result.parent_child_chunks[0]!.child_contents).toEqual(['Child 1', 'Child 2']) }) it('should limit parent chunks in paragraph mode', () => { @@ -129,8 +129,8 @@ describe('Chunk Preview Formatting', () => { } expect(result.parent_child_chunks).toHaveLength(1) - expect(result.parent_child_chunks[0].parent_content).toBe('Full document content') - expect(result.parent_child_chunks[0].parent_mode).toBe('full-doc') + expect(result.parent_child_chunks[0]!.parent_content).toBe('Full document content') + expect(result.parent_child_chunks[0]!.parent_mode).toBe('full-doc') }) it('should limit child chunks in full-doc mode', () => { @@ -149,7 +149,7 @@ describe('Chunk Preview Formatting', () => { parent_child_chunks: Array<{ child_contents: string[] }> } - expect(result.parent_child_chunks[0].child_contents).toHaveLength(3) // Mocked limit + expect(result.parent_child_chunks[0]!.child_contents).toHaveLength(3) // Mocked limit }) }) @@ -168,8 +168,8 @@ describe('Chunk Preview Formatting', () => { } expect(result.qa_chunks).toHaveLength(2) - expect(result.qa_chunks[0].question).toBe('What is AI?') - expect(result.qa_chunks[0].answer).toBe('Artificial Intelligence is...') + expect(result.qa_chunks[0]!.question).toBe('What is AI?') + expect(result.qa_chunks[0]!.answer).toBe('Artificial Intelligence is...') }) it('should limit QA chunks', () => { diff --git a/web/__tests__/rag-pipeline/input-field-crud-flow.test.ts b/web/__tests__/rag-pipeline/input-field-crud-flow.test.ts index 233c9a288a..981107fe44 100644 --- a/web/__tests__/rag-pipeline/input-field-crud-flow.test.ts +++ b/web/__tests__/rag-pipeline/input-field-crud-flow.test.ts @@ -268,11 +268,11 @@ describe('Input Field CRUD Flow', () => { const restoredFields = formDataList.map(fd => convertFormDataToINputField(fd)) expect(restoredFields).toHaveLength(2) - expect(restoredFields[0].variable).toBe('name') - expect(restoredFields[0].default_value).toBe('Alice') - expect(restoredFields[1].variable).toBe('count') - expect(restoredFields[1].default_value).toBe('10') - expect(restoredFields[1].unit).toBe('items') + expect(restoredFields[0]!.variable).toBe('name') + expect(restoredFields[0]!.default_value).toBe('Alice') + expect(restoredFields[1]!.variable).toBe('count') + expect(restoredFields[1]!.default_value).toBe('10') + expect(restoredFields[1]!.unit).toBe('items') }) }) }) diff --git a/web/__tests__/rag-pipeline/test-run-flow.test.ts b/web/__tests__/rag-pipeline/test-run-flow.test.ts index a2bf557acd..4ed31a5713 100644 --- a/web/__tests__/rag-pipeline/test-run-flow.test.ts +++ b/web/__tests__/rag-pipeline/test-run-flow.test.ts @@ -139,8 +139,8 @@ describe('Test Run Flow Integration', () => { const { result } = renderHook(() => useTestRunSteps()) expect(result.current.steps).toHaveLength(2) - expect(result.current.steps[0].value).toBe('dataSource') - expect(result.current.steps[1].value).toBe('documentProcessing') + expect(result.current.steps[0]!.value).toBe('dataSource') + expect(result.current.steps[1]!.value).toBe('documentProcessing') }) }) @@ -153,8 +153,8 @@ describe('Test Run Flow Integration', () => { // Should only include DataSource nodes, not KnowledgeBase expect(result.current).toHaveLength(2) - expect(result.current[0].value).toBe('ds-1') - expect(result.current[1].value).toBe('ds-2') + expect(result.current[0]!.value).toBe('ds-1') + expect(result.current[1]!.value).toBe('ds-2') }) it('should include node data in options', async () => { @@ -163,8 +163,8 @@ describe('Test Run Flow Integration', () => { ) const { result } = renderHook(() => useDatasourceOptions()) - expect(result.current[0].label).toBe('Local Files') - expect(result.current[0].data.type).toBe(BlockEnum.DataSource) + expect(result.current[0]!.label).toBe('Local Files') + expect(result.current[0]!.data.type).toBe(BlockEnum.DataSource) }) }) diff --git a/web/__tests__/real-browser-flicker.test.tsx b/web/__tests__/real-browser-flicker.test.tsx index 60f97d6938..7c3b45af19 100644 --- a/web/__tests__/real-browser-flicker.test.tsx +++ b/web/__tests__/real-browser-flicker.test.tsx @@ -296,7 +296,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { // Wait for theme system to fully initialize await waitFor(() => { - expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: dark') + expect(screen.getByTestId('theme-indicator'))!.toHaveTextContent('Current Theme: dark') }) const finalState = { @@ -319,10 +319,10 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: light') + expect(screen.getByTestId('theme-indicator'))!.toHaveTextContent('Current Theme: light') }) - expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light') + expect(screen.getByTestId('visual-appearance'))!.toHaveTextContent('Appearance: light') }) it('handles system theme with dark preference', async () => { @@ -335,10 +335,10 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: dark') + expect(screen.getByTestId('theme-indicator'))!.toHaveTextContent('Current Theme: dark') }) - expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: dark') + expect(screen.getByTestId('visual-appearance'))!.toHaveTextContent('Appearance: dark') }) it('handles system theme with light preference', async () => { @@ -351,10 +351,10 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: light') + expect(screen.getByTestId('theme-indicator'))!.toHaveTextContent('Current Theme: light') }) - expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light') + expect(screen.getByTestId('visual-appearance'))!.toHaveTextContent('Appearance: light') }) it('handles no stored theme (defaults to system)', async () => { @@ -367,7 +367,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('theme-indicator')).toHaveTextContent('Current Theme: light') + expect(screen.getByTestId('theme-indicator'))!.toHaveTextContent('Current Theme: light') }) }) @@ -384,7 +384,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('timing-status')).toHaveTextContent('Phase: CSR') + expect(screen.getByTestId('timing-status'))!.toHaveTextContent('Phase: CSR') }) // Analyze timing and style changes @@ -395,7 +395,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { // Check if there are style changes (this is visible flicker) const hasStyleChange = timingData.length > 1 - && timingData[0].styles.backgroundColor !== timingData[timingData.length - 1].styles.backgroundColor + && timingData[0]!.styles.backgroundColor !== timingData[timingData.length - 1]!.styles.backgroundColor if (hasStyleChange) console.log('⚠️ Style changes detected - this causes visible flicker') @@ -420,7 +420,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('css-classes')).toHaveTextContent('bg-gray-900 text-white') + expect(screen.getByTestId('css-classes'))!.toHaveTextContent('bg-gray-900 text-white') }) console.log('\n=== CSS Class Change Detection ===') @@ -430,12 +430,12 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { // Check if CSS classes have changed const hasCSSChange = cssStates.length > 1 - && cssStates[0].className !== cssStates[cssStates.length - 1].className + && cssStates[0]!.className !== cssStates[cssStates.length - 1]!.className if (hasCSSChange) { console.log('⚠️ CSS class changes detected - may cause style flicker') - console.log(`From: "${cssStates[0].className}"`) - console.log(`To: "${cssStates[cssStates.length - 1].className}"`) + console.log(`From: "${cssStates[0]!.className}"`) + console.log(`To: "${cssStates[cssStates.length - 1]!.className}"`) } expect(hasCSSChange).toBe(true) // We expect to see this change @@ -469,11 +469,12 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { // Should fallback gracefully without crashing await waitFor(() => { - expect(screen.getByTestId('theme-indicator')).toBeInTheDocument() + expect(screen.getByTestId('theme-indicator'))!.toBeInTheDocument() }) // Should default to light theme when localStorage fails - expect(screen.getByTestId('visual-appearance')).toHaveTextContent('Appearance: light') + // Should default to light theme when localStorage fails + expect(screen.getByTestId('visual-appearance'))!.toHaveTextContent('Appearance: light') } finally { Reflect.deleteProperty(window, 'localStorage') @@ -490,12 +491,12 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('theme-indicator')).toBeInTheDocument() + expect(screen.getByTestId('theme-indicator'))!.toBeInTheDocument() }) // Should handle invalid values gracefully const themeIndicator = screen.getByTestId('theme-indicator') - expect(themeIndicator).toBeInTheDocument() + expect(themeIndicator)!.toBeInTheDocument() }) }) @@ -516,7 +517,7 @@ describe('Real Browser Environment Dark Mode Flicker Test', () => { ) await waitFor(() => { - expect(screen.getByTestId('performance-test')).toHaveTextContent('Theme: dark') + expect(screen.getByTestId('performance-test'))!.toHaveTextContent('Theme: dark') }) // Analyze performance timeline diff --git a/web/__tests__/tools/provider-list-shell-flow.test.tsx b/web/__tests__/tools/provider-list-shell-flow.test.tsx index 5b6ba8a64b..d0d096f072 100644 --- a/web/__tests__/tools/provider-list-shell-flow.test.tsx +++ b/web/__tests__/tools/provider-list-shell-flow.test.tsx @@ -174,7 +174,7 @@ describe('Tool Provider List Shell Flow', () => { fireEvent.click(screen.getByTestId('tool-card-plugin-tool')) await waitFor(() => { - expect(screen.getByTestId('tool-plugin-detail-panel')).toBeInTheDocument() + expect(screen.getByTestId('tool-plugin-detail-panel'))!.toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: 'update-plugin-detail' })) @@ -196,10 +196,10 @@ describe('Tool Provider List Shell Flow', () => { fireEvent.click(screen.getByTestId('tab-item-workflow')) await waitFor(() => { - expect(screen.getByTestId('workflow-empty')).toBeInTheDocument() + expect(screen.getByTestId('workflow-empty'))!.toBeInTheDocument() }) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.searchParams.get('category')).toBe('workflow') }) }) diff --git a/web/__tests__/tools/tool-data-processing.test.ts b/web/__tests__/tools/tool-data-processing.test.ts index 120461201f..69be82447a 100644 --- a/web/__tests__/tools/tool-data-processing.test.ts +++ b/web/__tests__/tools/tool-data-processing.test.ts @@ -50,11 +50,11 @@ describe('Tool Data Processing Pipeline Integration', () => { const formSchemas = toolParametersToFormSchemas(rawParameters as unknown as Parameters[0]) expect(formSchemas).toHaveLength(2) - expect(formSchemas[0].variable).toBe('query') - expect(formSchemas[0].required).toBe(true) - expect(formSchemas[0].type).toBe('text-input') - expect(formSchemas[1].variable).toBe('limit') - expect(formSchemas[1].type).toBe('number-input') + expect(formSchemas[0]!.variable).toBe('query') + expect(formSchemas[0]!.required).toBe(true) + expect(formSchemas[0]!.type).toBe('text-input') + expect(formSchemas[1]!.variable).toBe('limit') + expect(formSchemas[1]!.type).toBe('number-input') const withDefaults = addDefaultValue({}, formSchemas) expect(withDefaults.query).toBe('hello') @@ -83,9 +83,9 @@ describe('Tool Data Processing Pipeline Integration', () => { const credentialSchemas = toolCredentialToFormSchemas(rawCredentials as Parameters[0]) expect(credentialSchemas).toHaveLength(1) - expect(credentialSchemas[0].variable).toBe('api_key') - expect(credentialSchemas[0].required).toBe(true) - expect(credentialSchemas[0].type).toBe('secret-input') + expect(credentialSchemas[0]!.variable).toBe('api_key') + expect(credentialSchemas[0]!.required).toBe(true) + expect(credentialSchemas[0]!.type).toBe('secret-input') }) it('processes trigger event parameters through the pipeline', () => { @@ -107,9 +107,9 @@ describe('Tool Data Processing Pipeline Integration', () => { const schemas = triggerEventParametersToFormSchemas(rawParams as unknown as Parameters[0]) expect(schemas).toHaveLength(1) - expect(schemas[0].name).toBe('event_type') - expect(schemas[0].type).toBe('select') - expect(schemas[0].options).toHaveLength(2) + expect(schemas[0]!.name).toBe('event_type') + expect(schemas[0]!.type).toBe('select') + expect(schemas[0]!.options).toHaveLength(2) }) }) @@ -189,16 +189,16 @@ describe('Tool Data Processing Pipeline Integration', () => { ] as Parameters[1] const sorted = sortAgentSorts(thoughts) - expect(sorted[0].id).toBe('t1') - expect(sorted[1].id).toBe('t2') - expect(sorted[2].id).toBe('t3') + expect(sorted[0]!.id).toBe('t1') + expect(sorted[1]!.id).toBe('t2') + expect(sorted[2]!.id).toBe('t3') const enriched = addFileInfos(sorted, messageFiles) - expect(enriched[0].message_files).toBeUndefined() - expect(enriched[1].message_files).toHaveLength(1) - expect(enriched[1].message_files![0].id).toBe('f2') - expect(enriched[2].message_files).toHaveLength(1) - expect(enriched[2].message_files![0].id).toBe('f1') + expect(enriched[0]!.message_files).toBeUndefined() + expect(enriched[1]!.message_files).toHaveLength(1) + expect(enriched[1]!.message_files![0]!.id).toBe('f2') + expect(enriched[2]!.message_files).toHaveLength(1) + expect(enriched[2]!.message_files![0]!.id).toBe('f1') }) it('handles null inputs gracefully in the pipeline', () => { diff --git a/web/app/account/oauth/authorize/page.tsx b/web/app/account/oauth/authorize/page.tsx index fe24afe7ff..f370efcd3e 100644 --- a/web/app/account/oauth/authorize/page.tsx +++ b/web/app/account/oauth/authorize/page.tsx @@ -151,7 +151,7 @@ export default function OAuthAuthorize() { return (
{Icon ? : } - {Icon.label} + {Icon!.label}
) })} diff --git a/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx b/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx index fc0bb56f75..2f0d748ae9 100644 --- a/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx +++ b/web/app/components/app-sidebar/app-info/__tests__/index.spec.tsx @@ -89,7 +89,7 @@ describe('AppInfo', () => { it('should render trigger when not onlyShowDetail', () => { render() - expect(screen.getByTestId('trigger')).toBeInTheDocument() + expect(screen.getByTestId('trigger'))!.toBeInTheDocument() }) it('should not render trigger when onlyShowDetail is true', () => { @@ -99,11 +99,11 @@ describe('AppInfo', () => { it('should pass expand prop to trigger', () => { render() - expect(screen.getByTestId('trigger')).toHaveAttribute('data-expand', 'true') + expect(screen.getByTestId('trigger'))!.toHaveAttribute('data-expand', 'true') const { unmount } = render() const triggers = screen.getAllByTestId('trigger') - expect(triggers[triggers.length - 1]).toHaveAttribute('data-expand', 'false') + expect(triggers[triggers.length - 1])!.toHaveAttribute('data-expand', 'false') unmount() }) @@ -114,7 +114,7 @@ describe('AppInfo', () => { await user.click(screen.getByTestId('trigger')) expect(mockSetPanelOpen).toHaveBeenCalled() - const updater = mockSetPanelOpen.mock.calls[0][0] as (v: boolean) => boolean + const updater = mockSetPanelOpen.mock.calls[0]![0] as (v: boolean) => boolean expect(updater(false)).toBe(true) expect(updater(true)).toBe(false) }) @@ -132,12 +132,12 @@ describe('AppInfo', () => { it('should show detail panel based on panelOpen when not onlyShowDetail', () => { mockUseAppInfoActions.panelOpen = true render() - expect(screen.getByTestId('detail-panel')).toBeInTheDocument() + expect(screen.getByTestId('detail-panel'))!.toBeInTheDocument() }) it('should show detail panel based on openState when onlyShowDetail', () => { render() - expect(screen.getByTestId('detail-panel')).toBeInTheDocument() + expect(screen.getByTestId('detail-panel'))!.toBeInTheDocument() }) it('should hide detail panel when openState is false and onlyShowDetail', () => { diff --git a/web/app/components/app/annotation/__tests__/list.spec.tsx b/web/app/components/app/annotation/__tests__/list.spec.tsx index aa47a5304b..ad6a1e5e9c 100644 --- a/web/app/components/app/annotation/__tests__/list.spec.tsx +++ b/web/app/components/app/annotation/__tests__/list.spec.tsx @@ -64,7 +64,7 @@ describe('List', () => { ) const checkboxes = getCheckboxes(container) - fireEvent.click(checkboxes[1]) + fireEvent.click(checkboxes[1]!) expect(onSelectedIdsChange).toHaveBeenCalledWith(['a']) rerender( @@ -79,10 +79,10 @@ describe('List', () => { />, ) const updatedCheckboxes = getCheckboxes(container) - fireEvent.click(updatedCheckboxes[1]) + fireEvent.click(updatedCheckboxes[1]!) expect(onSelectedIdsChange).toHaveBeenCalledWith([]) - fireEvent.click(updatedCheckboxes[0]) + fireEvent.click(updatedCheckboxes[0]!) expect(onSelectedIdsChange).toHaveBeenCalledWith(['a', 'b']) }) @@ -103,13 +103,13 @@ describe('List', () => { const row = screen.getByText(item.question).closest('tr') as HTMLTableRowElement const actionButtons = within(row).getAllByRole('button') - fireEvent.click(actionButtons[1]) + fireEvent.click(actionButtons[1]!) - expect(await screen.findByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument() + expect(await screen.findByText('appDebug.feature.annotation.removeConfirm'))!.toBeInTheDocument() const confirmButton = await screen.findByRole('button', { name: 'common.operation.confirm' }) fireEvent.click(confirmButton) expect(onRemove).toHaveBeenCalledWith(item.id) - expect(screen.getByText('appAnnotation.batchAction.selected')).toBeInTheDocument() + expect(screen.getByText('appAnnotation.batchAction.selected'))!.toBeInTheDocument() }) }) diff --git a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx index 5104706045..3176b1addb 100644 --- a/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/batch-add-annotation-modal/__tests__/index.spec.tsx @@ -96,15 +96,15 @@ describe('BatchModal', () => { renderComponent() - expect(screen.getByTestId('annotation-full')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'appAnnotation.batchModal.run' })).toBeDisabled() + expect(screen.getByTestId('annotation-full'))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'appAnnotation.batchModal.run' }))!.toBeDisabled() }) it('should reset uploader state when modal closes and allow manual cancellation', () => { const { rerender, props } = renderComponent() fireEvent.click(screen.getByTestId('mock-uploader')) - expect(screen.getByTestId('selected-file')).toHaveTextContent('batch.csv') + expect(screen.getByTestId('selected-file'))!.toHaveTextContent('batch.csv') rerender() rerender() @@ -137,7 +137,7 @@ describe('BatchModal', () => { expect(annotationBatchImportMock).toHaveBeenCalledTimes(1) }) - const formData = annotationBatchImportMock.mock.calls[0][0].body as FormData + const formData = annotationBatchImportMock.mock.calls[0]![0].body as FormData expect(formData.get('file')).toBe(lastUploadedFile) await waitFor(() => { diff --git a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx index 53a6f7356a..4844017dde 100644 --- a/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/edit-annotation-modal/__tests__/index.spec.tsx @@ -75,7 +75,8 @@ describe('EditAnnotationModal', () => { render() // Assert - Check for modal title as it appears in the mock - expect(screen.getByText('appAnnotation.editModal.title')).toBeInTheDocument() + // Assert - Check for modal title as it appears in the mock + expect(screen.getByText('appAnnotation.editModal.title'))!.toBeInTheDocument() }) it('should not render modal when isShow is false', () => { @@ -85,6 +86,37 @@ describe('EditAnnotationModal', () => { // Act render() + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert // Assert expect(screen.queryByText('appAnnotation.editModal.title')).not.toBeInTheDocument() }) @@ -97,8 +129,9 @@ describe('EditAnnotationModal', () => { render() // Assert - Look for query and answer content - expect(screen.getByText('Test query')).toBeInTheDocument() - expect(screen.getByText('Test answer')).toBeInTheDocument() + // Assert - Look for query and answer content + expect(screen.getByText('Test query'))!.toBeInTheDocument() + expect(screen.getByText('Test answer'))!.toBeInTheDocument() }) }) @@ -116,8 +149,9 @@ describe('EditAnnotationModal', () => { render() // Assert - Check content is displayed - expect(screen.getByText('Custom query content')).toBeInTheDocument() - expect(screen.getByText('Custom answer content')).toBeInTheDocument() + // Assert - Check content is displayed + expect(screen.getByText('Custom query content'))!.toBeInTheDocument() + expect(screen.getByText('Custom answer content'))!.toBeInTheDocument() }) it('should show remove option when annotationId is provided', () => { @@ -131,7 +165,8 @@ describe('EditAnnotationModal', () => { render() // Assert - Remove option should be present (using pattern) - expect(screen.getByText('appAnnotation.editModal.removeThisCache')).toBeInTheDocument() + // Assert - Remove option should be present (using pattern) + expect(screen.getByText('appAnnotation.editModal.removeThisCache'))!.toBeInTheDocument() }) }) @@ -160,7 +195,8 @@ describe('EditAnnotationModal', () => { render() // Assert - expect(screen.getByText('appAnnotation.editModal.removeThisCache')).toBeInTheDocument() + // Assert + expect(screen.getByText('appAnnotation.editModal.removeThisCache'))!.toBeInTheDocument() }) it('should save content when edited', async () => { @@ -183,7 +219,7 @@ describe('EditAnnotationModal', () => { // Find and click edit link for query const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) // Find textarea and enter new content const textarea = screen.getByRole('textbox') @@ -225,7 +261,7 @@ describe('EditAnnotationModal', () => { // Edit query content const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) const textarea = screen.getByRole('textbox') await user.clear(textarea) @@ -258,7 +294,7 @@ describe('EditAnnotationModal', () => { // Edit query content const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) const textarea = screen.getByRole('textbox') await user.clear(textarea) @@ -289,6 +325,37 @@ describe('EditAnnotationModal', () => { // Act render() + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially + // Assert - Confirm dialog should not be visible initially // Assert - Confirm dialog should not be visible initially expect(screen.queryByText('appDebug.feature.annotation.removeConfirm')).not.toBeInTheDocument() }) @@ -306,7 +373,8 @@ describe('EditAnnotationModal', () => { await user.click(screen.getByText('appAnnotation.editModal.removeThisCache')) // Assert - Confirmation dialog should appear - expect(screen.getByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument() + // Assert - Confirmation dialog should appear + expect(screen.getByText('appDebug.feature.annotation.removeConfirm'))!.toBeInTheDocument() }) it('should call onRemove when removal is confirmed', async () => { @@ -342,7 +410,7 @@ describe('EditAnnotationModal', () => { render() await user.click(screen.getByText('appAnnotation.editModal.removeThisCache')) - expect(screen.getByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument() + expect(screen.getByText('appDebug.feature.annotation.removeConfirm'))!.toBeInTheDocument() await user.click(screen.getByRole('button', { name: 'common.operation.cancel' })) @@ -366,7 +434,8 @@ describe('EditAnnotationModal', () => { render() // Assert - expect(screen.getByText('appAnnotation.editModal.title')).toBeInTheDocument() + // Assert + expect(screen.getByText('appAnnotation.editModal.title'))!.toBeInTheDocument() }) it('should handle very long content', () => { @@ -383,8 +452,9 @@ describe('EditAnnotationModal', () => { render() // Assert - expect(screen.getByText(longQuery)).toBeInTheDocument() - expect(screen.getByText(longAnswer)).toBeInTheDocument() + // Assert + expect(screen.getByText(longQuery))!.toBeInTheDocument() + expect(screen.getByText(longAnswer))!.toBeInTheDocument() }) it('should handle special characters in content', () => { @@ -401,8 +471,9 @@ describe('EditAnnotationModal', () => { render() // Assert - expect(screen.getByText(specialQuery)).toBeInTheDocument() - expect(screen.getByText(specialAnswer)).toBeInTheDocument() + // Assert + expect(screen.getByText(specialQuery))!.toBeInTheDocument() + expect(screen.getByText(specialAnswer))!.toBeInTheDocument() }) it('should handle onlyEditResponse prop', () => { @@ -440,7 +511,7 @@ describe('EditAnnotationModal', () => { // Find and click edit link for query const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) // Find textarea and enter new content const textarea = screen.getByRole('textbox') @@ -458,8 +529,9 @@ describe('EditAnnotationModal', () => { expect(mockOnAdded).not.toHaveBeenCalled() // Verify edit mode remains open (textarea should still be visible) - expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' }))!.toBeInTheDocument() }) it('should show fallback error message when addAnnotation error has no message', async () => { @@ -477,7 +549,7 @@ describe('EditAnnotationModal', () => { render() const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) const textarea = screen.getByRole('textbox') await user.clear(textarea) @@ -493,8 +565,9 @@ describe('EditAnnotationModal', () => { expect(mockOnAdded).not.toHaveBeenCalled() // Verify edit mode remains open (textarea should still be visible) - expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' }))!.toBeInTheDocument() }) it('should show error toast and skip callbacks when editAnnotation fails', async () => { @@ -516,7 +589,7 @@ describe('EditAnnotationModal', () => { // Edit query content const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) const textarea = screen.getByRole('textbox') await user.clear(textarea) @@ -532,8 +605,9 @@ describe('EditAnnotationModal', () => { expect(mockOnEdited).not.toHaveBeenCalled() // Verify edit mode remains open (textarea should still be visible) - expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' }))!.toBeInTheDocument() }) it('should show fallback error message when editAnnotation error is not an Error instance', async () => { @@ -553,7 +627,7 @@ describe('EditAnnotationModal', () => { render() const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) const textarea = screen.getByRole('textbox') await user.clear(textarea) @@ -569,8 +643,9 @@ describe('EditAnnotationModal', () => { expect(mockOnEdited).not.toHaveBeenCalled() // Verify edit mode remains open (textarea should still be visible) - expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'common.operation.save' })).toBeInTheDocument() + // Verify edit mode remains open (textarea should still be visible) + expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'common.operation.save' }))!.toBeInTheDocument() }) }) @@ -589,7 +664,7 @@ describe('EditAnnotationModal', () => { // Assert - Check that the formatted time appears somewhere in the component const container = screen.getByRole('dialog') - expect(container).toHaveTextContent('2023-12-01 10:30:00') + expect(container)!.toHaveTextContent('2023-12-01 10:30:00') }) it('should not show createdAt when not provided', () => { @@ -619,7 +694,8 @@ describe('EditAnnotationModal', () => { render() // Assert - Should have remove functionality - expect(screen.getByText('appAnnotation.editModal.removeThisCache')).toBeInTheDocument() + // Assert - Should have remove functionality + expect(screen.getByText('appAnnotation.editModal.removeThisCache'))!.toBeInTheDocument() }) }) @@ -634,7 +710,7 @@ describe('EditAnnotationModal', () => { render() const editLinks = screen.getAllByText(/common\.operation\.edit/i) - await user.click(editLinks[0]) + await user.click(editLinks[0]!) const textarea = screen.getByRole('textbox') await user.clear(textarea) @@ -661,7 +737,8 @@ describe('EditAnnotationModal', () => { rerender() // Assert - Component should still be visible (no errors thrown) - expect(screen.getByText('appAnnotation.editModal.title')).toBeInTheDocument() + // Assert - Component should still be visible (no errors thrown) + expect(screen.getByText('appAnnotation.editModal.title'))!.toBeInTheDocument() }) it('should re-render when props change', () => { @@ -674,7 +751,8 @@ describe('EditAnnotationModal', () => { rerender() // Assert - Should show new content - expect(screen.getByText('New query content')).toBeInTheDocument() + // Assert - Should show new content + expect(screen.getByText('New query content'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx b/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx index a4e2d98917..944a8563eb 100644 --- a/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/header-opts/__tests__/index.spec.tsx @@ -172,7 +172,7 @@ type HeaderOptionsProps = ComponentProps const renderComponent = ( props: Partial = {}, - locale: Locale = LanguagesSupported[0], + locale: Locale = LanguagesSupported[0]!, ) => { ;(useLocale as Mock).mockReturnValue(locale) @@ -279,8 +279,8 @@ describe('HeaderOptions', () => { const { csvButton, jsonButton } = await getExportButtons() - expect(csvButton).toBeDisabled() - expect(jsonButton).toBeDisabled() + expect(csvButton)!.toBeDisabled() + expect(jsonButton)!.toBeDisabled() expect(lastCSVDownloaderProps).toMatchObject({ data: [['Question', 'Answer']], @@ -323,7 +323,7 @@ describe('HeaderOptions', () => { await openOperationsPopover(user) await clickOperationAction(user, 'appAnnotation.table.header.bulkImport') - expect(await screen.findByText('appAnnotation.batchModal.title')).toBeInTheDocument() + expect(await screen.findByText('appAnnotation.batchModal.title'))!.toBeInTheDocument() await user.click( screen.getByRole('button', { name: 'appAnnotation.batchModal.cancel' }), ) @@ -375,7 +375,7 @@ describe('HeaderOptions', () => { }) const lines = blobContent.trim().split('\n') expect(lines).toHaveLength(1) - expect(JSON.parse(lines[0])).toEqual({ + expect(JSON.parse(lines[0]!)).toEqual({ messages: [ { role: 'system', content: '' }, { role: 'user', content: 'Question 1' }, diff --git a/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx b/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx index d05fa27ccd..1b46443bd8 100644 --- a/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/__tests__/index.spec.tsx @@ -126,7 +126,8 @@ describe('ViewAnnotationModal', () => { fireEvent.click(screen.getByText('appAnnotation.viewModal.hitHistory')) // Assert - expect(await screen.findByText('appAnnotation.viewModal.noHitHistory')).toBeInTheDocument() + // Assert + expect(await screen.findByText('appAnnotation.viewModal.noHitHistory'))!.toBeInTheDocument() expect(mockFormatTime).toHaveBeenCalledWith(props.item.created_at, 'appLog.dateTimeFormat') }) @@ -138,16 +139,16 @@ describe('ViewAnnotationModal', () => { fireEvent.click(await screen.findByText('appAnnotation.viewModal.hitHistory')) - expect(await screen.findByText('user input')).toBeInTheDocument() - expect(screen.getByText('15 appAnnotation.viewModal.hits')).toBeInTheDocument() - expect(mockFormatTime).toHaveBeenCalledWith(hits[0].created_at, 'appLog.dateTimeFormat') + expect(await screen.findByText('user input'))!.toBeInTheDocument() + expect(screen.getByText('15 appAnnotation.viewModal.hits'))!.toBeInTheDocument() + expect(mockFormatTime).toHaveBeenCalledWith(hits[0]!.created_at, 'appLog.dateTimeFormat') }) it('should confirm before removing the annotation and hide on success', async () => { const { props } = renderComponent() fireEvent.click(screen.getByText('appAnnotation.editModal.removeThisCache')) - expect(await screen.findByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument() + expect(await screen.findByText('appDebug.feature.annotation.removeConfirm'))!.toBeInTheDocument() const confirmButton = await screen.findByRole('button', { name: 'common.operation.confirm' }) fireEvent.click(confirmButton) @@ -162,7 +163,7 @@ describe('ViewAnnotationModal', () => { renderComponent() fireEvent.click(screen.getByText('appAnnotation.editModal.removeThisCache')) - expect(await screen.findByText('appDebug.feature.annotation.removeConfirm')).toBeInTheDocument() + expect(await screen.findByText('appDebug.feature.annotation.removeConfirm'))!.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) diff --git a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx index 125a4865a7..8c8087cf17 100644 --- a/web/app/components/app/app-access-control/add-member-or-group-pop.tsx +++ b/web/app/components/app/app-access-control/add-member-or-group-pop.tsx @@ -36,7 +36,7 @@ export default function AddMemberOrGroupDialog() { let observer: IntersectionObserver | undefined if (anchorRef.current) { observer = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !isLoading && hasMore) + if (entries[0]!.isIntersecting && !isLoading && hasMore) fetchNextPage() }, { rootMargin: '20px' }) observer.observe(anchorRef.current) diff --git a/web/app/components/app/app-publisher/__tests__/index.spec.tsx b/web/app/components/app/app-publisher/__tests__/index.spec.tsx index 1e29e44c82..bba2f53afc 100644 --- a/web/app/components/app/app-publisher/__tests__/index.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/index.spec.tsx @@ -207,7 +207,7 @@ describe('AppPublisher', () => { fireEvent.click(screen.getByText('common.publish')) - expect(screen.getByText('publisher-summary-publish')).toBeInTheDocument() + expect(screen.getByText('publisher-summary-publish'))!.toBeInTheDocument() expect(mockOnToggle).toHaveBeenCalledWith(true) await waitFor(() => { @@ -248,7 +248,7 @@ describe('AppPublisher', () => { fireEvent.click(screen.getByText('common.publish')) fireEvent.click(screen.getByText('publisher-embed')) - expect(screen.getByTestId('embedded-modal')).toBeInTheDocument() + expect(screen.getByTestId('embedded-modal'))!.toBeInTheDocument() }) it('should close embedded and access control panels through child callbacks', async () => { @@ -265,7 +265,7 @@ describe('AppPublisher', () => { fireEvent.click(screen.getByText('common.publish')) fireEvent.click(screen.getByText('publisher-access-control')) - expect(screen.getByTestId('access-control')).toBeInTheDocument() + expect(screen.getByTestId('access-control'))!.toBeInTheDocument() fireEvent.click(screen.getByText('close-access-control')) expect(screen.queryByTestId('access-control')).not.toBeInTheDocument() }) @@ -280,7 +280,7 @@ describe('AppPublisher', () => { fireEvent.click(screen.getByText('common.publish')) fireEvent.click(screen.getByText('publisher-access-control')) - expect(screen.getByTestId('access-control')).toBeInTheDocument() + expect(screen.getByTestId('access-control'))!.toBeInTheDocument() fireEvent.click(screen.getByText('confirm-access-control')) @@ -338,7 +338,7 @@ describe('AppPublisher', () => { />, ) - ahooksMocks.keyPressHandlers[0]({ preventDefault }) + ahooksMocks.keyPressHandlers[0]!({ preventDefault }) await waitFor(() => { expect(preventDefault).toHaveBeenCalled() @@ -367,7 +367,7 @@ describe('AppPublisher', () => { />, ) - ahooksMocks.keyPressHandlers[0]({ preventDefault }) + ahooksMocks.keyPressHandlers[0]!({ preventDefault }) await waitFor(() => { expect(preventDefault).toHaveBeenCalled() @@ -381,7 +381,7 @@ describe('AppPublisher', () => { await waitFor(() => { expect(onRestore).toHaveBeenCalledTimes(1) }) - expect(screen.getByText('publisher-summary-publish')).toBeInTheDocument() + expect(screen.getByText('publisher-summary-publish'))!.toBeInTheDocument() }) it('should report missing explore installations', async () => { @@ -455,6 +455,6 @@ describe('AppPublisher', () => { await waitFor(() => { expect(mockFetchAppDetailDirect).not.toHaveBeenCalled() }) - expect(screen.getByTestId('access-control')).toBeInTheDocument() + expect(screen.getByTestId('access-control'))!.toBeInTheDocument() }) }) diff --git a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx index fa8b1c92fc..ed86bb8571 100644 --- a/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx +++ b/web/app/components/app/app-publisher/__tests__/version-info-modal.spec.tsx @@ -34,8 +34,8 @@ describe('VersionInfoModal', () => { />, ) - expect(screen.getByDisplayValue('Release 1')).toBeInTheDocument() - expect(screen.getByDisplayValue('Initial release')).toBeInTheDocument() + expect(screen.getByDisplayValue('Release 1'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('Initial release'))!.toBeInTheDocument() }) it('should reject overlong titles', () => { @@ -50,7 +50,7 @@ describe('VersionInfoModal', () => { ) const [titleInput] = screen.getAllByRole('textbox') - fireEvent.change(titleInput, { target: { value: 'a'.repeat(16) } }) + fireEvent.change(titleInput!, { target: { value: 'a'.repeat(16) } }) fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.titleLengthLimit') @@ -75,8 +75,8 @@ describe('VersionInfoModal', () => { ) const [titleInput, notesInput] = screen.getAllByRole('textbox') - fireEvent.change(titleInput, { target: { value: 'Release 2' } }) - fireEvent.change(notesInput, { target: { value: 'Updated notes' } }) + fireEvent.change(titleInput!, { target: { value: 'Release 2' } }) + fireEvent.change(notesInput!, { target: { value: 'Updated notes' } }) fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) expect(handlePublish).toHaveBeenCalledWith({ @@ -106,16 +106,16 @@ describe('VersionInfoModal', () => { const [titleInput, notesInput] = screen.getAllByRole('textbox') - fireEvent.change(titleInput, { target: { value: 'a'.repeat(16) } }) + fireEvent.change(titleInput!, { target: { value: 'a'.repeat(16) } }) fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.titleLengthLimit') - fireEvent.change(titleInput, { target: { value: 'Release 3' } }) - fireEvent.change(notesInput, { target: { value: 'b'.repeat(101) } }) + fireEvent.change(titleInput!, { target: { value: 'Release 3' } }) + fireEvent.change(notesInput!, { target: { value: 'b'.repeat(101) } }) fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) expect(toast.error).toHaveBeenCalledWith('versionHistory.editField.releaseNotesLengthLimit') - fireEvent.change(notesInput, { target: { value: 'Stable release notes' } }) + fireEvent.change(notesInput!, { target: { value: 'Stable release notes' } }) fireEvent.click(screen.getByRole('button', { name: 'common.publish' })) expect(handlePublish).toHaveBeenCalledWith({ diff --git a/web/app/components/app/app-publisher/features-wrapper.tsx b/web/app/components/app/app-publisher/features-wrapper.tsx index 47ce63645c..7c39535701 100644 --- a/web/app/components/app/app-publisher/features-wrapper.tsx +++ b/web/app/components/app/app-publisher/features-wrapper.tsx @@ -62,7 +62,7 @@ const FeaturesWrappedAppPublisher = (props: Props) => { }, enabled: !!(file_upload?.enabled || file_upload?.image?.enabled), allowed_file_types: file_upload?.allowed_file_types || [SupportUploadFileTypes.image], - allowed_file_extensions: file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`), + allowed_file_extensions: file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image]!.map(ext => `.${ext}`), allowed_file_upload_methods: file_upload?.allowed_file_upload_methods || file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], number_limits: file_upload?.number_limits || file_upload?.image?.number_limits || 3, } as FileUpload diff --git a/web/app/components/app/app-publisher/index.tsx b/web/app/components/app/app-publisher/index.tsx index 6738860298..ce3698dbb7 100644 --- a/web/app/components/app/app-publisher/index.tsx +++ b/web/app/components/app/app-publisher/index.tsx @@ -197,7 +197,7 @@ const AppPublisher = ({ throw new Error('App not found') const { installed_apps } = await fetchInstalledAppList(appDetail.id) if (installed_apps?.length > 0) - return `${basePath}/explore/installed/${installed_apps[0].id}` + return `${basePath}/explore/installed/${installed_apps[0]!.id}` throw new Error('No app found in Explore') }, { onError: (err) => { diff --git a/web/app/components/app/configuration/__tests__/utils.spec.ts b/web/app/components/app/configuration/__tests__/utils.spec.ts index 65a6192177..6db1c0c09c 100644 --- a/web/app/components/app/configuration/__tests__/utils.spec.ts +++ b/web/app/components/app/configuration/__tests__/utils.spec.ts @@ -60,8 +60,8 @@ describe('configuration utils', () => { { id: 'tool-2', icon: '/console/icons/prefixed.svg' }, ] as never, '/console') - expect(result[0].icon).toBe('/console/icons/tool.svg') - expect(result[1].icon).toBe('/console/icons/prefixed.svg') + expect(result[0]!.icon).toBe('/console/icons/tool.svg') + expect(result[1]!.icon).toBe('/console/icons/prefixed.svg') }) }) diff --git a/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx index 413721ee2e..f7cfff94fb 100644 --- a/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/advanced-prompt-input.spec.tsx @@ -208,7 +208,7 @@ describe('AdvancedPromptInput', () => { fireEvent.click(screen.getByText('open-advanced-tool-modal')) - const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0][0] + const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0]![0] expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'existing_var' })).toBe(false) expect(mockToastError).toHaveBeenCalledWith('varKeyError.keyAlreadyExists') diff --git a/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx index d42eedf16b..70283e02c9 100644 --- a/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/index.spec.tsx @@ -144,8 +144,8 @@ describe('Prompt config component', () => { renderComponent({ onChange }, { isAdvancedMode: false }) const simplePrompt = screen.getByTestId('simple-prompt-input') - expect(simplePrompt).toBeInTheDocument() - expect(simplePrompt).toHaveAttribute('data-mode', AppModeEnum.CHAT) + expect(simplePrompt)!.toBeInTheDocument() + expect(simplePrompt)!.toHaveAttribute('data-mode', AppModeEnum.CHAT) expect(mockSimplePromptInputProps?.promptTemplate).toBe('initial template') fireEvent.click(simplePrompt) expect(onChange).toHaveBeenCalledWith('mocked prompt', defaultPromptVariables) @@ -171,9 +171,9 @@ describe('Prompt config component', () => { const renderedMessages = screen.getAllByTestId('advanced-message-input') expect(renderedMessages).toHaveLength(2) - expect(renderedMessages[0]).toHaveAttribute('data-context-missing', 'true') - fireEvent.click(screen.getAllByText('hide-context')[0]) - expect(screen.getAllByTestId('advanced-message-input')[0]).toHaveAttribute('data-context-missing', 'false') + expect(renderedMessages[0])!.toHaveAttribute('data-context-missing', 'true') + fireEvent.click(screen.getAllByText('hide-context')[0]!) + expect(screen.getAllByTestId('advanced-message-input')[0])!.toHaveAttribute('data-context-missing', 'false') }) // Chat message mutations @@ -193,7 +193,7 @@ describe('Prompt config component', () => { }, ) - fireEvent.click(screen.getAllByText('change')[0]) + fireEvent.click(screen.getAllByText('change')[0]!) expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith( [ { role: PromptRole.user, text: 'updated text' }, @@ -219,7 +219,7 @@ describe('Prompt config component', () => { }, ) - fireEvent.click(screen.getAllByText('type')[1]) + fireEvent.click(screen.getAllByText('type')[1]!) expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith( [ { role: PromptRole.user, text: 'first' }, @@ -244,7 +244,7 @@ describe('Prompt config component', () => { }, ) - fireEvent.click(screen.getAllByText('delete')[0]) + fireEvent.click(screen.getAllByText('delete')[0]!) expect(setCurrentAdvancedPrompt).toHaveBeenCalledWith([{ role: PromptRole.assistant, text: 'second' }]) }) diff --git a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx index a0bc072760..0f8b9b032f 100644 --- a/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx +++ b/web/app/components/app/configuration/config-prompt/__tests__/simple-prompt-input.spec.tsx @@ -151,7 +151,7 @@ describe('SimplePromptInput', () => { fireEvent.click(screen.getByText('blur-prompt')) - expect(screen.getByText('autoAddVar')).toBeInTheDocument() + expect(screen.getByText('autoAddVar'))!.toBeInTheDocument() fireEvent.click(screen.getByText('operation.add')) @@ -180,7 +180,7 @@ describe('SimplePromptInput', () => { fireEvent.click(screen.getByText('open-tool-modal')) expect(mockSetShowExternalDataToolModal).toHaveBeenCalledTimes(1) - const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0][0] + const modalConfig = mockSetShowExternalDataToolModal.mock.calls[0]![0] expect(modalConfig.onValidateBeforeSaveCallback({ variable: 'existing_var' })).toBe(false) expect(mockToastError).toHaveBeenCalledWith('varKeyError.keyAlreadyExists') @@ -267,10 +267,10 @@ describe('SimplePromptInput', () => { , ) - expect(screen.getByText('datasets:Knowledge Base')).toBeInTheDocument() - expect(screen.getByText('variables:existing_var')).toBeInTheDocument() - expect(screen.getByText('external-tools:search_api')).toBeInTheDocument() - expect(screen.getByText('query-selectable:false')).toBeInTheDocument() + expect(screen.getByText('datasets:Knowledge Base'))!.toBeInTheDocument() + expect(screen.getByText('variables:existing_var'))!.toBeInTheDocument() + expect(screen.getByText('external-tools:search_api'))!.toBeInTheDocument() + expect(screen.getByText('query-selectable:false'))!.toBeInTheDocument() }) it('should skip external tool variables and incomplete prompt variables when deciding whether to auto add', () => { @@ -312,7 +312,7 @@ describe('SimplePromptInput', () => { ) fireEvent.click(screen.getByText('blur-prompt')) - expect(screen.getByText('autoAddVar')).toBeInTheDocument() + expect(screen.getByText('autoAddVar'))!.toBeInTheDocument() fireEvent.click(screen.getByText('operation.cancel')) expect(mockOnChange).toHaveBeenCalledWith('Hello {{existing_var}}', []) diff --git a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx index 2783f66c3f..1de6e6ce0c 100644 --- a/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx @@ -96,8 +96,8 @@ const AdvancedPromptInput: FC = ({ }, onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { - if (promptVariables[i].key === newExternalDataTool.variable) { - toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) + if (promptVariables[i]!.key === newExternalDataTool.variable) { + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i]!.key })) return false } } diff --git a/web/app/components/app/configuration/config-prompt/index.tsx b/web/app/components/app/configuration/config-prompt/index.tsx index 3a36358d5e..8a77d4bb58 100644 --- a/web/app/components/app/configuration/config-prompt/index.tsx +++ b/web/app/components/app/configuration/config-prompt/index.tsx @@ -53,7 +53,7 @@ const Prompt: FC = ({ const handleMessageTypeChange = (index: number, role: PromptRole) => { const newPrompt = produce(currentAdvancedPrompt as PromptItem[], (draft) => { - draft[index].role = role + draft[index]!.role = role }) setCurrentAdvancedPrompt(newPrompt) } @@ -61,7 +61,7 @@ const Prompt: FC = ({ const handleValueChange = (value: string, index?: number) => { if (modelModeType === ModelModeType.chat) { const newPrompt = produce(currentAdvancedPrompt as PromptItem[], (draft) => { - draft[index as number].text = value + draft[index as number]!.text = value }) setCurrentAdvancedPrompt(newPrompt, true) } diff --git a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx index 0f6d2b94a6..6b5c3acccb 100644 --- a/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx +++ b/web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx @@ -94,8 +94,8 @@ const Prompt: FC = ({ }, onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { - if (promptVariables[i].key === newExternalDataTool.variable) { - toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) + if (promptVariables[i]!.key === newExternalDataTool.variable) { + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i]!.key })) return false } } diff --git a/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx index fb190d844a..4486489a20 100644 --- a/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/__tests__/index.spec.tsx @@ -118,7 +118,7 @@ describe('ConfigVar', () => { it('should show empty state when no variables exist', () => { renderConfigVar({ promptVariables: [] }) - expect(screen.getByText('appDebug.notSetVar')).toBeInTheDocument() + expect(screen.getByText('appDebug.notSetVar'))!.toBeInTheDocument() }) it('should render variable items and allow reordering via sortable list', () => { @@ -131,8 +131,8 @@ describe('ConfigVar', () => { onPromptVariablesChange, }) - expect(screen.getByText('first')).toBeInTheDocument() - expect(screen.getByText('second')).toBeInTheDocument() + expect(screen.getByText('first'))!.toBeInTheDocument() + expect(screen.getByText('second'))!.toBeInTheDocument() act(() => { latestSortableProps?.setList([ @@ -163,7 +163,7 @@ describe('ConfigVar', () => { fireEvent.click(await screen.findByText('appDebug.variableConfig.string')) expect(onPromptVariablesChange).toHaveBeenCalledTimes(1) - const [nextVariables] = onPromptVariablesChange.mock.calls[0] + const [nextVariables] = (onPromptVariablesChange.mock.calls[0] ?? []) as [any] expect(nextVariables).toHaveLength(1) expect(nextVariables[0].type).toBe('string') }) @@ -178,7 +178,7 @@ describe('ConfigVar', () => { expect(onPromptVariablesChange).toHaveBeenCalledTimes(1) expect(setShowExternalDataToolModal).toHaveBeenCalledTimes(1) - const modalState = setShowExternalDataToolModal.mock.calls[0][0] + const modalState = setShowExternalDataToolModal.mock.calls[0]![0] expect(modalState.payload.type).toBe('api') act(() => { @@ -197,13 +197,13 @@ describe('ConfigVar', () => { fireEvent.click(screen.getByText('common.operation.add')) fireEvent.click(await screen.findByText('appDebug.variableConfig.apiBasedVar')) - const modalState = setShowExternalDataToolModal.mock.calls[0][0] + const modalState = setShowExternalDataToolModal.mock.calls[0]![0] act(() => { modalState.onCancelCallback?.() }) expect(onPromptVariablesChange).toHaveBeenCalledTimes(2) - const [addedVariables] = onPromptVariablesChange.mock.calls[0] + const [addedVariables] = (onPromptVariablesChange.mock.calls[0] ?? []) as [any] expect(addedVariables).toHaveLength(2) expect(addedVariables[0]).toBe(existingVar) expect(addedVariables[1].type).toBe('api') @@ -235,7 +235,7 @@ describe('ConfigVar', () => { expect(itemContainer).not.toBeNull() const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') expect(actionButtons).toHaveLength(2) - fireEvent.click(actionButtons[0]) + fireEvent.click(actionButtons[0]!) const editDialog = await screen.findByRole('dialog') const saveButton = within(editDialog).getByRole('button', { name: 'common.operation.save' }) @@ -261,10 +261,10 @@ describe('ConfigVar', () => { expect(itemContainer).not.toBeNull() const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') expect(actionButtons).toHaveLength(2) - fireEvent.click(actionButtons[0]) + fireEvent.click(actionButtons[0]!) const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder') - fireEvent.change(inputs[0], { target: { value: 'second' } }) + fireEvent.change(inputs[0]!, { target: { value: 'second' } }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) @@ -287,10 +287,10 @@ describe('ConfigVar', () => { expect(itemContainer).not.toBeNull() const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') expect(actionButtons).toHaveLength(2) - fireEvent.click(actionButtons[0]) + fireEvent.click(actionButtons[0]!) const inputs = await screen.findAllByPlaceholderText('appDebug.variableConfig.inputPlaceholder') - fireEvent.change(inputs[1], { target: { value: 'Second' } }) + fireEvent.change(inputs[1]!, { target: { value: 'Second' } }) fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) @@ -412,7 +412,7 @@ describe('ConfigVar', () => { expect(itemContainer).not.toBeNull() const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') - fireEvent.click(actionButtons[0]) + fireEvent.click(actionButtons[0]!) const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] @@ -461,7 +461,7 @@ describe('ConfigVar', () => { expect(itemContainer).not.toBeNull() const actionButtons = itemContainer!.querySelectorAll('div.h-6.w-6') - fireEvent.click(actionButtons[0]) + fireEvent.click(actionButtons[0]!) const modalState = setShowExternalDataToolModal.mock.calls.at(-1)?.[0] diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx index 101cbab8fb..8cc8775bf5 100644 --- a/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/form-fields.spec.tsx @@ -178,8 +178,8 @@ describe('ConfigModalFormFields', () => { render() fireEvent.click(screen.getByText('single-file-setting')) fireEvent.click(screen.getByText('upload-file')) - fireEvent.click(screen.getAllByText('unchecked')[0]) - fireEvent.click(screen.getAllByText('unchecked')[1]) + fireEvent.click(screen.getAllByText('unchecked')[0]!) + fireEvent.click(screen.getAllByText('unchecked')[1]!) expect(singleFileProps.onFilePayloadChange).toHaveBeenCalledWith({ number_limits: 1 }) expect(singleFileProps.payloadChangeHandlers.default).toHaveBeenCalledWith(expect.objectContaining({ @@ -198,7 +198,7 @@ describe('ConfigModalFormFields', () => { } render() fireEvent.click(screen.getByText('multi-file-setting')) - fireEvent.click(screen.getAllByText('upload-file')[1]) + fireEvent.click(screen.getAllByText('upload-file')[1]!) expect(multiFileProps.onFilePayloadChange).toHaveBeenCalledWith({ number_limits: 3 }) expect(multiFileProps.payloadChangeHandlers.default).toHaveBeenCalledWith([ expect.objectContaining({ fileId: 'file-1' }), diff --git a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx index 31256f0c08..d15d6527c5 100644 --- a/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-modal/__tests__/index.spec.tsx @@ -44,9 +44,9 @@ describe('ConfigModal', () => { ) const textboxes = screen.getAllByRole('textbox') - fireEvent.blur(textboxes[0], { target: { value: 'question' } }) + fireEvent.blur(textboxes[0]!, { target: { value: 'question' } }) - expect(textboxes[1]).toHaveValue('question') + expect(textboxes[1])!.toHaveValue('question') }) it('should submit the edited payload when the form is valid', () => { diff --git a/web/app/components/app/configuration/config-var/config-string/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-var/config-string/__tests__/index.spec.tsx index 5fb2aaf732..b83a27d670 100644 --- a/web/app/components/app/configuration/config-var/config-string/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-var/config-string/__tests__/index.spec.tsx @@ -27,15 +27,15 @@ describe('ConfigString', () => { const input = screen.getByRole('spinbutton') - expect(input).toHaveValue(3) - expect(input).toHaveAttribute('min', '1') - expect(input).toHaveAttribute('max', '8') + expect(input)!.toHaveValue(3) + expect(input)!.toHaveAttribute('min', '1') + expect(input)!.toHaveAttribute('max', '8') }) it('should render empty input when value is undefined', () => { const { onChange } = renderConfigString({ value: undefined }) - expect(screen.getByRole('spinbutton')).toHaveValue(null) + expect(screen.getByRole('spinbutton'))!.toHaveValue(null) expect(onChange).not.toHaveBeenCalled() }) }) @@ -116,7 +116,7 @@ describe('ConfigString', () => { fireEvent.change(screen.getByRole('spinbutton'), { target: { value: '' } }) expect(onChange).toHaveBeenCalledTimes(1) - expect(onChange.mock.calls[0][0]).toBeNaN() + expect(onChange.mock.calls[0]![0]).toBeNaN() }) }) }) diff --git a/web/app/components/app/configuration/config-var/index.tsx b/web/app/components/app/configuration/config-var/index.tsx index 951680c035..aca2817249 100644 --- a/web/app/components/app/configuration/config-var/index.tsx +++ b/web/app/components/app/configuration/config-var/index.tsx @@ -165,8 +165,8 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar }, onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => { for (let i = 0; i < promptVariables.length; i++) { - if (promptVariables[i].key === newExternalDataTool.variable && i !== index) { - toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i].key })) + if (promptVariables[i]!.key === newExternalDataTool.variable && i !== index) { + toast.error(t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: promptVariables[i]!.key })) return false } } @@ -220,7 +220,7 @@ const ConfigVar: FC = ({ promptVariables, readonly, onPromptVar const handleRemoveVar = useCallback((index: number) => { const removeVar = promptVariables[index] - if (mode === AppModeEnum.COMPLETION && dataSets.length > 0 && removeVar.is_context_var) { + if (mode === AppModeEnum.COMPLETION && dataSets.length > 0 && removeVar!.is_context_var) { showDeleteContextVarModal() setRemoveIndex(index) return diff --git a/web/app/components/app/configuration/config-vision/__tests__/index.spec.tsx b/web/app/components/app/configuration/config-vision/__tests__/index.spec.tsx index 6ff73ae008..b46f561bdb 100644 --- a/web/app/components/app/configuration/config-vision/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config-vision/__tests__/index.spec.tsx @@ -69,7 +69,7 @@ const setupFeatureStore = (fileOverrides: Partial = {}) => { const getLatestFileConfig = () => { expect(setFeaturesMock).toHaveBeenCalled() - const latestFeatures = setFeaturesMock.mock.calls[setFeaturesMock.mock.calls.length - 1][0] as { file: FileUpload } + const latestFeatures = setFeaturesMock.mock.calls[setFeaturesMock.mock.calls.length - 1]![0] as { file: FileUpload } return latestFeatures.file } @@ -98,8 +98,8 @@ describe('ConfigVision', () => { it('should show the toggle and parameter controls when visible', () => { render() - expect(screen.getByText('appDebug.vision.name')).toBeInTheDocument() - expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false') + expect(screen.getByText('appDebug.vision.name'))!.toBeInTheDocument() + expect(screen.getByRole('switch'))!.toHaveAttribute('aria-checked', 'false') }) it('should enable both image and video uploads when toggled on with video support', async () => { @@ -178,7 +178,7 @@ describe('ParamConfig', () => { await user.click(screen.getByRole('button', { name: 'appDebug.voice.settings' })) - expect(await screen.findByText('appDebug.vision.visionSettings.title')).toBeInTheDocument() + expect(await screen.findByText('appDebug.vision.visionSettings.title'))!.toBeInTheDocument() }) }) diff --git a/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx b/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx index 073ff5a639..b32f14d089 100644 --- a/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/config/assistant-type-picker/__tests__/index.spec.tsx @@ -46,7 +46,8 @@ describe('AssistantTypePicker', () => { renderComponent() // Assert - expect(screen.getByText(/chatAssistant.name/i)).toBeInTheDocument() + // Assert + expect(screen.getByText(/chatAssistant.name/i))!.toBeInTheDocument() }) it('should render chat assistant by default when value is "chat"', () => { @@ -54,7 +55,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'chat' }) // Assert - expect(screen.getByText(/chatAssistant.name/i)).toBeInTheDocument() + // Assert + expect(screen.getByText(/chatAssistant.name/i))!.toBeInTheDocument() }) it('should render agent assistant when value is "agent"', () => { @@ -62,7 +64,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent' }) // Assert - expect(screen.getByText(/agentAssistant.name/i)).toBeInTheDocument() + // Assert + expect(screen.getByText(/agentAssistant.name/i))!.toBeInTheDocument() }) }) @@ -73,7 +76,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'agent' }) // Assert - expect(screen.getByText(/agentAssistant.name/i)).toBeInTheDocument() + // Assert + expect(screen.getByText(/agentAssistant.name/i))!.toBeInTheDocument() }) it('should handle agentConfig prop', () => { @@ -91,7 +95,8 @@ describe('AssistantTypePicker', () => { }).not.toThrow() // Assert - expect(screen.getByText(/chatAssistant.name/i)).toBeInTheDocument() + // Assert + expect(screen.getByText(/chatAssistant.name/i))!.toBeInTheDocument() }) it('should handle undefined agentConfig prop', () => { @@ -101,7 +106,8 @@ describe('AssistantTypePicker', () => { }).not.toThrow() // Assert - expect(screen.getByText(/chatAssistant.name/i)).toBeInTheDocument() + // Assert + expect(screen.getByText(/chatAssistant.name/i))!.toBeInTheDocument() }) }) @@ -137,7 +143,7 @@ describe('AssistantTypePicker', () => { // Wait for dropdown to open and find chat option await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() }) // Find and click the chat option by its unique description @@ -160,7 +166,7 @@ describe('AssistantTypePicker', () => { // Wait for dropdown to open and click agent option await waitFor(() => { - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) const agentOption = getOptionByDescription(/agentAssistant.description/i) @@ -181,7 +187,7 @@ describe('AssistantTypePicker', () => { // Wait for dropdown and select chat await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() }) const chatOption = getOptionByDescription(/chatAssistant.description/i) @@ -209,11 +215,11 @@ describe('AssistantTypePicker', () => { }) const agentOptions = screen.getAllByText(/agentAssistant.name/i) - await user.click(agentOptions[0]) + await user.click(agentOptions[0]!) // Assert - Dropdown should remain open (agent settings should be visible) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) }) @@ -234,7 +240,7 @@ describe('AssistantTypePicker', () => { }) const chatOptions = screen.getAllByText(/chatAssistant.name/i) - await user.click(chatOptions[1]) + await user.click(chatOptions[1]!) // Assert expect(onChange).not.toHaveBeenCalled() @@ -255,7 +261,7 @@ describe('AssistantTypePicker', () => { // Wait for dropdown to open await waitFor(() => { - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) // Act - Try to click an option @@ -292,7 +298,7 @@ describe('AssistantTypePicker', () => { // Assert - Agent settings option should be visible await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) }) }) @@ -310,7 +316,7 @@ describe('AssistantTypePicker', () => { // Click agent settings await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) @@ -318,7 +324,7 @@ describe('AssistantTypePicker', () => { // Assert await waitFor(() => { - expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i))!.toBeInTheDocument() }) }) @@ -333,9 +339,40 @@ describe('AssistantTypePicker', () => { // Wait for dropdown to open await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() }) + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') + // Assert - Agent settings modal should not appear (value is 'chat') // Assert - Agent settings modal should not appear (value is 'chat') expect(screen.queryByText(/common.operation.save/i)).not.toBeInTheDocument() }) @@ -351,7 +388,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) @@ -359,7 +396,7 @@ describe('AssistantTypePicker', () => { // Wait for modal and click save await waitFor(() => { - expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i))!.toBeInTheDocument() }) const saveButton = screen.getByText(/common.operation.save/i) @@ -379,14 +416,14 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) await user.click(agentSettingsTrigger) await waitFor(() => { - expect(screen.getByText(/appDebug.agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/appDebug.agent.setting.name/i))!.toBeInTheDocument() }) const saveButton = screen.getByText(/common.operation.save/i) @@ -409,14 +446,14 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) await user.click(agentSettingsTrigger) await waitFor(() => { - expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i))!.toBeInTheDocument() }) const cancelButton = screen.getByText(/common.operation.cancel/i) @@ -439,7 +476,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) @@ -447,7 +484,7 @@ describe('AssistantTypePicker', () => { // Assert - Modal should be open and dropdown should close await waitFor(() => { - expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i))!.toBeInTheDocument() }) // The dropdown should be closed (agent settings description should not be visible) @@ -472,7 +509,8 @@ describe('AssistantTypePicker', () => { await user.click(trigger) // Assert - Should not crash - expect(trigger).toBeInTheDocument() + // Assert - Should not crash + expect(trigger)!.toBeInTheDocument() }) it('should handle multiple rapid selection changes', async () => { @@ -486,7 +524,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) // Click agent option - this stays open because value is 'agent' @@ -523,7 +561,8 @@ describe('AssistantTypePicker', () => { }).not.toThrow() // Assert - expect(screen.getByText(/chatAssistant.name/i)).toBeInTheDocument() + // Assert + expect(screen.getByText(/chatAssistant.name/i))!.toBeInTheDocument() }) describe('should render with different prop combinations', () => { @@ -542,7 +581,7 @@ describe('AssistantTypePicker', () => { // Assert const expectedText = combo.value === 'agent' ? 'agentAssistant.name' : 'chatAssistant.name' - expect(screen.getByText(new RegExp(expectedText, 'i'))).toBeInTheDocument() + expect(screen.getByText(new RegExp(expectedText, 'i')))!.toBeInTheDocument() }, ) }) @@ -561,15 +600,15 @@ describe('AssistantTypePicker', () => { // Assert - Both options should be visible and clickable await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) // Verify we can interact with option elements using helper function const chatOption = getOptionByDescription(/chatAssistant.description/i) const agentOption = getOptionByDescription(/agentAssistant.description/i) - expect(chatOption).toBeInTheDocument() - expect(agentOption).toBeInTheDocument() + expect(chatOption)!.toBeInTheDocument() + expect(agentOption)!.toBeInTheDocument() }) }) @@ -586,16 +625,16 @@ describe('AssistantTypePicker', () => { // Assert - Both options should be visible with radio components await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) // The SelectItem components render with different visual states // based on isChecked prop - we verify both options are rendered const chatOption = getOptionByDescription(/chatAssistant.description/i) const agentOption = getOptionByDescription(/agentAssistant.description/i) - expect(chatOption).toBeInTheDocument() - expect(agentOption).toBeInTheDocument() + expect(chatOption)!.toBeInTheDocument() + expect(agentOption)!.toBeInTheDocument() }) it('should render description text', async () => { @@ -609,8 +648,8 @@ describe('AssistantTypePicker', () => { // Assert - Descriptions should be visible await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) }) @@ -625,8 +664,8 @@ describe('AssistantTypePicker', () => { // Assert - Radio components should be present (both options visible) await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) }) }) @@ -643,7 +682,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) @@ -651,9 +690,9 @@ describe('AssistantTypePicker', () => { // Assert await waitFor(() => { - expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i))!.toBeInTheDocument() }) - expect(screen.getByText(/appDebug.agent.agentModeType.functionCall/i)).toBeInTheDocument() + expect(screen.getByText(/appDebug.agent.agentModeType.functionCall/i))!.toBeInTheDocument() }) it('should show built-in prompt when isFunctionCall is false', async () => { @@ -666,7 +705,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) @@ -674,9 +713,9 @@ describe('AssistantTypePicker', () => { // Assert await waitFor(() => { - expect(screen.getByText(/common.operation.save/i)).toBeInTheDocument() + expect(screen.getByText(/common.operation.save/i))!.toBeInTheDocument() }) - expect(screen.getByText(/tools.builtInPromptTitle/i)).toBeInTheDocument() + expect(screen.getByText(/tools.builtInPromptTitle/i))!.toBeInTheDocument() }) it('should initialize max iteration from agentConfig payload', async () => { @@ -696,7 +735,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettingsTrigger = screen.getByText(/agent.setting.name/i) @@ -705,7 +744,7 @@ describe('AssistantTypePicker', () => { // Assert await screen.findByText(/common.operation.save/i) const maxIterationInput = await screen.findByRole('spinbutton') - expect(maxIterationInput).toHaveValue(10) + expect(maxIterationInput)!.toHaveValue(10) }) }) @@ -721,7 +760,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() }) // Press Escape @@ -741,8 +780,9 @@ describe('AssistantTypePicker', () => { const trigger = screen.getByText(/chatAssistant.name/i) // Assert - Element should be focusable - expect(trigger).toBeInTheDocument() - expect(trigger.parentElement).toBeInTheDocument() + // Assert - Element should be focusable + expect(trigger)!.toBeInTheDocument() + expect(trigger.parentElement)!.toBeInTheDocument() }) it('should allow keyboard focus on dropdown options', async () => { @@ -755,7 +795,7 @@ describe('AssistantTypePicker', () => { await user.click(trigger) await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() }) // Get options @@ -763,8 +803,9 @@ describe('AssistantTypePicker', () => { const agentOption = getOptionByDescription(/agentAssistant.description/i) // Assert - Options should be focusable - expect(chatOption).toBeInTheDocument() - expect(agentOption).toBeInTheDocument() + // Assert - Options should be focusable + expect(chatOption)!.toBeInTheDocument() + expect(agentOption)!.toBeInTheDocument() // Verify options exist and can receive focus programmatically // Note: focus() doesn't always update document.activeElement in JSDOM @@ -787,11 +828,11 @@ describe('AssistantTypePicker', () => { // Assert - Agent settings button should be focusable await waitFor(() => { - expect(screen.getByText(/agent.setting.name/i)).toBeInTheDocument() + expect(screen.getByText(/agent.setting.name/i))!.toBeInTheDocument() }) const agentSettings = screen.getByText(/agent.setting.name/i) - expect(agentSettings).toBeInTheDocument() + expect(agentSettings)!.toBeInTheDocument() }) }) @@ -804,7 +845,7 @@ describe('AssistantTypePicker', () => { // Act - Check initial state const portalContainer = container.querySelector('[data-state]') - expect(portalContainer).toHaveAttribute('data-state', 'closed') + expect(portalContainer)!.toHaveAttribute('data-state', 'closed') // Open dropdown const trigger = screen.getByText(/chatAssistant.name/i) @@ -813,7 +854,7 @@ describe('AssistantTypePicker', () => { // Assert - State should change to open await waitFor(() => { const openPortal = container.querySelector('[data-state="open"]') - expect(openPortal).toBeInTheDocument() + expect(openPortal)!.toBeInTheDocument() }) }) @@ -823,11 +864,12 @@ describe('AssistantTypePicker', () => { // Assert - Portal should have data-state for accessibility const portalContainer = container.querySelector('[data-state]') - expect(portalContainer).toBeInTheDocument() - expect(portalContainer).toHaveAttribute('data-state') + expect(portalContainer)!.toBeInTheDocument() + expect(portalContainer)!.toHaveAttribute('data-state') // Should start in closed state - expect(portalContainer).toHaveAttribute('data-state', 'closed') + // Should start in closed state + expect(portalContainer)!.toHaveAttribute('data-state', 'closed') }) it('should maintain accessible structure for screen readers', () => { @@ -835,7 +877,8 @@ describe('AssistantTypePicker', () => { renderComponent({ value: 'chat' }) // Assert - Text content should be accessible - expect(screen.getByText(/chatAssistant.name/i)).toBeInTheDocument() + // Assert - Text content should be accessible + expect(screen.getByText(/chatAssistant.name/i))!.toBeInTheDocument() // Icons should have proper structure const { container } = renderComponent() @@ -854,12 +897,13 @@ describe('AssistantTypePicker', () => { // Assert - All options should have descriptive text await waitFor(() => { - expect(screen.getByText(/chatAssistant.description/i)).toBeInTheDocument() - expect(screen.getByText(/agentAssistant.description/i)).toBeInTheDocument() + expect(screen.getByText(/chatAssistant.description/i))!.toBeInTheDocument() + expect(screen.getByText(/agentAssistant.description/i))!.toBeInTheDocument() }) // Title text should be visible - expect(screen.getByText(/assistantType.name/i)).toBeInTheDocument() + // Title text should be visible + expect(screen.getByText(/assistantType.name/i))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/configuration/config/automatic/__tests__/result.spec.tsx b/web/app/components/app/configuration/config/automatic/__tests__/result.spec.tsx index 7e9c26bd06..30f41b7828 100644 --- a/web/app/components/app/configuration/config/automatic/__tests__/result.spec.tsx +++ b/web/app/components/app/configuration/config/automatic/__tests__/result.spec.tsx @@ -81,11 +81,11 @@ describe('Result', () => { />, ) - expect(screen.getByTestId('prompt-toast')).toHaveTextContent('optimization note') - expect(screen.getByTestId('prompt-res')).toHaveTextContent('generated output') + expect(screen.getByTestId('prompt-toast'))!.toHaveTextContent('optimization note') + expect(screen.getByTestId('prompt-res'))!.toHaveTextContent('generated output') fireEvent.click(screen.getByTestId('version-selector')) - fireEvent.click(screen.getAllByRole('button')[1]) + fireEvent.click(screen.getAllByRole('button')[1]!) fireEvent.click(screen.getByText('generate.apply')) expect(mockSetCurrentVersionIndex).toHaveBeenCalledWith(1) @@ -106,7 +106,7 @@ describe('Result', () => { />, ) - expect(screen.getByTestId('prompt-res-in-workflow')).toHaveTextContent('generated output') + expect(screen.getByTestId('prompt-res-in-workflow'))!.toHaveTextContent('generated output') expect(mockPromptResInWorkflow).toHaveBeenCalledWith(expect.objectContaining({ nodeId: 'node-1', value: 'generated output', @@ -121,6 +121,6 @@ describe('Result', () => { />, ) - expect(screen.getByTestId('code-editor')).toHaveTextContent('generated output') + expect(screen.getByTestId('code-editor'))!.toHaveTextContent('generated output') }) }) diff --git a/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx index 11562e8d9b..25e38b4855 100644 --- a/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/__tests__/index.spec.tsx @@ -61,7 +61,7 @@ vi.mock('es-toolkit/compat', () => ({ return [] // Start with first array and filter down - return validArrays[0].filter((item: any) => { + return validArrays[0]!.filter((item: any) => { if (!item || !item.name) return false @@ -317,14 +317,14 @@ describe('DatasetConfig', () => { it('should render dataset configuration panel when component mounts', () => { renderDatasetConfig() - expect(screen.getByText('appDebug.feature.dataSet.title')).toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.title'))!.toBeInTheDocument() }) it('should display empty state message when no datasets are configured', () => { renderDatasetConfig() - expect(screen.getByText(/no.*data/i)).toBeInTheDocument() - expect(screen.getByTestId('params-config')).toBeDisabled() + expect(screen.getByText(/no.*data/i))!.toBeInTheDocument() + expect(screen.getByTestId('params-config'))!.toBeDisabled() }) it('should render dataset cards and enable parameters when datasets exist', () => { @@ -333,16 +333,16 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId(`card-item-${dataset.id}`)).toBeInTheDocument() - expect(screen.getByText(dataset.name)).toBeInTheDocument() + expect(screen.getByTestId(`card-item-${dataset.id}`))!.toBeInTheDocument() + expect(screen.getByText(dataset.name))!.toBeInTheDocument() expect(screen.getByTestId('params-config')).not.toBeDisabled() }) it('should show configuration title and add dataset button in header', () => { renderDatasetConfig() - expect(screen.getByText('appDebug.feature.dataSet.title')).toBeInTheDocument() - expect(screen.getByText('common.operation.add')).toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.title'))!.toBeInTheDocument() + expect(screen.getByText('common.operation.add'))!.toBeInTheDocument() }) it('should hide parameters configuration when in agent mode', () => { @@ -436,7 +436,7 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId(`card-item-${dataset.id}`)).toBeInTheDocument() + expect(screen.getByTestId(`card-item-${dataset.id}`))!.toBeInTheDocument() }) }) @@ -456,9 +456,10 @@ describe('DatasetConfig', () => { }, }) - expect(screen.getByTestId('context-var')).toBeInTheDocument() + expect(screen.getByTestId('context-var'))!.toBeInTheDocument() // Should find the selected context variable in the options - expect(screen.getByText('Select context variable')).toBeInTheDocument() + // Should find the selected context variable in the options + expect(screen.getByText('Select context variable'))!.toBeInTheDocument() }) it('should not show context variable selector in chat mode', () => { @@ -514,8 +515,8 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId('metadata-filter')).toBeInTheDocument() - expect(screen.getByTestId('metadata-list-count')).toHaveTextContent('2') // both 'category' and 'priority' + expect(screen.getByTestId('metadata-filter'))!.toBeInTheDocument() + expect(screen.getByTestId('metadata-list-count'))!.toHaveTextContent('2') // both 'category' and 'priority' }) it('should handle metadata filter mode change', async () => { @@ -740,8 +741,8 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId('metadata-filter')).toBeInTheDocument() - expect(screen.getByTestId('metadata-list-count')).toHaveTextContent('0') + expect(screen.getByTestId('metadata-filter'))!.toBeInTheDocument() + expect(screen.getByTestId('metadata-list-count'))!.toHaveTextContent('0') }) it('should handle empty doc_metadata array', () => { @@ -753,8 +754,8 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId('metadata-filter')).toBeInTheDocument() - expect(screen.getByTestId('metadata-list-count')).toHaveTextContent('0') + expect(screen.getByTestId('metadata-filter'))!.toBeInTheDocument() + expect(screen.getByTestId('metadata-list-count'))!.toHaveTextContent('0') }) it('should handle missing userProfile', () => { @@ -769,7 +770,7 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId(`card-item-${dataset.id}`)).toBeInTheDocument() + expect(screen.getByTestId(`card-item-${dataset.id}`))!.toBeInTheDocument() }) it('should handle missing datasetConfigsRef gracefully', () => { @@ -816,10 +817,10 @@ describe('DatasetConfig', () => { dataSets: datasets, }) - expect(screen.getByTestId('card-item-ds1')).toBeInTheDocument() - expect(screen.getByTestId('card-item-ds2')).toBeInTheDocument() - expect(screen.getByText('Dataset 1')).toBeInTheDocument() - expect(screen.getByText('Dataset 2')).toBeInTheDocument() + expect(screen.getByTestId('card-item-ds1'))!.toBeInTheDocument() + expect(screen.getByTestId('card-item-ds2'))!.toBeInTheDocument() + expect(screen.getByText('Dataset 1'))!.toBeInTheDocument() + expect(screen.getByText('Dataset 2'))!.toBeInTheDocument() }) it('should integrate with params config component', () => { @@ -833,8 +834,8 @@ describe('DatasetConfig', () => { }) const paramsConfig = screen.getByTestId('params-config') - expect(paramsConfig).toBeInTheDocument() - expect(paramsConfig).toHaveTextContent('Params (2)') + expect(paramsConfig)!.toBeInTheDocument() + expect(paramsConfig)!.toHaveTextContent('Params (2)') expect(paramsConfig).not.toBeDisabled() }) @@ -860,9 +861,10 @@ describe('DatasetConfig', () => { }) const metadataFilter = screen.getByTestId('metadata-filter') - expect(metadataFilter).toBeInTheDocument() + expect(metadataFilter)!.toBeInTheDocument() // Should show intersection (only 'category') - expect(screen.getByTestId('metadata-list-count')).toHaveTextContent('1') + // Should show intersection (only 'category') + expect(screen.getByTestId('metadata-list-count'))!.toHaveTextContent('1') }) }) @@ -884,7 +886,7 @@ describe('DatasetConfig', () => { }) const metadataFilter = screen.getByTestId('metadata-filter') - expect(metadataFilter).toBeInTheDocument() + expect(metadataFilter)!.toBeInTheDocument() fireEvent.click(within(metadataFilter).getByText('Change Metadata Model')) @@ -915,7 +917,7 @@ describe('DatasetConfig', () => { }) const metadataFilter = screen.getByTestId('metadata-filter') - expect(metadataFilter).toBeInTheDocument() + expect(metadataFilter)!.toBeInTheDocument() fireEvent.click(within(metadataFilter).getByText('Change Metadata Params')) @@ -941,7 +943,8 @@ describe('DatasetConfig', () => { }) // The editable property should be false when no permission - expect(screen.getByTestId(`card-item-${dataset.id}`)).toBeInTheDocument() + // The editable property should be false when no permission + expect(screen.getByTestId(`card-item-${dataset.id}`))!.toBeInTheDocument() }) it('should show readonly state for non-editable datasets', () => { @@ -956,7 +959,7 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId(`card-item-${dataset.id}`)).toBeInTheDocument() + expect(screen.getByTestId(`card-item-${dataset.id}`))!.toBeInTheDocument() }) it('should allow editing when user has partial member permission', () => { @@ -972,7 +975,7 @@ describe('DatasetConfig', () => { dataSets: [dataset], }) - expect(screen.getByTestId(`card-item-${dataset.id}`)).toBeInTheDocument() + expect(screen.getByTestId(`card-item-${dataset.id}`))!.toBeInTheDocument() }) }) @@ -989,9 +992,10 @@ describe('DatasetConfig', () => { }) // Verify order is maintained - expect(screen.getByText('Dataset 1')).toBeInTheDocument() - expect(screen.getByText('Dataset 2')).toBeInTheDocument() - expect(screen.getByText('Dataset 3')).toBeInTheDocument() + // Verify order is maintained + expect(screen.getByText('Dataset 1'))!.toBeInTheDocument() + expect(screen.getByText('Dataset 2'))!.toBeInTheDocument() + expect(screen.getByText('Dataset 3'))!.toBeInTheDocument() }) it('should handle multiple dataset operations correctly', async () => { @@ -1007,7 +1011,7 @@ describe('DatasetConfig', () => { // Remove first dataset const removeButton1 = screen.getAllByText('Remove')[0] - await user.click(removeButton1) + await user.click(removeButton1!) expect(mockConfigContext.setDataSets).toHaveBeenCalledWith([datasets[1]]) }) @@ -1050,7 +1054,7 @@ describe('DatasetConfig', () => { dataSets: datasets, }) - expect(screen.getByTestId('params-config')).toHaveTextContent('Params (2)') + expect(screen.getByTestId('params-config'))!.toHaveTextContent('Params (2)') }) it('should handle external knowledge base integration', () => { @@ -1068,8 +1072,8 @@ describe('DatasetConfig', () => { dataSets: [externalDataset], }) - expect(screen.getByTestId(`card-item-${externalDataset.id}`)).toBeInTheDocument() - expect(screen.getByText(externalDataset.name)).toBeInTheDocument() + expect(screen.getByTestId(`card-item-${externalDataset.id}`))!.toBeInTheDocument() + expect(screen.getByText(externalDataset.name))!.toBeInTheDocument() }) }) @@ -1090,7 +1094,7 @@ describe('DatasetConfig', () => { dataSets: manyDatasets, }) - expect(screen.getByTestId('params-config')).toHaveTextContent('Params (50)') + expect(screen.getByTestId('params-config'))!.toHaveTextContent('Params (50)') }) it('should handle metadata intersection calculation efficiently', () => { @@ -1118,7 +1122,8 @@ describe('DatasetConfig', () => { }) // Should calculate intersection correctly - expect(screen.getByTestId('metadata-filter')).toBeInTheDocument() + // Should calculate intersection correctly + expect(screen.getByTestId('metadata-filter'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx index 16a539bae3..1d14f7dbd2 100644 --- a/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/card-item/__tests__/index.spec.tsx @@ -157,9 +157,9 @@ describe('dataset-config/card-item', () => { const card = screen.getByText(dataset.name).closest('.group') as HTMLElement const actionButtons = within(card).getAllByRole('button', { hidden: true }) - expect(screen.getByText(dataset.name)).toBeInTheDocument() - expect(screen.getByText('dataset.indexingTechnique.high_quality · dataset.indexingMethod.semantic_search')).toBeInTheDocument() - expect(screen.getByText('dataset.externalTag')).toBeInTheDocument() + expect(screen.getByText(dataset.name))!.toBeInTheDocument() + expect(screen.getByText('dataset.indexingTechnique.high_quality · dataset.indexingMethod.semantic_search'))!.toBeInTheDocument() + expect(screen.getByText('dataset.externalTag'))!.toBeInTheDocument() expect(actionButtons).toHaveLength(2) }) @@ -170,9 +170,9 @@ describe('dataset-config/card-item', () => { const card = screen.getByText(dataset.name).closest('.group') as HTMLElement const [editButton] = within(card).getAllByRole('button', { hidden: true }) - await user.click(editButton) + await user.click(editButton!) - expect(await screen.findByText('Mock settings modal')).toBeInTheDocument() + expect(await screen.findByText('Mock settings modal'))!.toBeInTheDocument() fireEvent.click(await screen.findByText('Save changes')) await waitFor(() => { @@ -213,8 +213,8 @@ describe('dataset-config/card-item', () => { const nameElement = screen.getByText(dataset.name) const iconElement = nameElement.parentElement?.firstElementChild as HTMLElement - expect(iconElement).toHaveStyle({ background: '#FFF4ED' }) - expect(iconElement.querySelector('em-emoji')).toHaveAttribute('id', '📙') + expect(iconElement)!.toHaveStyle({ background: '#FFF4ED' }) + expect(iconElement.querySelector('em-emoji'))!.toHaveAttribute('id', '📙') }) it('should apply mask overlay on mobile when drawer is open', async () => { @@ -226,12 +226,12 @@ describe('dataset-config/card-item', () => { const card = screen.getByText(dataset.name).closest('.group') as HTMLElement const [editButton] = within(card).getAllByRole('button', { hidden: true }) - await user.click(editButton) - expect(screen.getByText('Mock settings modal')).toBeInTheDocument() + await user.click(editButton!) + expect(screen.getByText('Mock settings modal'))!.toBeInTheDocument() const overlay = [...document.querySelectorAll('[class]')] .find(element => element.className.toString().includes('bg-black/30')) - expect(overlay).toBeInTheDocument() + expect(overlay)!.toBeInTheDocument() }) }) diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx index 0d886fa9e5..6726ba0583 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/index.spec.tsx @@ -87,7 +87,8 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title')).toBeInTheDocument() + // Assert + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title'))!.toBeInTheDocument() }) it('should show selected variable with proper formatting when value is provided', () => { @@ -98,9 +99,10 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('var1')).toBeInTheDocument() - expect(screen.getByText('{{')).toBeInTheDocument() - expect(screen.getByText('}}')).toBeInTheDocument() + // Assert + expect(screen.getByText('var1'))!.toBeInTheDocument() + expect(screen.getByText('{{'))!.toBeInTheDocument() + expect(screen.getByText('}}'))!.toBeInTheDocument() }) }) @@ -114,7 +116,8 @@ describe('ContextVar', () => { render() // Assert - Should display the selected value - expect(screen.getByText('var2')).toBeInTheDocument() + // Assert - Should display the selected value + expect(screen.getByText('var2'))!.toBeInTheDocument() }) it('should show placeholder text when no value is selected', () => { @@ -127,9 +130,40 @@ describe('ContextVar', () => { // Act render() + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable + // Assert - Should show placeholder instead of variable // Assert - Should show placeholder instead of variable expect(screen.queryByText('var1')).not.toBeInTheDocument() - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() }) it('should display custom tip message when notSelectedVarTip is provided', () => { @@ -144,7 +178,8 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('Select a variable')).toBeInTheDocument() + // Assert + expect(screen.getByText('Select a variable'))!.toBeInTheDocument() }) it('should apply custom className to VarPicker when provided', () => { @@ -158,7 +193,8 @@ describe('ContextVar', () => { const { container } = render() // Assert - expect(container.querySelector('.custom-class')).toBeInTheDocument() + // Assert + expect(container.querySelector('.custom-class'))!.toBeInTheDocument() }) }) @@ -176,13 +212,13 @@ describe('ContextVar', () => { const triggers = screen.getAllByTestId('portal-trigger') const varPickerTrigger = triggers[triggers.length - 1] - await user.click(varPickerTrigger) - expect(screen.getByTestId('portal-content')).toBeInTheDocument() + await user.click(varPickerTrigger!) + expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() // Select a different option const options = screen.getAllByText('var2') expect(options.length).toBeGreaterThan(0) - await user.click(options[0]) + await user.click(options[0]!) // Assert expect(onChange).toHaveBeenCalledWith('var2') @@ -201,11 +237,11 @@ describe('ContextVar', () => { const varPickerTrigger = triggers[triggers.length - 1] // Open dropdown - await user.click(varPickerTrigger) - expect(screen.getByTestId('portal-content')).toBeInTheDocument() + await user.click(varPickerTrigger!) + expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() // Close dropdown - await user.click(varPickerTrigger) + await user.click(varPickerTrigger!) expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() }) }) @@ -223,8 +259,9 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title')).toBeInTheDocument() - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() + // Assert + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() expect(screen.queryByText('var1')).not.toBeInTheDocument() }) @@ -240,8 +277,9 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title')).toBeInTheDocument() - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() + // Assert + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() }) it('should handle null value without crashing', () => { @@ -255,8 +293,9 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title')).toBeInTheDocument() - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() + // Assert + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.title'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() }) it('should handle options with different data types', () => { @@ -275,9 +314,10 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('strVar')).toBeInTheDocument() - expect(screen.getByText('{{')).toBeInTheDocument() - expect(screen.getByText('}}')).toBeInTheDocument() + // Assert + expect(screen.getByText('strVar'))!.toBeInTheDocument() + expect(screen.getByText('{{'))!.toBeInTheDocument() + expect(screen.getByText('}}'))!.toBeInTheDocument() }) it('should render variable names with special characters safely', () => { @@ -294,7 +334,8 @@ describe('ContextVar', () => { render() // Assert - expect(screen.getByText('specialVar')).toBeInTheDocument() + // Assert + expect(screen.getByText('specialVar'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx index 15052e9351..1d81a31091 100644 --- a/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/context-var/__tests__/var-picker.spec.tsx @@ -88,8 +88,9 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() - expect(screen.getByText('var1')).toBeInTheDocument() + // Assert + expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() + expect(screen.getByText('var1'))!.toBeInTheDocument() }) it('should display selected variable with type icon when value is provided', () => { @@ -100,11 +101,13 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByText('var1')).toBeInTheDocument() - expect(screen.getByText('{{')).toBeInTheDocument() - expect(screen.getByText('}}')).toBeInTheDocument() + // Assert + expect(screen.getByText('var1'))!.toBeInTheDocument() + expect(screen.getByText('{{'))!.toBeInTheDocument() + expect(screen.getByText('}}'))!.toBeInTheDocument() // IconTypeIcon should be rendered (check for svg icon) - expect(document.querySelector('svg')).toBeInTheDocument() + // IconTypeIcon should be rendered (check for svg icon) + expect(document.querySelector('svg'))!.toBeInTheDocument() }) it('should show placeholder text when no value is selected', () => { @@ -117,9 +120,40 @@ describe('VarPicker', () => { // Act render() + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert // Assert expect(screen.queryByText('var1')).not.toBeInTheDocument() - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() }) it('should display custom tip message when notSelectedVarTip is provided', () => { @@ -134,7 +168,8 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByText('Select a variable')).toBeInTheDocument() + // Assert + expect(screen.getByText('Select a variable'))!.toBeInTheDocument() }) it('should render dropdown indicator icon', () => { @@ -145,7 +180,8 @@ describe('VarPicker', () => { render() // Assert - Trigger should be present - expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() + // Assert - Trigger should be present + expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() }) }) @@ -162,7 +198,8 @@ describe('VarPicker', () => { const { container } = render() // Assert - expect(container.querySelector('.custom-class')).toBeInTheDocument() + // Assert + expect(container.querySelector('.custom-class'))!.toBeInTheDocument() }) it('should apply custom triggerClassName to trigger button', () => { @@ -176,7 +213,8 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByTestId('portal-trigger')).toHaveClass('custom-trigger-class') + // Assert + expect(screen.getByTestId('portal-trigger'))!.toHaveClass('custom-trigger-class') }) it('should display selected value with proper formatting', () => { @@ -193,9 +231,10 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByText('customVar')).toBeInTheDocument() - expect(screen.getByText('{{')).toBeInTheDocument() - expect(screen.getByText('}}')).toBeInTheDocument() + // Assert + expect(screen.getByText('customVar'))!.toBeInTheDocument() + expect(screen.getByText('{{'))!.toBeInTheDocument() + expect(screen.getByText('}}'))!.toBeInTheDocument() }) }) @@ -212,7 +251,8 @@ describe('VarPicker', () => { await user.click(screen.getByTestId('portal-trigger')) // Assert - expect(screen.getByTestId('portal-content')).toBeInTheDocument() + // Assert + expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() }) it('should call onChange and close dropdown when selecting an option', async () => { @@ -226,12 +266,12 @@ describe('VarPicker', () => { // Open dropdown await user.click(screen.getByTestId('portal-trigger')) - expect(screen.getByTestId('portal-content')).toBeInTheDocument() + expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() // Select a different option const options = screen.getAllByText('var2') expect(options.length).toBeGreaterThan(0) - await user.click(options[0]) + await user.click(options[0]!) // Assert expect(onChange).toHaveBeenCalledWith('var2') @@ -250,7 +290,7 @@ describe('VarPicker', () => { // Open dropdown await user.click(trigger) - expect(screen.getByTestId('portal-content')).toBeInTheDocument() + expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() // Close dropdown await user.click(trigger) @@ -267,6 +307,37 @@ describe('VarPicker', () => { // Act render() + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert // Assert expect(screen.queryByTestId('portal-content')).not.toBeInTheDocument() }) @@ -284,7 +355,7 @@ describe('VarPicker', () => { // Open dropdown await user.click(trigger) - expect(screen.getByTestId('portal-content')).toBeInTheDocument() + expect(screen.getByTestId('portal-content'))!.toBeInTheDocument() // Close dropdown await user.click(trigger) @@ -305,7 +376,8 @@ describe('VarPicker', () => { await user.click(trigger) // Assert - expect(screen.getByText('var1')).toBeInTheDocument() // Original value still displayed + // Assert + expect(screen.getByText('var1'))!.toBeInTheDocument() // Original value still displayed }) }) @@ -322,8 +394,9 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() - expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() + // Assert + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() }) it('should handle empty options array', () => { @@ -338,8 +411,9 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() + // Assert + expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() }) it('should handle null value without crashing', () => { @@ -353,7 +427,8 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder')).toBeInTheDocument() + // Assert + expect(screen.getByText('appDebug.feature.dataSet.queryVariable.choosePlaceholder'))!.toBeInTheDocument() }) it('should handle variable names with special characters safely', () => { @@ -370,7 +445,8 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByText('specialVar')).toBeInTheDocument() + // Assert + expect(screen.getByText('specialVar'))!.toBeInTheDocument() }) it('should handle long variable names', () => { @@ -387,8 +463,9 @@ describe('VarPicker', () => { render() // Assert - expect(screen.getByText('longVar')).toBeInTheDocument() - expect(screen.getByTestId('portal-trigger')).toBeInTheDocument() + // Assert + expect(screen.getByText('longVar'))!.toBeInTheDocument() + expect(screen.getByTestId('portal-trigger'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx index 6d734763ea..a17b39e4b4 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/__tests__/config-content.spec.tsx @@ -203,7 +203,7 @@ describe('ConfigContent', () => { await waitFor(() => { expect(onChange).toHaveBeenCalled() }) - const [nextConfigs] = onChange.mock.calls[0] + const [nextConfigs] = (onChange.mock.calls[0] ?? []) as [any] expect(nextConfigs.retrieval_model).toBe(RETRIEVE_TYPE.multiWay) }) }) @@ -239,10 +239,11 @@ describe('ConfigContent', () => { ) // Assert - expect(screen.getByText('dataset.weightedScore.title')).toBeInTheDocument() - expect(screen.getByText('common.modelProvider.rerankModel.key')).toBeInTheDocument() - expect(screen.getByText('dataset.weightedScore.semantic')).toBeInTheDocument() - expect(screen.getByText('dataset.weightedScore.keyword')).toBeInTheDocument() + // Assert + expect(screen.getByText('dataset.weightedScore.title'))!.toBeInTheDocument() + expect(screen.getByText('common.modelProvider.rerankModel.key'))!.toBeInTheDocument() + expect(screen.getByText('dataset.weightedScore.semantic'))!.toBeInTheDocument() + expect(screen.getByText('dataset.weightedScore.keyword'))!.toBeInTheDocument() }) }) diff --git a/web/app/components/app/configuration/dataset-config/params-config/__tests__/index.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/__tests__/index.spec.tsx index d75f46b371..a892de7fee 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/__tests__/index.spec.tsx @@ -164,7 +164,8 @@ describe('dataset-config/params-config', () => { renderParamsConfig({ disabled: true }) // Assert - expect(screen.getByRole('button', { name: 'dataset.retrievalSettings' })).toBeDisabled() + // Assert + expect(screen.getByRole('button', { name: 'dataset.retrievalSettings' }))!.toBeDisabled() }) }) @@ -181,11 +182,11 @@ describe('dataset-config/params-config', () => { const dialogScope = within(dialog) const incrementButtons = dialogScope.getAllByRole('button', { name: /increment/i }) - await user.click(incrementButtons[0]) + await user.click(incrementButtons[0]!) await waitFor(() => { const [topKInput] = dialogScope.getAllByRole('textbox') - expect(topKInput).toHaveValue('5') + expect(topKInput)!.toHaveValue('5') }) await user.click(dialogScope.getByRole('button', { name: 'common.operation.save' })) @@ -200,7 +201,8 @@ describe('dataset-config/params-config', () => { const [reopenedTopKInput] = reopenedScope.getAllByRole('textbox') // Assert - expect(reopenedTopKInput).toHaveValue('5') + // Assert + expect(reopenedTopKInput)!.toHaveValue('5') }) it('should discard changes when cancel is clicked', async () => { @@ -214,11 +216,11 @@ describe('dataset-config/params-config', () => { const dialogScope = within(dialog) const incrementButtons = dialogScope.getAllByRole('button', { name: /increment/i }) - await user.click(incrementButtons[0]) + await user.click(incrementButtons[0]!) await waitFor(() => { const [topKInput] = dialogScope.getAllByRole('textbox') - expect(topKInput).toHaveValue('5') + expect(topKInput)!.toHaveValue('5') }) const cancelButton = await dialogScope.findByRole('button', { name: 'common.operation.cancel' }) @@ -234,7 +236,8 @@ describe('dataset-config/params-config', () => { const [reopenedTopKInput] = reopenedScope.getAllByRole('textbox') // Assert - expect(reopenedTopKInput).toHaveValue('4') + // Assert + expect(reopenedTopKInput)!.toHaveValue('4') }) it('should prevent saving when rerank model is required but invalid', async () => { @@ -255,7 +258,7 @@ describe('dataset-config/params-config', () => { // Assert expect(toastErrorSpy).toHaveBeenCalledWith('appDebug.datasetConfig.rerankModelRequired') - expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByRole('dialog'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx index f712d7017a..bb260fb499 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/config-content.tsx @@ -121,10 +121,10 @@ const ConfigContent: FC = ({ ...datasetConfigs.weights!, vector_setting: { ...datasetConfigs.weights!.vector_setting!, - vector_weight: value.value[0], + vector_weight: value.value[0]!, }, keyword_setting: { - keyword_weight: value.value[1], + keyword_weight: value.value[1]!, }, }, } diff --git a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx index d4ce935a4d..c8f7bc49ec 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/weighted-score.tsx @@ -54,10 +54,10 @@ const WeightedScore = ({
{t('weightedScore.semantic', { ns: 'dataset' })}
- {formatNumber(value.value[0])} + {formatNumber(value.value[0]!)}
- {formatNumber(value.value[1])} + {formatNumber(value.value[1]!)}
{t('weightedScore.keyword', { ns: 'dataset' })}
diff --git a/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx b/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx index e100ed9490..e4e3f5ab71 100644 --- a/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/settings-modal/__tests__/retrieval-section.spec.tsx @@ -152,7 +152,8 @@ describe('RetrievalChangeTip', () => { await userEvent.click(screen.getByRole('button', { name: 'close-retrieval-change-tip' })) // Assert - expect(screen.getByText('Test message')).toBeInTheDocument() + // Assert + expect(screen.getByText('Test message'))!.toBeInTheDocument() expect(onDismiss).toHaveBeenCalledTimes(1) }) @@ -160,6 +161,37 @@ describe('RetrievalChangeTip', () => { // Arrange & Act render() + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert // Assert expect(screen.queryByText('Test message')).not.toBeInTheDocument() }) @@ -213,12 +245,13 @@ describe('RetrievalSection', () => { />, ) const [topKIncrement] = screen.getAllByRole('button', { name: /increment/i }) - await userEvent.click(topKIncrement) + await userEvent.click(topKIncrement!) // Assert - expect(screen.getByText('External API')).toBeInTheDocument() - expect(screen.getByText('https://api.external.com')).toBeInTheDocument() - expect(screen.getByText('ext-id-999')).toBeInTheDocument() + // Assert + expect(screen.getByText('External API'))!.toBeInTheDocument() + expect(screen.getByText('https://api.external.com'))!.toBeInTheDocument() + expect(screen.getByText('ext-id-999'))!.toBeInTheDocument() expect(handleExternalChange).toHaveBeenCalledWith(expect.objectContaining({ top_k: 4 })) }) @@ -243,9 +276,10 @@ describe('RetrievalSection', () => { ) // Assert - expect(screen.getByText('dataset.retrieval.semantic_search.title')).toBeInTheDocument() + // Assert + expect(screen.getByText('dataset.retrieval.semantic_search.title'))!.toBeInTheDocument() const learnMoreLink = screen.getByRole('link', { name: 'datasetSettings.form.retrievalSetting.learnMore' }) - expect(learnMoreLink).toHaveAttribute('href', 'https://docs.example/use-dify/knowledge/create-knowledge/setting-indexing-methods') + expect(learnMoreLink)!.toHaveAttribute('href', 'https://docs.example/use-dify/knowledge/create-knowledge/setting-indexing-methods') expect(docLink).toHaveBeenCalledWith('/use-dify/knowledge/create-knowledge/setting-indexing-methods') }) @@ -268,10 +302,11 @@ describe('RetrievalSection', () => { />, ) const [topKIncrement] = screen.getAllByRole('button', { name: /increment/i }) - await userEvent.click(topKIncrement) + await userEvent.click(topKIncrement!) // Assert - expect(screen.getByText('dataset.retrieval.keyword_search.title')).toBeInTheDocument() + // Assert + expect(screen.getByText('dataset.retrieval.keyword_search.title'))!.toBeInTheDocument() expect(handleRetrievalChange).toHaveBeenCalledWith(expect.objectContaining({ top_k: 3, })) diff --git a/web/app/components/app/configuration/debug/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/__tests__/index.spec.tsx index 071307303b..4a50ecc626 100644 --- a/web/app/components/app/configuration/debug/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/__tests__/index.spec.tsx @@ -479,8 +479,8 @@ describe('Debug', () => { }, }) - expect(screen.getByText('appDebug.noModelProviderConfigured')).toBeInTheDocument() - expect(screen.getByText('appDebug.noModelProviderConfiguredTip')).toBeInTheDocument() + expect(screen.getByText('appDebug.noModelProviderConfigured'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.noModelProviderConfiguredTip'))!.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'appDebug.manageModels' })) expect(onSetting).toHaveBeenCalledTimes(1) @@ -500,8 +500,8 @@ describe('Debug', () => { }, }) - expect(screen.getByText('appDebug.noModelSelected')).toBeInTheDocument() - expect(screen.getByText('appDebug.noModelSelectedTip')).toBeInTheDocument() + expect(screen.getByText('appDebug.noModelSelected'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.noModelSelectedTip'))!.toBeInTheDocument() expect(screen.queryByText('appDebug.noModelProviderConfigured')).not.toBeInTheDocument() }) }) @@ -510,9 +510,9 @@ describe('Debug', () => { it('should render single-model panel and refresh conversation', () => { renderDebug() - expect(screen.getByTestId('debug-with-single-model')).toBeInTheDocument() + expect(screen.getByTestId('debug-with-single-model'))!.toBeInTheDocument() - fireEvent.click(screen.getAllByTestId('action-button')[0]) + fireEvent.click(screen.getAllByTestId('action-button')[0]!) expect(mockState.mockHandleRestart).toHaveBeenCalledTimes(1) }) @@ -535,8 +535,8 @@ describe('Debug', () => { }, }) - expect(screen.getByTestId('chat-user-input')).toBeInTheDocument() - fireEvent.click(screen.getAllByTestId('action-button')[1]) + expect(screen.getByTestId('chat-user-input'))!.toBeInTheDocument() + fireEvent.click(screen.getAllByTestId('action-button')[1]!) expect(screen.queryByTestId('chat-user-input')).not.toBeInTheDocument() }) @@ -560,7 +560,7 @@ describe('Debug', () => { }, }) - expect(screen.getByTestId('formatting-changed')).toBeInTheDocument() + expect(screen.getByTestId('formatting-changed'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('formatting-cancel')) expect(setFormattingChanged).toHaveBeenCalledWith(false) }) @@ -623,8 +623,8 @@ describe('Debug', () => { }, }) - expect(screen.getByTestId('prompt-value-panel')).toBeInTheDocument() - expect(screen.getByText('appDebug.noResult')).toBeInTheDocument() + expect(screen.getByTestId('prompt-value-panel'))!.toBeInTheDocument() + expect(screen.getByText('appDebug.noResult'))!.toBeInTheDocument() }) it('should notify when required input is missing', () => { @@ -696,7 +696,7 @@ describe('Debug', () => { }) fireEvent.click(screen.getByTestId('panel-send')) - expect(screen.getByTestId('cannot-query-dataset')).toBeInTheDocument() + expect(screen.getByTestId('cannot-query-dataset'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('cannot-query-confirm')) expect(screen.queryByTestId('cannot-query-dataset')).not.toBeInTheDocument() @@ -752,7 +752,7 @@ describe('Debug', () => { fireEvent.click(screen.getByTestId('panel-send')) await waitFor(() => expect(mockState.mockSendCompletionMessage).toHaveBeenCalledTimes(1)) - const [, requestData] = mockState.mockSendCompletionMessage.mock.calls[0] + const [, requestData] = (mockState.mockSendCompletionMessage.mock.calls[0] ?? []) as [unknown, any] expect(requestData).toMatchObject({ inputs: { question: 'hello' }, model_config: { @@ -763,9 +763,9 @@ describe('Debug', () => { dataset_query_variable: 'question', }, }) - expect(screen.getByTestId('text-generation')).toHaveTextContent('final answer') - expect(screen.getByTestId('text-generation')).toHaveAttribute('data-message-id', 'msg-1') - expect(screen.getByTestId('text-generation')).toHaveAttribute('data-tts', 'true') + expect(screen.getByTestId('text-generation'))!.toHaveTextContent('final answer') + expect(screen.getByTestId('text-generation'))!.toHaveAttribute('data-message-id', 'msg-1') + expect(screen.getByTestId('text-generation'))!.toHaveAttribute('data-tts', 'true') }) it('should notify when sending again while a response is in progress', async () => { @@ -830,7 +830,7 @@ describe('Debug', () => { fireEvent.click(screen.getByTestId('panel-send')) await waitFor(() => expect(mockState.mockSendCompletionMessage).toHaveBeenCalledTimes(1)) - expect(screen.getByText('appDebug.noResult')).toBeInTheDocument() + expect(screen.getByText('appDebug.noResult'))!.toBeInTheDocument() }) it('should render prompt log modal in completion mode when store flag is enabled', () => { @@ -845,7 +845,7 @@ describe('Debug', () => { }, }) - expect(screen.getByTestId('prompt-log-modal')).toBeInTheDocument() + expect(screen.getByTestId('prompt-log-modal'))!.toBeInTheDocument() }) it('should close prompt log modal in completion mode', () => { @@ -904,7 +904,7 @@ describe('Debug', () => { }, }) - expect(screen.getByRole('button', { name: 'common.modelProvider.addModel(4/4)' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'common.modelProvider.addModel(4/4)' }))!.toBeDisabled() }) it('should emit completion event in multiple-model completion mode', () => { @@ -945,7 +945,7 @@ describe('Debug', () => { }, }) - fireEvent.click(screen.getAllByTestId('action-button')[0]) + fireEvent.click(screen.getAllByTestId('action-button')[0]!) expect(mockState.mockEventEmitterEmit).toHaveBeenCalledWith({ type: APP_CHAT_WITH_MULTIPLE_MODEL_RESTART, }) @@ -1012,8 +1012,8 @@ describe('Debug', () => { }, }) - expect(screen.getByTestId('prompt-log-modal')).toBeInTheDocument() - expect(screen.getByTestId('agent-log-modal')).toBeInTheDocument() + expect(screen.getByTestId('prompt-log-modal'))!.toBeInTheDocument() + expect(screen.getByTestId('agent-log-modal'))!.toBeInTheDocument() }) it('should close prompt and agent log modals in multiple-model mode', () => { diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/context-provider.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/context-provider.spec.tsx index 5608f4c5a2..6ed016ecc7 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/context-provider.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/context-provider.spec.tsx @@ -8,7 +8,7 @@ const ContextConsumer = () => {
{value.multipleModelConfigs.length}
- +
{String(value.checkCanSend?.())}
) @@ -32,7 +32,7 @@ describe('DebugWithMultipleModelContextProvider', () => { , ) - expect(screen.getByText('1')).toBeInTheDocument() - expect(screen.getByText('true')).toBeInTheDocument() + expect(screen.getByText('1'))!.toBeInTheDocument() + expect(screen.getByText('true'))!.toBeInTheDocument() }) }) diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx index 19c5fbf3fa..747ea4efcb 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/debug-item.spec.tsx @@ -141,8 +141,8 @@ describe('DebugItem', () => { it('should render with basic props', () => { renderComponent() - expect(screen.getByTestId('model-parameter-trigger')).toBeInTheDocument() - expect(screen.getByTestId('dropdown')).toBeInTheDocument() + expect(screen.getByTestId('model-parameter-trigger'))!.toBeInTheDocument() + expect(screen.getByTestId('dropdown'))!.toBeInTheDocument() }) it('should display correct index number', () => { @@ -170,7 +170,7 @@ describe('DebugItem', () => { }) const wrapper = container.firstChild as HTMLElement - expect(wrapper).toHaveClass('custom-class') + expect(wrapper)!.toHaveClass('custom-class') expect(wrapper.style.backgroundColor).toBe('red') }) @@ -193,7 +193,7 @@ describe('DebugItem', () => { renderComponent() - expect(screen.getByTestId('chat-item')).toBeInTheDocument() + expect(screen.getByTestId('chat-item'))!.toBeInTheDocument() expect(screen.queryByTestId('text-generation-item')).not.toBeInTheDocument() }) @@ -207,7 +207,7 @@ describe('DebugItem', () => { renderComponent() - expect(screen.getByTestId('chat-item')).toBeInTheDocument() + expect(screen.getByTestId('chat-item'))!.toBeInTheDocument() }) it('should not render ChatItem when model is not active', () => { @@ -261,7 +261,7 @@ describe('DebugItem', () => { renderComponent() - expect(screen.getByTestId('text-generation-item')).toBeInTheDocument() + expect(screen.getByTestId('text-generation-item'))!.toBeInTheDocument() expect(screen.queryByTestId('chat-item')).not.toBeInTheDocument() }) @@ -502,7 +502,7 @@ describe('DebugItem', () => { expect.arrayContaining([ models[0], models[1], - expect.objectContaining({ model: models[1].model }), + expect.objectContaining({ model: models[1]!.model }), models[2], ]), ) @@ -545,6 +545,37 @@ describe('DebugItem', () => { renderComponent() + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render + // When provider/model doesn't match, ChatItem won't render // When provider/model doesn't match, ChatItem won't render expect(screen.queryByTestId('chat-item')).not.toBeInTheDocument() }) diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx index f1ee03ebbc..bd12ff2c14 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/__tests__/index.spec.tsx @@ -194,13 +194,13 @@ describe('DebugWithMultipleModel', () => { it('should handle empty multipleModelConfigs array', () => { renderComponent({ multipleModelConfigs: [] }) expect(screen.queryByTestId('debug-item')).not.toBeInTheDocument() - expect(screen.getByTestId('chat-input-area')).toBeInTheDocument() + expect(screen.getByTestId('chat-input-area'))!.toBeInTheDocument() }) it('should handle model config with missing required fields', () => { const incompleteConfig = { id: 'incomplete' } as ModelAndParameter renderComponent({ multipleModelConfigs: [incompleteConfig] }) - expect(screen.getByTestId('debug-item')).toBeInTheDocument() + expect(screen.getByTestId('debug-item'))!.toBeInTheDocument() }) it('should handle more than 4 model configs', () => { @@ -257,7 +257,8 @@ describe('DebugWithMultipleModel', () => { renderComponent() // Should still render but handle gracefully - expect(screen.getByTestId('chat-input-area')).toBeInTheDocument() + // Should still render but handle gracefully + expect(screen.getByTestId('chat-input-area'))!.toBeInTheDocument() expect(capturedChatInputProps?.inputsForm).toHaveLength(3) }) }) @@ -287,7 +288,7 @@ describe('DebugWithMultipleModel', () => { rerender() const items = screen.getAllByTestId('debug-item') - expect(items[0]).toHaveAttribute('data-model-id', 'model-2') + expect(items[0])!.toHaveAttribute('data-model-id', 'model-2') }) }) @@ -296,14 +297,14 @@ describe('DebugWithMultipleModel', () => { renderComponent() const chatInput = screen.getByTestId('chat-input-area') - expect(chatInput).toBeInTheDocument() + expect(chatInput)!.toBeInTheDocument() // Check for button accessibility const sendButton = screen.getByRole('button', { name: /send/i }) - expect(sendButton).toBeInTheDocument() + expect(sendButton)!.toBeInTheDocument() const featureButton = screen.getByRole('button', { name: /feature/i }) - expect(featureButton).toBeInTheDocument() + expect(featureButton)!.toBeInTheDocument() }) it('should apply ARIA attributes correctly', () => { @@ -312,8 +313,8 @@ describe('DebugWithMultipleModel', () => { // Debug items should be identifiable const debugItem = screen.getByTestId('debug-item') - expect(debugItem).toBeInTheDocument() - expect(debugItem).toHaveAttribute('data-model-id') + expect(debugItem)!.toBeInTheDocument() + expect(debugItem)!.toHaveAttribute('data-model-id') }) }) @@ -422,7 +423,8 @@ describe('DebugWithMultipleModel', () => { fireEvent.click(screen.getByRole('button', { name: /feature/i })) // Assert - expect(screen.getByTestId('chat-input-area')).toBeInTheDocument() + // Assert + expect(screen.getByTestId('chat-input-area'))!.toBeInTheDocument() expect(capturedChatInputProps?.inputs).toEqual({ audience: 'engineers' }) expect(capturedChatInputProps?.inputsForm).toEqual([ expect.objectContaining({ label: 'City', variable: 'city', hide: false, required: true }), @@ -446,7 +448,8 @@ describe('DebugWithMultipleModel', () => { renderComponent() // Assert - expect(screen.getByTestId('chat-input-area')).toBeInTheDocument() + // Assert + expect(screen.getByTestId('chat-input-area'))!.toBeInTheDocument() }) it('should hide chat input when not in chat mode', () => { @@ -459,6 +462,37 @@ describe('DebugWithMultipleModel', () => { // Act renderComponent({ multipleModelConfigs }) + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert + // Assert // Assert expect(screen.queryByTestId('chat-input-area')).not.toBeInTheDocument() expect(screen.getAllByTestId('debug-item')).toHaveLength(1) @@ -538,7 +572,8 @@ describe('DebugWithMultipleModel', () => { expect(secondItems).toHaveLength(1) // Check that the element still renders the same content - expect(firstItems[0]).toHaveTextContent(secondItems[0].textContent || '') + // Check that the element still renders the same content + expect(firstItems[0])!.toHaveTextContent(secondItems[0]!.textContent || '') }) it('should recalculate size and position when number of models changes', () => { @@ -557,8 +592,8 @@ describe('DebugWithMultipleModel', () => { ) const twoItems = screen.getAllByTestId('debug-item') - expect(twoItems[0].style.width).toBe('calc(50% - 4px - 24px)') - expect(twoItems[1].style.width).toBe('calc(50% - 4px - 24px)') + expect(twoItems[0]!.style.width).toBe('calc(50% - 4px - 24px)') + expect(twoItems[1]!.style.width).toBe('calc(50% - 4px - 24px)') }) }) @@ -583,7 +618,7 @@ describe('DebugWithMultipleModel', () => { expect(element.style.height).toBe('') expect(element.style.transform).toBe(expectation.transform) - expectation.classes?.forEach(cls => expect(element).toHaveClass(cls)) + expectation.classes?.forEach(cls => expect(element)!.toHaveClass(cls)) } it('should arrange items in two-column layout for two models', () => { @@ -596,13 +631,13 @@ describe('DebugWithMultipleModel', () => { // Assert expect(items).toHaveLength(2) - expectItemLayout(items[0], { + expectItemLayout(items[0]!, { width: 'calc(50% - 4px - 24px)', height: '100%', transform: 'translateX(0) translateY(0)', classes: ['mr-2'], }) - expectItemLayout(items[1], { + expectItemLayout(items[1]!, { width: 'calc(50% - 4px - 24px)', height: '100%', transform: 'translateX(calc(100% + 8px)) translateY(0)', @@ -620,19 +655,19 @@ describe('DebugWithMultipleModel', () => { // Assert expect(items).toHaveLength(3) - expectItemLayout(items[0], { + expectItemLayout(items[0]!, { width: 'calc(33.3% - 5.33px - 16px)', height: '100%', transform: 'translateX(0) translateY(0)', classes: ['mr-2'], }) - expectItemLayout(items[1], { + expectItemLayout(items[1]!, { width: 'calc(33.3% - 5.33px - 16px)', height: '100%', transform: 'translateX(calc(100% + 8px)) translateY(0)', classes: ['mr-2'], }) - expectItemLayout(items[2], { + expectItemLayout(items[2]!, { width: 'calc(33.3% - 5.33px - 16px)', height: '100%', transform: 'translateX(calc(200% + 16px)) translateY(0)', @@ -655,25 +690,25 @@ describe('DebugWithMultipleModel', () => { // Assert expect(items).toHaveLength(4) - expectItemLayout(items[0], { + expectItemLayout(items[0]!, { width: 'calc(50% - 4px - 24px)', height: 'calc(50% - 4px)', transform: 'translateX(0) translateY(0)', classes: ['mr-2', 'mb-2'], }) - expectItemLayout(items[1], { + expectItemLayout(items[1]!, { width: 'calc(50% - 4px - 24px)', height: 'calc(50% - 4px)', transform: 'translateX(calc(100% + 8px)) translateY(0)', classes: ['mb-2'], }) - expectItemLayout(items[2], { + expectItemLayout(items[2]!, { width: 'calc(50% - 4px - 24px)', height: 'calc(50% - 4px)', transform: 'translateX(0) translateY(calc(100% + 8px))', classes: ['mr-2'], }) - expectItemLayout(items[3], { + expectItemLayout(items[3]!, { width: 'calc(50% - 4px - 24px)', height: 'calc(50% - 4px)', transform: 'translateX(calc(100% + 8px)) translateY(calc(100% + 8px))', @@ -699,7 +734,7 @@ describe('DebugWithMultipleModel', () => { it('should set scroll area height for chat modes', () => { const { container } = renderComponent() const scrollArea = container.querySelector('.relative.mb-3.grow.overflow-auto.px-6') as HTMLElement - expect(scrollArea).toBeInTheDocument() + expect(scrollArea)!.toBeInTheDocument() expect(scrollArea.style.height).toBe('calc(100% - 60px)') }) diff --git a/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx b/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx index 65e6067e1a..a870538edc 100644 --- a/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx +++ b/web/app/components/app/configuration/debug/debug-with-multiple-model/model-parameter-trigger.tsx @@ -40,7 +40,7 @@ const ModelParameterTrigger: FC = ({ const handleSelectModel = ({ modelId, provider }: { modelId: string, provider: string }) => { const newModelConfigs = [...multipleModelConfigs] newModelConfigs[index] = { - ...newModelConfigs[index], + ...newModelConfigs[index]!, model: modelId, provider, } @@ -49,7 +49,7 @@ const ModelParameterTrigger: FC = ({ const handleParamsChange = (params: FormValue) => { const newModelConfigs = [...multipleModelConfigs] newModelConfigs[index] = { - ...newModelConfigs[index], + ...newModelConfigs[index]!, parameters: params, } onMultipleModelConfigsChange(true, newModelConfigs) diff --git a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx index 0ab6e86c17..634dd6217f 100644 --- a/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx +++ b/web/app/components/app/configuration/debug/debug-with-single-model/__tests__/index.spec.tsx @@ -591,9 +591,10 @@ describe('DebugWithSingleModel', () => { render(} />) // Verify Chat component is rendered - expect(screen.getByTestId('chat-component')).toBeInTheDocument() - expect(screen.getByTestId('chat-input')).toBeInTheDocument() - expect(screen.getByTestId('send-button')).toBeInTheDocument() + // Verify Chat component is rendered + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() + expect(screen.getByTestId('chat-input'))!.toBeInTheDocument() + expect(screen.getByTestId('send-button'))!.toBeInTheDocument() }) it('should render with custom checkCanSend prop', () => { @@ -601,7 +602,7 @@ describe('DebugWithSingleModel', () => { render(} checkCanSend={checkCanSend} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) }) @@ -620,7 +621,7 @@ describe('DebugWithSingleModel', () => { expect(mockSsePost).toHaveBeenCalled() }) - expect(mockSsePost.mock.calls[0][0]).toBe('apps/test-app-id/chat-messages') + expect(mockSsePost.mock.calls[0]![0]).toBe('apps/test-app-id/chat-messages') }) it('should prevent send when checkCanSend returns false', async () => { @@ -666,7 +667,7 @@ describe('DebugWithSingleModel', () => { expect(mockSsePost).toHaveBeenCalled() }) - const body = mockSsePost.mock.calls[0][1].body + const body = mockSsePost.mock.calls[0]![1].body expect(body.model_config.opening_statement).toBe('Hello!') expect(body.model_config.suggested_questions).toEqual(['Q1']) }) @@ -685,7 +686,7 @@ describe('DebugWithSingleModel', () => { expect(mockSsePost).toHaveBeenCalled() }) - const body = mockSsePost.mock.calls[0][1].body + const body = mockSsePost.mock.calls[0]![1].body expect(body.model_config.opening_statement).toBe('') expect(body.model_config.suggested_questions).toEqual([]) }) @@ -717,7 +718,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) it('should handle missing model in provider list', () => { @@ -735,7 +736,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) }) @@ -759,7 +760,8 @@ describe('DebugWithSingleModel', () => { render(} />) // Component should render successfully with filtered variables - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + // Component should render successfully with filtered variables + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) it('should handle empty prompt variables', () => { @@ -775,7 +777,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) }) @@ -784,7 +786,7 @@ describe('DebugWithSingleModel', () => { it('should map tool icons from collection list', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) it('should handle empty tools list', () => { @@ -802,7 +804,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) it('should handle missing collection for tool', () => { @@ -829,7 +831,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) }) @@ -843,7 +845,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) it('should handle missing user profile', () => { @@ -859,7 +861,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) it('should handle null completion params', () => { @@ -870,7 +872,7 @@ describe('DebugWithSingleModel', () => { render(} />) - expect(screen.getByTestId('chat-component')).toBeInTheDocument() + expect(screen.getByTestId('chat-component'))!.toBeInTheDocument() }) }) @@ -940,7 +942,7 @@ describe('DebugWithSingleModel', () => { expect(mockSsePost).toHaveBeenCalled() }) - const body = mockSsePost.mock.calls[0][1].body + const body = mockSsePost.mock.calls[0]![1].body expect(body.files).toEqual([]) }) @@ -989,7 +991,7 @@ describe('DebugWithSingleModel', () => { expect(mockSsePost).toHaveBeenCalled() }) - const body = mockSsePost.mock.calls[0][1].body + const body = mockSsePost.mock.calls[0]![1].body expect(body.files).toHaveLength(1) }) }) diff --git a/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts b/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts index b5d00bd573..c0353c233d 100644 --- a/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts +++ b/web/app/components/app/configuration/hooks/__tests__/use-configuration-utils.spec.ts @@ -345,7 +345,7 @@ describe('useConfiguration utils', () => { }, url: '/datasets', }) - expect(state.collectionList[0].icon).toBe('/console/tool.svg') + expect(state.collectionList[0]!.icon).toBe('/console/tool.svg') expect(state.promptMode).toBe('advanced') expect(state.nextDataSets).toEqual([{ id: 'dataset-1', name: 'Dataset One' }]) expect(state.annotationConfig).toEqual(expect.objectContaining({ diff --git a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts index 5d55c303c5..03f77671d9 100644 --- a/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts +++ b/web/app/components/app/configuration/tools/__tests__/external-data-tool-modal-utils.spec.ts @@ -42,7 +42,7 @@ describe('external-data-tool-modal-utils', () => { it('should build localized providers and default configs for code-based tools', () => { const providers = buildProviders({ codeBasedExtensionList, - locale: LanguagesSupported[1], + locale: LanguagesSupported[1]!, t, }) diff --git a/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts b/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts index 702ebdb5f4..0a9d0724e4 100644 --- a/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts +++ b/web/app/components/app/configuration/tools/external-data-tool-modal-utils.ts @@ -114,8 +114,8 @@ export const getValidationError = ({ if (!systemTypes.includes(localeData.type as typeof systemTypes[number]) && currentProvider?.form_schema) { for (let i = 0; i < currentProvider.form_schema.length; i++) { const form = currentProvider.form_schema[i] - if (!localeData.config?.[form.variable] && form.required) { - return t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale === LanguagesSupported[1] ? form.label['zh-Hans'] : form.label['en-US'] }) + if (!localeData.config?.[form!.variable] && form!.required) { + return t('errorMessage.valueOfVarRequired', { ns: 'appDebug', key: locale === LanguagesSupported[1] ? form!.label['zh-Hans'] : form!.label['en-US'] }) } } } diff --git a/web/app/components/app/configuration/utils.ts b/web/app/components/app/configuration/utils.ts index 3f06c863a4..18bc144b5e 100644 --- a/web/app/components/app/configuration/utils.ts +++ b/web/app/components/app/configuration/utils.ts @@ -40,7 +40,7 @@ export const buildConfigurationFeaturesData = ( }, enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled), allowed_file_types: modelConfig.file_upload?.allowed_file_types || [], - allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || [...FILE_EXTS[SupportUploadFileTypes.image], ...FILE_EXTS[SupportUploadFileTypes.video]].map(ext => `.${ext}`), + allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || [...(FILE_EXTS[SupportUploadFileTypes.image] ?? []), ...(FILE_EXTS[SupportUploadFileTypes.video] ?? [])].map(ext => `.${ext}`), allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3, fileUploadConfig: fileUploadConfigResponse, diff --git a/web/app/components/app/create-app-dialog/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/__tests__/index.spec.tsx index 6fa30ded24..cc59ce7456 100644 --- a/web/app/components/app/create-app-dialog/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/__tests__/index.spec.tsx @@ -53,6 +53,37 @@ describe('CreateAppTemplateDialog', () => { it('should not render when show is false', () => { render() + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false + // FullScreenModal should not render any content when open is false // FullScreenModal should not render any content when open is false expect(screen.queryByRole('dialog')).not.toBeInTheDocument() }) @@ -61,14 +92,15 @@ describe('CreateAppTemplateDialog', () => { render() // FullScreenModal renders with role="dialog" - expect(screen.getByRole('dialog')).toBeInTheDocument() - expect(screen.getByTestId('app-list')).toBeInTheDocument() + // FullScreenModal renders with role="dialog" + expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByTestId('app-list'))!.toBeInTheDocument() }) it('should render create from blank button when onCreateFromBlank is provided', () => { render() - expect(screen.getByTestId('create-from-blank')).toBeInTheDocument() + expect(screen.getByTestId('create-from-blank'))!.toBeInTheDocument() }) it('should not render create from blank button when onCreateFromBlank is not provided', () => { @@ -87,7 +119,7 @@ describe('CreateAppTemplateDialog', () => { expect(screen.queryByRole('dialog')).not.toBeInTheDocument() rerender() - expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByRole('dialog'))!.toBeInTheDocument() }) it('should pass closable prop to FullScreenModal', () => { @@ -97,8 +129,8 @@ describe('CreateAppTemplateDialog', () => { // Verify that the modal has the proper dialog structure const dialog = screen.getByRole('dialog') - expect(dialog).toBeInTheDocument() - expect(dialog).toHaveAttribute('aria-modal', 'true') + expect(dialog)!.toBeInTheDocument() + expect(dialog)!.toHaveAttribute('aria-modal', 'true') }) }) @@ -109,11 +141,12 @@ describe('CreateAppTemplateDialog', () => { // Test that the modal is rendered const dialog = screen.getByRole('dialog') - expect(dialog).toBeInTheDocument() + expect(dialog)!.toBeInTheDocument() // Test that AppList component renders (child component interactions) - expect(screen.getByTestId('app-list')).toBeInTheDocument() - expect(screen.getByTestId('app-list-success')).toBeInTheDocument() + // Test that AppList component renders (child component interactions) + expect(screen.getByTestId('app-list'))!.toBeInTheDocument() + expect(screen.getByTestId('app-list-success'))!.toBeInTheDocument() }) it('should call both onSuccess and onClose when app list success is triggered', () => { @@ -204,8 +237,8 @@ describe('CreateAppTemplateDialog', () => { // Verify that useKeyPress was called with a function const calls = mockUseKeyPress.mock.calls expect(calls.length).toBeGreaterThan(0) - expect(calls[0][0]).toBe('esc') - expect(typeof calls[0][1]).toBe('function') + expect(calls[0]![0]).toBe('esc') + expect(typeof calls[0]![1]).toBe('function') }) }) @@ -243,7 +276,7 @@ describe('CreateAppTemplateDialog', () => { // Test show state render() - expect(screen.getByRole('dialog')).toBeInTheDocument() + expect(screen.getByRole('dialog'))!.toBeInTheDocument() // Test hide state render() @@ -258,7 +291,7 @@ describe('CreateAppTemplateDialog', () => { render() }).not.toThrow() - expect(screen.getByTestId('app-list')).toBeInTheDocument() + expect(screen.getByTestId('app-list'))!.toBeInTheDocument() expect(screen.queryByTestId('create-from-blank')).not.toBeInTheDocument() }) @@ -273,8 +306,8 @@ describe('CreateAppTemplateDialog', () => { render() }).not.toThrow() - expect(screen.getByRole('dialog')).toBeInTheDocument() - expect(screen.getByTestId('app-list')).toBeInTheDocument() + expect(screen.getByRole('dialog'))!.toBeInTheDocument() + expect(screen.getByTestId('app-list'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx index a319bb58f7..34024da54c 100644 --- a/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-list/__tests__/index.spec.tsx @@ -183,15 +183,15 @@ describe('Apps', () => { render() expect(screen.getAllByTestId('app-card')).toHaveLength(6) - expect(screen.getByText('Alpha')).toBeInTheDocument() - expect(screen.getByText('Bravo')).toBeInTheDocument() + expect(screen.getByText('Alpha'))!.toBeInTheDocument() + expect(screen.getByText('Bravo'))!.toBeInTheDocument() }) it('opens create modal when a template card is clicked', () => { render() - fireEvent.click(screen.getAllByTestId('app-card')[0]) - expect(screen.getByTestId('create-from-template-modal')).toBeInTheDocument() + fireEvent.click(screen.getAllByTestId('app-card')[0]!) + expect(screen.getByTestId('create-from-template-modal'))!.toBeInTheDocument() }) it('shows no template message when list is empty', () => { @@ -202,8 +202,8 @@ describe('Apps', () => { render() - expect(screen.getByText('app.newApp.noTemplateFound')).toBeInTheDocument() - expect(screen.getByText('app.newApp.noTemplateFoundTip')).toBeInTheDocument() + expect(screen.getByText('app.newApp.noTemplateFound'))!.toBeInTheDocument() + expect(screen.getByText('app.newApp.noTemplateFoundTip'))!.toBeInTheDocument() }) it('filters templates by keyword and selected app type', async () => { @@ -214,7 +214,7 @@ describe('Apps', () => { }) await waitFor(() => { - expect(screen.getByText('Bravo')).toBeInTheDocument() + expect(screen.getByText('Bravo'))!.toBeInTheDocument() expect(screen.queryByText('Alpha')).not.toBeInTheDocument() }) @@ -224,8 +224,8 @@ describe('Apps', () => { fireEvent.click(screen.getByTestId('type-selector-chat')) await waitFor(() => { - expect(screen.getByText('Alpha')).toBeInTheDocument() - expect(screen.getByText('Bravo')).toBeInTheDocument() + expect(screen.getByText('Alpha'))!.toBeInTheDocument() + expect(screen.getByText('Bravo'))!.toBeInTheDocument() expect(screen.queryByText('Charlie')).not.toBeInTheDocument() }) }) @@ -235,7 +235,7 @@ describe('Apps', () => { render() - fireEvent.click(screen.getAllByTestId('app-card')[0]) + fireEvent.click(screen.getAllByTestId('app-card')[0]!) fireEvent.click(screen.getByTestId('confirm-create')) await waitFor(() => { @@ -264,7 +264,7 @@ describe('Apps', () => { render() - fireEvent.click(screen.getAllByTestId('app-card')[0]) + fireEvent.click(screen.getAllByTestId('app-card')[0]!) fireEvent.click(screen.getByTestId('confirm-create')) await waitFor(() => { @@ -290,7 +290,7 @@ describe('Apps', () => { render() - expect(screen.getByRole('status')).toBeInTheDocument() + expect(screen.getByRole('status'))!.toBeInTheDocument() }) it('should handle an undefined template payload by falling back to the empty state', () => { @@ -301,7 +301,7 @@ describe('Apps', () => { render() - expect(screen.getByText('app.newApp.noTemplateFound')).toBeInTheDocument() + expect(screen.getByText('app.newApp.noTemplateFound'))!.toBeInTheDocument() }) it('should filter templates by category and the remaining app modes', async () => { @@ -309,8 +309,8 @@ describe('Apps', () => { fireEvent.click(screen.getByText('Cat C')) expect(screen.queryByText('Alpha')).not.toBeInTheDocument() - expect(screen.getByText('Echo')).toBeInTheDocument() - expect(screen.getByText('Foxtrot')).toBeInTheDocument() + expect(screen.getByText('Echo'))!.toBeInTheDocument() + expect(screen.getByText('Foxtrot'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('type-selector-advanced')) await waitFor(() => { @@ -322,13 +322,13 @@ describe('Apps', () => { fireEvent.click(screen.getByText('Cat C')) fireEvent.click(screen.getByTestId('type-selector-agent')) await waitFor(() => { - expect(screen.getByText('Echo')).toBeInTheDocument() + expect(screen.getByText('Echo'))!.toBeInTheDocument() expect(screen.queryByText('Foxtrot')).not.toBeInTheDocument() }) fireEvent.click(screen.getByTestId('type-selector-workflow')) await waitFor(() => { - expect(screen.getByText('Foxtrot')).toBeInTheDocument() + expect(screen.getByText('Foxtrot'))!.toBeInTheDocument() expect(screen.queryByText('Echo')).not.toBeInTheDocument() }) @@ -356,11 +356,11 @@ describe('Apps', () => { }) await waitFor(() => { - expect(screen.getByText('Cat A')).toBeInTheDocument() + expect(screen.getByText('Cat A'))!.toBeInTheDocument() }) - fireEvent.click(screen.getAllByTestId('app-card')[0]) - expect(screen.getByTestId('create-from-template-modal')).toBeInTheDocument() + fireEvent.click(screen.getAllByTestId('app-card')[0]!) + expect(screen.getByTestId('create-from-template-modal'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('hide-create-modal')) diff --git a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx index e106cc7eb3..40c32b7dd6 100644 --- a/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx +++ b/web/app/components/app/create-from-dsl-modal/__tests__/index.spec.tsx @@ -137,10 +137,10 @@ describe('CreateFromDSLModal', () => { />, ) - expect(screen.getByText('importFromDSL')).toBeInTheDocument() + expect(screen.getByText('importFromDSL'))!.toBeInTheDocument() await waitFor(() => { - expect(screen.getByText('demo.yml')).toBeInTheDocument() + expect(screen.getByText('demo.yml'))!.toBeInTheDocument() }) }) @@ -159,7 +159,7 @@ describe('CreateFromDSLModal', () => { await act(async () => { fireEvent.click(screen.getByText('importFromDSLUrl')) }) - expect(screen.getByPlaceholderText('importFromDSLUrlPlaceholder')).toBeInTheDocument() + expect(screen.getByPlaceholderText('importFromDSLUrlPlaceholder'))!.toBeInTheDocument() const closeTrigger = screen.getByText('importFromDSL').parentElement?.querySelector('.cursor-pointer.items-center') as HTMLElement fireEvent.click(closeTrigger) @@ -222,7 +222,7 @@ describe('CreateFromDSLModal', () => { ) await waitFor(() => { - expect(screen.getByText('demo.yml')).toBeInTheDocument() + expect(screen.getByText('demo.yml'))!.toBeInTheDocument() }) await act(async () => { @@ -245,7 +245,7 @@ describe('CreateFromDSLModal', () => { ) await waitFor(() => { - expect(screen.getByText('demo.yml')).toBeInTheDocument() + expect(screen.getByText('demo.yml'))!.toBeInTheDocument() }) const removeButton = screen.getByText('demo.yml').closest('.group')?.querySelector('button') as HTMLButtonElement @@ -255,7 +255,7 @@ describe('CreateFromDSLModal', () => { await waitFor(() => { expect(screen.queryByText('demo.yml')).not.toBeInTheDocument() - expect(getCreateButton()).toBeDisabled() + expect(getCreateButton())!.toBeDisabled() }) ahooksMocks.handlers.findLast(item => Array.isArray(item.keys))?.handler() @@ -294,10 +294,10 @@ describe('CreateFromDSLModal', () => { vi.advanceTimersByTime(300) }) - expect(screen.getAllByText('newApp.appCreateDSLErrorTitle')[0]).toBeInTheDocument() + expect(screen.getAllByText('newApp.appCreateDSLErrorTitle')[0])!.toBeInTheDocument() await act(async () => { - fireEvent.click(screen.getAllByRole('button', { name: 'newApp.Confirm' })[0]) + fireEvent.click(screen.getAllByRole('button', { name: 'newApp.Confirm' })[0]!) }) expect(mockImportDSLConfirm).toHaveBeenCalledWith({ @@ -386,7 +386,7 @@ describe('CreateFromDSLModal', () => { />, ) - expect(screen.getByText('apps-full')).toBeInTheDocument() + expect(screen.getByText('apps-full'))!.toBeInTheDocument() ahooksMocks.handlers.findLast(item => Array.isArray(item.keys))?.handler() expect(mockImportDSL).toHaveBeenCalledTimes(1) }) @@ -461,12 +461,12 @@ describe('CreateFromDSLModal', () => { vi.advanceTimersByTime(300) }) await act(async () => { - fireEvent.click(screen.getAllByRole('button', { name: 'newApp.Confirm' })[0]) + fireEvent.click(screen.getAllByRole('button', { name: 'newApp.Confirm' })[0]!) }) expect(toastMocks.error).toHaveBeenCalledWith('newApp.appCreateFailed') await act(async () => { - fireEvent.click(screen.getAllByRole('button', { name: 'newApp.Confirm' })[0]) + fireEvent.click(screen.getAllByRole('button', { name: 'newApp.Confirm' })[0]!) }) expect(toastMocks.error).toHaveBeenCalledTimes(2) }) diff --git a/web/app/components/app/log/__tests__/filter.spec.tsx b/web/app/components/app/log/__tests__/filter.spec.tsx index 4c377ecde7..ec1a0183be 100644 --- a/web/app/components/app/log/__tests__/filter.spec.tsx +++ b/web/app/components/app/log/__tests__/filter.spec.tsx @@ -77,7 +77,7 @@ describe('Filter', () => { it('should render filter components', () => { render() - expect(screen.getByPlaceholderText('operation.search')).toBeInTheDocument() + expect(screen.getByPlaceholderText('operation.search'))!.toBeInTheDocument() }) it('should return null when loading', () => { @@ -89,13 +89,13 @@ describe('Filter', () => { it('should render sort component in chat mode', () => { render() - expect(screen.getByPlaceholderText('operation.search')).toBeInTheDocument() + expect(screen.getByPlaceholderText('operation.search'))!.toBeInTheDocument() }) it('should not render sort component when not in chat mode', () => { render() - expect(screen.getByPlaceholderText('operation.search')).toBeInTheDocument() + expect(screen.getByPlaceholderText('operation.search'))!.toBeInTheDocument() }) }) @@ -105,23 +105,23 @@ describe('Filter', () => { }) it('should have today period with value 0', () => { - expect(TIME_PERIOD_MAPPING['1'].value).toBe(0) - expect(TIME_PERIOD_MAPPING['1'].name).toBe('today') + expect(TIME_PERIOD_MAPPING['1']!.value).toBe(0) + expect(TIME_PERIOD_MAPPING['1']!.name).toBe('today') }) it('should have last7days period with value 7', () => { - expect(TIME_PERIOD_MAPPING['2'].value).toBe(7) - expect(TIME_PERIOD_MAPPING['2'].name).toBe('last7days') + expect(TIME_PERIOD_MAPPING['2']!.value).toBe(7) + expect(TIME_PERIOD_MAPPING['2']!.name).toBe('last7days') }) it('should have last4weeks period with value 28', () => { - expect(TIME_PERIOD_MAPPING['3'].value).toBe(28) - expect(TIME_PERIOD_MAPPING['3'].name).toBe('last4weeks') + expect(TIME_PERIOD_MAPPING['3']!.value).toBe(28) + expect(TIME_PERIOD_MAPPING['3']!.name).toBe('last4weeks') }) it('should have allTime period with value -1', () => { - expect(TIME_PERIOD_MAPPING['9'].value).toBe(-1) - expect(TIME_PERIOD_MAPPING['9'].name).toBe('allTime') + expect(TIME_PERIOD_MAPPING['9']!.value).toBe(-1) + expect(TIME_PERIOD_MAPPING['9']!.name).toBe('allTime') }) }) @@ -158,25 +158,25 @@ describe('Filter', () => { it('should update and clear period, annotation, and sort filters', () => { render() - fireEvent.click(screen.getAllByText('select-9')[0]) + fireEvent.click(screen.getAllByText('select-9')[0]!) expect(mockSetQueryParams).toHaveBeenCalledWith({ ...defaultQueryParams, period: '9', }) - fireEvent.click(screen.getAllByText('clear-chip')[0]) + fireEvent.click(screen.getAllByText('clear-chip')[0]!) expect(mockSetQueryParams).toHaveBeenCalledWith({ ...defaultQueryParams, period: '9', }) - fireEvent.click(screen.getAllByText('select-not_annotated')[0]) + fireEvent.click(screen.getAllByText('select-not_annotated')[0]!) expect(mockSetQueryParams).toHaveBeenCalledWith({ ...defaultQueryParams, annotation_status: 'not_annotated', }) - fireEvent.click(screen.getAllByText('clear-chip')[1]) + fireEvent.click(screen.getAllByText('clear-chip')[1]!) expect(mockSetQueryParams).toHaveBeenCalledWith({ ...defaultQueryParams, annotation_status: 'all', @@ -200,7 +200,8 @@ describe('Filter', () => { render() // Period '1' maps to 'today' in TIME_PERIOD_MAPPING - expect(screen.getByText('filter.period.today')).toBeInTheDocument() + // Period '1' maps to 'today' in TIME_PERIOD_MAPPING + expect(screen.getByText('filter.period.today'))!.toBeInTheDocument() }) it('should display "last7days" when period is set to 2', () => { @@ -211,14 +212,15 @@ describe('Filter', () => { render() - expect(screen.getByText('filter.period.last7days')).toBeInTheDocument() + expect(screen.getByText('filter.period.last7days'))!.toBeInTheDocument() }) it('should display "allTime" when period is set to 9', () => { render() // Default period is '9' which maps to 'allTime' - expect(screen.getByText('filter.period.allTime')).toBeInTheDocument() + // Default period is '9' which maps to 'allTime' + expect(screen.getByText('filter.period.allTime'))!.toBeInTheDocument() }) it('should display annotated status with count when annotation_status is annotated', () => { @@ -230,7 +232,8 @@ describe('Filter', () => { render() // The mock returns count: 10, so the text should include the count - expect(screen.getByText('filter.annotation.annotated (10)')).toBeInTheDocument() + // The mock returns count: 10, so the text should include the count + expect(screen.getByText('filter.annotation.annotated (10)'))!.toBeInTheDocument() }) it('should display not_annotated status when annotation_status is not_annotated', () => { @@ -241,14 +244,15 @@ describe('Filter', () => { render() - expect(screen.getByText('filter.annotation.not_annotated')).toBeInTheDocument() + expect(screen.getByText('filter.annotation.not_annotated'))!.toBeInTheDocument() }) it('should display all annotation status when annotation_status is all', () => { render() // Default annotation_status is 'all' - expect(screen.getByText('filter.annotation.all')).toBeInTheDocument() + // Default annotation_status is 'all' + expect(screen.getByText('filter.annotation.all'))!.toBeInTheDocument() }) it('should return null when annotation count data is unavailable', () => { @@ -269,7 +273,7 @@ describe('Filter', () => { render() - expect(screen.getByPlaceholderText('operation.search')).toBeInTheDocument() + expect(screen.getByPlaceholderText('operation.search'))!.toBeInTheDocument() }) it('should handle descending sort order', () => { @@ -281,7 +285,7 @@ describe('Filter', () => { render() - expect(screen.getByPlaceholderText('operation.search')).toBeInTheDocument() + expect(screen.getByPlaceholderText('operation.search'))!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/app/log/__tests__/list-utils.spec.ts b/web/app/components/app/log/__tests__/list-utils.spec.ts index 245ff0567e..deaaa01591 100644 --- a/web/app/components/app/log/__tests__/list-utils.spec.ts +++ b/web/app/components/app/log/__tests__/list-utils.spec.ts @@ -150,12 +150,12 @@ describe('log list utils', () => { it('should update annotation state helpers', () => { const items = createChatItems() - expect(applyAnnotationEdited(items, 'updated question', 'updated answer', 1)[0].content).toBe('updated question') - expect(applyAnnotationAdded(items, 'annotation-1', 'Dify', 'question', 'answer', 1)[1].annotation).toEqual(expect.objectContaining({ + expect(applyAnnotationEdited(items, 'updated question', 'updated answer', 1)[0]!.content).toBe('updated question') + expect(applyAnnotationAdded(items, 'annotation-1', 'Dify', 'question', 'answer', 1)[1]!.annotation).toEqual(expect.objectContaining({ id: 'annotation-1', authorName: 'Dify', })) - expect(applyAnnotationRemoved(items, 1)[1].annotation).toBeUndefined() + expect(applyAnnotationRemoved(items, 1)[1]!.annotation).toBeUndefined() }) it('should derive urls, scroll thresholds, row values, and detail metadata', () => { diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index c30cf6cf51..411ec8694d 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -78,7 +78,7 @@ const Logs: FC = ({ appDetail }) => { limit, ...((debouncedQueryParams.period !== '9') ? { - start: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period].value, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), + start: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period]!.value, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), end: dayjs().endOf('day').format('YYYY-MM-DD HH:mm'), } : {}), diff --git a/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts b/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts index a087df241a..5064af5f36 100644 --- a/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts +++ b/web/app/components/app/overview/__tests__/app-chart-utils.spec.ts @@ -97,7 +97,7 @@ describe('app-chart-utils', () => { expect(dataset.dimensions).toEqual(['date', 'count']) expect(dataset.source).toHaveLength(2) expect(yAxis.max).toBe(100) - expect(series[0].lineStyle.color).toBe('rgba(6, 148, 162, 1)') + expect(series[0]!.lineStyle.color).toBe('rgba(6, 148, 162, 1)') }) it('should build token-aware tooltip content and split-line intervals for cost charts', () => { @@ -112,11 +112,11 @@ describe('app-chart-utils', () => { }) const xAxis = options.xAxis as Array> - const formatter = xAxis[0].axisLabel.formatter as (value: string) => string - const outerInterval = xAxis[0].splitLine.interval as (index: number) => boolean - const innerInterval = xAxis[1].splitLine.interval as (_index: number, value: string) => boolean + const formatter = xAxis[0]!.axisLabel.formatter as (value: string) => string + const outerInterval = xAxis[0]!.splitLine.interval as (index: number) => boolean + const innerInterval = xAxis[1]!.splitLine.interval as (_index: number, value: string) => boolean const series = options.series as Array> - const tooltipFormatter = series[0].tooltip.formatter as (params: { name: string, data: { total_cost: number, total_price: string } }) => string + const tooltipFormatter = series[0]!.tooltip.formatter as (params: { name: string, data: { total_cost: number, total_price: string } }) => string expect(formatter('Jan 2, 2024')).toBe('Jan 2, 2024') expect(outerInterval(0)).toBe(true) diff --git a/web/app/components/app/overview/__tests__/app-chart.spec.tsx b/web/app/components/app/overview/__tests__/app-chart.spec.tsx index ccd59a8f4a..d6aa241405 100644 --- a/web/app/components/app/overview/__tests__/app-chart.spec.tsx +++ b/web/app/components/app/overview/__tests__/app-chart.spec.tsx @@ -58,10 +58,10 @@ describe('app-chart', () => { />, ) - expect(screen.getByText('Cost title')).toBeInTheDocument() - expect(screen.getByText('300')).toBeInTheDocument() - expect(screen.getByText(/\$3\.7500/)).toBeInTheDocument() - expect(screen.getByTestId('echarts')).toBeInTheDocument() + expect(screen.getByText('Cost title'))!.toBeInTheDocument() + expect(screen.getByText('300'))!.toBeInTheDocument() + expect(screen.getByText(/\$3\.7500/))!.toBeInTheDocument() + expect(screen.getByTestId('echarts'))!.toBeInTheDocument() }) }) @@ -85,10 +85,10 @@ describe('app-chart', () => { />, ) - expect(screen.getByText('analysis.totalMessages.title')).toBeInTheDocument() - expect(screen.getByTestId('echarts')).toBeInTheDocument() + expect(screen.getByText('analysis.totalMessages.title'))!.toBeInTheDocument() + expect(screen.getByTestId('echarts'))!.toBeInTheDocument() - const options = reactEChartsMock.mock.calls[0][0] as { + const options = reactEChartsMock.mock.calls[0]![0] as { dataset: { source: Array> } yAxis: { max: number } } diff --git a/web/app/components/app/overview/embedded/__tests__/index.spec.tsx b/web/app/components/app/overview/embedded/__tests__/index.spec.tsx index 9b8a2ad8c4..0a843c26fd 100644 --- a/web/app/components/app/overview/embedded/__tests__/index.spec.tsx +++ b/web/app/components/app/overview/embedded/__tests__/index.spec.tsx @@ -102,12 +102,12 @@ describe('Embedded', () => { const optionButtons = document.body.querySelectorAll('[class*="option"]') expect(optionButtons.length).toBeGreaterThanOrEqual(3) act(() => { - fireEvent.click(optionButtons[2]) + fireEvent.click(optionButtons[2]!) }) const [chromeText] = screen.getAllByText('appOverview.overview.appInfo.embedded.chromePlugin') act(() => { - fireEvent.click(chromeText) + fireEvent.click(chromeText!) }) expect(mockWindowOpen).toHaveBeenCalledWith( diff --git a/web/app/components/app/overview/embedded/index.tsx b/web/app/components/app/overview/embedded/index.tsx index 547d80f0fe..029566587e 100644 --- a/web/app/components/app/overview/embedded/index.tsx +++ b/web/app/components/app/overview/embedded/index.tsx @@ -105,7 +105,7 @@ const Embedded = ({ siteInfo, isShow, onClose, appBaseUrl, accessToken, classNam if (option === 'chromePlugin') { const splitUrl = OPTION_MAP[option].getContent(appBaseUrl, accessToken).split(': ') if (splitUrl.length > 1) - copy(splitUrl[1]) + copy(splitUrl[1]!) } else { copy(OPTION_MAP[option].getContent(appBaseUrl, accessToken, themeBuilder.theme?.primaryColor ?? '#1C64F2', isTestEnv)) diff --git a/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx b/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx index ddfea01951..3989817a71 100644 --- a/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx +++ b/web/app/components/app/text-generate/saved-items/__tests__/index.spec.tsx @@ -37,8 +37,8 @@ describe('SavedItems', () => { const { container } = render() const markdownElement = container.querySelector('.markdown-body') - expect(markdownElement).toBeInTheDocument() - expect(screen.getByText('11 common.unit.char')).toBeInTheDocument() + expect(markdownElement)!.toBeInTheDocument() + expect(screen.getByText('11 common.unit.char'))!.toBeInTheDocument() const actionArea = container.querySelector('[class*="bg-components-actionbar-bg"]') const actionButtons = actionArea?.querySelectorAll('button') ?? [] @@ -56,11 +56,11 @@ describe('SavedItems', () => { const copyButton = actionButtons[1] const deleteButton = actionButtons[2] - fireEvent.click(copyButton) + fireEvent.click(copyButton!) expect(mockCopy).toHaveBeenCalledWith('hello world') expect(toastSuccessSpy).toHaveBeenCalledWith('common.actionMsg.copySuccessfully') - fireEvent.click(deleteButton) + fireEvent.click(deleteButton!) expect(handleRemove).toHaveBeenCalledWith('1') }) }) diff --git a/web/app/components/app/type-selector/index.tsx b/web/app/components/app/type-selector/index.tsx index 6b8f8985c2..873593a600 100644 --- a/web/app/components/app/type-selector/index.tsx +++ b/web/app/components/app/type-selector/index.tsx @@ -148,9 +148,9 @@ function AppTypeSelectTrigger({ values }: { readonly values: AppSelectorProps['v 'flex h-8 flex-nowrap items-center justify-between gap-1', )} > - +
- +
) diff --git a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx index d255537354..237862fd71 100644 --- a/web/app/components/app/workflow-log/__tests__/filter.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/filter.spec.tsx @@ -56,8 +56,9 @@ describe('Filter', () => { ) // Should render status chip, period chip, and search input - expect(screen.getByText('All')).toBeInTheDocument() - expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + // Should render status chip, period chip, and search input + expect(screen.getByText('All'))!.toBeInTheDocument() + expect(screen.getByPlaceholderText('common.operation.search'))!.toBeInTheDocument() }) it('should render all filter components', () => { @@ -69,11 +70,14 @@ describe('Filter', () => { ) // Status chip - expect(screen.getByText('All')).toBeInTheDocument() + // Status chip + expect(screen.getByText('All'))!.toBeInTheDocument() // Period chip (shows translated key) - expect(screen.getByText('appLog.filter.period.last7days')).toBeInTheDocument() + // Period chip (shows translated key) + expect(screen.getByText('appLog.filter.period.last7days'))!.toBeInTheDocument() // Search input - expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + // Search input + expect(screen.getByPlaceholderText('common.operation.search'))!.toBeInTheDocument() }) }) @@ -90,7 +94,8 @@ describe('Filter', () => { ) // Chip should show Success for succeeded status - expect(screen.getByText('Success')).toBeInTheDocument() + // Chip should show Success for succeeded status + expect(screen.getByText('Success'))!.toBeInTheDocument() }) it('should open status dropdown when clicked', async () => { @@ -107,10 +112,10 @@ describe('Filter', () => { // Should show all status options await waitFor(() => { - expect(screen.getByText('Success')).toBeInTheDocument() - expect(screen.getByText('Fail')).toBeInTheDocument() - expect(screen.getByText('Stop')).toBeInTheDocument() - expect(screen.getByText('Partial Success')).toBeInTheDocument() + expect(screen.getByText('Success'))!.toBeInTheDocument() + expect(screen.getByText('Fail'))!.toBeInTheDocument() + expect(screen.getByText('Stop'))!.toBeInTheDocument() + expect(screen.getByText('Partial Success'))!.toBeInTheDocument() }) }) @@ -167,7 +172,7 @@ describe('Filter', () => { // Find the clear icon (div with group/clear class) in the status chip const clearIcon = container.querySelector('.group\\/clear') - expect(clearIcon).toBeInTheDocument() + expect(clearIcon)!.toBeInTheDocument() await user.click(clearIcon!) expect(setQueryParams).toHaveBeenCalledWith({ @@ -190,7 +195,7 @@ describe('Filter', () => { />, ) - expect(screen.getByText(expectedLabel)).toBeInTheDocument() + expect(screen.getByText(expectedLabel))!.toBeInTheDocument() }) }) @@ -206,7 +211,7 @@ describe('Filter', () => { />, ) - expect(screen.getByText('appLog.filter.period.today')).toBeInTheDocument() + expect(screen.getByText('appLog.filter.period.today'))!.toBeInTheDocument() }) it('should open period dropdown when clicked', async () => { @@ -223,10 +228,10 @@ describe('Filter', () => { // Should show all period options await waitFor(() => { - expect(screen.getByText('appLog.filter.period.today')).toBeInTheDocument() - expect(screen.getByText('appLog.filter.period.last4weeks')).toBeInTheDocument() - expect(screen.getByText('appLog.filter.period.last3months')).toBeInTheDocument() - expect(screen.getByText('appLog.filter.period.allTime')).toBeInTheDocument() + expect(screen.getByText('appLog.filter.period.today'))!.toBeInTheDocument() + expect(screen.getByText('appLog.filter.period.last4weeks'))!.toBeInTheDocument() + expect(screen.getByText('appLog.filter.period.last3months'))!.toBeInTheDocument() + expect(screen.getByText('appLog.filter.period.allTime'))!.toBeInTheDocument() }) }) @@ -287,7 +292,7 @@ describe('Filter', () => { />, ) - expect(screen.getByDisplayValue('test search')).toBeInTheDocument() + expect(screen.getByDisplayValue('test search'))!.toBeInTheDocument() }) it('should call setQueryParams when typing in search', async () => { @@ -337,7 +342,7 @@ describe('Filter', () => { // Find the clear icon div (has cursor-pointer class and contains RiCloseCircleFill) const clearIconDiv = inputWrapper?.querySelector('div.cursor-pointer') - expect(clearIconDiv).toBeInTheDocument() + expect(clearIconDiv)!.toBeInTheDocument() await user.click(clearIconDiv!) expect(setQueryParams).toHaveBeenCalledWith({ @@ -399,11 +404,11 @@ describe('Filter', () => { ['9', 'allTime', -1], ])('TIME_PERIOD_MAPPING[%s] should have name=%s and correct value', (key, name, expectedValue) => { const mapping = TIME_PERIOD_MAPPING[key] - expect(mapping.name).toBe(name) + expect(mapping!.name).toBe(name) if (expectedValue >= 0) - expect(mapping.value).toBe(expectedValue) + expect(mapping!.value).toBe(expectedValue) else - expect(mapping.value).toBe(-1) + expect(mapping!.value).toBe(-1) }) }) @@ -420,7 +425,7 @@ describe('Filter', () => { ) const input = screen.getByPlaceholderText('common.operation.search') - expect(input).toHaveValue('') + expect(input)!.toHaveValue('') }) it('should handle empty string keyword', () => { @@ -432,7 +437,7 @@ describe('Filter', () => { ) const input = screen.getByPlaceholderText('common.operation.search') - expect(input).toHaveValue('') + expect(input)!.toHaveValue('') }) it('should preserve other query params when updating status', async () => { @@ -515,9 +520,9 @@ describe('Filter', () => { />, ) - expect(screen.getByText('Success')).toBeInTheDocument() - expect(screen.getByText('appLog.filter.period.today')).toBeInTheDocument() - expect(screen.getByDisplayValue('integration test')).toBeInTheDocument() + expect(screen.getByText('Success'))!.toBeInTheDocument() + expect(screen.getByText('appLog.filter.period.today'))!.toBeInTheDocument() + expect(screen.getByDisplayValue('integration test'))!.toBeInTheDocument() }) it('should have proper layout with flex and gap', () => { @@ -529,9 +534,9 @@ describe('Filter', () => { ) const filterContainer = container.firstChild as HTMLElement - expect(filterContainer).toHaveClass('flex') - expect(filterContainer).toHaveClass('flex-row') - expect(filterContainer).toHaveClass('gap-2') + expect(filterContainer)!.toHaveClass('flex') + expect(filterContainer)!.toHaveClass('flex-row') + expect(filterContainer)!.toHaveClass('gap-2') }) }) }) diff --git a/web/app/components/app/workflow-log/__tests__/list.spec.tsx b/web/app/components/app/workflow-log/__tests__/list.spec.tsx index 356aaa5a48..35e6369e67 100644 --- a/web/app/components/app/workflow-log/__tests__/list.spec.tsx +++ b/web/app/components/app/workflow-log/__tests__/list.spec.tsx @@ -181,7 +181,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(container.querySelector('.spin-animation')).toBeInTheDocument() + expect(container.querySelector('.spin-animation'))!.toBeInTheDocument() }) it('should render loading state when appDetail is undefined', () => { @@ -191,7 +191,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(container.querySelector('.spin-animation')).toBeInTheDocument() + expect(container.querySelector('.spin-animation'))!.toBeInTheDocument() }) it('should render table when data is available', () => { @@ -201,7 +201,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByRole('table')).toBeInTheDocument() + expect(screen.getByRole('table'))!.toBeInTheDocument() }) it('should render all table headers', () => { @@ -211,11 +211,11 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('appLog.table.header.startTime')).toBeInTheDocument() - expect(screen.getByText('appLog.table.header.status')).toBeInTheDocument() - expect(screen.getByText('appLog.table.header.runtime')).toBeInTheDocument() - expect(screen.getByText('appLog.table.header.tokens')).toBeInTheDocument() - expect(screen.getByText('appLog.table.header.user')).toBeInTheDocument() + expect(screen.getByText('appLog.table.header.startTime'))!.toBeInTheDocument() + expect(screen.getByText('appLog.table.header.status'))!.toBeInTheDocument() + expect(screen.getByText('appLog.table.header.runtime'))!.toBeInTheDocument() + expect(screen.getByText('appLog.table.header.tokens'))!.toBeInTheDocument() + expect(screen.getByText('appLog.table.header.user'))!.toBeInTheDocument() }) it('should render trigger column for workflow apps', () => { @@ -226,7 +226,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('appLog.table.header.triggered_from')).toBeInTheDocument() + expect(screen.getByText('appLog.table.header.triggered_from'))!.toBeInTheDocument() }) it('should not render trigger column for non-workflow apps', () => { @@ -256,7 +256,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('Success')).toBeInTheDocument() + expect(screen.getByText('Success'))!.toBeInTheDocument() }) it('should render failure status correctly', () => { @@ -270,7 +270,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('Failure')).toBeInTheDocument() + expect(screen.getByText('Failure'))!.toBeInTheDocument() }) it('should render stopped status correctly', () => { @@ -284,7 +284,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('Stop')).toBeInTheDocument() + expect(screen.getByText('Stop'))!.toBeInTheDocument() }) it('should render running status correctly', () => { @@ -298,7 +298,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('Running')).toBeInTheDocument() + expect(screen.getByText('Running'))!.toBeInTheDocument() }) it('should render partial-succeeded status correctly', () => { @@ -312,7 +312,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('Partial Success')).toBeInTheDocument() + expect(screen.getByText('Partial Success'))!.toBeInTheDocument() }) }) @@ -332,7 +332,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('John Doe')).toBeInTheDocument() + expect(screen.getByText('John Doe'))!.toBeInTheDocument() }) it('should display end user session id when created by end user', () => { @@ -347,7 +347,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('session-abc-123')).toBeInTheDocument() + expect(screen.getByText('session-abc-123'))!.toBeInTheDocument() }) it('should display N/A when no user info', () => { @@ -362,7 +362,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('N/A')).toBeInTheDocument() + expect(screen.getByText('N/A'))!.toBeInTheDocument() }) }) @@ -405,7 +405,7 @@ describe('WorkflowAppLogList', () => { // Arrow should rotate (indicated by class change) // The sort icon should have rotate-180 class for ascending const sortIcon = startTimeHeader.closest('div')?.querySelector('svg') - expect(sortIcon).toBeInTheDocument() + expect(sortIcon)!.toBeInTheDocument() }) it('should render sort arrow icon', () => { @@ -417,7 +417,7 @@ describe('WorkflowAppLogList', () => { // Check for ArrowDownIcon presence const sortArrow = container.querySelector('svg.ml-0\\.5') - expect(sortArrow).toBeInTheDocument() + expect(sortArrow)!.toBeInTheDocument() }) }) @@ -440,11 +440,11 @@ describe('WorkflowAppLogList', () => { ) const dataRows = screen.getAllByRole('row') - await user.click(dataRows[1]) // Click first data row + await user.click(dataRows[1]!) // Click first data row const dialog = await screen.findByRole('dialog') - expect(dialog).toBeInTheDocument() - expect(screen.getByText('appLog.runDetail.workflowTitle')).toBeInTheDocument() + expect(dialog)!.toBeInTheDocument() + expect(screen.getByText('appLog.runDetail.workflowTitle'))!.toBeInTheDocument() }) it('should close drawer and call onRefresh when closing', async () => { @@ -459,7 +459,7 @@ describe('WorkflowAppLogList', () => { // Open drawer const dataRows = screen.getAllByRole('row') - await user.click(dataRows[1]) + await user.click(dataRows[1]!) await screen.findByRole('dialog') // Close drawer using Escape key @@ -482,14 +482,46 @@ describe('WorkflowAppLogList', () => { const dataRows = screen.getAllByRole('row') const dataRow = dataRows[1] + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight + // Before click - no highlight // Before click - no highlight expect(dataRow).not.toHaveClass('bg-background-default-hover') // After click - has highlight (via currentLog state) - await user.click(dataRow) + await user.click(dataRow!) // The row should have the selected class - expect(dataRow).toHaveClass('bg-background-default-hover') + // The row should have the selected class + expect(dataRow)!.toHaveClass('bg-background-default-hover') }) }) @@ -515,7 +547,7 @@ describe('WorkflowAppLogList', () => { // Open drawer const dataRows = screen.getAllByRole('row') - await user.click(dataRows[1]) + await user.click(dataRows[1]!) await screen.findByRole('dialog') // Replay button should be present for app-run triggers @@ -543,12 +575,12 @@ describe('WorkflowAppLogList', () => { // Open drawer const dataRows = screen.getAllByRole('row') - await user.click(dataRows[1]) + await user.click(dataRows[1]!) await screen.findByRole('dialog') // Replay button should be present for debugging triggers const replayButton = screen.getByRole('button', { name: 'appLog.runDetail.testWithParams' }) - expect(replayButton).toBeInTheDocument() + expect(replayButton)!.toBeInTheDocument() }) it('should not show replay for webhook triggers', async () => { @@ -569,9 +601,40 @@ describe('WorkflowAppLogList', () => { // Open drawer const dataRows = screen.getAllByRole('row') - await user.click(dataRows[1]) + await user.click(dataRows[1]!) await screen.findByRole('dialog') + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers + // Replay button should not be present for webhook triggers // Replay button should not be present for webhook triggers expect(screen.queryByRole('button', { name: 'appLog.runDetail.testWithParams' })).not.toBeInTheDocument() }) @@ -594,7 +657,7 @@ describe('WorkflowAppLogList', () => { // Unread indicator is a small blue dot const unreadDot = container.querySelector('.bg-util-colors-blue-blue-500') - expect(unreadDot).toBeInTheDocument() + expect(unreadDot)!.toBeInTheDocument() }) it('should not show unread indicator for read logs', () => { @@ -629,7 +692,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('1.235s')).toBeInTheDocument() + expect(screen.getByText('1.235s'))!.toBeInTheDocument() }) it('should display 0 elapsed time with special styling', () => { @@ -644,8 +707,8 @@ describe('WorkflowAppLogList', () => { ) const zeroTime = screen.getByText('0.000s') - expect(zeroTime).toBeInTheDocument() - expect(zeroTime).toHaveClass('text-text-quaternary') + expect(zeroTime)!.toBeInTheDocument() + expect(zeroTime)!.toHaveClass('text-text-quaternary') }) }) @@ -664,7 +727,7 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('12345')).toBeInTheDocument() + expect(screen.getByText('12345'))!.toBeInTheDocument() }) }) @@ -680,7 +743,7 @@ describe('WorkflowAppLogList', () => { ) const table = screen.getByRole('table') - expect(table).toBeInTheDocument() + expect(table)!.toBeInTheDocument() // Should only have header row const rows = screen.getAllByRole('row') @@ -721,8 +784,8 @@ describe('WorkflowAppLogList', () => { , ) - expect(screen.getByText('0.000s')).toBeInTheDocument() - expect(screen.getByText('0')).toBeInTheDocument() + expect(screen.getByText('0.000s'))!.toBeInTheDocument() + expect(screen.getByText('0'))!.toBeInTheDocument() }) it('should handle null workflow_run.triggered_from for non-workflow apps', () => { @@ -739,6 +802,37 @@ describe('WorkflowAppLogList', () => { , ) + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column + // Should render without trigger column // Should render without trigger column expect(screen.queryByText('appLog.table.header.triggered_from')).not.toBeInTheDocument() }) diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index 79ae2ed83c..242fd7d093 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -47,7 +47,7 @@ const Logs: FC = ({ appDetail }) => { ...(debouncedQueryParams.keyword ? { keyword: debouncedQueryParams.keyword } : {}), ...((debouncedQueryParams.period !== '9') ? { - created_at__after: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period].value, 'day').startOf('day').tz(timezone).format('YYYY-MM-DDTHH:mm:ssZ'), + created_at__after: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period]!.value, 'day').startOf('day').tz(timezone).format('YYYY-MM-DDTHH:mm:ssZ'), created_at__before: dayjs().endOf('day').tz(timezone).format('YYYY-MM-DDTHH:mm:ssZ'), } : {}), diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 9b82648986..eddcb31d60 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -222,55 +222,55 @@ describe('List', () => { describe('Rendering', () => { it('should render without crashing', () => { renderList() - expect(screen.getByText('app.types.all')).toBeInTheDocument() + expect(screen.getByText('app.types.all'))!.toBeInTheDocument() }) it('should render tab slider with all app types', () => { renderList() - expect(screen.getByText('app.types.all')).toBeInTheDocument() - expect(screen.getByText('app.types.workflow')).toBeInTheDocument() - expect(screen.getByText('app.types.advanced')).toBeInTheDocument() - expect(screen.getByText('app.types.chatbot')).toBeInTheDocument() - expect(screen.getByText('app.types.agent')).toBeInTheDocument() - expect(screen.getByText('app.types.completion')).toBeInTheDocument() + expect(screen.getByText('app.types.all'))!.toBeInTheDocument() + expect(screen.getByText('app.types.workflow'))!.toBeInTheDocument() + expect(screen.getByText('app.types.advanced'))!.toBeInTheDocument() + expect(screen.getByText('app.types.chatbot'))!.toBeInTheDocument() + expect(screen.getByText('app.types.agent'))!.toBeInTheDocument() + expect(screen.getByText('app.types.completion'))!.toBeInTheDocument() }) it('should render search input', () => { renderList() - expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox'))!.toBeInTheDocument() }) it('should render tag filter', () => { renderList() - expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() + expect(screen.getByText('common.tag.placeholder'))!.toBeInTheDocument() }) it('should render created by me checkbox', () => { renderList() - expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() + expect(screen.getByText('app.showMyCreatedAppsOnly'))!.toBeInTheDocument() }) it('should render app cards when apps exist', () => { renderList() - expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument() - expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument() + expect(screen.getByTestId('app-card-app-1'))!.toBeInTheDocument() + expect(screen.getByTestId('app-card-app-2'))!.toBeInTheDocument() }) it('should render new app card for editors', () => { renderList() - expect(screen.getByTestId('new-app-card')).toBeInTheDocument() + expect(screen.getByTestId('new-app-card'))!.toBeInTheDocument() }) it('should render footer when branding is disabled', () => { renderList() - expect(screen.getByTestId('footer')).toBeInTheDocument() + expect(screen.getByTestId('footer'))!.toBeInTheDocument() }) it('should render drop DSL hint for editors', () => { renderList() - expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument() + expect(screen.getByText('app.newApp.dropDSLToCreateApp'))!.toBeInTheDocument() }) }) @@ -281,7 +281,7 @@ describe('List', () => { fireEvent.click(screen.getByText('app.types.workflow')) await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(lastCall.searchParams.get('category')).toBe(AppModeEnum.WORKFLOW) }) @@ -291,7 +291,7 @@ describe('List', () => { fireEvent.click(screen.getByText('app.types.all')) await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] // nuqs removes the default value ('all') from URL params expect(lastCall.searchParams.has('category')).toBe(false) }) @@ -300,7 +300,7 @@ describe('List', () => { describe('Search Functionality', () => { it('should render search input field', () => { renderList() - expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox'))!.toBeInTheDocument() }) it('should handle search input change', () => { @@ -318,7 +318,7 @@ describe('List', () => { renderList() const clearButton = document.querySelector('.group') - expect(clearButton).toBeInTheDocument() + expect(clearButton)!.toBeInTheDocument() if (clearButton) fireEvent.click(clearButton) @@ -329,14 +329,14 @@ describe('List', () => { describe('Tag Filter', () => { it('should render tag filter component', () => { renderList() - expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() + expect(screen.getByText('common.tag.placeholder'))!.toBeInTheDocument() }) }) describe('Created By Me Filter', () => { it('should render checkbox with correct label', () => { renderList() - expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() + expect(screen.getByText('app.showMyCreatedAppsOnly'))!.toBeInTheDocument() }) it('should handle checkbox change', () => { @@ -391,39 +391,39 @@ describe('List', () => { describe('Edge Cases', () => { it('should handle multiple renders without issues', () => { const { unmount } = renderWithNuqs() - expect(screen.getByText('app.types.all')).toBeInTheDocument() + expect(screen.getByText('app.types.all'))!.toBeInTheDocument() unmount() renderList() - expect(screen.getByText('app.types.all')).toBeInTheDocument() + expect(screen.getByText('app.types.all'))!.toBeInTheDocument() }) it('should render app cards correctly', () => { renderList() - expect(screen.getByText('Test App 1')).toBeInTheDocument() - expect(screen.getByText('Test App 2')).toBeInTheDocument() + expect(screen.getByText('Test App 1'))!.toBeInTheDocument() + expect(screen.getByText('Test App 2'))!.toBeInTheDocument() }) it('should render with all filter options visible', () => { renderList() - expect(screen.getByRole('textbox')).toBeInTheDocument() - expect(screen.getByText('common.tag.placeholder')).toBeInTheDocument() - expect(screen.getByText('app.showMyCreatedAppsOnly')).toBeInTheDocument() + expect(screen.getByRole('textbox'))!.toBeInTheDocument() + expect(screen.getByText('common.tag.placeholder'))!.toBeInTheDocument() + expect(screen.getByText('app.showMyCreatedAppsOnly'))!.toBeInTheDocument() }) }) describe('Dragging State', () => { it('should show drop hint when DSL feature is enabled for editors', () => { renderList() - expect(screen.getByText('app.newApp.dropDSLToCreateApp')).toBeInTheDocument() + expect(screen.getByText('app.newApp.dropDSLToCreateApp'))!.toBeInTheDocument() }) it('should render dragging state overlay when dragging', () => { mockDragging = true const { container } = renderList() - expect(container).toBeInTheDocument() + expect(container)!.toBeInTheDocument() }) }) @@ -431,12 +431,12 @@ describe('List', () => { it('should render all app type tabs', () => { renderList() - expect(screen.getByText('app.types.all')).toBeInTheDocument() - expect(screen.getByText('app.types.workflow')).toBeInTheDocument() - expect(screen.getByText('app.types.advanced')).toBeInTheDocument() - expect(screen.getByText('app.types.chatbot')).toBeInTheDocument() - expect(screen.getByText('app.types.agent')).toBeInTheDocument() - expect(screen.getByText('app.types.completion')).toBeInTheDocument() + expect(screen.getByText('app.types.all'))!.toBeInTheDocument() + expect(screen.getByText('app.types.workflow'))!.toBeInTheDocument() + expect(screen.getByText('app.types.advanced'))!.toBeInTheDocument() + expect(screen.getByText('app.types.chatbot'))!.toBeInTheDocument() + expect(screen.getByText('app.types.agent'))!.toBeInTheDocument() + expect(screen.getByText('app.types.completion'))!.toBeInTheDocument() }) it('should update URL for each app type tab click', async () => { @@ -454,7 +454,7 @@ describe('List', () => { onUrlUpdate.mockClear() fireEvent.click(screen.getByText(text)) await vi.waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const lastCall = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(lastCall.searchParams.get('category')).toBe(mode) } }) @@ -464,22 +464,22 @@ describe('List', () => { it('should display all app cards from data', () => { renderList() - expect(screen.getByTestId('app-card-app-1')).toBeInTheDocument() - expect(screen.getByTestId('app-card-app-2')).toBeInTheDocument() + expect(screen.getByTestId('app-card-app-1'))!.toBeInTheDocument() + expect(screen.getByTestId('app-card-app-2'))!.toBeInTheDocument() }) it('should display app names correctly', () => { renderList() - expect(screen.getByText('Test App 1')).toBeInTheDocument() - expect(screen.getByText('Test App 2')).toBeInTheDocument() + expect(screen.getByText('Test App 1'))!.toBeInTheDocument() + expect(screen.getByText('Test App 2'))!.toBeInTheDocument() }) }) describe('Footer Visibility', () => { it('should render footer when branding is disabled', () => { renderList() - expect(screen.getByTestId('footer')).toBeInTheDocument() + expect(screen.getByTestId('footer'))!.toBeInTheDocument() }) }) @@ -493,7 +493,7 @@ describe('List', () => { mockOnDSLFileDropped(mockFile) }) - expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument() + expect(screen.getByTestId('create-dsl-modal'))!.toBeInTheDocument() }) it('should close DSL modal when onClose is called', () => { @@ -505,7 +505,7 @@ describe('List', () => { mockOnDSLFileDropped(mockFile) }) - expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument() + expect(screen.getByTestId('create-dsl-modal'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('close-dsl-modal')) @@ -521,7 +521,7 @@ describe('List', () => { mockOnDSLFileDropped(mockFile) }) - expect(screen.getByTestId('create-dsl-modal')).toBeInTheDocument() + expect(screen.getByTestId('create-dsl-modal'))!.toBeInTheDocument() fireEvent.click(screen.getByTestId('success-dsl-modal')) @@ -585,7 +585,7 @@ describe('List', () => { it('should handle error state in useEffect', () => { mockServiceState.error = new Error('Test error') const { container } = renderList() - expect(container).toBeInTheDocument() + expect(container)!.toBeInTheDocument() }) }) }) diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index ba85ce5178..6d76d632f7 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -115,7 +115,7 @@ const AppCardOperationsMenu: React.FC = ({ await openAsyncWindow(async () => { const { installed_apps } = await fetchInstalledAppList(app.id) if (installed_apps?.length > 0) - return `${basePath}/explore/installed/${installed_apps[0].id}` + return `${basePath}/explore/installed/${installed_apps[0]!.id}` throw new Error('No app found in Explore') }, { onError: (err) => { diff --git a/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx b/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx index 8e8e5821a8..4b0c63f580 100644 --- a/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx +++ b/web/app/components/apps/hooks/__tests__/use-apps-query-state.spec.tsx @@ -114,7 +114,7 @@ describe('useAppsQueryState', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.searchParams.get('keywords')).toBe('search') expect(update.options.history).toBe('push') }) @@ -127,7 +127,7 @@ describe('useAppsQueryState', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.searchParams.get('tagIDs')).toBe('tag1;tag2') }) @@ -139,7 +139,7 @@ describe('useAppsQueryState', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.searchParams.get('isCreatedByMe')).toBe('true') }) @@ -151,7 +151,7 @@ describe('useAppsQueryState', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.searchParams.has('keywords')).toBe(false) }) @@ -163,7 +163,7 @@ describe('useAppsQueryState', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.searchParams.has('tagIDs')).toBe(false) }) @@ -175,7 +175,7 @@ describe('useAppsQueryState', () => { }) await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled()) - const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1][0] + const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0] expect(update.searchParams.has('isCreatedByMe')).toBe(false) }) }) diff --git a/web/app/components/apps/hooks/use-dsl-drag-drop.ts b/web/app/components/apps/hooks/use-dsl-drag-drop.ts index 77d89b87da..3e795018b0 100644 --- a/web/app/components/apps/hooks/use-dsl-drag-drop.ts +++ b/web/app/components/apps/hooks/use-dsl-drag-drop.ts @@ -41,8 +41,8 @@ export const useDSLDragDrop = ({ onDSLFileDropped, containerRef, enabled = true return const file = files[0] - if (file.name.toLowerCase().endsWith('.yaml') || file.name.toLowerCase().endsWith('.yml')) - onDSLFileDropped(file) + if (file!.name.toLowerCase().endsWith('.yaml') || file!.name.toLowerCase().endsWith('.yml')) + onDSLFileDropped(file!) } useEffect(() => { diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index a898e682f5..3b30a839d4 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -151,7 +151,7 @@ const List: FC = ({ const dynamicMargin = Math.max(100, Math.min(containerHeight * 0.2, 200)) // Clamps to 100-200px range, using 20% of container height as the base value observer = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore) + if (entries[0]!.isIntersecting && !isLoading && !isFetchingNextPage && !error && hasMore) fetchNextPage() }, { root: containerRef.current, diff --git a/web/app/components/base/amplitude/__tests__/utils.spec.ts b/web/app/components/base/amplitude/__tests__/utils.spec.ts index f1ff5db1e3..87ac9159d0 100644 --- a/web/app/components/base/amplitude/__tests__/utils.spec.ts +++ b/web/app/components/base/amplitude/__tests__/utils.spec.ts @@ -85,7 +85,7 @@ describe('amplitude utils', () => { setUserProperties(properties) expect(mockIdentify).toHaveBeenCalledTimes(1) - const identifyArg = mockIdentify.mock.calls[0][0] as InstanceType + const identifyArg = mockIdentify.mock.calls[0]![0] as InstanceType expect(identifyArg).toBeInstanceOf(MockIdentify) expect(identifyArg.setCalls).toEqual([ ['role', 'owner'], diff --git a/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx b/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx index 8334512047..07dd809f41 100644 --- a/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx +++ b/web/app/components/base/app-icon-picker/__tests__/index.spec.tsx @@ -156,10 +156,10 @@ describe('AppIconPicker', () => { it('should render emoji and image tabs when upload is enabled', async () => { renderPicker() - expect(await screen.findByText(/emoji/i)).toBeInTheDocument() - expect(screen.getByText(/image/i)).toBeInTheDocument() - expect(screen.getByText(/cancel/i)).toBeInTheDocument() - expect(screen.getByText(/ok/i)).toBeInTheDocument() + expect(await screen.findByText(/emoji/i))!.toBeInTheDocument() + expect(screen.getByText(/image/i))!.toBeInTheDocument() + expect(screen.getByText(/cancel/i))!.toBeInTheDocument() + expect(screen.getByText(/ok/i))!.toBeInTheDocument() }) it('should hide the image tab when upload is disabled', () => { @@ -167,7 +167,7 @@ describe('AppIconPicker', () => { renderPicker() expect(screen.queryByText(/image/i)).not.toBeInTheDocument() - expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument() + expect(screen.getByPlaceholderText(/search/i))!.toBeInTheDocument() }) }) @@ -184,10 +184,10 @@ describe('AppIconPicker', () => { renderPicker() await userEvent.click(screen.getByText(/image/i)) - expect(screen.getByText(/drop.*here/i)).toBeInTheDocument() + expect(screen.getByText(/drop.*here/i))!.toBeInTheDocument() await userEvent.click(screen.getByText(/emoji/i)) - expect(screen.getByPlaceholderText(/search/i)).toBeInTheDocument() + expect(screen.getByPlaceholderText(/search/i))!.toBeInTheDocument() }) it('should call onSelect with emoji data after emoji selection', async () => { @@ -251,7 +251,7 @@ describe('AppIconPicker', () => { fireEvent.change(input, { target: { files: [new File(['png'], 'avatar.png', { type: 'image/png' })] } }) await waitFor(() => { - expect(screen.getByTestId('mock-cropper')).toBeInTheDocument() + expect(screen.getByTestId('mock-cropper'))!.toBeInTheDocument() }) await userEvent.click(screen.getByTestId('trigger-crop')) @@ -261,7 +261,7 @@ describe('AppIconPicker', () => { expect(mocks.handleLocalFileUpload).toHaveBeenCalledTimes(1) }) - const uploadedFile = mocks.handleLocalFileUpload.mock.calls[0][0] + const uploadedFile = mocks.handleLocalFileUpload.mock.calls[0]![0] expect(uploadedFile).toBeInstanceOf(File) expect(uploadedFile.name).toBe('avatar.png') expect(uploadedFile.type).toBe('image/png') @@ -291,7 +291,7 @@ describe('AppIconPicker', () => { await waitFor(() => { expect(screen.queryByTestId('mock-cropper')).not.toBeInTheDocument() const preview = screen.queryByTestId('animated-image') - expect(preview).toBeInTheDocument() + expect(preview)!.toBeInTheDocument() expect(preview?.getAttribute('src')).toContain('blob:mock-url') }) diff --git a/web/app/components/base/audio-btn/__tests__/audio.player.manager.spec.ts b/web/app/components/base/audio-btn/__tests__/audio.player.manager.spec.ts index c613aa2c11..4a98524ed2 100644 --- a/web/app/components/base/audio-btn/__tests__/audio.player.manager.spec.ts +++ b/web/app/components/base/audio-btn/__tests__/audio.player.manager.spec.ts @@ -90,8 +90,8 @@ describe('AudioPlayerManager', () => { expect(mockAudioPlayerConstructor).toHaveBeenCalledTimes(1) expect(first).toBe(second) - expect(mockState.instances[0].setCallback).toHaveBeenCalledTimes(1) - expect(mockState.instances[0].setCallback).toHaveBeenCalledWith(secondCallback) + expect(mockState.instances[0]!.setCallback).toHaveBeenCalledTimes(1) + expect(mockState.instances[0]!.setCallback).toHaveBeenCalledWith(secondCallback) }) it('should cleanup existing player and create a new one when msg id changes', () => { @@ -102,9 +102,9 @@ describe('AudioPlayerManager', () => { const next = manager.getAudioPlayer('/apps/1/text-to-audio', false, 'msg-2', 'world', 'en-US', callback) - expect(previous.pauseAudio).toHaveBeenCalledTimes(1) - expect(previous.cacheBuffers).toEqual([]) - expect(previous.sourceBuffer?.abort).toHaveBeenCalledTimes(1) + expect(previous!.pauseAudio).toHaveBeenCalledTimes(1) + expect(previous!.cacheBuffers).toEqual([]) + expect(previous!.sourceBuffer?.abort).toHaveBeenCalledTimes(1) expect(mockAudioPlayerConstructor).toHaveBeenCalledTimes(2) expect(next).toBe(mockState.instances[1]) }) @@ -114,7 +114,7 @@ describe('AudioPlayerManager', () => { const callback = vi.fn() manager.getAudioPlayer('/text-to-audio', false, 'msg-1', 'hello', 'en-US', callback) const previous = mockState.instances[0] - previous.pauseAudio.mockImplementation(() => { + previous!.pauseAudio.mockImplementation(() => { throw new Error('cleanup failure') }) @@ -122,7 +122,7 @@ describe('AudioPlayerManager', () => { manager.getAudioPlayer('/apps/1/text-to-audio', false, 'msg-2', 'world', 'en-US', callback) }).not.toThrow() - expect(previous.pauseAudio).toHaveBeenCalledTimes(1) + expect(previous!.pauseAudio).toHaveBeenCalledTimes(1) expect(mockAudioPlayerConstructor).toHaveBeenCalledTimes(2) }) }) @@ -135,8 +135,8 @@ describe('AudioPlayerManager', () => { manager.resetMsgId('msg-updated') - expect(mockState.instances[0].resetMsgId).toHaveBeenCalledTimes(1) - expect(mockState.instances[0].resetMsgId).toHaveBeenCalledWith('msg-updated') + expect(mockState.instances[0]!.resetMsgId).toHaveBeenCalledTimes(1) + expect(mockState.instances[0]!.resetMsgId).toHaveBeenCalledWith('msg-updated') }) it('should not throw when resetting message id without an audio player', () => { diff --git a/web/app/components/base/audio-btn/__tests__/audio.spec.ts b/web/app/components/base/audio-btn/__tests__/audio.spec.ts index 4399cb40fd..16f880719d 100644 --- a/web/app/components/base/audio-btn/__tests__/audio.spec.ts +++ b/web/app/components/base/audio-btn/__tests__/audio.spec.ts @@ -241,10 +241,10 @@ describe('AudioPlayer', () => { expect(player.mediaSource).toBe(mediaSource as unknown as MediaSource) expect(globalThis.URL.createObjectURL).toHaveBeenCalledTimes(1) - expect(audio.src).toBe('blob:mock-url') - expect(audio.autoplay).toBe(true) - expect(audioContext.createMediaElementSource).toHaveBeenCalledWith(audio) - expect(audioContext.connect).toHaveBeenCalledTimes(1) + expect(audio!.src).toBe('blob:mock-url') + expect(audio!.autoplay).toBe(true) + expect(audioContext!.createMediaElementSource).toHaveBeenCalledWith(audio) + expect(audioContext!.connect).toHaveBeenCalledTimes(1) }) it('should notify unsupported browser when no MediaSource implementation exists', () => { @@ -254,7 +254,7 @@ describe('AudioPlayer', () => { const audio = testState.audios[0] expect(player.mediaSource).toBeNull() - expect(audio.src).toBe('') + expect(audio!.src).toBe('') expect(mockToastNotify).toHaveBeenCalledTimes(1) expect(mockToastNotify).toHaveBeenCalledWith( expect.objectContaining({ @@ -271,8 +271,8 @@ describe('AudioPlayer', () => { const audio = testState.audios[0] expect(player.mediaSource).not.toBeNull() - expect(audio.disableRemotePlayback).toBe(true) - expect(audio.controls).toBe(true) + expect(audio!.disableRemotePlayback).toBe(true) + expect(audio!.controls).toBe(true) }) }) @@ -282,14 +282,14 @@ describe('AudioPlayer', () => { const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', callback) const audio = testState.audios[0] - audio.emit('play') - audio.emit('ended') - audio.emit('error') - audio.emit('paused') - audio.emit('loaded') - audio.emit('timeupdate') - audio.emit('loadeddate') - audio.emit('canplay') + audio!.emit('play') + audio!.emit('ended') + audio!.emit('error') + audio!.emit('paused') + audio!.emit('loaded') + audio!.emit('timeupdate') + audio!.emit('loadeddate') + audio!.emit('canplay') expect(player.callback).toBe(callback) expect(callback).toHaveBeenCalledWith('play') @@ -306,11 +306,11 @@ describe('AudioPlayer', () => { const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', vi.fn()) const mediaSource = testState.mediaSources[0] - mediaSource.emit('sourceopen') - mediaSource.emit('sourceopen') + mediaSource!.emit('sourceopen') + mediaSource!.emit('sourceopen') - expect(mediaSource.addSourceBuffer).toHaveBeenCalledTimes(1) - expect(player.sourceBuffer).toBe(mediaSource.sourceBuffer) + expect(mediaSource!.addSourceBuffer).toHaveBeenCalledTimes(1) + expect(player.sourceBuffer).toBe(mediaSource!.sourceBuffer) }) }) @@ -366,12 +366,12 @@ describe('AudioPlayer', () => { const audioContext = testState.audioContexts[0] player.isLoadData = true - audioContext.state = 'suspended' + audioContext!.state = 'suspended' player.playAudio() await Promise.resolve() - expect(audioContext.resume).toHaveBeenCalledTimes(1) - expect(audio.play).toHaveBeenCalledTimes(1) + expect(audioContext!.resume).toHaveBeenCalledTimes(1) + expect(audio!.play).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('play') }) @@ -382,11 +382,11 @@ describe('AudioPlayer', () => { const audioContext = testState.audioContexts[0] player.isLoadData = true - audioContext.state = 'running' - audio.ended = true + audioContext!.state = 'running' + audio!.ended = true player.playAudio() - expect(audio.play).toHaveBeenCalledTimes(1) + expect(audio!.play).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('play') }) @@ -397,11 +397,11 @@ describe('AudioPlayer', () => { const audioContext = testState.audioContexts[0] player.isLoadData = true - audioContext.state = 'running' - audio.ended = false + audioContext!.state = 'running' + audio!.ended = false player.playAudio() - expect(audio.play).not.toHaveBeenCalled() + expect(audio!.play).not.toHaveBeenCalled() expect(callback).toHaveBeenCalledWith('play') }) @@ -427,8 +427,8 @@ describe('AudioPlayer', () => { player.pauseAudio() expect(callback).toHaveBeenCalledWith('paused') - expect(audio.pause).toHaveBeenCalledTimes(1) - expect(audioContext.suspend).toHaveBeenCalledTimes(1) + expect(audio!.pause).toHaveBeenCalledTimes(1) + expect(audioContext!.suspend).toHaveBeenCalledTimes(1) }) }) @@ -453,7 +453,7 @@ describe('AudioPlayer', () => { expect(player.isLoadData).toBe(false) expect(player.cacheBuffers).toHaveLength(0) - expect(mediaSource.endOfStream).toHaveBeenCalledTimes(1) + expect(mediaSource!.endOfStream).toHaveBeenCalledTimes(1) expect(callback).not.toHaveBeenCalledWith('play') } finally { @@ -469,19 +469,19 @@ describe('AudioPlayer', () => { const mediaSource = testState.mediaSources[0] const audioBase64 = Buffer.from('hello').toString('base64') - mediaSource.emit('sourceopen') - audio.paused = true + mediaSource!.emit('sourceopen') + audio!.paused = true await player.playAudioWithAudio(audioBase64, true) await Promise.resolve() expect(player.isLoadData).toBe(true) expect(player.cacheBuffers).toHaveLength(0) - expect(mediaSource.sourceBuffer.appendBuffer).toHaveBeenCalledTimes(1) - const appendedAudioData = mediaSource.sourceBuffer.appendBuffer.mock.calls[0][0] + expect(mediaSource!.sourceBuffer.appendBuffer).toHaveBeenCalledTimes(1) + const appendedAudioData = mediaSource!.sourceBuffer.appendBuffer.mock.calls[0]![0] expect(appendedAudioData).toBeInstanceOf(ArrayBuffer) expect(appendedAudioData.byteLength).toBeGreaterThan(0) - expect(audioContext.resume).toHaveBeenCalledTimes(1) - expect(audio.play).toHaveBeenCalledTimes(1) + expect(audioContext!.resume).toHaveBeenCalledTimes(1) + expect(audio!.play).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('play') }) @@ -494,8 +494,8 @@ describe('AudioPlayer', () => { await player.playAudioWithAudio(Buffer.from('hello').toString('base64'), false) expect(player.isLoadData).toBe(false) - expect(audioContext.resume).not.toHaveBeenCalled() - expect(audio.play).not.toHaveBeenCalled() + expect(audioContext!.resume).not.toHaveBeenCalled() + expect(audio!.play).not.toHaveBeenCalled() expect(callback).not.toHaveBeenCalledWith('play') }) @@ -504,11 +504,11 @@ describe('AudioPlayer', () => { const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', callback) const audio = testState.audios[0] - audio.paused = false - audio.ended = true + audio!.paused = false + audio!.ended = true await player.playAudioWithAudio(Buffer.from('hello').toString('base64'), true) - expect(audio.play).toHaveBeenCalledTimes(1) + expect(audio!.play).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('play') }) @@ -517,12 +517,12 @@ describe('AudioPlayer', () => { const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', callback) const audio = testState.audios[0] - audio.paused = false - audio.ended = false - audio.played = {} + audio!.paused = false + audio!.ended = false + audio!.played = {} await player.playAudioWithAudio(Buffer.from('hello').toString('base64'), true) - expect(audio.play).not.toHaveBeenCalled() + expect(audio!.play).not.toHaveBeenCalled() expect(callback).not.toHaveBeenCalledWith('play') }) @@ -531,12 +531,12 @@ describe('AudioPlayer', () => { const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', callback) const audio = testState.audios[0] - audio.paused = false - audio.ended = false - audio.played = null + audio!.paused = false + audio!.ended = false + audio!.played = null await player.playAudioWithAudio(Buffer.from('hello').toString('base64'), true) - expect(audio.play).toHaveBeenCalledTimes(1) + expect(audio!.play).toHaveBeenCalledTimes(1) expect(callback).toHaveBeenCalledWith('play') }) }) @@ -567,8 +567,8 @@ describe('AudioPlayer', () => { it('should queue incoming buffer when source buffer is updating', () => { const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', null) const mediaSource = testState.mediaSources[0] - mediaSource.emit('sourceopen') - mediaSource.sourceBuffer.updating = true + mediaSource!.emit('sourceopen') + mediaSource!.sourceBuffer.updating = true ; (player as unknown as { receiveAudioData: (data: Uint8Array) => void }).receiveAudioData(new Uint8Array([1, 2, 3])) @@ -578,16 +578,16 @@ describe('AudioPlayer', () => { it('should append previously queued buffer before new one when source buffer is idle', () => { const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', null) const mediaSource = testState.mediaSources[0] - mediaSource.emit('sourceopen') + mediaSource!.emit('sourceopen') const existingBuffer = new ArrayBuffer(2) player.cacheBuffers = [existingBuffer] - mediaSource.sourceBuffer.updating = false + mediaSource!.sourceBuffer.updating = false ; (player as unknown as { receiveAudioData: (data: Uint8Array) => void }).receiveAudioData(new Uint8Array([9])) - expect(mediaSource.sourceBuffer.appendBuffer).toHaveBeenCalledTimes(1) - expect(mediaSource.sourceBuffer.appendBuffer).toHaveBeenCalledWith(existingBuffer) + expect(mediaSource!.sourceBuffer.appendBuffer).toHaveBeenCalledTimes(1) + expect(mediaSource!.sourceBuffer.appendBuffer).toHaveBeenCalledWith(existingBuffer) expect(player.cacheBuffers.length).toBe(1) }) @@ -595,15 +595,15 @@ describe('AudioPlayer', () => { vi.useFakeTimers() const player = new AudioPlayer('/text-to-audio', true, 'msg-1', 'hello', 'en-US', null) const mediaSource = testState.mediaSources[0] - mediaSource.emit('sourceopen') - mediaSource.sourceBuffer.updating = false + mediaSource!.emit('sourceopen') + mediaSource!.sourceBuffer.updating = false player.cacheBuffers = [new ArrayBuffer(3)] ; (player as unknown as { finishStream: () => void }).finishStream() vi.advanceTimersByTime(50) - expect(mediaSource.sourceBuffer.appendBuffer).toHaveBeenCalledTimes(1) - expect(mediaSource.endOfStream).toHaveBeenCalledTimes(1) + expect(mediaSource!.sourceBuffer.appendBuffer).toHaveBeenCalledTimes(1) + expect(mediaSource!.endOfStream).toHaveBeenCalledTimes(1) vi.useRealTimers() }) }) diff --git a/web/app/components/base/audio-btn/__tests__/index.spec.tsx b/web/app/components/base/audio-btn/__tests__/index.spec.tsx index 8f6c26d12b..0fb81e9ab6 100644 --- a/web/app/components/base/audio-btn/__tests__/index.spec.tsx +++ b/web/app/components/base/audio-btn/__tests__/index.spec.tsx @@ -32,7 +32,7 @@ describe('AudioBtn', () => { const hoverAndCheckTooltip = async (expectedText: string) => { await userEvent.hover(getButton()) - expect(await screen.findByText(expectedText)).toBeInTheDocument() + expect(await screen.findByText(expectedText))!.toBeInTheDocument() } const getLatestAudioCallback = () => { @@ -64,7 +64,7 @@ describe('AudioBtn', () => { it('should render button with play tooltip by default', async () => { render() - expect(getButton()).toBeInTheDocument() + expect(getButton())!.toBeInTheDocument() expect(getButton()).not.toBeDisabled() await hoverAndCheckTooltip('play') }) @@ -73,7 +73,7 @@ describe('AudioBtn', () => { const { container } = render() const wrapper = container.firstElementChild - expect(wrapper).toHaveClass('custom-wrapper') + expect(wrapper)!.toHaveClass('custom-wrapper') }) }) @@ -87,8 +87,8 @@ describe('AudioBtn', () => { await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled()) const call = mockGetAudioPlayer.mock.calls[0] - expect(call[0]).toBe('/text-to-audio') - expect(call[1]).toBe(true) + expect(call![0]).toBe('/text-to-audio') + expect(call![1]).toBe(true) }) it('should call app endpoint when appId exists', async () => { @@ -100,8 +100,8 @@ describe('AudioBtn', () => { await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled()) const call = mockGetAudioPlayer.mock.calls[0] - expect(call[0]).toBe('/apps/123/text-to-audio') - expect(call[1]).toBe(false) + expect(call![0]).toBe('/apps/123/text-to-audio') + expect(call![1]).toBe(false) }) it('should call installed app endpoint for explore installed routes', async () => { @@ -113,8 +113,8 @@ describe('AudioBtn', () => { await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled()) const call = mockGetAudioPlayer.mock.calls[0] - expect(call[0]).toBe('/installed-apps/456/text-to-audio') - expect(call[1]).toBe(false) + expect(call![0]).toBe('/installed-apps/456/text-to-audio') + expect(call![1]).toBe(false) }) }) @@ -126,9 +126,9 @@ describe('AudioBtn', () => { await waitFor(() => { expect(mockPlayAudio).toHaveBeenCalledTimes(1) - expect(getButton()).toBeDisabled() + expect(getButton())!.toBeDisabled() }) - expect(screen.getByRole('status')).toBeInTheDocument() + expect(screen.getByRole('status'))!.toBeInTheDocument() await hoverAndCheckTooltip('loading') }) @@ -159,7 +159,7 @@ describe('AudioBtn', () => { }) await hoverAndCheckTooltip('loading') - expect(getButton()).toBeDisabled() + expect(getButton())!.toBeDisabled() }) it.each(['ended', 'paused', 'error'])('should return to play tooltip when %s event is received', async (event) => { @@ -183,9 +183,9 @@ describe('AudioBtn', () => { await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled()) const call = mockGetAudioPlayer.mock.calls[0] - expect(call[2]).toBe('msg-1') - expect(call[3]).toBe('hello') - expect(call[4]).toBe('en-US') + expect(call![2]).toBe('msg-1') + expect(call![3]).toBe('hello') + expect(call![4]).toBe('en-US') }) it('should keep empty route when neither token nor appId is present', async () => { @@ -194,9 +194,9 @@ describe('AudioBtn', () => { await waitFor(() => expect(mockGetAudioPlayer).toHaveBeenCalled()) const call = mockGetAudioPlayer.mock.calls[0] - expect(call[0]).toBe('') - expect(call[1]).toBe(false) - expect(call[3]).toBeUndefined() + expect(call![0]).toBe('') + expect(call![1]).toBe(false) + expect(call![3]).toBeUndefined() }) }) }) diff --git a/web/app/components/base/audio-gallery/AudioPlayer.tsx b/web/app/components/base/audio-gallery/AudioPlayer.tsx index 9174b13356..c3b2056698 100644 --- a/web/app/components/base/audio-gallery/AudioPlayer.tsx +++ b/web/app/components/base/audio-gallery/AudioPlayer.tsx @@ -95,7 +95,7 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { for (let i = 0; i < samples; i++) { let sum = 0 for (let j = 0; j < blockSize; j++) - sum += Math.abs(channelData[i * blockSize + j]) + sum += Math.abs(channelData[i * blockSize + j]!) // Apply nonlinear scaling to enhance small amplitudes waveformData.push((sum / blockSize) * 5) } @@ -145,7 +145,7 @@ const AudioPlayer: React.FC = ({ src, srcs }) => { e.preventDefault() const getClientX = (event: React.MouseEvent | React.TouchEvent): number => { if ('touches' in event) - return event.touches[0].clientX + return event.touches[0]!.clientX return event.clientX } const updateProgress = (clientX: number) => { diff --git a/web/app/components/base/audio-gallery/__tests__/AudioPlayer.spec.tsx b/web/app/components/base/audio-gallery/__tests__/AudioPlayer.spec.tsx index a86188f635..fc5578d5a4 100644 --- a/web/app/components/base/audio-gallery/__tests__/AudioPlayer.spec.tsx +++ b/web/app/components/base/audio-gallery/__tests__/AudioPlayer.spec.tsx @@ -47,7 +47,7 @@ async function advanceWaveformTimer() { type ReactEventHandler = ((...args: any[]) => void) | undefined function getReactProps(el: T): Record { const key = Object.keys(el).find(k => k.startsWith('__reactProps$')) - return key ? (el as unknown as Record>)[key] : {} + return key ? (el as unknown as Record>)[key]! : {} } // ─── Setup / teardown ───────────────────────────────────────────────────────── @@ -77,8 +77,8 @@ describe('AudioPlayer — rendering', () => { it('should render the play button and audio element when given a src', () => { render() - expect(screen.getByTestId('play-pause-btn')).toBeInTheDocument() - expect(document.querySelector('audio')).toBeInTheDocument() + expect(screen.getByTestId('play-pause-btn'))!.toBeInTheDocument() + expect(document.querySelector('audio'))!.toBeInTheDocument() expect(document.querySelector('audio')?.getAttribute('src')).toBe('https://example.com/a.mp3') }) @@ -93,7 +93,7 @@ describe('AudioPlayer — rendering', () => { it('should render without crashing when no props are supplied', () => { render() - expect(screen.getByTestId('play-pause-btn')).toBeInTheDocument() + expect(screen.getByTestId('play-pause-btn'))!.toBeInTheDocument() }) }) @@ -129,14 +129,14 @@ describe('AudioPlayer — play/pause', () => { render() const btn = screen.getByTestId('play-pause-btn') - expect(btn.querySelector('.i-ri-play-large-fill')).toBeInTheDocument() + expect(btn.querySelector('.i-ri-play-large-fill'))!.toBeInTheDocument() expect(btn.querySelector('.i-ri-pause-circle-fill')).not.toBeInTheDocument() await act(async () => { fireEvent.click(btn) }) - expect(btn.querySelector('.i-ri-pause-circle-fill')).toBeInTheDocument() + expect(btn.querySelector('.i-ri-pause-circle-fill'))!.toBeInTheDocument() expect(btn.querySelector('.i-ri-play-large-fill')).not.toBeInTheDocument() }) @@ -147,14 +147,14 @@ describe('AudioPlayer — play/pause', () => { await act(async () => { fireEvent.click(btn) }) - expect(btn.querySelector('.i-ri-pause-circle-fill')).toBeInTheDocument() + expect(btn.querySelector('.i-ri-pause-circle-fill'))!.toBeInTheDocument() const audio = document.querySelector('audio') as HTMLAudioElement await act(async () => { audio.dispatchEvent(new Event('ended')) }) - expect(btn.querySelector('.i-ri-play-large-fill')).toBeInTheDocument() + expect(btn.querySelector('.i-ri-play-large-fill'))!.toBeInTheDocument() }) it('should disable the play button when an audio error occurs', async () => { @@ -165,7 +165,7 @@ describe('AudioPlayer — play/pause', () => { audio.dispatchEvent(new Event('error')) }) - expect(screen.getByTestId('play-pause-btn')).toBeDisabled() + expect(screen.getByTestId('play-pause-btn'))!.toBeDisabled() }) }) @@ -181,7 +181,7 @@ describe('AudioPlayer — audio events', () => { audio.dispatchEvent(new Event('loadedmetadata')) }) - expect(screen.getByText('1:30')).toBeInTheDocument() + expect(screen.getByText('1:30'))!.toBeInTheDocument() }) it('should update bufferedTime on progress event', async () => { @@ -216,7 +216,7 @@ describe('AudioPlayer — audio events', () => { audio.dispatchEvent(new Event('error')) }) - expect(screen.getByTestId('play-pause-btn')).toBeDisabled() + expect(screen.getByTestId('play-pause-btn'))!.toBeDisabled() }) }) @@ -230,7 +230,7 @@ describe('AudioPlayer — waveform generation', () => { render() await advanceWaveformTimer() - expect(screen.getByTestId('waveform-canvas')).toBeInTheDocument() + expect(screen.getByTestId('waveform-canvas'))!.toBeInTheDocument() }) it('should use fallback random waveform when fetch returns not-ok', async () => { @@ -240,7 +240,7 @@ describe('AudioPlayer — waveform generation', () => { render() await advanceWaveformTimer() - expect(screen.getByTestId('waveform-canvas')).toBeInTheDocument() + expect(screen.getByTestId('waveform-canvas'))!.toBeInTheDocument() }) it('should use fallback waveform when decodeAudioData rejects', async () => { @@ -257,7 +257,7 @@ describe('AudioPlayer — waveform generation', () => { render() await advanceWaveformTimer() - expect(screen.getByTestId('waveform-canvas')).toBeInTheDocument() + expect(screen.getByTestId('waveform-canvas'))!.toBeInTheDocument() }) it('should show Toast when AudioContext is not available', async () => { @@ -276,7 +276,7 @@ describe('AudioPlayer — waveform generation', () => { render() await advanceWaveformTimer() - expect(screen.getByTestId('play-pause-btn')).toBeDisabled() + expect(screen.getByTestId('play-pause-btn'))!.toBeDisabled() }) it('should not trigger waveform generation when no src or srcs provided', async () => { @@ -305,7 +305,7 @@ describe('AudioPlayer — waveform generation', () => { render() await advanceWaveformTimer() - expect(screen.getByTestId('waveform-canvas')).toBeInTheDocument() + expect(screen.getByTestId('waveform-canvas'))!.toBeInTheDocument() }) it('should use webkitAudioContext when AudioContext is unavailable', async () => { @@ -316,7 +316,7 @@ describe('AudioPlayer — waveform generation', () => { render() await advanceWaveformTimer() - expect(screen.getByTestId('waveform-canvas')).toBeInTheDocument() + expect(screen.getByTestId('waveform-canvas'))!.toBeInTheDocument() }) }) @@ -425,7 +425,7 @@ describe('AudioPlayer — missing coverage', () => { HTMLCanvasElement.prototype.getContext = vi.fn().mockReturnValue(null) render() - expect(screen.getByTestId('waveform-canvas')).toBeInTheDocument() + expect(screen.getByTestId('waveform-canvas'))!.toBeInTheDocument() HTMLCanvasElement.prototype.getContext = originalGetContext }) @@ -535,7 +535,7 @@ describe('AudioPlayer — missing coverage', () => { fireEvent.click(btn) }) - expect(btn).toBeDisabled() + expect(btn)!.toBeDisabled() expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled() expect(toastSpy).not.toHaveBeenCalled() toastSpy.mockRestore() @@ -606,7 +606,7 @@ describe('AudioPlayer — additional branch coverage', () => { audio.dispatchEvent(new Event('error')) }) - expect(screen.queryByTestId('play-pause-btn')).toBeDisabled() + expect(screen.queryByTestId('play-pause-btn'))!.toBeDisabled() }) it('should update current time on timeupdate event', async () => { @@ -632,7 +632,7 @@ describe('AudioPlayer — additional branch coverage', () => { fireEvent.click(btn) }) - expect(btn).toBeDisabled() + expect(btn)!.toBeDisabled() expect(HTMLMediaElement.prototype.play).not.toHaveBeenCalled() expect(toastSpy).not.toHaveBeenCalled() toastSpy.mockRestore() @@ -654,7 +654,7 @@ describe('AudioPlayer — additional branch coverage', () => { }) await advanceWaveformTimer() - expect(screen.getByTestId('waveform-canvas')).toBeInTheDocument() + expect(screen.getByTestId('waveform-canvas'))!.toBeInTheDocument() }) it('should handle missing canvas/audio in handleCanvasInteraction/handleMouseMove', async () => { @@ -683,7 +683,7 @@ describe('AudioPlayer — additional branch coverage', () => { audio.dispatchEvent(new Event('timeupdate')) }) - expect(canvas).toBeInTheDocument() + expect(canvas)!.toBeInTheDocument() }) it('should hit null-ref guards in canvas handlers after unmount', async () => { @@ -710,6 +710,6 @@ describe('AudioPlayer — additional branch coverage', () => { fireEvent.mouseMove(canvas, { clientX: 180 }) // time near 90, outside 0-10 }) - expect(canvas).toBeInTheDocument() + expect(canvas)!.toBeInTheDocument() }) }) diff --git a/web/app/components/base/block-input/index.tsx b/web/app/components/base/block-input/index.tsx index 3a467e97dc..70f66ad3ad 100644 --- a/web/app/components/base/block-input/index.tsx +++ b/web/app/components/base/block-input/index.tsx @@ -75,7 +75,7 @@ const BlockInput: FC = ({ return ( ) } diff --git a/web/app/components/base/carousel/__tests__/index.spec.tsx b/web/app/components/base/carousel/__tests__/index.spec.tsx index e409b85757..f448d02825 100644 --- a/web/app/components/base/carousel/__tests__/index.spec.tsx +++ b/web/app/components/base/carousel/__tests__/index.spec.tsx @@ -99,10 +99,10 @@ describe('Carousel', () => { it('should render region and slides when used with content and items', () => { renderCarouselWithControls() - expect(screen.getByRole('region')).toHaveAttribute('aria-roledescription', 'carousel') - expect(screen.getByTestId('carousel-content')).toHaveClass('flex') + expect(screen.getByRole('region'))!.toHaveAttribute('aria-roledescription', 'carousel') + expect(screen.getByTestId('carousel-content'))!.toHaveClass('flex') screen.getAllByRole('group').forEach((slide) => { - expect(slide).toHaveAttribute('aria-roledescription', 'slide') + expect(slide)!.toHaveAttribute('aria-roledescription', 'slide') }) }) }) @@ -130,7 +130,7 @@ describe('Carousel', () => { { axis: 'y' }, undefined, ) - expect(screen.getByTestId('carousel-content')).toHaveClass('flex-col') + expect(screen.getByTestId('carousel-content'))!.toHaveClass('flex-col') }) }) @@ -171,7 +171,7 @@ describe('Carousel', () => { renderCarouselWithControls() const dots = screen.getAllByRole('button', { name: 'Dot' }) - fireEvent.click(dots[2]) + fireEvent.click(dots[2]!) expect(mockApi.scrollTo).toHaveBeenCalledWith(2) }) @@ -191,9 +191,9 @@ describe('Carousel', () => { }) const dots = screen.getAllByRole('button', { name: 'Dot' }) - expect(screen.getByRole('button', { name: 'Prev' })).toBeEnabled() - expect(screen.getByRole('button', { name: 'Next' })).toBeEnabled() - expect(dots[2]).toHaveAttribute('data-state', 'active') + expect(screen.getByRole('button', { name: 'Prev' }))!.toBeEnabled() + expect(screen.getByRole('button', { name: 'Next' }))!.toBeEnabled() + expect(dots[2])!.toHaveAttribute('data-state', 'active') }) it('should subscribe to embla events and unsubscribe from select on unmount', () => { @@ -232,8 +232,8 @@ describe('Carousel', () => { renderCarouselWithControls() - expect(screen.getByRole('button', { name: 'Prev' })).toBeDisabled() - expect(screen.getByRole('button', { name: 'Next' })).toBeDisabled() + expect(screen.getByRole('button', { name: 'Prev' }))!.toBeDisabled() + expect(screen.getByRole('button', { name: 'Next' }))!.toBeDisabled() expect(screen.queryByRole('button', { name: 'Dot' })).not.toBeInTheDocument() }) diff --git a/web/app/components/base/chat/__tests__/utils.spec.ts b/web/app/components/base/chat/__tests__/utils.spec.ts index a9a05c6065..0951880f9c 100644 --- a/web/app/components/base/chat/__tests__/utils.spec.ts +++ b/web/app/components/base/chat/__tests__/utils.spec.ts @@ -418,11 +418,11 @@ describe('chat utils - url params and answer helpers', () => { const tree = buildChatItemTree(list) expect(tree.length).toBe(1) - expect(tree[0].id).toBe('q1') - expect(tree[0].children?.[0].id).toBe('a1') - expect(tree[0].children?.[0].children?.[0].id).toBe('q2') - expect(tree[0].children?.[0].children?.[0].children?.[0].id).toBe('a2') - expect(tree[0].children?.[0].children?.[0].children?.[0].siblingIndex).toBe(0) + expect(tree[0]!.id).toBe('q1') + expect(tree[0]!.children?.[0]!.id).toBe('a1') + expect(tree[0]!.children?.[0]!.children?.[0]!.id).toBe('q2') + expect(tree[0]!.children?.[0]!.children?.[0]!.children?.[0]!.id).toBe('a2') + expect(tree[0]!.children?.[0]!.children?.[0]!.children?.[0]!.siblingIndex).toBe(0) }) it('buildChatItemTree builds nested tree based on parentMessageId', () => { @@ -439,23 +439,23 @@ describe('chat utils - url params and answer helpers', () => { const tree = buildChatItemTree(list) expect(tree.length).toBe(2) - expect(tree[0].id).toBe('q1') - expect(tree[1].id).toBe('q4') + expect(tree[0]!.id).toBe('q1') + expect(tree[1]!.id).toBe('q4') - const a1 = tree[0].children![0] - expect(a1.id).toBe('a1') - expect(a1.children?.length).toBe(2) - expect(a1.children![0].id).toBe('q2') - expect(a1.children![1].id).toBe('q3') - expect(a1.children![0].children![0].siblingIndex).toBe(0) - expect(a1.children![1].children![0].siblingIndex).toBe(1) + const a1 = tree[0]!.children![0] + expect(a1!.id).toBe('a1') + expect(a1!.children?.length).toBe(2) + expect(a1!.children![0]!.id).toBe('q2') + expect(a1!.children![1]!.id).toBe('q3') + expect(a1!.children![0]!.children![0]!.siblingIndex).toBe(0) + expect(a1!.children![1]!.children![0]!.siblingIndex).toBe(1) }) it('getThreadMessages node without children', () => { const tree = [{ id: 'q1', isAnswer: false }] const thread = getThreadMessages(tree as unknown as ChatItemInTree[], 'q1') expect(thread.length).toBe(1) - expect(thread[0].id).toBe('q1') + expect(thread[0]!.id).toBe('q1') }) it('getThreadMessages target not found', () => { @@ -494,8 +494,8 @@ describe('chat utils - url params and answer helpers', () => { const thread = getThreadMessages(tree as unknown as ChatItemInTree[]) expect(thread.length).toBe(4) expect(thread.map(t => t.id)).toEqual(['q1', 'a1', 'q2', 'a2']) - expect(thread[1].siblingCount).toBe(1) - expect(thread[3].siblingCount).toBe(1) + expect(thread[1]!.siblingCount).toBe(1) + expect(thread[3]!.siblingCount).toBe(1) }) it('getThreadMessages to specific target', () => { @@ -531,8 +531,8 @@ describe('chat utils - url params and answer helpers', () => { const thread = getThreadMessages(tree as unknown as ChatItemInTree[], 'a3') expect(thread.length).toBe(4) expect(thread.map(t => t.id)).toEqual(['q1', 'a1', 'q3', 'a3']) - expect(thread[3].prevSibling).toBe('a2') - expect(thread[3].nextSibling).toBeUndefined() + expect(thread[3]!.prevSibling).toBe('a2') + expect(thread[3]!.nextSibling).toBeUndefined() }) it('getThreadMessages targetNode has descendants', () => { @@ -568,7 +568,7 @@ describe('chat utils - url params and answer helpers', () => { const thread = getThreadMessages(tree as unknown as ChatItemInTree[], 'a1') expect(thread.length).toBe(4) expect(thread.map(t => t.id)).toEqual(['q1', 'a1', 'q3', 'a3']) - expect(thread[3].prevSibling).toBe('a2') + expect(thread[3]!.prevSibling).toBe('a2') }) }) }) diff --git a/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx index bd5f01bcda..83a8666e79 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/chat-wrapper.spec.tsx @@ -151,8 +151,8 @@ describe('ChatWrapper', () => { render() - expect(await screen.findByText('Welcome')).toBeInTheDocument() - expect(await screen.findByText('Q1')).toBeInTheDocument() + expect(await screen.findByText('Welcome'))!.toBeInTheDocument() + expect(await screen.findByText('Q1'))!.toBeInTheDocument() fireEvent.click(screen.getByText('Q1')) expect(handleSend).toHaveBeenCalled() @@ -170,7 +170,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Default opening statement')).toBeInTheDocument() + expect(screen.getByText('Default opening statement'))!.toBeInTheDocument() }) it('should render welcome screen without suggested questions', async () => { @@ -186,7 +186,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Welcome message')).toBeInTheDocument() + expect(await screen.findByText('Welcome message'))!.toBeInTheDocument() }) it('should show responding state', async () => { @@ -197,7 +197,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Bot thinking...')).toBeInTheDocument() + expect(await screen.findByText('Bot thinking...'))!.toBeInTheDocument() }) it('should handle manual message input and stop responding', async () => { @@ -320,9 +320,9 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const disabledContainer = chatInput.closest('.pointer-events-none') - expect(disabledContainer).toBeInTheDocument() - expect(disabledContainer).toHaveClass('opacity-50') + const disabledContainer = chatInput!.closest('.pointer-events-none') + expect(disabledContainer)!.toBeInTheDocument() + expect(disabledContainer)!.toHaveClass('opacity-50') }) it('should not disable input when required field has value', () => { @@ -337,7 +337,7 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput.closest('.pointer-events-none') + const container = chatInput!.closest('.pointer-events-none') expect(container).not.toBeInTheDocument() }) @@ -361,8 +361,8 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput.closest('.pointer-events-none') - expect(container).toBeInTheDocument() + const container = chatInput!.closest('.pointer-events-none') + expect(container)!.toBeInTheDocument() }) it('should not disable input when file is fully uploaded', () => { @@ -411,8 +411,8 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput.closest('.pointer-events-none') - expect(container).toBeInTheDocument() + const container = chatInput!.closest('.pointer-events-none') + expect(container)!.toBeInTheDocument() }) it('should not disable when all files are uploaded', () => { @@ -457,7 +457,7 @@ describe('ChatWrapper', () => { render() const textarea = screen.getByRole('textbox') const container = textarea.closest('.pointer-events-none') - expect(container).toBeInTheDocument() + expect(container)!.toBeInTheDocument() }) it('should not disable input when allInputsHidden is true', () => { @@ -523,7 +523,7 @@ describe('ChatWrapper', () => { render() expect(handleSwitchSibling).toHaveBeenCalledWith('resume-node', expect.any(Object)) - const resumeOptions = handleSwitchSibling.mock.calls[0][1] + const resumeOptions = handleSwitchSibling.mock.calls[0]![1] resumeOptions.onGetSuggestedQuestions('response-from-resume') expect(fetchSuggestedQuestions).toHaveBeenCalledWith('response-from-resume', 'webApp', 'test-app-id') }) @@ -619,7 +619,7 @@ describe('ChatWrapper', () => { render() - const onStopCallback = vi.mocked(useChat).mock.calls[0][3] as (taskId: string) => void + const onStopCallback = vi.mocked(useChat).mock.calls[0]![3] as (taskId: string) => void onStopCallback('taskId-123') expect(stopChatMessageResponding).toHaveBeenCalledWith('', 'taskId-123', 'webApp', 'test-app-id') }) @@ -645,7 +645,7 @@ describe('ChatWrapper', () => { expect(handleSend).toHaveBeenCalled() // Get the options passed to handleSend - const options = handleSend.mock.calls[0][2] + const options = handleSend.mock.calls[0]![2] expect(options.isPublicAPI).toBe(true) // Call onGetSuggestedQuestions @@ -679,7 +679,7 @@ describe('ChatWrapper', () => { fireEvent.click(nextButton) expect(handleSwitchSibling).toHaveBeenCalled() - const options = handleSwitchSibling.mock.calls[0][1] + const options = handleSwitchSibling.mock.calls[0]![1] options.onGetSuggestedQuestions('response-id') expect(fetchSuggestedQuestions).toHaveBeenCalledWith('response-id', 'webApp', 'test-app-id') } @@ -708,8 +708,8 @@ describe('ChatWrapper', () => { expect(handleSend).toHaveBeenCalled() const args = handleSend.mock.calls[0] // args[1] is data - expect(args[1].query).toBe('Q1') - expect(args[1].parent_message_id).toBeNull() + expect(args![1].query).toBe('Q1') + expect(args![1].parent_message_id).toBeNull() } }) @@ -737,7 +737,7 @@ describe('ChatWrapper', () => { fireEvent.click(regenerateBtn) expect(handleSend).toHaveBeenCalled() const args = handleSend.mock.calls[0] - expect(args[1].parent_message_id).toBe('a0') + expect(args![1].parent_message_id).toBe('a0') } }) @@ -774,10 +774,10 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Node 1')).toBeInTheDocument() + expect(await screen.findByText('Node 1'))!.toBeInTheDocument() const input = screen.getAllByRole('textbox').find(el => el.closest('.chat-answer-container')) || screen.getAllByRole('textbox')[0] - fireEvent.change(input, { target: { value: 'test' } }) + fireEvent.change(input!, { target: { value: 'test' } }) const runButton = screen.getByText('Run') fireEvent.click(runButton) @@ -817,10 +817,10 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(await screen.findByText('Node Web 1')).toBeInTheDocument() + expect(await screen.findByText('Node Web 1'))!.toBeInTheDocument() const input = screen.getAllByRole('textbox').find(el => el.closest('.chat-answer-container')) || screen.getAllByRole('textbox')[0] - fireEvent.change(input, { target: { value: 'web-test' } }) + fireEvent.change(input!, { target: { value: 'web-test' } }) fireEvent.click(screen.getByText('Run')) await waitFor(() => { @@ -841,7 +841,7 @@ describe('ChatWrapper', () => { render() expect(document.querySelector('.chat-answer-container')).not.toBeInTheDocument() - expect(screen.getByText('Welcome')).toBeInTheDocument() + expect(screen.getByText('Welcome'))!.toBeInTheDocument() }) it('should show all messages including opening statement when there are multiple messages', () => { @@ -861,7 +861,7 @@ describe('ChatWrapper', () => { render() const welcomeElements = screen.getAllByText('Welcome') expect(welcomeElements.length).toBeGreaterThan(0) - expect(screen.getByText('User message')).toBeInTheDocument() + expect(screen.getByText('User message'))!.toBeInTheDocument() }) it('should show chatNode and inputs form on desktop for new conversation', () => { @@ -873,7 +873,7 @@ describe('ChatWrapper', () => { }) render() - expect(screen.getByText('Test')).toBeInTheDocument() + expect(screen.getByText('Test'))!.toBeInTheDocument() }) it('should show chatNode on mobile for new conversation only', () => { @@ -885,7 +885,7 @@ describe('ChatWrapper', () => { }) const { rerender } = render() - expect(screen.getByText('Test')).toBeInTheDocument() + expect(screen.getByText('Test'))!.toBeInTheDocument() vi.mocked(useChatWithHistoryContext).mockReturnValue({ ...defaultContextValue, @@ -974,8 +974,8 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Answer')).toBeInTheDocument() - expect(screen.getByAltText('answer icon')).toBeInTheDocument() + expect(screen.getByText('Answer'))!.toBeInTheDocument() + expect(screen.getByAltText('answer icon'))!.toBeInTheDocument() }) it('should render question icon fallback when user avatar is available', () => { @@ -993,7 +993,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('J')).toBeInTheDocument() + expect(screen.getByText('J'))!.toBeInTheDocument() }) it('should use fallback values for nullable appData, appMeta and avatar name', () => { @@ -1012,8 +1012,8 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Question with fallback avatar name')).toBeInTheDocument() - expect(screen.getByText('U')).toBeInTheDocument() + expect(screen.getByText('Question with fallback avatar name'))!.toBeInTheDocument() + expect(screen.getByText('U'))!.toBeInTheDocument() }) it('should set handleStop on currentChatInstanceRef', () => { @@ -1101,8 +1101,8 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput.closest('.pointer-events-none') - expect(container).toBeInTheDocument() + const container = chatInput!.closest('.pointer-events-none') + expect(container)!.toBeInTheDocument() }) it('should call formatBooleanInputs when sending message', async () => { @@ -1223,7 +1223,8 @@ describe('ChatWrapper', () => { render() // This tests line 91 - using currentConversationItem.introduction - expect(screen.getByText('Custom introduction from conversation item')).toBeInTheDocument() + // This tests line 91 - using currentConversationItem.introduction + expect(screen.getByText('Custom introduction from conversation item'))!.toBeInTheDocument() }) it('should handle early return when hasEmptyInput is already set', () => { @@ -1242,8 +1243,8 @@ describe('ChatWrapper', () => { // This tests line 106 - early return when hasEmptyInput is set const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput.closest('.pointer-events-none') - expect(container).toBeInTheDocument() + const container = chatInput!.closest('.pointer-events-none') + expect(container)!.toBeInTheDocument() }) it('should handle early return when fileIsUploading is already set', () => { @@ -1270,8 +1271,8 @@ describe('ChatWrapper', () => { // This tests line 109 - early return when fileIsUploading is set const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput.closest('.pointer-events-none') - expect(container).toBeInTheDocument() + const container = chatInput!.closest('.pointer-events-none') + expect(container)!.toBeInTheDocument() }) it('should handle doSend with no parent message id', async () => { @@ -1561,7 +1562,7 @@ describe('ChatWrapper', () => { } as unknown as ChatHookReturn) render() - expect(screen.getByText('Default opening statement')).toBeInTheDocument() + expect(screen.getByText('Default opening statement'))!.toBeInTheDocument() }) it('should handle doSend when regenerating with null parentAnswer', async () => { @@ -1609,7 +1610,9 @@ describe('ChatWrapper', () => { // Just verify the component renders - the actual editedQuestion flow // is tested through the doRegenerate callback that's passed to Chat - expect(screen.getByText('Answer')).toBeInTheDocument() + // Just verify the component renders - the actual editedQuestion flow + // is tested through the doRegenerate callback that's passed to Chat + expect(screen.getByText('Answer'))!.toBeInTheDocument() expect(handleSend).toBeDefined() }) @@ -1629,7 +1632,9 @@ describe('ChatWrapper', () => { // The doRegenerate is passed to Chat component and would be called // This ensures lines 198-200 are covered - expect(screen.getByText('A1')).toBeInTheDocument() + // The doRegenerate is passed to Chat component and would be called + // This ensures lines 198-200 are covered + expect(screen.getByText('A1'))!.toBeInTheDocument() }) it('should handle doRegenerate when question has message_files', async () => { @@ -1809,7 +1814,38 @@ describe('ChatWrapper', () => { render() const textboxes = screen.getAllByRole('textbox') const chatInput = textboxes[textboxes.length - 1] - const container = chatInput.closest('.pointer-events-none') + const container = chatInput!.closest('.pointer-events-none') + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required + // Should not be disabled because it's not required // Should not be disabled because it's not required expect(container).not.toBeInTheDocument() }) diff --git a/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx index d75f9897a7..a183a7670b 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/header-in-mobile.spec.tsx @@ -143,7 +143,7 @@ describe('HeaderInMobile', () => { it('should render title when no conversation', () => { render() - expect(screen.getByText('Test Chat')).toBeInTheDocument() + expect(screen.getByText('Test Chat'))!.toBeInTheDocument() }) it('should render conversation name when active', async () => { @@ -154,7 +154,7 @@ describe('HeaderInMobile', () => { }) render() - expect(await screen.findByText('Conv 1')).toBeInTheDocument() + expect(await screen.findByText('Conv 1'))!.toBeInTheDocument() }) it('should open and close sidebar', async () => { @@ -162,11 +162,12 @@ describe('HeaderInMobile', () => { // Open sidebar (menu button is the first action btn) const menuButton = screen.getAllByRole('button')[0] - fireEvent.click(menuButton) + fireEvent.click(menuButton!) // HeaderInMobile renders MobileSidebar which renders Sidebar and overlay - expect(await screen.findByTestId('mobile-sidebar-overlay')).toBeInTheDocument() - expect(screen.getByTestId('sidebar-content')).toBeInTheDocument() + // HeaderInMobile renders MobileSidebar which renders Sidebar and overlay + expect(await screen.findByTestId('mobile-sidebar-overlay'))!.toBeInTheDocument() + expect(screen.getByTestId('sidebar-content'))!.toBeInTheDocument() // Close sidebar via overlay click fireEvent.click(screen.getByTestId('mobile-sidebar-overlay')) @@ -180,15 +181,16 @@ describe('HeaderInMobile', () => { // Open sidebar const menuButton = screen.getAllByRole('button')[0] - fireEvent.click(menuButton) + fireEvent.click(menuButton!) - expect(await screen.findByTestId('mobile-sidebar-overlay')).toBeInTheDocument() + expect(await screen.findByTestId('mobile-sidebar-overlay'))!.toBeInTheDocument() // Click inside sidebar content (should not close) fireEvent.click(screen.getByTestId('sidebar-content')) // Sidebar should still be visible - expect(screen.getByTestId('mobile-sidebar-overlay')).toBeInTheDocument() + // Sidebar should still be visible + expect(screen.getByTestId('mobile-sidebar-overlay'))!.toBeInTheDocument() }) it('should open and close chat settings', async () => { @@ -204,12 +206,13 @@ describe('HeaderInMobile', () => { // Find and click "View Chat Settings" await waitFor(() => { - expect(screen.getByText(/share\.chat\.viewChatSettings/i)).toBeInTheDocument() + expect(screen.getByText(/share\.chat\.viewChatSettings/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/share\.chat\.viewChatSettings/i)) // Check if chat settings overlay is open - expect(screen.getByTestId('mobile-chat-settings-overlay')).toBeInTheDocument() + // Check if chat settings overlay is open + expect(screen.getByTestId('mobile-chat-settings-overlay'))!.toBeInTheDocument() // Close chat settings via overlay click fireEvent.click(screen.getByTestId('mobile-chat-settings-overlay')) @@ -229,18 +232,19 @@ describe('HeaderInMobile', () => { // Open dropdown and chat settings fireEvent.click(await screen.findByTestId('mobile-more-btn')) await waitFor(() => { - expect(screen.getByText(/share\.chat\.viewChatSettings/i)).toBeInTheDocument() + expect(screen.getByText(/share\.chat\.viewChatSettings/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/share\.chat\.viewChatSettings/i)) - expect(screen.getByTestId('mobile-chat-settings-overlay')).toBeInTheDocument() + expect(screen.getByTestId('mobile-chat-settings-overlay'))!.toBeInTheDocument() // Click inside the settings panel (find the title) const settingsTitle = screen.getByText(/share\.chat\.chatSettingsTitle/i) fireEvent.click(settingsTitle) // Settings should still be visible - expect(screen.getByTestId('mobile-chat-settings-overlay')).toBeInTheDocument() + // Settings should still be visible + expect(screen.getByTestId('mobile-chat-settings-overlay'))!.toBeInTheDocument() }) it('should hide chat settings option when no input forms', async () => { @@ -274,7 +278,7 @@ describe('HeaderInMobile', () => { // Click "New Conversation" or "Reset Chat" await waitFor(() => { - expect(screen.getByText(/share\.chat\.resetChat/i)).toBeInTheDocument() + expect(screen.getByText(/share\.chat\.resetChat/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/share\.chat\.resetChat/i)) @@ -297,7 +301,7 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) await waitFor(() => { - expect(screen.getByText(/explore\.sidebar\.action\.pin/i)).toBeInTheDocument() + expect(screen.getByText(/explore\.sidebar\.action\.pin/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/explore\.sidebar\.action\.pin/i)) expect(handlePin).toHaveBeenCalledWith('1') @@ -319,7 +323,7 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) await waitFor(() => { - expect(screen.getByText(/explore\.sidebar\.action\.unpin/i)).toBeInTheDocument() + expect(screen.getByText(/explore\.sidebar\.action\.unpin/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/explore\.sidebar\.action\.unpin/i)) expect(handleUnpin).toHaveBeenCalledWith('1') @@ -339,12 +343,13 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) await waitFor(() => { - expect(screen.getByText(/explore\.sidebar\.action\.rename/i)).toBeInTheDocument() + expect(screen.getByText(/explore\.sidebar\.action\.rename/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/explore\.sidebar\.action\.rename/i)) // RenameModal should be visible - expect(screen.getByRole('dialog')).toBeInTheDocument() + // RenameModal should be visible + expect(screen.getByRole('dialog'))!.toBeInTheDocument() const input = screen.getByDisplayValue('Conv 1') fireEvent.change(input, { target: { value: 'New Name' } }) @@ -367,12 +372,13 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) await waitFor(() => { - expect(screen.getByText(/explore\.sidebar\.action\.rename/i)).toBeInTheDocument() + expect(screen.getByText(/explore\.sidebar\.action\.rename/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/explore\.sidebar\.action\.rename/i)) // RenameModal should be visible - expect(screen.getByRole('dialog')).toBeInTheDocument() + // RenameModal should be visible + expect(screen.getByRole('dialog'))!.toBeInTheDocument() // Click cancel button const cancelButton = screen.getByRole('button', { name: /common\.operation\.cancel/i }) @@ -399,12 +405,13 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) await waitFor(() => { - expect(screen.getByText(/explore\.sidebar\.action\.rename/i)).toBeInTheDocument() + expect(screen.getByText(/explore\.sidebar\.action\.rename/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/explore\.sidebar\.action\.rename/i)) // RenameModal should be visible with loading state - expect(screen.getByRole('dialog')).toBeInTheDocument() + // RenameModal should be visible with loading state + expect(screen.getByRole('dialog'))!.toBeInTheDocument() }) it('should handle delete conversation', async () => { @@ -421,13 +428,13 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) await waitFor(() => { - expect(screen.getByText(/explore\.sidebar\.action\.delete/i)).toBeInTheDocument() + expect(screen.getByText(/explore\.sidebar\.action\.delete/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/explore\.sidebar\.action\.delete/i)) // Confirm modal await waitFor(() => { - expect(screen.getAllByText(/share\.chat\.deleteConversation\.title/i)[0]).toBeInTheDocument() + expect(screen.getAllByText(/share\.chat\.deleteConversation\.title/i)[0])!.toBeInTheDocument() }) fireEvent.click(screen.getByRole('button', { name: /common\.operation\.confirm/i })) expect(handleDelete).toHaveBeenCalledWith('1', expect.any(Object)) @@ -447,13 +454,13 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) await waitFor(() => { - expect(screen.getByText(/explore\.sidebar\.action\.delete/i)).toBeInTheDocument() + expect(screen.getByText(/explore\.sidebar\.action\.delete/i))!.toBeInTheDocument() }) fireEvent.click(screen.getByText(/explore\.sidebar\.action\.delete/i)) // Confirm modal should be visible await waitFor(() => { - expect(screen.getAllByText(/share\.chat\.deleteConversation\.title/i)[0]).toBeInTheDocument() + expect(screen.getAllByText(/share\.chat\.deleteConversation\.title/i)[0])!.toBeInTheDocument() }) // Click cancel @@ -519,7 +526,7 @@ describe('HeaderInMobile', () => { }) render() - expect(screen.getByText('My App')).toBeInTheDocument() + expect(screen.getByText('My App'))!.toBeInTheDocument() }) it('should properly show and hide modals conditionally', async () => { @@ -537,6 +544,37 @@ describe('HeaderInMobile', () => { render() + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals + // Initially no modals // Initially no modals expect(screen.queryByRole('dialog')).not.toBeInTheDocument() expect(screen.queryByText('share.chat.deleteConversation.title')).not.toBeInTheDocument() @@ -565,7 +603,7 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText('Conv 1')) fireEvent.click(await screen.findByText(/sidebar\.action\.delete/i)) - expect(await screen.findByRole('button', { name: /common\.operation\.confirm|operation\.confirm/i })).toBeInTheDocument() + expect(await screen.findByRole('button', { name: /common\.operation\.confirm|operation\.confirm/i }))!.toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: /common\.operation\.confirm|operation\.confirm/i })) expect(handleDelete).toHaveBeenCalledWith('1', expect.any(Object)) } @@ -590,7 +628,7 @@ describe('HeaderInMobile', () => { fireEvent.click(await screen.findByText(/explore\.sidebar\.action\.rename|sidebar\.action\.rename/i)) const input = await screen.findByRole('textbox') - expect(input).toHaveValue('') + expect(input)!.toHaveValue('') fireEvent.change(input, { target: { value: 'Renamed from empty' } }) fireEvent.click(screen.getByRole('button', { name: /common\.operation\.save/i })) diff --git a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx index f4c8ef0c45..3f69cab96f 100644 --- a/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/__tests__/hooks.spec.tsx @@ -497,7 +497,7 @@ describe('useChatWithHistory', () => { expect(mockRenameConversation).toHaveBeenCalledWith(AppSourceType.webApp, 'app-1', 'conversation-1', 'New Name') expect(onSuccess).toHaveBeenCalledTimes(1) await waitFor(() => { - expect(result!.current.conversationList[0].name).toBe('New Name') + expect(result!.current.conversationList[0]!.name).toBe('New Name') }) }) @@ -615,7 +615,7 @@ describe('useChatWithHistory', () => { // Assert: new item with empty id prepended await waitFor(() => { - expect(result!.current.conversationList[0].id).toBe('') + expect(result!.current.conversationList[0]!.id).toBe('') }) }) }) @@ -1597,7 +1597,7 @@ describe('useChatWithHistory', () => { }) // Assert - expect(result!.current.conversationList[0].id).toBe('conversation-1') + expect(result!.current.conversationList[0]!.id).toBe('conversation-1') }) }) @@ -1857,7 +1857,7 @@ describe('useChatWithHistory', () => { // Assert await waitFor(() => { - expect(result!.current.conversationList[0].name).toBe('Updated Name') + expect(result!.current.conversationList[0]!.name).toBe('Updated Name') }) }) }) diff --git a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx index 5feaccd191..b1c23a129b 100644 --- a/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx +++ b/web/app/components/base/chat/chat-with-history/header/__tests__/index.spec.tsx @@ -108,7 +108,7 @@ describe('Header Component', () => { currentConversationItem: mockConv, sidebarCollapseState: true, }) - expect(screen.getByText('My Chat')).toBeInTheDocument() + expect(screen.getByText('My Chat'))!.toBeInTheDocument() }) it('should render ViewFormDropdown trigger when inputsForms are present', () => { @@ -133,7 +133,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') // Sidebar, NewChat, ResetChat (3) const resetChatBtn = buttons[buttons.length - 1] - await userEvent.click(resetChatBtn) + await userEvent.click(resetChatBtn!) expect(handleNewConversation).toHaveBeenCalled() }) @@ -144,7 +144,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') const sidebarBtn = buttons[0] - await userEvent.click(sidebarBtn) + await userEvent.click(sidebarBtn!) expect(handleSidebarCollapse).toHaveBeenCalledWith(false) }) @@ -163,7 +163,7 @@ describe('Header Component', () => { await userEvent.click(trigger) const pinBtn = await screen.findByText('explore.sidebar.action.pin') - expect(pinBtn).toBeInTheDocument() + expect(pinBtn)!.toBeInTheDocument() await userEvent.click(pinBtn) @@ -225,7 +225,7 @@ describe('Header Component', () => { const renameMenuBtn = await screen.findByText('explore.sidebar.action.rename') await userEvent.click(renameMenuBtn) - expect(await screen.findByText('common.chat.renameConversation')).toBeInTheDocument() + expect(await screen.findByText('common.chat.renameConversation'))!.toBeInTheDocument() const input = screen.getByDisplayValue('My Chat') await userEvent.clear(input) @@ -236,7 +236,7 @@ describe('Header Component', () => { expect(handleRenameConversation).toHaveBeenCalledWith('conv-1', 'New Name', expect.any(Object)) - const successCallback = handleRenameConversation.mock.calls[0][2].onSuccess + const successCallback = handleRenameConversation.mock.calls[0]![2].onSuccess await act(async () => { successCallback() }) @@ -262,14 +262,14 @@ describe('Header Component', () => { await userEvent.click(deleteMenuBtn) expect(handleDeleteConversation).not.toHaveBeenCalled() - expect(await screen.findByText('share.chat.deleteConversation.title')).toBeInTheDocument() + expect(await screen.findByText('share.chat.deleteConversation.title'))!.toBeInTheDocument() const confirmBtn = await screen.findByText('common.operation.confirm') await userEvent.click(confirmBtn) expect(handleDeleteConversation).toHaveBeenCalledWith('conv-1', expect.any(Object)) - const successCallback = handleDeleteConversation.mock.calls[0][1].onSuccess + const successCallback = handleDeleteConversation.mock.calls[0]![1].onSuccess await act(async () => { successCallback() }) @@ -311,7 +311,7 @@ describe('Header Component', () => { await userEvent.click(screen.getByText('My Chat')) await userEvent.click(await screen.findByText('explore.sidebar.action.delete')) - expect(await screen.findByText('share.chat.deleteConversation.title')).toBeInTheDocument() + expect(await screen.findByText('share.chat.deleteConversation.title'))!.toBeInTheDocument() }) }) @@ -332,7 +332,7 @@ describe('Header Component', () => { it('should render system title if conversation id is missing', () => { setup({ currentConversationId: '', sidebarCollapseState: true }) const titleEl = screen.getByText('Test App') - expect(titleEl).toHaveClass('system-md-semibold') + expect(titleEl)!.toHaveClass('system-md-semibold') }) it('should render app icon from URL when icon_url is provided', () => { @@ -347,7 +347,7 @@ describe('Header Component', () => { }, }) const img = screen.getByAltText('app icon') - expect(img).toHaveAttribute('src', 'https://example.com/icon.png') + expect(img)!.toHaveAttribute('src', 'https://example.com/icon.png') }) it('should handle undefined appData gracefully (optional chaining)', () => { @@ -364,7 +364,8 @@ describe('Header Component', () => { sidebarCollapseState: true, }) // The separator is just a div with text content '/' - expect(screen.getByText('/')).toBeInTheDocument() + // The separator is just a div with text content '/' + expect(screen.getByText('/'))!.toBeInTheDocument() }) it('should handle New Chat button state when currentConversationId is present but isResponding is true', () => { @@ -377,7 +378,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') // Sidebar, NewChat, ResetChat (3) const newChatBtn = buttons[1] - expect(newChatBtn).toBeDisabled() + expect(newChatBtn)!.toBeDisabled() }) it('should handle New Chat button state when currentConversationId is missing and isResponding is false', () => { @@ -390,7 +391,7 @@ describe('Header Component', () => { const buttons = screen.getAllByRole('button') // Sidebar, NewChat (2) const newChatBtn = buttons[1] - expect(newChatBtn).toBeDisabled() + expect(newChatBtn)!.toBeDisabled() }) it('should not render operation menu if conversation id is missing', () => { diff --git a/web/app/components/base/chat/chat-with-history/hooks.tsx b/web/app/components/base/chat/chat-with-history/hooks.tsx index e6f5657ff5..df261d750c 100644 --- a/web/app/components/base/chat/chat-with-history/hooks.tsx +++ b/web/app/components/base/chat/chat-with-history/hooks.tsx @@ -452,7 +452,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => { toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) setOriginConversationList(produce((draft) => { const index = originConversationList.findIndex(item => item.id === conversationId) - const item = draft[index] + const item = draft[index]! draft[index] = { ...item, name: newName, diff --git a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx index 89327341de..9d65246a7f 100644 --- a/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx +++ b/web/app/components/base/chat/chat/__tests__/hooks.spec.tsx @@ -102,8 +102,8 @@ describe('useChat', () => { const { result } = renderHook(() => useChat(config as ChatConfig, formSettings)) expect(result.current.chatList).toHaveLength(1) - expect(result.current.chatList[0].content).toBe('Hello Alice') - expect(result.current.chatList[0].suggestedQuestions).toEqual(['One', 'Two']) + expect(result.current.chatList[0]!.content).toBe('Hello Alice') + expect(result.current.chatList[0]!.suggestedQuestions).toEqual(['One', 'Two']) }) it('should update existing opening statement if already present in threadMessages', () => { @@ -121,7 +121,7 @@ describe('useChat', () => { const { result } = renderHook(() => useChat(config as ChatConfig, undefined, prevChatTree as ChatItemInTree[])) expect(result.current.chatList).toHaveLength(1) - expect(result.current.chatList[0].content).toBe('Hello updated') + expect(result.current.chatList[0]!.content).toBe('Hello updated') }) it('should update existing opening statement suggested questions using processed values', () => { @@ -143,8 +143,8 @@ describe('useChat', () => { const { result } = renderHook(() => useChat(config as ChatConfig, formSettings as UseChatFormSettings, prevChatTree as ChatItemInTree[])) - expect(result.current.chatList[0].content).toBe('Hello Bob') - expect(result.current.chatList[0].suggestedQuestions).toEqual(['Ask Bob']) + expect(result.current.chatList[0]!.content).toBe('Hello Bob') + expect(result.current.chatList[0]!.suggestedQuestions).toEqual(['Ask Bob']) }) describe('opening statement referential stability', () => { @@ -162,8 +162,8 @@ describe('useChat', () => { const { result } = renderHook(() => useChat(config as ChatConfig)) const openerInitial = result.current.chatList[0] - expect(openerInitial.isOpeningStatement).toBe(true) - expect(openerInitial.content).toBe('Welcome!') + expect(openerInitial!.isOpeningStatement).toBe(true) + expect(openerInitial!.content).toBe('Welcome!') act(() => { result.current.handleSend('url', { query: 'hello' }, {}) @@ -207,8 +207,8 @@ describe('useChat', () => { ) const openerBefore = result.current.chatList[0] - expect(openerBefore.content).toBe('Hello Alice') - expect(openerBefore.suggestedQuestions).toEqual(['Ask about Alice']) + expect(openerBefore!.content).toBe('Hello Alice') + expect(openerBefore!.suggestedQuestions).toEqual(['Ask about Alice']) rerender({ fs: { inputs: { name: 'Alice' }, inputsForm: [] } }) @@ -232,8 +232,8 @@ describe('useChat', () => { const after = result.current.chatList[0] expect(after).not.toBe(before) - expect(after.content).toBe('Hello Bob') - expect(after.suggestedQuestions).toEqual(['Ask Bob']) + expect(after!.content).toBe('Hello Bob') + expect(after!.suggestedQuestions).toEqual(['Ask Bob']) }) it('should keep content and suggestedQuestions stable for opener already in prevChatTree even when sibling metadata changes', () => { @@ -259,11 +259,11 @@ describe('useChat', () => { ) const openerBefore = result.current.chatList[0] - expect(openerBefore.content).toBe('Hello updated') - expect(openerBefore.suggestedQuestions).toEqual(['S1']) + expect(openerBefore!.content).toBe('Hello updated') + expect(openerBefore!.suggestedQuestions).toEqual(['S1']) - const contentBefore = openerBefore.content - const suggestionsBefore = openerBefore.suggestedQuestions + const contentBefore = openerBefore!.content + const suggestionsBefore = openerBefore!.suggestedQuestions act(() => { result.current.handleSend('url', { query: 'msg' }, {}) @@ -274,15 +274,15 @@ describe('useChat', () => { expect(result.current.chatList.length).toBeGreaterThan(1) const openerAfter = result.current.chatList[0] - expect(openerAfter.content).toBe(contentBefore) - expect(openerAfter.suggestedQuestions).toBe(suggestionsBefore) + expect(openerAfter!.content).toBe(contentBefore) + expect(openerAfter!.suggestedQuestions).toBe(suggestionsBefore) }) it('should use a stable id of "opening-statement"', () => { const { result } = renderHook(() => useChat({ opening_statement: 'Hi' } as ChatConfig), ) - expect(result.current.chatList[0].id).toBe('opening-statement') + expect(result.current.chatList[0]!.id).toBe('opening-statement') }) }) @@ -329,8 +329,8 @@ describe('useChat', () => { }) expect(result.current.isResponding).toBe(false) - expect(result.current.chatList[1].content).toBe('hi there') - expect(result.current.chatList[1].id).toBe('m-1') + expect(result.current.chatList[1]!.content).toBe('hi there') + expect(result.current.chatList[1]!.id).toBe('m-1') }) it('should handle onThought and different workflow events', async () => { @@ -387,10 +387,10 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.agent_thoughts).toHaveLength(2) - expect(lastResponse.agent_thoughts![0].thought).toContain('thinking...') - expect(lastResponse.agent_thoughts![1].thought).toContain('second thought') - expect(lastResponse.workflowProcess?.tracing).toHaveLength(3) // node, iteration, loop + expect(lastResponse!.agent_thoughts).toHaveLength(2) + expect(lastResponse!.agent_thoughts![0]!.thought).toContain('thinking...') + expect(lastResponse!.agent_thoughts![1]!.thought).toContain('second thought') + expect(lastResponse!.workflowProcess?.tracing).toHaveLength(3) // node, iteration, loop }) it('should handle human input forms, pauses, TTS, and message ends', async () => { @@ -435,11 +435,11 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.humanInputFormDataList).toHaveLength(0) // Removed when filled - expect(lastResponse.humanInputFilledFormDataList).toHaveLength(2) + expect(lastResponse!.humanInputFormDataList).toHaveLength(0) // Removed when filled + expect(lastResponse!.humanInputFilledFormDataList).toHaveLength(2) expect(sseGet).toHaveBeenCalled() // from workflowPaused - expect(lastResponse.annotation?.id).toBe('anno-1') - expect(lastResponse.content).toBe('Replaced content') + expect(lastResponse!.annotation?.id).toBe('anno-1') + expect(lastResponse!.content).toBe('Replaced content') expect(result.current.isResponding).toBe(false) // from onError }) @@ -466,8 +466,8 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.message_files).toHaveLength(1) - expect(lastResponse.agent_thoughts![0].message_files).toHaveLength(1) + expect(lastResponse!.message_files).toHaveLength(1) + expect(lastResponse!.agent_thoughts![0]!.message_files).toHaveLength(1) }) it('should fetch conversation messages and suggested questions onCompleted', async () => { @@ -524,7 +524,7 @@ describe('useChat', () => { expect(onConversationComplete).toHaveBeenCalledWith('c-1') const updatedResponse = result.current.chatList[1] - expect(updatedResponse.content).toBe('Updated answer from history') // Fetched from mock + expect(updatedResponse!.content).toBe('Updated answer from history') // Fetched from mock expect(result.current.suggestedQuestions).toEqual(['Suggested 1', 'Suggested 2']) }) @@ -590,8 +590,8 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.workflowProcess?.tracing).toHaveLength(3) // node, iter, loop - expect(lastResponse.workflowProcess?.status).toBe('failed') + expect(lastResponse!.workflowProcess?.tracing).toHaveLength(3) // node, iter, loop + expect(lastResponse!.workflowProcess?.status).toBe('failed') }) it('should handle early exits in tracing events during iteration or loop', async () => { @@ -629,7 +629,7 @@ describe('useChat', () => { callbacks.onNodeFinished({ data: { id: 'n-1', iteration_id: 'iter-1' } }) }) - const traceLen1 = result.current.chatList[result.current.chatList.length - 1].workflowProcess?.tracing?.length + const traceLen1 = result.current.chatList[result.current.chatList.length - 1]!.workflowProcess?.tracing?.length expect(traceLen1).toBe(0) // None added due to iteration early hits }) @@ -678,8 +678,8 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.workflowProcess?.tracing).toBeDefined() - expect(lastResponse.workflowProcess?.status).toBe('failed') + expect(lastResponse!.workflowProcess?.tracing).toBeDefined() + expect(lastResponse!.workflowProcess?.status).toBe('failed') }) it('should insert and then replace child QA when sending with parent_message_id', () => { @@ -713,7 +713,7 @@ describe('useChat', () => { expect(result.current.chatList.some(item => item.id === 'question-m-child')).toBe(true) expect(result.current.chatList.some(item => item.id === 'm-child')).toBe(true) - expect(result.current.chatList[result.current.chatList.length - 1].content).toBe('child answer') + expect(result.current.chatList[result.current.chatList.length - 1]!.content).toBe('child answer') }) it('should strip local file urls before sending payload', () => { @@ -746,7 +746,7 @@ describe('useChat', () => { result.current.handleSend('test-url', { query: 'file payload', files: [localFile as FileEntity, remoteFile as FileEntity] }, {}) }) - const payload = vi.mocked(ssePost).mock.calls[0][1] as { + const payload = vi.mocked(ssePost).mock.calls[0]![1] as { body: { files: Array<{ transfer_method: string @@ -776,10 +776,10 @@ describe('useChat', () => { result.current.handleSend('test-url', { query: 'first request' }, {}) }) act(() => { - callbacksList[0].getAbortController(previousWorkflowAbort) + callbacksList[0]!.getAbortController(previousWorkflowAbort) }) await act(async () => { - await callbacksList[0].onCompleted(true) + await callbacksList[0]!.onCompleted(true) }) act(() => { @@ -811,7 +811,7 @@ describe('useChat', () => { }) expect(onGetConversationMessages).toHaveBeenCalled() - expect(result.current.chatList[result.current.chatList.length - 1].content).toBe('streamed content') + expect(result.current.chatList[result.current.chatList.length - 1]!.content).toBe('streamed content') }) it('should clear suggested questions when suggestion fetch fails after completion', async () => { @@ -858,7 +858,7 @@ describe('useChat', () => { }) const latestResponse = result.current.chatList[result.current.chatList.length - 1] - expect(latestResponse.workflowProcess?.tracing).toHaveLength(0) + expect(latestResponse!.workflowProcess?.tracing).toHaveLength(0) }) it('should handle paused workflow finish, thought id binding, empty tts chunk, and human-input pause updates', () => { @@ -885,11 +885,11 @@ describe('useChat', () => { }) const latestResponse = result.current.chatList[result.current.chatList.length - 1] - expect(latestResponse.id).toBe('m-th-bind') - expect(latestResponse.conversationId).toBe('c-th-bind') - expect(latestResponse.workflowProcess?.status).toBe('succeeded') - expect(latestResponse.humanInputFormDataList?.map(item => item.node_id)).toEqual(['human-node', 'human-node-2']) - expect(latestResponse.workflowProcess?.tracing?.find(item => item.node_id === 'human-node')?.status).toBe('paused') + expect(latestResponse!.id).toBe('m-th-bind') + expect(latestResponse!.conversationId).toBe('c-th-bind') + expect(latestResponse!.workflowProcess?.status).toBe('succeeded') + expect(latestResponse!.humanInputFormDataList?.map(item => item.node_id)).toEqual(['human-node', 'human-node-2']) + expect(latestResponse!.workflowProcess?.tracing?.find(item => item.node_id === 'human-node')?.status).toBe('paused') }) }) @@ -978,13 +978,13 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[result.current.chatList.length - 1] - expect(lastResponse.agent_thoughts![0].thought).toContain('resumed') + expect(lastResponse!.agent_thoughts![0]!.thought).toContain('resumed') - expect(lastResponse.workflowProcess?.tracing?.length).toBeGreaterThan(0) - expect(lastResponse.workflowProcess?.status).toBe('paused') - expect(lastResponse.humanInputFilledFormDataList).toHaveLength(1) - expect(lastResponse.humanInputFormDataList).toHaveLength(0) - expect(lastResponse.content).toBe('replaced resume') + expect(lastResponse!.workflowProcess?.tracing?.length).toBeGreaterThan(0) + expect(lastResponse!.workflowProcess?.status).toBe('paused') + expect(lastResponse!.humanInputFilledFormDataList).toHaveLength(1) + expect(lastResponse!.humanInputFormDataList).toHaveLength(0) + expect(lastResponse!.content).toBe('replaced resume') }) it('should handle non-agent mode resume', async () => { @@ -1017,7 +1017,7 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.content).toBe('initial append') + expect(lastResponse!.content).toBe('initial append') }) it('should stop resume completion flow early when hasError is true', async () => { @@ -1084,7 +1084,7 @@ describe('useChat', () => { result.current.handleResume('m-resume', 'wr-1', { isPublicAPI: true }) }) act(() => { - callbacksList[0].getAbortController(previousWorkflowAbort) + callbacksList[0]!.getAbortController(previousWorkflowAbort) }) act(() => { result.current.handleResume('m-resume', 'wr-2', { isPublicAPI: true }) @@ -1127,7 +1127,7 @@ describe('useChat', () => { callbacks.onTTSChunk('m-guard', '') }) - expect(result.current.chatList[1].content).toBe('initial') + expect(result.current.chatList[1]!.content).toBe('initial') }) it('should clear suggested questions when resume suggestion fetch fails', async () => { @@ -1207,9 +1207,9 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.humanInputFormDataList?.map(item => item.node_id)).toEqual(['node-2']) - expect(lastResponse.humanInputFilledFormDataList?.map(item => item.node_id)).toEqual(['node-1', 'node-3']) - expect(lastResponse.workflowProcess?.tracing?.find(item => item.node_id === 'node-1')?.status).toBe('paused') + expect(lastResponse!.humanInputFormDataList?.map(item => item.node_id)).toEqual(['node-2']) + expect(lastResponse!.humanInputFilledFormDataList?.map(item => item.node_id)).toEqual(['node-1', 'node-3']) + expect(lastResponse!.workflowProcess?.tracing?.find(item => item.node_id === 'node-1')?.status).toBe('paused') }) it('should handle resume non-annotation lifecycle branches and parallel node finish', () => { @@ -1256,11 +1256,11 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.message_files).toHaveLength(1) - expect(lastResponse.conversationId).toBe('c-resume-branches') - expect(lastResponse.citation).toEqual([{ id: 'r-1' }]) - expect(lastResponse.workflowProcess?.status).toBe('succeeded') - expect(lastResponse.workflowProcess?.tracing?.some(item => item.id === 'n-parallel')).toBe(true) + expect(lastResponse!.message_files).toHaveLength(1) + expect(lastResponse!.conversationId).toBe('c-resume-branches') + expect(lastResponse!.citation).toEqual([{ id: 'r-1' }]) + expect(lastResponse!.workflowProcess?.status).toBe('succeeded') + expect(lastResponse!.workflowProcess?.tracing?.some(item => item.id === 'n-parallel')).toBe(true) }) }) @@ -1349,7 +1349,7 @@ describe('useChat', () => { }) // Simulate taskIdRef population - const callbacks = vi.mocked(ssePost).mock.calls[0][2] as HookCallbacks + const callbacks = vi.mocked(ssePost).mock.calls[0]![2] as HookCallbacks act(() => { callbacks.onWorkflowStarted({ task_id: 'task-123' }) }) @@ -1467,21 +1467,21 @@ describe('useChat', () => { act(() => { result.current.handleAnnotationEdited('edited query', 'edited answer', 1) }) - expect(result.current.chatList[0].content).toBe('edited query') - expect(result.current.chatList[1].content).toBe('edited answer') + expect(result.current.chatList[0]!.content).toBe('edited query') + expect(result.current.chatList[1]!.content).toBe('edited answer') // Added act(() => { result.current.handleAnnotationAdded('anno-1', 'admin', 'q2', 'a2', 1) }) - expect(result.current.chatList[1].annotation?.id).toBe('anno-1') - expect(result.current.chatList[1].annotation?.authorName).toBe('admin') + expect(result.current.chatList[1]!.annotation?.id).toBe('anno-1') + expect(result.current.chatList[1]!.annotation?.authorName).toBe('admin') // Removed act(() => { result.current.handleAnnotationRemoved(1) }) - expect(result.current.chatList[1].annotation?.id).toBe('') + expect(result.current.chatList[1]!.annotation?.id).toBe('') }) it('should handle switch sibling and trigger handleResume if human input', () => { @@ -1554,13 +1554,13 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.message_files).toHaveLength(3) - expect(lastResponse.message_files![0].type).toBe('video/mp4') - expect(lastResponse.message_files![0].supportFileType).toBe('video') - expect(lastResponse.message_files![1].type).toBe('audio/mpeg') - expect(lastResponse.message_files![1].supportFileType).toBe('audio') - expect(lastResponse.message_files![2].type).toBe('application/octet-stream') - expect(lastResponse.message_files![2].supportFileType).toBe('document') + expect(lastResponse!.message_files).toHaveLength(3) + expect(lastResponse!.message_files![0]!.type).toBe('video/mp4') + expect(lastResponse!.message_files![0]!.supportFileType).toBe('video') + expect(lastResponse!.message_files![1]!.type).toBe('audio/mpeg') + expect(lastResponse!.message_files![1]!.supportFileType).toBe('audio') + expect(lastResponse!.message_files![2]!.type).toBe('application/octet-stream') + expect(lastResponse!.message_files![2]!.supportFileType).toBe('document') }) it('should handle onMessageEnd empty citation and empty processed files fallbacks', () => { @@ -1580,7 +1580,7 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.citation).toEqual([]) + expect(lastResponse!.citation).toEqual([]) }) it('should handle iteration and loop tracing edge cases (lazy arrays, node finish index -1)', () => { @@ -1660,9 +1660,9 @@ describe('useChat', () => { // Ensure the tracing array exists and holds the loop item const lastResponse = result3.current.chatList[1] - expect(lastResponse.workflowProcess?.tracing).toBeDefined() - expect(lastResponse.workflowProcess?.tracing).toHaveLength(1) - expect(lastResponse.workflowProcess?.tracing![0].node_id).toBe('loop-1') + expect(lastResponse!.workflowProcess?.tracing).toBeDefined() + expect(lastResponse!.workflowProcess?.tracing).toHaveLength(1) + expect(lastResponse!.workflowProcess?.tracing![0]!.node_id).toBe('loop-1') }) it('should handle onCompleted fallback to answer when agent thought does not match and provider latency is 0', async () => { @@ -1699,9 +1699,9 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.content).toBe('final answer') - expect(lastResponse.more?.latency).toBe('0.00') - expect(lastResponse.more?.tokens_per_second).toBeUndefined() + expect(lastResponse!.content).toBe('final answer') + expect(lastResponse!.more?.latency).toBe('0.00') + expect(lastResponse!.more?.tokens_per_second).toBeUndefined() }) it('should handle onCompleted using agent thought when thought matches answer', async () => { @@ -1738,7 +1738,7 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.content).toBe('') // isUseAgentThought sets content to empty string + expect(lastResponse!.content).toBe('') // isUseAgentThought sets content to empty string }) it('should cover pausedStateRef reset on workflowFinished and missing tracing arrays in node finish / human input', () => { @@ -1781,7 +1781,7 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.workflowProcess?.status).toBe('succeeded') + expect(lastResponse!.workflowProcess?.status).toBe('succeeded') }) it('should cover onThought creating tracing and appending message correctly when isAgentMode=true', () => { @@ -1806,8 +1806,8 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[result.current.chatList.length - 1] - expect(lastResponse.agent_thoughts).toHaveLength(1) - expect(lastResponse.agent_thoughts![0].thought).toBe('initial thought appended') + expect(lastResponse!.agent_thoughts).toHaveLength(1) + expect(lastResponse!.agent_thoughts![0]!.thought).toBe('initial thought appended') }) }) @@ -1897,10 +1897,10 @@ describe('useChat', () => { }) const lastSendResponse = result.current.chatList[1] - expect(lastSendResponse.message_files).toHaveLength(2) + expect(lastSendResponse!.message_files).toHaveLength(2) const lastResumeResponse = resumeResult.current.chatList[1] - expect(lastResumeResponse.message_files).toHaveLength(1) + expect(lastResumeResponse!.message_files).toHaveLength(1) }) it('should cover parallel_id tracing matches in iteration and loop finish', () => { @@ -1931,10 +1931,10 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - const tracing = lastResponse.workflowProcess!.tracing! + const tracing = lastResponse!.workflowProcess!.tracing! expect(tracing).toHaveLength(3) - expect(tracing[0].status).toBe('succeeded') - expect(tracing[1].status).toBe('succeeded') + expect(tracing[0]!.status).toBe('succeeded') + expect(tracing[1]!.status).toBe('succeeded') }) it('should cover baseFile with ALL fields, avoiding all fallbacks', () => { @@ -1964,9 +1964,9 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[result.current.chatList.length - 1] - expect(lastResponse.message_files).toHaveLength(1) - expect(lastResponse.message_files![0].type).toBe('custom/mime') - expect(lastResponse.message_files![0].size).toBe(1024) + expect(lastResponse!.message_files).toHaveLength(1) + expect(lastResponse!.message_files![0]!.type).toBe('custom/mime') + expect(lastResponse!.message_files![0]!.size).toBe(1024) }) it('should cover handleResume missing branches for onMessageEnd, onFile fallbacks, and workflow edges', () => { @@ -2010,13 +2010,14 @@ describe('useChat', () => { resumeCallbacks.onHumanInputFormTimeout({ data: { node_id: 'timeout-id' } }) // Empty file list - result.current.chatList[1].message_files = undefined + // Empty file list + result.current.chatList[1]!.message_files = undefined // Call onFile while agent_thoughts is empty/undefined to hit the `else` fallback branch resumeCallbacks.onFile({ id: 'f-agent', type: 'image', url: 'agent.png' }) }) const lastResponse = result.current.chatList[1] - expect(lastResponse.message_files![0]).toBeDefined() + expect(lastResponse!.message_files![0]).toBeDefined() }) it('should cover edge case where node_id is missing or index is -1 in handleResume onNodeFinished and onLoopFinish', () => { @@ -2054,7 +2055,7 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.workflowProcess?.tracing).toHaveLength(0) // None were updated + expect(lastResponse!.workflowProcess?.tracing).toHaveLength(0) // None were updated }) it('should cover TTS chunks branching where audio is empty', () => { @@ -2092,7 +2093,8 @@ describe('useChat', () => { sendCallbacks.onData(' append', false, { conversationId: 'c-1' } as Record) // Empty message files fallback - result.current.chatList[1].message_files = undefined + // Empty message files fallback + result.current.chatList[1]!.message_files = undefined sendCallbacks.onFile({ id: 'f-send', type: 'image', url: 'img.png' }) // Empty message files passing to processing fallback @@ -2113,7 +2115,7 @@ describe('useChat', () => { sendCallbacks.onHumanInputFormTimeout({ data: { node_id: 'timeout' } } as Record) }) - expect(result.current.chatList[1].message_files).toBeDefined() + expect(result.current.chatList[1]!.message_files).toBeDefined() }) it('should cover handleSwitchSibling target message not found early returns', () => { @@ -2137,7 +2139,7 @@ describe('useChat', () => { act(() => { sendCallbacks.onNodeStarted({ data: { node_id: 'n-new', id: 'n-new' } }) }) - expect(result.current.chatList[1].workflowProcess).toBeUndefined() + expect(result.current.chatList[1]!.workflowProcess).toBeUndefined() }) it('should cover handleSend onNodeStarted missing tracing in workflowProcess (L969)', () => { @@ -2155,14 +2157,14 @@ describe('useChat', () => { // Get the shared reference from the tree to mutate the local closed-over responseItem's workflowProcess act(() => { const response = result.current.chatList[1] - if (response.workflowProcess) { + if (response!.workflowProcess) { // @ts-expect-error deliberately removing tracing to cover the fallback branch delete response.workflowProcess.tracing } sendCallbacks.onNodeStarted({ data: { node_id: 'n-new', id: 'n-new' } }) }) - expect(result.current.chatList[1].workflowProcess?.tracing).toBeDefined() - expect(result.current.chatList[1].workflowProcess?.tracing?.length).toBe(1) + expect(result.current.chatList[1]!.workflowProcess?.tracing).toBeDefined() + expect(result.current.chatList[1]!.workflowProcess?.tracing?.length).toBe(1) }) it('should cover handleSend onTTSChunk and onTTSEnd truthy audio strings', () => { @@ -2256,7 +2258,7 @@ describe('useChat', () => { sendCallbacks.onWorkflowStarted({ workflow_run_id: 'wr-1', task_id: 't-1' }) }) - expect(result.current.chatList[1].workflowProcess!.tracing).toHaveLength(1) + expect(result.current.chatList[1]!.workflowProcess!.tracing).toHaveLength(1) }) it('should cover handleResume onHumanInputFormFilled splicing and onHumanInputFormTimeout updating', () => { @@ -2292,8 +2294,8 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.humanInputFormDataList).toHaveLength(0) - expect(lastResponse.humanInputFilledFormDataList).toHaveLength(1) + expect(lastResponse!.humanInputFormDataList).toHaveLength(0) + expect(lastResponse!.humanInputFilledFormDataList).toHaveLength(1) }) it('should cover handleResume branches where workflowProcess exists but tracing is missing (L386, L414, L472)', () => { @@ -2333,7 +2335,7 @@ describe('useChat', () => { }) const lastResponse = result.current.chatList[1] - expect(lastResponse.workflowProcess?.tracing).toHaveLength(3) + expect(lastResponse!.workflowProcess?.tracing).toHaveLength(3) }) it('should cover handleRestart with and without callback', () => { @@ -2363,10 +2365,10 @@ describe('useChat', () => { // (annotationId, authorName, query, answer, index) result.current.handleAnnotationAdded('anno-id', 'author', 'q-new', 'a-new', 1) }) - expect(result.current.chatList[0].content).toBe('q-new') - expect(result.current.chatList[1].content).toBe('a') - expect(result.current.chatList[1].annotation?.logAnnotation?.content).toBe('a-new') - expect(result.current.chatList[1].annotation?.id).toBe('anno-id') + expect(result.current.chatList[0]!.content).toBe('q-new') + expect(result.current.chatList[1]!.content).toBe('a') + expect(result.current.chatList[1]!.annotation?.logAnnotation?.content).toBe('a-new') + expect(result.current.chatList[1]!.annotation?.id).toBe('anno-id') }) it('should cover handleAnnotationEdited updating node', async () => { @@ -2381,8 +2383,8 @@ describe('useChat', () => { // (query, answer, index) result.current.handleAnnotationEdited('q-edit', 'a-edit', 1) }) - expect(result.current.chatList[0].content).toBe('q-edit') - expect(result.current.chatList[1].content).toBe('a-edit') + expect(result.current.chatList[0]!.content).toBe('q-edit') + expect(result.current.chatList[1]!.content).toBe('a-edit') }) it('should cover handleAnnotationRemoved updating node', () => { @@ -2402,6 +2404,6 @@ describe('useChat', () => { act(() => { result.current.handleAnnotationRemoved(1) }) - expect(result.current.chatList[1].annotation?.id).toBe('') + expect(result.current.chatList[1]!.annotation?.id).toBe('') }) }) diff --git a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx index 588b261323..e3bc2dcd38 100644 --- a/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx +++ b/web/app/components/base/chat/chat/answer/__tests__/operation.spec.tsx @@ -219,8 +219,8 @@ describe('Operation', () => { it('should show copy and regenerate buttons', () => { renderOperation() - expect(screen.getByTestId('copy-btn')).toBeInTheDocument() - expect(screen.getByTestId('regenerate-btn')).toBeInTheDocument() + expect(screen.getByTestId('copy-btn'))!.toBeInTheDocument() + expect(screen.getByTestId('regenerate-btn'))!.toBeInTheDocument() }) it('should hide regenerate button when noChatInput is true', () => { @@ -231,7 +231,7 @@ describe('Operation', () => { it('should show TTS button when text_to_speech is enabled', () => { mockContextValue.config = makeChatConfig({ text_to_speech: { enabled: true } }) renderOperation() - expect(screen.getByTestId('audio-btn')).toBeInTheDocument() + expect(screen.getByTestId('audio-btn'))!.toBeInTheDocument() }) it('should show annotation button when config supports it', () => { @@ -240,12 +240,12 @@ describe('Operation', () => { annotation_reply: { id: 'ar-1', score_threshold: 0.5, embedding_model: { embedding_provider_name: '', embedding_model_name: '' }, enabled: true }, }) renderOperation() - expect(screen.getByTestId('annotation-ctrl')).toBeInTheDocument() + expect(screen.getByTestId('annotation-ctrl'))!.toBeInTheDocument() }) it('should show prompt log when showPromptLog is true', () => { renderOperation({ ...baseProps, showPromptLog: true }) - expect(screen.getByTestId('log-btn')).toBeInTheDocument() + expect(screen.getByTestId('log-btn'))!.toBeInTheDocument() }) it('should not show prompt log for opening statements', () => { @@ -311,8 +311,8 @@ describe('Operation', () => { it('should show like/dislike buttons', () => { renderOperation() const bar = screen.getByTestId('operation-bar') - expect(bar.querySelector('.i-ri-thumb-up-line')).toBeInTheDocument() - expect(bar.querySelector('.i-ri-thumb-down-line')).toBeInTheDocument() + expect(bar.querySelector('.i-ri-thumb-up-line'))!.toBeInTheDocument() + expect(bar.querySelector('.i-ri-thumb-down-line'))!.toBeInTheDocument() }) it('should call onFeedback with like on like click', async () => { @@ -328,7 +328,7 @@ describe('Operation', () => { renderOperation() const thumbDown = screen.getByTestId('operation-bar').querySelector('.i-ri-thumb-down-line')!.closest('button')! await user.click(thumbDown) - expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox'))!.toBeInTheDocument() }) it('should submit dislike feedback from modal', async () => { @@ -348,7 +348,7 @@ describe('Operation', () => { renderOperation() const thumbDown = screen.getByTestId('operation-bar').querySelector('.i-ri-thumb-down-line')!.closest('button')! await user.click(thumbDown) - expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox'))!.toBeInTheDocument() const cancelBtn = screen.getByText(/cancel/i) await user.click(cancelBtn) expect(screen.queryByRole('textbox')).not.toBeInTheDocument() @@ -405,14 +405,14 @@ describe('Operation', () => { const item = { ...baseItem, feedback: { rating: 'dislike' as const, content: 'Too slow' } } renderOperation({ ...baseProps, item }) const bar = screen.getByTestId('operation-bar') - expect(bar.querySelector('.i-ri-thumb-down-line')).toBeInTheDocument() + expect(bar.querySelector('.i-ri-thumb-down-line'))!.toBeInTheDocument() }) it('should show tooltip with only rating', () => { const item = { ...baseItem, feedback: { rating: 'like' as const } } renderOperation({ ...baseProps, item }) const bar = screen.getByTestId('operation-bar') - expect(bar.querySelector('.i-ri-thumb-up-line')).toBeInTheDocument() + expect(bar.querySelector('.i-ri-thumb-up-line'))!.toBeInTheDocument() }) it('should not show feedback bar for opening statements', () => { @@ -442,7 +442,8 @@ describe('Operation', () => { const thumbDown = screen.getByTestId('operation-bar').querySelector('.i-ri-thumb-down-line')!.closest('button')! await user.click(thumbDown) // Check if modal title/labels fallback works - expect(screen.getByRole('tooltip')).toBeInTheDocument() + // Check if modal title/labels fallback works + expect(screen.getByRole('tooltip'))!.toBeInTheDocument() mockT.mockImplementation(key => key) }) }) @@ -463,7 +464,7 @@ describe('Operation', () => { const user = userEvent.setup() renderOperation() const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-up-line') - const adminThumb = thumbs[thumbs.length - 1].closest('button')! + const adminThumb = thumbs[thumbs.length - 1]!.closest('button')! await user.click(adminThumb) expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: 'like', content: undefined }) }) @@ -472,9 +473,9 @@ describe('Operation', () => { const user = userEvent.setup() renderOperation() const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-down-line') - const adminThumb = thumbs[thumbs.length - 1].closest('button')! + const adminThumb = thumbs[thumbs.length - 1]!.closest('button')! await user.click(adminThumb) - expect(screen.getByRole('textbox')).toBeInTheDocument() + expect(screen.getByRole('textbox'))!.toBeInTheDocument() }) it('should show user feedback read-only in admin bar when user has liked', () => { @@ -488,7 +489,7 @@ describe('Operation', () => { const item = { ...baseItem, feedback: { rating: 'dislike' as const } } renderOperation({ ...baseProps, item }) const bar = screen.getByTestId('operation-bar') - expect(bar.querySelector('.bg-components-actionbar-border')).toBeInTheDocument() + expect(bar.querySelector('.bg-components-actionbar-border'))!.toBeInTheDocument() }) it('should show existing admin like feedback and allow undo', async () => { @@ -513,12 +514,12 @@ describe('Operation', () => { const user = userEvent.setup() renderOperation() const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-up-line') - const adminThumb = thumbs[thumbs.length - 1].closest('button')! + const adminThumb = thumbs[thumbs.length - 1]!.closest('button')! await user.click(adminThumb) expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: 'like', content: undefined }) const thumbsUndo = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-up-line') - const adminThumbUndo = thumbsUndo[thumbsUndo.length - 1].closest('button')! + const adminThumbUndo = thumbsUndo[thumbsUndo.length - 1]!.closest('button')! await user.click(adminThumbUndo) expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: null, content: undefined }) }) @@ -527,13 +528,13 @@ describe('Operation', () => { const user = userEvent.setup() renderOperation() const thumbs = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-down-line') - const adminThumb = thumbs[thumbs.length - 1].closest('button')! + const adminThumb = thumbs[thumbs.length - 1]!.closest('button')! await user.click(adminThumb) const submitBtn = screen.getByText(/submit/i) await user.click(submitBtn) const thumbsUndo = screen.getByTestId('operation-bar').querySelectorAll('.i-ri-thumb-down-line') - const adminThumbUndo = thumbsUndo[thumbsUndo.length - 1].closest('button')! + const adminThumbUndo = thumbsUndo[thumbsUndo.length - 1]!.closest('button')! await user.click(adminThumbUndo) expect(mockContextValue.onFeedback).toHaveBeenCalledWith('msg-1', { rating: null, content: undefined }) }) @@ -554,7 +555,9 @@ describe('Operation', () => { renderOperation({ ...baseProps, item }) // Since it renders the 'else' block for hasAdminFeedback (which is false due to !) // the like/dislike regular ActionButtons should hit the Default state - expect(screen.getByTestId('operation-bar')).toBeInTheDocument() + // Since it renders the 'else' block for hasAdminFeedback (which is false due to !) + // the like/dislike regular ActionButtons should hit the Default state + expect(screen.getByTestId('operation-bar'))!.toBeInTheDocument() }) }) @@ -587,7 +590,7 @@ describe('Operation', () => { const item = { ...baseItem, feedback: { rating: 'like' as const }, adminFeedback: { rating: 'dislike' as const } } renderOperation({ ...baseProps, item, showPromptLog: true }) const bar = screen.getByTestId('operation-bar') - expect(bar).toBeInTheDocument() + expect(bar)!.toBeInTheDocument() }) it('should show separator when user has feedback in admin mode', () => { @@ -595,7 +598,7 @@ describe('Operation', () => { const item = { ...baseItem, feedback: { rating: 'like' as const } } renderOperation({ ...baseProps, item }) const bar = screen.getByTestId('operation-bar') - expect(bar.querySelector('.bg-components-actionbar-border')).toBeInTheDocument() + expect(bar.querySelector('.bg-components-actionbar-border'))!.toBeInTheDocument() }) it('should handle missing translation fallbacks in buildFeedbackTooltip', () => { @@ -608,7 +611,7 @@ describe('Operation', () => { }) renderOperation() - expect(screen.getByTestId('operation-bar')).toBeInTheDocument() + expect(screen.getByTestId('operation-bar'))!.toBeInTheDocument() // Reset to default behavior mockT.mockImplementation(key => key) @@ -625,7 +628,7 @@ describe('Operation', () => { }) const itemLike = { ...baseItem, feedback: { rating: 'like' as const, content: 'test content' } } const { rerender } = renderOperation({ ...baseProps, item: itemLike }) - expect(screen.getByTestId('operation-bar')).toBeInTheDocument() + expect(screen.getByTestId('operation-bar'))!.toBeInTheDocument() const itemDislike = { ...baseItem, feedback: { rating: 'dislike' as const, content: 'test content' } } rerender( @@ -633,7 +636,7 @@ describe('Operation', () => { , ) - expect(screen.getByTestId('operation-bar')).toBeInTheDocument() + expect(screen.getByTestId('operation-bar'))!.toBeInTheDocument() mockT.mockImplementation(key => key) }) @@ -643,7 +646,7 @@ describe('Operation', () => { const item = { ...baseItem, feedback: { rating: null } as unknown as Record } as unknown as ChatItem renderOperation({ ...baseProps, item }) const bar = screen.getByTestId('operation-bar') - expect(bar).toBeInTheDocument() + expect(bar)!.toBeInTheDocument() }) it('should handle missing onFeedback gracefully in handleFeedback', async () => { @@ -703,7 +706,7 @@ describe('Operation', () => { renderOperation({ ...baseProps, item }) const editBtn = screen.getByTestId('annotation-edit-btn') await user.click(editBtn) - expect(screen.getByTestId('edit-reply-modal')).toBeInTheDocument() + expect(screen.getByTestId('edit-reply-modal'))!.toBeInTheDocument() }) it('should call onAnnotationEdited from edit reply modal', async () => { @@ -742,7 +745,7 @@ describe('Operation', () => { renderOperation({ ...baseProps, item }) const editBtn = screen.getByTestId('annotation-edit-btn') await user.click(editBtn) - expect(screen.getByTestId('edit-reply-modal')).toBeInTheDocument() + expect(screen.getByTestId('edit-reply-modal'))!.toBeInTheDocument() await user.click(screen.getByTestId('modal-hide')) expect(screen.queryByTestId('edit-reply-modal')).not.toBeInTheDocument() }) @@ -755,7 +758,7 @@ describe('Operation', () => { it('should show audio play button when TTS enabled', () => { renderOperation() - expect(screen.getByTestId('audio-btn')).toBeInTheDocument() + expect(screen.getByTestId('audio-btn'))!.toBeInTheDocument() }) it('should not show audio button for humanInputFormDataList', () => { diff --git a/web/app/components/base/chat/chat/answer/__tests__/suggested-questions.spec.tsx b/web/app/components/base/chat/chat/answer/__tests__/suggested-questions.spec.tsx index 16128759bd..6fe7c959cb 100644 --- a/web/app/components/base/chat/chat/answer/__tests__/suggested-questions.spec.tsx +++ b/web/app/components/base/chat/chat/answer/__tests__/suggested-questions.spec.tsx @@ -35,8 +35,8 @@ describe('SuggestedQuestions', () => { const questions = screen.getAllByTestId('suggested-question') expect(questions).toHaveLength(2) - expect(questions[0]).toHaveTextContent('What is Dify?') - expect(questions[1]).toHaveTextContent('How to use it?') + expect(questions[0])!.toHaveTextContent('What is Dify?') + expect(questions[1])!.toHaveTextContent('How to use it?') }) it('should call onSend when a question is clicked', async () => { @@ -44,7 +44,7 @@ describe('SuggestedQuestions', () => { render() const questions = screen.getAllByTestId('suggested-question') - await user.click(questions[0]) + await user.click(questions[0]!) expect(mockOnSend).toHaveBeenCalledWith('What is Dify?') }) @@ -74,10 +74,10 @@ describe('SuggestedQuestions', () => { render() const questions = screen.getAllByTestId('suggested-question') - expect(questions[0]).toHaveClass('pointer-events-none') - expect(questions[0]).toHaveClass('opacity-50') + expect(questions[0])!.toHaveClass('pointer-events-none') + expect(questions[0])!.toHaveClass('opacity-50') - await user.click(questions[0]) + await user.click(questions[0]!) expect(mockOnSend).not.toHaveBeenCalled() }) }) diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx index 89ba71b2d9..4b3f7b2445 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx @@ -46,16 +46,16 @@ describe('HumanInputForm', () => { // splitByOutputVar should yield 3 parts: "Part 1 ", "{{#$output.field1#}}", " Part 2" const contentItems = screen.getAllByTestId('mock-content-item') expect(contentItems).toHaveLength(3) - expect(contentItems[0]).toHaveTextContent('Part 1') - expect(contentItems[1]).toHaveTextContent('{{#$output.field1#}}') - expect(contentItems[2]).toHaveTextContent('Part 2') + expect(contentItems[0])!.toHaveTextContent('Part 1') + expect(contentItems[1])!.toHaveTextContent('{{#$output.field1#}}') + expect(contentItems[2])!.toHaveTextContent('Part 2') const buttons = screen.getAllByTestId('action-button') expect(buttons).toHaveLength(4) - expect(buttons[0]).toHaveTextContent('Submit') - expect(buttons[1]).toHaveTextContent('Cancel') - expect(buttons[2]).toHaveTextContent('Accent') - expect(buttons[3]).toHaveTextContent('Ghost') + expect(buttons[0])!.toHaveTextContent('Submit') + expect(buttons[1])!.toHaveTextContent('Cancel') + expect(buttons[2])!.toHaveTextContent('Accent') + expect(buttons[3])!.toHaveTextContent('Ghost') }) it('should handle input changes and submit correctly', async () => { @@ -64,7 +64,7 @@ describe('HumanInputForm', () => { render() // Update input via mock ContentItem - await user.click(screen.getAllByTestId('update-input')[0]) + await user.click(screen.getAllByTestId('update-input')[0]!) // Submit const submitButton = screen.getByRole('button', { name: 'Submit' }) @@ -91,8 +91,8 @@ describe('HumanInputForm', () => { await user.click(submitButton) - expect(submitButton).toBeDisabled() - expect(cancelButton).toBeDisabled() + expect(submitButton)!.toBeDisabled() + expect(cancelButton)!.toBeDisabled() // Finish submission await act(async () => { diff --git a/web/app/components/base/chat/chat/answer/human-input-content/content-item.tsx b/web/app/components/base/chat/chat/answer/human-input-content/content-item.tsx index 9649a92167..b958b48a17 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/content-item.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/content-item.tsx @@ -18,7 +18,7 @@ const ContentItem = ({ const extractFieldName = (str: string): string => { const outputVarRegex = /\{\{#\$output\.([^#]+)#\}\}/ const match = outputVarRegex.exec(str) - return match ? match[1] : '' + return match ? match[1]! : '' } const fieldName = useMemo(() => { @@ -43,7 +43,7 @@ const ContentItem = ({ {formInputField.type === 'paragraph' && (