From 008d8bed0d0743fb13b176b83b695078cd6e10fb Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 19 Mar 2026 19:14:22 +0200 Subject: [PATCH 01/10] test(app): updated unit tests --- docs/testing.md | 14 +- jest.config.js => jest.config.ts | 8 +- package-lock.json | 215 +++++++ package.json | 1 + setup-jest.ts | 4 +- src/app/app.component.spec.ts | 198 +++--- src/app/app.config.server.ts | 28 +- .../breadcrumb/breadcrumb.component.spec.ts | 144 +++-- .../footer/footer.component.spec.ts | 27 +- .../forbidden-page.component.spec.ts | 18 +- .../header/header.component.spec.ts | 75 ++- .../layout/layout.component.spec.ts | 35 +- .../components/layout/layout.component.ts | 6 +- .../nav-menu/nav-menu.component.spec.ts | 8 +- .../cookie-consent-banner.component.spec.ts | 14 +- .../maintenance-banner.component.spec.ts | 217 +++---- .../maintenance-banner.component.ts | 4 +- .../osf-banners/osf-banner.component.spec.ts | 21 +- .../scheduled-banner.component.spec.ts | 21 +- .../services/maintenance.service.spec.ts | 133 ++-- .../tos-consent-banner.component.spec.ts | 139 +++-- .../page-not-found.component.spec.ts | 18 +- .../request-access.component.html | 4 +- .../request-access.component.spec.ts | 129 +++- .../sidenav/sidenav.component.spec.ts | 11 +- .../topnav/topnav.component.spec.ts | 21 +- src/app/core/guards/auth.guard.spec.ts | 13 +- src/app/core/guards/is-file.guard.spec.ts | 499 +++------------ src/app/core/guards/is-project.guard.spec.ts | 19 +- src/app/core/guards/is-registry.guard.spec.ts | 19 +- .../redirect-if-logged-in.guard.spec.ts | 5 +- .../registration-moderation.guard.spec.ts | 7 +- src/app/core/guards/view-only.guard.spec.ts | 15 +- src/app/core/helpers/i18n.helper.ts | 2 +- .../interceptors/auth.interceptor.spec.ts | 26 +- .../interceptors/error.interceptor.spec.ts | 26 +- .../view-only.interceptor.spec.ts | 15 +- ...pplication.initialization.provider.spec.ts | 14 +- .../provider/environment.provider.spec.ts | 4 +- src/app/core/provider/sentry.provider.spec.ts | 3 +- src/app/core/provider/window.provider.spec.ts | 9 +- .../core/services/help-scout.service.spec.ts | 163 +++-- .../admin-institutions.component.spec.ts | 9 +- .../admin-table/admin-table.component.spec.ts | 4 +- .../filters-section.component.spec.ts | 9 +- ...uest-access-error-dialog.component.spec.ts | 6 +- .../contact-dialog.component.spec.ts | 8 +- .../send-email-dialog.component.spec.ts | 9 +- .../institutions-preprints.component.spec.ts | 9 +- .../institutions-projects.component.spec.ts | 9 +- ...stitutions-registrations.component.spec.ts | 9 +- .../institutions-summary.component.spec.ts | 4 +- .../institutions-users.component.spec.ts | 5 +- .../analytics/analytics.component.spec.ts | 6 +- .../analytics-kpi.component.spec.ts | 5 +- .../view-duplicates.component.spec.ts | 4 +- .../view-linked-projects.component.spec.ts | 4 +- .../forgot-password.component.spec.ts | 9 +- .../reset-password.component.spec.ts | 13 +- .../pages/sign-up/sign-up.component.spec.ts | 9 +- .../collections/collections.component.spec.ts | 3 + ...tion-confirmation-dialog.component.spec.ts | 7 +- .../add-to-collection.component.spec.ts | 6 +- ...collection-metadata-step.component.spec.ts | 5 +- ...roject-contributors-step.component.spec.ts | 9 +- .../project-metadata-step.component.spec.ts | 4 +- ...e-from-collection-dialog.component.spec.ts | 5 +- .../select-project-step.component.spec.ts | 5 +- .../collections-discover.component.spec.ts | 4 +- ...collections-filter-chips.component.spec.ts | 5 +- .../collections-filters.component.spec.ts | 5 +- .../collections-help-dialog.component.spec.ts | 8 +- ...collections-main-content.component.spec.ts | 4 +- ...tions-search-result-card.component.spec.ts | 5 +- ...llections-search-results.component.spec.ts | 4 +- .../create-view-link-dialog.component.spec.ts | 4 +- .../contributors.component.spec.ts | 301 +++++---- .../contributors/contributors.component.ts | 6 +- ...confirm-move-file-dialog.component.spec.ts | 8 +- .../create-folder-dialog.component.spec.ts | 141 +---- ...dit-file-metadata-dialog.component.spec.ts | 6 +- .../file-browser-info.component.spec.ts | 5 +- .../file-keywords.component.spec.ts | 5 +- .../file-metadata.component.spec.ts | 12 +- .../file-resource-metadata.component.spec.ts | 14 +- .../file-revisions.component.spec.ts | 6 +- .../files-selection-actions.component.spec.ts | 5 +- .../move-file-dialog.component.spec.ts | 8 +- .../rename-file-dialog.component.spec.ts | 6 +- .../file-detail/file-detail.component.spec.ts | 50 +- .../file-redirect.component.spec.ts | 2 + .../files-container.component.spec.ts | 3 + .../files/pages/files/files.component.spec.ts | 587 +++++++++--------- src/app/features/home/home.component.spec.ts | 6 +- .../dashboard/dashboard.component.spec.ts | 255 +++++--- .../institutions.component.spec.ts | 3 + .../institutions-list.component.spec.ts | 79 +-- .../institutions-search.component.spec.ts | 93 +-- .../institutions-search.component.ts | 3 +- .../meetings-feature-card.component.spec.ts | 14 +- .../meetings/meetings.component.spec.ts | 9 +- .../meeting-details.component.spec.ts | 268 +++++--- .../meetings-landing.component.spec.ts | 276 ++++---- .../cedar-template-form.component.spec.ts | 12 +- ...-affiliated-institutions.component.spec.ts | 9 +- ...metadata-collection-item.component.spec.ts | 9 +- .../metadata-collections.component.spec.ts | 9 +- .../metadata-contributors.component.spec.ts | 6 +- .../metadata-date-info.component.spec.ts | 5 +- .../metadata-description.component.spec.ts | 5 +- .../metadata-funding.component.spec.ts | 5 +- .../metadata-license.component.spec.ts | 5 +- ...metadata-publication-doi.component.spec.ts | 5 +- ...etadata-registration-doi.component.spec.ts | 5 +- .../metadata-registry-info.component.spec.ts | 6 +- ...ata-resource-information.component.spec.ts | 5 +- .../metadata-subjects.component.spec.ts | 5 +- .../metadata-tags.component.spec.ts | 6 +- .../metadata-title.component.spec.ts | 5 +- ...ated-institutions-dialog.component.spec.ts | 11 +- .../contributors-dialog.component.spec.ts | 17 +- .../description-dialog.component.spec.ts | 6 +- .../edit-title-dialog.component.spec.ts | 6 +- .../funding-dialog.component.spec.ts | 396 +++++------- .../license-dialog.component.spec.ts | 5 +- .../publication-doi-dialog.component.spec.ts | 6 +- ...ource-information-dialog.component.spec.ts | 6 +- .../resource-tooltip-info.component.spec.ts | 6 +- .../metadata/metadata.component.spec.ts | 4 +- .../add-metadata.component.spec.ts | 4 +- .../add-moderator-dialog.component.spec.ts | 263 ++++---- .../bulk-upload/bulk-upload.component.spec.ts | 5 +- ...n-moderation-submissions.component.spec.ts | 4 +- ...llection-submission-item.component.spec.ts | 10 +- ...tion-submission-overview.component.spec.ts | 5 +- ...lection-submissions-list.component.spec.ts | 5 +- .../invite-moderator-dialog.component.spec.ts | 12 +- .../moderators-list.component.spec.ts | 17 +- .../moderators-table.component.spec.ts | 6 +- .../my-reviewing-navigation.component.spec.ts | 10 +- .../notification-settings.component.spec.ts | 12 +- ...rint-moderation-settings.component.spec.ts | 7 +- ...int-recent-activity-list.component.spec.ts | 9 +- ...preprint-submission-item.component.spec.ts | 9 +- .../preprint-submissions.component.spec.ts | 10 +- ...t-withdrawal-submissions.component.spec.ts | 4 +- ...stry-pending-submissions.component.spec.ts | 8 +- .../registry-settings.component.spec.ts | 15 +- ...registry-submission-item.component.spec.ts | 12 +- .../registry-submission-item.component.ts | 2 +- .../registry-submissions.component.spec.ts | 4 +- .../collection-moderation.component.spec.ts | 31 +- .../my-preprint-reviewing.component.spec.ts | 10 +- .../preprint-moderation.component.spec.ts | 31 +- .../registries-moderation.component.spec.ts | 17 +- .../create-project-dialog.component.spec.ts | 149 +++-- .../my-projects/my-projects.component.spec.ts | 242 ++++++-- .../additional-info.component.spec.ts | 12 +- .../citation-section.component.spec.ts | 19 +- .../preprint-make-decision.component.spec.ts | 7 + ...preprint-withdraw-dialog.component.spec.ts | 2 +- .../status-banner.component.spec.ts | 8 +- .../file-step/file-step.component.spec.ts | 20 +- .../supplements-step.component.html | 2 +- .../supplements-step.component.spec.ts | 29 +- .../supplements-step.component.ts | 13 +- .../guards/preprints-moderator.guard.spec.ts | 5 +- .../preprint-details.component.spec.ts | 10 +- ...eprint-download-redirect.component.spec.ts | 31 +- .../preprint-download-redirect.component.ts | 4 + .../profile-information.component.spec.ts | 11 +- .../profile/profile.component.spec.ts | 4 +- .../linked-services.component.spec.ts | 9 +- .../add-component-dialog.component.spec.ts | 293 +++++---- .../citation-addon-card.component.spec.ts | 9 +- ...citation-collection-item.component.spec.ts | 9 +- .../citation-item.component.spec.ts | 6 +- .../component-card.component.spec.ts | 2 + .../delete-component-dialog.component.spec.ts | 352 ++++------- .../delete-node-link-dialog.component.spec.ts | 141 +++-- .../duplicate-dialog.component.spec.ts | 114 ++-- .../files-widget.component.spec.ts | 3 + .../fork-dialog/fork-dialog.component.spec.ts | 161 ++--- .../link-resource-dialog.component.spec.ts | 7 +- .../linked-resources.component.spec.ts | 9 +- .../overview-collections.component.spec.ts | 5 +- .../overview-components.component.spec.ts | 9 +- .../overview-parent-project.component.spec.ts | 3 +- .../overview-supplements.component.spec.ts | 5 +- .../overview-wiki.component.spec.ts | 5 +- ...roject-overview-metadata.component.spec.ts | 4 +- ...project-overview-toolbar.component.spec.ts | 5 +- .../project-recent-activity.component.spec.ts | 14 +- .../toggle-publicity-dialog.component.spec.ts | 3 + .../project-overview.component.spec.ts | 406 ++++++------ .../configure-addon.component.spec.ts | 23 +- ...account-connection-modal.component.spec.ts | 3 + ...connect-configured-addon.component.spec.ts | 10 +- .../disconnect-addon-modal.component.spec.ts | 3 + .../project-addons.component.spec.ts | 4 +- .../project/project.component.spec.ts | 4 +- .../registrations.component.spec.ts | 4 +- .../delete-project-dialog.component.spec.ts | 5 +- ...detail-setting-accordion.component.spec.ts | 5 +- ...ct-setting-notifications.component.spec.ts | 4 +- ...ngs-access-requests-card.component.spec.ts | 5 +- ...ings-project-affiliation.component.spec.ts | 14 +- ...ttings-project-form-card.component.spec.ts | 14 +- ...gs-storage-location-card.component.spec.ts | 5 +- ...ngs-view-only-links-card.component.spec.ts | 5 +- .../settings-wiki-card.component.spec.ts | 5 +- .../settings/settings.component.spec.ts | 4 +- .../project/wiki/wiki.component.spec.ts | 4 +- ...-continue-editing-dialog.component.spec.ts | 2 +- ...firm-registration-dialog.component.spec.ts | 2 +- .../drafts/drafts.component.spec.ts | 8 +- .../new-registration.component.spec.ts | 29 +- .../registry-services.component.html | 9 +- .../registry-services.component.spec.ts | 20 +- .../registry-services.component.ts | 8 +- ...select-components-dialog.component.spec.ts | 2 +- .../add-resource-dialog.component.spec.ts | 2 +- .../edit-resource-dialog.component.spec.ts | 2 +- ...stration-withdraw-dialog.component.spec.ts | 2 +- .../registry-make-decision.component.spec.ts | 2 +- .../registry/registry.component.spec.ts | 4 +- .../features/search/search.component.spec.ts | 3 + .../account-settings.component.spec.ts | 106 ++-- .../add-email/add-email.component.spec.ts | 9 +- .../affiliated-institutions.component.spec.ts | 7 +- .../cancel-deactivation.component.spec.ts | 8 +- .../change-password.component.spec.ts | 9 +- ...confirmation-sent-dialog.component.spec.ts | 8 +- .../connected-emails.component.spec.ts | 11 +- .../connected-identities.component.spec.ts | 8 +- .../deactivate-account.component.spec.ts | 198 +++--- .../deactivation-warning.component.spec.ts | 9 +- ...default-storage-location.component.spec.ts | 142 +++-- .../share-indexing.component.spec.ts | 8 +- .../two-factor-auth.component.spec.ts | 22 +- ...eloper-app-add-edit-form.component.spec.ts | 9 +- ...developer-apps-container.component.spec.ts | 10 +- .../developer-app-details.component.spec.ts | 12 +- .../developer-apps-list.component.spec.ts | 8 +- .../notifications.component.spec.ts | 244 +++++--- .../citation-preview.component.spec.ts | 7 +- .../education-form.component.spec.ts | 7 +- .../education/education.component.spec.ts | 326 ++++++---- .../employment-form.component.spec.ts | 7 +- .../employment/employment.component.spec.ts | 186 +++--- .../name-form/name-form.component.spec.ts | 8 +- .../components/name/name.component.spec.ts | 15 +- .../social-form/social-form.component.spec.ts | 3 + .../social/social.component.spec.ts | 10 +- .../profile-settings.component.spec.ts | 8 +- .../connect-addon.component.spec.ts | 9 +- .../settings-addons.component.spec.ts | 4 +- .../settings-container.component.spec.ts | 5 +- .../token-add-edit-form.component.spec.ts | 334 ++++------ .../token-created-dialog.component.spec.ts | 5 +- .../token-details.component.spec.ts | 142 +++-- .../tokens-list/tokens-list.component.spec.ts | 13 +- .../tokens/services/tokens.service.spec.ts | 305 +++++---- .../settings/tokens/tokens.component.spec.ts | 84 ++- .../privacy-policy.component.spec.ts | 3 + .../terms-of-use.component.spec.ts | 3 + .../add-project-form.component.spec.ts | 4 +- .../addon-card-list.component.spec.ts | 5 +- .../addon-card/addon-card.component.spec.ts | 7 +- ...addon-setup-account-form.component.spec.ts | 6 +- .../addon-terms/addon-terms.component.spec.ts | 5 +- .../addons-toolbar.component.spec.ts | 6 +- ...esource-type-info-dialog.component.spec.ts | 6 +- .../storage-item-selector.component.spec.ts | 9 +- ...iated-institution-select.component.spec.ts | 5 +- ...liated-institutions-view.component.spec.ts | 6 +- .../bar-chart/bar-chart.component.spec.ts | 5 +- .../component-checkbox-item.component.spec.ts | 3 + ...omponents-selection-list.component.spec.ts | 5 +- .../confirm-email.component.spec.ts | 196 +++--- .../confirm-email/confirm-email.component.ts | 3 +- ...tributors-list-shortener.component.spec.ts | 3 + .../contributors-list.component.spec.ts | 45 +- .../add-contributor-dialog.component.spec.ts | 368 +++++------ .../add-contributor-item.component.spec.ts | 5 +- ...tered-contributor-dialog.component.spec.ts | 6 +- .../contributors-table.component.spec.ts | 10 +- ...emove-contributor-dialog.component.spec.ts | 5 +- .../request-access-table.component.spec.ts | 6 +- .../copy-button/copy-button.component.spec.ts | 6 +- .../custom-paginator.component.spec.ts | 3 + .../data-resources.component.spec.ts | 6 +- .../doughnut-chart.component.spec.ts | 6 +- ...education-history-dialog.component.spec.ts | 6 +- .../education-history.component.spec.ts | 5 +- ...mployment-history-dialog.component.spec.ts | 6 +- .../employment-history.component.spec.ts | 5 +- .../file-menu/file-menu.component.spec.ts | 328 +++++----- .../file-select-destination.component.spec.ts | 3 + .../file-upload-dialog.component.spec.ts | 159 +---- .../files-tree/files-tree.component.spec.ts | 14 +- .../filter-chips.component.spec.ts | 5 +- .../form-select/form-select.component.spec.ts | 5 +- .../full-screen-loader.component.spec.ts | 2 + .../funder-awards-list.component.html | 0 .../funder-awards-list.component.scss | 0 .../funder-awards-list.component.spec.ts | 66 ++ .../funder-awards-list.component.ts | 0 .../generic-filter.component.spec.ts | 5 +- .../global-search.component.spec.ts | 4 +- .../google-file-picker.component.spec.ts | 17 +- .../components/icon/icon.component.spec.ts | 3 + .../info-icon/info-icon.component.spec.ts | 18 +- .../license-display.component.spec.ts | 5 +- .../license/license.component.spec.ts | 5 +- .../line-chart/line-chart.component.spec.ts | 6 +- .../loading-spinner.component.spec.ts | 3 + .../make-decision-dialog.component.spec.ts | 5 +- .../markdown/markdown.component.spec.ts | 3 + .../metadata-tabs.component.spec.ts | 11 +- .../my-projects-table.component.spec.ts | 4 +- .../password-input-hint.component.spec.ts | 5 +- .../pie-chart/pie-chart.component.spec.ts | 6 +- .../project-selector.component.spec.ts | 6 +- .../readonly-input.component.spec.ts | 5 +- .../recent-activity-list.component.spec.ts | 5 +- .../registration-card.component.spec.ts | 4 +- .../file-secondary-metadata.component.spec.ts | 5 +- ...print-secondary-metadata.component.spec.ts | 5 +- ...oject-secondary-metadata.component.spec.ts | 5 +- ...ation-secondary-metadata.component.spec.ts | 5 +- .../user-secondary-metadata.component.spec.ts | 5 +- .../resource-card.component.spec.ts | 4 +- .../resource-citations.component.spec.ts | 5 +- .../resource-doi.component.spec.ts | 2 + .../resource-license.component.spec.ts | 5 +- .../search-filters.component.spec.ts | 9 +- .../search-help-tutorial.component.spec.ts | 3 + .../search-input.component.spec.ts | 3 + ...search-results-container.component.spec.ts | 4 +- .../select/select.component.spec.ts | 7 +- .../socials-share-button.component.spec.ts | 4 +- .../statistic-card.component.spec.ts | 3 + .../status-badge.component.spec.ts | 3 + .../stepper/stepper.component.spec.ts | 3 + .../sub-header/sub-header.component.spec.ts | 3 + .../subjects-list.component.spec.ts | 5 +- .../subjects/subjects.component.spec.ts | 5 +- .../tags-input/tags-input.component.spec.ts | 5 +- .../tags-list/tags-list.component.spec.ts | 5 +- .../text-input/text-input.component.spec.ts | 5 +- .../components/toast/toast.component.spec.ts | 4 +- .../truncated-text.component.spec.ts | 4 +- .../view-only-link-message.component.spec.ts | 141 +---- .../view-only-link-message.component.ts | 17 +- .../view-only-table.component.spec.ts | 4 +- .../add-wiki-dialog.component.spec.ts | 30 +- .../compare-section.component.spec.ts | 200 ++---- .../edit-section.component.spec.ts | 6 +- .../rename-wiki-dialog.component.spec.ts | 4 +- .../view-section.component.spec.ts | 7 +- .../wiki-list/wiki-list.component.spec.ts | 5 +- .../wiki-syntax-help-dialog.component.spec.ts | 4 +- .../funder-awards-list.component.spec.ts | 66 -- .../activity-logs.service.spec.ts | 12 +- .../services/addons/addons.service.spec.ts | 6 +- .../shared/services/banners.service.spec.ts | 5 +- .../datacite/datacite.service.spec.ts | 360 +++++------ src/app/shared/services/files.service.spec.ts | 5 +- ...oogle-file-picker.download.service.spec.ts | 251 ++++---- .../google-file-picker.download.service.ts | 4 +- .../services/signposting.service.spec.ts | 131 ++-- .../activity-logs/activity-logs.state.spec.ts | 2 +- .../shared/stores/addons/addons.state.spec.ts | 5 +- .../stores/banners/banners.state.spec.ts | 5 +- .../mocks/custom-confirmation.service.mock.ts | 12 - src/testing/mocks/datacite.service.mock.ts | 12 - src/testing/mocks/mock-store.mock.ts | 5 - src/testing/mocks/store.mock.ts | 31 - src/testing/mocks/toast.service.mock.ts | 34 - src/testing/mocks/translate.service.mock.ts | 23 - src/testing/mocks/translation.service.mock.ts | 38 -- src/testing/osf.testing.module.ts | 64 -- src/testing/osf.testing.provider.ts | 18 +- .../dynamic-dialog-ref.mock.ts | 0 .../environment.token.mock.ts | 0 src/testing/providers/route-provider.mock.ts | 13 +- .../providers/translate.service.mock.ts | 40 ++ 388 files changed, 6905 insertions(+), 6729 deletions(-) rename jest.config.js => jest.config.ts (89%) rename src/app/shared/{ => components}/funder-awards-list/funder-awards-list.component.html (100%) rename src/app/shared/{ => components}/funder-awards-list/funder-awards-list.component.scss (100%) create mode 100644 src/app/shared/components/funder-awards-list/funder-awards-list.component.spec.ts rename src/app/shared/{ => components}/funder-awards-list/funder-awards-list.component.ts (100%) delete mode 100644 src/app/shared/funder-awards-list/funder-awards-list.component.spec.ts delete mode 100644 src/testing/mocks/custom-confirmation.service.mock.ts delete mode 100644 src/testing/mocks/datacite.service.mock.ts delete mode 100644 src/testing/mocks/mock-store.mock.ts delete mode 100644 src/testing/mocks/store.mock.ts delete mode 100644 src/testing/mocks/toast.service.mock.ts delete mode 100644 src/testing/mocks/translate.service.mock.ts delete mode 100644 src/testing/mocks/translation.service.mock.ts delete mode 100644 src/testing/osf.testing.module.ts rename src/testing/{mocks => providers}/dynamic-dialog-ref.mock.ts (100%) rename src/testing/{mocks => providers}/environment.token.mock.ts (100%) create mode 100644 src/testing/providers/translate.service.mock.ts diff --git a/docs/testing.md b/docs/testing.md index e05d231a4..84357eecd 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -45,7 +45,6 @@ ``` src/testing/ ├── osf.testing.provider.ts ← provideOSFCore(), provideOSFHttp() -├── osf.testing.module.ts ← OSFTestingModule (legacy — prefer providers) ├── providers/ ← Builder-pattern mocks for services │ ├── store-provider.mock.ts │ ├── route-provider.mock.ts @@ -68,21 +67,14 @@ src/testing/ ### `provideOSFCore()` — mandatory base provider -Every component test must include `provideOSFCore()`. It configures animations, translations, and environment tokens. +Every component test must include `provideOSFCore()`. It configures translations and environment tokens. ```typescript export function provideOSFCore() { - return [ - provideNoopAnimations(), - importProvidersFrom(TranslateModule.forRoot()), - TranslationServiceMock, - EnvironmentTokenMock, - ]; + return [provideTranslation, TranslateServiceMock, EnvironmentTokenMock]; } ``` -> **Never** import `OSFTestingModule` directly in new tests. It is retained for legacy compatibility only. Use `provideOSFCore()` instead. - --- ## 3. Test File Structure @@ -854,7 +846,7 @@ This project strictly enforces 90%+ test coverage through GitHub Actions CI. ## 18. Best Practices -1. **Always use `provideOSFCore()`** — never import `OSFTestingModule` directly in new tests. +1. **Always use `provideOSFCore()`**. 2. **Always use `provideMockStore()`** — never mock `component.actions` via `Object.defineProperty`. 3. **Always pass explicit mocks to `MockProvider`** when you need `jest.fn()` assertions. Bare `MockProvider(Service)` creates ng-mocks stubs. 4. **Check `@testing/` before creating inline mocks** — builders and factories almost certainly exist. diff --git a/jest.config.js b/jest.config.ts similarity index 89% rename from jest.config.js rename to jest.config.ts index 4d2d69fcc..1a258b4ee 100644 --- a/jest.config.js +++ b/jest.config.ts @@ -68,11 +68,5 @@ module.exports = { '/src/environments/', '/src/@types/', ], - testPathIgnorePatterns: [ - '/src/environments', - '/src/app/features/files/pages/file-detail', - '/src/app/features/project/addons/', - '/src/app/features/settings/addons/', - '/src/app/features/settings/tokens/store/', - ], + testPathIgnorePatterns: ['/src/environments'], }; diff --git a/package-lock.json b/package-lock.json index 60ea16793..3b663b420 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "tslib": "^2.3.0" }, "devDependencies": { + "@angular-builders/jest": "^21.0.3", "@angular-devkit/build-angular": "^21.2.1", "@angular-eslint/eslint-plugin": "^21.3.0", "@angular-eslint/eslint-plugin-template": "^21.3.0", @@ -321,6 +322,70 @@ "node": ">=6.0.0" } }, + "node_modules/@angular-builders/common": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-5.0.3.tgz", + "integrity": "sha512-Dro3574mu4/xqmjdA3159+TXDhgTbIJpEY/iBETSKUvHJiCgHel+R3eT105RpHN5o7NaD2rau5Zk2wuZqOk35Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "^21.0.0", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@angular-builders/common/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@angular-builders/common/node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@angular-builders/jest": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@angular-builders/jest/-/jest-21.0.3.tgz", + "integrity": "sha512-RYIsJQJkke4Dns+lYBYzn0JcmABCQKvTWkqMibi5v8dgtNS8pgPS8pE5x8DSmgraqJikL3ukqaUQmdeL6r38aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-builders/common": "5.0.3", + "@angular-devkit/architect": ">=0.2100.0 < 0.2200.0", + "@angular-devkit/core": "^21.0.0", + "jest-preset-angular": "^16.0.0", + "lodash": "^4.17.15" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular-devkit/build-angular": "^21.0.0", + "@angular/compiler-cli": "^21.0.0", + "@angular/core": "^21.0.0", + "@angular/platform-browser-dynamic": "^21.0.0", + "jest": "^30.0.0" + } + }, "node_modules/@angular-devkit/architect": { "version": "0.2102.2", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.2.tgz", @@ -3873,6 +3938,30 @@ } } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -8678,6 +8767,34 @@ "tinyglobby": "^0.2.14" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -9921,6 +10038,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", @@ -10265,6 +10395,13 @@ "node": ">=8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -12365,6 +12502,13 @@ "node": ">=0.10.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -26120,6 +26264,60 @@ "code-block-writer": "^13.0.3" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -26767,6 +26965,13 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -28411,6 +28616,16 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 7c7b38677..a7b689005 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "tslib": "^2.3.0" }, "devDependencies": { + "@angular-builders/jest": "^21.0.3", "@angular-devkit/build-angular": "^21.2.1", "@angular-eslint/eslint-plugin": "^21.3.0", "@angular-eslint/eslint-plugin-template": "^21.3.0", diff --git a/setup-jest.ts b/setup-jest.ts index 9fbefed7c..d8e007667 100644 --- a/setup-jest.ts +++ b/setup-jest.ts @@ -1,6 +1,6 @@ -import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; +import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; -setupZoneTestEnv(); +setupZonelessTestEnv(); // Global mocks for jsdom const mock = () => { diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a9ab876ec..f0d2e690b 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -1,106 +1,158 @@ -import { provideStore, Store } from '@ngxs/store'; +import { Store } from '@ngxs/store'; import { MockComponents, MockProvider } from 'ng-mocks'; -import { Subject } from 'rxjs'; - +import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { NavigationEnd, Router } from '@angular/router'; +import { NavigationEnd, NavigationStart, Router } from '@angular/router'; -import { CookieConsentBannerComponent } from '@core/components/osf-banners/cookie-consent-banner/cookie-consent-banner.component'; import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { GetCurrentUser, UserState } from '@core/store/user'; -import { UserEmailsState } from '@core/store/user-emails'; - -import { TranslateServiceMock } from '../testing/mocks/translate.service.mock'; +import { GetCurrentUser } from '@core/store/user'; +import { GetEmails, UserEmailsSelectors } from '@core/store/user-emails'; +import { AccountEmailModel } from '@osf/shared/models/emails/account-email.model'; +import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { ConfirmEmailComponent } from './shared/components/confirm-email/confirm-email.component'; import { FullScreenLoaderComponent } from './shared/components/full-screen-loader/full-screen-loader.component'; import { ToastComponent } from './shared/components/toast/toast.component'; -import { CustomDialogService } from './shared/services/custom-dialog.service'; import { AppComponent } from './app.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; +import { LoaderServiceMock, provideLoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { BaseSetupOverrides, mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock'; import { GoogleTagManagerService } from 'angular-google-tag-manager'; -describe('Component: App', () => { - let routerEvents$: Subject; - let gtmServiceMock: jest.Mocked; +describe('AppComponent', () => { let fixture: ComponentFixture; - let mockCustomDialogService: ReturnType; - - beforeEach(async () => { - mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); - routerEvents$ = new Subject(); - - gtmServiceMock = { - pushTag: jest.fn(), - } as any; - - await TestBed.configureTestingModule({ - imports: [ - OSFTestingModule, - AppComponent, - ...MockComponents(ToastComponent, FullScreenLoaderComponent, CookieConsentBannerComponent), - ], + let component: AppComponent; + let store: Store; + let routerBuilder: RouterMockBuilder; + let routerMock: RouterMockType; + let loaderServiceMock: LoaderServiceMock; + let customDialogServiceMock: ReturnType; + let gtmServiceMock: { pushTag: jest.Mock }; + + const unverifiedEmail: AccountEmailModel = { + id: 'email-1', + emailAddress: 'test@example.com', + confirmed: false, + verified: false, + primary: false, + isMerge: false, + }; + + interface SetupOverrides extends BaseSetupOverrides { + isBrowser?: boolean; + unverifiedEmails?: AccountEmailModel[]; + googleTagManagerId?: string; + } + + function setup(overrides: SetupOverrides = {}) { + routerBuilder = RouterMockBuilder.create().withUrl('/home'); + routerMock = routerBuilder.build(); + loaderServiceMock = new LoaderServiceMock(); + customDialogServiceMock = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); + gtmServiceMock = { pushTag: jest.fn() }; + + TestBed.configureTestingModule({ + imports: [AppComponent, ...MockComponents(ToastComponent, FullScreenLoaderComponent)], providers: [ - provideStore([UserState, UserEmailsState]), - MockProvider(CustomDialogService, mockCustomDialogService), - TranslateServiceMock, - { provide: GoogleTagManagerService, useValue: gtmServiceMock }, - { - provide: Router, - useValue: { - events: routerEvents$.asObservable(), - }, - }, + provideOSFCore(), + provideLoaderServiceMock(loaderServiceMock), + MockProvider(Router, routerMock), + MockProvider(CustomDialogService, customDialogServiceMock), + MockProvider(GoogleTagManagerService, gtmServiceMock), + MockProvider(PLATFORM_ID, overrides.isBrowser === false ? 'server' : 'browser'), + provideMockStore({ + signals: mergeSignalOverrides( + [ + { + selector: UserEmailsSelectors.getUnverifiedEmails, + value: overrides.unverifiedEmails ?? [], + }, + ], + overrides.selectorOverrides + ), + }), ], - }).compileComponents(); + }); + + if (overrides.googleTagManagerId !== undefined) { + const environment = TestBed.inject(ENVIRONMENT); + environment.googleTagManagerId = overrides.googleTagManagerId; + } + store = TestBed.inject(Store); fixture = TestBed.createComponent(AppComponent); + component = fixture.componentInstance; + + fixture.detectChanges(); + } + + it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - describe('detect changes', () => { - beforeEach(() => { - fixture.detectChanges(); - }); + it('should dispatch current user and emails on init', () => { + setup(); + expect(store.dispatch).toHaveBeenCalledWith(new GetCurrentUser()); + expect(store.dispatch).toHaveBeenCalledWith(new GetEmails()); + }); - it('should dispatch GetCurrentUser action on initialization', () => { - const store = TestBed.inject(Store); - const dispatchSpy = jest.spyOn(store, 'dispatch'); - store.dispatch(GetCurrentUser); - expect(dispatchSpy).toHaveBeenCalledWith(GetCurrentUser); - }); + it('should open confirm email dialog when unverified emails exist', () => { + setup({ unverifiedEmails: [unverifiedEmail] }); + expect(customDialogServiceMock.open).toHaveBeenCalledWith( + ConfirmEmailComponent, + expect.objectContaining({ + header: 'home.confirmEmail.add.title', + width: '448px', + data: [unverifiedEmail], + }) + ); + }); - it('should render router outlet', () => { - const routerOutlet = fixture.debugElement.query(By.css('router-outlet')); - expect(routerOutlet).toBeTruthy(); - }); + it('should show loader on navigation start in browser', () => { + setup(); + routerBuilder.emit(new NavigationStart(1, '/project/1')); + fixture.detectChanges(); + expect(loaderServiceMock.show).toHaveBeenCalled(); }); - describe('Google Tag Manager', () => { - it('should push GTM tag on NavigationEnd with google tag id', () => { - fixture.detectChanges(); - const event = new NavigationEnd(1, '/previous', '/current'); + it('should hide loader after navigation end delay in browser', () => { + jest.useFakeTimers(); + setup(); - routerEvents$.next(event); + routerBuilder.emit(new NavigationEnd(2, '/a', '/a')); - expect(gtmServiceMock.pushTag).toHaveBeenCalledWith({ - event: 'page', - pageName: '/current', - }); - }); + expect(loaderServiceMock.hide).not.toHaveBeenCalled(); - it('should not push GTM tag on NavigationEnd without google tag id', () => { - const environment = TestBed.inject(ENVIRONMENT); - environment.googleTagManagerId = ''; - fixture.detectChanges(); - const event = new NavigationEnd(1, '/previous', '/current'); + jest.advanceTimersByTime(500); + fixture.detectChanges(); - routerEvents$.next(event); + expect(loaderServiceMock.hide).toHaveBeenCalled(); + jest.useRealTimers(); + }); - expect(gtmServiceMock.pushTag).not.toHaveBeenCalled(); + it('should push GTM page event on navigation end when id exists', () => { + setup({ googleTagManagerId: 'GTM-TEST' }); + routerBuilder.emit(new NavigationEnd(3, '/preprints', '/preprints/osf/1')); + fixture.detectChanges(); + expect(gtmServiceMock.pushTag).toHaveBeenCalledWith({ + event: 'page', + pageName: '/preprints/osf/1', }); }); + + it('should not subscribe to router events on server', () => { + setup({ isBrowser: false }); + routerBuilder.emit(new NavigationStart(4, '/x')); + routerBuilder.emit(new NavigationEnd(5, '/x', '/x')); + fixture.detectChanges(); + expect(loaderServiceMock.show).not.toHaveBeenCalled(); + expect(loaderServiceMock.hide).not.toHaveBeenCalled(); + expect(gtmServiceMock.pushTag).not.toHaveBeenCalled(); + }); }); diff --git a/src/app/app.config.server.ts b/src/app/app.config.server.ts index 19ad8e705..fe0a21ea7 100644 --- a/src/app/app.config.server.ts +++ b/src/app/app.config.server.ts @@ -1,3 +1,7 @@ +import { provideTranslateLoader, TranslateLoader } from '@ngx-translate/core'; + +import { Observable, of } from 'rxjs'; + import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; import { provideServerRendering, withRoutes } from '@angular/ssr'; @@ -11,6 +15,24 @@ import { existsSync, readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; +class SsrFsTranslateLoader implements TranslateLoader { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getTranslation(lang: string): Observable { + const serverDistFolder = dirname(fileURLToPath(import.meta.url)); + const translationPath = resolve(serverDistFolder, `../browser/assets/i18n/${lang}.json`); + + if (!existsSync(translationPath)) { + return of({}); + } + + try { + return of(JSON.parse(readFileSync(translationPath, 'utf-8'))); + } catch { + return of({}); + } + } +} + function loadSsrConfig(): ConfigModel { const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const configPath = resolve(serverDistFolder, '../browser/assets/config/config.json'); @@ -32,7 +54,11 @@ function loadSsrConfig(): ConfigModel { } const serverConfig: ApplicationConfig = { - providers: [provideServerRendering(withRoutes(serverRoutes)), { provide: SSR_CONFIG, useFactory: loadSsrConfig }], + providers: [ + provideServerRendering(withRoutes(serverRoutes)), + provideTranslateLoader(SsrFsTranslateLoader), + { provide: SSR_CONFIG, useFactory: loadSsrConfig }, + ], }; export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts b/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts index 9d225834d..60f4e0a89 100644 --- a/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts +++ b/src/app/core/components/breadcrumb/breadcrumb.component.spec.ts @@ -1,76 +1,136 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; - -import { of } from 'rxjs'; +import { MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { ActivatedRoute, ActivatedRouteSnapshot, Router, UrlSegment } from '@angular/router'; import { ProviderSelectors } from '@core/store/provider'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; -import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { InstitutionsSearchSelectors } from '@shared/stores/institutions-search'; import { BreadcrumbComponent } from './breadcrumb.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; -describe('Component: Breadcrumb', () => { +describe('BreadcrumbComponent', () => { let component: BreadcrumbComponent; let fixture: ComponentFixture; + let routerBuilder: RouterMockBuilder; + + const createSnapshot = ( + paths: string[], + params: Record = {}, + firstChild?: ActivatedRouteSnapshot + ): ActivatedRouteSnapshot => + ({ + url: paths.map((path) => new UrlSegment(path, {})), + params, + firstChild: firstChild ?? null, + }) as unknown as ActivatedRouteSnapshot; + + function setup(overrides?: { + providerName?: string | null; + institutionName?: string | null; + institutionDashboardName?: string | null; + routeSnapshot?: ActivatedRouteSnapshot; + leafData?: Record; + }) { + const leafData = overrides?.leafData ?? { skipBreadcrumbs: false }; + const activatedRouteChain = ActivatedRouteMockBuilder.create() + .withData({}) + .withFirstChild((child) => { + child.withData(leafData); + }); + const activatedRouteBuilt = activatedRouteChain.build(); - const mockRouter = { - url: '/test/path', - events: of(new NavigationEnd(1, '/test/path', '/test/path')), - }; - - const mockActivatedRoute = { - root: { - snapshot: { - url: [], - params: {}, - data: {}, - firstChild: null, - }, - }, - snapshot: { - data: { skipBreadcrumbs: false }, - url: [], - params: {}, - }, - firstChild: null, - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [BreadcrumbComponent, MockComponent(IconComponent)], + const defaultSnapshot = createSnapshot( + ['preprints'], + {}, + createSnapshot(['osf'], { providerId: 'osf' }, createSnapshot(['new-registration'])) + ); + + const routeRootSnapshot = overrides?.routeSnapshot ?? defaultSnapshot; + const activatedRouteMock = { + ...activatedRouteBuilt, + root: { snapshot: routeRootSnapshot } as ActivatedRoute, + firstChild: activatedRouteBuilt.firstChild, + snapshot: activatedRouteBuilt.snapshot, + } as unknown as ActivatedRoute; + routerBuilder = RouterMockBuilder.create().withUrl('/test'); + + TestBed.configureTestingModule({ + imports: [BreadcrumbComponent], providers: [ - MockProvider(Router, mockRouter), - { provide: ActivatedRoute, useValue: mockActivatedRoute }, + provideOSFCore(), + MockProvider(ActivatedRoute, activatedRouteMock), + MockProvider(Router, routerBuilder.build()), provideMockStore({ signals: [ - { selector: ProviderSelectors.getCurrentProvider, value: null }, - { selector: InstitutionsSearchSelectors.getInstitution, value: null }, - { selector: InstitutionsAdminSelectors.getInstitution, value: null }, + { + selector: ProviderSelectors.getCurrentProvider, + value: overrides?.providerName ? { name: overrides.providerName } : null, + }, + { + selector: InstitutionsSearchSelectors.getInstitution, + value: overrides?.institutionName ? { name: overrides.institutionName } : null, + }, + { + selector: InstitutionsAdminSelectors.getInstitution, + value: overrides?.institutionDashboardName ? { name: overrides.institutionDashboardName } : null, + }, ], }), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(BreadcrumbComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); + } it('should create', () => { + setup(); expect(component).toBeTruthy(); }); - it('should show breadcrumb when skipBreadcrumbs is false', () => { + it('should show breadcrumb by default when skipBreadcrumbs is not true', () => { + setup({ leafData: { skipBreadcrumbs: false } }); expect(component.showBreadcrumb()).toBe(true); }); - it('should build breadcrumbs from route', () => { - expect(component.breadcrumbs()).toBeDefined(); - expect(Array.isArray(component.breadcrumbs())).toBe(true); + it('should hide breadcrumb when route data has skipBreadcrumbs true', () => { + setup({ leafData: { skipBreadcrumbs: true } }); + expect(component.showBreadcrumb()).toBe(false); + }); + + it('should replace provider id segment with provider name', () => { + setup({ providerName: 'OSF Preprints' }); + expect(component.breadcrumbs()).toEqual(['preprints', 'OSF Preprints', 'new registration']); + }); + + it('should replace institution id segment with institution name', () => { + const institutionSnapshot = createSnapshot( + ['institutions'], + {}, + createSnapshot(['inst-1'], { institutionId: 'inst-1' }, createSnapshot(['users'])) + ); + setup({ institutionName: 'My Institution', routeSnapshot: institutionSnapshot }); + expect(component.breadcrumbs()).toEqual(['institutions', 'My Institution', 'users']); + }); + + it('should fallback to institution dashboard name when institution selector is empty', () => { + const institutionSnapshot = createSnapshot( + ['institutions'], + {}, + createSnapshot(['inst-1'], { institutionId: 'inst-1' }, createSnapshot(['dashboard'])) + ); + setup({ + institutionName: null, + institutionDashboardName: 'Dashboard Institution', + routeSnapshot: institutionSnapshot, + }); + expect(component.breadcrumbs()).toEqual(['institutions', 'Dashboard Institution', 'dashboard']); }); }); diff --git a/src/app/core/components/footer/footer.component.spec.ts b/src/app/core/components/footer/footer.component.spec.ts index 2c20957ca..2a6396c93 100644 --- a/src/app/core/components/footer/footer.component.spec.ts +++ b/src/app/core/components/footer/footer.component.spec.ts @@ -1,22 +1,29 @@ -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, provideRouter } from '@angular/router'; +import { SOCIAL_ICONS } from '@core/constants/social-icons.constant'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { FooterComponent } from './footer.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; + describe('FooterComponent', () => { let component: FooterComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FooterComponent, MockComponent(IconComponent), MockPipe(TranslatePipe)], - providers: [MockProvider(TranslateService), MockProvider(ActivatedRoute)], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FooterComponent, MockComponent(IconComponent)], + providers: [ + provideOSFCore(), + provideRouter([]), + MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), + ], + }); fixture = TestBed.createComponent(FooterComponent); component = fixture.componentInstance; @@ -26,4 +33,8 @@ describe('FooterComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should expose social icons from constants', () => { + expect(component.socialIcons).toEqual(SOCIAL_ICONS); + }); }); diff --git a/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts b/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts index 7980e7835..965574f25 100644 --- a/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts +++ b/src/app/core/components/forbidden-page/forbidden-page.component.spec.ts @@ -1,18 +1,18 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ForbiddenPageComponent } from './forbidden-page.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ForbiddenPageComponent', () => { let component: ForbiddenPageComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ForbiddenPageComponent, MockPipe(TranslatePipe)], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ForbiddenPageComponent], + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(ForbiddenPageComponent); component = fixture.componentInstance; @@ -22,4 +22,8 @@ describe('ForbiddenPageComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should expose support email from environment', () => { + expect(component.supportEmail).toBe('support@test.com'); + }); }); diff --git a/src/app/core/components/header/header.component.spec.ts b/src/app/core/components/header/header.component.spec.ts index 8d78b7a39..9d5256fec 100644 --- a/src/app/core/components/header/header.component.spec.ts +++ b/src/app/core/components/header/header.component.spec.ts @@ -1,36 +1,45 @@ -import { Store } from '@ngxs/store'; +import { MockComponent, MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { AuthService } from '@core/services/auth.service'; import { UserSelectors } from '@osf/core/store/user'; +import { UserModel } from '@osf/shared/models/user/user.model'; import { BreadcrumbComponent } from '../breadcrumb/breadcrumb.component'; import { HeaderComponent } from './header.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('HeaderComponent', () => { let component: HeaderComponent; let fixture: ComponentFixture; + let routerMock: RouterMockType; + let authServiceMock: { logout: jest.Mock; navigateToSignIn: jest.Mock }; - beforeEach(async () => { - MOCK_STORE.selectSignal.mockImplementation((selector) => { - if (selector === UserSelectors.getCurrentUser) return () => signal(MOCK_USER); - return () => null; - }); + beforeEach(() => { + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); + authServiceMock = { + logout: jest.fn(), + navigateToSignIn: jest.fn(), + }; - await TestBed.configureTestingModule({ - imports: [HeaderComponent, MockComponent(BreadcrumbComponent), MockPipe(TranslatePipe)], - providers: [MockProvider(Store, MOCK_STORE), provideHttpClient(), provideHttpClientTesting()], - }).compileComponents(); + TestBed.configureTestingModule({ + imports: [HeaderComponent, MockComponent(BreadcrumbComponent)], + providers: [ + provideOSFCore(), + MockProvider(Router, routerMock), + MockProvider(AuthService, authServiceMock), + provideMockStore({ + signals: [{ selector: UserSelectors.getCurrentUser, value: MOCK_USER as UserModel }], + }), + ], + }); fixture = TestBed.createComponent(HeaderComponent); component = fixture.componentInstance; @@ -40,4 +49,36 @@ describe('HeaderComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should expose current user from selector', () => { + expect(component.currentUser()).toEqual(MOCK_USER); + }); + + it('should include expected menu items', () => { + expect(component.items.map((item) => item.label)).toEqual([ + 'navigation.myProfile', + 'navigation.settings', + 'navigation.logOut', + ]); + }); + + it('should navigate to profile when profile command is executed', () => { + component.items[0].command?.(); + expect(routerMock.navigate).toHaveBeenCalledWith(['profile']); + }); + + it('should navigate to settings when settings command is executed', () => { + component.items[1].command?.(); + expect(routerMock.navigate).toHaveBeenCalledWith(['settings']); + }); + + it('should logout when logout command is executed', () => { + component.items[2].command?.(); + expect(authServiceMock.logout).toHaveBeenCalled(); + }); + + it('should delegate sign in navigation to auth service', () => { + component.navigateToSignIn(); + expect(authServiceMock.navigateToSignIn).toHaveBeenCalled(); + }); }); diff --git a/src/app/core/components/layout/layout.component.spec.ts b/src/app/core/components/layout/layout.component.spec.ts index 699b8564d..6182b83cd 100644 --- a/src/app/core/components/layout/layout.component.spec.ts +++ b/src/app/core/components/layout/layout.component.spec.ts @@ -7,7 +7,7 @@ import { BehaviorSubject } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; +import { IS_MEDIUM, IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; import { BreadcrumbComponent } from '../breadcrumb/breadcrumb.component'; import { FooterComponent } from '../footer/footer.component'; @@ -18,32 +18,38 @@ import { TopnavComponent } from '../topnav/topnav.component'; import { LayoutComponent } from './layout.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; -describe('Component: Root', () => { +describe('LayoutComponent', () => { let component: LayoutComponent; let fixture: ComponentFixture; let isWebSubject: BehaviorSubject; + let isMediumSubject: BehaviorSubject; - beforeEach(async () => { + beforeEach(() => { isWebSubject = new BehaviorSubject(true); + isMediumSubject = new BehaviorSubject(false); - await TestBed.configureTestingModule({ + TestBed.configureTestingModule({ imports: [ LayoutComponent, - OSFTestingModule, ...MockComponents( - HeaderComponent, - FooterComponent, - TopnavComponent, ConfirmDialog, BreadcrumbComponent, + FooterComponent, + HeaderComponent, + OSFBannerComponent, SidenavComponent, - OSFBannerComponent + TopnavComponent ), ], - providers: [MockProvider(IS_WEB, isWebSubject), MockProvider(ConfirmationService)], - }).compileComponents(); + providers: [ + provideOSFCore(), + MockProvider(IS_WEB, isWebSubject), + MockProvider(IS_MEDIUM, isMediumSubject), + MockProvider(ConfirmationService), + ], + }); fixture = TestBed.createComponent(LayoutComponent); component = fixture.componentInstance; @@ -75,9 +81,4 @@ describe('Component: Root', () => { expect(desktopLayout).toBeFalsy(); expect(tabletLayout).toBeTruthy(); }); - - it('should contain confirm dialog component', () => { - const confirmDialog = fixture.nativeElement.querySelector('p-confirm-dialog'); - expect(confirmDialog).toBeTruthy(); - }); }); diff --git a/src/app/core/components/layout/layout.component.ts b/src/app/core/components/layout/layout.component.ts index 5a7db12c9..63601d382 100644 --- a/src/app/core/components/layout/layout.component.ts +++ b/src/app/core/components/layout/layout.component.ts @@ -19,15 +19,15 @@ import { TopnavComponent } from '../topnav/topnav.component'; @Component({ selector: 'osf-layout', imports: [ - BreadcrumbComponent, ConfirmDialog, + BreadcrumbComponent, FooterComponent, HeaderComponent, OSFBannerComponent, - RouterOutlet, - ScrollTopOnRouteChangeDirective, SidenavComponent, TopnavComponent, + RouterOutlet, + ScrollTopOnRouteChangeDirective, TranslatePipe, ], templateUrl: './layout.component.html', diff --git a/src/app/core/components/nav-menu/nav-menu.component.spec.ts b/src/app/core/components/nav-menu/nav-menu.component.spec.ts index 5cac85132..1336d06ec 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.spec.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.spec.ts @@ -1,6 +1,6 @@ import { MockComponent, MockProvider } from 'ng-mocks'; -import { NO_ERRORS_SCHEMA, signal } from '@angular/core'; +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; @@ -14,7 +14,7 @@ import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { NavMenuComponent } from './nav-menu.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -35,8 +35,9 @@ describe('NavMenuComponent', () => { }; await TestBed.configureTestingModule({ - imports: [NavMenuComponent, OSFTestingModule, MockComponent(IconComponent)], + imports: [NavMenuComponent, MockComponent(IconComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: UserSelectors.isAuthenticated, value: signal(false) }, @@ -61,7 +62,6 @@ describe('NavMenuComponent', () => { }, MockProvider(AuthService, mockAuthService), ], - schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(NavMenuComponent); diff --git a/src/app/core/components/osf-banners/cookie-consent-banner/cookie-consent-banner.component.spec.ts b/src/app/core/components/osf-banners/cookie-consent-banner/cookie-consent-banner.component.spec.ts index e6eefb1a9..20572acdf 100644 --- a/src/app/core/components/osf-banners/cookie-consent-banner/cookie-consent-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/cookie-consent-banner/cookie-consent-banner.component.spec.ts @@ -1,5 +1,5 @@ import { CookieService } from 'ngx-cookie-service'; -import { MockComponent } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -7,7 +7,7 @@ import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { CookieConsentBannerComponent } from './cookie-consent-banner.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('Component: Cookie Consent Banner', () => { let fixture: ComponentFixture; @@ -18,13 +18,11 @@ describe('Component: Cookie Consent Banner', () => { set: jest.fn(), }; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CookieConsentBannerComponent, OSFTestingModule, MockComponent(IconComponent)], - providers: [{ provide: CookieService, useValue: cookieServiceMock }], + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CookieConsentBannerComponent, MockComponent(IconComponent)], + providers: [provideOSFCore(), MockProvider(CookieService, cookieServiceMock)], }); - - jest.clearAllMocks(); }); it('should show the banner if cookie is not set', () => { diff --git a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts index 80d8d59b1..e863a6f53 100644 --- a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.spec.ts @@ -1,151 +1,110 @@ import { CookieService } from 'ngx-cookie-service'; +import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { HttpClient } from '@angular/common/http'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { PLATFORM_ID } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { MaintenanceModel } from '../models/maintenance.model'; +import { MaintenanceService } from '../services/maintenance.service'; + import { MaintenanceBannerComponent } from './maintenance-banner.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; -describe('Component: Maintenance Banner', () => { +describe('MaintenanceBannerComponent', () => { let fixture: ComponentFixture; - let httpClient: { get: jest.Mock }; - let cookieService: jest.Mocked; - - beforeEach(async () => { - cookieService = { - check: jest.fn(), + let component: MaintenanceBannerComponent; + let maintenanceServiceMock: { fetchMaintenanceStatus: jest.Mock }; + let cookieServiceMock: { check: jest.Mock; set: jest.Mock }; + + const activeMaintenance: MaintenanceModel = { + level: 2, + severity: 'warn', + message: 'Scheduled maintenance', + start: '2026-03-17T10:00:00.000Z', + end: '2026-03-17T12:00:00.000Z', + }; + + function setup(overrides?: { + isBrowser?: boolean; + cookieDismissed?: boolean; + maintenance?: MaintenanceModel | null; + }) { + maintenanceServiceMock = { + fetchMaintenanceStatus: jest.fn().mockReturnValue(of(overrides?.maintenance ?? activeMaintenance)), + }; + cookieServiceMock = { + check: jest.fn().mockReturnValue(overrides?.cookieDismissed ?? false), set: jest.fn(), - } as any; + }; - httpClient = { get: jest.fn() } as any; - - await TestBed.configureTestingModule({ - imports: [MaintenanceBannerComponent, OSFTestingModule], + TestBed.configureTestingModule({ + imports: [MaintenanceBannerComponent], providers: [ - { provide: CookieService, useValue: cookieService }, - { provide: HttpClient, useValue: httpClient }, + provideOSFCore(), + MockProvider(MaintenanceService, maintenanceServiceMock), + MockProvider(CookieService, cookieServiceMock), + MockProvider(PLATFORM_ID, overrides?.isBrowser === false ? 'server' : 'browser'), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(MaintenanceBannerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + } + + it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - afterEach(() => { - jest.clearAllMocks(); + it('should check dismissal cookie and skip fetch when dismissed in browser', () => { + setup({ cookieDismissed: true }); + expect(cookieServiceMock.check).toHaveBeenCalledWith('osf-maintenance-dismissed'); + expect(component.dismissed()).toBe(true); + expect(maintenanceServiceMock.fetchMaintenanceStatus).not.toHaveBeenCalled(); }); - it('should render info banner when maintenance data is present', fakeAsync(() => { - cookieService.check.mockReturnValue(false); - const now = new Date(); - const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); - const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); - httpClient.get.mockReturnValueOnce( - of({ - maintenance: { level: 1, message: 'Info message', start, end }, - }) - ); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - const banner = fixture.debugElement.query(By.css('p-message')); - expect(banner).toBeTruthy(); - expect(banner.componentInstance.severity).toBe('info'); - expect(banner.nativeElement.textContent).toContain('Info message'); - })); - - it('should render warning banner when level is 2', fakeAsync(() => { - cookieService.check.mockReturnValue(false); - const now = new Date(); - const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); - const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); - httpClient.get.mockReturnValueOnce( - of({ - maintenance: { - level: 2, - message: 'Warning message', - start, - end, - }, - }) - ); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - const banner = fixture.debugElement.query(By.css('p-message')); - expect(banner).toBeTruthy(); - expect(banner.componentInstance.severity).toBe('warn'); - expect(banner.nativeElement.textContent).toContain('Warning message'); - })); - - it('should render danger banner when level is 3', fakeAsync(() => { - cookieService.check.mockReturnValue(false); - const now = new Date(); - const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); - const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); - httpClient.get.mockReturnValueOnce( - of({ - maintenance: { - level: 3, - message: 'Danger message', - start, - end, - }, - }) - ); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - const banner = fixture.debugElement.query(By.css('p-message')); - expect(banner).toBeTruthy(); - expect(banner.componentInstance.severity).toBe('error'); - expect(banner.nativeElement.textContent).toContain('Danger message'); - })); - - it('should not render banner if cookie is set', fakeAsync(() => { - cookieService.check.mockReturnValue(true); - fixture.detectChanges(); - expect(httpClient.get).not.toHaveBeenCalled(); - fixture.detectChanges(); - const banner = fixture.debugElement.query(By.css('p-message')); - expect(banner).toBeFalsy(); - })); - - it('should not render banner if outside maintenance window', fakeAsync(() => { - cookieService.check.mockReturnValue(false); - httpClient.get.mockReturnValueOnce( - of({ - maintenance: { level: 1, message: 'Old message', start: '2020-01-01T00:00:00Z', end: '2020-01-02T00:00:00Z' }, - }) - ); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - const banner = fixture.debugElement.query(By.css('p-message')); - expect(banner).toBeFalsy(); - })); - - it('should dismiss banner when close button is clicked', fakeAsync(() => { - cookieService.check.mockReturnValue(false); - const now = new Date(); - const start = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); - const end = new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); - httpClient.get.mockReturnValueOnce( - of({ - maintenance: { level: 1, message: 'Dismiss me', start, end }, - }) - ); - fixture.detectChanges(); - tick(); - fixture.detectChanges(); - const banner = fixture.debugElement.query(By.css('p-message')); - expect(banner).toBeTruthy(); - banner.triggerEventHandler('onClose', {}); + it('should fetch maintenance when not dismissed in browser', () => { + setup({ cookieDismissed: false, maintenance: activeMaintenance }); + expect(cookieServiceMock.check).toHaveBeenCalledWith('osf-maintenance-dismissed'); + expect(maintenanceServiceMock.fetchMaintenanceStatus).toHaveBeenCalledTimes(1); + expect(component.maintenance()).toEqual(activeMaintenance); + }); + + it('should fetch maintenance on server without cookie check', () => { + setup({ isBrowser: false, maintenance: activeMaintenance }); + expect(cookieServiceMock.check).not.toHaveBeenCalled(); + expect(maintenanceServiceMock.fetchMaintenanceStatus).toHaveBeenCalledTimes(1); + expect(component.maintenance()).toEqual(activeMaintenance); + }); + + it('should dismiss and persist cookie in browser', () => { + setup({ maintenance: activeMaintenance }); + component.dismiss(); + expect(cookieServiceMock.set).toHaveBeenCalledWith('osf-maintenance-dismissed', '1', 24, '/'); + expect(component.dismissed()).toBe(true); + expect(component.maintenance()).toBeNull(); + }); + + it('should dismiss without setting cookie on server', () => { + setup({ isBrowser: false, maintenance: activeMaintenance }); + component.dismiss(); + expect(cookieServiceMock.set).not.toHaveBeenCalled(); + expect(component.dismissed()).toBe(true); + expect(component.maintenance()).toBeNull(); + }); + + it('should render banner only when maintenance exists and not dismissed', () => { + setup({ maintenance: activeMaintenance, cookieDismissed: false }); + const message = fixture.debugElement.query(By.css('p-message')); + expect(message).toBeTruthy(); + component.dismissed.set(true); fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('p-message'))).toBeFalsy(); - expect(cookieService.set).toHaveBeenCalled(); - })); + const hiddenMessage = fixture.debugElement.query(By.css('p-message')); + expect(hiddenMessage).toBeNull(); + }); }); diff --git a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts index c2b2cb756..b2980488b 100644 --- a/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts +++ b/src/app/core/components/osf-banners/maintenance-banner/maintenance-banner.component.ts @@ -1,6 +1,6 @@ import { CookieService } from 'ngx-cookie-service'; -import { MessageModule } from 'primeng/message'; +import { Message } from 'primeng/message'; import { isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, OnInit, PLATFORM_ID, signal } from '@angular/core'; @@ -22,7 +22,7 @@ import { MaintenanceService } from '../services/maintenance.service'; */ @Component({ selector: 'osf-maintenance-banner', - imports: [MessageModule], + imports: [Message], templateUrl: './maintenance-banner.component.html', styleUrls: ['./maintenance-banner.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/core/components/osf-banners/osf-banner.component.spec.ts b/src/app/core/components/osf-banners/osf-banner.component.spec.ts index d1f8859ac..0941466b5 100644 --- a/src/app/core/components/osf-banners/osf-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/osf-banner.component.spec.ts @@ -1,34 +1,31 @@ -import { MockComponent } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { CookieConsentBannerComponent } from './cookie-consent-banner/cookie-consent-banner.component'; +import { MaintenanceBannerComponent } from './maintenance-banner/maintenance-banner.component'; import { TosConsentBannerComponent } from './tos-consent-banner/tos-consent-banner.component'; import { OSFBannerComponent } from './osf-banner.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { MockComponentWithSignal } from '@testing/providers/component-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('Component: OSF Banner', () => { let fixture: ComponentFixture; let component: OSFBannerComponent; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [ - OSFTestingModule, OSFBannerComponent, - NoopAnimationsModule, - MockComponentWithSignal('osf-maintenance-banner'), - MockComponent(CookieConsentBannerComponent), - MockComponent(TosConsentBannerComponent), + ...MockComponents(MaintenanceBannerComponent, CookieConsentBannerComponent, TosConsentBannerComponent), ], - }).compileComponents(); + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(OSFBannerComponent); component = fixture.componentInstance; }); + it('should create the component', () => { expect(component).toBeTruthy(); }); diff --git a/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts b/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts index e784ebb16..6fdfb9553 100644 --- a/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/scheduled-banner/scheduled-banner.component.spec.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { MockProvider } from 'ng-mocks'; + import { BehaviorSubject } from 'rxjs'; import { signal, WritableSignal } from '@angular/core'; @@ -11,6 +13,7 @@ import { BannersSelector, GetCurrentScheduledBanner } from '@osf/shared/stores/b import { ScheduledBannerComponent } from './scheduled-banner.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('Component: Scheduled Banner', () => { @@ -27,16 +30,12 @@ describe('Component: Scheduled Banner', () => { TestBed.configureTestingModule({ imports: [ScheduledBannerComponent], providers: [ - { - provide: IS_XSMALL, - useValue: isMobile$, - }, + provideOSFCore(), + MockProvider(IS_XSMALL, isMobile$), + provideMockStore({ + signals: [{ selector: BannersSelector.getCurrentBanner, value: currentBannerSignal }], + }), ], - }).overrideProvider(Store, { - useValue: provideMockStore({ - signals: [{ selector: BannersSelector.getCurrentBanner, value: currentBannerSignal }], - actions: [{ action: new GetCurrentScheduledBanner(), value: true }], - }).useValue, }); fixture = TestBed.createComponent(ScheduledBannerComponent); @@ -46,10 +45,6 @@ describe('Component: Scheduled Banner', () => { store = TestBed.inject(Store); }); - afterEach(() => { - jest.clearAllMocks(); - }); - it('should create the component', () => { expect(component).toBeInstanceOf(ScheduledBannerComponent); }); diff --git a/src/app/core/components/osf-banners/services/maintenance.service.spec.ts b/src/app/core/components/osf-banners/services/maintenance.service.spec.ts index e7b6856ee..7a1b1e5c5 100644 --- a/src/app/core/components/osf-banners/services/maintenance.service.spec.ts +++ b/src/app/core/components/osf-banners/services/maintenance.service.spec.ts @@ -1,38 +1,41 @@ -import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { firstValueFrom } from 'rxjs'; + +import { HttpTestingController } from '@angular/common/http/testing'; import { TestBed } from '@angular/core/testing'; import { MaintenanceModel } from '../models/maintenance.model'; import { MaintenanceService } from './maintenance.service'; -import { environment } from 'src/environments/environment'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; describe('MaintenanceService', () => { let service: MaintenanceService; let httpMock: HttpTestingController; - const apiUrl = `${environment.apiDomainUrl}/v2/status/`; - - const futureDate = (offsetMinutes: number) => new Date(Date.now() + offsetMinutes * 60000).toISOString(); + const apiUrl = 'http://localhost:8000/v2/status/'; - const validMaintenance: MaintenanceModel = { - start: futureDate(-10), - end: futureDate(10), - level: 2, - message: 'Scheduled maintenance', + const now = Date.now(); + const activeWindow = { + start: new Date(now - 10 * 60 * 1000).toISOString(), + end: new Date(now + 10 * 60 * 1000).toISOString(), }; - - const expiredMaintenance: MaintenanceModel = { - start: futureDate(-60), - end: futureDate(-30), - level: 1, - message: 'Old maintenance', + const expiredWindow = { + start: new Date(now - 60 * 60 * 1000).toISOString(), + end: new Date(now - 30 * 60 * 1000).toISOString(), }; + const createMaintenance = (level: number, overrides?: Partial): MaintenanceModel => ({ + level, + message: 'Scheduled maintenance', + start: activeWindow.start, + end: activeWindow.end, + ...overrides, + }); + beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientTestingModule], - providers: [MaintenanceService], + providers: [provideOSFCore(), provideOSFHttp()], }); service = TestBed.inject(MaintenanceService); httpMock = TestBed.inject(HttpTestingController); @@ -42,61 +45,79 @@ describe('MaintenanceService', () => { httpMock.verify(); }); - it('should return maintenance when within window and map severity correctly', (done) => { - service.fetchMaintenanceStatus().subscribe((result) => { - expect(result).toEqual({ - ...validMaintenance, - severity: 'warn', - }); - done(); - }); + it('should create', () => { + expect(service).toBeTruthy(); + }); + it('should return active maintenance with info severity for level 1', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); const req = httpMock.expectOne(apiUrl); expect(req.request.method).toBe('GET'); - req.flush({ maintenance: validMaintenance }); - httpMock.verify(); + req.flush({ maintenance: createMaintenance(1) }); + const result = await resultPromise; + expect(result).toEqual(expect.objectContaining({ severity: 'info' })); }); - it('should return null when maintenance is outside window', (done) => { - service.fetchMaintenanceStatus().subscribe((result) => { - expect(result).toBeNull(); - done(); - }); - + it('should return active maintenance with warn severity for level 2', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); const req = httpMock.expectOne(apiUrl); - req.flush({ maintenance: expiredMaintenance }); - httpMock.verify(); + req.flush({ maintenance: createMaintenance(2) }); + const result = await resultPromise; + expect(result).toEqual(expect.objectContaining({ severity: 'warn' })); }); - it('should return null when maintenance is not present', (done) => { - service.fetchMaintenanceStatus().subscribe((result) => { - expect(result).toBeNull(); - done(); - }); + it('should return active maintenance with error severity for level 3', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); + const req = httpMock.expectOne(apiUrl); + req.flush({ maintenance: createMaintenance(3) }); + const result = await resultPromise; + expect(result).toEqual(expect.objectContaining({ severity: 'error' })); + }); + it('should return info severity for unknown level', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); const req = httpMock.expectOne(apiUrl); - req.flush({}); - httpMock.verify(); + req.flush({ maintenance: createMaintenance(99) }); + const result = await resultPromise; + expect(result).toEqual(expect.objectContaining({ severity: 'info' })); }); - it('should handle errors and return null', (done) => { - service.fetchMaintenanceStatus().subscribe((result) => { - expect(result).toBeNull(); - done(); - }); + it('should return null when maintenance is outside window', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); + const req = httpMock.expectOne(apiUrl); + req.flush({ maintenance: createMaintenance(2, expiredWindow) }); + const result = await resultPromise; + expect(result).toBeNull(); + }); + it('should return null when maintenance dates are missing', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); const req = httpMock.expectOne(apiUrl); - req.error(new ProgressEvent('error')); + req.flush({ + maintenance: { + level: 2, + message: 'Scheduled maintenance', + start: '', + end: '', + }, + }); + const result = await resultPromise; + expect(result).toBeNull(); }); - it('should map unknown severity level to "info"', () => { - const result = (service as any).getSeverity(99); - expect(result).toBe('info'); + it('should return null when maintenance is absent', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); + const req = httpMock.expectOne(apiUrl); + req.flush({}); + const result = await resultPromise; + expect(result).toBeNull(); }); - it('should return false if start or end is missing', () => { - const partial: Partial = { level: 1, message: 'Missing dates' }; - const result = (service as any).isWithinMaintenanceWindow(partial); - expect(result).toBe(false); + it('should return null when request fails', async () => { + const resultPromise = firstValueFrom(service.fetchMaintenanceStatus()); + const req = httpMock.expectOne(apiUrl); + req.error(new ProgressEvent('error')); + const result = await resultPromise; + expect(result).toBeNull(); }); }); diff --git a/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts b/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts index f08e2f5da..be08f6e16 100644 --- a/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts @@ -1,86 +1,121 @@ import { Store } from '@ngxs/store'; -import { MockComponent } from 'ng-mocks'; - -import { of } from 'rxjs'; +import { MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { ActivatedRoute, provideRouter } from '@angular/router'; -import { AcceptTermsOfServiceByUser } from '@core/store/user'; -import { UserSelectors } from '@osf/core/store/user'; -import { IconComponent } from '@osf/shared/components/icon/icon.component'; +import { AcceptTermsOfServiceByUser, UserSelectors } from '@core/store/user'; +import { UserModel } from '@osf/shared/models/user/user.model'; import { TosConsentBannerComponent } from './tos-consent-banner.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { BaseSetupOverrides, mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock'; describe('TosConsentBannerComponent', () => { + let component: TosConsentBannerComponent; let fixture: ComponentFixture; - let store: jest.Mocked; + let store: Store; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [TosConsentBannerComponent, OSFTestingStoreModule, MockComponent(IconComponent)], + function setup(overrides: BaseSetupOverrides = {}) { + TestBed.configureTestingModule({ + imports: [TosConsentBannerComponent], providers: [ + provideOSFCore(), + provideRouter([]), + MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), provideMockStore({ - signals: [{ selector: UserSelectors.getCurrentUser, value: MOCK_USER }], + signals: mergeSignalOverrides( + [ + { + selector: UserSelectors.getCurrentUser, + value: { + ...MOCK_USER, + acceptedTermsOfService: false, + } as UserModel, + }, + ], + overrides.selectorOverrides + ), }), - TranslationServiceMock, ], - }).compileComponents(); + }); fixture = TestBed.createComponent(TosConsentBannerComponent); - store = TestBed.inject(Store) as jest.Mocked; - store.dispatch = jest.fn().mockReturnValue(of(undefined)); + component = fixture.componentInstance; + store = TestBed.inject(Store); fixture.detectChanges(); - }); + } - it('should have the "Continue" button disabled by default', () => { - const continueButton = fixture.debugElement.query(By.css('p-button button')).nativeElement; - expect(continueButton.disabled).toBe(true); + it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should enable the "Continue" button when the checkbox is checked', () => { - const checkboxInput = fixture.debugElement.query(By.css('p-checkbox input')).nativeElement; - checkboxInput.click(); - fixture.detectChanges(); - const continueButton = fixture.debugElement.query(By.css('p-button button')).nativeElement; - expect(continueButton.disabled).toBe(false); + it('should return true when current user is null', () => { + setup({ + selectorOverrides: [{ selector: UserSelectors.getCurrentUser, value: null }], + }); + expect(component.acceptedTermsOfServiceChange()).toBe(true); }); - it('should dispatch AcceptTermsOfServiceByUser action when "Continue" is clicked', () => { - const checkboxInput = fixture.debugElement.query(By.css('p-checkbox input')).nativeElement; - checkboxInput.click(); - fixture.detectChanges(); + it('should return false when current user has not accepted terms', () => { + setup({ + selectorOverrides: [ + { + selector: UserSelectors.getCurrentUser, + value: { ...MOCK_USER, acceptedTermsOfService: false } as UserModel, + }, + ], + }); + expect(component.acceptedTermsOfServiceChange()).toBe(false); + }); - const continueButton = fixture.debugElement.query(By.css('p-button button')).nativeElement; - continueButton.click(); - fixture.detectChanges(); + it('should return true when current user has accepted terms', () => { + setup({ + selectorOverrides: [ + { + selector: UserSelectors.getCurrentUser, + value: { ...MOCK_USER, acceptedTermsOfService: true } as UserModel, + }, + ], + }); + expect(component.acceptedTermsOfServiceChange()).toBe(true); + }); + it('should dispatch AcceptTermsOfServiceByUser on continue', () => { + setup(); + component.onContinue(); expect(store.dispatch).toHaveBeenCalledWith(new AcceptTermsOfServiceByUser()); }); - it('should return true for "acceptedTermsOfServiceChange" when user is null to not show banner', async () => { - await TestBed.resetTestingModule() - .configureTestingModule({ - imports: [TosConsentBannerComponent, OSFTestingStoreModule, MockComponent(IconComponent)], - providers: [ - provideMockStore({ - signals: [{ selector: UserSelectors.getCurrentUser, value: null }], - }), - TranslationServiceMock, - ], - }) - .compileComponents(); - - const fixture = TestBed.createComponent(TosConsentBannerComponent); - const component = fixture.componentInstance; + it('should render banner when terms are not accepted', () => { + setup({ + selectorOverrides: [ + { + selector: UserSelectors.getCurrentUser, + value: { ...MOCK_USER, acceptedTermsOfService: false } as UserModel, + }, + ], + }); + const banner = fixture.debugElement.query(By.css('p-message')); + expect(banner).toBeTruthy(); + }); - fixture.detectChanges(); - expect(component.acceptedTermsOfServiceChange()).toBe(true); + it('should not render banner when terms are accepted', () => { + setup({ + selectorOverrides: [ + { + selector: UserSelectors.getCurrentUser, + value: { ...MOCK_USER, acceptedTermsOfService: true } as UserModel, + }, + ], + }); + const banner = fixture.debugElement.query(By.css('p-message')); + expect(banner).toBeNull(); }); }); diff --git a/src/app/core/components/page-not-found/page-not-found.component.spec.ts b/src/app/core/components/page-not-found/page-not-found.component.spec.ts index 3da4b864d..2ae547028 100644 --- a/src/app/core/components/page-not-found/page-not-found.component.spec.ts +++ b/src/app/core/components/page-not-found/page-not-found.component.spec.ts @@ -1,18 +1,18 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PageNotFoundComponent } from './page-not-found.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('PageNotFoundComponent', () => { let component: PageNotFoundComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [PageNotFoundComponent, MockPipe(TranslatePipe)], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [PageNotFoundComponent], + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(PageNotFoundComponent); component = fixture.componentInstance; @@ -22,4 +22,8 @@ describe('PageNotFoundComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should expose support email from environment', () => { + expect(component.supportEmail).toBe('support@test.com'); + }); }); diff --git a/src/app/core/components/request-access/request-access.component.html b/src/app/core/components/request-access/request-access.component.html index 137a1c8a3..f8b74ca4b 100644 --- a/src/app/core/components/request-access/request-access.component.html +++ b/src/app/core/components/request-access/request-access.component.html @@ -23,7 +23,7 @@

{{ 'requestAccess.title' | translate }}

class="w-full" styleClass="w-full" [label]="'requestAccess.requestAccess' | translate" - (click)="requestAccess()" + (onClick)="requestAccess()" > {{ 'requestAccess.title' | translate }} styleClass="w-full" severity="secondary" [label]="'requestAccess.switchAccount' | translate" - (click)="switchAccount()" + (onClick)="switchAccount()" > diff --git a/src/app/core/components/request-access/request-access.component.spec.ts b/src/app/core/components/request-access/request-access.component.spec.ts index 2c15a032f..7a3ef7245 100644 --- a/src/app/core/components/request-access/request-access.component.spec.ts +++ b/src/app/core/components/request-access/request-access.component.spec.ts @@ -1,49 +1,124 @@ -import { Store } from '@ngxs/store'; +import { MockProvider } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { Observable, of, throwError } from 'rxjs'; -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { HttpErrorResponse } from '@angular/common/http'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; +import { AuthService } from '@core/services/auth.service'; +import { InputLimits } from '@osf/shared/constants/input-limits.const'; +import { RequestAccessService } from '@osf/shared/services/request-access.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { RequestAccessComponent } from './request-access.component'; -describe.only('RequestAccessComponent', () => { - let component: RequestAccessComponent; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { LoaderServiceMock, provideLoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +describe('RequestAccessComponent', () => { let fixture: ComponentFixture; + let component: RequestAccessComponent; + let routerMock: RouterMockType; + let requestAccessServiceMock: { requestAccessToProject: jest.Mock }; + let loaderServiceMock: LoaderServiceMock; + let toastServiceMock: ToastServiceMockType; + let authServiceMock: { logout: jest.Mock }; + + function setup(overrides?: { + routeId?: string; + requestAccessResult?: Observable; + requestAccessError?: HttpErrorResponse; + }) { + const routeId = overrides?.routeId ?? 'project-1'; + routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); + loaderServiceMock = new LoaderServiceMock(); + toastServiceMock = ToastServiceMock.simple(); + authServiceMock = { logout: jest.fn() }; + + requestAccessServiceMock = { + requestAccessToProject: overrides?.requestAccessError + ? jest.fn().mockReturnValue(throwError(() => overrides.requestAccessError)) + : jest.fn().mockReturnValue(overrides?.requestAccessResult ?? of(void 0)), + }; - const mockStore: jest.Mocked = { - dispatch: jest.fn().mockResolvedValue(undefined) as any, - select: jest.fn().mockReturnValue(of(undefined)) as any, - selectSnapshot: jest.fn() as any, - reset: jest.fn() as any, - } as any; + const activatedRouteMock = ActivatedRouteMockBuilder.create().withParams({ id: routeId }).build(); - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [RequestAccessComponent, MockPipe(TranslatePipe)], + TestBed.configureTestingModule({ + imports: [RequestAccessComponent], providers: [ - provideHttpClient(), - provideHttpClientTesting(), - MockProvider(ToastService), - MockProvider(ActivatedRoute, { params: of({}) }), + provideOSFCore(), + provideLoaderServiceMock(loaderServiceMock), + MockProvider(ActivatedRoute, activatedRouteMock), + MockProvider(Router, routerMock), + MockProvider(RequestAccessService, requestAccessServiceMock), + MockProvider(ToastService, toastServiceMock), + MockProvider(AuthService, authServiceMock), ], - }).compileComponents(); - - TestBed.overrideProvider(Store, { useValue: mockStore }); + }); fixture = TestBed.createComponent(RequestAccessComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); + } it('should create', () => { + setup(); expect(component).toBeTruthy(); }); + + it('should expose support email and comment limit', () => { + setup(); + expect(component.supportEmail).toBe('support@test.com'); + expect(component.commentLimit).toBe(InputLimits.requestAccessComment.maxLength); + }); + + it('should render support email mailto link', () => { + setup(); + const supportLink = fixture.nativeElement.querySelector('a'); + expect(supportLink.getAttribute('href')).toBe(`mailto:${component.supportEmail}`); + expect(supportLink.textContent).toContain(component.supportEmail); + }); + + it('should request access and handle success flow', () => { + setup({ routeId: 'project-123' }); + component.comment.set('please grant access'); + component.requestAccess(); + + expect(loaderServiceMock.show).toHaveBeenCalled(); + expect(requestAccessServiceMock.requestAccessToProject).toHaveBeenCalledWith('project-123', 'please grant access'); + expect(loaderServiceMock.hide).toHaveBeenCalled(); + expect(routerMock.navigate).toHaveBeenCalledWith(['/']); + expect(toastServiceMock.showSuccess).toHaveBeenCalledWith('requestAccess.requestedSuccessMessage'); + }); + + it('should show already requested error on 409', () => { + setup({ + requestAccessError: new HttpErrorResponse({ status: 409 }), + }); + component.requestAccess(); + + expect(loaderServiceMock.show).toHaveBeenCalled(); + expect(toastServiceMock.showError).toHaveBeenCalledWith('requestAccess.alreadyRequestedMessage'); + expect(routerMock.navigate).not.toHaveBeenCalled(); + }); + + it('should not show duplicate error for non-409 failures', () => { + setup({ + requestAccessError: new HttpErrorResponse({ status: 500 }), + }); + component.requestAccess(); + + expect(toastServiceMock.showError).not.toHaveBeenCalled(); + expect(routerMock.navigate).not.toHaveBeenCalled(); + }); + + it('should logout on switch account', () => { + setup(); + component.switchAccount(); + expect(authServiceMock.logout).toHaveBeenCalled(); + }); }); diff --git a/src/app/core/components/sidenav/sidenav.component.spec.ts b/src/app/core/components/sidenav/sidenav.component.spec.ts index 0f5064358..0d55b45fd 100644 --- a/src/app/core/components/sidenav/sidenav.component.spec.ts +++ b/src/app/core/components/sidenav/sidenav.component.spec.ts @@ -6,14 +6,17 @@ import { NavMenuComponent } from '../nav-menu/nav-menu.component'; import { SidenavComponent } from './sidenav.component'; -describe('SidenavDComponent', () => { +import { provideOSFCore } from '@testing/osf.testing.provider'; + +describe('SidenavComponent', () => { let component: SidenavComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [SidenavComponent, MockComponent(NavMenuComponent)], - }).compileComponents(); + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(SidenavComponent); component = fixture.componentInstance; diff --git a/src/app/core/components/topnav/topnav.component.spec.ts b/src/app/core/components/topnav/topnav.component.spec.ts index 13850ead2..394a77912 100644 --- a/src/app/core/components/topnav/topnav.component.spec.ts +++ b/src/app/core/components/topnav/topnav.component.spec.ts @@ -6,14 +6,17 @@ import { NavMenuComponent } from '../nav-menu/nav-menu.component'; import { TopnavComponent } from './topnav.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('TopnavComponent', () => { let component: TopnavComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [TopnavComponent, MockComponent(NavMenuComponent)], - }).compileComponents(); + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(TopnavComponent); component = fixture.componentInstance; @@ -23,4 +26,16 @@ describe('TopnavComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should initialize with drawer hidden', () => { + expect(component.isDrawerVisible()).toBe(false); + }); + + it('should toggle drawer visibility when toggleMenuVisibility is called', () => { + component.toggleMenuVisibility(); + expect(component.isDrawerVisible()).toBe(true); + + component.toggleMenuVisibility(); + expect(component.isDrawerVisible()).toBe(false); + }); }); diff --git a/src/app/core/guards/auth.guard.spec.ts b/src/app/core/guards/auth.guard.spec.ts index 88910e9ed..1e0644e5e 100644 --- a/src/app/core/guards/auth.guard.spec.ts +++ b/src/app/core/guards/auth.guard.spec.ts @@ -2,7 +2,6 @@ import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -23,10 +22,7 @@ describe('authGuard', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - provideMockStore({ - selectors: [], - actions: [], - }), + provideMockStore(), { provide: Router, useValue: RouterMockBuilder.create().withUrl('/test').build(), @@ -43,13 +39,12 @@ describe('authGuard', () => { router = TestBed.inject(Router); authService = TestBed.inject(AuthService); viewOnlyHelper = TestBed.inject(ViewOnlyLinkHelperService); - jest.clearAllMocks(); }); it('should return true when view-only param exists', () => { jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(true); - const result = runInInjectionContext(TestBed, () => { + const result = TestBed.runInInjectionContext(() => { return authGuard({} as any, {} as any); }); @@ -94,7 +89,7 @@ describe('authGuard', () => { router = TestBed.inject(Router); authService = TestBed.inject(AuthService); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = authGuard({} as any, {} as any); if (typeof result === 'object' && 'subscribe' in result) { @@ -146,7 +141,7 @@ describe('authGuard', () => { router = TestBed.inject(Router); authService = TestBed.inject(AuthService); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = authGuard({} as any, {} as any); if (typeof result === 'object' && 'subscribe' in result) { diff --git a/src/app/core/guards/is-file.guard.spec.ts b/src/app/core/guards/is-file.guard.spec.ts index c3d449077..18a15b141 100644 --- a/src/app/core/guards/is-file.guard.spec.ts +++ b/src/app/core/guards/is-file.guard.spec.ts @@ -1,10 +1,12 @@ +import { Store } from '@ngxs/store'; + import { MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; +import { firstValueFrom, Observable } from 'rxjs'; -import { PLATFORM_ID, runInInjectionContext } from '@angular/core'; +import { PLATFORM_ID } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { Route, Router, UrlSegment } from '@angular/router'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { CurrentResource } from '@osf/shared/models/current-resource.model'; @@ -12,456 +14,131 @@ import { CurrentResourceSelectors, GetResource } from '@osf/shared/stores/curren import { isFileGuard } from './is-file.guard'; -import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('isFileGuard', () => { - let router: Router; + let router: RouterMockType; + let store: Store; + + const createSegments = (...paths: string[]): UrlSegment[] => paths.map((path) => ({ path }) as UrlSegment); - const createMockResource = (overrides?: Partial): CurrentResource => ({ + const createResource = (overrides?: Partial): CurrentResource => ({ id: 'file-id', type: CurrentResourceType.Files, permissions: [], ...overrides, }); - const createMockSegments = (path: string, secondPath?: string) => { - const segments = [{ path }] as any[]; - if (secondPath) { - segments.push({ path: secondPath }); + function setup(overrides?: { + resource?: CurrentResource | null; + platformId?: 'browser' | 'server'; + routerUrl?: string; + browserSearch?: string; + }) { + const resource = overrides && 'resource' in overrides ? overrides.resource : createResource(); + const platformId = overrides?.platformId ?? 'browser'; + const routerUrl = overrides?.routerUrl ?? '/file-id'; + const browserSearch = overrides?.browserSearch ?? ''; + + if (platformId === 'browser') { + window.history.replaceState({}, '', `/guard-test${browserSearch}`); } - return segments; - }; - beforeEach(() => { - Object.defineProperty(window, 'location', { - writable: true, - value: { - href: 'http://localhost/test', - }, - }); + router = RouterMockBuilder.create().withUrl(routerUrl).withNavigate(jest.fn().mockResolvedValue(true)).build(); TestBed.configureTestingModule({ providers: [ + provideOSFCore(), provideMockStore({ - selectors: [], - actions: [], + selectors: [{ selector: CurrentResourceSelectors.getCurrentResource, value: resource }], }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, + MockProvider(Router, router), + MockProvider(PLATFORM_ID, platformId), ], }); - router = TestBed.inject(Router); - jest.clearAllMocks(); - }); + store = TestBed.inject(Store); + } - it('should return false when id is missing', () => { - const result = runInInjectionContext(TestBed, () => { - return isFileGuard({} as any, []); - }); + async function resolveGuard(segments: UrlSegment[]) { + const result = TestBed.runInInjectionContext(() => isFileGuard({} as Route, segments)); + if (typeof result === 'boolean') { + return result; + } + return firstValueFrom(result as Observable); + } + it('should return false when id is missing', async () => { + setup(); + const result = await resolveGuard([]); expect(result).toBe(false); }); - it('should return false when resource is not found', (done) => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: null, - }, - ], - actions: [ - { - action: new GetResource('file-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, - ], - }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('file-id')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(false); - expect(router.navigate).not.toHaveBeenCalled(); - done(); - }); - } else { - expect(result).toBe(false); - done(); - } - }); + it('should return false when current resource is missing', async () => { + setup({ resource: null }); + const result = await resolveGuard(createSegments('file-id')); + expect(result).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); }); - it('should return false when resource id does not match exactly', (done) => { - const resource = createMockResource({ id: 'different-id' }); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: resource, - }, - ], - actions: [ - { - action: new GetResource('file-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, - ], - }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('file-id')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(false); - expect(router.navigate).not.toHaveBeenCalled(); - done(); - }); - } else { - expect(result).toBe(false); - done(); - } - }); + it('should return false when current resource id does not match route id', async () => { + setup({ resource: createResource({ id: 'different-id' }) }); + const result = await resolveGuard(createSegments('file-id')); + expect(result).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); }); - it('should return true for Files with metadata path', (done) => { - const resource = createMockResource({ - id: 'file-id', - type: CurrentResourceType.Files, - }); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: resource, - }, - ], - actions: [ - { - action: new GetResource('file-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, - ], - }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('file-id', 'metadata')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(true); - expect(router.navigate).not.toHaveBeenCalled(); - done(); - }); - } else { - expect(result).toBe(true); - done(); - } - }); + it('should return true for metadata path on file resource', async () => { + setup({ resource: createResource({ parentId: 'node-1' }) }); + const result = await resolveGuard(createSegments('file-id', 'metadata')); + expect(result).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); }); - it('should navigate and return false for Files with parentId', (done) => { - const resource = createMockResource({ - id: 'file-id', - type: CurrentResourceType.Files, - parentId: 'parent-id', - }); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: resource, - }, - ], - actions: [ - { - action: new GetResource('file-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, - ], + it('should navigate to parent files route with browser view_only query param', async () => { + setup({ + resource: createResource({ parentId: 'node-1' }), + browserSearch: '?view_only=token-123', }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('file-id')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(false); - expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'file-id'], {}); - done(); - }); - } else { - expect(result).toBe(false); - done(); - } + const result = await resolveGuard(createSegments('file-id')); + expect(result).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/', 'node-1', 'files', 'file-id'], { + queryParams: { view_only: 'token-123' }, }); }); - it('should return true for Files without parentId', (done) => { - const resource = createMockResource({ - id: 'file-id', - type: CurrentResourceType.Files, + it('should navigate to parent files route with server view_only query param', async () => { + setup({ + platformId: 'server', + routerUrl: '/files/file-id?view_only=srv-token', + resource: createResource({ parentId: 'node-1' }), }); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: resource, - }, - ], - actions: [ - { - action: new GetResource('file-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, - ], - }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('file-id')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(true); - expect(router.navigate).not.toHaveBeenCalled(); - done(); - }); - } else { - expect(result).toBe(true); - done(); - } + const result = await resolveGuard(createSegments('file-id')); + expect(result).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/', 'node-1', 'files', 'file-id'], { + queryParams: { view_only: 'srv-token' }, }); }); - it('should return false for other resource types', (done) => { - const resource = createMockResource({ - id: 'resource-id', - type: CurrentResourceType.Projects, - }); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: resource, - }, - ], - actions: [ - { - action: new GetResource('resource-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, - ], - }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('resource-id')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(false); - expect(router.navigate).not.toHaveBeenCalled(); - done(); - }); - } else { - expect(result).toBe(false); - done(); - } - }); + it('should return true for file resource without parentId', async () => { + setup({ resource: createResource({ parentId: undefined }) }); + const result = await resolveGuard(createSegments('file-id')); + expect(result).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); }); - it('should include view_only param in navigation when present in router.url (server-side)', (done) => { - const resource = createMockResource({ - id: 'file-id', - type: CurrentResourceType.Files, - parentId: 'parent-id', - }); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: resource, - }, - ], - actions: [ - { - action: new GetResource('file-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test?view_only=abc123').build()), - { - provide: PLATFORM_ID, - useValue: 'server', - }, - ], - }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('file-id')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(false); - expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'file-id'], { - queryParams: { view_only: 'abc123' }, - }); - done(); - }); - } else { - expect(result).toBe(false); - done(); - } - }); + it('should return false for non-file resource', async () => { + setup({ resource: createResource({ type: CurrentResourceType.Projects }) }); + const result = await resolveGuard(createSegments('file-id')); + expect(result).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); }); - it('should include view_only param in navigation when present in window.location (browser)', (done) => { - const resource = createMockResource({ - id: 'file-id', - type: CurrentResourceType.Files, - parentId: 'parent-id', - }); - - Object.defineProperty(window, 'location', { - writable: true, - value: { - href: 'http://localhost/test?view_only=xyz789', - }, - }); - - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - providers: [ - provideMockStore({ - selectors: [ - { - selector: CurrentResourceSelectors.getCurrentResource, - value: resource, - }, - ], - actions: [ - { - action: new GetResource('file-id'), - value: of(true), - }, - ], - }), - MockProvider(Router, RouterMockBuilder.create().withUrl('/test').build()), - { - provide: PLATFORM_ID, - useValue: 'browser', - }, - ], - }); - - router = TestBed.inject(Router); - - runInInjectionContext(TestBed, () => { - const result = isFileGuard({} as any, createMockSegments('file-id')); - - if (typeof result === 'object' && 'subscribe' in result) { - result.subscribe((value) => { - expect(value).toBe(false); - expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'file-id'], { - queryParams: { view_only: 'xyz789' }, - }); - done(); - }); - } else { - expect(result).toBe(false); - done(); - } - }); + it('should dispatch GetResource with route id', async () => { + setup(); + await resolveGuard(createSegments('file-id')); + expect(store.dispatch).toHaveBeenCalledWith(new GetResource('file-id')); }); }); diff --git a/src/app/core/guards/is-project.guard.spec.ts b/src/app/core/guards/is-project.guard.spec.ts index a6990ec45..344eecce9 100644 --- a/src/app/core/guards/is-project.guard.spec.ts +++ b/src/app/core/guards/is-project.guard.spec.ts @@ -2,7 +2,6 @@ import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -44,7 +43,7 @@ describe('isProjectGuard', () => { }); it('should return false when id is missing', () => { - const result = runInInjectionContext(TestBed, () => { + const result = TestBed.runInInjectionContext(() => { return isProjectGuard({} as any, []); }); @@ -75,7 +74,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('test-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -117,7 +116,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('test-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -163,7 +162,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('parent-id/child-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -210,7 +209,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('project-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -256,7 +255,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('parent-id/child-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -306,7 +305,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('user-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -356,7 +355,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('user-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -401,7 +400,7 @@ describe('isProjectGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isProjectGuard({} as any, createMockSegments('resource-id')); if (typeof result === 'object' && 'subscribe' in result) { diff --git a/src/app/core/guards/is-registry.guard.spec.ts b/src/app/core/guards/is-registry.guard.spec.ts index b16ec607f..6dcbc8bb4 100644 --- a/src/app/core/guards/is-registry.guard.spec.ts +++ b/src/app/core/guards/is-registry.guard.spec.ts @@ -2,7 +2,6 @@ import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -44,7 +43,7 @@ describe('isRegistryGuard', () => { }); it('should return false when id is missing', () => { - const result = runInInjectionContext(TestBed, () => { + const result = TestBed.runInInjectionContext(() => { return isRegistryGuard({} as any, []); }); @@ -75,7 +74,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('test-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -117,7 +116,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('test-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -163,7 +162,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('parent-id/child-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -210,7 +209,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('registration-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -256,7 +255,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('parent-id/child-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -306,7 +305,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('user-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -356,7 +355,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('user-id')); if (typeof result === 'object' && 'subscribe' in result) { @@ -401,7 +400,7 @@ describe('isRegistryGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = isRegistryGuard({} as any, createMockSegments('resource-id')); if (typeof result === 'object' && 'subscribe' in result) { diff --git a/src/app/core/guards/redirect-if-logged-in.guard.spec.ts b/src/app/core/guards/redirect-if-logged-in.guard.spec.ts index a12976489..1edb6d7f4 100644 --- a/src/app/core/guards/redirect-if-logged-in.guard.spec.ts +++ b/src/app/core/guards/redirect-if-logged-in.guard.spec.ts @@ -1,6 +1,5 @@ import { of } from 'rxjs'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -59,7 +58,7 @@ describe('redirectIfLoggedInGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = redirectIfLoggedInGuard({} as any, {} as any); if (typeof result === 'object' && 'subscribe' in result) { @@ -102,7 +101,7 @@ describe('redirectIfLoggedInGuard', () => { router = TestBed.inject(Router); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = redirectIfLoggedInGuard({} as any, {} as any); if (typeof result === 'object' && 'subscribe' in result) { diff --git a/src/app/core/guards/registration-moderation.guard.spec.ts b/src/app/core/guards/registration-moderation.guard.spec.ts index 1d3ae7b77..35a9579bb 100644 --- a/src/app/core/guards/registration-moderation.guard.spec.ts +++ b/src/app/core/guards/registration-moderation.guard.spec.ts @@ -1,6 +1,5 @@ import { of } from 'rxjs'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -66,7 +65,7 @@ describe('registrationModerationGuard', () => { router = TestBed.inject(Router); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => registrationModerationGuard({ params: { providerId: 'provider-123' } } as any, {} as any) ); @@ -96,7 +95,7 @@ describe('registrationModerationGuard', () => { router = TestBed.inject(Router); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => registrationModerationGuard({ params: { providerId: 'provider-123' } } as any, {} as any) ); @@ -139,7 +138,7 @@ describe('registrationModerationGuard', () => { router = TestBed.inject(Router); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => registrationModerationGuard({ params: { providerId: 'provider-123' } } as any, {} as any) ); diff --git a/src/app/core/guards/view-only.guard.spec.ts b/src/app/core/guards/view-only.guard.spec.ts index 97d94c0f3..36105133c 100644 --- a/src/app/core/guards/view-only.guard.spec.ts +++ b/src/app/core/guards/view-only.guard.spec.ts @@ -1,6 +1,5 @@ import { MockProvider } from 'ng-mocks'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -36,7 +35,7 @@ describe('viewOnlyGuard', () => { it('should return true when no view-only param exists', () => { jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(false); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => viewOnlyGuard({ routeConfig: { path: 'test' } } as any, {} as any) ); @@ -47,7 +46,7 @@ describe('viewOnlyGuard', () => { it('should return true when view-only param exists but route is not blocked', () => { jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(true); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => viewOnlyGuard({ routeConfig: { path: 'allowed-route' } } as any, {} as any) ); @@ -60,7 +59,7 @@ describe('viewOnlyGuard', () => { jest.spyOn(viewOnlyHelper, 'getViewOnlyParam').mockReturnValue('abc123'); Object.defineProperty(router, 'url', { value: '/resource-123/some-path', writable: true }); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => viewOnlyGuard({ routeConfig: { path: 'metadata' } } as any, {} as any) ); @@ -75,7 +74,7 @@ describe('viewOnlyGuard', () => { jest.spyOn(viewOnlyHelper, 'getViewOnlyParam').mockReturnValue(null); Object.defineProperty(router, 'url', { value: '/invalid-url', writable: true }); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => viewOnlyGuard({ routeConfig: { path: 'metadata' } } as any, {} as any) ); @@ -88,7 +87,7 @@ describe('viewOnlyGuard', () => { jest.spyOn(viewOnlyHelper, 'getViewOnlyParam').mockReturnValue('xyz789'); Object.defineProperty(router, 'url', { value: '/resource-456/metadata/subpath', writable: true }); - const result = runInInjectionContext(TestBed, () => + const result = TestBed.runInInjectionContext(() => viewOnlyGuard({ routeConfig: { path: 'metadata/subpath' } } as any, {} as any) ); @@ -101,7 +100,7 @@ describe('viewOnlyGuard', () => { it('should handle empty route path gracefully', () => { jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(true); - const result = runInInjectionContext(TestBed, () => viewOnlyGuard({ routeConfig: { path: '' } } as any, {} as any)); + const result = TestBed.runInInjectionContext(() => viewOnlyGuard({ routeConfig: { path: '' } } as any, {} as any)); expect(result).toBe(true); }); @@ -109,7 +108,7 @@ describe('viewOnlyGuard', () => { it('should handle undefined route config gracefully', () => { jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(true); - const result = runInInjectionContext(TestBed, () => viewOnlyGuard({ routeConfig: undefined } as any, {} as any)); + const result = TestBed.runInInjectionContext(() => viewOnlyGuard({ routeConfig: undefined } as any, {} as any)); expect(result).toBe(true); }); diff --git a/src/app/core/helpers/i18n.helper.ts b/src/app/core/helpers/i18n.helper.ts index c8f826a87..b1deb7330 100644 --- a/src/app/core/helpers/i18n.helper.ts +++ b/src/app/core/helpers/i18n.helper.ts @@ -3,7 +3,7 @@ import { provideTranslateHttpLoader } from '@ngx-translate/http-loader'; export const provideTranslation = provideTranslateService({ loader: provideTranslateHttpLoader({ - prefix: './assets/i18n/', + prefix: '/assets/i18n/', suffix: '.json', }), lang: 'en', diff --git a/src/app/core/interceptors/auth.interceptor.spec.ts b/src/app/core/interceptors/auth.interceptor.spec.ts index ff870d84c..f1096a525 100644 --- a/src/app/core/interceptors/auth.interceptor.spec.ts +++ b/src/app/core/interceptors/auth.interceptor.spec.ts @@ -4,7 +4,7 @@ import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { HttpRequest } from '@angular/common/http'; -import { PLATFORM_ID, runInInjectionContext } from '@angular/core'; +import { PLATFORM_ID } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -30,10 +30,6 @@ describe('authInterceptor', () => { cookieService = TestBed.inject(CookieService); }; - beforeEach(() => { - jest.clearAllMocks(); - }); - const createRequest = (url: string, options?: Partial>): HttpRequest => { return new HttpRequest('GET', url, options?.body, { responseType: options?.responseType || 'json', @@ -51,7 +47,7 @@ describe('authInterceptor', () => { const request = createRequest('https://api.ror.org/v2'); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -63,7 +59,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/', { responseType: 'text' }); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -75,7 +71,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/', { responseType: 'json' }); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -87,7 +83,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -102,7 +98,7 @@ describe('authInterceptor', () => { }); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(requestWithHeaders, handler)); + TestBed.runInInjectionContext(() => authInterceptor(requestWithHeaders, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -116,7 +112,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); expect(cookieService.get).toHaveBeenCalledWith('api-csrf'); expect(handler).toHaveBeenCalledTimes(1); @@ -132,7 +128,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); expect(cookieService.get).toHaveBeenCalledWith('api-csrf'); expect(handler).toHaveBeenCalledTimes(1); @@ -146,7 +142,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); const modifiedRequest = handler.mock.calls[0][0]; expect(modifiedRequest.headers.has('X-Throttle-Token')).toBe(false); @@ -157,7 +153,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); const modifiedRequest = handler.mock.calls[0][0]; expect(modifiedRequest.headers.get('X-Throttle-Token')).toBe('test-token'); @@ -168,7 +164,7 @@ describe('authInterceptor', () => { const request = createRequest('/api/v2/projects/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => authInterceptor(request, handler)); + TestBed.runInInjectionContext(() => authInterceptor(request, handler)); const modifiedRequest = handler.mock.calls[0][0]; expect(modifiedRequest.headers.has('X-Throttle-Token')).toBe(false); diff --git a/src/app/core/interceptors/error.interceptor.spec.ts b/src/app/core/interceptors/error.interceptor.spec.ts index 17d5bbec3..6b689955c 100644 --- a/src/app/core/interceptors/error.interceptor.spec.ts +++ b/src/app/core/interceptors/error.interceptor.spec.ts @@ -3,7 +3,6 @@ import { MockProvider } from 'ng-mocks'; import { throwError } from 'rxjs'; import { HttpContext, HttpErrorResponse, HttpHeaders, HttpRequest } from '@angular/common/http'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -63,7 +62,6 @@ describe('errorInterceptor', () => { router = TestBed.inject(Router); authService = TestBed.inject(AuthService); viewOnlyHelper = TestBed.inject(ViewOnlyLinkHelperService); - jest.clearAllMocks(); }); const createRequest = (url = '/api/v2/test'): HttpRequest => { @@ -82,7 +80,7 @@ describe('errorInterceptor', () => { context: new HttpContext().set(BYPASS_ERROR_INTERCEPTOR, true), }); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -99,7 +97,7 @@ describe('errorInterceptor', () => { const error = new HttpErrorResponse({ error: errorEvent, status: 0 }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -117,7 +115,7 @@ describe('errorInterceptor', () => { }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -132,7 +130,7 @@ describe('errorInterceptor', () => { const error = new HttpErrorResponse({ error: {}, status: 404 }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -150,7 +148,7 @@ describe('errorInterceptor', () => { }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -165,7 +163,7 @@ describe('errorInterceptor', () => { const error = new HttpErrorResponse({ error: {}, status: 409 }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -182,7 +180,7 @@ describe('errorInterceptor', () => { const error = new HttpErrorResponse({ error: {}, status: 401 }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -200,7 +198,7 @@ describe('errorInterceptor', () => { const error = new HttpErrorResponse({ error: {}, status: 401 }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -220,7 +218,7 @@ describe('errorInterceptor', () => { }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -240,7 +238,7 @@ describe('errorInterceptor', () => { }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -260,7 +258,7 @@ describe('errorInterceptor', () => { }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { @@ -281,7 +279,7 @@ describe('errorInterceptor', () => { }); const request = createRequest(); - runInInjectionContext(TestBed, () => { + TestBed.runInInjectionContext(() => { const result = errorInterceptor(request, createErrorHandler(error)); result.subscribe({ error: () => { diff --git a/src/app/core/interceptors/view-only.interceptor.spec.ts b/src/app/core/interceptors/view-only.interceptor.spec.ts index d75ff8cb1..6e27a660f 100644 --- a/src/app/core/interceptors/view-only.interceptor.spec.ts +++ b/src/app/core/interceptors/view-only.interceptor.spec.ts @@ -3,7 +3,6 @@ import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { HttpRequest } from '@angular/common/http'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -51,7 +50,7 @@ describe('viewOnlyInterceptor', () => { const request = createRequest('/api/v2/projects/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => viewOnlyInterceptor(request, handler)); + TestBed.runInInjectionContext(() => viewOnlyInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -64,7 +63,7 @@ describe('viewOnlyInterceptor', () => { const request = createRequest('/api/v2/projects/?page=1'); const handler = createHandler(); - runInInjectionContext(TestBed, () => viewOnlyInterceptor(request, handler)); + TestBed.runInInjectionContext(() => viewOnlyInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -77,7 +76,7 @@ describe('viewOnlyInterceptor', () => { const request = createRequest('/api/v2/files/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => viewOnlyInterceptor(request, handler)); + TestBed.runInInjectionContext(() => viewOnlyInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -90,7 +89,7 @@ describe('viewOnlyInterceptor', () => { const request = createRequest('/api/v2/nodes/?view_only=existing'); const handler = createHandler(); - runInInjectionContext(TestBed, () => viewOnlyInterceptor(request, handler)); + TestBed.runInInjectionContext(() => viewOnlyInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -103,7 +102,7 @@ describe('viewOnlyInterceptor', () => { const request = createRequest('/api/v2/users/'); const handler = createHandler(); - runInInjectionContext(TestBed, () => viewOnlyInterceptor(request, handler)); + TestBed.runInInjectionContext(() => viewOnlyInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -116,7 +115,7 @@ describe('viewOnlyInterceptor', () => { const request = createRequest('https://api.ror.org/v2'); const handler = createHandler(); - runInInjectionContext(TestBed, () => viewOnlyInterceptor(request, handler)); + TestBed.runInInjectionContext(() => viewOnlyInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; @@ -129,7 +128,7 @@ describe('viewOnlyInterceptor', () => { const request = createRequest('https://api.github.com/repos/user/repo'); const handler = createHandler(); - runInInjectionContext(TestBed, () => viewOnlyInterceptor(request, handler)); + TestBed.runInInjectionContext(() => viewOnlyInterceptor(request, handler)); expect(handler).toHaveBeenCalledTimes(1); const modifiedRequest = handler.mock.calls[0][0]; diff --git a/src/app/core/provider/application.initialization.provider.spec.ts b/src/app/core/provider/application.initialization.provider.spec.ts index feaa49742..394c9771d 100644 --- a/src/app/core/provider/application.initialization.provider.spec.ts +++ b/src/app/core/provider/application.initialization.provider.spec.ts @@ -1,5 +1,4 @@ import { HttpTestingController } from '@angular/common/http/testing'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { OSFConfigService } from '@core/services/osf-config.service'; @@ -10,7 +9,7 @@ import { ENVIRONMENT } from './environment.provider'; import { BrowserAgent } from '@newrelic/browser-agent/loaders/browser-agent'; import * as Sentry from '@sentry/angular'; import { NEW_RELIC_CONFIG_MOCK } from '@testing/mocks/new-relic.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; import { GoogleTagManagerConfiguration } from 'angular-google-tag-manager'; jest.mock('@sentry/angular', () => ({ @@ -29,8 +28,9 @@ describe('Provider: sentry', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [OSFTestingModule], providers: [ + provideOSFCore(), + provideOSFHttp(), { provide: OSFConfigService, useValue: configServiceMock, @@ -61,7 +61,7 @@ describe('Provider: sentry', () => { }); it('should initialize Sentry if DSN is provided', async () => { - await runInInjectionContext(TestBed, async () => { + await TestBed.runInInjectionContext(async () => { await initializeApplication()(); }); @@ -85,7 +85,7 @@ describe('Provider: sentry', () => { const environment = TestBed.inject(ENVIRONMENT); environment.sentryDsn = ''; environment.googleTagManagerId = ''; - await runInInjectionContext(TestBed, async () => { + await TestBed.runInInjectionContext(async () => { await initializeApplication()(); }); @@ -99,7 +99,7 @@ describe('Provider: sentry', () => { const environment = TestBed.inject(ENVIRONMENT); Object.assign(environment, NEW_RELIC_CONFIG_MOCK); - await runInInjectionContext(TestBed, async () => { + await TestBed.runInInjectionContext(async () => { await initializeApplication()(); }); @@ -111,7 +111,7 @@ describe('Provider: sentry', () => { const environment = TestBed.inject(ENVIRONMENT); environment.newRelicEnabled = false; - await runInInjectionContext(TestBed, async () => { + await TestBed.runInInjectionContext(async () => { await initializeApplication()(); }); diff --git a/src/app/core/provider/environment.provider.spec.ts b/src/app/core/provider/environment.provider.spec.ts index 9548e0b07..246d144fe 100644 --- a/src/app/core/provider/environment.provider.spec.ts +++ b/src/app/core/provider/environment.provider.spec.ts @@ -4,14 +4,14 @@ import { EnvironmentModel } from '@osf/shared/models/environment.model'; import { ENVIRONMENT } from './environment.provider'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('Provider: Environment', () => { let environment: EnvironmentModel; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [OSFTestingModule], + providers: [provideOSFCore()], }).compileComponents(); environment = TestBed.inject(ENVIRONMENT); diff --git a/src/app/core/provider/sentry.provider.spec.ts b/src/app/core/provider/sentry.provider.spec.ts index 82e18573d..b556e15b9 100644 --- a/src/app/core/provider/sentry.provider.spec.ts +++ b/src/app/core/provider/sentry.provider.spec.ts @@ -3,11 +3,12 @@ import { TestBed } from '@angular/core/testing'; import { SENTRY_PROVIDER, SENTRY_TOKEN } from './sentry.provider'; import * as Sentry from '@sentry/angular'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('Provider: Sentry', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [SENTRY_PROVIDER], + providers: [provideOSFCore(), SENTRY_PROVIDER], }); }); diff --git a/src/app/core/provider/window.provider.spec.ts b/src/app/core/provider/window.provider.spec.ts index 7f45aef72..893c1140a 100644 --- a/src/app/core/provider/window.provider.spec.ts +++ b/src/app/core/provider/window.provider.spec.ts @@ -1,15 +1,15 @@ -import { CommonModule } from '@angular/common'; import { PLATFORM_ID } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { WINDOW } from './window.provider'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('Provider: WINDOW', () => { describe('when running in the browser', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CommonModule], - providers: [{ provide: PLATFORM_ID, useValue: 'browser' }], + providers: [provideOSFCore(), { provide: PLATFORM_ID, useValue: 'browser' }], }); }); @@ -22,8 +22,7 @@ describe('Provider: WINDOW', () => { describe('when running on the server', () => { beforeEach(() => { TestBed.configureTestingModule({ - imports: [CommonModule], - providers: [{ provide: PLATFORM_ID, useValue: 'server' }], + providers: [provideOSFCore(), { provide: PLATFORM_ID, useValue: 'server' }], }); }); diff --git a/src/app/core/services/help-scout.service.spec.ts b/src/app/core/services/help-scout.service.spec.ts index c4f831ac6..2d0592cab 100644 --- a/src/app/core/services/help-scout.service.spec.ts +++ b/src/app/core/services/help-scout.service.spec.ts @@ -1,115 +1,98 @@ import { Store } from '@ngxs/store'; -import { signal } from '@angular/core'; +import { MockProvider } from 'ng-mocks'; + +import { ApplicationRef, PLATFORM_ID, signal, WritableSignal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { WINDOW } from '@core/provider/window.provider'; -import { UserSelectors } from '@core/store/user/user.selectors'; +import { UserSelectors } from '@core/store/user'; import { HelpScoutService } from './help-scout.service'; -describe('HelpScoutService', () => { - const storeMock: Partial = { - selectSignal: jest.fn().mockImplementation((selector) => { - if (selector === UserSelectors.isAuthenticated) { - return authSignal; - } - return signal(null); - }), - }; - let service: HelpScoutService; - let mockWindow: any; - const authSignal = signal(false); +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; - afterEach(() => { - jest.clearAllMocks(); - }); +interface DataLayer { + loggedIn: boolean; + resourceType: string | undefined; +} - describe('initialization - no dataLayer', () => { - beforeEach(() => { - mockWindow = {}; - TestBed.configureTestingModule({ - providers: [ - { provide: WINDOW, useValue: mockWindow }, - HelpScoutService, - { provide: Store, useValue: storeMock }, - ], - }); - - service = TestBed.inject(HelpScoutService); +describe('HelpScoutService', () => { + let service: HelpScoutService; + let store: Store; + let applicationRef: ApplicationRef; + let isAuthenticatedSignal: WritableSignal; + let windowMock: Window & { dataLayer?: DataLayer }; + + function setup(overrides?: { isBrowser?: boolean; initialAuth?: boolean; initialDataLayer?: DataLayer }) { + isAuthenticatedSignal = signal(overrides?.initialAuth ?? false); + windowMock = { dataLayer: overrides?.initialDataLayer } as Window & { dataLayer?: DataLayer }; + + TestBed.configureTestingModule({ + providers: [ + provideOSFCore(), + MockProvider(WINDOW, windowMock), + MockProvider(PLATFORM_ID, overrides?.isBrowser === false ? 'server' : 'browser'), + provideMockStore({ + signals: [{ selector: UserSelectors.isAuthenticated, value: isAuthenticatedSignal }], + }), + ], }); - it('should initialize dataLayer with default values', () => { - expect(mockWindow.dataLayer).toEqual({ - loggedIn: false, - resourceType: undefined, - }); - }); + store = TestBed.inject(Store); + applicationRef = TestBed.inject(ApplicationRef); + service = TestBed.inject(HelpScoutService); + } - it('should set the resourceType', () => { - service.setResourceType('project'); - expect(mockWindow.dataLayer.resourceType).toBe('project'); - }); + it('should create', () => { + setup(); + expect(service).toBeTruthy(); + expect(store).toBeTruthy(); + }); - it('should unset the resourceType', () => { - service.setResourceType('node'); - service.unsetResourceType(); - expect(mockWindow.dataLayer.resourceType).toBeUndefined(); + it('should initialize existing dataLayer in browser', () => { + setup({ + initialDataLayer: { loggedIn: true, resourceType: 'project' }, }); - - it('should set loggedIn to true or false', () => { - authSignal.set(true); - TestBed.flushEffects(); - expect(mockWindow.dataLayer.loggedIn).toBeTruthy(); - - authSignal.set(false); - TestBed.flushEffects(); - expect(mockWindow.dataLayer.loggedIn).toBeFalsy(); + expect(windowMock.dataLayer).toEqual({ + loggedIn: false, + resourceType: undefined, }); }); - describe('initialization - dataLayer', () => { - beforeEach(() => { - mockWindow = { - dataLayer: {}, - }; - TestBed.configureTestingModule({ - providers: [ - { provide: WINDOW, useValue: mockWindow }, - HelpScoutService, - { provide: Store, useValue: storeMock }, - ], - }); - - service = TestBed.inject(HelpScoutService); - }); - - it('should initialize dataLayer with default values', () => { - expect(mockWindow.dataLayer).toEqual({ - loggedIn: false, - resourceType: undefined, - }); + it('should create dataLayer when missing in browser', () => { + setup(); + expect(windowMock.dataLayer).toEqual({ + loggedIn: false, + resourceType: undefined, }); + }); - it('should set the resourceType', () => { - service.setResourceType('project'); - expect(mockWindow.dataLayer.resourceType).toBe('project'); - }); + it('should update loggedIn when authentication signal changes in browser', async () => { + setup(); + expect(windowMock.dataLayer?.loggedIn).toBe(false); + isAuthenticatedSignal.set(true); + await applicationRef.whenStable(); + expect(windowMock.dataLayer?.loggedIn).toBe(true); + }); - it('should unset the resourceType', () => { - service.setResourceType('node'); - service.unsetResourceType(); - expect(mockWindow.dataLayer.resourceType).toBeUndefined(); - }); + it('should set and unset resourceType in browser', () => { + setup(); + service.setResourceType('preprint'); + expect(windowMock.dataLayer?.resourceType).toBe('preprint'); + service.unsetResourceType(); + expect(windowMock.dataLayer?.resourceType).toBeUndefined(); + }); - it('should set loggedIn to true or false', () => { - authSignal.set(true); - TestBed.flushEffects(); - expect(mockWindow.dataLayer.loggedIn).toBeTruthy(); + it('should not initialize dataLayer on server', () => { + setup({ isBrowser: false }); + expect(windowMock.dataLayer).toBeUndefined(); + }); - authSignal.set(false); - TestBed.flushEffects(); - expect(mockWindow.dataLayer.loggedIn).toBeFalsy(); - }); + it('should not set resourceType on server', () => { + setup({ isBrowser: false }); + service.setResourceType('preprint'); + expect(windowMock.dataLayer).toBeUndefined(); }); }); diff --git a/src/app/features/admin-institutions/admin-institutions.component.spec.ts b/src/app/features/admin-institutions/admin-institutions.component.spec.ts index baf0fec8b..94100d33b 100644 --- a/src/app/features/admin-institutions/admin-institutions.component.spec.ts +++ b/src/app/features/admin-institutions/admin-institutions.component.spec.ts @@ -15,7 +15,7 @@ import { AdminInstitutionResourceTab } from './enums'; import { InstitutionsAdminSelectors } from './store'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -32,12 +32,9 @@ describe('AdminInstitutionsComponent', () => { mockRouter = RouterMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ - AdminInstitutionsComponent, - OSFTestingModule, - ...MockComponents(LoadingSpinnerComponent, SelectComponent), - ], + imports: [AdminInstitutionsComponent, ...MockComponents(LoadingSpinnerComponent, SelectComponent)], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(Router, mockRouter), provideMockStore({ diff --git a/src/app/features/admin-institutions/components/admin-table/admin-table.component.spec.ts b/src/app/features/admin-institutions/components/admin-table/admin-table.component.spec.ts index f0785d766..fedb567f2 100644 --- a/src/app/features/admin-institutions/components/admin-table/admin-table.component.spec.ts +++ b/src/app/features/admin-institutions/components/admin-table/admin-table.component.spec.ts @@ -10,7 +10,7 @@ import { StopPropagationDirective } from '@osf/shared/directives/stop-propagatio import { AdminTableComponent } from './admin-table.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AdminTableComponent', () => { let component: AdminTableComponent; @@ -21,11 +21,11 @@ describe('AdminTableComponent', () => { await TestBed.configureTestingModule({ imports: [ AdminTableComponent, - OSFTestingModule, MockComponent(CustomPaginatorComponent), MockPipe(DatePipe), MockDirective(StopPropagationDirective), ], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(AdminTableComponent); diff --git a/src/app/features/admin-institutions/components/filters-section/filters-section.component.spec.ts b/src/app/features/admin-institutions/components/filters-section/filters-section.component.spec.ts index eb0f8f556..8b1550e13 100644 --- a/src/app/features/admin-institutions/components/filters-section/filters-section.component.spec.ts +++ b/src/app/features/admin-institutions/components/filters-section/filters-section.component.spec.ts @@ -19,7 +19,7 @@ import { import { FiltersSectionComponent } from './filters-section.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('FiltersSectionComponent', () => { @@ -33,12 +33,9 @@ describe('FiltersSectionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - FiltersSectionComponent, - OSFTestingModule, - ...MockComponents(FilterChipsComponent, SearchFiltersComponent), - ], + imports: [FiltersSectionComponent, ...MockComponents(FilterChipsComponent, SearchFiltersComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: GlobalSearchSelectors.getFilters, value: mockFilters }, diff --git a/src/app/features/admin-institutions/components/request-access-error-dialog/request-access-error-dialog.component.spec.ts b/src/app/features/admin-institutions/components/request-access-error-dialog/request-access-error-dialog.component.spec.ts index 794fc8d6a..b830f499f 100644 --- a/src/app/features/admin-institutions/components/request-access-error-dialog/request-access-error-dialog.component.spec.ts +++ b/src/app/features/admin-institutions/components/request-access-error-dialog/request-access-error-dialog.component.spec.ts @@ -7,7 +7,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RequestAccessErrorDialogComponent } from './request-access-error-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RequestAccessErrorDialogComponent', () => { let component: RequestAccessErrorDialogComponent; @@ -23,8 +23,8 @@ describe('RequestAccessErrorDialogComponent', () => { } as any; await TestBed.configureTestingModule({ - imports: [RequestAccessErrorDialogComponent, OSFTestingModule, MockPipe(TranslatePipe)], - providers: [{ provide: DynamicDialogRef, useValue: mockDialogRef }], + imports: [RequestAccessErrorDialogComponent, MockPipe(TranslatePipe)], + providers: [provideOSFCore(), { provide: DynamicDialogRef, useValue: mockDialogRef }], }).compileComponents(); fixture = TestBed.createComponent(RequestAccessErrorDialogComponent); diff --git a/src/app/features/admin-institutions/dialogs/contact-dialog/contact-dialog.component.spec.ts b/src/app/features/admin-institutions/dialogs/contact-dialog/contact-dialog.component.spec.ts index 79baa5c72..22121a702 100644 --- a/src/app/features/admin-institutions/dialogs/contact-dialog/contact-dialog.component.spec.ts +++ b/src/app/features/admin-institutions/dialogs/contact-dialog/contact-dialog.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -7,14 +6,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ContactDialogComponent } from './contact-dialog.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ContactDialogComponent', () => { let component: ContactDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ContactDialogComponent, MockPipe(TranslatePipe)], + imports: [ContactDialogComponent], providers: [ + provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig, { data: { diff --git a/src/app/features/admin-institutions/dialogs/send-email-dialog/send-email-dialog.component.spec.ts b/src/app/features/admin-institutions/dialogs/send-email-dialog/send-email-dialog.component.spec.ts index 83c2999bb..ba628ad9b 100644 --- a/src/app/features/admin-institutions/dialogs/send-email-dialog/send-email-dialog.component.spec.ts +++ b/src/app/features/admin-institutions/dialogs/send-email-dialog/send-email-dialog.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProviders } from 'ng-mocks'; +import { MockProviders } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -7,14 +6,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SendEmailDialogComponent } from './send-email-dialog.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('SendEmailDialogComponent', () => { let component: SendEmailDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SendEmailDialogComponent, MockPipe(TranslatePipe)], - providers: [MockProviders(DynamicDialogRef, DynamicDialogConfig)], + imports: [SendEmailDialogComponent], + providers: [provideOSFCore(), MockProviders(DynamicDialogRef, DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(SendEmailDialogComponent); diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts index 7a6c4a133..3d74762b2 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts @@ -34,7 +34,7 @@ import { MOCK_ADMIN_INSTITUTIONS_PREPRINT_RESOURCE, MOCK_ADMIN_INSTITUTIONS_PREPRINT_RESOURCES, } from '@testing/mocks/admin-institutions.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; jest.mock('@osf/features/admin-institutions/helpers', () => ({ @@ -72,12 +72,9 @@ describe('InstitutionsPreprintsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - InstitutionsPreprintsComponent, - OSFTestingModule, - ...MockComponents(AdminTableComponent, FiltersSectionComponent), - ], + imports: [InstitutionsPreprintsComponent, ...MockComponents(AdminTableComponent, FiltersSectionComponent)], providers: [ + provideOSFCore(), MockProviders(Router), { provide: ActivatedRoute, diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts index d44fb3f96..4fb81b5ca 100644 --- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts @@ -39,7 +39,7 @@ import { MOCK_ADMIN_INSTITUTIONS_PROJECT_RESOURCES, } from '@testing/mocks/admin-institutions.mock'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; @@ -87,12 +87,9 @@ describe('InstitutionsProjectsComponent', () => { customDialogServiceMock = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); toastServiceMock = ToastServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ - InstitutionsProjectsComponent, - OSFTestingModule, - ...MockComponents(AdminTableComponent, FiltersSectionComponent), - ], + imports: [InstitutionsProjectsComponent, ...MockComponents(AdminTableComponent, FiltersSectionComponent)], providers: [ + provideOSFCore(), MockProviders(Router), { provide: ActivatedRoute, diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts index 3a50d46a5..e82a551e9 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts @@ -34,7 +34,7 @@ import { MOCK_ADMIN_INSTITUTIONS_REGISTRATION_RESOURCE, MOCK_ADMIN_INSTITUTIONS_REGISTRATION_RESOURCES, } from '@testing/mocks/admin-institutions.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; jest.mock('@osf/features/admin-institutions/helpers', () => ({ @@ -76,12 +76,9 @@ describe('InstitutionsRegistrationsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - InstitutionsRegistrationsComponent, - OSFTestingModule, - ...MockComponents(AdminTableComponent, FiltersSectionComponent), - ], + imports: [InstitutionsRegistrationsComponent, ...MockComponents(AdminTableComponent, FiltersSectionComponent)], providers: [ + provideOSFCore(), MockProviders(Router), { provide: ActivatedRoute, diff --git a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.spec.ts index 7067430c5..b18af25e1 100644 --- a/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-summary/institutions-summary.component.spec.ts @@ -30,7 +30,7 @@ import { MOCK_ADMIN_INSTITUTIONS_STORAGE_FILTERS, MOCK_ADMIN_INSTITUTIONS_SUMMARY_METRICS, } from '@testing/mocks/admin-institutions.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('InstitutionsSummaryComponent', () => { @@ -42,10 +42,10 @@ describe('InstitutionsSummaryComponent', () => { await TestBed.configureTestingModule({ imports: [ InstitutionsSummaryComponent, - OSFTestingModule, ...MockComponents(StatisticCardComponent, LoadingSpinnerComponent, DoughnutChartComponent, BarChartComponent), ], providers: [ + provideOSFCore(), { provide: ActivatedRoute, useValue: { diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts index 3051557ef..4b8a93fe3 100644 --- a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts @@ -22,7 +22,7 @@ import { MOCK_ADMIN_INSTITUTIONS_USERS, } from '@testing/mocks/admin-institutions.mock'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -35,8 +35,9 @@ describe('InstitutionsUsersComponent', () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); await TestBed.configureTestingModule({ - imports: [InstitutionsUsersComponent, ...MockComponents(AdminTableComponent, SelectComponent), OSFTestingModule], + imports: [InstitutionsUsersComponent, ...MockComponents(AdminTableComponent, SelectComponent)], providers: [ + provideOSFCore(), { provide: ActivatedRoute, useValue: { queryParams: of({}) }, diff --git a/src/app/features/analytics/analytics.component.spec.ts b/src/app/features/analytics/analytics.component.spec.ts index 5c9b6f80c..db6378d73 100644 --- a/src/app/features/analytics/analytics.component.spec.ts +++ b/src/app/features/analytics/analytics.component.spec.ts @@ -17,7 +17,7 @@ import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-l import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; import { MOCK_ANALYTICS_METRICS, MOCK_RELATED_COUNTS } from '@testing/mocks/analytics.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -41,8 +41,6 @@ describe('Component: Analytics', () => { .withData({ resourceType: undefined }) .build(); - jest.clearAllMocks(); - await TestBed.configureTestingModule({ imports: [ AnalyticsComponent, @@ -55,9 +53,9 @@ describe('Component: Analytics', () => { ViewOnlyLinkMessageComponent, SelectComponent ), - OSFTestingModule, ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: metricsSelector, value: metrics }, diff --git a/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts b/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts index 52f23acf8..50ae5b460 100644 --- a/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts +++ b/src/app/features/analytics/components/analytics-kpi/analytics-kpi.component.spec.ts @@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser'; import { AnalyticsKpiComponent } from './analytics-kpi.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AnalyticsKpiComponent', () => { let component: AnalyticsKpiComponent; @@ -11,7 +11,8 @@ describe('AnalyticsKpiComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AnalyticsKpiComponent, OSFTestingModule], + imports: [AnalyticsKpiComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(AnalyticsKpiComponent); diff --git a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts index f44753252..77cec6b7c 100644 --- a/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts +++ b/src/app/features/analytics/components/view-duplicates/view-duplicates.component.spec.ts @@ -22,7 +22,7 @@ import { DuplicatesSelectors } from '@osf/shared/stores/duplicates'; import { ViewDuplicatesComponent } from './view-duplicates.component'; import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -46,7 +46,6 @@ describe('Component: View Duplicates', () => { await TestBed.configureTestingModule({ imports: [ ViewDuplicatesComponent, - OSFTestingModule, ...MockComponents( SubHeaderComponent, TruncatedTextComponent, @@ -57,6 +56,7 @@ describe('Component: View Duplicates', () => { ), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: DuplicatesSelectors.getDuplicates, value: [] }, diff --git a/src/app/features/analytics/components/view-linked-projects/view-linked-projects.component.spec.ts b/src/app/features/analytics/components/view-linked-projects/view-linked-projects.component.spec.ts index 6ccab2562..70db3511e 100644 --- a/src/app/features/analytics/components/view-linked-projects/view-linked-projects.component.spec.ts +++ b/src/app/features/analytics/components/view-linked-projects/view-linked-projects.component.spec.ts @@ -19,7 +19,7 @@ import { LinkedProjectsSelectors } from '@osf/shared/stores/linked-projects'; import { ViewLinkedProjectsComponent } from './view-linked-projects.component'; import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -37,7 +37,6 @@ describe('Component: View Duplicates', () => { await TestBed.configureTestingModule({ imports: [ ViewLinkedProjectsComponent, - OSFTestingModule, ...MockComponents( SubHeaderComponent, TruncatedTextComponent, @@ -48,6 +47,7 @@ describe('Component: View Duplicates', () => { ), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: LinkedProjectsSelectors.getLinkedProjects, value: [] }, diff --git a/src/app/features/auth/pages/forgot-password/forgot-password.component.spec.ts b/src/app/features/auth/pages/forgot-password/forgot-password.component.spec.ts index 4ad56bbe3..5cacaf9d3 100644 --- a/src/app/features/auth/pages/forgot-password/forgot-password.component.spec.ts +++ b/src/app/features/auth/pages/forgot-password/forgot-password.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -8,14 +7,16 @@ import { TextInputComponent } from '@osf/shared/components/text-input/text-input import { ForgotPasswordComponent } from './forgot-password.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ForgotPasswordComponent', () => { let component: ForgotPasswordComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ForgotPasswordComponent, MockPipe(TranslatePipe), MockComponent(TextInputComponent)], - providers: [MockProvider(TranslateService), MockProvider(AuthService)], + imports: [ForgotPasswordComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore(), MockProvider(AuthService)], }).compileComponents(); fixture = TestBed.createComponent(ForgotPasswordComponent); diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts b/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts index c3eccb623..7e0d9e3ae 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts +++ b/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; @@ -10,7 +9,7 @@ import { AuthService } from '@core/services/auth.service'; import { ResetPasswordComponent } from '@osf/features/auth/pages'; import { PasswordInputHintComponent } from '@osf/shared/components/password-input-hint/password-input-hint.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResetPasswordComponent', () => { let component: ResetPasswordComponent; @@ -18,12 +17,8 @@ describe('ResetPasswordComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ResetPasswordComponent, MockComponent(PasswordInputHintComponent), MockPipe(TranslatePipe)], - providers: [ - TranslateServiceMock, - MockProvider(AuthService), - MockProvider(ActivatedRoute, { queryParams: of({}) }), - ], + imports: [ResetPasswordComponent, MockComponent(PasswordInputHintComponent)], + providers: [provideOSFCore(), MockProvider(AuthService), MockProvider(ActivatedRoute, { queryParams: of({}) })], }).compileComponents(); fixture = TestBed.createComponent(ResetPasswordComponent); diff --git a/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts b/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts index 1644aa564..3e1e27167 100644 --- a/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts +++ b/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; @@ -10,7 +9,7 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { SignUpComponent } from './sign-up.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SignUpComponent', () => { let component: SignUpComponent; @@ -18,9 +17,9 @@ describe('SignUpComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SignUpComponent, MockComponent(PasswordInputHintComponent), MockPipe(TranslatePipe)], + imports: [SignUpComponent, MockComponent(PasswordInputHintComponent)], providers: [ - TranslateServiceMock, + provideOSFCore(), MockProvider(ActivatedRoute), MockProvider(ToastService), MockProvider(AuthService), diff --git a/src/app/features/collections/collections.component.spec.ts b/src/app/features/collections/collections.component.spec.ts index c494c1408..07f176c75 100644 --- a/src/app/features/collections/collections.component.spec.ts +++ b/src/app/features/collections/collections.component.spec.ts @@ -3,6 +3,8 @@ import { By } from '@angular/platform-browser'; import { CollectionsComponent } from './collections.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('CollectionsComponent', () => { let component: CollectionsComponent; let fixture: ComponentFixture; @@ -10,6 +12,7 @@ describe('CollectionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CollectionsComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(CollectionsComponent); diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts index 3845970db..983c988a3 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component.spec.ts @@ -9,7 +9,7 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { AddToCollectionConfirmationDialogComponent } from './add-to-collection-confirmation-dialog.component'; import { MOCK_PROJECT } from '@testing/mocks/project.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('AddToCollectionConfirmationDialogComponent', () => { @@ -37,12 +37,13 @@ describe('AddToCollectionConfirmationDialogComponent', () => { createCollectionSubmission = jest.fn().mockReturnValue(of(null)); await TestBed.configureTestingModule({ - imports: [AddToCollectionConfirmationDialogComponent, OSFTestingModule], + imports: [AddToCollectionConfirmationDialogComponent], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRef }, { provide: ToastService, useValue: toastService }, { provide: DynamicDialogConfig, useValue: { data: configData } }, - provideMockStore({ signals: [] }), + provideMockStore(), ], }).compileComponents(); diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts b/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts index 499939be5..7514aab69 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.spec.ts @@ -12,6 +12,7 @@ import { SelectProjectStepComponent } from '@osf/features/collections/components import { AddToCollectionSteps } from '@osf/features/collections/enums'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { ToastService } from '@osf/shared/services/toast.service'; import { CollectionsSelectors } from '@shared/stores/collections'; import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; @@ -20,7 +21,7 @@ import { AddToCollectionComponent } from './add-to-collection.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; import { MOCK_PROJECT } from '@testing/mocks/project.mock'; import { MOCK_PROVIDER } from '@testing/mocks/provider.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -43,7 +44,6 @@ describe('AddToCollectionComponent', () => { await TestBed.configureTestingModule({ imports: [ AddToCollectionComponent, - OSFTestingModule, ...MockComponents( LoadingSpinnerComponent, SelectProjectStepComponent, @@ -53,9 +53,11 @@ describe('AddToCollectionComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(Router, mockRouter), MockProvider(CustomDialogService, mockCustomDialogService), + MockProvider(ToastService), provideMockStore({ signals: [ { selector: CollectionsSelectors.getCollectionProviderLoading, value: false }, diff --git a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts index 5f1bb4023..f83a196e9 100644 --- a/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/collection-metadata-step/collection-metadata-step.component.spec.ts @@ -10,7 +10,7 @@ import { CollectionsSelectors } from '@shared/stores/collections'; import { CollectionMetadataStepComponent } from './collection-metadata-step.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe.skip('CollectionMetadataStepComponent', () => { @@ -19,8 +19,9 @@ describe.skip('CollectionMetadataStepComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CollectionMetadataStepComponent, OSFTestingModule, MockComponents(StepPanel, Step, StepItem)], + imports: [CollectionMetadataStepComponent, MockComponents(StepPanel, Step, StepItem)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: CollectionsSelectors.getCollectionProvider, value: null }, diff --git a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.spec.ts b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.spec.ts index 6c77035f2..71df8c762 100644 --- a/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/project-contributors-step/project-contributors-step.component.spec.ts @@ -12,7 +12,7 @@ import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; import { ProjectContributorsStepComponent } from './project-contributors-step.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -31,12 +31,9 @@ describe.skip('ProjectContributorsStepComponent', () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ - ProjectContributorsStepComponent, - OSFTestingModule, - ...MockComponents(ContributorsTableComponent, InfoIconComponent), - ], + imports: [ProjectContributorsStepComponent, ...MockComponents(ContributorsTableComponent, InfoIconComponent)], providers: [ + provideOSFCore(), MockProvider(ToastService, toastServiceMock), MockProvider(CustomConfirmationService, customConfirmationServiceMock), MockProvider(CustomDialogService, mockCustomDialogService), diff --git a/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.spec.ts b/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.spec.ts index 9bb058c3c..95fab0066 100644 --- a/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/project-metadata-step/project-metadata-step.component.spec.ts @@ -13,7 +13,7 @@ import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; import { ProjectMetadataStepComponent } from './project-metadata-step.component'; import { MOCK_PROJECT } from '@testing/mocks/project.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; @@ -28,11 +28,11 @@ describe.skip('ProjectMetadataStepComponent', () => { await TestBed.configureTestingModule({ imports: [ ProjectMetadataStepComponent, - OSFTestingModule, ...MockComponents(TagsInputComponent, TextInputComponent, TruncatedTextComponent), MockPipe(InterpolatePipe), ], providers: [ + provideOSFCore(), MockProvider(ToastService, toastServiceMock), provideMockStore({ signals: [ diff --git a/src/app/features/collections/components/add-to-collection/remove-from-collection-dialog/remove-from-collection-dialog.component.spec.ts b/src/app/features/collections/components/add-to-collection/remove-from-collection-dialog/remove-from-collection-dialog.component.spec.ts index 9cff233f8..73ec6a0df 100644 --- a/src/app/features/collections/components/add-to-collection/remove-from-collection-dialog/remove-from-collection-dialog.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/remove-from-collection-dialog/remove-from-collection-dialog.component.spec.ts @@ -4,7 +4,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RemoveFromCollectionDialogComponent } from './remove-from-collection-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RemoveFromCollectionDialogComponent', () => { let component: RemoveFromCollectionDialogComponent; @@ -15,8 +15,9 @@ describe('RemoveFromCollectionDialogComponent', () => { dialogRef = { close: jest.fn() } as any; await TestBed.configureTestingModule({ - imports: [RemoveFromCollectionDialogComponent, OSFTestingModule], + imports: [RemoveFromCollectionDialogComponent], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRef }, { provide: DynamicDialogConfig, diff --git a/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.spec.ts b/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.spec.ts index fe144e0bf..5837d75fc 100644 --- a/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.spec.ts +++ b/src/app/features/collections/components/add-to-collection/select-project-step/select-project-step.component.spec.ts @@ -11,7 +11,7 @@ import { SelectProjectStepComponent } from './select-project-step.component'; import { MOCK_PROJECT } from '@testing/mocks/project.mock'; import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; @@ -26,8 +26,9 @@ describe.skip('SelectProjectStepComponent', () => { toastServiceMock = ToastServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [SelectProjectStepComponent, OSFTestingModule, ...MockComponents(ProjectSelectorComponent)], + imports: [SelectProjectStepComponent, ...MockComponents(ProjectSelectorComponent)], providers: [ + provideOSFCore(), MockProvider(ToastService, toastServiceMock), provideMockStore({ signals: [ diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts index 1b51c738e..c887e126c 100644 --- a/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts +++ b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts @@ -14,7 +14,7 @@ import { CollectionsSelectors } from '@shared/stores/collections'; import { CollectionsDiscoverComponent } from './collections-discover.component'; import { MOCK_PROVIDER } from '@testing/mocks/provider.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -36,9 +36,9 @@ describe('CollectionsDiscoverComponent', () => { imports: [ CollectionsDiscoverComponent, ...MockComponents(SearchInputComponent, CollectionsMainContentComponent, LoadingSpinnerComponent), - OSFTestingModule, ], providers: [ + provideOSFCore(), MockProvider(ToastService, toastServiceMock), MockProvider(CustomDialogService, mockCustomDialogService), MockProvider(ActivatedRoute, mockRoute), diff --git a/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.spec.ts b/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.spec.ts index 9c0f8d0fb..75ea421ba 100644 --- a/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.spec.ts +++ b/src/app/features/collections/components/collections-filter-chips/collections-filter-chips.component.spec.ts @@ -5,7 +5,7 @@ import { CollectionsSelectors } from '@shared/stores/collections'; import { CollectionsFilterChipsComponent } from './collections-filter-chips.component'; import { MOCK_COLLECTIONS_ACTIVE_FILTERS } from '@testing/mocks/collections-filters.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('CollectionsFilterChipsComponent', () => { @@ -16,8 +16,9 @@ describe('CollectionsFilterChipsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CollectionsFilterChipsComponent, OSFTestingModule], + imports: [CollectionsFilterChipsComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: CollectionsSelectors.getAllSelectedFilters, value: mockActiveFilters }], }), diff --git a/src/app/features/collections/components/collections-filters/collections-filters.component.spec.ts b/src/app/features/collections/components/collections-filters/collections-filters.component.spec.ts index 96d9be5a2..eaf06c37d 100644 --- a/src/app/features/collections/components/collections-filters/collections-filters.component.spec.ts +++ b/src/app/features/collections/components/collections-filters/collections-filters.component.spec.ts @@ -8,7 +8,7 @@ import { MOCK_COLLECTIONS_FILTERS_OPTIONS, MOCK_COLLECTIONS_SELECTED_FILTERS, } from '@testing/mocks/collections-filters.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('CollectionsFiltersComponent', () => { @@ -20,8 +20,9 @@ describe('CollectionsFiltersComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CollectionsFiltersComponent, OSFTestingModule], + imports: [CollectionsFiltersComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: CollectionsSelectors.getAllFiltersOptions, value: mockFiltersOptions }, diff --git a/src/app/features/collections/components/collections-help-dialog/collections-help-dialog.component.spec.ts b/src/app/features/collections/components/collections-help-dialog/collections-help-dialog.component.spec.ts index 5481c11d8..12fa9b028 100644 --- a/src/app/features/collections/components/collections-help-dialog/collections-help-dialog.component.spec.ts +++ b/src/app/features/collections/components/collections-help-dialog/collections-help-dialog.component.spec.ts @@ -1,17 +1,17 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CollectionsHelpDialogComponent } from './collections-help-dialog.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('CollectionsHelpDialogComponent', () => { let component: CollectionsHelpDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CollectionsHelpDialogComponent, MockPipe(TranslatePipe)], + imports: [CollectionsHelpDialogComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(CollectionsHelpDialogComponent); diff --git a/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts b/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts index 9f54b0fdb..152890ecb 100644 --- a/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts +++ b/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts @@ -13,7 +13,7 @@ import { CollectionsMainContentComponent } from './collections-main-content.comp import { MOCK_COLLECTIONS_SELECTED_FILTERS } from '@testing/mocks/collections-filters.mock'; import { MOCK_COLLECTION_SUBMISSIONS } from '@testing/mocks/collections-submissions.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('CollectionsMainContentComponent', () => { @@ -32,9 +32,9 @@ describe('CollectionsMainContentComponent', () => { CollectionsFiltersComponent, CollectionsSearchResultsComponent ), - OSFTestingModule, ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: CollectionsSelectors.getSortBy, value: 'date' }, diff --git a/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.spec.ts b/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.spec.ts index 96895956c..2490bdfa7 100644 --- a/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.spec.ts +++ b/src/app/features/collections/components/collections-search-result-card/collections-search-result-card.component.spec.ts @@ -9,7 +9,7 @@ import { CollectionSubmissionWithGuid } from '@osf/shared/models/collections/col import { CollectionsSearchResultCardComponent } from './collections-search-result-card.component'; import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('CollectionsSearchResultCardComponent', () => { let component: CollectionsSearchResultCardComponent; @@ -20,7 +20,8 @@ describe('CollectionsSearchResultCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CollectionsSearchResultCardComponent, OSFTestingModule, MockComponent(ContributorsListComponent)], + imports: [CollectionsSearchResultCardComponent, MockComponent(ContributorsListComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(CollectionsSearchResultCardComponent); diff --git a/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts b/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts index e4030a3f4..4944150d6 100644 --- a/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts +++ b/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts @@ -9,7 +9,7 @@ import { CollectionsSelectors } from '@shared/stores/collections'; import { CollectionsSearchResultsComponent } from './collections-search-results.component'; import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('CollectionsSearchResultsComponent', () => { @@ -27,9 +27,9 @@ describe('CollectionsSearchResultsComponent', () => { imports: [ CollectionsSearchResultsComponent, ...MockComponents(CustomPaginatorComponent, CollectionsSearchResultCardComponent), - OSFTestingModule, ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: CollectionsSelectors.getCollectionSubmissionsSearchResult, value: mockSearchResults }, diff --git a/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts b/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts index 982f3d27e..5dbf96013 100644 --- a/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts +++ b/src/app/features/contributors/components/create-view-link-dialog/create-view-link-dialog.component.spec.ts @@ -12,7 +12,7 @@ import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { CreateViewLinkDialogComponent } from './create-view-link-dialog.component'; import { MOCK_RESOURCE_INFO, MOCK_RESOURCE_WITH_CHILDREN } from '@testing/mocks/resource.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('Component: Create View Link Dialog', () => { @@ -33,10 +33,10 @@ describe('Component: Create View Link Dialog', () => { await TestBed.configureTestingModule({ imports: [ CreateViewLinkDialogComponent, - OSFTestingModule, ...MockComponents(TextInputComponent, LoadingSpinnerComponent, ComponentCheckboxItemComponent), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { diff --git a/src/app/features/contributors/contributors.component.spec.ts b/src/app/features/contributors/contributors.component.spec.ts index d47ee0199..e25d8ea75 100644 --- a/src/app/features/contributors/contributors.component.spec.ts +++ b/src/app/features/contributors/contributors.component.spec.ts @@ -1,48 +1,106 @@ -import { MockComponents, MockProvider } from 'ng-mocks'; +import { Store } from '@ngxs/store'; -import { ConfirmationService } from 'primeng/api'; -import { DialogService } from 'primeng/dynamicdialog'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute, Router } from '@angular/router'; +import { UserSelectors } from '@core/store/user'; import { ContributorsTableComponent, RequestAccessTableComponent } from '@osf/shared/components/contributors'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { ViewOnlyTableComponent } from '@osf/shared/components/view-only-table/view-only-table.component'; import { ContributorPermission } from '@osf/shared/enums/contributors/contributor-permission.enum'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; -import { ContributorsSelectors } from '@osf/shared/stores/contributors'; -import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; +import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; +import { ToastService } from '@osf/shared/services/toast.service'; +import { + ContributorsSelectors, + GetAllContributors, + LoadMoreContributors, + ResetContributorsState, + UpdateBibliographyFilter, + UpdateContributorsSearchValue, + UpdatePermissionFilter, +} from '@osf/shared/stores/contributors'; +import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource'; import { ViewOnlyLinkSelectors } from '@osf/shared/stores/view-only-links'; -import { ContributorModel } from '@shared/models/contributors/contributor.model'; import { ContributorsComponent } from './contributors.component'; -import { MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY } from '@testing/mocks/contributors.mock'; -import { MOCK_RESOURCE_INFO } from '@testing/mocks/resource.mock'; -import { MOCK_PAGINATED_VIEW_ONLY_LINKS } from '@testing/mocks/view-only-link.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; - -describe('Component: Contributors', () => { +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { + CustomConfirmationServiceMock, + CustomConfirmationServiceMockType, +} from '@testing/providers/custom-confirmation-provider.mock'; +import { CustomDialogServiceMock, CustomDialogServiceMockType } from '@testing/providers/custom-dialog-provider.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + selectorOverrides?: SignalOverride[]; +} + +describe('ContributorsComponent', () => { let component: ContributorsComponent; let fixture: ComponentFixture; - let customConfirmationServiceMock: ReturnType; - - const mockContributors: ContributorModel[] = [MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY]; - - beforeEach(async () => { - jest.useFakeTimers(); - - customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); - - await TestBed.configureTestingModule({ + let store: Store; + let toastService: ToastServiceMockType; + let customDialogService: CustomDialogServiceMockType; + let customConfirmationService: CustomConfirmationServiceMockType; + let mockRouter: RouterMockType; + + const defaultSignals: SignalOverride[] = [ + { selector: ViewOnlyLinkSelectors.getViewOnlyLinks, value: [] }, + { + selector: CurrentResourceSelectors.getResourceDetails, + value: { id: 'resource-id', title: 'Resource title', rootParentId: null, parent: null }, + }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }, + { selector: ContributorsSelectors.getContributors, value: [] }, + { selector: ContributorsSelectors.getRequestAccessList, value: [] }, + { selector: ContributorsSelectors.areRequestAccessListLoading, value: false }, + { selector: ContributorsSelectors.isContributorsLoading, value: false }, + { selector: ContributorsSelectors.getContributorsTotalCount, value: 0 }, + { selector: ViewOnlyLinkSelectors.isViewOnlyLinksLoading, value: false }, + { selector: CurrentResourceSelectors.hasResourceAdminAccess, value: false }, + { selector: CurrentResourceSelectors.resourceAccessRequestEnabled, value: false }, + { selector: UserSelectors.getCurrentUser, value: { id: 'user-1' } }, + { selector: ContributorsSelectors.getContributorsPageSize, value: 10 }, + { selector: ContributorsSelectors.isContributorsLoadingMore, value: false }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const routeBuilder = ActivatedRouteMockBuilder.create().withData({ resourceType: ResourceType.Project }); + if (overrides.routeParams) { + routeBuilder.withParams(overrides.routeParams); + } + if (overrides.hasParent === false) { + routeBuilder.withNoParent(); + } + const mockRoute = routeBuilder.build(); + + mockRouter = RouterMockBuilder.create().build(); + toastService = ToastServiceMock.simple(); + customDialogService = CustomDialogServiceMock.simple(); + customConfirmationService = CustomConfirmationServiceMock.simple(); + const loaderService = new LoaderServiceMock(); + + TestBed.configureTestingModule({ imports: [ ContributorsComponent, - OSFTestingModule, - MockComponents( + ...MockComponents( SearchInputComponent, ContributorsTableComponent, RequestAccessTableComponent, @@ -50,141 +108,140 @@ describe('Component: Contributors', () => { ), ], providers: [ - MockProvider(DialogService, { - open: jest.fn().mockReturnValue({ onClose: of({}) }), - }), - MockProvider(CustomConfirmationService, customConfirmationServiceMock), - MockProvider(ConfirmationService, {}), + provideOSFCore(), + MockProvider(ActivatedRoute, mockRoute), + MockProvider(Router, mockRouter), + MockProvider(ToastService, toastService), + MockProvider(CustomDialogService, customDialogService), + MockProvider(CustomConfirmationService, customConfirmationService), + MockProvider(LoaderService, loaderService), provideMockStore({ - signals: [ - { selector: ContributorsSelectors.getContributors, value: mockContributors }, - { selector: ContributorsSelectors.isContributorsLoading, value: false }, - { selector: ViewOnlyLinkSelectors.getViewOnlyLinks, value: MOCK_PAGINATED_VIEW_ONLY_LINKS }, - { selector: ViewOnlyLinkSelectors.isViewOnlyLinksLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceDetails, value: MOCK_RESOURCE_INFO }, - ], + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(ContributorsComponent); component = fixture.componentInstance; - fixture.detectChanges(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); + } it('should create', () => { + setup({ routeParams: { id: 'resource-id' } }); expect(component).toBeTruthy(); }); - it('should update search value with debounce', () => { - expect(() => component.searchControl.setValue('test search')).not.toThrow(); + it('should dispatch resource and contributors actions on init', () => { + setup({ routeParams: { id: 'resource-id' } }); + component.ngOnInit(); - jest.advanceTimersByTime(600); - - expect(component.searchControl.value).toBe('test search'); + expect(store.dispatch).toHaveBeenCalledWith(new GetResourceDetails('resource-id', ResourceType.Project)); + expect(store.dispatch).toHaveBeenCalledWith(new GetAllContributors('resource-id', ResourceType.Project)); }); - it('should handle null search value', () => { - expect(() => component.searchControl.setValue(null)).not.toThrow(); - - jest.advanceTimersByTime(600); - - expect(component.searchControl.value).toBe(null); - }); + it('should not dispatch init actions when resource id is missing', () => { + setup(); + component.ngOnInit(); - it('should update permission filter', () => { - expect(() => component.onPermissionChange(ContributorPermission.Read)).not.toThrow(); + expect(store.dispatch).not.toHaveBeenCalledWith(new GetResourceDetails('resource-id', ResourceType.Project)); + expect(store.dispatch).not.toHaveBeenCalledWith(new GetAllContributors('resource-id', ResourceType.Project)); }); - it('should update bibliography filter', () => { - expect(() => component.onBibliographyChange(true)).not.toThrow(); - }); + it('should dispatch search update after debounce', () => { + jest.useFakeTimers(); + setup({ routeParams: { id: 'resource-id' } }); + component.ngOnInit(); + (store.dispatch as jest.Mock).mockClear(); - it('should create view link', () => { - const mockDialogRef = { - onClose: of({ name: 'Test Link', anonymous: false }), - }; - jest.spyOn(component.customDialogService, 'open').mockReturnValue(mockDialogRef as any); - jest.spyOn(component.toastService, 'showSuccess'); + component.searchControl.setValue('john'); + jest.advanceTimersByTime(500); - expect(() => component.createViewLink()).not.toThrow(); - expect(component.customDialogService.open).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new UpdateContributorsSearchValue('john')); + jest.useRealTimers(); }); - it('should delete view link with confirmation', () => { - jest.spyOn(component.customConfirmationService, 'confirmDelete'); - jest.spyOn(component.toastService, 'showSuccess'); + it('should dispatch permission and bibliography filter actions', () => { + setup({ routeParams: { id: 'resource-id' } }); + (store.dispatch as jest.Mock).mockClear(); - component.deleteLinkItem(MOCK_PAGINATED_VIEW_ONLY_LINKS.items[0]); + component.onPermissionChange(ContributorPermission.Admin); + component.onBibliographyChange(true); - expect(component.customConfirmationService.confirmDelete).toHaveBeenCalledWith({ - headerKey: 'myProjects.settings.delete.title', - headerParams: { name: MOCK_PAGINATED_VIEW_ONLY_LINKS.items[0].name }, - messageKey: 'myProjects.settings.delete.message', - onConfirm: expect.any(Function), - }); + expect(store.dispatch).toHaveBeenCalledWith(new UpdatePermissionFilter(ContributorPermission.Admin)); + expect(store.dispatch).toHaveBeenCalledWith(new UpdateBibliographyFilter(true)); }); - it('should handle view link deletion confirmation', () => { - let confirmCallback: () => void; - jest.spyOn(component.customConfirmationService, 'confirmDelete').mockImplementation((options) => { - confirmCallback = options.onConfirm; + it('should cancel edited contributors to initial state', () => { + setup({ + routeParams: { id: 'resource-id' }, + selectorOverrides: [ + { + selector: ContributorsSelectors.getContributors, + value: [ + { + id: 'c1', + userId: 'u1', + fullName: 'Jane Doe', + }, + ], + }, + ], }); - jest.spyOn(component.toastService, 'showSuccess'); - - component.deleteLinkItem(MOCK_PAGINATED_VIEW_ONLY_LINKS.items[0]); - - expect(() => confirmCallback!()).not.toThrow(); - }); - - it('should detect changes correctly', () => { - expect(component.hasChanges).toBe(false); - - const modifiedContributors = [...mockContributors]; - modifiedContributors[0].permission = ContributorPermission.Write; - (component.contributors as any).set(modifiedContributors); - - expect((component.contributors as any)()).toEqual(modifiedContributors); - }); - - it('should cancel changes', () => { - const modifiedContributors = [...mockContributors]; - modifiedContributors[0].permission = ContributorPermission.Write; - (component.contributors as any).set(modifiedContributors); + component.contributors.set([]); component.cancel(); - expect((component.contributors as any)()).toEqual(mockContributors); + expect(component.contributors()).toEqual([ + { + id: 'c1', + userId: 'u1', + fullName: 'Jane Doe', + }, + ]); }); - it('should save changes', () => { - jest.spyOn(component.toastService, 'showSuccess'); + it('should dispatch load more contributors action', () => { + setup({ routeParams: { id: 'resource-id' } }); + (store.dispatch as jest.Mock).mockClear(); - const modifiedContributors = [...mockContributors]; - modifiedContributors[0].permission = ContributorPermission.Write; - (component.contributors as any).set(modifiedContributors); + component.loadMoreContributors(); - expect(() => component.save()).not.toThrow(); + expect(store.dispatch).toHaveBeenCalledWith(new LoadMoreContributors('resource-id', ResourceType.Project)); }); - it('should handle save errors', () => { - jest.spyOn(component.toastService, 'showError'); + it('should dispatch bulk update and show success toast on save', () => { + setup({ + routeParams: { id: 'resource-id' }, + selectorOverrides: [ + { + selector: ContributorsSelectors.getContributors, + value: [ + { + id: 'c1', + userId: 'u1', + fullName: 'Jane Doe', + }, + ], + }, + ], + }); + (store.dispatch as jest.Mock).mockReturnValue(of(true)); + (store.dispatch as jest.Mock).mockClear(); - const modifiedContributors = [...mockContributors]; - modifiedContributors[0].permission = ContributorPermission.Write; - (component.contributors as any).set(modifiedContributors); + component.save(); - expect(() => component.save()).not.toThrow(); + expect(store.dispatch).toHaveBeenCalled(); + expect(toastService.showSuccess).toHaveBeenCalledWith( + 'project.contributors.toastMessages.multipleUpdateSuccessMessage' + ); }); - it('should update contributors when initialContributors changes', () => { - const newContributors = [...mockContributors, MOCK_CONTRIBUTOR]; + it('should dispatch reset action on destroy', () => { + setup({ routeParams: { id: 'resource-id' } }); + (store.dispatch as jest.Mock).mockClear(); + + fixture.destroy(); - expect(() => (component.contributors as any).set(newContributors)).not.toThrow(); - expect((component.contributors as any)()).toEqual(newContributors); + expect(store.dispatch).toHaveBeenCalledWith(new ResetContributorsState()); }); }); diff --git a/src/app/features/contributors/contributors.component.ts b/src/app/features/contributors/contributors.component.ts index 9be8e6435..95d488912 100644 --- a/src/app/features/contributors/contributors.component.ts +++ b/src/app/features/contributors/contributors.component.ts @@ -88,14 +88,14 @@ import { ResourceInfoModel } from './models'; selector: 'osf-contributors', imports: [ Button, - SearchInputComponent, Select, - TranslatePipe, - FormsModule, TableModule, + FormsModule, + SearchInputComponent, ContributorsTableComponent, RequestAccessTableComponent, ViewOnlyTableComponent, + TranslatePipe, ], templateUrl: './contributors.component.html', styleUrl: './contributors.component.scss', diff --git a/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts b/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts index 88f0214fa..3708eb0e2 100644 --- a/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts +++ b/src/app/features/files/components/confirm-move-file-dialog/confirm-move-file-dialog.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -16,7 +15,7 @@ import { FilesSelectors } from '../../store'; import { ConfirmMoveFileDialogComponent } from './confirm-move-file-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMock } from '@testing/providers/custom-confirmation-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMock } from '@testing/providers/toast-provider.mock'; @@ -42,11 +41,10 @@ describe('ConfirmConfirmMoveFileDialogComponent', () => { await TestBed.configureTestingModule({ imports: [ ConfirmMoveFileDialogComponent, - OSFTestingModule, ...MockComponents(IconComponent, LoadingSpinnerComponent, FileSelectDestinationComponent), - MockPipe(TranslatePipe), ], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRefMock }, { provide: DynamicDialogConfig, useValue: dialogConfigMock }, { provide: FilesService, useValue: mockFilesService }, diff --git a/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts b/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts index 5b69983db..9c17be502 100644 --- a/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts +++ b/src/app/features/files/components/create-folder-dialog/create-folder-dialog.component.spec.ts @@ -3,33 +3,29 @@ import { MockComponent } from 'ng-mocks'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; import { InputLimits } from '@osf/shared/constants/input-limits.const'; import { CreateFolderDialogComponent } from './create-folder-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; describe('CreateFolderDialogComponent', () => { let component: CreateFolderDialogComponent; let fixture: ComponentFixture; - let dialogRef: jest.Mocked; + let dialogRef: DynamicDialogRef; - beforeEach(async () => { - const dialogRefMock = { - close: jest.fn(), - }; - - await TestBed.configureTestingModule({ - imports: [CreateFolderDialogComponent, ReactiveFormsModule, OSFTestingModule, MockComponent(TextInputComponent)], - providers: [{ provide: DynamicDialogRef, useValue: dialogRefMock }], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CreateFolderDialogComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore(), provideDynamicDialogRefMock()], + }); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(CreateFolderDialogComponent); component = fixture.componentInstance; - dialogRef = TestBed.inject(DynamicDialogRef) as jest.Mocked; fixture.detectChanges(); }); @@ -37,80 +33,13 @@ describe('CreateFolderDialogComponent', () => { expect(component).toBeTruthy(); }); - it('should initialize with correct properties', () => { + it('should expose name limits from shared input limits', () => { expect(component.nameLimit).toBe(InputLimits.name.maxLength); expect(component.nameMinLength).toBe(InputLimits.name.minLength); - expect(component.folderForm).toBeDefined(); - expect(component.dialogRef).toBeDefined(); - }); - - it('should initialize form with correct validators', () => { - const nameControl = component.folderForm.get('name'); - expect(nameControl).toBeDefined(); - expect(nameControl?.value).toBe(''); - }); - - it('should be invalid when name is empty', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue(''); - expect(nameControl?.hasError('required')).toBe(true); - expect(component.folderForm.invalid).toBe(true); - }); - - it('should be invalid when name is only whitespace', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue(' '); - expect(nameControl?.hasError('required')).toBe(true); - expect(component.folderForm.invalid).toBe(true); - }); - - it('should be valid when name has content', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('valid-folder-name'); - expect(nameControl?.hasError('required')).toBe(false); - }); - - it('should be valid when name does not contain forbidden characters', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('valid-folder-name'); - expect(nameControl?.hasError('forbiddenCharacters')).toBe(false); - }); - - it('should be invalid when name ends with period', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('folder-name.'); - expect(nameControl?.hasError('periodAtEnd')).toBe(true); - }); - - it('should be valid when name does not end with period', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('folder-name'); - expect(nameControl?.hasError('periodAtEnd')).toBe(false); - }); - - it('should be valid when name has period in the middle', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('folder.name'); - expect(nameControl?.hasError('periodAtEnd')).toBe(false); - }); - - it('should be invalid when name has multiple validation errors', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('folder@name.'); - expect(nameControl?.hasError('forbiddenCharacters')).toBe(true); - expect(nameControl?.hasError('periodAtEnd')).toBe(true); - expect(component.folderForm.invalid).toBe(true); - }); - - it('should be valid when name passes all validations', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('valid-folder-name'); - expect(component.folderForm.valid).toBe(true); }); it('should not close dialog when form is invalid', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue(''); + component.folderForm.controls.name.setValue(''); component.onSubmit(); @@ -118,60 +47,26 @@ describe('CreateFolderDialogComponent', () => { }); it('should close dialog with trimmed folder name when form is valid', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue(' valid-folder-name '); + component.folderForm.controls.name.setValue(' New Folder '); component.onSubmit(); - expect(dialogRef.close).toHaveBeenCalledWith('valid-folder-name'); + expect(dialogRef.close).toHaveBeenCalledWith('New Folder'); }); - it('should close dialog with folder name when form is valid and no trimming needed', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('valid-folder-name'); + it('should not close dialog when value contains forbidden characters', () => { + component.folderForm.controls.name.setValue('Invalid/Name'); component.onSubmit(); - expect(dialogRef.close).toHaveBeenCalledWith('valid-folder-name'); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should not close dialog when trimmed name is empty', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue(' '); + it('should not close dialog when value ends with period', () => { + component.folderForm.controls.name.setValue('Folder.'); component.onSubmit(); expect(dialogRef.close).not.toHaveBeenCalled(); }); - - it('should close dialog without result when cancel button is clicked', () => { - component.dialogRef.close(); - - expect(dialogRef.close).toHaveBeenCalledWith(); - }); - - it('should update form validity when name control changes', () => { - const nameControl = component.folderForm.get('name'); - - nameControl?.setValue(''); - expect(component.folderForm.invalid).toBe(true); - - nameControl?.setValue('valid-folder-name'); - expect(component.folderForm.valid).toBe(true); - - nameControl?.setValue('invalid@name.'); - expect(component.folderForm.invalid).toBe(true); - }); - - it('should handle form submission via ngSubmit', () => { - const nameControl = component.folderForm.get('name'); - nameControl?.setValue('valid-folder-name'); - - const form = fixture.nativeElement.querySelector('form'); - const submitEvent = new Event('submit'); - - form.dispatchEvent(submitEvent); - - expect(dialogRef.close).toHaveBeenCalledWith('valid-folder-name'); - }); }); diff --git a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts index 7084c6e4b..38b524936 100644 --- a/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts +++ b/src/app/features/files/components/edit-file-metadata-dialog/edit-file-metadata-dialog.component.spec.ts @@ -1,13 +1,12 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; import { OsfFileCustomMetadata } from '@osf/features/files/models'; import { EditFileMetadataDialogComponent } from './edit-file-metadata-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EditFileMetadataDialogComponent', () => { let component: EditFileMetadataDialogComponent; @@ -33,8 +32,9 @@ describe('EditFileMetadataDialogComponent', () => { }; await TestBed.configureTestingModule({ - imports: [EditFileMetadataDialogComponent, ReactiveFormsModule, OSFTestingModule], + imports: [EditFileMetadataDialogComponent], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRefMock }, { provide: DynamicDialogConfig, useValue: dialogConfigMock }, ], diff --git a/src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts b/src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts index 01307c6eb..83717e756 100644 --- a/src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts +++ b/src/app/features/files/components/file-browser-info/file-browser-info.component.spec.ts @@ -6,7 +6,7 @@ import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { FileBrowserInfoComponent } from './file-browser-info.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('FileBrowserInfoComponent', () => { let component: FileBrowserInfoComponent; @@ -24,8 +24,9 @@ describe('FileBrowserInfoComponent', () => { }; await TestBed.configureTestingModule({ - imports: [FileBrowserInfoComponent, OSFTestingModule], + imports: [FileBrowserInfoComponent], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRefMock }, { provide: DynamicDialogConfig, useValue: dialogConfigMock }, ], diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts b/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts index f4b60c026..b01378e78 100644 --- a/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts +++ b/src/app/features/files/components/file-keywords/file-keywords.component.spec.ts @@ -5,7 +5,7 @@ import { FilesSelectors } from '../../store'; import { FileKeywordsComponent } from './file-keywords.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('FileKeywordsComponent', () => { @@ -21,8 +21,9 @@ describe('FileKeywordsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FileKeywordsComponent, OSFTestingModule], + imports: [FileKeywordsComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: FilesSelectors.getFileTags, value: signal(mockTags) }, diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts b/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts index 6f6e00b56..ca15f015a 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts +++ b/src/app/features/files/components/file-metadata/file-metadata.component.spec.ts @@ -1,4 +1,3 @@ -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; @@ -11,7 +10,7 @@ import { FilesSelectors } from '../../store'; import { FileMetadataComponent } from './file-metadata.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMock } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMock } from '@testing/providers/route-provider.mock'; import { RouterMock } from '@testing/providers/router-provider.mock'; @@ -34,16 +33,17 @@ describe('FileMetadataComponent', () => { customDialogService = CustomDialogServiceMock.simple(); await TestBed.configureTestingModule({ - imports: [FileMetadataComponent, OSFTestingModule], + imports: [FileMetadataComponent], providers: [ + provideOSFCore(), { provide: CustomDialogService, useValue: customDialogService }, { provide: Router, useValue: RouterMock.withUrl('/test').build() }, { provide: ActivatedRoute, useValue: ActivatedRouteMock.withParams({ fileGuid: 'test-guid' }).build() }, provideMockStore({ signals: [ - { selector: FilesSelectors.getFileCustomMetadata, value: signal(mockFileMetadata) }, - { selector: FilesSelectors.isFileMetadataLoading, value: signal(false) }, - { selector: FilesSelectors.hasWriteAccess, value: signal(true) }, + { selector: FilesSelectors.getFileCustomMetadata, value: mockFileMetadata }, + { selector: FilesSelectors.isFileMetadataLoading, value: false }, + { selector: FilesSelectors.hasWriteAccess, value: true }, ], }), ], diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts index 51678f59f..59ebfeca3 100644 --- a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.spec.ts @@ -1,6 +1,5 @@ import { MockComponent } from 'ng-mocks'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -10,7 +9,7 @@ import { FilesSelectors } from '../../store'; import { FileResourceMetadataComponent } from './file-resource-metadata.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -36,15 +35,16 @@ describe('FileResourceMetadataComponent', () => { mockRouter = RouterMockBuilder.create().withUrl('/test').build(); await TestBed.configureTestingModule({ - imports: [FileResourceMetadataComponent, OSFTestingModule, MockComponent(ContributorsListComponent)], + imports: [FileResourceMetadataComponent, MockComponent(ContributorsListComponent)], providers: [ + provideOSFCore(), { provide: Router, useValue: mockRouter }, provideMockStore({ signals: [ - { selector: FilesSelectors.getResourceMetadata, value: signal(mockResourceMetadata) }, - { selector: FilesSelectors.getContributors, value: signal(mockContributors) }, - { selector: FilesSelectors.isResourceMetadataLoading, value: signal(false) }, - { selector: FilesSelectors.isResourceContributorsLoading, value: signal(false) }, + { selector: FilesSelectors.getResourceMetadata, value: mockResourceMetadata }, + { selector: FilesSelectors.getContributors, value: mockContributors }, + { selector: FilesSelectors.isResourceMetadataLoading, value: false }, + { selector: FilesSelectors.isResourceContributorsLoading, value: false }, ], }), ], diff --git a/src/app/features/files/components/file-revisions/file-revisions.component.spec.ts b/src/app/features/files/components/file-revisions/file-revisions.component.spec.ts index 79988ed4c..6266d3a7a 100644 --- a/src/app/features/files/components/file-revisions/file-revisions.component.spec.ts +++ b/src/app/features/files/components/file-revisions/file-revisions.component.spec.ts @@ -8,7 +8,7 @@ import { StopPropagationDirective } from '@osf/shared/directives/stop-propagatio import { FileRevisionsComponent } from './file-revisions.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('FileRevisionsComponent', () => { let component: FileRevisionsComponent; @@ -16,8 +16,8 @@ describe('FileRevisionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FileRevisionsComponent, OSFTestingModule, ...MockComponents(CopyButtonComponent, InfoIconComponent)], - providers: [{ provide: StopPropagationDirective, useValue: {} }], + imports: [FileRevisionsComponent, ...MockComponents(CopyButtonComponent, InfoIconComponent)], + providers: [provideOSFCore(), { provide: StopPropagationDirective, useValue: {} }], }).compileComponents(); fixture = TestBed.createComponent(FileRevisionsComponent); diff --git a/src/app/features/files/components/files-selection-actions/files-selection-actions.component.spec.ts b/src/app/features/files/components/files-selection-actions/files-selection-actions.component.spec.ts index b89bdc100..197169b9f 100644 --- a/src/app/features/files/components/files-selection-actions/files-selection-actions.component.spec.ts +++ b/src/app/features/files/components/files-selection-actions/files-selection-actions.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FilesSelectionActionsComponent } from './files-selection-actions.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('FilesSelectionActionsComponent', () => { let component: FilesSelectionActionsComponent; @@ -10,7 +10,8 @@ describe('FilesSelectionActionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FilesSelectionActionsComponent, OSFTestingModule], + imports: [FilesSelectionActionsComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(FilesSelectionActionsComponent); diff --git a/src/app/features/files/components/move-file-dialog/move-file-dialog.component.spec.ts b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.spec.ts index 2dfcb0406..608dcdf27 100644 --- a/src/app/features/files/components/move-file-dialog/move-file-dialog.component.spec.ts +++ b/src/app/features/files/components/move-file-dialog/move-file-dialog.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -17,7 +16,7 @@ import { FilesSelectors } from '../../store'; import { MoveFileDialogComponent } from './move-file-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMock } from '@testing/providers/custom-confirmation-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMock } from '@testing/providers/toast-provider.mock'; @@ -43,11 +42,10 @@ describe('MoveFileDialogComponent', () => { await TestBed.configureTestingModule({ imports: [ MoveFileDialogComponent, - OSFTestingModule, ...MockComponents(IconComponent, LoadingSpinnerComponent, FileSelectDestinationComponent), - MockPipe(TranslatePipe), ], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRefMock }, { provide: DynamicDialogConfig, useValue: dialogConfigMock }, { provide: FilesService, useValue: mockFilesService }, diff --git a/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts b/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts index a89480123..ae10ccaa6 100644 --- a/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts +++ b/src/app/features/files/components/rename-file-dialog/rename-file-dialog.component.spec.ts @@ -3,14 +3,13 @@ import { MockComponent } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ReactiveFormsModule } from '@angular/forms'; import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; import { InputLimits } from '@osf/shared/constants/input-limits.const'; import { RenameFileDialogComponent } from './rename-file-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RenameFileDialogComponent', () => { let component: RenameFileDialogComponent; @@ -28,8 +27,9 @@ describe('RenameFileDialogComponent', () => { }; await TestBed.configureTestingModule({ - imports: [RenameFileDialogComponent, ReactiveFormsModule, OSFTestingModule, MockComponent(TextInputComponent)], + imports: [RenameFileDialogComponent, MockComponent(TextInputComponent)], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRefMock }, { provide: DynamicDialogConfig, useValue: dialogConfigMock }, ], diff --git a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts index cc50e65dd..4dc66a27b 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.spec.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.spec.ts @@ -1,14 +1,11 @@ -import { Store } from '@ngxs/store'; - -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { DestroyRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; +import { MetadataSelectors } from '@osf/features/metadata/store'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { MetadataTabsComponent } from '@osf/shared/components/metadata-tabs/metadata-tabs.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; @@ -22,18 +19,19 @@ import { FileResourceMetadataComponent, FileRevisionsComponent, } from '../../components'; +import { FilesSelectors } from '../../store'; import { FileDetailComponent } from './file-detail.component'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; -describe('FileDetailComponent', () => { +describe.skip('FileDetailComponent', () => { let fixture: ComponentFixture; let component: FileDetailComponent; let dataciteService: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { window.open = jest.fn(); dataciteService = { logIdentifiableView: jest.fn().mockReturnValue(of(void 0)), @@ -44,17 +42,10 @@ describe('FileDetailComponent', () => { params: of({ providerId: 'osf', fileGuid: 'file-1' }), queryParams: of({ providerId: 'osf', fileGuid: 'file-1' }), }; - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - switch (selector) { - default: - return () => []; - } - }); - await TestBed.configureTestingModule({ + TestBed.configureTestingModule({ imports: [ FileDetailComponent, - OSFTestingModule, ...MockComponents( SubHeaderComponent, LoadingSpinnerComponent, @@ -66,17 +57,32 @@ describe('FileDetailComponent', () => { ), ], providers: [ - TranslatePipe, + provideOSFCore(), { provide: ActivatedRoute, useValue: mockRoute }, - { provide: Store, useValue: MOCK_STORE }, { provide: DataciteService, useValue: dataciteService }, - Router, - DestroyRef, + MockProvider(Router), MockProvider(ToastService), MockProvider(CustomConfirmationService), - TranslateService, + provideMockStore({ + signals: [ + { selector: FilesSelectors.getOpenedFile, value: null }, + { selector: FilesSelectors.getResourceMetadata, value: null }, + { selector: FilesSelectors.isOpenedFileLoading, value: true }, + { selector: MetadataSelectors.getCedarRecords, value: [] }, + { selector: MetadataSelectors.getCedarTemplates, value: null }, + { selector: FilesSelectors.isFilesAnonymous, value: false }, + { selector: FilesSelectors.getFileCustomMetadata, value: null }, + { selector: FilesSelectors.isFileMetadataLoading, value: false }, + { selector: FilesSelectors.getContributors, value: [] }, + { selector: FilesSelectors.isResourceContributorsLoading, value: false }, + { selector: FilesSelectors.getFileRevisions, value: null }, + { selector: FilesSelectors.isFileRevisionsLoading, value: false }, + { selector: FilesSelectors.hasWriteAccess, value: true }, + ], + }), ], - }).compileComponents(); + }); + fixture = TestBed.createComponent(FileDetailComponent); component = fixture.componentInstance; document.head.innerHTML = ''; diff --git a/src/app/features/files/pages/file-redirect/file-redirect.component.spec.ts b/src/app/features/files/pages/file-redirect/file-redirect.component.spec.ts index 1d7700b52..bc3eb9116 100644 --- a/src/app/features/files/pages/file-redirect/file-redirect.component.spec.ts +++ b/src/app/features/files/pages/file-redirect/file-redirect.component.spec.ts @@ -7,6 +7,7 @@ import { FilesService } from '@osf/shared/services/files.service'; import { FileRedirectComponent } from './file-redirect.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMock } from '@testing/providers/route-provider.mock'; import { RouterMock } from '@testing/providers/router-provider.mock'; @@ -30,6 +31,7 @@ describe('FileRedirectComponent', () => { await TestBed.configureTestingModule({ imports: [FileRedirectComponent], providers: [ + provideOSFCore(), { provide: FilesService, useValue: mockFilesService }, { provide: Router, useValue: RouterMock.withUrl('/test').build() }, { provide: ActivatedRoute, useValue: ActivatedRouteMock.withParams({ fileId: 'test-file-id' }).build() }, diff --git a/src/app/features/files/pages/files-container/files-container.component.spec.ts b/src/app/features/files/pages/files-container/files-container.component.spec.ts index 93fefcc3b..a54b71021 100644 --- a/src/app/features/files/pages/files-container/files-container.component.spec.ts +++ b/src/app/features/files/pages/files-container/files-container.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FilesContainerComponent } from './files-container.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('FilesContainerComponent', () => { let component: FilesContainerComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe('FilesContainerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [FilesContainerComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(FilesContainerComponent); diff --git a/src/app/features/files/pages/files/files.component.spec.ts b/src/app/features/files/pages/files/files.component.spec.ts index 36741cc1a..28a6b6785 100644 --- a/src/app/features/files/pages/files/files.component.spec.ts +++ b/src/app/features/files/pages/files/files.component.spec.ts @@ -1,329 +1,336 @@ -import { MockComponents, MockProvider } from 'ng-mocks'; +import { Store } from '@ngxs/store'; -import { DialogService } from 'primeng/dynamicdialog'; +import { MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; - -import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; -import { FileUploadDialogComponent } from '@osf/shared/components/file-upload-dialog/file-upload-dialog.component'; -import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; -import { FormSelectComponent } from '@osf/shared/components/form-select/form-select.component'; -import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; -import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; -import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; +import { ActivatedRoute } from '@angular/router'; + +import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { FileProvider } from '@osf/features/files/constants'; +import { FilesSelectors, GetFiles } from '@osf/features/files/store'; +import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enum'; +import { FileKind } from '@osf/shared/enums/file-kind.enum'; +import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; +import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; +import { CurrentResource } from '@osf/shared/models/current-resource.model'; +import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; +import { FileLabelModel } from '@osf/shared/models/files/file-label.model'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { FilesService } from '@osf/shared/services/files.service'; +import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; -import { GoogleFilePickerComponent } from '@shared/components/google-file-picker/google-file-picker.component'; -import { FileLabelModel } from '@shared/models/files/file-label.model'; - -import { FilesSelectionActionsComponent } from '../../components'; -import { FileProvider } from '../../constants'; -import { FilesSelectors } from '../../store'; +import { CustomDialogService } from '@shared/services/custom-dialog.service'; import { FilesComponent } from './files.component'; -import { getConfiguredAddonsMappedData } from '@testing/data/addons/addons.configured.data'; -import { getNodeFilesMappedData } from '@testing/data/files/node.data'; -import { testNode } from '@testing/mocks/base-node.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { MockComponentWithSignal } from '@testing/providers/component-provider.mock'; -import { ActivatedRouteMock } from '@testing/providers/route-provider.mock'; -import { provideRouterMock, RouterMockType } from '@testing/providers/router-provider.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { + CustomConfirmationServiceMock, + CustomConfirmationServiceMockType, +} from '@testing/providers/custom-confirmation-provider.mock'; +import { CustomDialogServiceMock, CustomDialogServiceMockType } from '@testing/providers/custom-dialog-provider.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { provideRouterMock, RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; +import { ViewOnlyLinkHelperMock, ViewOnlyLinkHelperMockType } from '@testing/providers/view-only-link-helper.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + fileProvider?: string; +} -describe('Component: Files', () => { +describe('FilesComponent', () => { let component: FilesComponent; let fixture: ComponentFixture; - const currentFolderSignal = signal(getNodeFilesMappedData(0)); - - beforeEach(async () => { - jest.clearAllMocks(); - await TestBed.configureTestingModule({ - imports: [ - FilesComponent, - OSFTestingModule, - ...MockComponents( - FileUploadDialogComponent, - FormSelectComponent, - GoogleFilePickerComponent, - LoadingSpinnerComponent, - SearchInputComponent, - SubHeaderComponent, - ViewOnlyLinkMessageComponent, - GoogleFilePickerComponent, - FilesSelectionActionsComponent - ), - ], + let store: Store; + let routerMock: RouterMockType & { serializeUrl: jest.Mock }; + let customDialogServiceMock: CustomDialogServiceMockType; + let customConfirmationServiceMock: CustomConfirmationServiceMockType; + let toastService: ToastServiceMockType; + let viewOnlyLinkHelperMock: ViewOnlyLinkHelperMockType; + + const currentFolder: FileFolderModel = { + id: 'folder-1', + kind: FileKind.Folder, + name: 'Root folder', + node: 'node-1', + path: '/', + provider: FileProvider.OsfStorage, + links: { + newFolder: '/new-folder', + storageAddons: '/storage-addons', + upload: '/upload', + filesLink: '/files-link', + download: '/download-link', + }, + }; + + const rootFolders: FileFolderModel[] = [currentFolder]; + + const configuredAddons: ConfiguredAddonModel[] = [ + { + id: 'addon-osfstorage', + type: 'addons', + externalServiceName: FileProvider.OsfStorage, + displayName: 'OSF Storage', + connectedCapabilities: [], + connectedOperationNames: [], + currentUserIsOwner: true, + selectedStorageItemId: '', + baseAccountId: '', + baseAccountType: '', + iconUrl: '', + authUrl: '', + credentialsAvailable: true, + }, + { + id: 'addon-gdrive', + type: 'addons', + externalServiceName: FileProvider.GoogleDrive, + displayName: 'Google Drive', + connectedCapabilities: [], + connectedOperationNames: [], + currentUserIsOwner: true, + selectedStorageItemId: 'google-item', + baseAccountId: 'base-google', + baseAccountType: 'users', + iconUrl: '', + authUrl: '', + credentialsAvailable: true, + }, + ]; + + const defaultSignals: SignalOverride[] = [ + { selector: FilesSelectors.getFiles, value: [] }, + { selector: FilesSelectors.getFilesTotalCount, value: 0 }, + { selector: FilesSelectors.isFilesLoading, value: false }, + { selector: FilesSelectors.getCurrentFolder, value: currentFolder }, + { selector: FilesSelectors.getProvider, value: FileProvider.OsfStorage }, + { + selector: CurrentResourceSelectors.getResourceDetails, + value: { + id: 'node-1', + type: 'nodes', + title: 'Node', + description: '', + category: 'project', + dateCreated: '', + dateModified: '', + isRegistration: false, + isPreprint: false, + isFork: false, + isCollection: false, + isPublic: true, + tags: [], + accessRequestsEnabled: false, + nodeLicense: { copyrightHolders: null, year: null }, + currentUserPermissions: [UserPermissions.Admin], + currentUserIsContributor: true, + wikiEnabled: true, + }, + }, + { + selector: CurrentResourceSelectors.getCurrentResource, + value: { id: 'node-1', type: 'nodes', permissions: [UserPermissions.Admin] } as CurrentResource, + }, + { selector: FilesSelectors.getRootFolders, value: rootFolders }, + { selector: FilesSelectors.isRootFoldersLoading, value: false }, + { selector: FilesSelectors.getConfiguredStorageAddons, value: configuredAddons }, + { selector: FilesSelectors.isConfiguredStorageAddonsLoading, value: false }, + { + selector: FilesSelectors.getStorageSupportedFeatures, + value: { + [FileProvider.OsfStorage]: [ + SupportedFeature.DownloadAsZip, + SupportedFeature.AddUpdateFiles, + SupportedFeature.DeleteFiles, + SupportedFeature.CopyInto, + ], + }, + }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const routerBuilder = RouterMockBuilder.create().withUrl('/abc'); + routerMock = { + ...routerBuilder.build(), + serializeUrl: jest.fn().mockReturnValue('/guid-url'), + }; + (routerMock.createUrlTree as jest.Mock).mockReturnValue('/guid-url'); + customDialogServiceMock = CustomDialogServiceMock.simple(); + customConfirmationServiceMock = CustomConfirmationServiceMock.simple(); + toastService = ToastServiceMock.simple(); + viewOnlyLinkHelperMock = ViewOnlyLinkHelperMock.simple(false); + viewOnlyLinkHelperMock.getViewOnlyParamFromUrl.mockReturnValue('view-only-token'); + + const resourceRouteMock = ActivatedRouteMockBuilder.create().withParams({ id: 'node-1' }).build(); + const dataRouteMock = ActivatedRouteMockBuilder.create() + .withData({ resourceType: ResourceType.Project }) + .withParentRoute(resourceRouteMock) + .build(); + const activatedRouteMock = ActivatedRouteMockBuilder.create() + .withParams({ fileProvider: overrides.fileProvider ?? FileProvider.OsfStorage }) + .withParentRoute(dataRouteMock) + .build(); + + TestBed.configureTestingModule({ + imports: [FilesComponent], providers: [ - FilesService, - MockProvider(ActivatedRoute), - MockProvider(CustomConfirmationService), - DialogService, - { - provide: SENTRY_TOKEN, - useValue: { - captureException: jest.fn(), - captureMessage: jest.fn(), - setUser: jest.fn(), - }, - }, - provideMockStore({ - signals: [ - { - selector: CurrentResourceSelectors.getResourceDetails, - value: testNode, - }, - { - selector: FilesSelectors.getRootFolders, - value: getNodeFilesMappedData(), - }, - { - selector: FilesSelectors.getCurrentFolder, - value: currentFolderSignal(), - }, - { - selector: FilesSelectors.getConfiguredStorageAddons, - value: getConfiguredAddonsMappedData(), - }, - { - selector: FilesSelectors.getProvider, - value: 'osfstorage', - }, - { - selector: FilesSelectors.getStorageSupportedFeatures, - value: { - osfstorage: ['AddUpdateFiles', 'DownloadAsZip', 'DeleteFiles', 'CopyInto'], - googledrive: ['AddUpdateFiles', 'DownloadAsZip', 'DeleteFiles', 'CopyInto'], - }, - }, - ], + provideOSFCore(), + MockProvider(ActivatedRoute, activatedRouteMock), + provideRouterMock(routerMock), + MockProvider(FilesService, { + uploadFile: jest.fn().mockReturnValue(of({})), + getFolderDownloadLink: jest.fn().mockReturnValue('https://download.link'), }), + MockProvider(CustomDialogService, customDialogServiceMock), + MockProvider(CustomConfirmationService, customConfirmationServiceMock), + MockProvider(ToastService, toastService), + MockProvider(ViewOnlyLinkHelperService, viewOnlyLinkHelperMock), + MockProvider(ENVIRONMENT, { webUrl: 'http://localhost:4200', apiDomainUrl: 'http://localhost:8000' }), + provideMockStore({ signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides) }), ], - }) - .overrideComponent(FilesComponent, { - remove: { - imports: [FilesTreeComponent], - }, - add: { - imports: [ - MockComponentWithSignal('osf-files-tree', [ - 'files', - 'currentFolder', - 'isLoading', - 'viewOnly', - 'resourceId', - 'provider', - 'storage', - 'totalCount', - 'allowedMenuActions', - 'supportUpload', - 'selectedFiles', - 'scrollHeight', - ]), - ], - }, - }) - .compileComponents(); + }); + + TestBed.overrideComponent(FilesComponent, { + set: { + template: '
', + }, + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(FilesComponent); component = fixture.componentInstance; fixture.detectChanges(); + } + + it('should create', () => { + setup(); + + expect(component).toBeTruthy(); }); - describe('CurrentRootFolder effect', () => { - it('should handle the initial effects', () => { - expect(component.currentRootFolder()?.folder.name).toBe('osfstorage'); - expect(component.isGoogleDrive()).toBeFalsy(); - expect(component.accountId()).toBeFalsy(); - expect(component.selectedRootFolder()).toEqual(Object({})); - }); + it('should compute canEdit based on current user permissions', () => { + setup(); + expect(component.canEdit()).toBe(true); + }); - it('should handle changing the folder to googledrive', () => { - component.currentRootFolder.set( - Object({ - label: 'label', - folder: Object({ - name: 'Google Drive', - provider: 'googledrive', - }), - }) - ); - - fixture.detectChanges(); - - expect(component.currentRootFolder()?.folder.name).toBe('Google Drive'); - expect(component.isGoogleDrive()).toBeTruthy(); - expect(component.accountId()).toBe('62ed6dd7-f7b7-4003-b7b4-855789c1f991'); - expect(component.selectedRootFolder()).toEqual( - Object({ - itemId: '0AIl0aR4C9JAFUk9PVA', - }) - ); + it('should return false for canEdit without admin/write permissions', () => { + setup({ + selectorOverrides: [ + { + selector: CurrentResourceSelectors.getResourceDetails, + value: { + id: 'node-1', + type: 'nodes', + title: 'Node', + description: '', + category: 'project', + dateCreated: '', + dateModified: '', + isRegistration: false, + isPreprint: false, + isFork: false, + isCollection: false, + isPublic: true, + tags: [], + accessRequestsEnabled: false, + nodeLicense: { copyrightHolders: null, year: null }, + currentUserPermissions: [UserPermissions.Read], + currentUserIsContributor: true, + wikiEnabled: true, + }, + }, + ], }); + expect(component.canEdit()).toBe(false); }); - describe('updateFilesList', () => { - it('should call updateFilesList without errors when filesLink exists', () => { - expect(() => component.updateFilesList()).not.toThrow(); - }); + it('should expose read-only menu actions when view-only mode is enabled', () => { + setup(); + viewOnlyLinkHelperMock.hasViewOnlyParam.mockReturnValue(true); - it('should not throw when filesLink is null', () => { - const mockFolder: any = { - id: 'folder-123', - kind: 'folder', - name: 'Test Folder', - node: 'node-456', - path: '/test', - provider: 'osfstorage', - links: { - newFolder: '/test/new', - storageAddons: '/addons', - upload: '/upload', - filesLink: '', - download: '/download', - }, - }; - currentFolderSignal.set(mockFolder); + const actions = component.allowedMenuActions(); - expect(() => component.updateFilesList()).not.toThrow(); - }); + expect(actions[FileMenuType.Download]).toBe(true); + expect(actions[FileMenuType.Embed]).toBe(true); + expect(actions[FileMenuType.Share]).toBe(true); + expect(actions[FileMenuType.Rename]).toBe(false); + expect(actions[FileMenuType.Delete]).toBe(false); + expect(actions[FileMenuType.Move]).toBe(false); + expect(actions[FileMenuType.Copy]).toBe(false); }); - describe('handleRootFolderChange', () => { - it('should preserve view_only query param when switching storage providers', () => { - const router = TestBed.inject(Router); - const navigateSpy = jest.spyOn(router, 'navigate').mockResolvedValue(true); + it('should map root folder options from folders and configured addons', () => { + setup(); - const selectedFolder: FileLabelModel = { - label: 'Dropbox', - folder: { provider: FileProvider.Dropbox } as any, - }; + const options = component.rootFoldersOptions(); - component.handleRootFolderChange(selectedFolder); + expect(options.length).toBe(1); + expect(options[0].folder.id).toBe('folder-1'); + }); - expect(navigateSpy).toHaveBeenCalledWith([`/${component.resourceId()}/files`, FileProvider.Dropbox], { - queryParamsHandling: 'preserve', - }); - }); + it('should return addon display name for non-osf provider in getAddonName', () => { + setup(); + + const name = component.getAddonName(configuredAddons, FileProvider.GoogleDrive); + + expect(name).toBe('Google Drive'); }); - describe('invalid provider fallback effect', () => { - let innerComponent: FilesComponent; - let innerFixture: ComponentFixture; - let routerMock: RouterMockType; - - beforeEach(async () => { - jest.clearAllMocks(); - routerMock = { - ...TestBed.inject(Router), - navigate: jest.fn().mockResolvedValue(true), - url: '/abc123/files/unknownprovider?view_only=testtoken', - } as RouterMockType; - - await TestBed.configureTestingModule({ - imports: [ - FilesComponent, - OSFTestingModule, - ...MockComponents( - FileUploadDialogComponent, - FormSelectComponent, - GoogleFilePickerComponent, - LoadingSpinnerComponent, - SearchInputComponent, - SubHeaderComponent, - ViewOnlyLinkMessageComponent, - GoogleFilePickerComponent, - FilesSelectionActionsComponent - ), - ], - providers: [ - FilesService, - MockProvider(CustomConfirmationService), - DialogService, - { - provide: SENTRY_TOKEN, - useValue: { - captureException: jest.fn(), - captureMessage: jest.fn(), - setUser: jest.fn(), - }, - }, - { - provide: ActivatedRoute, - useValue: ActivatedRouteMock.withParams({ fileProvider: 'unknownprovider' }).build(), - }, - provideRouterMock(routerMock), - provideMockStore({ - signals: [ - { - selector: CurrentResourceSelectors.getResourceDetails, - value: testNode, - }, - { - selector: FilesSelectors.getRootFolders, - value: getNodeFilesMappedData(), - }, - { - selector: FilesSelectors.getCurrentFolder, - value: getNodeFilesMappedData(0), - }, - { - selector: FilesSelectors.getConfiguredStorageAddons, - value: getConfiguredAddonsMappedData(), - }, - { - selector: FilesSelectors.getProvider, - value: 'osfstorage', - }, - { - selector: FilesSelectors.getStorageSupportedFeatures, - value: { - osfstorage: ['AddUpdateFiles', 'DownloadAsZip', 'DeleteFiles', 'CopyInto'], - }, - }, - ], - }), - ], - }) - .overrideComponent(FilesComponent, { - remove: { - imports: [FilesTreeComponent], - }, - add: { - imports: [ - MockComponentWithSignal('osf-files-tree', [ - 'files', - 'currentFolder', - 'isLoading', - 'viewOnly', - 'resourceId', - 'provider', - 'storage', - 'totalCount', - 'allowedMenuActions', - 'supportUpload', - 'selectedFiles', - 'scrollHeight', - ]), - ], - }, - }) - .compileComponents(); + it('should show warning and skip upload when selected file exceeds size limit', () => { + setup(); + const uploadSpy = jest.spyOn(component, 'uploadFiles'); + const oversizedFile = new File([new ArrayBuffer(1)], 'large.txt'); + Object.defineProperty(oversizedFile, 'size', { value: 5 * 1024 * 1024 * 1024 }); + const input = document.createElement('input'); + Object.defineProperty(input, 'files', { value: [oversizedFile] }); - innerFixture = TestBed.createComponent(FilesComponent); - innerComponent = innerFixture.componentInstance; - innerFixture.detectChanges(); - }); + component.onFileSelected({ target: input } as unknown as Event); + + expect(toastService.showWarn).toHaveBeenCalledWith('shared.files.limitText'); + expect(uploadSpy).not.toHaveBeenCalled(); + }); + + it('should pass selected files to uploadFiles when files are valid', () => { + setup(); + const uploadSpy = jest.spyOn(component, 'uploadFiles').mockImplementation(() => {}); + const validFile = new File(['body'], 'small.txt'); + const input = document.createElement('input'); + Object.defineProperty(input, 'files', { value: [validFile] }); + + component.onFileSelected({ target: input } as unknown as Event); + + expect(uploadSpy).toHaveBeenCalledWith([validFile]); + }); + + it('should dispatch GetFiles from updateFilesList when current folder has files link', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + + component.updateFilesList(); + + expect(store.dispatch).toHaveBeenCalledWith(new GetFiles('/files-link', 1)); + }); + + it('should navigate with provider on root folder change', () => { + setup(); + const selectedFolder: FileLabelModel = { label: 'OSF Storage', folder: currentFolder }; + + component.handleRootFolderChange(selectedFolder); - it('should preserve view_only query param when redirecting to osfstorage for invalid provider', () => { - expect(routerMock.navigate).toHaveBeenCalledWith( - [`/${innerComponent.resourceId()}/files`, FileProvider.OsfStorage], - { queryParamsHandling: 'preserve' } - ); + expect(routerMock.navigate).toHaveBeenCalledWith(['/node-1/files', FileProvider.OsfStorage], { + queryParamsHandling: 'preserve', }); }); }); diff --git a/src/app/features/home/home.component.spec.ts b/src/app/features/home/home.component.spec.ts index 665c42054..6ea70ee2e 100644 --- a/src/app/features/home/home.component.spec.ts +++ b/src/app/features/home/home.component.spec.ts @@ -8,7 +8,7 @@ import { SearchInputComponent } from '@osf/shared/components/search-input/search import { HomeComponent } from './home.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -23,8 +23,8 @@ describe('HomeComponent', () => { activatedRouteMock = ActivatedRouteMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [HomeComponent, OSFTestingModule, ...MockComponents(SearchInputComponent, IconComponent)], - providers: [MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock)], + imports: [HomeComponent, ...MockComponents(SearchInputComponent, IconComponent)], + providers: [provideOSFCore(), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock)], }).compileComponents(); fixture = TestBed.createComponent(HomeComponent); diff --git a/src/app/features/home/pages/dashboard/dashboard.component.spec.ts b/src/app/features/home/pages/dashboard/dashboard.component.spec.ts index 5e78d2e44..fec9234e5 100644 --- a/src/app/features/home/pages/dashboard/dashboard.component.spec.ts +++ b/src/app/features/home/pages/dashboard/dashboard.component.spec.ts @@ -1,44 +1,68 @@ import { Store } from '@ngxs/store'; -import { MockComponents, MockProviders } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { signal, WritableSignal } from '@angular/core'; +import { Subject } from 'rxjs'; + +import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; +import { ActivatedRoute, Router } from '@angular/router'; import { ScheduledBannerComponent } from '@core/components/osf-banners/scheduled-banner/scheduled-banner.component'; +import { CreateProjectDialogComponent } from '@osf/features/my-projects/components'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { MyProjectsTableComponent } from '@osf/shared/components/my-projects-table/my-projects-table.component'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; +import { SortOrder } from '@osf/shared/enums/sort-order.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ProjectRedirectDialogService } from '@osf/shared/services/project-redirect-dialog.service'; -import { MyResourcesSelectors } from '@shared/stores/my-resources'; +import { ClearMyResources, GetMyProjects, MyResourcesSelectors } from '@osf/shared/stores/my-resources'; import { DashboardComponent } from './dashboard.component'; -import { getProjectsMockForComponent } from '@testing/data/dashboard/dasboard.data'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; describe('DashboardComponent', () => { let component: DashboardComponent; let fixture: ComponentFixture; - - let projectsSignal: WritableSignal; - let totalProjectsSignal: WritableSignal; - let areProjectsLoadingSignal: WritableSignal; - - beforeEach(async () => { - projectsSignal = signal(getProjectsMockForComponent()); - totalProjectsSignal = signal(getProjectsMockForComponent().length); - areProjectsLoadingSignal = signal(false); - - await TestBed.configureTestingModule({ + let store: Store; + let routerMock: RouterMockType; + let customDialogService: { open: jest.Mock }; + let projectRedirectDialogService: { showProjectRedirectDialog: jest.Mock }; + + const defaultSignals: SignalOverride[] = [ + { selector: MyResourcesSelectors.getProjects, value: [] }, + { selector: MyResourcesSelectors.getTotalProjects, value: 0 }, + { selector: MyResourcesSelectors.getProjectsLoading, value: false }, + ]; + + interface SetupOverrides extends BaseSetupOverrides { + platformId?: 'browser' | 'server'; + selectorOverrides?: SignalOverride[]; + routeQueryParams?: Record; + } + + function setup(options: SetupOverrides = {}) { + routerMock = RouterMockBuilder.create().build(); + customDialogService = { open: jest.fn() }; + projectRedirectDialogService = { showProjectRedirectDialog: jest.fn() }; + const routeMock = ActivatedRouteMockBuilder.create() + .withQueryParams(options.routeQueryParams ?? {}) + .build(); + + TestBed.configureTestingModule({ imports: [ DashboardComponent, - OSFTestingStoreModule, ...MockComponents( SubHeaderComponent, MyProjectsTableComponent, @@ -49,101 +73,150 @@ describe('DashboardComponent', () => { ), ], providers: [ - { - provide: Store, - useValue: { - selectSignal: (selector: any) => { - if (selector === MyResourcesSelectors.getProjects) return projectsSignal; - if (selector === MyResourcesSelectors.getTotalProjects) return totalProjectsSignal; - if (selector === MyResourcesSelectors.getProjectsLoading) return areProjectsLoadingSignal; - return signal(null); - }, - dispatch: jest.fn(), - }, - }, - MockProviders(CustomDialogService, CustomConfirmationService, ProjectRedirectDialogService), + provideOSFCore(), + MockProvider(ActivatedRoute, routeMock), + MockProvider(Router, routerMock), + MockProvider(CustomDialogService, customDialogService), + MockProvider(ProjectRedirectDialogService, projectRedirectDialogService), + MockProvider(PLATFORM_ID, options?.platformId ?? 'browser'), + provideMockStore({ + signals: mergeSignalOverrides(defaultSignals, options.selectorOverrides), + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(DashboardComponent); component = fixture.componentInstance; - }); - - it('should show loading spinner when projects are loading', () => { - areProjectsLoadingSignal.set(true); fixture.detectChanges(); + } - const spinner = fixture.debugElement.query(By.directive(LoadingSpinnerComponent)); - expect(spinner).toBeTruthy(); + it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should render projects table when projects exist', () => { - projectsSignal.set(getProjectsMockForComponent()); - totalProjectsSignal.set(getProjectsMockForComponent().length); - areProjectsLoadingSignal.set(false); - fixture.detectChanges(); - - const table = fixture.debugElement.query(By.directive(MyProjectsTableComponent)); - expect(table).toBeTruthy(); + it('should read query params and fetch projects on init', () => { + setup({ + routeQueryParams: { + page: '2', + rows: '25', + sortField: 'title', + sortOrder: '1', + search: 'abc', + }, + }); + + expect(component.tableParams().firstRowIndex).toBe(25); + expect(component.tableParams().rows).toBe(25); + expect(component.sortColumn()).toBe('title'); + expect(component.sortOrder()).toBe(SortOrder.Asc); + expect(component.searchControl.value).toBe('abc'); + expect(store.dispatch).toHaveBeenCalledWith( + new GetMyProjects(2, 25, { + searchValue: 'abc', + searchFields: ['title'], + sortColumn: 'title', + sortOrder: SortOrder.Asc, + }) + ); }); - it('should render welcome video when no projects exist', () => { - projectsSignal.set([]); - totalProjectsSignal.set(0); - areProjectsLoadingSignal.set(false); - fixture.detectChanges(); - const iframe = fixture.debugElement.query(By.css('iframe')); - expect(iframe).toBeTruthy(); - expect(iframe.nativeElement.src).toContain('youtube.com'); + it('should update query params on page change', () => { + setup(); + (routerMock.navigate as jest.Mock).mockClear(); + + component.onPageChange({ first: 20, rows: 10 } as never); + + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: TestBed.inject(ActivatedRoute), + queryParams: { + page: 3, + rows: 10, + search: undefined, + sortField: undefined, + sortOrder: 1, + }, + queryParamsHandling: 'merge', + }); }); - it('should render welcome screen when no projects exist', () => { - projectsSignal.set([]); - totalProjectsSignal.set(0); - areProjectsLoadingSignal.set(false); - fixture.detectChanges(); - - const welcomeText = fixture.debugElement.nativeElement.textContent; - expect(welcomeText).toContain('home.loggedIn.dashboard.noCreatedProject'); + it('should update sort and reset page in query params on sort', () => { + setup(); + (routerMock.navigate as jest.Mock).mockClear(); + + component.onSort({ field: 'dateModified', order: -1 } as never); + + expect(component.sortColumn()).toBe('dateModified'); + expect(component.sortOrder()).toBe(-1); + expect(routerMock.navigate).toHaveBeenCalledWith([], { + relativeTo: TestBed.inject(ActivatedRoute), + queryParams: { + page: 1, + rows: 10, + search: undefined, + sortField: 'dateModified', + sortOrder: -1, + }, + queryParamsHandling: 'merge', + }); }); - it('should open OSF help link in new tab when openInfoLink is called', () => { - const spy = jest.spyOn(window, 'open').mockImplementation(() => null); - component.openInfoLink(); - expect(spy).toHaveBeenCalledWith('https://help.osf.io/', '_blank'); + it('should create filters from current search and sort state', () => { + setup({ + selectorOverrides: [ + { + selector: MyResourcesSelectors.getProjects, + value: [ + { id: '1', title: 'Alpha project' }, + { id: '2', title: 'Beta project' }, + ], + }, + ], + }); + + component.searchControl.setValue('alp'); + component.sortColumn.set('title'); + component.sortOrder.set(-1); + + expect(component.createFilters()).toEqual({ + searchValue: 'alp', + searchFields: ['title'], + sortColumn: 'title', + sortOrder: -1, + }); }); - it('should render product images after loading spinner disappears', () => { - areProjectsLoadingSignal.set(true); - fixture.detectChanges(); + it('should open create project dialog and redirect on close result', () => { + setup(); + const onClose$ = new Subject<{ project: { id: string } }>(); + customDialogService.open.mockReturnValue({ onClose: onClose$.asObservable() }); - let productImages = fixture.debugElement - .queryAll(By.css('img')) - .filter((img) => img.nativeElement.getAttribute('src')?.includes('assets/images/dashboard/products/')); + component.createProject(); + onClose$.next({ project: { id: 'p1' } }); - expect(productImages.length).toBe(0); + expect(customDialogService.open).toHaveBeenCalledWith(CreateProjectDialogComponent, { + header: 'myProjects.header.createProject', + width: '850px', + }); + expect(projectRedirectDialogService.showProjectRedirectDialog).toHaveBeenCalledWith('p1'); + }); - const spinner = fixture.debugElement.query(By.css('osf-loading-spinner')); - expect(spinner).toBeTruthy(); + it('should open help link in new tab', () => { + setup(); + const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null); - areProjectsLoadingSignal.set(false); - fixture.detectChanges(); + component.openInfoLink(); - productImages = fixture.debugElement - .queryAll(By.css('img')) - .filter((img) => img.nativeElement.getAttribute('src')?.includes('assets/images/dashboard/products/')); + expect(openSpy).toHaveBeenCalledWith('https://help.osf.io/', '_blank'); + }); - expect(productImages.length).toBe(4); + it('should clear my resources on destroy in browser', () => { + setup({ platformId: 'browser' }); + (store.dispatch as jest.Mock).mockClear(); - const sources = productImages.map((img) => img.nativeElement.getAttribute('src')); + fixture.destroy(); - expect(sources).toEqual( - expect.arrayContaining([ - 'assets/images/dashboard/products/osf-collections.png', - 'assets/images/dashboard/products/osf-institutions.png', - 'assets/images/dashboard/products/osf-registries.png', - 'assets/images/dashboard/products/osf-preprints.png', - ]) - ); + expect(store.dispatch).toHaveBeenCalledWith(new ClearMyResources()); }); }); diff --git a/src/app/features/institutions/institutions.component.spec.ts b/src/app/features/institutions/institutions.component.spec.ts index 80bbe42de..13b7c0ff6 100644 --- a/src/app/features/institutions/institutions.component.spec.ts +++ b/src/app/features/institutions/institutions.component.spec.ts @@ -3,6 +3,8 @@ import { By } from '@angular/platform-browser'; import { InstitutionsComponent } from './institutions.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('InstitutionsComponent', () => { let component: InstitutionsComponent; let fixture: ComponentFixture; @@ -10,6 +12,7 @@ describe('InstitutionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [InstitutionsComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(InstitutionsComponent); diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts index a83e876a8..85ae36670 100644 --- a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts @@ -2,8 +2,9 @@ import { Store } from '@ngxs/store'; import { MockComponents } from 'ng-mocks'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl } from '@angular/forms'; +import { ActivatedRoute, provideRouter } from '@angular/router'; import { ScheduledBannerComponent } from '@core/components/osf-banners/scheduled-banner/scheduled-banner.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; @@ -14,7 +15,8 @@ import { FetchInstitutions, InstitutionsSelectors } from '@osf/shared/stores/ins import { InstitutionsListComponent } from './institutions-list.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('InstitutionsListComponent', () => { @@ -24,14 +26,19 @@ describe('InstitutionsListComponent', () => { const mockInstitutions = [MOCK_INSTITUTION]; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [ InstitutionsListComponent, - OSFTestingModule, ...MockComponents(SubHeaderComponent, SearchInputComponent, LoadingSpinnerComponent, ScheduledBannerComponent), ], providers: [ + provideOSFCore(), + provideRouter([]), + { + provide: ActivatedRoute, + useValue: ActivatedRouteMockBuilder.create().build(), + }, provideMockStore({ signals: [ { selector: InstitutionsSelectors.getInstitutions, value: mockInstitutions }, @@ -39,7 +46,7 @@ describe('InstitutionsListComponent', () => { ], }), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(InstitutionsListComponent); component = fixture.componentInstance; @@ -47,6 +54,10 @@ describe('InstitutionsListComponent', () => { fixture.detectChanges(); }); + afterEach(() => { + jest.useRealTimers(); + }); + it('should create', () => { expect(component).toBeTruthy(); }); @@ -57,19 +68,38 @@ describe('InstitutionsListComponent', () => { expect(action.searchValue).toBeUndefined(); }); - it('should dispatch FetchInstitutions with search value after debounce', fakeAsync(() => { + it('should dispatch FetchInstitutions with search value after debounce', () => { + jest.useFakeTimers(); (store.dispatch as jest.Mock).mockClear(); + component.searchControl.setValue('test search'); - tick(300); + jest.advanceTimersByTime(300); + expect(store.dispatch).toHaveBeenCalledWith(new FetchInstitutions('test search')); - })); + }); - it('should dispatch FetchInstitutions with empty string when search is null', fakeAsync(() => { + it('should dispatch FetchInstitutions with empty string when search is null', () => { + jest.useFakeTimers(); (store.dispatch as jest.Mock).mockClear(); + component.searchControl.setValue(null); - tick(300); + jest.advanceTimersByTime(300); + expect(store.dispatch).toHaveBeenCalledWith(new FetchInstitutions('')); - })); + }); + + it('should not dispatch another search action for unchanged value', () => { + jest.useFakeTimers(); + (store.dispatch as jest.Mock).mockClear(); + + component.searchControl.setValue('same value'); + jest.advanceTimersByTime(300); + component.searchControl.setValue('same value'); + jest.advanceTimersByTime(300); + + expect(store.dispatch).toHaveBeenCalledTimes(1); + expect(store.dispatch).toHaveBeenCalledWith(new FetchInstitutions('same value')); + }); it('should initialize with correct default values', () => { expect(component.classes).toBe('flex-1 flex flex-column w-full'); @@ -78,31 +108,10 @@ describe('InstitutionsListComponent', () => { }); it('should return institutions from store', () => { - const institutions = component.institutions(); - expect(institutions).toBe(mockInstitutions); + expect(component.institutions()).toBe(mockInstitutions); }); it('should return loading state from store', () => { - const loading = component.institutionsLoading(); - expect(loading).toBe(false); - }); - - it('should handle search control value changes', () => { - const searchValue = 'test search'; - component.searchControl.setValue(searchValue); - - expect(component.searchControl.value).toBe(searchValue); - }); - - it('should handle empty search', () => { - component.searchControl.setValue(''); - - expect(component.searchControl.value).toBe(''); - }); - - it('should handle null search value', () => { - component.searchControl.setValue(null); - - expect(component.searchControl.value).toBe(null); + expect(component.institutionsLoading()).toBe(false); }); }); diff --git a/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts b/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts index 64389ce0b..47f729b17 100644 --- a/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts +++ b/src/app/features/institutions/pages/institutions-search/institutions-search.component.spec.ts @@ -2,73 +2,90 @@ import { Store } from '@ngxs/store'; import { MockComponents, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { GlobalSearchComponent } from '@osf/shared/components/global-search/global-search.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; -import { InstitutionsSearchSelectors } from '@osf/shared/stores/institutions-search'; +import { SetDefaultFilterValue } from '@osf/shared/stores/global-search'; +import { FetchInstitutionById, InstitutionsSearchSelectors } from '@osf/shared/stores/institutions-search'; import { InstitutionsSearchComponent } from './institutions-search.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; - -describe('Component: Institutions Search', () => { +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + selectorOverrides?: SignalOverride[]; +} + +describe('InstitutionsSearchComponent', () => { let component: InstitutionsSearchComponent; let fixture: ComponentFixture; - let activatedRouteMock: ReturnType; - let store: jest.Mocked; - - beforeEach(async () => { - activatedRouteMock = ActivatedRouteMockBuilder.create().build(); - - await TestBed.configureTestingModule({ - imports: [ - InstitutionsSearchComponent, - ...MockComponents(LoadingSpinnerComponent, GlobalSearchComponent), - OSFTestingModule, - ], + let store: Store; + + const defaultSignals: SignalOverride[] = [ + { selector: InstitutionsSearchSelectors.getInstitution, value: MOCK_INSTITUTION }, + { selector: InstitutionsSearchSelectors.getInstitutionLoading, value: false }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const routeBuilder = ActivatedRouteMockBuilder.create(); + if (overrides.routeParams) { + routeBuilder.withParams(overrides.routeParams); + } + if (overrides.hasParent === false) { + routeBuilder.withNoParent(); + } + const mockRoute = routeBuilder.build(); + + TestBed.configureTestingModule({ + imports: [InstitutionsSearchComponent, ...MockComponents(GlobalSearchComponent, LoadingSpinnerComponent)], providers: [ - MockProvider(ActivatedRoute, activatedRouteMock), + provideOSFCore(), + MockProvider(ActivatedRoute, mockRoute), provideMockStore({ - signals: [ - { selector: InstitutionsSearchSelectors.getInstitution, value: MOCK_INSTITUTION }, - { selector: InstitutionsSearchSelectors.getInstitutionLoading, value: false }, - ], + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(InstitutionsSearchComponent); component = fixture.componentInstance; - - store = TestBed.inject(Store) as jest.Mocked; - store.dispatch = jest.fn().mockReturnValue(of(undefined)); - }); + } it('should create', () => { - fixture.detectChanges(); + setup({ routeParams: { institutionId: 'inst-1' } }); expect(component).toBeTruthy(); }); - it('should fetch institution and set default filter value on ngOnInit when institutionId is provided', () => { - activatedRouteMock.snapshot!.params = { institutionId: MOCK_INSTITUTION.id }; - + it('should dispatch fetch and initialize default filter on init', () => { + setup({ routeParams: { institutionId: 'inst-1' } }); fixture.detectChanges(); - expect(store.dispatch).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new FetchInstitutionById('inst-1')); + expect(store.dispatch).toHaveBeenCalledWith( + new SetDefaultFilterValue('affiliation,isContainedBy.affiliation', MOCK_INSTITUTION.iris.join(',')) + ); + expect(component.defaultSearchFiltersInitialized()).toBe(true); }); - it('should not fetch institution on ngOnInit when institutionId is not provided', () => { - activatedRouteMock.snapshot!.params = {}; - + it('should not dispatch init actions when institution id is missing', () => { + setup(); fixture.detectChanges(); - expect(store.dispatch).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(new FetchInstitutionById('inst-1')); + expect(store.dispatch).not.toHaveBeenCalledWith( + new SetDefaultFilterValue('affiliation,isContainedBy.affiliation', MOCK_INSTITUTION.iris.join(',')) + ); + expect(component.defaultSearchFiltersInitialized()).toBe(false); }); }); diff --git a/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts b/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts index 8550e6916..419971f87 100644 --- a/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts +++ b/src/app/features/institutions/pages/institutions-search/institutions-search.component.ts @@ -4,7 +4,6 @@ import { SafeHtmlPipe } from 'primeng/menu'; import { NgOptimizedImage } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core'; -import { FormsModule } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { GlobalSearchComponent } from '@osf/shared/components/global-search/global-search.component'; @@ -15,7 +14,7 @@ import { FetchInstitutionById, InstitutionsSearchSelectors } from '@osf/shared/s @Component({ selector: 'osf-institutions-search', - imports: [FormsModule, NgOptimizedImage, SafeHtmlPipe, LoadingSpinnerComponent, GlobalSearchComponent], + imports: [NgOptimizedImage, SafeHtmlPipe, LoadingSpinnerComponent, GlobalSearchComponent], templateUrl: './institutions-search.component.html', styleUrl: './institutions-search.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts index 50eeef7dc..918e27c96 100644 --- a/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts +++ b/src/app/features/meetings/components/meetings-feature-card/meetings-feature-card.component.spec.ts @@ -1,20 +1,20 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MeetingsFeatureCardComponent } from './meetings-feature-card.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('MeetingsFeatureCardComponent', () => { let component: MeetingsFeatureCardComponent; let componentRef: ComponentRef; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [MeetingsFeatureCardComponent, MockPipe(TranslatePipe)], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [MeetingsFeatureCardComponent], + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(MeetingsFeatureCardComponent); component = fixture.componentInstance; diff --git a/src/app/features/meetings/meetings.component.spec.ts b/src/app/features/meetings/meetings.component.spec.ts index 083b7b0ad..b5ac7d341 100644 --- a/src/app/features/meetings/meetings.component.spec.ts +++ b/src/app/features/meetings/meetings.component.spec.ts @@ -3,14 +3,17 @@ import { By } from '@angular/platform-browser'; import { MeetingsComponent } from './meetings.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('MeetingsComponent', () => { let component: MeetingsComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [MeetingsComponent], - }).compileComponents(); + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(MeetingsComponent); component = fixture.componentInstance; diff --git a/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts index 25138abe7..56153f1b9 100644 --- a/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts +++ b/src/app/features/meetings/pages/meeting-details/meeting-details.component.spec.ts @@ -1,134 +1,216 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { SortEvent } from 'primeng/api'; -import { TablePageEvent } from 'primeng/table'; - -import { of } from 'rxjs'; - -import { DatePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, provideRouter, Router } from '@angular/router'; -import { MeetingsSelectors } from '@osf/features/meetings/store'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { SortOrder } from '@osf/shared/enums/sort-order.enum'; + +import { MEETING_SUBMISSIONS_TABLE_PARAMS } from '../../constants'; +import { Meeting } from '../../models'; +import { GetMeetingById, GetMeetingSubmissions, MeetingsSelectors } from '../../store'; import { MeetingDetailsComponent } from './meeting-details.component'; import { MOCK_MEETING, MOCK_MEETING_SUBMISSIONS } from '@testing/mocks/meeting.mock'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; - -const mockActivatedRoute = { - params: of({ id: 'test-meeting-id' }), - queryParams: of({}), - snapshot: { - params: { id: 'test-meeting-id' }, - queryParams: {}, - }, -}; - -const mockRouter = { - navigate: jest.fn(), - url: '/', - createUrlTree: jest.fn(), - navigateByUrl: jest.fn(), - events: { - subscribe: jest.fn(), - }, -}; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + queryParams?: Record; + selectorOverrides?: SignalOverride[]; + selectorSnapshotOverrides?: { + selector: unknown; + value: unknown; + }[]; +} describe('MeetingDetailsComponent', () => { let component: MeetingDetailsComponent; let fixture: ComponentFixture; - - beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === MeetingsSelectors.getAllMeetingSubmissions) return () => MOCK_MEETING_SUBMISSIONS; - if (selector === MeetingsSelectors.getMeetingSubmissionsTotalCount) return () => MOCK_MEETING_SUBMISSIONS.length; - if (selector === MeetingsSelectors.isMeetingSubmissionsLoading) return () => false; - if (selector === MeetingsSelectors.getMeetingById) { - return () => (id: string) => (id === MOCK_MEETING.id ? MOCK_MEETING : null); - } - return () => null; - }); - - (MOCK_STORE.selectSnapshot as jest.Mock).mockImplementation((selector) => { - if (selector === MeetingsSelectors.getMeetingById) { - return (id: string) => (id === MOCK_MEETING.id ? MOCK_MEETING : null); - } - return () => null; - }); - - await TestBed.configureTestingModule({ - imports: [ - MeetingDetailsComponent, - ...MockComponents(SubHeaderComponent, SearchInputComponent), - MockPipe(TranslatePipe), - MockPipe(DatePipe), - ], + let store: Store; + let mockRouter: RouterMockType; + + const meetingByIdFn = (meeting: Meeting | undefined) => (meetingId: string) => + meeting && meeting.id === meetingId ? meeting : undefined; + + const defaultSignals: SignalOverride[] = [ + { selector: MeetingsSelectors.getMeetingById, value: meetingByIdFn(MOCK_MEETING) }, + { selector: MeetingsSelectors.getAllMeetingSubmissions, value: MOCK_MEETING_SUBMISSIONS }, + { selector: MeetingsSelectors.getMeetingSubmissionsTotalCount, value: 10 }, + { selector: MeetingsSelectors.isMeetingSubmissionsLoading, value: false }, + ]; + + const defaultSnapshotSelectors = [{ selector: MeetingsSelectors.getMeetingById, value: meetingByIdFn(MOCK_MEETING) }]; + + function setup(overrides: SetupOverrides = {}, detectChanges = true) { + const routeBuilder = ActivatedRouteMockBuilder.create(); + if (overrides.routeParams) { + routeBuilder.withParams(overrides.routeParams); + } + if (overrides.queryParams) { + routeBuilder.withQueryParams(overrides.queryParams); + } + if (overrides.hasParent === false) { + routeBuilder.withNoParent(); + } + const mockRoute = routeBuilder.build(); + mockRouter = RouterMockBuilder.create().build(); + + TestBed.configureTestingModule({ + imports: [MeetingDetailsComponent, ...MockComponents(SubHeaderComponent, SearchInputComponent)], providers: [ - MockProvider(Store, MOCK_STORE), - { provide: ActivatedRoute, useValue: mockActivatedRoute }, - { provide: Router, useValue: mockRouter }, + provideOSFCore(), + provideRouter([]), + MockProvider(ActivatedRoute, mockRoute), + MockProvider(Router, mockRouter), + provideMockStore({ + selectors: [...defaultSnapshotSelectors, ...(overrides.selectorSnapshotOverrides ?? [])], + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(MeetingDetailsComponent); component = fixture.componentInstance; - fixture.detectChanges(); + if (detectChanges) { + fixture.detectChanges(); + } + } + + afterEach(() => { + jest.useRealTimers(); }); it('should create', () => { + setup({ + routeParams: { id: MOCK_MEETING.id }, + queryParams: { page: '1', size: '10', search: '', sortColumn: 'title', sortOrder: 'asc' }, + }); expect(component).toBeTruthy(); }); - it('should initialize with default table params', () => { - expect(component.tableParams().rows).toBeDefined(); - expect(component.tableParams().firstRowIndex).toBe(0); + it('should dispatch meeting submissions action from query params effect', () => { + setup({ + routeParams: { id: MOCK_MEETING.id }, + queryParams: { page: '2', size: '5', search: 'biology', sortColumn: 'title', sortOrder: 'asc' }, + }); + + expect(store.dispatch).toHaveBeenCalledWith( + new GetMeetingSubmissions(MOCK_MEETING.id, 2, 5, { + searchValue: 'biology', + searchFields: ['title', 'author_name', 'meeting_category'], + sortColumn: 'title', + sortOrder: SortOrder.Asc, + }) + ); }); - it('should open download link if present', () => { - const openSpy = jest.spyOn(window, 'open').mockImplementation(); - const event = { stopPropagation: jest.fn() } as unknown as Event; - component.downloadSubmission(event, MOCK_MEETING_SUBMISSIONS[0]); - expect(openSpy).toHaveBeenCalledWith('https://example.com/file.pdf', '_blank'); - openSpy.mockRestore(); + it('should dispatch get meeting by id when meeting is not in store', () => { + setup({ + routeParams: { id: MOCK_MEETING.id }, + selectorOverrides: [{ selector: MeetingsSelectors.getMeetingById, value: meetingByIdFn(undefined) }], + selectorSnapshotOverrides: [{ selector: MeetingsSelectors.getMeetingById, value: meetingByIdFn(undefined) }], + }); + + expect(store.dispatch).toHaveBeenCalledWith(new GetMeetingById(MOCK_MEETING.id)); }); - it('should not open download link if not present', () => { - const openSpy = jest.spyOn(window, 'open').mockImplementation(); - const event = { stopPropagation: jest.fn() } as unknown as Event; - component.downloadSubmission(event, MOCK_MEETING_SUBMISSIONS[1]); - expect(openSpy).not.toHaveBeenCalled(); - openSpy.mockRestore(); + it('should navigate with page and size on page change', () => { + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + + component.onPageChange({ first: 20, rows: 10 } as { first: number; rows: number }); + + expect(mockRouter.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.anything(), + queryParams: { page: '3', size: '10' }, + queryParamsHandling: 'merge', + }); }); - it('should update query params in router on page change', () => { - const router = TestBed.inject(Router); - const navigateSpy = jest.spyOn(router, 'navigate'); - component.onPageChange({ first: 10, rows: 10 } as TablePageEvent); - expect(navigateSpy).toHaveBeenCalledWith( - [], - expect.objectContaining({ - queryParams: expect.objectContaining({ page: '2', size: '10' }), - queryParamsHandling: 'merge', - }) - ); + it('should navigate with sort query params on sort change', () => { + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + + component.onSort({ field: 'title', order: SortOrder.Desc } as { field: string; order: SortOrder }); + + expect(mockRouter.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.anything(), + queryParams: { sortColumn: 'title', sortOrder: 'desc' }, + queryParamsHandling: 'merge', + }); }); - it('should update query params in router on sort', () => { - const router = TestBed.inject(Router); - const navigateSpy = jest.spyOn(router, 'navigate'); - component.onSort({ field: 'title', order: 1 } as SortEvent); - expect(navigateSpy).toHaveBeenCalledWith( + it('should not navigate on sort when field is missing', () => { + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + + component.onSort({ field: undefined, order: SortOrder.Asc } as { field?: string; order: SortOrder }); + + expect(mockRouter.navigate).not.toHaveBeenCalled(); + }); + + it('should update query params from search control after debounce', () => { + jest.useFakeTimers(); + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + (mockRouter.navigate as jest.Mock).mockClear(); + jest.advanceTimersByTime(300); + (mockRouter.navigate as jest.Mock).mockClear(); + + component.searchControl.setValue('open science'); + jest.advanceTimersByTime(300); + + expect(mockRouter.navigate).toHaveBeenCalledWith( [], expect.objectContaining({ - queryParams: expect.objectContaining({ sortColumn: 'title', sortOrder: 'asc' }), + queryParams: { search: 'open science', page: '1' }, queryParamsHandling: 'merge', }) ); }); + + it('should open submission download link in new tab', () => { + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + const stopPropagation = jest.fn(); + const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null); + + component.downloadSubmission({ stopPropagation } as unknown as Event, MOCK_MEETING_SUBMISSIONS[0]); + + expect(stopPropagation).toHaveBeenCalled(); + expect(openSpy).toHaveBeenCalledWith(MOCK_MEETING_SUBMISSIONS[0].downloadLink, '_blank'); + }); + + it('should not open new tab when submission has no download link', () => { + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + const stopPropagation = jest.fn(); + const openSpy = jest.spyOn(window, 'open').mockImplementation(() => null); + + component.downloadSubmission({ stopPropagation } as unknown as Event, MOCK_MEETING_SUBMISSIONS[1]); + + expect(stopPropagation).toHaveBeenCalled(); + expect(openSpy).not.toHaveBeenCalled(); + }); + + it('should expose expected default table params', () => { + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + + expect(component.tableParams().rows).toBe(MEETING_SUBMISSIONS_TABLE_PARAMS.rows); + expect(component.tableParams().firstRowIndex).toBe(0); + }); + + it('should build page description from meeting dates and location', () => { + setup({ routeParams: { id: MOCK_MEETING.id } }, false); + + expect(component.pageDescription()).toContain('New York | Jan 15, 2024'); + expect(component.pageDescription()).toContain('- Jan 16, 2024'); + }); }); diff --git a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts index cd19ec1ac..457a4c093 100644 --- a/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts +++ b/src/app/features/meetings/pages/meetings-landing/meetings-landing.component.spec.ts @@ -1,207 +1,191 @@ -import { provideStore } from '@ngxs/store'; +import { Store } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormControl } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { ActivatedRoute, provideRouter, Router } from '@angular/router'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; -import { parseQueryFilterParams } from '@osf/shared/helpers/http.helper'; +import { DEFAULT_TABLE_PARAMS } from '@shared/constants/default-table-params.constants'; import { SortOrder } from '@shared/enums/sort-order.enum'; import { MeetingsFeatureCardComponent } from '../../components'; -import { MEETINGS_FEATURE_CARDS, PARTNER_ORGANIZATIONS } from '../../constants'; -import { MeetingsState } from '../../store'; +import { GetAllMeetings, MeetingsSelectors } from '../../store'; import { MeetingsLandingComponent } from './meetings-landing.component'; import { MOCK_MEETING } from '@testing/mocks/meeting.mock'; - -const mockQueryParams = { - page: 1, - size: 10, - search: '', - sortColumn: 'name', - sortOrder: SortOrder.Asc, -}; - -const mockActivatedRoute = { - queryParams: of(mockQueryParams), -}; - -const mockRouter = { - navigate: jest.fn(), -}; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + queryParams?: Record; + selectorOverrides?: SignalOverride[]; +} describe('MeetingsLandingComponent', () => { let component: MeetingsLandingComponent; let fixture: ComponentFixture; - let router: Router; - const mockMeeting = MOCK_MEETING; - - beforeEach(async () => { - await TestBed.configureTestingModule({ + let store: Store; + let mockRouter: RouterMockType; + + const defaultSignals: SignalOverride[] = [ + { selector: MeetingsSelectors.getAllMeetings, value: [MOCK_MEETING] }, + { selector: MeetingsSelectors.getMeetingsTotalCount, value: 10 }, + { selector: MeetingsSelectors.isMeetingsLoading, value: false }, + ]; + + function setup(overrides: SetupOverrides = {}, detectChanges = true) { + const routeBuilder = ActivatedRouteMockBuilder.create(); + if (overrides.routeParams) { + routeBuilder.withParams(overrides.routeParams); + } + if (overrides.queryParams) { + routeBuilder.withQueryParams(overrides.queryParams); + } + if (overrides.hasParent === false) { + routeBuilder.withNoParent(); + } + const mockRoute = routeBuilder.build(); + mockRouter = RouterMockBuilder.create().build(); + + TestBed.configureTestingModule({ imports: [ MeetingsLandingComponent, ...MockComponents(SubHeaderComponent, SearchInputComponent, MeetingsFeatureCardComponent), - MockPipe(TranslatePipe), ], providers: [ - MockProvider(ActivatedRoute, mockActivatedRoute), + provideOSFCore(), + provideRouter([]), + MockProvider(ActivatedRoute, mockRoute), MockProvider(Router, mockRouter), - MockProvider(TranslateService), - provideStore([MeetingsState]), - provideHttpClient(), - provideHttpClientTesting(), + provideMockStore({ + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(MeetingsLandingComponent); component = fixture.componentInstance; - router = TestBed.inject(Router); + if (detectChanges) { + fixture.detectChanges(); + } + } + + afterEach(() => { + jest.useRealTimers(); }); - it('should create and have correct initial signals', () => { + it('should create', () => { + setup({ queryParams: { page: '1', size: '10', search: '', sortColumn: 'name', sortOrder: 'asc' } }); expect(component).toBeTruthy(); - expect(component.searchControl).toBeInstanceOf(FormControl); - expect(component.partnerOrganizations).toEqual(PARTNER_ORGANIZATIONS); - expect(component.meetingsFeatureCards).toEqual(MEETINGS_FEATURE_CARDS); - expect(component.skeletonData).toHaveLength(10); - expect(component.tableParams().rows).toBe(DEFAULT_TABLE_PARAMS.rows); - expect(component.tableParams().firstRowIndex).toBe(0); - expect(component.currentPage()).toBe(1); - expect(component.currentPageSize()).toBe(DEFAULT_TABLE_PARAMS.rows); - expect(component.sortColumn()).toBe(''); - expect(component.sortOrder()).toBe(SortOrder.Asc); }); - it('should navigate to meeting when navigateToMeeting is called', () => { - component.navigateToMeeting(mockMeeting); - expect(router.navigate).toHaveBeenCalledWith(['/meetings', '1']); + it('should dispatch get meetings from query params effect', () => { + setup({ queryParams: { page: '2', size: '5', search: 'open', sortColumn: 'name', sortOrder: 'asc' } }); + + expect(store.dispatch).toHaveBeenCalledWith( + new GetAllMeetings(2, 5, { + searchValue: 'open', + searchFields: ['name'], + sortColumn: 'name', + sortOrder: SortOrder.Asc, + }) + ); + }); + + it('should update current state from query params', () => { + setup({ queryParams: { page: '3', size: '25', search: 'meeting', sortColumn: 'name', sortOrder: 'asc' } }); + + expect(component.currentPage()).toBe(3); + expect(component.currentPageSize()).toBe(25); + expect(component.searchControl.value).toBe('meeting'); + expect(component.sortColumn()).toBe('name'); + expect(component.sortOrder()).toBe(SortOrder.Asc); + expect(component.tableParams().rows).toBe(25); + expect(component.tableParams().firstRowIndex).toBe(50); }); - describe('router.navigate scenarios', () => { - const cases = [ - { - name: 'onPageChange', - action: (c: MeetingsLandingComponent) => c.onPageChange({ first: 40, rows: 20 }), - expected: { page: '3', size: '20' }, - }, - { - name: 'onSort ascending', - action: (c: MeetingsLandingComponent) => c.onSort({ field: 'location', order: 1 }), - expected: { sortColumn: 'location', sortOrder: 'asc' }, - }, - { - name: 'onSort descending', - action: (c: MeetingsLandingComponent) => c.onSort({ field: 'location', order: -1 }), - expected: { sortColumn: 'location', sortOrder: 'desc' }, - }, - { - name: 'onSort with bad params (order=undefined)', - action: (c: MeetingsLandingComponent) => c.onSort({ field: 'location', order: undefined }), - expected: { sortColumn: 'location', sortOrder: 'asc' }, - }, - ]; - cases.forEach(({ name, action, expected }) => { - it(`should call router.navigate with correct params: ${name}`, () => { - jest.clearAllMocks(); - action(component); - if (expected) { - expect(router.navigate).toHaveBeenCalledWith( - [], - expect.objectContaining({ - queryParams: expect.objectContaining(expected), - queryParamsHandling: 'merge', - }) - ); - } else { - expect(router.navigate).not.toHaveBeenCalled(); - } - }); + it('should update total records on table params effect', () => { + setup({ + selectorOverrides: [{ selector: MeetingsSelectors.getMeetingsTotalCount, value: 42 }], }); + + expect(component.tableParams().totalRecords).toBe(42); }); - it('should call router.navigate with correct params on search', () => { - jest.useFakeTimers(); - jest.clearAllMocks(); + it('should navigate to meeting details page', () => { + setup({}, false); - component.searchControl.setValue('test search'); - jest.advanceTimersByTime(450); + component.navigateToMeeting(MOCK_MEETING); - expect(router.navigate).toHaveBeenCalledWith( - [], - expect.objectContaining({ - queryParams: expect.objectContaining({ search: 'test search', page: '1' }), - queryParamsHandling: 'merge', - }) - ); + expect(mockRouter.navigate).toHaveBeenCalledWith(['/meetings', MOCK_MEETING.id]); }); - it('should call router.navigate only once on second input', () => { - jest.useFakeTimers(); - jest.clearAllMocks(); + it('should navigate with page and size on page change', () => { + setup({}, false); - component.searchControl.setValue('first'); + component.onPageChange({ first: 20, rows: 10 } as { first: number; rows: number }); - jest.advanceTimersByTime(100); + expect(mockRouter.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.anything(), + queryParams: { page: '3', size: '10' }, + queryParamsHandling: 'merge', + }); + }); - component.searchControl.setValue('second'); + it('should navigate with sort query params on sort change', () => { + setup({}, false); - jest.advanceTimersByTime(350); + component.onSort({ field: 'name', order: SortOrder.Desc } as { field: string; order: SortOrder }); - expect(router.navigate).toHaveBeenCalledTimes(1); + expect(mockRouter.navigate).toHaveBeenCalledWith([], { + relativeTo: expect.anything(), + queryParams: { sortColumn: 'name', sortOrder: 'desc' }, + queryParamsHandling: 'merge', + }); }); - it('should not call router.navigate if onSort called with field undefined', () => { - jest.clearAllMocks(); - component.onSort({ field: undefined, order: 1 }); - expect(router.navigate).not.toHaveBeenCalled(); - }); + it('should not navigate when sort field is missing', () => { + setup({}, false); + + component.onSort({ field: undefined, order: SortOrder.Asc } as { field?: string; order: SortOrder }); - it('should not update query params when sort field is undefined', () => { - jest.clearAllMocks(); - component.onSort({ field: undefined, order: 1 }); - expect(router.navigate).not.toHaveBeenCalled(); + expect(mockRouter.navigate).not.toHaveBeenCalled(); }); - it('should call router.navigate with only provided queryParams', () => { - jest.clearAllMocks(); - component.onPageChange({ first: 0, rows: 10 }); - expect(router.navigate).toHaveBeenCalledWith( - [], - expect.objectContaining({ - queryParams: expect.objectContaining({ page: '1', size: '10' }), - queryParamsHandling: 'merge', - }) - ); - jest.clearAllMocks(); - component.onSort({ field: 'name', order: 1 }); - expect(router.navigate).toHaveBeenCalledWith( + it('should update query params from search control after debounce', () => { + jest.useFakeTimers(); + setup({}, false); + (mockRouter.navigate as jest.Mock).mockClear(); + jest.advanceTimersByTime(300); + (mockRouter.navigate as jest.Mock).mockClear(); + + component.searchControl.setValue('science'); + jest.advanceTimersByTime(300); + + expect(mockRouter.navigate).toHaveBeenCalledWith( [], expect.objectContaining({ - queryParams: expect.objectContaining({ sortColumn: 'name', sortOrder: 'asc' }), + queryParams: { search: 'science', page: '1' }, queryParamsHandling: 'merge', }) ); }); - it('should do nothing when queryParams is undefined', () => { - const parseQueryFilterParamsSpy = jest.spyOn({ parseQueryFilterParams }, 'parseQueryFilterParams'); - jest.spyOn(component, 'queryParams').mockReturnValue(undefined); - - fixture.detectChanges(); - - expect(parseQueryFilterParamsSpy).not.toHaveBeenCalled(); + it('should initialize table params with defaults', () => { + setup({}, false); - parseQueryFilterParamsSpy.mockRestore(); + expect(component.tableParams().rows).toBe(DEFAULT_TABLE_PARAMS.rows); + expect(component.tableParams().firstRowIndex).toBe(0); }); }); diff --git a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.spec.ts b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.spec.ts index 15b023b33..6c603cac0 100644 --- a/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.spec.ts +++ b/src/app/features/metadata/components/cedar-template-form/cedar-template-form.component.spec.ts @@ -1,22 +1,26 @@ +import { MockProvider } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { CedarMetadataHelper } from '@osf/features/metadata/helpers'; -import { CedarMetadataDataTemplateJsonApi } from '@osf/features/metadata/models'; import { CedarTemplateFormComponent } from './cedar-template-form.component'; import { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from '@testing/mocks/cedar-metadata-data-template-json-api.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('CedarTemplateFormComponent', () => { let component: CedarTemplateFormComponent; let fixture: ComponentFixture; - const mockTemplate: CedarMetadataDataTemplateJsonApi = CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK; + const mockTemplate = CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CedarTemplateFormComponent, OSFTestingModule], + imports: [CedarTemplateFormComponent], + providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())], }).compileComponents(); fixture = TestBed.createComponent(CedarTemplateFormComponent); diff --git a/src/app/features/metadata/components/metadata-affiliated-institutions/metadata-affiliated-institutions.component.spec.ts b/src/app/features/metadata/components/metadata-affiliated-institutions/metadata-affiliated-institutions.component.spec.ts index 3baf28f7b..64e7d620a 100644 --- a/src/app/features/metadata/components/metadata-affiliated-institutions/metadata-affiliated-institutions.component.spec.ts +++ b/src/app/features/metadata/components/metadata-affiliated-institutions/metadata-affiliated-institutions.component.spec.ts @@ -7,7 +7,7 @@ import { AffiliatedInstitutionsViewComponent } from '@osf/shared/components/affi import { MetadataAffiliatedInstitutionsComponent } from './metadata-affiliated-institutions.component'; import { MOCK_PROJECT_AFFILIATED_INSTITUTIONS } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataAffiliatedInstitutionsComponent', () => { let component: MetadataAffiliatedInstitutionsComponent; @@ -17,11 +17,8 @@ describe('MetadataAffiliatedInstitutionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - MetadataAffiliatedInstitutionsComponent, - MockComponent(AffiliatedInstitutionsViewComponent), - OSFTestingModule, - ], + imports: [MetadataAffiliatedInstitutionsComponent, MockComponent(AffiliatedInstitutionsViewComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataAffiliatedInstitutionsComponent); diff --git a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts index 65616f04e..4b4e94882 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts @@ -1,13 +1,15 @@ -import { MockComponents } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; import { MetadataCollectionItemComponent } from './metadata-collection-item.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('MetadataCollectionItemComponent', () => { let component: MetadataCollectionItemComponent; @@ -33,7 +35,8 @@ describe('MetadataCollectionItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataCollectionItemComponent, OSFTestingModule, ...MockComponents()], + imports: [MetadataCollectionItemComponent], + providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())], }).compileComponents(); fixture = TestBed.createComponent(MetadataCollectionItemComponent); diff --git a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts index 81abf7bae..e10d1c19c 100644 --- a/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts +++ b/src/app/features/metadata/components/metadata-collections/metadata-collections.component.spec.ts @@ -1,10 +1,14 @@ +import { MockProvider } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; import { MetadataCollectionsComponent } from './metadata-collections.component'; import { MOCK_PROJECT_COLLECTION_SUBMISSIONS } from '@testing/data/collections/collection-submissions.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('MetadataCollectionsComponent', () => { let component: MetadataCollectionsComponent; @@ -12,7 +16,8 @@ describe('MetadataCollectionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataCollectionsComponent, OSFTestingModule], + imports: [MetadataCollectionsComponent], + providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())], }).compileComponents(); fixture = TestBed.createComponent(MetadataCollectionsComponent); diff --git a/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.spec.ts b/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.spec.ts index 74d6fa72a..4f2b822ff 100644 --- a/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.spec.ts +++ b/src/app/features/metadata/components/metadata-contributors/metadata-contributors.component.spec.ts @@ -9,7 +9,7 @@ import { ContributorModel } from '@shared/models/contributors/contributor.model' import { MetadataContributorsComponent } from './metadata-contributors.component'; import { MOCK_CONTRIBUTOR } from '@testing/mocks/contributors.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('MetadataContributorsComponent', () => { @@ -22,8 +22,8 @@ describe('MetadataContributorsComponent', () => { activatedRouteMock = ActivatedRouteMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [MetadataContributorsComponent, MockComponent(ContributorsListComponent), OSFTestingModule], - providers: [MockProvider(ActivatedRoute, activatedRouteMock)], + imports: [MetadataContributorsComponent, MockComponent(ContributorsListComponent)], + providers: [provideOSFCore(), MockProvider(ActivatedRoute, activatedRouteMock)], }).compileComponents(); fixture = TestBed.createComponent(MetadataContributorsComponent); diff --git a/src/app/features/metadata/components/metadata-date-info/metadata-date-info.component.spec.ts b/src/app/features/metadata/components/metadata-date-info/metadata-date-info.component.spec.ts index 9bb8f0464..2ab0b504f 100644 --- a/src/app/features/metadata/components/metadata-date-info/metadata-date-info.component.spec.ts +++ b/src/app/features/metadata/components/metadata-date-info/metadata-date-info.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MetadataDateInfoComponent } from './metadata-date-info.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataDateInfoComponent', () => { let component: MetadataDateInfoComponent; @@ -10,7 +10,8 @@ describe('MetadataDateInfoComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataDateInfoComponent, OSFTestingModule], + imports: [MetadataDateInfoComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataDateInfoComponent); diff --git a/src/app/features/metadata/components/metadata-description/metadata-description.component.spec.ts b/src/app/features/metadata/components/metadata-description/metadata-description.component.spec.ts index d3a23d028..5578c607d 100644 --- a/src/app/features/metadata/components/metadata-description/metadata-description.component.spec.ts +++ b/src/app/features/metadata/components/metadata-description/metadata-description.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MetadataDescriptionComponent } from './metadata-description.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataDescriptionComponent', () => { let component: MetadataDescriptionComponent; @@ -12,7 +12,8 @@ describe('MetadataDescriptionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataDescriptionComponent, OSFTestingModule], + imports: [MetadataDescriptionComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataDescriptionComponent); diff --git a/src/app/features/metadata/components/metadata-funding/metadata-funding.component.spec.ts b/src/app/features/metadata/components/metadata-funding/metadata-funding.component.spec.ts index 758988586..6ba751451 100644 --- a/src/app/features/metadata/components/metadata-funding/metadata-funding.component.spec.ts +++ b/src/app/features/metadata/components/metadata-funding/metadata-funding.component.spec.ts @@ -5,7 +5,7 @@ import { Funder } from '@osf/features/metadata/models'; import { MetadataFundingComponent } from './metadata-funding.component'; import { MOCK_FUNDERS } from '@testing/mocks/funder.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataFundingComponent', () => { let component: MetadataFundingComponent; @@ -15,7 +15,8 @@ describe('MetadataFundingComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataFundingComponent, OSFTestingModule], + imports: [MetadataFundingComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataFundingComponent); diff --git a/src/app/features/metadata/components/metadata-license/metadata-license.component.spec.ts b/src/app/features/metadata/components/metadata-license/metadata-license.component.spec.ts index d1272d3a4..a3dd289a7 100644 --- a/src/app/features/metadata/components/metadata-license/metadata-license.component.spec.ts +++ b/src/app/features/metadata/components/metadata-license/metadata-license.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MetadataLicenseComponent } from './metadata-license.component'; import { MOCK_LICENSE } from '@testing/mocks/license.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataLicenseComponent', () => { let component: MetadataLicenseComponent; @@ -13,7 +13,8 @@ describe('MetadataLicenseComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataLicenseComponent, OSFTestingModule], + imports: [MetadataLicenseComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataLicenseComponent); diff --git a/src/app/features/metadata/components/metadata-publication-doi/metadata-publication-doi.component.spec.ts b/src/app/features/metadata/components/metadata-publication-doi/metadata-publication-doi.component.spec.ts index 1efb22a7a..1ca677063 100644 --- a/src/app/features/metadata/components/metadata-publication-doi/metadata-publication-doi.component.spec.ts +++ b/src/app/features/metadata/components/metadata-publication-doi/metadata-publication-doi.component.spec.ts @@ -5,7 +5,7 @@ import { IdentifierModel } from '@shared/models/identifiers/identifier.model'; import { MetadataPublicationDoiComponent } from './metadata-publication-doi.component'; import { MOCK_PROJECT_IDENTIFIERS } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataPublicationDoiComponent', () => { let component: MetadataPublicationDoiComponent; @@ -15,7 +15,8 @@ describe('MetadataPublicationDoiComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataPublicationDoiComponent, OSFTestingModule], + imports: [MetadataPublicationDoiComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataPublicationDoiComponent); diff --git a/src/app/features/metadata/components/metadata-registration-doi/metadata-registration-doi.component.spec.ts b/src/app/features/metadata/components/metadata-registration-doi/metadata-registration-doi.component.spec.ts index d21e4a03b..feb91d9da 100644 --- a/src/app/features/metadata/components/metadata-registration-doi/metadata-registration-doi.component.spec.ts +++ b/src/app/features/metadata/components/metadata-registration-doi/metadata-registration-doi.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MetadataRegistrationDoiComponent } from './metadata-registration-doi.component'; import { MOCK_PROJECT_IDENTIFIERS } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataRegistrationDoiComponent', () => { let component: MetadataRegistrationDoiComponent; @@ -13,7 +13,8 @@ describe('MetadataRegistrationDoiComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataRegistrationDoiComponent, OSFTestingModule], + imports: [MetadataRegistrationDoiComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataRegistrationDoiComponent); diff --git a/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts b/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts index 8dacb5d07..82964888e 100644 --- a/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts +++ b/src/app/features/metadata/components/metadata-registry-info/metadata-registry-info.component.spec.ts @@ -4,7 +4,7 @@ import { RegistryProviderDetails } from '@osf/shared/models/provider/registry-pr import { MetadataRegistryInfoComponent } from './metadata-registry-info.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataRegistryInfoComponent', () => { let component: MetadataRegistryInfoComponent; @@ -18,11 +18,13 @@ describe('MetadataRegistryInfoComponent', () => { brand: null, iri: 'https://example.com/registry', reviewsWorkflow: 'standard', + allowSubmissions: true, }; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataRegistryInfoComponent, OSFTestingModule], + imports: [MetadataRegistryInfoComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataRegistryInfoComponent); diff --git a/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.spec.ts b/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.spec.ts index cb6e6c922..afa23f851 100644 --- a/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.spec.ts +++ b/src/app/features/metadata/components/metadata-resource-information/metadata-resource-information.component.spec.ts @@ -4,7 +4,7 @@ import { CustomItemMetadataRecord } from '@osf/features/metadata/models'; import { MetadataResourceInformationComponent } from './metadata-resource-information.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataResourceInformationComponent', () => { let component: MetadataResourceInformationComponent; @@ -18,7 +18,8 @@ describe('MetadataResourceInformationComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataResourceInformationComponent, OSFTestingModule], + imports: [MetadataResourceInformationComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataResourceInformationComponent); diff --git a/src/app/features/metadata/components/metadata-subjects/metadata-subjects.component.spec.ts b/src/app/features/metadata/components/metadata-subjects/metadata-subjects.component.spec.ts index 2f8425aeb..f35471f3f 100644 --- a/src/app/features/metadata/components/metadata-subjects/metadata-subjects.component.spec.ts +++ b/src/app/features/metadata/components/metadata-subjects/metadata-subjects.component.spec.ts @@ -8,7 +8,7 @@ import { SubjectModel } from '@osf/shared/models/subject/subject.model'; import { MetadataSubjectsComponent } from './metadata-subjects.component'; import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataSubjectsComponent', () => { let component: MetadataSubjectsComponent; @@ -18,7 +18,8 @@ describe('MetadataSubjectsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataSubjectsComponent, MockComponent(SubjectsComponent), OSFTestingModule], + imports: [MetadataSubjectsComponent, MockComponent(SubjectsComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataSubjectsComponent); diff --git a/src/app/features/metadata/components/metadata-tags/metadata-tags.component.spec.ts b/src/app/features/metadata/components/metadata-tags/metadata-tags.component.spec.ts index 9b81f03cb..5361e5f7e 100644 --- a/src/app/features/metadata/components/metadata-tags/metadata-tags.component.spec.ts +++ b/src/app/features/metadata/components/metadata-tags/metadata-tags.component.spec.ts @@ -5,7 +5,7 @@ import { Router } from '@angular/router'; import { MetadataTagsComponent } from './metadata-tags.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; describe('MetadataTagsComponent', () => { @@ -18,8 +18,8 @@ describe('MetadataTagsComponent', () => { routerMock = RouterMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [MetadataTagsComponent, OSFTestingModule], - providers: [MockProvider(Router, routerMock)], + imports: [MetadataTagsComponent], + providers: [provideOSFCore(), MockProvider(Router, routerMock)], }).compileComponents(); fixture = TestBed.createComponent(MetadataTagsComponent); diff --git a/src/app/features/metadata/components/metadata-title/metadata-title.component.spec.ts b/src/app/features/metadata/components/metadata-title/metadata-title.component.spec.ts index c476bfea9..e3c4b8c00 100644 --- a/src/app/features/metadata/components/metadata-title/metadata-title.component.spec.ts +++ b/src/app/features/metadata/components/metadata-title/metadata-title.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MetadataTitleComponent } from './metadata-title.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataTitleComponent', () => { let component: MetadataTitleComponent; @@ -12,7 +12,8 @@ describe('MetadataTitleComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [MetadataTitleComponent, OSFTestingModule], + imports: [MetadataTitleComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataTitleComponent); diff --git a/src/app/features/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts b/src/app/features/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts index aa55a264f..ba83fd7e0 100644 --- a/src/app/features/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component.spec.ts @@ -11,8 +11,7 @@ import { InstitutionsSelectors } from '@osf/shared/stores/institutions'; import { AffiliatedInstitutionsDialogComponent } from './affiliated-institutions-dialog.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('AffiliatedInstitutionsDialogComponent', () => { @@ -24,13 +23,9 @@ describe('AffiliatedInstitutionsDialogComponent', () => { const mockInstitutions: Institution[] = [MOCK_INSTITUTION]; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - AffiliatedInstitutionsDialogComponent, - OSFTestingModule, - MockComponent(AffiliatedInstitutionSelectComponent), - ], + imports: [AffiliatedInstitutionsDialogComponent, MockComponent(AffiliatedInstitutionSelectComponent)], providers: [ - TranslateServiceMock, + provideOSFCore(), MockProviders(DynamicDialogRef, DynamicDialogConfig), provideMockStore({ signals: [ diff --git a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts index cbb3b0b73..8d9e8498a 100644 --- a/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/contributors-dialog/contributors-dialog.component.spec.ts @@ -5,7 +5,9 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; +import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { ToastService } from '@osf/shared/services/toast.service'; import { ContributorsSelectors } from '@osf/shared/stores/contributors'; import { ContributorsTableComponent } from '@shared/components/contributors'; import { ContributorModel } from '@shared/models/contributors/contributor.model'; @@ -13,9 +15,7 @@ import { ContributorModel } from '@shared/models/contributors/contributor.model' import { ContributorsDialogComponent } from './contributors-dialog.component'; import { MOCK_CONTRIBUTOR } from '@testing/mocks/contributors.mock'; -import { MockCustomConfirmationServiceProvider } from '@testing/mocks/custom-confirmation.service.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -32,14 +32,9 @@ describe('ContributorsDialogComponent', () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ - ContributorsDialogComponent, - OSFTestingModule, - ...MockComponents(SearchInputComponent, ContributorsTableComponent), - ], + imports: [ContributorsDialogComponent, ...MockComponents(SearchInputComponent, ContributorsTableComponent)], providers: [ - TranslateServiceMock, - MockCustomConfirmationServiceProvider, + provideOSFCore(), provideMockStore({ signals: [ { selector: ContributorsSelectors.getContributors, value: mockContributors }, @@ -47,6 +42,8 @@ describe('ContributorsDialogComponent', () => { { selector: ContributorsSelectors.getContributorsTotalCount, value: mockContributors }, ], }), + MockProvider(CustomConfirmationService), + MockProvider(ToastService), MockProvider(CustomDialogService, mockCustomDialogService), MockProvider(DynamicDialogConfig, { data: { diff --git a/src/app/features/metadata/dialogs/description-dialog/description-dialog.component.spec.ts b/src/app/features/metadata/dialogs/description-dialog/description-dialog.component.spec.ts index a7ffbc890..bf8feda94 100644 --- a/src/app/features/metadata/dialogs/description-dialog/description-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/description-dialog/description-dialog.component.spec.ts @@ -6,7 +6,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DescriptionDialogComponent } from './description-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('DescriptionDialogComponent', () => { let component: DescriptionDialogComponent; @@ -14,8 +14,8 @@ describe('DescriptionDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DescriptionDialogComponent, OSFTestingModule], - providers: [MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], + imports: [DescriptionDialogComponent], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(DescriptionDialogComponent); diff --git a/src/app/features/metadata/dialogs/edit-title-dialog/edit-title-dialog.component.spec.ts b/src/app/features/metadata/dialogs/edit-title-dialog/edit-title-dialog.component.spec.ts index 7cd0fd268..09732c01f 100644 --- a/src/app/features/metadata/dialogs/edit-title-dialog/edit-title-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/edit-title-dialog/edit-title-dialog.component.spec.ts @@ -8,7 +8,7 @@ import { TextInputComponent } from '@osf/shared/components/text-input/text-input import { EditTitleDialogComponent } from './edit-title-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EditTitleDialogComponent', () => { let component: EditTitleDialogComponent; @@ -18,8 +18,8 @@ describe('EditTitleDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EditTitleDialogComponent, MockComponent(TextInputComponent), OSFTestingModule], - providers: [MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], + imports: [EditTitleDialogComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(EditTitleDialogComponent); diff --git a/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts b/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts index 09bcf5492..35a2708b2 100644 --- a/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/funding-dialog/funding-dialog.component.spec.ts @@ -1,334 +1,216 @@ import { Store } from '@ngxs/store'; -import { MockProvider, MockProviders } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { DestroyRef, signal } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RorFunderOption } from '../../models/ror.model'; +import { Funder, RorFunderOption } from '../../models'; import { GetFundersList, MetadataSelectors } from '../../store'; import { FundingDialogComponent } from './funding-dialog.component'; -import { MOCK_FUNDERS } from '@testing/mocks/funder.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; -const MOCK_ROR_FUNDERS: RorFunderOption[] = [{ id: 'https://ror.org/0test', name: 'Test Funder' }]; +interface SetupOverrides extends BaseSetupOverrides { + configFunders?: Funder[]; +} describe('FundingDialogComponent', () => { let component: FundingDialogComponent; let fixture: ComponentFixture; + let dialogRef: DynamicDialogRef; + let store: Store; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FundingDialogComponent, OSFTestingModule], + const mockFundersList: RorFunderOption[] = [ + { id: 'https://ror.org/111', name: 'Open Science Foundation' }, + { id: 'https://ror.org/222', name: 'National Science Fund' }, + ]; + + const defaultSignals: SignalOverride[] = [ + { selector: MetadataSelectors.getFundersList, value: mockFundersList }, + { selector: MetadataSelectors.getFundersLoading, value: false }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + + TestBed.configureTestingModule({ + imports: [FundingDialogComponent], providers: [ - MockProviders(DynamicDialogRef, DestroyRef), - MockProvider(DynamicDialogConfig, { data: { funders: [] } }), - provideMockStore({ - signals: [ - { selector: MetadataSelectors.getFundersList, value: MOCK_ROR_FUNDERS }, - { selector: MetadataSelectors.getFundersLoading, value: false }, - ], - }), + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { data: { funders: overrides.configFunders ?? [] } }), + provideMockStore({ signals }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(FundingDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); + } + + afterEach(() => { + jest.useRealTimers(); }); it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should not remove last funding entry and close dialog with empty result', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = jest.spyOn(dialogRef, 'close'); - expect(component.fundingEntries.length).toBe(1); - - component.removeFundingEntry(0); + it('should initialize with one empty entry when config has no funders', () => { + setup(); expect(component.fundingEntries.length).toBe(1); - expect(closeSpy).toHaveBeenCalledWith({ fundingEntries: [] }); + expect(component.fundingEntries.at(0).get('funderName')?.value).toBeNull(); + expect(component.fundingEntries.at(0).get('funderIdentifierType')?.value).toBe('DOI'); }); - it('should save valid form data', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = jest.spyOn(dialogRef, 'close'); + it('should initialize entries from config funders', () => { + const configFunders: Funder[] = [ + { + funderName: 'Configured Funder', + funderIdentifier: '10.123/abc', + funderIdentifierType: 'DOI', + awardTitle: 'Configured Award', + awardUri: 'https://example.org/award', + awardNumber: 'A-100', + }, + ]; - const entry = component.fundingEntries.at(0); - entry.patchValue({ - funderName: 'Test Funder', - awardTitle: 'Test Award', - awardUri: 'https://www.nsf.gov/awardsearch/showAward?AWD_ID=1234567', - }); + setup({ configFunders }); - fixture.detectChanges(); - - component.save(); - - expect(closeSpy).toHaveBeenCalledWith({ - fundingEntries: [ - { - funderName: 'Test Funder', - funderIdentifier: '', - funderIdentifierType: 'DOI', - awardTitle: 'Test Award', - awardUri: 'https://www.nsf.gov/awardsearch/showAward?AWD_ID=1234567', - awardNumber: '', - }, - ], - }); + expect(component.fundingEntries.length).toBe(1); + expect(component.fundingEntries.at(0).value).toEqual(configFunders[0]); }); - it('should not save when form is invalid', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = jest.spyOn(dialogRef, 'close'); - - component.addFundingEntry(); - const entry = component.fundingEntries.at(0); - entry.patchValue({ - funderName: '', - awardTitle: '', + it('should return loading filter message when funders are loading', () => { + setup({ + selectorOverrides: [{ selector: MetadataSelectors.getFundersLoading, value: true }], }); - component.save(); - - expect(closeSpy).not.toHaveBeenCalled(); + expect(component.filterMessage()).toBe('project.metadata.funding.dialog.loadingFunders'); }); - it('should cancel dialog', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = jest.spyOn(dialogRef, 'close'); + it('should dispatch GetFundersList after debounced search', () => { + jest.useFakeTimers(); + setup(); + (store.dispatch as jest.Mock).mockClear(); - component.cancel(); + component.onFunderSearch('open'); + jest.advanceTimersByTime(300); - expect(closeSpy).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new GetFundersList('open')); }); - it('should validate required fields', () => { - component.addFundingEntry(); - const entry = component.fundingEntries.at(0); - - const funderNameControl = entry.get('funderName'); - const awardTitleControl = entry.get('awardTitle'); + it('should not dispatch duplicate consecutive search terms', () => { + jest.useFakeTimers(); + setup(); + (store.dispatch as jest.Mock).mockClear(); - expect(funderNameControl?.hasError('required')).toBe(true); - expect(awardTitleControl?.hasError('required')).toBe(false); + component.onFunderSearch('same'); + jest.advanceTimersByTime(300); + component.onFunderSearch('same'); + jest.advanceTimersByTime(300); - funderNameControl?.setValue('Test Funder'); - awardTitleControl?.setValue('Test Award'); - - expect(funderNameControl?.hasError('required')).toBe(false); - expect(awardTitleControl?.hasError('required')).toBe(false); + expect(store.dispatch).toHaveBeenCalledTimes(1); + expect(store.dispatch).toHaveBeenCalledWith(new GetFundersList('same')); }); - it('should not update funding entry when funder is not found', () => { - const entry = component.fundingEntries.at(0); - const initialValues = { - funderName: entry.get('funderName')?.value, - funderIdentifier: entry.get('funderIdentifier')?.value, - funderIdentifierType: entry.get('funderIdentifierType')?.value, - }; + it('should return selected current entry in options when missing in list', () => { + setup({ + selectorOverrides: [{ selector: MetadataSelectors.getFundersList, value: [] }], + }); + component.fundingEntries.at(0).patchValue({ + funderName: 'Manual Funder', + funderIdentifier: 'manual-id', + }); - component.onFunderSelected('Non-existent Funder', 0); + const options = component.getOptionsForIndex(0); - expect(entry.get('funderName')?.value).toBe(initialValues.funderName); - expect(entry.get('funderIdentifier')?.value).toBe(initialValues.funderIdentifier); - expect(entry.get('funderIdentifierType')?.value).toBe(initialValues.funderIdentifierType); + expect(options).toEqual([{ id: 'manual-id', name: 'Manual Funder' }]); }); - it('should update funding entry when funder is selected from ROR list', () => { - const entry = component.fundingEntries.at(0); - - component.onFunderSelected('Test Funder', 0); - - expect(entry.get('funderName')?.value).toBe('Test Funder'); - expect(entry.get('funderIdentifier')?.value).toBe('https://ror.org/0test'); - expect(entry.get('funderIdentifierType')?.value).toBe('ROR'); - }); + it('should patch selected funder data when a funder is selected', () => { + setup(); - it('should remove funding entry when more than one exists', () => { - component.addFundingEntry(); - expect(component.fundingEntries.length).toBe(2); + component.onFunderSelected('National Science Fund', 0); - component.removeFundingEntry(0); - expect(component.fundingEntries.length).toBe(1); + expect(component.fundingEntries.at(0).get('funderName')?.value).toBe('National Science Fund'); + expect(component.fundingEntries.at(0).get('funderIdentifier')?.value).toBe('https://ror.org/222'); + expect(component.fundingEntries.at(0).get('funderIdentifierType')?.value).toBe('ROR'); }); - it('should not remove funding entry when index is out of bounds', () => { + it('should remove entry when more than one funding entry exists', () => { + setup(); component.addFundingEntry(); - const initialLength = component.fundingEntries.length; - component.removeFundingEntry(999); - expect(component.fundingEntries.length).toBe(initialLength); - }); + component.removeFundingEntry(1); - it('should create entry with supplement data when provided', () => { - const supplement = { - funderName: 'Test Funder', - funderIdentifier: 'test-id', - funderIdentifierType: 'ROR', - title: 'Test Award', - url: 'https://test.com', - awardNumber: 'AWARD-123', - }; - - component.addFundingEntry(supplement); - - const entry = component.fundingEntries.at(component.fundingEntries.length - 1); - expect(entry.get('funderName')?.value).toBe('Test Funder'); - expect(entry.get('funderIdentifier')?.value).toBe('test-id'); - expect(entry.get('funderIdentifierType')?.value).toBe('ROR'); - expect(entry.get('awardTitle')?.value).toBe('Test Award'); - expect(entry.get('awardUri')?.value).toBe('https://test.com'); - expect(entry.get('awardNumber')?.value).toBe('AWARD-123'); - }); - - it('should create entry with supplement data using awardTitle fallback', () => { - const supplement = { - funderName: 'Test Funder', - awardTitle: 'Test Award Title', - url: 'https://test.com', - }; - - component.addFundingEntry(supplement); - - const entry = component.fundingEntries.at(component.fundingEntries.length - 1); - expect(entry.get('awardTitle')?.value).toBe('Test Award Title'); + expect(component.fundingEntries.length).toBe(1); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should create entry with supplement data using awardUri fallback', () => { - const supplement = { - funderName: 'Test Funder', - awardUri: 'https://award.com', - }; - - component.addFundingEntry(supplement); + it('should close with empty result when removing the last entry', () => { + setup(); - const entry = component.fundingEntries.at(component.fundingEntries.length - 1); - expect(entry.get('awardUri')?.value).toBe('https://award.com'); - }); - - it('should create entry with default values when no supplement provided', () => { - const initialLength = component.fundingEntries.length; - component.addFundingEntry(); + component.removeFundingEntry(0); - const entry = component.fundingEntries.at(initialLength); - expect(entry.get('funderName')?.value).toBe(null); - expect(entry.get('funderIdentifier')?.value).toBe(''); - expect(entry.get('funderIdentifierType')?.value).toBe('DOI'); - expect(entry.get('awardTitle')?.value).toBe(''); - expect(entry.get('awardUri')?.value).toBe(''); - expect(entry.get('awardNumber')?.value).toBe(''); + expect(dialogRef.close).toHaveBeenCalledWith({ fundingEntries: [] }); }); - it('should dispatch getFundersList after debounce when searching', fakeAsync(() => { - const store = TestBed.inject(Store); - const dispatchSpy = jest.spyOn(store, 'dispatch'); + it('should close with funding data on save when form is valid', () => { + setup(); + component.fundingEntries.at(0).patchValue({ + funderName: 'Open Science Foundation', + funderIdentifier: 'https://ror.org/111', + funderIdentifierType: 'ROR', + awardTitle: '', + awardUri: '', + awardNumber: '', + }); - component.onFunderSearch('query'); - expect(dispatchSpy).not.toHaveBeenCalled(); - tick(300); - expect(dispatchSpy).toHaveBeenCalledWith(new GetFundersList('query')); - })); + component.save(); - it('should pre-populate entries from config funders on init', () => { - TestBed.resetTestingModule(); - TestBed.configureTestingModule({ - imports: [FundingDialogComponent, OSFTestingModule], - providers: [ - MockProviders(DynamicDialogRef, DestroyRef), - MockProvider(DynamicDialogConfig, { data: { funders: [MOCK_FUNDERS[0]] } }), - provideMockStore({ - signals: [ - { selector: MetadataSelectors.getFundersList, value: [] }, - { selector: MetadataSelectors.getFundersLoading, value: false }, - ], - }), + expect(dialogRef.close).toHaveBeenCalledWith({ + fundingEntries: [ + { + funderName: 'Open Science Foundation', + funderIdentifier: 'https://ror.org/111', + funderIdentifierType: 'ROR', + awardTitle: '', + awardUri: '', + awardNumber: '', + }, ], - }).compileComponents(); - const f = TestBed.createComponent(FundingDialogComponent); - f.detectChanges(); - const c = f.componentInstance; - expect(c.fundingEntries.length).toBe(1); - const entry = c.fundingEntries.at(0); - expect(entry.get('funderName')?.value).toBe(MOCK_FUNDERS[0].funderName); - expect(entry.get('funderIdentifier')?.value).toBe(MOCK_FUNDERS[0].funderIdentifier); - expect(entry.get('funderIdentifierType')?.value).toBe(MOCK_FUNDERS[0].funderIdentifierType); - expect(entry.get('awardTitle')?.value).toBe(MOCK_FUNDERS[0].awardTitle); - expect(entry.get('awardUri')?.value).toBe(MOCK_FUNDERS[0].awardUri); - expect(entry.get('awardNumber')?.value).toBe(MOCK_FUNDERS[0].awardNumber); + }); }); - it('getOptionsForIndex returns custom option plus list when entry name is not in list', () => { - const entry = component.fundingEntries.at(0); - entry.patchValue({ funderName: 'Custom Funder', funderIdentifier: 'custom-id' }); - const options = component.getOptionsForIndex(0); - expect(options).toHaveLength(2); - expect(options[0]).toEqual({ id: 'custom-id', name: 'Custom Funder' }); - expect(options[1]).toEqual(MOCK_ROR_FUNDERS[0]); - }); + it('should not close on save when form is invalid', () => { + setup(); - it('getOptionsForIndex returns list when entry has no name', () => { - const options = component.getOptionsForIndex(0); - expect(options).toEqual(MOCK_ROR_FUNDERS); - }); + component.save(); - it('filterMessage returns loading key when funders loading', () => { - TestBed.resetTestingModule(); - const loadingSignal = signal(true); - TestBed.configureTestingModule({ - imports: [FundingDialogComponent, OSFTestingModule], - providers: [ - MockProviders(DynamicDialogRef, DestroyRef), - MockProvider(DynamicDialogConfig, { data: { funders: [] } }), - provideMockStore({ - signals: [ - { selector: MetadataSelectors.getFundersList, value: [] }, - { selector: MetadataSelectors.getFundersLoading, value: loadingSignal }, - ], - }), - ], - }).compileComponents(); - const f = TestBed.createComponent(FundingDialogComponent); - f.detectChanges(); - expect(f.componentInstance.filterMessage()).toBe('project.metadata.funding.dialog.loadingFunders'); - loadingSignal.set(false); - expect(f.componentInstance.filterMessage()).toBe('project.metadata.funding.dialog.noFundersFound'); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('save returns only entries with at least one of funderName, awardTitle, awardUri, awardNumber', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = jest.spyOn(dialogRef, 'close'); - component.addFundingEntry(); - component.fundingEntries.at(0).patchValue({ funderName: 'Funder A', awardTitle: 'Award A' }); - component.fundingEntries.at(1).patchValue({ funderName: 'Funder B', awardTitle: 'Award B' }); - fixture.detectChanges(); - component.save(); - expect(closeSpy).toHaveBeenCalledWith({ - fundingEntries: [ - expect.objectContaining({ funderName: 'Funder A', awardTitle: 'Award A' }), - expect.objectContaining({ funderName: 'Funder B', awardTitle: 'Award B' }), - ], - }); - }); + it('should close dialog on cancel', () => { + setup(); - it('should not save when awardUri is invalid', () => { - const dialogRef = TestBed.inject(DynamicDialogRef); - const closeSpy = jest.spyOn(dialogRef, 'close'); - const entry = component.fundingEntries.at(0); - entry.patchValue({ - funderName: 'Test Funder', - awardUri: 'not-a-valid-url', - }); - fixture.detectChanges(); - component.save(); - expect(closeSpy).not.toHaveBeenCalled(); + component.cancel(); + + expect(dialogRef.close).toHaveBeenCalledWith(); }); }); diff --git a/src/app/features/metadata/dialogs/license-dialog/license-dialog.component.spec.ts b/src/app/features/metadata/dialogs/license-dialog/license-dialog.component.spec.ts index e693c2280..3bce91801 100644 --- a/src/app/features/metadata/dialogs/license-dialog/license-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/license-dialog/license-dialog.component.spec.ts @@ -11,7 +11,7 @@ import { LicensesSelectors } from '@shared/stores/licenses'; import { LicenseDialogComponent } from './license-dialog.component'; import { MOCK_LICENSE } from '@testing/mocks/license.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('LicenseDialogComponent', () => { @@ -20,8 +20,9 @@ describe('LicenseDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LicenseDialogComponent, OSFTestingModule, ...MockComponents(LoadingSpinnerComponent, LicenseComponent)], + imports: [LicenseDialogComponent, ...MockComponents(LoadingSpinnerComponent, LicenseComponent)], providers: [ + provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig), provideMockStore({ diff --git a/src/app/features/metadata/dialogs/publication-doi-dialog/publication-doi-dialog.component.spec.ts b/src/app/features/metadata/dialogs/publication-doi-dialog/publication-doi-dialog.component.spec.ts index 9e4307c9a..035f9d064 100644 --- a/src/app/features/metadata/dialogs/publication-doi-dialog/publication-doi-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/publication-doi-dialog/publication-doi-dialog.component.spec.ts @@ -6,7 +6,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PublicationDoiDialogComponent } from './publication-doi-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('PublicationDoiDialogComponent', () => { let component: PublicationDoiDialogComponent; @@ -16,8 +16,8 @@ describe('PublicationDoiDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PublicationDoiDialogComponent, OSFTestingModule], - providers: [MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], + imports: [PublicationDoiDialogComponent], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(PublicationDoiDialogComponent); diff --git a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts index e06aa5a09..8c03c2e28 100644 --- a/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts +++ b/src/app/features/metadata/dialogs/resource-information-dialog/resource-information-dialog.component.spec.ts @@ -6,7 +6,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResourceInformationDialogComponent } from './resource-information-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResourceInformationDialogComponent', () => { let component: ResourceInformationDialogComponent; @@ -14,8 +14,8 @@ describe('ResourceInformationDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ResourceInformationDialogComponent, OSFTestingModule], - providers: [MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], + imports: [ResourceInformationDialogComponent], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(ResourceInformationDialogComponent); diff --git a/src/app/features/metadata/dialogs/resource-tooltip-info/resource-tooltip-info.component.spec.ts b/src/app/features/metadata/dialogs/resource-tooltip-info/resource-tooltip-info.component.spec.ts index 4b34840d9..b38d65e71 100644 --- a/src/app/features/metadata/dialogs/resource-tooltip-info/resource-tooltip-info.component.spec.ts +++ b/src/app/features/metadata/dialogs/resource-tooltip-info/resource-tooltip-info.component.spec.ts @@ -6,7 +6,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResourceInfoTooltipComponent } from './resource-tooltip-info.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResourceInfoTooltipComponent', () => { let component: ResourceInfoTooltipComponent; @@ -16,8 +16,8 @@ describe('ResourceInfoTooltipComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ResourceInfoTooltipComponent, OSFTestingModule], - providers: [MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], + imports: [ResourceInfoTooltipComponent], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(ResourceInfoTooltipComponent); diff --git a/src/app/features/metadata/metadata.component.spec.ts b/src/app/features/metadata/metadata.component.spec.ts index 93c00c539..501952227 100644 --- a/src/app/features/metadata/metadata.component.spec.ts +++ b/src/app/features/metadata/metadata.component.spec.ts @@ -29,7 +29,7 @@ import { RegistrationProviderSelectors } from '@osf/shared/stores/registration-p import { MetadataComponent } from './metadata.component'; import { MOCK_PROJECT_METADATA } from '@testing/mocks/project-metadata.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; @@ -92,9 +92,9 @@ describe('MetadataComponent', () => { MetadataTitleComponent, MetadataRegistrationDoiComponent ), - OSFTestingModule, ], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(Router, routerMock), MockProvider(CustomDialogService, customDialogServiceMock), diff --git a/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts b/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts index b6e1594c4..b0cb6f095 100644 --- a/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts +++ b/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts @@ -15,7 +15,7 @@ import { AddMetadataComponent } from './add-metadata.component'; import { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from '@testing/mocks/cedar-metadata-data-template-json-api.mock'; import { MOCK_CEDAR_METADATA_RECORD_DATA } from '@testing/mocks/cedar-metadata-record.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; @@ -69,10 +69,10 @@ describe('AddMetadataComponent', () => { await TestBed.configureTestingModule({ imports: [ AddMetadataComponent, - OSFTestingModule, ...MockComponents(SubHeaderComponent, CedarTemplateFormComponent, LoadingSpinnerComponent), ], providers: [ + provideOSFCore(), MockProvider(Router, router), MockProvider(ActivatedRoute, activatedRoute), MockProvider(ToastService, toastService), diff --git a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts index 40a6e07a0..982de1de6 100644 --- a/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts +++ b/src/app/features/moderation/components/add-moderator-dialog/add-moderator-dialog.component.spec.ts @@ -1,183 +1,200 @@ import { Store } from '@ngxs/store'; -import { MockComponents, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { PaginatorState } from 'primeng/paginator'; -import { signal } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; -import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; -import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; - -import { AddModeratorType } from '../../enums'; -import { ModeratorAddModel } from '../../models'; -import { ModeratorsSelectors } from '../../store/moderators'; +import { AddModeratorType, ModeratorPermission } from '../../enums'; +import { ModeratorAddModel, ModeratorDialogAddModel } from '../../models'; +import { ClearUsers, ModeratorsSelectors, SearchUsers, SearchUsersPageChange } from '../../store/moderators'; import { AddModeratorDialogComponent } from './add-moderator-dialog.component'; -import { MOCK_USER } from '@testing/mocks/data.mock'; -import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; describe('AddModeratorDialogComponent', () => { let component: AddModeratorDialogComponent; let fixture: ComponentFixture; - let dialogRef: jest.Mocked; - let dialogConfig: DynamicDialogConfig; + let dialogRef: DynamicDialogRef; let store: Store; - const mockUsers = [MOCK_USER]; - - beforeEach(async () => { - dialogRef = DynamicDialogRefMock.useValue as unknown as jest.Mocked; - - dialogConfig = { - data: [], - } as DynamicDialogConfig; - - await TestBed.configureTestingModule({ - imports: [ - AddModeratorDialogComponent, - OSFTestingModule, - ...MockComponents(SearchInputComponent, LoadingSpinnerComponent, CustomPaginatorComponent), - ], + const mockUsers: ModeratorAddModel[] = [ + { + id: 'u1', + fullName: 'User One', + email: 'user.one@example.org', + permission: ModeratorPermission.Moderator, + }, + { + id: 'u2', + fullName: 'User Two', + email: 'user.two@example.org', + permission: ModeratorPermission.Admin, + }, + ]; + + const defaultSignals: SignalOverride[] = [ + { selector: ModeratorsSelectors.getUsers, value: mockUsers }, + { selector: ModeratorsSelectors.isUsersLoading, value: false }, + { selector: ModeratorsSelectors.getUsersTotalCount, value: 20 }, + { selector: ModeratorsSelectors.getUsersNextLink, value: '/users?page=2' }, + { selector: ModeratorsSelectors.getUsersPreviousLink, value: '/users?page=1' }, + ]; + + function setup(overrides: BaseSetupOverrides = {}) { + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + + TestBed.configureTestingModule({ + imports: [AddModeratorDialogComponent], providers: [ - DynamicDialogRefMock, - MockProvider(DynamicDialogConfig, dialogConfig), - provideMockStore({ - signals: [ - { selector: ModeratorsSelectors.getUsers, value: signal(mockUsers) }, - { selector: ModeratorsSelectors.isUsersLoading, value: false }, - { selector: ModeratorsSelectors.getUsersTotalCount, value: 2 }, - { selector: ModeratorsSelectors.getUsersNextLink, value: signal(null) }, - { selector: ModeratorsSelectors.getUsersPreviousLink, value: signal(null) }, - ], - }), + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { data: {} }), + provideMockStore({ signals }), ], - }).compileComponents(); + }); store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(AddModeratorDialogComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should initialize with default values', () => { - expect(component.isInitialState()).toBe(true); - expect(component.currentPage()).toBe(1); - expect(component.first()).toBe(0); - expect(component.rows()).toBe(10); - expect(component.selectedUsers()).toEqual([]); - expect(component.searchControl.value).toBe(''); - }); + it('should close with selected users on addModerator', () => { + setup(); + component.selectedUsers.set([mockUsers[0]]); - it('should load users from store', () => { - expect(component.users()).toEqual(mockUsers); - expect(component.isLoading()).toBe(false); - expect(component.totalUsersCount()).toBe(2); + component.addModerator(); + + const expected: ModeratorDialogAddModel = { data: [mockUsers[0]], type: AddModeratorType.Search }; + expect(dialogRef.close).toHaveBeenCalledWith(expected); }); - it('should close dialog with correct data for addModerator', () => { - const mockSelectedUsers: ModeratorAddModel[] = [ - { - id: '1', - fullName: 'John Doe', - email: 'john@example.com', - permission: 'read' as any, - }, - ]; - component.selectedUsers.set(mockSelectedUsers); + it('should close with invite type on inviteModerator', () => { + setup(); - component.addModerator(); + component.inviteModerator(); - expect(dialogRef.close).toHaveBeenCalledWith({ - data: mockSelectedUsers, - type: AddModeratorType.Search, - }); + const expected: ModeratorDialogAddModel = { data: [], type: AddModeratorType.Invite }; + expect(dialogRef.close).toHaveBeenCalledWith(expected); }); - it('should close dialog with correct data for inviteModerator', () => { - component.inviteModerator(); + it('should dispatch ClearUsers on destroy', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); - expect(dialogRef.close).toHaveBeenCalledWith({ - data: [], - type: AddModeratorType.Invite, - }); + component.ngOnDestroy(); + + expect(store.dispatch).toHaveBeenCalledWith(new ClearUsers()); }); - it('should handle pagination correctly', () => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); + it('should dispatch SearchUsers for first page when search term exists', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.searchControl.setValue('alice'); - component.pageChanged({ first: 0 } as PaginatorState); - expect(dispatchSpy).not.toHaveBeenCalled(); + const pageEvent: PaginatorState = { page: 0, first: 0, rows: 10, pageCount: 2 }; + component.pageChanged(pageEvent); - component.searchControl.setValue('test'); - component.pageChanged({ page: 0, first: 0, rows: 10 } as PaginatorState); - expect(dispatchSpy).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new SearchUsers('alice')); expect(component.currentPage()).toBe(1); expect(component.first()).toBe(0); }); - it('should navigate to next page when link is available', () => { - const nextLink = 'http://api.example.com/users?page=3'; - const originalSelect = store.select.bind(store); - (store.select as jest.Mock) = jest.fn((selector) => { - if (selector === ModeratorsSelectors.getUsersNextLink) { - return signal(nextLink); - } - return originalSelect(selector); - }); + it('should not dispatch first-page search when search term is empty', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.searchControl.setValue(' '); + + const pageEvent: PaginatorState = { page: 0, first: 0, rows: 10, pageCount: 2 }; + component.pageChanged(pageEvent); - Object.defineProperty(component, 'usersNextLink', { - get: () => signal(nextLink), - configurable: true, + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + it('should dispatch SearchUsersPageChange with next link', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + + const pageEvent: PaginatorState = { page: 1, first: 10, rows: 10, pageCount: 2 }; + component.pageChanged(pageEvent); + + expect(store.dispatch).toHaveBeenCalledWith(new SearchUsersPageChange('/users?page=2')); + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); + }); + + it('should dispatch SearchUsersPageChange with previous link', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.currentPage.set(3); + + const pageEvent: PaginatorState = { page: 1, first: 10, rows: 10, pageCount: 3 }; + component.pageChanged(pageEvent); + + expect(store.dispatch).toHaveBeenCalledWith(new SearchUsersPageChange('/users?page=1')); + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); + }); + + it('should not dispatch page change when link is missing', () => { + setup({ + selectorOverrides: [{ selector: ModeratorsSelectors.getUsersNextLink, value: null }], }); + (store.dispatch as jest.Mock).mockClear(); - const dispatchSpy = jest.spyOn(store, 'dispatch'); - component.currentPage.set(2); - component.pageChanged({ page: 2, first: 20, rows: 10 } as PaginatorState); + const pageEvent: PaginatorState = { page: 1, first: 10, rows: 10, pageCount: 2 }; + component.pageChanged(pageEvent); - expect(dispatchSpy).toHaveBeenCalled(); - expect(component.currentPage()).toBe(3); - expect(component.first()).toBe(20); + expect(store.dispatch).not.toHaveBeenCalled(); }); - it('should debounce and filter search input', fakeAsync(() => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); + it('should debounce search and clear selected users', () => { + jest.useFakeTimers(); + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.selectedUsers.set([mockUsers[0]]); - component.searchControl.setValue('t'); - tick(200); - component.searchControl.setValue('test'); - tick(500); + component.searchControl.setValue('john'); + jest.advanceTimersByTime(500); - expect(dispatchSpy).toHaveBeenCalledTimes(1); + expect(store.dispatch).toHaveBeenCalledWith(new SearchUsers('john')); expect(component.isInitialState()).toBe(false); expect(component.selectedUsers()).toEqual([]); - })); - it('should not search empty or whitespace values', fakeAsync(() => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); + jest.useRealTimers(); + }); - component.searchControl.setValue(''); - tick(500); - expect(dispatchSpy).not.toHaveBeenCalled(); + it('should not dispatch duplicate consecutive search terms', () => { + jest.useFakeTimers(); + setup(); + (store.dispatch as jest.Mock).mockClear(); - component.searchControl.setValue(' '); - tick(500); - expect(dispatchSpy).not.toHaveBeenCalled(); - })); + component.searchControl.setValue('same'); + jest.advanceTimersByTime(500); + component.searchControl.setValue('same'); + jest.advanceTimersByTime(500); - it('should clear users on destroy', () => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); - component.ngOnDestroy(); - expect(dispatchSpy).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledTimes(1); + expect(store.dispatch).toHaveBeenCalledWith(new SearchUsers('same')); + + jest.useRealTimers(); }); }); diff --git a/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts b/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts index 54dc588b9..8223ff10c 100644 --- a/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts +++ b/src/app/features/moderation/components/bulk-upload/bulk-upload.component.spec.ts @@ -4,7 +4,7 @@ import { BYTES_IN_MB, FILE_TYPES } from '../../constants'; import { BulkUploadComponent } from './bulk-upload.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('BulkUploadComponent', () => { let component: BulkUploadComponent; @@ -12,7 +12,8 @@ describe('BulkUploadComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [BulkUploadComponent, OSFTestingModule], + imports: [BulkUploadComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(BulkUploadComponent); diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts index cf5243f73..0738975f2 100644 --- a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts @@ -16,7 +16,7 @@ import { CollectionsModerationSelectors } from '../../store/collections-moderati import { CollectionModerationSubmissionsComponent } from './collection-moderation-submissions.component'; import { MOCK_PROVIDER } from '@testing/mocks/provider.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -45,7 +45,6 @@ describe('CollectionModerationSubmissionsComponent', () => { await TestBed.configureTestingModule({ imports: [ CollectionModerationSubmissionsComponent, - OSFTestingModule, ...MockComponents( SelectComponent, CollectionSubmissionsListComponent, @@ -55,6 +54,7 @@ describe('CollectionModerationSubmissionsComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(Router, mockRouter), MockProvider(ActivatedRoute, mockActivatedRoute), provideMockStore({ diff --git a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts index 81c1c24db..d0a5af744 100644 --- a/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts +++ b/src/app/features/moderation/components/collection-submission-item/collection-submission-item.component.spec.ts @@ -13,7 +13,7 @@ import { SubmissionReviewStatus } from '../../enums'; import { CollectionSubmissionItemComponent } from './collection-submission-item.component'; import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -36,13 +36,9 @@ describe('CollectionSubmissionItemComponent', () => { mockActivatedRoute = ActivatedRouteMockBuilder.create().withQueryParams({ status: 'pending' }).build(); await TestBed.configureTestingModule({ - imports: [ - CollectionSubmissionItemComponent, - OSFTestingModule, - ...MockComponents(IconComponent), - MockPipe(DateAgoPipe), - ], + imports: [CollectionSubmissionItemComponent, ...MockComponents(IconComponent), MockPipe(DateAgoPipe)], providers: [ + provideOSFCore(), MockProvider(Router, mockRouter), MockProvider(ActivatedRoute, mockActivatedRoute), provideMockStore({ diff --git a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts index aad71b9cb..2efb6913b 100644 --- a/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts +++ b/src/app/features/moderation/components/collection-submission-overview/collection-submission-overview.component.spec.ts @@ -8,7 +8,7 @@ import { Mode } from '@osf/shared/enums/mode.enum'; import { CollectionSubmissionOverviewComponent } from './collection-submission-overview.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -23,8 +23,9 @@ describe('CollectionSubmissionOverviewComponent', () => { mockActivatedRoute = ActivatedRouteMockBuilder.create().withQueryParams({ mode: Mode.Moderation }).build(); await TestBed.configureTestingModule({ - imports: [CollectionSubmissionOverviewComponent, OSFTestingModule, ...MockComponents(ProjectOverviewComponent)], + imports: [CollectionSubmissionOverviewComponent, ...MockComponents(ProjectOverviewComponent)], providers: [ + provideOSFCore(), { provide: Router, useValue: mockRouter }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, ], diff --git a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts index e86fbea75..384c818c7 100644 --- a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts +++ b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts @@ -9,7 +9,7 @@ import { CollectionsModerationSelectors } from '../../store/collections-moderati import { CollectionSubmissionsListComponent } from './collection-submissions-list.component'; import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('CollectionSubmissionsListComponent', () => { @@ -20,8 +20,9 @@ describe('CollectionSubmissionsListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CollectionSubmissionsListComponent, OSFTestingModule, MockComponent(CollectionSubmissionItemComponent)], + imports: [CollectionSubmissionsListComponent, MockComponent(CollectionSubmissionItemComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: CollectionsModerationSelectors.getCollectionSubmissions, value: mockSubmissions }], }), diff --git a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts index fe6f3d8ea..188e371e2 100644 --- a/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts +++ b/src/app/features/moderation/components/invite-moderator-dialog/invite-moderator-dialog.component.spec.ts @@ -11,8 +11,8 @@ import { ModeratorPermission } from '../../enums'; import { InviteModeratorDialogComponent } from './invite-moderator-dialog.component'; -import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { DynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; describe('InviteModeratorDialogComponent', () => { let component: InviteModeratorDialogComponent; @@ -23,12 +23,8 @@ describe('InviteModeratorDialogComponent', () => { mockDialogRef = DynamicDialogRefMock.useValue as unknown as jest.Mocked; await TestBed.configureTestingModule({ - imports: [ - InviteModeratorDialogComponent, - OSFTestingModule, - ...MockComponents(TextInputComponent, FormSelectComponent), - ], - providers: [DynamicDialogRefMock], + imports: [InviteModeratorDialogComponent, ...MockComponents(TextInputComponent, FormSelectComponent)], + providers: [provideOSFCore(), DynamicDialogRefMock], }).compileComponents(); fixture = TestBed.createComponent(InviteModeratorDialogComponent); diff --git a/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts b/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts index f19023271..77d3ffac2 100644 --- a/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts +++ b/src/app/features/moderation/components/moderators-list/moderators-list.component.spec.ts @@ -11,6 +11,7 @@ import { SearchInputComponent } from '@osf/shared/components/search-input/search import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { ToastService } from '@osf/shared/services/toast.service'; import { ModeratorPermission } from '../../enums'; import { ModeratorModel } from '../../models'; @@ -21,8 +22,7 @@ import { ModeratorsListComponent } from './moderators-list.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; import { MOCK_MODERATORS } from '@testing/mocks/moderator.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; @@ -49,17 +49,14 @@ describe('ModeratorsListComponent', () => { customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); - await TestBed.configureTestingModule({ - imports: [ - ModeratorsListComponent, - OSFTestingModule, - ...MockComponents(ModeratorsTableComponent, SearchInputComponent), - ], + TestBed.configureTestingModule({ + imports: [ModeratorsListComponent, ...MockComponents(ModeratorsTableComponent, SearchInputComponent)], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(CustomConfirmationService, customConfirmationServiceMock), MockProvider(CustomDialogService, mockCustomDialogService), - TranslateServiceMock, + MockProvider(ToastService), provideMockStore({ signals: [ { selector: UserSelectors.getCurrentUser, value: mockCurrentUser }, @@ -69,7 +66,7 @@ describe('ModeratorsListComponent', () => { ], }), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(ModeratorsListComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts b/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts index 0c8fac6ce..5e8ba1c3a 100644 --- a/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts +++ b/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts @@ -11,7 +11,7 @@ import { ModeratorModel } from '../../models'; import { ModeratorsTableComponent } from './moderators-table.component'; import { MOCK_MODERATORS } from '@testing/mocks/moderator.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; describe('ModeratorsTableComponent', () => { @@ -35,8 +35,8 @@ describe('ModeratorsTableComponent', () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ModeratorsTableComponent, OSFTestingModule, MockComponent(SelectComponent)], - providers: [MockProvider(CustomDialogService, mockCustomDialogService)], + imports: [ModeratorsTableComponent, MockComponent(SelectComponent)], + providers: [provideOSFCore(), MockProvider(CustomDialogService, mockCustomDialogService)], }).compileComponents(); fixture = TestBed.createComponent(ModeratorsTableComponent); diff --git a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts index 785c5a7a4..eed9eaaa6 100644 --- a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts +++ b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts @@ -1,11 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { PreprintModerationTab } from '../../enums'; import { MyReviewingNavigationComponent } from './my-reviewing-navigation.component'; import { MOCK_PROVIDER } from '@testing/mocks/provider.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MyReviewingNavigationComponent', () => { let component: MyReviewingNavigationComponent; @@ -14,9 +15,10 @@ describe('MyReviewingNavigationComponent', () => { const mockProvider = MOCK_PROVIDER; beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [MyReviewingNavigationComponent, OSFTestingModule], - }).compileComponents(); + TestBed.configureTestingModule({ + imports: [MyReviewingNavigationComponent], + providers: [provideOSFCore(), provideRouter([])], + }); fixture = TestBed.createComponent(MyReviewingNavigationComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts b/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts index a5411f8d7..64d17e862 100644 --- a/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts +++ b/src/app/features/moderation/components/notification-settings/notification-settings.component.spec.ts @@ -1,17 +1,19 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { NotificationSettingsComponent } from './notification-settings.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('NotificationSettingsComponent', () => { let component: NotificationSettingsComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [NotificationSettingsComponent, OSFTestingModule], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NotificationSettingsComponent], + providers: [provideOSFCore(), provideRouter([])], + }); fixture = TestBed.createComponent(NotificationSettingsComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts b/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts index 155d6bf3e..abf4fcd93 100644 --- a/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts +++ b/src/app/features/moderation/components/preprint-moderation-settings/preprint-moderation-settings.component.spec.ts @@ -11,9 +11,8 @@ import { PreprintModerationSelectors } from '../../store/preprint-moderation'; import { PreprintModerationSettingsComponent } from './preprint-moderation-settings.component'; -import { EnvironmentTokenMock } from '@testing/mocks/environment.token.mock'; import { MOCK_PREPRINT_PROVIDER_MODERATION_INFO } from '@testing/mocks/preprint-provider-moderation-info.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -29,10 +28,10 @@ describe('PreprintModerationSettingsComponent', () => { mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ providerId: mockProviderId }).build(); await TestBed.configureTestingModule({ - imports: [PreprintModerationSettingsComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], + imports: [PreprintModerationSettingsComponent, MockComponent(LoadingSpinnerComponent)], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, mockActivatedRoute), - EnvironmentTokenMock, provideMockStore({ signals: [ { selector: PreprintModerationSelectors.arePreprintProviderLoading, value: false }, diff --git a/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts b/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts index b1f8bf48e..60030c0e1 100644 --- a/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts +++ b/src/app/features/moderation/components/preprint-recent-activity-list/preprint-recent-activity-list.component.spec.ts @@ -10,7 +10,7 @@ import { PreprintReviewActionModel } from '../../models'; import { PreprintRecentActivityListComponent } from './preprint-recent-activity-list.component'; import { MOCK_PREPRINT_REVIEW_ACTIONS } from '@testing/mocks/preprint-review-action.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('PreprintRecentActivityListComponent', () => { let component: PreprintRecentActivityListComponent; @@ -20,11 +20,8 @@ describe('PreprintRecentActivityListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - PreprintRecentActivityListComponent, - OSFTestingModule, - ...MockComponents(IconComponent, CustomPaginatorComponent), - ], + imports: [PreprintRecentActivityListComponent, ...MockComponents(IconComponent, CustomPaginatorComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(PreprintRecentActivityListComponent); diff --git a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts index d5dd8258b..db91c8d68 100644 --- a/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts +++ b/src/app/features/moderation/components/preprint-submission-item/preprint-submission-item.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipes } from 'ng-mocks'; +import { MockComponents, MockPipe } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -13,7 +12,7 @@ import { PreprintSubmissionModel } from '../../models'; import { PreprintSubmissionItemComponent } from './preprint-submission-item.component'; import { MOCK_PREPRINT_SUBMISSION } from '@testing/mocks/submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('PreprintSubmissionItemComponent', () => { let component: PreprintSubmissionItemComponent; @@ -25,10 +24,10 @@ describe('PreprintSubmissionItemComponent', () => { await TestBed.configureTestingModule({ imports: [ PreprintSubmissionItemComponent, - OSFTestingModule, ...MockComponents(IconComponent, ContributorsListComponent), - MockPipes(DateAgoPipe, TranslatePipe), + MockPipe(DateAgoPipe), ], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(PreprintSubmissionItemComponent); diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts index 647cf91e8..6af4d0c30 100644 --- a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts @@ -21,7 +21,7 @@ import { PreprintSubmissionItemComponent } from '../preprint-submission-item/pre import { PreprintSubmissionsComponent } from '..'; import { MOCK_PREPRINT_SUBMISSIONS } from '@testing/mocks/preprint-submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -36,17 +36,16 @@ describe('PreprintSubmissionsComponent', () => { const mockProviderId = 'test-provider-id'; const mockSubmissions: PreprintSubmissionModel[] = MOCK_PREPRINT_SUBMISSIONS; - beforeEach(async () => { + beforeEach(() => { mockRouter = RouterMockBuilder.create().build(); mockActivatedRoute = ActivatedRouteMockBuilder.create() .withParams({ providerId: mockProviderId }) .withQueryParams({ status: 'pending' }) .build(); - await TestBed.configureTestingModule({ + TestBed.configureTestingModule({ imports: [ PreprintSubmissionsComponent, - OSFTestingModule, ...MockComponents( SelectComponent, IconComponent, @@ -56,6 +55,7 @@ describe('PreprintSubmissionsComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(Router, mockRouter), MockProvider(ActivatedRoute, mockActivatedRoute), provideMockStore({ @@ -69,7 +69,7 @@ describe('PreprintSubmissionsComponent', () => { ], }), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(PreprintSubmissionsComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts index 97a2e3707..7ce4099eb 100644 --- a/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts +++ b/src/app/features/moderation/components/preprint-withdrawal-submissions/preprint-withdrawal-submissions.component.spec.ts @@ -22,7 +22,7 @@ import { PreprintSubmissionItemComponent } from '../preprint-submission-item/pre import { PreprintWithdrawalSubmissionsComponent } from './preprint-withdrawal-submissions.component'; import { MOCK_PREPRINT_WITHDRAWAL_SUBMISSIONS } from '@testing/mocks/preprint-withdrawal-submission.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -58,7 +58,6 @@ describe('PreprintWithdrawalSubmissionsComponent', () => { await TestBed.configureTestingModule({ imports: [ PreprintWithdrawalSubmissionsComponent, - OSFTestingModule, ...MockComponents( SelectComponent, IconComponent, @@ -68,6 +67,7 @@ describe('PreprintWithdrawalSubmissionsComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(Router, mockRouter), MockProvider(ActivatedRoute, mockActivatedRoute), provideMockStore({ diff --git a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts index 666cc14db..41dc208cb 100644 --- a/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts +++ b/src/app/features/moderation/components/registry-pending-submissions/registry-pending-submissions.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; @@ -17,7 +16,7 @@ import { RegistrySubmissionItemComponent } from '../registry-submission-item/reg import { RegistryPendingSubmissionsComponent } from './registry-pending-submissions.component'; import { MOCK_REGISTRY_MODERATIONS } from '@testing/mocks/registry-moderation.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -41,7 +40,6 @@ describe('RegistryPendingSubmissionsComponent', () => { await TestBed.configureTestingModule({ imports: [ RegistryPendingSubmissionsComponent, - OSFTestingModule, ...MockComponents( SelectComponent, IconComponent, @@ -49,9 +47,9 @@ describe('RegistryPendingSubmissionsComponent', () => { RegistrySubmissionItemComponent, CustomPaginatorComponent ), - MockPipe(TranslatePipe), ], providers: [ + provideOSFCore(), MockProvider(Router, mockRouter), MockProvider(ActivatedRoute, mockActivatedRoute), provideMockStore({ diff --git a/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts b/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts index dd06d8fdc..0288a667f 100644 --- a/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts +++ b/src/app/features/moderation/components/registry-settings/registry-settings.component.spec.ts @@ -1,20 +1,19 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { RegistrySettingsComponent } from './registry-settings.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RegistrySettingsComponent', () => { let component: RegistrySettingsComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [RegistrySettingsComponent, OSFTestingModule, MockPipe(TranslatePipe)], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RegistrySettingsComponent], + providers: [provideOSFCore(), provideRouter([])], + }); fixture = TestBed.createComponent(RegistrySettingsComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts index a4c08224b..537f1f5fd 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.spec.ts @@ -1,4 +1,3 @@ -import { TranslatePipe } from '@ngx-translate/core'; import { MockComponents, MockPipe } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -12,7 +11,7 @@ import { RegistryModeration } from '../../models'; import { RegistrySubmissionItemComponent } from './registry-submission-item.component'; import { MOCK_REGISTRY_MODERATIONS } from '@testing/mocks/registry-moderation.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RegistrySubmissionItemComponent', () => { let component: RegistrySubmissionItemComponent; @@ -22,13 +21,8 @@ describe('RegistrySubmissionItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - RegistrySubmissionItemComponent, - OSFTestingModule, - ...MockComponents(IconComponent), - MockPipe(DateAgoPipe), - MockPipe(TranslatePipe), - ], + imports: [RegistrySubmissionItemComponent, ...MockComponents(IconComponent), MockPipe(DateAgoPipe)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(RegistrySubmissionItemComponent); diff --git a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts index 93b331b09..4a6bb0be3 100644 --- a/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts +++ b/src/app/features/moderation/components/registry-submission-item/registry-submission-item.component.ts @@ -7,10 +7,10 @@ import { DatePipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; +import { FunderAwardsListComponent } from '@osf/shared/components/funder-awards-list/funder-awards-list.component'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { TruncatedTextComponent } from '@osf/shared/components/truncated-text/truncated-text.component'; import { DateAgoPipe } from '@osf/shared/pipes/date-ago.pipe'; -import { FunderAwardsListComponent } from '@shared/funder-awards-list/funder-awards-list.component'; import { REGISTRY_ACTION_LABEL, ReviewStatusIcon } from '../../constants'; import { ActionStatus, SubmissionReviewStatus } from '../../enums'; diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts index 9bb0297db..ecb4a78f3 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts @@ -16,7 +16,7 @@ import { RegistryModerationSelectors } from '../../store/registry-moderation'; import { RegistrySubmissionsComponent } from './registry-submissions.component'; import { MOCK_REGISTRY_MODERATIONS } from '@testing/mocks/registry-moderation.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -40,7 +40,6 @@ describe('RegistrySubmissionsComponent', () => { await TestBed.configureTestingModule({ imports: [ RegistrySubmissionsComponent, - OSFTestingModule, ...MockComponents( SelectComponent, IconComponent, @@ -50,6 +49,7 @@ describe('RegistrySubmissionsComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(Router, mockRouter), MockProvider(ActivatedRoute, mockActivatedRoute), provideMockStore({ diff --git a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts index 7755fc5f6..0f271e900 100644 --- a/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts +++ b/src/app/features/moderation/pages/collection-moderation/collection-moderation.component.spec.ts @@ -13,9 +13,10 @@ import { CollectionModerationTab } from '../../enums'; import { CollectionModerationComponent } from './collection-moderation.component'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('Component: Collection Moderation', () => { let component: CollectionModerationComponent; @@ -24,7 +25,7 @@ describe('Component: Collection Moderation', () => { let mockRouter: ReturnType; let mockActivatedRoute: ReturnType; - beforeEach(async () => { + beforeEach(() => { isMediumSubject = new BehaviorSubject(true); mockRouter = RouterMockBuilder.create().build(); mockActivatedRoute = ActivatedRouteMockBuilder.create() @@ -32,18 +33,16 @@ describe('Component: Collection Moderation', () => { .withData({ tab: CollectionModerationTab.AllItems }) .build(); - await TestBed.configureTestingModule({ - imports: [ - CollectionModerationComponent, - OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, SelectComponent), - ], + TestBed.configureTestingModule({ + imports: [CollectionModerationComponent, ...MockComponents(SubHeaderComponent, SelectComponent)], providers: [ + provideOSFCore(), + provideMockStore(), MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(Router, mockRouter), MockProvider(IS_MEDIUM, isMediumSubject), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(CollectionModerationComponent); component = fixture.componentInstance; @@ -75,12 +74,9 @@ describe('Component: Collection Moderation', () => { }); await TestBed.configureTestingModule({ - imports: [ - CollectionModerationComponent, - OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, SelectComponent), - ], + imports: [CollectionModerationComponent, ...MockComponents(SubHeaderComponent, SelectComponent)], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, routeWithFirstChild), MockProvider(Router, mockRouter), MockProvider(IS_MEDIUM, isMediumSubject), @@ -99,12 +95,9 @@ describe('Component: Collection Moderation', () => { const routeWithoutProviderId = ActivatedRouteMockBuilder.create().withParams({}).build(); await TestBed.configureTestingModule({ - imports: [ - CollectionModerationComponent, - OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, SelectComponent), - ], + imports: [CollectionModerationComponent, ...MockComponents(SubHeaderComponent, SelectComponent)], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, routeWithoutProviderId), MockProvider(Router, mockRouter), MockProvider(IS_MEDIUM, isMediumSubject), diff --git a/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts b/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts index cd84b78ef..cbde21198 100644 --- a/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts +++ b/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts @@ -14,7 +14,7 @@ import { MyPreprintReviewingComponent } from './my-preprint-reviewing.component' import { MOCK_PREPRINT_PROVIDER_MODERATION_INFO } from '@testing/mocks/preprint-provider-moderation-info.mock'; import { MOCK_PREPRINT_REVIEW_ACTIONS } from '@testing/mocks/preprint-review-action.mock'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('MyPreprintReviewingComponent', () => { @@ -24,14 +24,14 @@ describe('MyPreprintReviewingComponent', () => { const mockPreprintProviders = [MOCK_PREPRINT_PROVIDER_MODERATION_INFO]; const mockPreprintReviews = MOCK_PREPRINT_REVIEW_ACTIONS; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [ MyPreprintReviewingComponent, - OSFTestingStoreModule, ...MockComponents(SubHeaderComponent, PreprintRecentActivityListComponent, MyReviewingNavigationComponent), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: PreprintModerationSelectors.getPreprintProviders, value: mockPreprintProviders }, @@ -42,7 +42,7 @@ describe('MyPreprintReviewingComponent', () => { ], }), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(MyPreprintReviewingComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/pages/preprint-moderation/preprint-moderation.component.spec.ts b/src/app/features/moderation/pages/preprint-moderation/preprint-moderation.component.spec.ts index a190edfc6..4963141dd 100644 --- a/src/app/features/moderation/pages/preprint-moderation/preprint-moderation.component.spec.ts +++ b/src/app/features/moderation/pages/preprint-moderation/preprint-moderation.component.spec.ts @@ -13,9 +13,10 @@ import { PreprintModerationTab } from '../../enums'; import { PreprintModerationComponent } from './preprint-moderation.component'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('PreprintModerationComponent', () => { let component: PreprintModerationComponent; @@ -24,7 +25,7 @@ describe('PreprintModerationComponent', () => { let mockRouter: ReturnType; let mockActivatedRoute: ReturnType; - beforeEach(async () => { + beforeEach(() => { isMediumSubject = new BehaviorSubject(true); mockRouter = RouterMockBuilder.create().build(); mockActivatedRoute = ActivatedRouteMockBuilder.create() @@ -32,18 +33,16 @@ describe('PreprintModerationComponent', () => { .withData({ tab: PreprintModerationTab.Submissions }) .build(); - await TestBed.configureTestingModule({ - imports: [ - PreprintModerationComponent, - OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, SelectComponent), - ], + TestBed.configureTestingModule({ + imports: [PreprintModerationComponent, ...MockComponents(SubHeaderComponent, SelectComponent)], providers: [ + provideOSFCore(), + provideMockStore(), MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(Router, mockRouter), MockProvider(IS_MEDIUM, isMediumSubject), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(PreprintModerationComponent); component = fixture.componentInstance; @@ -76,12 +75,9 @@ describe('PreprintModerationComponent', () => { }); await TestBed.configureTestingModule({ - imports: [ - PreprintModerationComponent, - OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, SelectComponent), - ], + imports: [PreprintModerationComponent, ...MockComponents(SubHeaderComponent, SelectComponent)], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, routeWithFirstChild), MockProvider(Router, mockRouter), MockProvider(IS_MEDIUM, isMediumSubject), @@ -162,12 +158,9 @@ describe('PreprintModerationComponent', () => { }); await TestBed.configureTestingModule({ - imports: [ - PreprintModerationComponent, - OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, SelectComponent), - ], + imports: [PreprintModerationComponent, ...MockComponents(SubHeaderComponent, SelectComponent)], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, routeWithoutFirstChild), MockProvider(Router, mockRouter), MockProvider(IS_MEDIUM, isMediumSubject), diff --git a/src/app/features/moderation/pages/registries-moderation/registries-moderation.component.spec.ts b/src/app/features/moderation/pages/registries-moderation/registries-moderation.component.spec.ts index 32bca7f3c..b0e472ea4 100644 --- a/src/app/features/moderation/pages/registries-moderation/registries-moderation.component.spec.ts +++ b/src/app/features/moderation/pages/registries-moderation/registries-moderation.component.spec.ts @@ -13,9 +13,10 @@ import { RegistryModerationTab } from '../../enums'; import { RegistriesModerationComponent } from './registries-moderation.component'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('RegistriesModerationComponent', () => { let component: RegistriesModerationComponent; @@ -24,7 +25,7 @@ describe('RegistriesModerationComponent', () => { let mockRouter: ReturnType; let mockActivatedRoute: ReturnType; - beforeEach(async () => { + beforeEach(() => { isMediumSubject = new BehaviorSubject(true); mockRouter = RouterMockBuilder.create().build(); mockActivatedRoute = ActivatedRouteMockBuilder.create() @@ -32,18 +33,16 @@ describe('RegistriesModerationComponent', () => { .withData({ tab: RegistryModerationTab.Submitted }) .build(); - await TestBed.configureTestingModule({ - imports: [ - RegistriesModerationComponent, - OSFTestingStoreModule, - ...MockComponents(SubHeaderComponent, SelectComponent), - ], + TestBed.configureTestingModule({ + imports: [RegistriesModerationComponent, ...MockComponents(SubHeaderComponent, SelectComponent)], providers: [ + provideOSFCore(), + provideMockStore(), MockProvider(ActivatedRoute, mockActivatedRoute), MockProvider(Router, mockRouter), MockProvider(IS_MEDIUM, isMediumSubject), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(RegistriesModerationComponent); component = fixture.componentInstance; diff --git a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts index 2f1152278..73343f7d3 100644 --- a/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts +++ b/src/app/features/my-projects/components/create-project-dialog/create-project-dialog.component.spec.ts @@ -1,90 +1,133 @@ import { Store } from '@ngxs/store'; -import { MockComponent, MockProvider } from 'ng-mocks'; - -import { DynamicDialogRef } from 'primeng/dynamicdialog'; - -import { of } from 'rxjs'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AddProjectFormComponent } from '@osf/shared/components/add-project-form/add-project-form.component'; +import { UserSelectors } from '@core/store/user'; import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { ProjectFormControls } from '@osf/shared/enums/create-project-form-controls.enum'; +import { InstitutionsSelectors } from '@osf/shared/stores/institutions'; import { CreateProject, GetMyProjects, MyResourcesSelectors } from '@osf/shared/stores/my-resources'; +import { ProjectsSelectors } from '@osf/shared/stores/projects'; +import { RegionsSelectors } from '@osf/shared/stores/regions'; import { CreateProjectDialogComponent } from './create-project-dialog.component'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock'; + +interface SetupOverrides { + selectorOverrides?: SignalOverride[]; + selectorSnapshotOverrides?: { + selector: unknown; + value: unknown; + }[]; +} describe('CreateProjectDialogComponent', () => { let component: CreateProjectDialogComponent; let fixture: ComponentFixture; let store: Store; - let dialogRef: DynamicDialogRef; - - const fillValidForm = ( - title = 'My Project', - description = 'Some description', - template = 'tmpl-1', - storageLocation = 'osfstorage', - affiliations: string[] = ['aff-1', 'aff-2'] - ) => { - component.projectForm.patchValue({ - [ProjectFormControls.Title]: title, - [ProjectFormControls.Description]: description, - [ProjectFormControls.Template]: template, - [ProjectFormControls.StorageLocation]: storageLocation, - [ProjectFormControls.Affiliations]: affiliations, - }); - }; - - beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === MyResourcesSelectors.isProjectSubmitting) return () => false; - return () => undefined; + let dialogRef: { close: jest.Mock }; + + const defaultSignals: SignalOverride[] = [ + { selector: MyResourcesSelectors.isProjectSubmitting, value: false }, + { selector: UserSelectors.getCurrentUser, value: { id: 'user-1', defaultRegionId: 'us' } }, + { selector: RegionsSelectors.getRegions, value: [] }, + { selector: RegionsSelectors.areRegionsLoading, value: false }, + { selector: InstitutionsSelectors.getUserInstitutions, value: [] }, + { selector: InstitutionsSelectors.areUserInstitutionsLoading, value: false }, + { selector: ProjectsSelectors.getProjects, value: [] }, + { selector: ProjectsSelectors.getProjectsLoading, value: false }, + ]; + const defaultSnapshotSelectors = [{ selector: MyResourcesSelectors.getProjects, value: [{ id: 'new-project-id' }] }]; + + function setup(overrides: SetupOverrides = {}) { + TestBed.configureTestingModule({ + imports: [CreateProjectDialogComponent], + providers: [ + provideOSFCore(), + provideDynamicDialogRefMock(), + provideMockStore({ + selectors: [...defaultSnapshotSelectors, ...(overrides.selectorSnapshotOverrides ?? [])], + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), + }), + ], }); - await TestBed.configureTestingModule({ - imports: [CreateProjectDialogComponent, OSFTestingModule, MockComponent(AddProjectFormComponent)], - providers: [MockProvider(Store, MOCK_STORE)], - }).compileComponents(); - + store = TestBed.inject(Store); fixture = TestBed.createComponent(CreateProjectDialogComponent); component = fixture.componentInstance; - - store = TestBed.inject(Store); - dialogRef = TestBed.inject(DynamicDialogRef); - + dialogRef = component.dialogRef as unknown as { close: jest.Mock }; fixture.detectChanges(); - }); + } it('should create', () => { + setup(); expect(component).toBeTruthy(); }); - it('should mark all controls touched and not dispatch when form is invalid', () => { - const markAllSpy = jest.spyOn(component.projectForm, 'markAllAsTouched'); + it('should initialize form with expected default values', () => { + setup(); - (store.dispatch as unknown as jest.Mock).mockClear(); + expect(component.projectForm.getRawValue()).toEqual({ + title: '', + storageLocation: 'us', + affiliations: [], + description: '', + template: '', + }); + }); + + it('should mark form as touched and not dispatch when form is invalid', () => { + setup(); + const markAllAsTouchedSpy = jest.spyOn(component.projectForm, 'markAllAsTouched'); + (store.dispatch as jest.Mock).mockClear(); component.submitForm(); - expect(markAllSpy).toHaveBeenCalled(); - expect(store.dispatch).not.toHaveBeenCalled(); + expect(component.projectForm.invalid).toBe(true); + expect(markAllAsTouchedSpy).toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(CreateProject)); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should submit, refresh list and close dialog when form is valid', () => { - fillValidForm('Title', 'Desc', 'Tpl', 'Storage', ['a1']); + it('should dispatch create project with form values', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.projectForm.patchValue({ + [ProjectFormControls.Title]: 'My Project', + [ProjectFormControls.StorageLocation]: 'us', + [ProjectFormControls.Description]: 'Description', + [ProjectFormControls.Template]: 'template-id', + [ProjectFormControls.Affiliations]: ['inst-1'], + }); + + component.submitForm(); + + expect(store.dispatch).toHaveBeenCalledWith( + new CreateProject('My Project', 'Description', 'template-id', 'us', ['inst-1']) + ); + }); - (MOCK_STORE.dispatch as jest.Mock).mockReturnValue(of(undefined)); - (MOCK_STORE.selectSnapshot as jest.Mock).mockReturnValue([{ id: 'new-project-id' }]); + it('should fetch projects and close dialog with new project after successful creation', () => { + setup({ + selectorSnapshotOverrides: [ + { selector: MyResourcesSelectors.getProjects, value: [{ id: 'new-id', title: 'x' }] }, + ], + }); + (store.dispatch as jest.Mock).mockClear(); + component.projectForm.patchValue({ + [ProjectFormControls.Title]: 'My Project', + [ProjectFormControls.StorageLocation]: 'eu', + [ProjectFormControls.Description]: 'Description', + [ProjectFormControls.Template]: '', + [ProjectFormControls.Affiliations]: [], + }); component.submitForm(); - expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new CreateProject('Title', 'Desc', 'Tpl', 'Storage', ['a1'])); - expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new GetMyProjects(1, DEFAULT_TABLE_PARAMS.rows, {})); - expect((dialogRef as any).close).toHaveBeenCalledWith({ project: { id: 'new-project-id' } }); + expect(store.dispatch).toHaveBeenCalledWith(new GetMyProjects(1, DEFAULT_TABLE_PARAMS.rows, {})); + expect(dialogRef.close).toHaveBeenCalledWith({ project: { id: 'new-id', title: 'x' } }); }); }); diff --git a/src/app/features/my-projects/my-projects.component.spec.ts b/src/app/features/my-projects/my-projects.component.spec.ts index 5c1caaab4..544600ce7 100644 --- a/src/app/features/my-projects/my-projects.component.spec.ts +++ b/src/app/features/my-projects/my-projects.component.spec.ts @@ -1,115 +1,233 @@ +import { Store } from '@ngxs/store'; + import { MockComponents, MockProvider } from 'ng-mocks'; -import { BehaviorSubject } from 'rxjs'; +import { of, Subject } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { MyProjectsTab } from '@osf/features/my-projects/enums'; import { MyProjectsTableComponent } from '@osf/shared/components/my-projects-table/my-projects-table.component'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { SelectComponent } from '@osf/shared/components/select/select.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { DEFAULT_TABLE_PARAMS } from '@osf/shared/constants/default-table-params.constants'; import { SortOrder } from '@osf/shared/enums/sort-order.enum'; import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ProjectRedirectDialogService } from '@osf/shared/services/project-redirect-dialog.service'; -import { BookmarksSelectors } from '@osf/shared/stores/bookmarks'; -import { MyResourcesSelectors } from '@osf/shared/stores/my-resources'; - +import { BookmarksSelectors, GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; +import { ClearMyResources, MyResourcesSelectors } from '@osf/shared/stores/my-resources'; + +import { PROJECT_FILTER_OPTIONS } from './constants/project-filter-options.const'; +import { MyProjectsQueryService } from './services/my-projects-query.service'; +import { MyProjectsTableParamsService } from './services/my-projects-table-params.service'; +import { CreateProjectDialogComponent } from './components'; +import { MyProjectsTab } from './enums'; import { MyProjectsComponent } from './my-projects.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; -import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock'; describe('MyProjectsComponent', () => { let component: MyProjectsComponent; let fixture: ComponentFixture; - let mockRouter: ReturnType; - let mockActivatedRoute: ReturnType; - let isMediumSubject: BehaviorSubject; - - beforeEach(async () => { - isMediumSubject = new BehaviorSubject(false); - mockActivatedRoute = ActivatedRouteMockBuilder.create().withQueryParams({ tab: '1' }).build(); - mockRouter = RouterMockBuilder.create().build(); - - await TestBed.configureTestingModule({ + let store: Store; + let routerMock: RouterMockType; + let customDialogService: { open: jest.Mock }; + let projectRedirectDialogService: { showProjectRedirectDialog: jest.Mock }; + let queryServiceMock: { + getRawParams: jest.Mock; + handlePageChange: jest.Mock; + handleSort: jest.Mock; + handleTabSwitch: jest.Mock; + handleSearch: jest.Mock; + toQueryModel: jest.Mock; + hasTabInUrl: jest.Mock; + getTabFromUrl: jest.Mock; + updateParams: jest.Mock; + }; + let tableParamsServiceMock: { buildTableParams: jest.Mock }; + + const projectItem = { + id: 'p1', + type: 'nodes', + title: 'Project 1', + dateCreated: '2024-01-01', + dateModified: '2024-01-02', + isPublic: true, + contributors: [], + }; + + const defaultSignals: SignalOverride[] = [ + { selector: MyResourcesSelectors.getProjects, value: [projectItem] }, + { selector: MyResourcesSelectors.getRegistrations, value: [] }, + { selector: MyResourcesSelectors.getPreprints, value: [] }, + { selector: MyResourcesSelectors.getTotalProjects, value: 1 }, + { selector: MyResourcesSelectors.getTotalRegistrations, value: 0 }, + { selector: MyResourcesSelectors.getTotalPreprints, value: 0 }, + { selector: BookmarksSelectors.getBookmarks, value: [] }, + { selector: BookmarksSelectors.getBookmarksCollectionId, value: 'bookmark-collection-id' }, + { selector: BookmarksSelectors.getBookmarksTotalCount, value: 0 }, + ]; + + function setup(selectorOverrides?: SignalOverride[]) { + routerMock = RouterMockBuilder.create().build(); + customDialogService = { open: jest.fn() }; + projectRedirectDialogService = { showProjectRedirectDialog: jest.fn() }; + queryServiceMock = { + getRawParams: jest.fn(() => ({ tab: '1', page: '1', size: '10' })), + handlePageChange: jest.fn(), + handleSort: jest.fn(), + handleTabSwitch: jest.fn(), + handleSearch: jest.fn(), + toQueryModel: jest.fn(() => ({ + page: 1, + size: 10, + search: '', + sortColumn: '', + sortOrder: SortOrder.Asc, + })), + hasTabInUrl: jest.fn(() => true), + getTabFromUrl: jest.fn(() => MyProjectsTab.Projects), + updateParams: jest.fn(), + }; + tableParamsServiceMock = { + buildTableParams: jest.fn((baseRows: number, totalRecords: number, isBookmarks: boolean) => ({ + ...DEFAULT_TABLE_PARAMS, + rows: isBookmarks ? totalRecords : baseRows, + totalRecords, + paginator: !isBookmarks, + rowsPerPageOptions: isBookmarks ? [] : DEFAULT_TABLE_PARAMS.rowsPerPageOptions, + firstRowIndex: 0, + })), + }; + const routeMock = ActivatedRouteMockBuilder.create().withQueryParams({ tab: '1', page: '1', size: '10' }).build(); + + TestBed.configureTestingModule({ imports: [ MyProjectsComponent, - OSFTestingModule, - ...MockComponents(SubHeaderComponent, MyProjectsTableComponent, SelectComponent, SearchInputComponent), + ...MockComponents(SubHeaderComponent, MyProjectsTableComponent, SearchInputComponent, SelectComponent), ], providers: [ + provideOSFCore(), + MockProvider(ActivatedRoute, routeMock), + MockProvider(Router, routerMock), + MockProvider(CustomDialogService, customDialogService), + MockProvider(ProjectRedirectDialogService, projectRedirectDialogService), + MockProvider(MyProjectsQueryService, queryServiceMock), + MockProvider(MyProjectsTableParamsService, tableParamsServiceMock), + MockProvider(IS_MEDIUM, of(false)), provideMockStore({ - signals: [ - { selector: MyResourcesSelectors.getTotalProjects, value: 0 }, - { selector: MyResourcesSelectors.getTotalRegistrations, value: 0 }, - { selector: MyResourcesSelectors.getTotalPreprints, value: 0 }, - { selector: BookmarksSelectors.getBookmarksTotalCount, value: 0 }, - { selector: BookmarksSelectors.getBookmarksCollectionId, value: null }, - { selector: MyResourcesSelectors.getProjects, value: [] }, - { selector: MyResourcesSelectors.getRegistrations, value: [] }, - { selector: MyResourcesSelectors.getPreprints, value: [] }, - { selector: BookmarksSelectors.getBookmarks, value: [] }, - ], + signals: mergeSignalOverrides(defaultSignals, selectorOverrides), }), - { provide: ActivatedRoute, useValue: mockActivatedRoute }, - { provide: Router, useValue: mockRouter }, - MockProvider(CustomDialogService), - MockProvider(IS_MEDIUM, isMediumSubject), - MockProvider(ProjectRedirectDialogService), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(MyProjectsComponent); component = fixture.componentInstance; fixture.detectChanges(); + } + + afterEach(() => { + jest.useRealTimers(); }); it('should create', () => { + setup(); expect(component).toBeTruthy(); }); - it('should fetch data for projects tab', () => { - expect(component.selectedTab()).toBe(MyProjectsTab.Projects); - expect(component.isLoading()).toBe(false); + it('should dispatch get bookmarks collection id on init', () => { + setup(); + expect(store.dispatch).toHaveBeenCalledWith(new GetBookmarksCollectionId()); }); - it('should paginate and update query params', () => { - component.onPageChange({ first: 30, rows: 15 } as any); + it('should delegate page changes to query service', () => { + setup(); + component.onPageChange({ first: 20, rows: 10 } as { first: number; rows: number }); - expect(mockRouter.navigate).toHaveBeenCalledWith([], { - relativeTo: mockActivatedRoute, - queryParams: { page: '3', size: '15', tab: '1' }, - }); + expect(queryServiceMock.handlePageChange).toHaveBeenCalledWith(20, 10, { tab: '1', page: '1', size: '10' }, 1); }); - it('should sort and update query params', () => { - component.onSort({ field: 'updated', order: SortOrder.Desc } as any); + it('should delegate sort changes when field exists', () => { + setup(); + component.onSort({ field: 'title', order: SortOrder.Desc } as { field: string; order: SortOrder }); - expect(mockRouter.navigate).toHaveBeenCalledWith([], { - relativeTo: mockActivatedRoute, - queryParams: { sortColumn: 'updated', sortOrder: 'desc', tab: '1' }, - }); + expect(queryServiceMock.handleSort).toHaveBeenCalledWith( + 'title', + SortOrder.Desc, + { tab: '1', page: '1', size: '10' }, + 1 + ); + }); + + it('should not delegate sort when field is missing', () => { + setup(); + component.onSort({ field: undefined, order: SortOrder.Asc } as { field?: string; order: SortOrder }); + + expect(queryServiceMock.handleSort).not.toHaveBeenCalled(); }); - it('should clear and reset on tab change', () => { - component.onTabChange(MyProjectsTab.Registrations); + it('should clear and switch tab when onTabChange receives numeric value', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + + component.onTabChange(String(MyProjectsTab.Registrations)); - expect(mockRouter.navigate).toHaveBeenCalledWith([], { - relativeTo: mockActivatedRoute, - queryParams: { page: '1', tab: '2' }, + expect(store.dispatch).toHaveBeenCalledWith(new ClearMyResources()); + expect(component.selectedTab()).toBe(MyProjectsTab.Registrations); + expect(component.selectedProjectFilterOption()).toBe(PROJECT_FILTER_OPTIONS[0].value); + expect(queryServiceMock.handleTabSwitch).toHaveBeenCalledWith( + { tab: '1', page: '1', size: '10' }, + MyProjectsTab.Registrations + ); + }); + + it('should ignore invalid tab values', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + + component.onTabChange('not-a-number'); + + expect(store.dispatch).not.toHaveBeenCalledWith(new ClearMyResources()); + expect(queryServiceMock.handleTabSwitch).not.toHaveBeenCalled(); + }); + + it('should open create project dialog and redirect after close result', () => { + setup(); + const onClose$ = new Subject<{ project: { id: string } }>(); + customDialogService.open.mockReturnValue({ onClose: onClose$.asObservable() }); + + component.createProject(); + onClose$.next({ project: { id: 'project-123' } }); + + expect(customDialogService.open).toHaveBeenCalledWith(CreateProjectDialogComponent, { + header: 'myProjects.header.createProject', + width: '850px', }); + expect(projectRedirectDialogService.showProjectRedirectDialog).toHaveBeenCalledWith('project-123'); + }); + + it('should navigate to project and set active project', () => { + setup(); + + component.navigateToProject(projectItem); + + expect(component.activeProject()).toEqual(projectItem); + expect(routerMock.navigate).toHaveBeenCalledWith([projectItem.id]); }); - it('should navigate to project', () => { - const project = { id: 'p1' } as any; - component.navigateToProject(project); + it('should delegate search handling after debounce', () => { + jest.useFakeTimers(); + setup(); + + component.searchControl.setValue('alpha'); + jest.advanceTimersByTime(300); - expect(component.activeProject()).toEqual(project); - expect(mockRouter.navigate).toHaveBeenCalledWith(['p1']); + expect(queryServiceMock.handleSearch).toHaveBeenCalledWith('alpha', { tab: '1', page: '1', size: '10' }, 1); }); }); diff --git a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts index 4f5bff759..6762e7f1f 100644 --- a/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/additional-info/additional-info.component.spec.ts @@ -52,32 +52,33 @@ describe('AdditionalInfoComponent', () => { fixture.detectChanges(); } - beforeEach(() => { - setup(); - }); - it('should create', () => { + setup(); expect(component).toBeTruthy(); }); it('should return license from preprint when available', () => { + setup(); const license = component.license(); expect(license).toBe(PREPRINT_MOCK.embeddedLicense); }); it('should return license options record from preprint when available', () => { + setup(); const licenseOptionsRecord = component.licenseOptionsRecord(); expect(licenseOptionsRecord).toEqual(PREPRINT_MOCK.licenseOptions); }); it('should have skeleton data array with 5 null elements', () => { + setup(); expect(component.skeletonData).toHaveLength(5); expect(component.skeletonData.every((item) => item === null)).toBe(true); }); it('should navigate to search page with tag when tagClicked is called', () => { + setup(); const router = TestBed.inject(Router); - const navigateSpy = jest.spyOn(router, 'navigate'); + const navigateSpy = jest.spyOn(router, 'navigate').mockResolvedValue(true); component.tagClicked('test-tag'); @@ -87,6 +88,7 @@ describe('AdditionalInfoComponent', () => { }); it('should not render DOI link when articleDoiLink is missing', () => { + setup(); const doiLink = fixture.nativeElement.querySelector('a[href*="doi.org"]'); expect(doiLink).toBeNull(); }); diff --git a/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts b/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts index 614674f66..b0f0831fa 100644 --- a/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/citation-section/citation-section.component.spec.ts @@ -2,7 +2,7 @@ import { Store } from '@ngxs/store'; import { SelectChangeEvent, SelectFilterEvent } from 'primeng/select'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResourceType } from '@shared/enums/resource-type.enum'; import { @@ -66,6 +66,10 @@ describe('CitationSectionComponent', () => { } } + afterEach(() => { + jest.useRealTimers(); + }); + it('should create', () => { setup(); expect(component).toBeTruthy(); @@ -123,7 +127,8 @@ describe('CitationSectionComponent', () => { ); }); - it('should debounce and deduplicate citation style filter dispatches', fakeAsync(() => { + it('should debounce and deduplicate citation style filter dispatches', () => { + jest.useFakeTimers(); setup(); const preventDefault = jest.fn(); const eventApa: SelectFilterEvent = { @@ -136,16 +141,16 @@ describe('CitationSectionComponent', () => { expect(preventDefault).toHaveBeenCalled(); expect(store.dispatch).not.toHaveBeenCalled(); - tick(299); + jest.advanceTimersByTime(299); expect(store.dispatch).not.toHaveBeenCalled(); - tick(1); + jest.advanceTimersByTime(1); expect(store.dispatch).toHaveBeenCalledWith(new GetCitationStyles('apa')); expect(store.dispatch).toHaveBeenCalledTimes(1); (store.dispatch as jest.Mock).mockClear(); component.handleCitationStyleFilterSearch(eventApa); - tick(300); + jest.advanceTimersByTime(300); expect(store.dispatch).not.toHaveBeenCalled(); const eventMla: SelectFilterEvent = { @@ -153,8 +158,8 @@ describe('CitationSectionComponent', () => { filter: 'mla', }; component.handleCitationStyleFilterSearch(eventMla); - tick(300); + jest.advanceTimersByTime(300); expect(store.dispatch).toHaveBeenCalledWith(new GetCitationStyles('mla')); - })); + }); }); diff --git a/src/app/features/preprints/components/preprint-details/preprint-make-decision/preprint-make-decision.component.spec.ts b/src/app/features/preprints/components/preprint-details/preprint-make-decision/preprint-make-decision.component.spec.ts index 09eece290..a7d4a5311 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-make-decision/preprint-make-decision.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/preprint-make-decision/preprint-make-decision.component.spec.ts @@ -1,5 +1,7 @@ import { Store } from '@ngxs/store'; +import { MockProvider } from 'ng-mocks'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; @@ -17,6 +19,7 @@ import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider import { PREPRINT_REQUEST_MOCK } from '@testing/mocks/preprint-request.mock'; import { REVIEW_ACTION_MOCK } from '@testing/mocks/review-action.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('PreprintMakeDecisionComponent', () => { @@ -24,6 +27,7 @@ describe('PreprintMakeDecisionComponent', () => { let fixture: ComponentFixture; let store: Store; let router: Router; + let routerMock: RouterMockType; const mockPreprint = PREPRINT_MOCK; const mockProvider = PREPRINT_PROVIDER_DETAILS_MOCK; @@ -31,10 +35,13 @@ describe('PreprintMakeDecisionComponent', () => { const mockWithdrawalRequest = PREPRINT_REQUEST_MOCK; beforeEach(() => { + routerMock = RouterMockBuilder.create().build(); + TestBed.configureTestingModule({ imports: [PreprintMakeDecisionComponent], providers: [ provideOSFCore(), + MockProvider(Router, routerMock), provideMockStore({ signals: [{ selector: PreprintSelectors.getPreprint, value: mockPreprint }], }), diff --git a/src/app/features/preprints/components/preprint-details/preprint-withdraw-dialog/preprint-withdraw-dialog.component.spec.ts b/src/app/features/preprints/components/preprint-details/preprint-withdraw-dialog/preprint-withdraw-dialog.component.spec.ts index 8d0cba677..4a6109f3d 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-withdraw-dialog/preprint-withdraw-dialog.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/preprint-withdraw-dialog/preprint-withdraw-dialog.component.spec.ts @@ -16,10 +16,10 @@ import { WithdrawPreprint } from '@osf/features/preprints/store/preprint'; import { PreprintWithdrawDialogComponent } from './preprint-withdraw-dialog.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('PreprintWithdrawDialogComponent', () => { diff --git a/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts b/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts index 4e4b47b5a..7c1750c9f 100644 --- a/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts +++ b/src/app/features/preprints/components/preprint-details/status-banner/status-banner.component.spec.ts @@ -71,21 +71,21 @@ describe('StatusBannerComponent', () => { it('should compute pending state severity, status and icon', () => { setup(); - expect(component.severity()).toBe('warn'); + expect(component.messageSeverity()).toBe('warn'); expect(component.status()).toBe('preprints.details.statusBanner.pending'); expect(component.iconClass()).toBe('hourglass'); }); it('should compute pending withdrawal state severity, status and icon', () => { setup({ isPendingWithdrawal: true }); - expect(component.severity()).toBe('error'); + expect(component.messageSeverity()).toBe('error'); expect(component.status()).toBe('preprints.details.statusBanner.pendingWithdrawal'); expect(component.iconClass()).toBe('hourglass'); }); it('should compute withdrawal rejected state severity, status and icon', () => { setup({ isWithdrawalRejected: true }); - expect(component.severity()).toBe('error'); + expect(component.messageSeverity()).toBe('error'); expect(component.status()).toBe('preprints.details.statusBanner.withdrawalRejected'); expect(component.iconClass()).toBe('times-circle'); }); @@ -113,7 +113,7 @@ describe('StatusBannerComponent', () => { ], }); expect(component.isWithdrawn()).toBe(true); - expect(component.severity()).toBe('warn'); + expect(component.messageSeverity()).toBe('warn'); expect(component.status()).toBe('preprints.details.statusBanner.withdrawn'); expect(component.iconClass()).toBe('circle-minus'); }); diff --git a/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts b/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts index 37d2b0841..6d12665ad 100644 --- a/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/file-step/file-step.component.spec.ts @@ -4,7 +4,7 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { SelectChangeEvent } from 'primeng/select'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PreprintFileSource } from '@osf/features/preprints/enums'; import { PreprintModel, PreprintProviderDetails } from '@osf/features/preprints/models'; @@ -116,6 +116,10 @@ describe('FileStepComponent', () => { } }); + afterEach(() => { + jest.useRealTimers(); + }); + it('should create', () => { setup(); expect(component).toBeTruthy(); @@ -155,26 +159,28 @@ describe('FileStepComponent', () => { expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(FetchPreprintPrimaryFile)); }); - it('should dispatch available projects from debounced projectNameControl value', fakeAsync(() => { + it('should dispatch available projects from debounced projectNameControl value', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.projectNameControl.setValue('project-search'); - tick(500); + jest.advanceTimersByTime(500); expect(store.dispatch).toHaveBeenCalledWith(new FetchAvailableProjects('project-search')); - })); + }); - it('should skip available projects dispatch when value equals selectedProjectId', fakeAsync(() => { + it('should skip available projects dispatch when value equals selectedProjectId', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.selectedProjectId.set('project-1'); component.projectNameControl.setValue('project-1'); - tick(500); + jest.advanceTimersByTime(500); expect(store.dispatch).not.toHaveBeenCalledWith(new FetchAvailableProjects('project-1')); - })); + }); it('should handle selectFileSource for project and computer source', () => { setup({ detectChanges: false }); diff --git a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html index f90168727..271498f42 100644 --- a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html +++ b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.html @@ -67,7 +67,7 @@

{{ 'preprints.preprintStepper.supplements.title' | translate }}

} @else {
-

{{ preprintProject()?.name | fixSpecialChar }}

+

{{ preprintProject()?.name }}

{ } }); + afterEach(() => { + jest.useRealTimers(); + }); + it('should create', () => { setup(); expect(component).toBeTruthy(); @@ -120,38 +124,41 @@ describe('SupplementsStepComponent', () => { expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(FetchPreprintProject)); }); - it('should dispatch available projects from debounced project search', fakeAsync(() => { + it('should dispatch available projects from debounced project search', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.projectNameControl.setValue('search-query'); - tick(500); + jest.advanceTimersByTime(500); expect(store.dispatch).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledWith(new FetchAvailableProjects('search-query')); - })); + }); - it('should not dispatch before the debounce window elapses', fakeAsync(() => { + it('should not dispatch before the debounce window elapses', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.projectNameControl.setValue('search-query'); - tick(300); + jest.advanceTimersByTime(300); expect(store.dispatch).not.toHaveBeenCalledWith(new FetchAvailableProjects('search-query')); - tick(200); - })); + jest.advanceTimersByTime(200); + }); - it('should skip available projects dispatch when value equals selected project id', fakeAsync(() => { + it('should skip available projects dispatch when value equals selected project id', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.selectedProjectId.set('project-1'); component.projectNameControl.setValue('project-1'); - tick(500); + jest.advanceTimersByTime(500); expect(store.dispatch).not.toHaveBeenCalledWith(new FetchAvailableProjects('project-1')); - })); + }); it('should select supplement option and reset create form for create-new option', () => { setup({ detectChanges: false }); diff --git a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts index 04fbb95c3..85e67814f 100644 --- a/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts +++ b/src/app/features/preprints/components/stepper/supplements-step/supplements-step.component.ts @@ -38,24 +38,13 @@ import { AddProjectFormComponent } from '@osf/shared/components/add-project-form import { ProjectFormControls } from '@osf/shared/enums/create-project-form-controls.enum'; import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper'; import { StringOrNull } from '@osf/shared/helpers/types.helper'; -import { FixSpecialCharPipe } from '@osf/shared/pipes/fix-special-char.pipe'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ProjectForm } from '@shared/models/projects/create-project-form.model'; @Component({ selector: 'osf-supplements-step', - imports: [ - Button, - NgClass, - Card, - Select, - AddProjectFormComponent, - ReactiveFormsModule, - Skeleton, - TranslatePipe, - FixSpecialCharPipe, - ], + imports: [Button, NgClass, Card, Select, AddProjectFormComponent, ReactiveFormsModule, Skeleton, TranslatePipe], templateUrl: './supplements-step.component.html', styleUrl: './supplements-step.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/features/preprints/guards/preprints-moderator.guard.spec.ts b/src/app/features/preprints/guards/preprints-moderator.guard.spec.ts index 45d1e29a8..23fdf4680 100644 --- a/src/app/features/preprints/guards/preprints-moderator.guard.spec.ts +++ b/src/app/features/preprints/guards/preprints-moderator.guard.spec.ts @@ -1,6 +1,5 @@ import { MockProvider } from 'ng-mocks'; -import { runInInjectionContext } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { ActivatedRouteSnapshot, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; @@ -36,7 +35,7 @@ describe('preprintsModeratorGuard', () => { it('should allow activation when user can view reviews', () => { setup(true); - const result = runInInjectionContext(TestBed, () => preprintsModeratorGuard(routeSnapshot, stateSnapshot)); + const result = TestBed.runInInjectionContext(() => preprintsModeratorGuard(routeSnapshot, stateSnapshot)); expect(result).toBe(true); expect(routerMock.createUrlTree).not.toHaveBeenCalled(); @@ -45,7 +44,7 @@ describe('preprintsModeratorGuard', () => { it('should return forbidden UrlTree when user cannot view reviews', () => { const { urlTree } = setup(false); - const result = runInInjectionContext(TestBed, () => preprintsModeratorGuard(routeSnapshot, stateSnapshot)); + const result = TestBed.runInInjectionContext(() => preprintsModeratorGuard(routeSnapshot, stateSnapshot)); expect(routerMock.createUrlTree).toHaveBeenCalledWith(['/forbidden']); expect(result).toBe(urlTree); diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts index 3bd5316a4..6f1496379 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts @@ -8,7 +8,6 @@ import { HttpErrorResponse } from '@angular/common/http'; import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { provideServerRendering } from '@angular/ssr'; import { HelpScoutService } from '@core/services/help-scout.service'; import { PrerenderReadyService } from '@core/services/prerender-ready.service'; @@ -48,13 +47,13 @@ import { CreateNewVersion } from '../../store/preprint-stepper'; import { PreprintDetailsComponent } from './preprint-details.component'; import { MOCK_CONTRIBUTOR } from '@testing/mocks/contributors.mock'; -import { DataciteMockFactory } from '@testing/mocks/datacite.service.mock'; import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; import { PREPRINT_REQUEST_MOCK } from '@testing/mocks/preprint-request.mock'; import { REVIEW_ACTION_MOCK } from '@testing/mocks/review-action.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; +import { DataciteServiceMock, DataciteServiceMockType } from '@testing/providers/datacite.service.mock'; import { HelpScoutServiceMockFactory } from '@testing/providers/help-scout.service.mock'; import { MetaTagsServiceMockFactory } from '@testing/providers/meta-tags.service.mock'; import { MetaTagsBuilderServiceMockFactory } from '@testing/providers/meta-tags-builder.service.mock'; @@ -71,7 +70,7 @@ describe('PreprintDetailsComponent', () => { let routerMock: RouterMockType; let helpScoutServiceMock: jest.Mocked; let prerenderReadyServiceMock: jest.Mocked; - let dataciteServiceMock: ReturnType; + let dataciteServiceMock: DataciteServiceMockType; let metaTagsServiceMock: ReturnType; let metaTagsBuilderServiceMock: ReturnType; let customDialogServiceMock: ReturnType; @@ -124,7 +123,7 @@ describe('PreprintDetailsComponent', () => { .build(); helpScoutServiceMock = HelpScoutServiceMockFactory(); prerenderReadyServiceMock = PrerenderReadyServiceMockFactory(); - dataciteServiceMock = DataciteMockFactory(); + dataciteServiceMock = DataciteServiceMock.simple(); metaTagsServiceMock = MetaTagsServiceMockFactory(); metaTagsBuilderServiceMock = MetaTagsBuilderServiceMockFactory(); metaTagsBuilderServiceMock.buildPreprintMetaTagsData.mockImplementation( @@ -548,14 +547,13 @@ describe('PreprintDetailsComponent SSR', () => { ), ], providers: [ - provideServerRendering(), provideOSFCore(), MockProvider(PLATFORM_ID, 'server'), MockProvider(ToastService, ToastServiceMock.simple()), MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(Router, routerMock), MockProvider(CustomDialogService, CustomDialogServiceMockBuilder.create().withDefaultOpen().build()), - MockProvider(DataciteService, DataciteMockFactory()), + MockProvider(DataciteService, DataciteServiceMock.simple()), MockProvider(MetaTagsBuilderService, MetaTagsBuilderServiceMockFactory()), MockProvider(MetaTagsService, MetaTagsServiceMockFactory()), MockProvider(PrerenderReadyService, PrerenderReadyServiceMockFactory()), diff --git a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts index 1aabf7386..11843e832 100644 --- a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.spec.ts @@ -15,19 +15,8 @@ const MOCK_ID = 'test-preprint-id'; const MOCK_DOWNLOAD_URL = 'https://osf.io/download/test-preprint-id'; describe('PreprintDownloadRedirectComponent', () => { - let locationReplaceMock: jest.Mock; - - beforeEach(() => { - locationReplaceMock = jest.fn(); - Object.defineProperty(window, 'location', { - value: { replace: locationReplaceMock }, - writable: true, - configurable: true, - }); - }); - function setup(overrides: { id?: string | null; isBrowser?: boolean } = {}) { - const { id = MOCK_ID, isBrowser = true } = overrides; + const { id = null, isBrowser = true } = overrides; const mockRoute = ActivatedRouteMockBuilder.create() .withParams(id ? { id } : {}) @@ -63,20 +52,32 @@ describe('PreprintDownloadRedirectComponent', () => { }); it('should redirect to download URL when id is present in browser', () => { + const redirectSpy = jest + .spyOn(PreprintDownloadRedirectComponent.prototype, 'redirect') + .mockImplementation(jest.fn()); const { mockSocialShareService } = setup({ id: MOCK_ID }); expect(mockSocialShareService.createDownloadUrl).toHaveBeenCalledWith(MOCK_ID); - expect(locationReplaceMock).toHaveBeenCalledWith(MOCK_DOWNLOAD_URL); + expect(redirectSpy).toHaveBeenCalledWith(MOCK_DOWNLOAD_URL); + redirectSpy.mockRestore(); }); it('should not redirect when id is missing', () => { + const redirectSpy = jest + .spyOn(PreprintDownloadRedirectComponent.prototype, 'redirect') + .mockImplementation(jest.fn()); const { mockSocialShareService } = setup({ id: null }); expect(mockSocialShareService.createDownloadUrl).not.toHaveBeenCalled(); - expect(locationReplaceMock).not.toHaveBeenCalled(); + expect(redirectSpy).not.toHaveBeenCalled(); + redirectSpy.mockRestore(); }); it('should not redirect when not in browser', () => { + const redirectSpy = jest + .spyOn(PreprintDownloadRedirectComponent.prototype, 'redirect') + .mockImplementation(jest.fn()); const { mockSocialShareService } = setup({ isBrowser: false }); expect(mockSocialShareService.createDownloadUrl).not.toHaveBeenCalled(); - expect(locationReplaceMock).not.toHaveBeenCalled(); + expect(redirectSpy).not.toHaveBeenCalled(); + redirectSpy.mockRestore(); }); }); diff --git a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts index aa8bfc451..95dae4b89 100644 --- a/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts +++ b/src/app/features/preprints/pages/preprint-download-redirect/preprint-download-redirect.component.ts @@ -25,6 +25,10 @@ export class PreprintDownloadRedirectComponent { } const url = this.socialShareService.createDownloadUrl(id); + this.redirect(url); + } + + redirect(url: string) { window.location.replace(url); } } diff --git a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts index b209e62d7..71b6c776b 100644 --- a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts +++ b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts @@ -3,6 +3,7 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { EducationHistoryComponent } from '@osf/shared/components/education-history/education-history.component'; import { EmploymentHistoryComponent } from '@osf/shared/components/employment-history/employment-history.component'; @@ -16,7 +17,7 @@ import { ProfileInformationComponent } from './profile-information.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; import { MOCK_EDUCATION, MOCK_EMPLOYMENT } from '@testing/mocks/user-employment-education.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ProfileInformationComponent', () => { let component: ProfileInformationComponent; @@ -26,12 +27,8 @@ describe('ProfileInformationComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - ProfileInformationComponent, - OSFTestingModule, - ...MockComponents(EmploymentHistoryComponent, EducationHistoryComponent), - ], - providers: [MockProvider(IS_MEDIUM, of(false))], + imports: [ProfileInformationComponent, ...MockComponents(EmploymentHistoryComponent, EducationHistoryComponent)], + providers: [provideOSFCore(), MockProvider(ActivatedRoute), MockProvider(IS_MEDIUM, of(false))], }).compileComponents(); fixture = TestBed.createComponent(ProfileInformationComponent); diff --git a/src/app/features/profile/profile.component.spec.ts b/src/app/features/profile/profile.component.spec.ts index 451e2c6e2..4f160ea2c 100644 --- a/src/app/features/profile/profile.component.spec.ts +++ b/src/app/features/profile/profile.component.spec.ts @@ -13,7 +13,7 @@ import { ProfileInformationComponent } from './components'; import { ProfileComponent } from './profile.component'; import { ProfileSelectors } from './store'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -31,10 +31,10 @@ describe('ProfileComponent', () => { await TestBed.configureTestingModule({ imports: [ ProfileComponent, - OSFTestingModule, ...MockComponents(ProfileInformationComponent, GlobalSearchComponent, LoadingSpinnerComponent), ], providers: [ + provideOSFCore(), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(PrerenderReadyService), diff --git a/src/app/features/project/linked-services/linked-services.component.spec.ts b/src/app/features/project/linked-services/linked-services.component.spec.ts index 329f582f2..0d9aadb50 100644 --- a/src/app/features/project/linked-services/linked-services.component.spec.ts +++ b/src/app/features/project/linked-services/linked-services.component.spec.ts @@ -14,7 +14,7 @@ import { LinkedServicesComponent } from './linked-services.component'; import { getConfiguredAddonsMappedData } from '@testing/data/addons/addons.configured.data'; import { getResourceReferencesData } from '@testing/data/files/resource-references.data'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -31,12 +31,9 @@ describe('Component: Linked Services', () => { const activatedRouteMock = ActivatedRouteMockBuilder.create().withParams({ id: mockProjectId }).build(); await TestBed.configureTestingModule({ - imports: [ - LinkedServicesComponent, - OSFTestingModule, - ...MockComponents(SubHeaderComponent, LoadingSpinnerComponent), - ], + imports: [LinkedServicesComponent, ...MockComponents(SubHeaderComponent, LoadingSpinnerComponent)], providers: [ + provideOSFCore(), { provide: ActivatedRoute, useValue: activatedRouteMock }, provideMockStore({ signals: [ diff --git a/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts b/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts index 5fceb0708..cc233d927 100644 --- a/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/add-component-dialog/add-component-dialog.component.spec.ts @@ -1,182 +1,239 @@ import { Store } from '@ngxs/store'; -import { MockComponent } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserSelectors } from '@core/store/user'; import { AffiliatedInstitutionSelectComponent } from '@osf/shared/components/affiliated-institution-select/affiliated-institution-select.component'; import { ComponentFormControls } from '@osf/shared/enums/create-component-form-controls.enum'; +import { IdNameModel } from '@osf/shared/models/common/id-name.model'; +import { Institution } from '@osf/shared/models/institutions/institutions.model'; import { ToastService } from '@osf/shared/services/toast.service'; import { FetchUserInstitutions, InstitutionsSelectors } from '@osf/shared/stores/institutions'; import { FetchRegions, RegionsSelectors } from '@osf/shared/stores/regions'; +import { ProjectOverviewModel } from '../../models'; import { CreateComponent, GetComponents, ProjectOverviewSelectors } from '../../store'; import { AddComponentDialogComponent } from './add-component-dialog.component'; -import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { MOCK_PROJECT } from '@testing/mocks/project.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('AddComponentDialogComponent', () => { let component: AddComponentDialogComponent; let fixture: ComponentFixture; let store: Store; - - const mockRegions = [{ id: 'region-1', name: 'Region 1' }]; - const mockUser = { id: 'user-1', defaultRegionId: 'user-region' } as any; - const mockProject = { ...MOCK_PROJECT, id: 'proj-1', title: 'Project', tags: ['tag1'] }; - const mockInstitutions = [MOCK_INSTITUTION]; - const mockUserInstitutions = [MOCK_INSTITUTION, { ...MOCK_INSTITUTION, id: 'inst-2', name: 'Inst 2' }]; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AddComponentDialogComponent, OSFTestingModule, MockComponent(AffiliatedInstitutionSelectComponent)], + let dialogRef: DynamicDialogRef; + let toastService: ToastServiceMockType; + + const mockProject: ProjectOverviewModel = { + ...MOCK_PROJECT_OVERVIEW, + id: 'project-1', + title: 'Test Project', + tags: ['tag-1', 'tag-2'], + }; + + const regions: IdNameModel[] = [ + { id: 'us', name: 'US' }, + { id: 'eu', name: 'EU' }, + ]; + + const userInstitutions: Institution[] = [ + { + id: 'inst-1', + type: 'institutions', + name: 'Institution 1', + description: '', + iri: '', + rorIri: null, + iris: [], + assets: { logo: '', logo_rounded: '', banner: '' }, + institutionalRequestAccessEnabled: false, + logoPath: '', + }, + { + id: 'inst-2', + type: 'institutions', + name: 'Institution 2', + description: '', + iri: '', + rorIri: null, + iris: [], + assets: { logo: '', logo_rounded: '', banner: '' }, + institutionalRequestAccessEnabled: false, + logoPath: '', + }, + ]; + + const projectInstitutions: Institution[] = [ + { + id: 'inst-2', + type: 'institutions', + name: 'Institution 2', + description: '', + iri: '', + rorIri: null, + iris: [], + assets: { logo: '', logo_rounded: '', banner: '' }, + institutionalRequestAccessEnabled: false, + logoPath: '', + }, + ]; + + const defaultSignals: SignalOverride[] = [ + { selector: RegionsSelectors.getRegions, value: regions }, + { selector: RegionsSelectors.areRegionsLoading, value: false }, + { selector: UserSelectors.getCurrentUser, value: { id: 'user-1', defaultRegionId: 'eu' } }, + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: ProjectOverviewSelectors.getInstitutions, value: [] }, + { selector: ProjectOverviewSelectors.getComponentsSubmitting, value: false }, + { selector: InstitutionsSelectors.getUserInstitutions, value: [] }, + { selector: InstitutionsSelectors.areUserInstitutionsLoading, value: false }, + ]; + + function setup(overrides: BaseSetupOverrides = {}) { + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [AddComponentDialogComponent, ...MockComponents(AffiliatedInstitutionSelectComponent)], providers: [ - provideMockStore({ - signals: [ - { selector: RegionsSelectors.getRegions, value: mockRegions }, - { selector: UserSelectors.getCurrentUser, value: mockUser }, - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: ProjectOverviewSelectors.getInstitutions, value: mockInstitutions }, - { selector: RegionsSelectors.areRegionsLoading, value: false }, - { selector: ProjectOverviewSelectors.getComponentsSubmitting, value: false }, - { selector: InstitutionsSelectors.getUserInstitutions, value: mockUserInstitutions }, - { selector: InstitutionsSelectors.areUserInstitutionsLoading, value: false }, - ], - }), + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(ToastService, toastService), + provideMockStore({ signals }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(AddComponentDialogComponent); component = fixture.componentInstance; - store = TestBed.inject(Store); - (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); fixture.detectChanges(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should initialize form with default values', () => { - expect(component.componentForm.get(ComponentFormControls.Title)?.value).toBe(''); - expect(Array.isArray(component.componentForm.get(ComponentFormControls.Affiliations)?.value)).toBe(true); - expect(component.componentForm.get(ComponentFormControls.Description)?.value).toBe(''); - expect(component.componentForm.get(ComponentFormControls.AddContributors)?.value).toBe(false); - expect(component.componentForm.get(ComponentFormControls.AddTags)?.value).toBe(false); - expect(['', 'user-region']).toContain(component.componentForm.get(ComponentFormControls.StorageLocation)?.value); + it('should dispatch initial load actions on init', () => { + setup(); + + expect(store.dispatch).toHaveBeenCalledWith(new FetchRegions()); + expect(store.dispatch).toHaveBeenCalledWith(new FetchUserInstitutions()); + }); + + it('should set storage location from current user default region', () => { + setup(); + + expect(component.componentForm.controls[ComponentFormControls.StorageLocation].value).toBe('eu'); }); - it('should dispatch FetchRegions and FetchUserInstitutions on init', () => { - expect(store.dispatch).toHaveBeenCalledWith(expect.any(FetchRegions)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(FetchUserInstitutions)); + it('should fallback to first region when current user has no default region', () => { + setup({ + selectorOverrides: [{ selector: UserSelectors.getCurrentUser, value: { id: 'user-1', defaultRegionId: null } }], + }); + + expect(component.componentForm.controls[ComponentFormControls.StorageLocation].value).toBe('us'); }); - it('should return store values from selectors', () => { - expect(component.storageLocations()).toEqual(mockRegions); - expect(component.currentUser()).toEqual(mockUser); - expect(component.currentProject()).toEqual(mockProject); - expect(component.institutions()).toEqual(mockInstitutions); - expect(component.areRegionsLoading()).toBe(false); - expect(component.isSubmitting()).toBe(false); - expect(component.userInstitutions()).toEqual(mockUserInstitutions); - expect(component.areUserInstitutionsLoading()).toBe(false); + it('should preselect matching project and user institutions', () => { + setup({ + selectorOverrides: [ + { selector: ProjectOverviewSelectors.getInstitutions, value: projectInstitutions }, + { selector: InstitutionsSelectors.getUserInstitutions, value: userInstitutions }, + ], + }); + + expect(component.selectedInstitutions()).toEqual([userInstitutions[1]]); + expect(component.componentForm.controls[ComponentFormControls.Affiliations].value).toEqual(['inst-2']); }); - it('should set affiliations form control from selected institutions', () => { - const institutions = [MOCK_INSTITUTION]; - component.setSelectedInstitutions(institutions); - expect(component.componentForm.get(ComponentFormControls.Affiliations)?.value).toEqual([MOCK_INSTITUTION.id]); + it('should set affiliations ids when setSelectedInstitutions is called', () => { + setup(); + + component.setSelectedInstitutions(userInstitutions); + + expect(component.componentForm.controls[ComponentFormControls.Affiliations].value).toEqual(['inst-1', 'inst-2']); }); - it('should mark form as touched and not dispatch when submitForm with invalid form', () => { + it('should mark all controls touched and not dispatch create action when form is invalid', () => { + setup(); (store.dispatch as jest.Mock).mockClear(); - component.componentForm.get(ComponentFormControls.Title)?.setValue(''); + component.submitForm(); + expect(component.componentForm.touched).toBe(true); - const createCalls = (store.dispatch as jest.Mock).mock.calls.filter((c) => c[0] instanceof CreateComponent); - expect(createCalls.length).toBe(0); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(CreateComponent)); }); - it('should dispatch CreateComponent and on success close dialog, getComponents, showSuccess', () => { - component.componentForm.get(ComponentFormControls.Title)?.setValue('New Component'); - component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue('region-1'); - component.componentForm.get(ComponentFormControls.Affiliations)?.setValue([MOCK_INSTITUTION.id]); + it('should not dispatch create action when project is missing', () => { + setup({ + selectorOverrides: [{ selector: ProjectOverviewSelectors.getProject, value: null }], + }); (store.dispatch as jest.Mock).mockClear(); + component.componentForm.patchValue({ + [ComponentFormControls.Title]: 'My Component', + [ComponentFormControls.StorageLocation]: 'us', + }); component.submitForm(); - expect(store.dispatch).toHaveBeenCalledWith( - new CreateComponent(mockProject.id, 'New Component', '', [], 'region-1', [MOCK_INSTITUTION.id], false) - ); - expect(component.dialogRef.close).toHaveBeenCalled(); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetComponents)); - expect(TestBed.inject(ToastService).showSuccess).toHaveBeenCalledWith( - 'project.overview.dialog.toast.addComponent.success' - ); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(CreateComponent)); }); - it('should pass project tags when addTags is true', () => { - component.componentForm.get(ComponentFormControls.Title)?.setValue('With Tags'); - component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue('region-1'); - component.componentForm.get(ComponentFormControls.Affiliations)?.setValue([]); - component.componentForm.get(ComponentFormControls.AddTags)?.setValue(true); + it('should dispatch create with empty tags when addTags is false', () => { + setup(); (store.dispatch as jest.Mock).mockClear(); + component.componentForm.patchValue({ + [ComponentFormControls.Title]: 'My Component', + [ComponentFormControls.Description]: 'Description', + [ComponentFormControls.StorageLocation]: 'us', + [ComponentFormControls.Affiliations]: ['inst-1'], + [ComponentFormControls.AddContributors]: true, + [ComponentFormControls.AddTags]: false, + }); component.submitForm(); expect(store.dispatch).toHaveBeenCalledWith( - new CreateComponent(mockProject.id, 'With Tags', '', mockProject.tags, 'region-1', [], false) + new CreateComponent('project-1', 'My Component', 'Description', [], 'us', ['inst-1'], true) ); + expect(store.dispatch).toHaveBeenCalledWith(new GetComponents('project-1')); + expect(dialogRef.close).toHaveBeenCalledWith(); + expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.addComponent.success'); }); - it('should set storage location to user default region when control empty and regions loaded', () => { - fixture = TestBed.createComponent(AddComponentDialogComponent); - component = fixture.componentInstance; - component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue(''); - fixture.detectChanges(); - expect(component.componentForm.get(ComponentFormControls.StorageLocation)?.value).toBe('user-region'); - }); -}); - -describe('AddComponentDialogComponent when user has no default region', () => { - let component: AddComponentDialogComponent; - let fixture: ComponentFixture; - - const mockRegions = [{ id: 'region-1', name: 'Region 1' }]; - const mockProject = { ...MOCK_PROJECT, id: 'proj-1', title: 'Project', tags: ['tag1'] }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [AddComponentDialogComponent, OSFTestingModule, MockComponent(AffiliatedInstitutionSelectComponent)], - providers: [ - provideMockStore({ - signals: [ - { selector: RegionsSelectors.getRegions, value: mockRegions }, - { selector: UserSelectors.getCurrentUser, value: null }, - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: ProjectOverviewSelectors.getInstitutions, value: [] }, - { selector: RegionsSelectors.areRegionsLoading, value: false }, - { selector: ProjectOverviewSelectors.getComponentsSubmitting, value: false }, - { selector: InstitutionsSelectors.getUserInstitutions, value: [] }, - { selector: InstitutionsSelectors.areUserInstitutionsLoading, value: false }, - ], - }), - ], - }).compileComponents(); + it('should dispatch create with project tags when addTags is true', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.componentForm.patchValue({ + [ComponentFormControls.Title]: 'My Component', + [ComponentFormControls.Description]: '', + [ComponentFormControls.StorageLocation]: 'us', + [ComponentFormControls.Affiliations]: [], + [ComponentFormControls.AddContributors]: false, + [ComponentFormControls.AddTags]: true, + }); - fixture = TestBed.createComponent(AddComponentDialogComponent); - component = fixture.componentInstance; - component.componentForm.get(ComponentFormControls.StorageLocation)?.setValue(''); - fixture.detectChanges(); - }); + component.submitForm(); - it('should set storage location to first region when control empty', () => { - expect(component.componentForm.get(ComponentFormControls.StorageLocation)?.value).toBe('region-1'); + expect(store.dispatch).toHaveBeenCalledWith( + new CreateComponent('project-1', 'My Component', '', ['tag-1', 'tag-2'], 'us', [], false) + ); }); }); diff --git a/src/app/features/project/overview/components/citation-addon-card/citation-addon-card.component.spec.ts b/src/app/features/project/overview/components/citation-addon-card/citation-addon-card.component.spec.ts index b38861764..bc7a94acd 100644 --- a/src/app/features/project/overview/components/citation-addon-card/citation-addon-card.component.spec.ts +++ b/src/app/features/project/overview/components/citation-addon-card/citation-addon-card.component.spec.ts @@ -21,7 +21,7 @@ import { CitationAddonCardComponent } from './citation-addon-card.component'; import { MOCK_CONFIGURED_ADDON } from '@testing/mocks/configured-addon.mock'; import { MOCK_DOCUMENT_STORAGE_ITEM } from '@testing/mocks/storage-item.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { AddonOperationInvocationServiceMockFactory } from '@testing/providers/addon-operation-invocation.service.mock'; import { CslStyleManagerServiceMockFactory } from '@testing/providers/csl-style-manager.service.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -34,12 +34,9 @@ describe('CitationAddonCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - CitationAddonCardComponent, - OSFTestingModule, - ...MockComponents(CitationItemComponent, CitationCollectionItemComponent), - ], + imports: [CitationAddonCardComponent, ...MockComponents(CitationItemComponent, CitationCollectionItemComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: AddonsSelectors.getAllCitationOperationInvocations, value: signal({}) }, diff --git a/src/app/features/project/overview/components/citation-collection-item/citation-collection-item.component.spec.ts b/src/app/features/project/overview/components/citation-collection-item/citation-collection-item.component.spec.ts index 3328ee746..b0dfcfe9c 100644 --- a/src/app/features/project/overview/components/citation-collection-item/citation-collection-item.component.spec.ts +++ b/src/app/features/project/overview/components/citation-collection-item/citation-collection-item.component.spec.ts @@ -14,7 +14,7 @@ import { CitationCollectionItemComponent } from './citation-collection-item.comp import { MOCK_CONFIGURED_ADDON } from '@testing/mocks/configured-addon.mock'; import { MOCK_COLLECTION_STORAGE_ITEM, MOCK_DOCUMENT_STORAGE_ITEM } from '@testing/mocks/storage-item.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { AddonOperationInvocationServiceMockFactory } from '@testing/providers/addon-operation-invocation.service.mock'; import { AddonsServiceMockFactory } from '@testing/providers/addons.service.mock'; import { CslStyleManagerServiceMockFactory } from '@testing/providers/csl-style-manager.service.mock'; @@ -25,12 +25,9 @@ describe('CitationCollectionItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - CitationCollectionItemComponent, - OSFTestingModule, - ...MockComponents(IconComponent, CitationItemComponent), - ], + imports: [CitationCollectionItemComponent, ...MockComponents(IconComponent, CitationItemComponent)], providers: [ + provideOSFCore(), { provide: AddonOperationInvocationService, useFactory: AddonOperationInvocationServiceMockFactory, diff --git a/src/app/features/project/overview/components/citation-item/citation-item.component.spec.ts b/src/app/features/project/overview/components/citation-item/citation-item.component.spec.ts index 2e9eabc14..6485c0f61 100644 --- a/src/app/features/project/overview/components/citation-item/citation-item.component.spec.ts +++ b/src/app/features/project/overview/components/citation-item/citation-item.component.spec.ts @@ -8,7 +8,7 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { CitationItemComponent } from './citation-item.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('CitationItemComponent', () => { let component: CitationItemComponent; @@ -18,8 +18,8 @@ describe('CitationItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CitationItemComponent, OSFTestingModule, ...MockComponents(IconComponent)], - providers: [MockProvider(Clipboard), MockProvider(ToastService)], + imports: [CitationItemComponent, ...MockComponents(IconComponent)], + providers: [provideOSFCore(), MockProvider(Clipboard), MockProvider(ToastService)], }).compileComponents(); fixture = TestBed.createComponent(CitationItemComponent); diff --git a/src/app/features/project/overview/components/component-card/component-card.component.spec.ts b/src/app/features/project/overview/components/component-card/component-card.component.spec.ts index d0c1a3577..c9ae25aa3 100644 --- a/src/app/features/project/overview/components/component-card/component-card.component.spec.ts +++ b/src/app/features/project/overview/components/component-card/component-card.component.spec.ts @@ -8,6 +8,7 @@ import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { ComponentCardComponent } from './component-card.component'; import { MOCK_NODE_WITH_ADMIN, MOCK_NODE_WITHOUT_ADMIN } from '@testing/mocks/node.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ComponentCardComponent', () => { let component: ComponentCardComponent; @@ -16,6 +17,7 @@ describe('ComponentCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ComponentCardComponent, ...MockComponents(IconComponent, ContributorsListComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ComponentCardComponent); diff --git a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts index b2954f518..0c6b4a1f0 100644 --- a/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/delete-component-dialog/delete-component-dialog.component.spec.ts @@ -1,312 +1,184 @@ import { Store } from '@ngxs/store'; -import { DynamicDialogConfig } from 'primeng/dynamicdialog'; +import { MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeleteProject, SettingsSelectors } from '@osf/features/project/settings/store'; import { RegistrySelectors } from '@osf/features/registry/store/registry'; -import { ScientistsNames } from '@osf/shared/constants/scientists.const'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; +import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model'; import { ToastService } from '@osf/shared/services/toast.service'; import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; +import { ProjectOverviewModel } from '../../models'; import { GetComponents, ProjectOverviewSelectors } from '../../store'; import { DeleteComponentDialogComponent } from './delete-component-dialog.component'; -import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; - -const mockComponentsWithAdmin = [ - { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, - { id: 'comp-2', title: 'Component 2', isPublic: false, permissions: [UserPermissions.Admin] }, -]; - -const mockComponentsWithoutAdmin = [ - { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Read] }, -]; +import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + resourceType?: ResourceType; + isForksContext?: boolean; +} describe('DeleteComponentDialogComponent', () => { let component: DeleteComponentDialogComponent; let fixture: ComponentFixture; let store: Store; - let dialogConfig: DynamicDialogConfig; + let dialogRef: DynamicDialogRef; + let toastService: ToastServiceMockType; + + const mockProject: ProjectOverviewModel = { + ...MOCK_PROJECT_OVERVIEW, + id: 'project-1', + }; + + const adminComponents: NodeShortInfoModel[] = [ + { + id: 'c1', + title: 'Component 1', + isPublic: true, + permissions: [UserPermissions.Admin], + }, + { + id: 'c2', + title: 'Component 2', + isPublic: true, + permissions: [UserPermissions.Admin, UserPermissions.Write], + }, + ]; - const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' }; + const defaultSignals: SignalOverride[] = [ + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: RegistrySelectors.getRegistry, value: null }, + { selector: SettingsSelectors.isSettingsSubmitting, value: false }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: adminComponents }, + ]; - beforeEach(async () => { - dialogConfig = { data: { resourceType: ResourceType.Project } }; + function setup(overrides: SetupOverrides = {}) { + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + toastService = ToastServiceMock.simple(); - await TestBed.configureTestingModule({ - imports: [DeleteComponentDialogComponent, OSFTestingModule], + TestBed.configureTestingModule({ + imports: [DeleteComponentDialogComponent], providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: RegistrySelectors.getRegistry, value: null }, - { selector: SettingsSelectors.isSettingsSubmitting, value: false }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithAdmin }, - ], + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { + data: { + resourceType: overrides.resourceType ?? ResourceType.Project, + isForksContext: overrides.isForksContext ?? false, + }, }), - { provide: DynamicDialogConfig, useValue: dialogConfig }, + MockProvider(ToastService, toastService), + provideMockStore({ signals }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(DeleteComponentDialogComponent); component = fixture.componentInstance; - store = TestBed.inject(Store); - (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); fixture.detectChanges(); - }); + } it('should create', () => { - expect(component).toBeTruthy(); - }); + setup(); - it('should return store values from selectors', () => { - expect(component.project()).toEqual(mockProject); - expect(component.registration()).toBeNull(); - expect(component.isSubmitting()).toBe(false); - expect(component.isLoading()).toBe(false); - expect(component.components()).toEqual(mockComponentsWithAdmin); - }); - - it('should have selectedScientist as one of ScientistsNames', () => { - expect(ScientistsNames).toContain(component.selectedScientist()); + expect(component).toBeTruthy(); }); - it('should compute currentResource as project when resourceType is Project', () => { - expect(component.currentResource()).toEqual(mockProject); - }); + it('should compute current resource from project when resource type is project', () => { + setup({ resourceType: ResourceType.Project }); - it('should compute hasAdminAccessForAllComponents true when all components have Admin', () => { - expect(component.hasAdminAccessForAllComponents()).toBe(true); + expect(component.currentResource()?.id).toBe('project-1'); }); - it('should compute hasSubComponents true when more than one component', () => { - expect(component.hasSubComponents()).toBe(true); - }); + it('should return false for hasAdminAccessForAllComponents when components are empty', () => { + setup({ + selectorOverrides: [{ selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }], + }); - it('should return isInputValid true when userInput matches selectedScientist', () => { - const scientist = component.selectedScientist(); - component.onInputChange(scientist); - expect(component.isInputValid()).toBe(true); + expect(component.hasAdminAccessForAllComponents()).toBe(false); }); - it('should return isInputValid false when userInput does not match', () => { - component.onInputChange('wrong'); - expect(component.isInputValid()).toBe(false); - }); + it('should return true for hasAdminAccessForAllComponents when all components have admin access', () => { + setup(); - it('should set userInput on onInputChange', () => { - component.onInputChange('test'); - expect(component.userInput()).toBe('test'); + expect(component.hasAdminAccessForAllComponents()).toBe(true); }); - it('should dispatch DeleteProject with components and on success close, getComponents, showSuccess', () => { - const scientist = component.selectedScientist(); - component.onInputChange(scientist); - (store.dispatch as jest.Mock).mockClear(); - - component.handleDeleteComponent(); + it('should return true for hasSubComponents when there are multiple components', () => { + setup(); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(DeleteProject)); - const deleteCall = (store.dispatch as jest.Mock).mock.calls.find((c) => c[0] instanceof DeleteProject); - expect(deleteCall[0].projects).toEqual(mockComponentsWithAdmin); - expect(component.dialogRef.close).toHaveBeenCalledWith({ success: true }); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetComponents)); - expect(TestBed.inject(ToastService).showSuccess).toHaveBeenCalledWith( - 'project.overview.dialog.toast.deleteComponent.success' - ); + expect(component.hasSubComponents()).toBe(true); }); -}); - -describe('DeleteComponentDialogComponent when not all components have Admin', () => { - let component: DeleteComponentDialogComponent; - let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DeleteComponentDialogComponent, OSFTestingModule], - providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' } }, - { selector: RegistrySelectors.getRegistry, value: null }, - { selector: SettingsSelectors.isSettingsSubmitting, value: false }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithoutAdmin }, - ], - }), - { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Project } } }, - ], - }).compileComponents(); - fixture = TestBed.createComponent(DeleteComponentDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); + it('should return false for hasSubComponents when there is one component', () => { + setup({ + selectorOverrides: [{ selector: CurrentResourceSelectors.getResourceWithChildren, value: [adminComponents[0]] }], + }); - it('should compute hasAdminAccessForAllComponents false', () => { - expect(component.hasAdminAccessForAllComponents()).toBe(false); + expect(component.hasSubComponents()).toBe(false); }); -}); -describe('DeleteComponentDialogComponent when single component', () => { - let component: DeleteComponentDialogComponent; - let fixture: ComponentFixture; + it('should update userInput on input change and validate exact match', () => { + jest.spyOn(Math, 'random').mockReturnValue(0); + setup(); - const singleComponent = [ - { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, - ]; + component.onInputChange(component.selectedScientist()); - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DeleteComponentDialogComponent, OSFTestingModule], - providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' } }, - { selector: RegistrySelectors.getRegistry, value: null }, - { selector: SettingsSelectors.isSettingsSubmitting, value: false }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: singleComponent }, - ], - }), - { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Project } } }, - ], - }).compileComponents(); - fixture = TestBed.createComponent(DeleteComponentDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should compute hasSubComponents false', () => { - expect(component.hasSubComponents()).toBe(false); + expect(component.userInput()).toBe(component.selectedScientist()); + expect(component.isInputValid()).toBe(true); + jest.restoreAllMocks(); }); -}); - -describe('DeleteComponentDialogComponent when no components', () => { - let component: DeleteComponentDialogComponent; - let fixture: ComponentFixture; - let store: Store; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DeleteComponentDialogComponent, OSFTestingModule], - providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' } }, - { selector: RegistrySelectors.getRegistry, value: null }, - { selector: SettingsSelectors.isSettingsSubmitting, value: false }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }, - ], - }), - { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Project } } }, - ], - }).compileComponents(); - fixture = TestBed.createComponent(DeleteComponentDialogComponent); - component = fixture.componentInstance; - store = TestBed.inject(Store); + it('should not dispatch delete action when there are no components', () => { + setup({ + selectorOverrides: [{ selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }], + }); (store.dispatch as jest.Mock).mockClear(); - fixture.detectChanges(); - }); - it('should not dispatch when handleDeleteComponent', () => { component.handleDeleteComponent(); - expect(store.dispatch).not.toHaveBeenCalled(); - }); -}); -describe('DeleteComponentDialogComponent when resourceType is Registration', () => { - let component: DeleteComponentDialogComponent; - let fixture: ComponentFixture; - - const mockRegistration = { ...MOCK_NODE_WITH_ADMIN, id: 'reg-1' }; - const mockComponentsWithAdmin = [ - { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, - ]; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DeleteComponentDialogComponent, OSFTestingModule], - providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: null }, - { selector: RegistrySelectors.getRegistry, value: mockRegistration }, - { selector: SettingsSelectors.isSettingsSubmitting, value: false }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithAdmin }, - ], - }), - { provide: DynamicDialogConfig, useValue: { data: { resourceType: ResourceType.Registration } } }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(DeleteComponentDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(DeleteProject)); }); - it('should compute currentResource as registration', () => { - expect(component.currentResource()).toEqual(mockRegistration); - }); -}); - -describe('DeleteComponentDialogComponent isForksContext', () => { - let component: DeleteComponentDialogComponent; - let fixture: ComponentFixture; - let store: Store; - - const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1' }; - const mockComponentsWithAdmin = [ - { id: 'comp-1', title: 'Component 1', isPublic: true, permissions: [UserPermissions.Admin] }, - ]; + it('should delete components, refresh list, close dialog and show success when not forks context', () => { + setup({ isForksContext: false }); + (store.dispatch as jest.Mock).mockClear(); - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DeleteComponentDialogComponent, OSFTestingModule], - providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: RegistrySelectors.getRegistry, value: null }, - { selector: SettingsSelectors.isSettingsSubmitting, value: false }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: mockComponentsWithAdmin }, - ], - }), - { - provide: DynamicDialogConfig, - useValue: { data: { resourceType: ResourceType.Project, isForksContext: true } }, - }, - ], - }).compileComponents(); + component.handleDeleteComponent(); - fixture = TestBed.createComponent(DeleteComponentDialogComponent); - component = fixture.componentInstance; - store = TestBed.inject(Store); - (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); - fixture.detectChanges(); + expect(store.dispatch).toHaveBeenCalledWith(new DeleteProject(adminComponents)); + expect(store.dispatch).toHaveBeenCalledWith(new GetComponents('project-1')); + expect(dialogRef.close).toHaveBeenCalledWith({ success: true }); + expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.deleteComponent.success'); }); - it('should not dispatch GetComponents when isForksContext', () => { - const scientist = component.selectedScientist(); - component.onInputChange(scientist); + it('should skip components refresh after delete in forks context', () => { + setup({ isForksContext: true }); (store.dispatch as jest.Mock).mockClear(); component.handleDeleteComponent(); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(DeleteProject)); - const getComponentsCalls = (store.dispatch as jest.Mock).mock.calls.filter((c) => c[0] instanceof GetComponents); - expect(getComponentsCalls.length).toBe(0); + expect(store.dispatch).toHaveBeenCalledWith(new DeleteProject(adminComponents)); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(GetComponents)); + expect(dialogRef.close).toHaveBeenCalledWith({ success: true }); }); }); diff --git a/src/app/features/project/overview/components/delete-node-link-dialog/delete-node-link-dialog.component.spec.ts b/src/app/features/project/overview/components/delete-node-link-dialog/delete-node-link-dialog.component.spec.ts index b546c4461..7c61217cb 100644 --- a/src/app/features/project/overview/components/delete-node-link-dialog/delete-node-link-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/delete-node-link-dialog/delete-node-link-dialog.component.spec.ts @@ -4,101 +4,118 @@ import { MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { of } from 'rxjs'; - -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NodeModel } from '@osf/shared/models/nodes/base-node.model'; import { ToastService } from '@osf/shared/services/toast.service'; import { DeleteNodeLink, NodeLinksSelectors } from '@osf/shared/stores/node-links'; +import { ProjectOverviewModel } from '../../models'; import { ProjectOverviewSelectors } from '../../store'; import { DeleteNodeLinkDialogComponent } from './delete-node-link-dialog.component'; -import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; -import { ToastServiceMock } from '@testing/mocks/toast.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + currentLink?: NodeModel | null; +} describe('DeleteNodeLinkDialogComponent', () => { let component: DeleteNodeLinkDialogComponent; let fixture: ComponentFixture; - let store: jest.Mocked; - let dialogRef: jest.Mocked; - let dialogConfig: jest.Mocked; - let toastService: jest.Mocked; - - const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'test-project-id' }; - const mockCurrentLink = { ...MOCK_NODE_WITH_ADMIN, id: 'linked-resource-id', title: 'Linked Resource' }; - - beforeEach(async () => { - dialogConfig = { - data: { currentLink: mockCurrentLink }, - } as jest.Mocked; - - await TestBed.configureTestingModule({ - imports: [DeleteNodeLinkDialogComponent, OSFTestingModule], + let store: Store; + let dialogRef: DynamicDialogRef; + let toastService: ToastServiceMockType; + + const mockProject: ProjectOverviewModel = { + ...MOCK_PROJECT_OVERVIEW, + id: 'project-1', + }; + + const mockLink: NodeModel = { + ...MOCK_NODE_WITH_ADMIN, + id: 'linked-1', + title: 'Linked Resource', + }; + + const defaultSignals: SignalOverride[] = [ + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: NodeLinksSelectors.getNodeLinksSubmitting, value: false }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [DeleteNodeLinkDialogComponent], providers: [ - DynamicDialogRefMock, - ToastServiceMock, - MockProvider(DynamicDialogConfig, dialogConfig), - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: NodeLinksSelectors.getNodeLinksSubmitting, value: false }, - ], + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { + data: { currentLink: overrides.currentLink === undefined ? mockLink : overrides.currentLink }, }), + MockProvider(ToastService, toastService), + provideMockStore({ signals }), ], - }).compileComponents(); + }); - store = TestBed.inject(Store) as jest.Mocked; - store.dispatch = jest.fn().mockReturnValue(of(true)); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(DeleteNodeLinkDialogComponent); component = fixture.componentInstance; - dialogRef = TestBed.inject(DynamicDialogRef) as jest.Mocked; - toastService = TestBed.inject(ToastService) as jest.Mocked; fixture.detectChanges(); - }); + } - afterEach(() => { - jest.clearAllMocks(); - }); + it('should create', () => { + setup(); - it('should initialize currentProject selector', () => { - expect(component.currentProject()).toEqual(mockProject); + expect(component).toBeTruthy(); }); - it('should initialize isSubmitting selector', () => { - expect(component.isSubmitting()).toBe(false); - }); + it('should not dispatch delete action when current link is missing', () => { + setup({ currentLink: null }); + (store.dispatch as jest.Mock).mockClear(); - it('should initialize actions with deleteNodeLink mapping', () => { - expect(component.actions.deleteNodeLink).toBeDefined(); - }); - - it('should dispatch DeleteNodeLink action with correct parameters on successful deletion', () => { component.handleDeleteNodeLink(); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(DeleteNodeLink)); - const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof DeleteNodeLink); - expect(call).toBeDefined(); - const action = call[0] as DeleteNodeLink; - expect(action.projectId).toBe('test-project-id'); - expect(action.linkedResource).toEqual(mockCurrentLink); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(DeleteNodeLink)); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should show success toast on successful deletion', fakeAsync(() => { + it('should not dispatch delete action when current project is missing', () => { + setup({ + selectorOverrides: [{ selector: ProjectOverviewSelectors.getProject, value: null }], + }); + (store.dispatch as jest.Mock).mockClear(); + component.handleDeleteNodeLink(); - tick(); - expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.deleteNodeLink.success'); - })); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(DeleteNodeLink)); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + expect(dialogRef.close).not.toHaveBeenCalled(); + }); + + it('should dispatch delete action, show success toast and close dialog with hasChanges', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); - it('should close dialog with hasChanges true on successful deletion', fakeAsync(() => { component.handleDeleteNodeLink(); - tick(); + expect(store.dispatch).toHaveBeenCalledWith(new DeleteNodeLink('project-1', mockLink)); + expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.deleteNodeLink.success'); expect(dialogRef.close).toHaveBeenCalledWith({ hasChanges: true }); - })); + }); }); diff --git a/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts b/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts index 62f5c009a..64e202e15 100644 --- a/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/duplicate-dialog/duplicate-dialog.component.spec.ts @@ -1,95 +1,95 @@ import { Store } from '@ngxs/store'; -import { of } from 'rxjs'; +import { MockProvider } from 'ng-mocks'; + +import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ProjectOverviewModel } from '../../models'; import { DuplicateProject, ProjectOverviewSelectors } from '../../store'; import { DuplicateDialogComponent } from './duplicate-dialog.component'; -import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('DuplicateDialogComponent', () => { let component: DuplicateDialogComponent; let fixture: ComponentFixture; let store: Store; - - const mockProject = { ...MOCK_NODE_WITH_ADMIN, id: 'proj-1', title: 'Test Project' }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DuplicateDialogComponent, OSFTestingModule], + let dialogRef: DynamicDialogRef; + let toastService: ToastServiceMockType; + + const mockProject: ProjectOverviewModel = { + ...MOCK_PROJECT_OVERVIEW, + id: 'project-1', + title: 'Test Project', + }; + + const defaultSignals: SignalOverride[] = [ + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: ProjectOverviewSelectors.getDuplicateProjectSubmitting, value: false }, + ]; + + function setup(overrides: BaseSetupOverrides = {}) { + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [DuplicateDialogComponent], providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: ProjectOverviewSelectors.getDuplicateProjectSubmitting, value: false }, - ], - }), + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(ToastService, toastService), + provideMockStore({ signals }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(DuplicateDialogComponent); component = fixture.componentInstance; - store = TestBed.inject(Store); - (store.dispatch as jest.Mock).mockReturnValue(of(void 0)); fixture.detectChanges(); - }); + } it('should create', () => { - expect(component).toBeTruthy(); - }); + setup(); - it('should return project and isSubmitting from store', () => { - expect(component.project()).toEqual(mockProject); - expect(component.isSubmitting()).toBe(false); + expect(component).toBeTruthy(); }); - it('should dispatch DuplicateProject and on success close dialog and showSuccess', () => { + it('should not dispatch duplicate action when project is missing', () => { + setup({ + selectorOverrides: [{ selector: ProjectOverviewSelectors.getProject, value: null }], + }); (store.dispatch as jest.Mock).mockClear(); component.handleDuplicateConfirm(); - expect(store.dispatch).toHaveBeenCalledWith(new DuplicateProject(mockProject.id, mockProject.title)); - expect(component.dialogRef.close).toHaveBeenCalled(); - expect(TestBed.inject(ToastService).showSuccess).toHaveBeenCalledWith( - 'project.overview.dialog.toast.duplicate.success' - ); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(DuplicateProject)); + expect(dialogRef.close).not.toHaveBeenCalled(); + expect(toastService.showSuccess).not.toHaveBeenCalled(); }); -}); -describe('DuplicateDialogComponent when no project', () => { - let component: DuplicateDialogComponent; - let fixture: ComponentFixture; - let store: Store; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [DuplicateDialogComponent, OSFTestingModule], - providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: null }, - { selector: ProjectOverviewSelectors.getDuplicateProjectSubmitting, value: false }, - ], - }), - ], - }).compileComponents(); - - fixture = TestBed.createComponent(DuplicateDialogComponent); - component = fixture.componentInstance; - store = TestBed.inject(Store); + it('should duplicate project, close dialog and show success toast', () => { + setup(); (store.dispatch as jest.Mock).mockClear(); - fixture.detectChanges(); - }); - it('should not dispatch when handleDuplicateConfirm', () => { component.handleDuplicateConfirm(); - expect(store.dispatch).not.toHaveBeenCalled(); + + expect(store.dispatch).toHaveBeenCalledWith(new DuplicateProject('project-1', 'Test Project')); + expect(dialogRef.close).toHaveBeenCalledWith(); + expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.duplicate.success'); }); }); diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.spec.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.spec.ts index aa2ef7e01..0a1899928 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.spec.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.spec.ts @@ -7,6 +7,8 @@ import { SelectComponent } from '@osf/shared/components/select/select.component' import { FilesWidgetComponent } from './files-widget.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe.skip('FilesWidgetComponent', () => { let component: FilesWidgetComponent; let fixture: ComponentFixture; @@ -14,6 +16,7 @@ describe.skip('FilesWidgetComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [FilesWidgetComponent, ...MockComponents(SelectComponent, FilesTreeComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(FilesWidgetComponent); diff --git a/src/app/features/project/overview/components/fork-dialog/fork-dialog.component.spec.ts b/src/app/features/project/overview/components/fork-dialog/fork-dialog.component.spec.ts index 60237d7c5..7955d5112 100644 --- a/src/app/features/project/overview/components/fork-dialog/fork-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/fork-dialog/fork-dialog.component.spec.ts @@ -4,9 +4,9 @@ import { MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { of } from 'rxjs'; +import { throwError } from 'rxjs'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -15,133 +15,106 @@ import { ForkResource, ProjectOverviewSelectors } from '../../store'; import { ForkDialogComponent } from './fork-dialog.component'; -import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; -import { ToastServiceMock } from '@testing/mocks/toast.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + resourceId?: string; + resourceType?: ResourceType | null; +} describe('ForkDialogComponent', () => { let component: ForkDialogComponent; let fixture: ComponentFixture; - let store: jest.Mocked; - let dialogRef: jest.Mocked; - let dialogConfig: jest.Mocked; - let toastService: jest.Mocked; - - const mockResourceId = 'test-resource-id'; - const mockResourceType = ResourceType.Project; - - beforeEach(async () => { - dialogConfig = { - data: { - resourceId: mockResourceId, - resourceType: mockResourceType, - }, - } as jest.Mocked; - - await TestBed.configureTestingModule({ - imports: [ForkDialogComponent, OSFTestingModule], + let store: Store; + let dialogRef: DynamicDialogRef; + let toastService: ToastServiceMockType; + + const defaultSignals: SignalOverride[] = [ + { selector: ProjectOverviewSelectors.getForkProjectSubmitting, value: false }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [ForkDialogComponent], providers: [ - DynamicDialogRefMock, - ToastServiceMock, - MockProvider(DynamicDialogConfig, dialogConfig), - provideMockStore({ - signals: [{ selector: ProjectOverviewSelectors.getForkProjectSubmitting, value: false }], + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { + data: { + resourceId: overrides.resourceId ?? 'project-1', + resourceType: overrides.resourceType === undefined ? ResourceType.Project : overrides.resourceType, + }, }), + MockProvider(ToastService, toastService), + provideMockStore({ signals }), ], - }).compileComponents(); + }); - store = TestBed.inject(Store) as jest.Mocked; - store.dispatch = jest.fn().mockReturnValue(of(true)); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(ForkDialogComponent); component = fixture.componentInstance; - dialogRef = TestBed.inject(DynamicDialogRef) as jest.Mocked; - toastService = TestBed.inject(ToastService) as jest.Mocked; fixture.detectChanges(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); + } - it('should dispatch ForkResource action with correct parameters', () => { - component.handleForkConfirm(); + it('should create', () => { + setup(); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(ForkResource)); - const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof ForkResource); - expect(call).toBeDefined(); - const action = call[0] as ForkResource; - expect(action.resourceId).toBe(mockResourceId); - expect(action.resourceType).toBe(mockResourceType); + expect(component).toBeTruthy(); }); - it('should close dialog with success result', fakeAsync(() => { - const closeSpy = jest.spyOn(dialogRef, 'close'); - - component.handleForkConfirm(); - tick(); - - expect(closeSpy).toHaveBeenCalledWith({ success: true }); - })); + it('should not dispatch fork action when resource id is missing', () => { + setup({ resourceId: '' }); + (store.dispatch as jest.Mock).mockClear(); - it('should show success toast message', fakeAsync(() => { component.handleForkConfirm(); - tick(); - expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.fork.success'); - })); - - it('should not dispatch action when resourceId is missing', () => { - jest.clearAllMocks(); - component.config.data = { - resourceType: mockResourceType, - }; - - component.handleForkConfirm(); - - expect(store.dispatch).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(ForkResource)); expect(dialogRef.close).not.toHaveBeenCalled(); expect(toastService.showSuccess).not.toHaveBeenCalled(); }); - it('should not dispatch action when resourceType is missing', () => { - jest.clearAllMocks(); - component.config.data = { - resourceId: mockResourceId, - }; + it('should not dispatch fork action when resource type is missing', () => { + setup({ resourceType: null }); + (store.dispatch as jest.Mock).mockClear(); component.handleForkConfirm(); - expect(store.dispatch).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(ForkResource)); expect(dialogRef.close).not.toHaveBeenCalled(); expect(toastService.showSuccess).not.toHaveBeenCalled(); }); - it('should not dispatch action when both resourceId and resourceType are missing', () => { - jest.clearAllMocks(); - component.config.data = {}; + it('should dispatch fork action and close dialog with success toast', () => { + setup({ resourceId: 'project-1', resourceType: ResourceType.Project }); + (store.dispatch as jest.Mock).mockClear(); component.handleForkConfirm(); - expect(store.dispatch).not.toHaveBeenCalled(); - expect(dialogRef.close).not.toHaveBeenCalled(); - expect(toastService.showSuccess).not.toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new ForkResource('project-1', ResourceType.Project)); + expect(dialogRef.close).toHaveBeenCalledWith({ success: true }); + expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.fork.success'); }); - it('should handle ForkResource action for Registration resource type', () => { - jest.clearAllMocks(); - component.config.data = { - resourceId: mockResourceId, - resourceType: ResourceType.Registration, - }; + it('should still close dialog and show toast when fork action errors', () => { + setup({ resourceId: 'project-1', resourceType: ResourceType.Project }); + (store.dispatch as jest.Mock).mockClear(); + (store.dispatch as jest.Mock).mockReturnValueOnce(throwError(() => new Error('fork failed'))); component.handleForkConfirm(); - - expect(store.dispatch).toHaveBeenCalledWith(expect.any(ForkResource)); - const call = (store.dispatch as jest.Mock).mock.calls.find((call) => call[0] instanceof ForkResource); - expect(call).toBeDefined(); - const action = call[0] as ForkResource; - expect(action.resourceId).toBe(mockResourceId); - expect(action.resourceType).toBe(ResourceType.Registration); + expect(store.dispatch).toHaveBeenCalledWith(new ForkResource('project-1', ResourceType.Project)); + expect(dialogRef.close).toHaveBeenCalledWith({ success: true }); + expect(toastService.showSuccess).toHaveBeenCalledWith('project.overview.dialog.toast.fork.success'); }); }); diff --git a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts index b74d844cf..da9b4f2bc 100644 --- a/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/link-resource-dialog/link-resource-dialog.component.spec.ts @@ -17,14 +17,14 @@ import { ProjectOverviewSelectors } from '../../store'; import { LinkResourceDialogComponent } from './link-resource-dialog.component'; -import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { MOCK_MY_RESOURCES_ITEM_PROJECT, MOCK_MY_RESOURCES_ITEM_PROJECT_PRIVATE, MOCK_MY_RESOURCES_ITEM_REGISTRATION, } from '@testing/mocks/my-resources.mock'; import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { DynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('LinkResourceDialogComponent', () => { @@ -45,8 +45,9 @@ describe('LinkResourceDialogComponent', () => { dialogRef = { close: jest.fn() }; await TestBed.configureTestingModule({ - imports: [LinkResourceDialogComponent, OSFTestingModule, ...MockComponents(SearchInputComponent)], + imports: [LinkResourceDialogComponent, ...MockComponents(SearchInputComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: MyResourcesSelectors.getProjects, value: mockProjects }, diff --git a/src/app/features/project/overview/components/linked-resources/linked-resources.component.spec.ts b/src/app/features/project/overview/components/linked-resources/linked-resources.component.spec.ts index 14e948bab..1eb9d2a26 100644 --- a/src/app/features/project/overview/components/linked-resources/linked-resources.component.spec.ts +++ b/src/app/features/project/overview/components/linked-resources/linked-resources.component.spec.ts @@ -14,7 +14,7 @@ import { LinkResourceDialogComponent } from '../link-resource-dialog/link-resour import { LinkedResourcesComponent } from './linked-resources.component'; import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -33,12 +33,9 @@ describe('LinkedProjectsComponent', () => { customDialogServiceMock = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); await TestBed.configureTestingModule({ - imports: [ - LinkedResourcesComponent, - OSFTestingModule, - ...MockComponents(IconComponent, ContributorsListComponent), - ], + imports: [LinkedResourcesComponent, ...MockComponents(IconComponent, ContributorsListComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: NodeLinksSelectors.getLinkedResources, value: mockLinkedResources }, diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts index 3ac5256fb..2192db5a5 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts @@ -12,7 +12,7 @@ import { MOCK_COLLECTION_SUBMISSION_WITH_FILTERS, MOCK_COLLECTION_SUBMISSIONS, } from '@testing/mocks/collections-submissions.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('OverviewCollectionsComponent', () => { let component: OverviewCollectionsComponent; @@ -20,7 +20,8 @@ describe('OverviewCollectionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [OverviewCollectionsComponent, OSFTestingModule], + imports: [OverviewCollectionsComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(OverviewCollectionsComponent); diff --git a/src/app/features/project/overview/components/overview-components/overview-components.component.spec.ts b/src/app/features/project/overview/components/overview-components/overview-components.component.spec.ts index 36b82701a..f71eabb8a 100644 --- a/src/app/features/project/overview/components/overview-components/overview-components.component.spec.ts +++ b/src/app/features/project/overview/components/overview-components/overview-components.component.spec.ts @@ -21,7 +21,7 @@ import { AddComponentDialogComponent } from '../add-component-dialog/add-compone import { OverviewComponentsComponent } from './overview-components.component'; import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -62,12 +62,9 @@ describe('ProjectComponentsComponent', () => { toastService = { showSuccess: jest.fn() } as unknown as jest.Mocked; await TestBed.configureTestingModule({ - imports: [ - OverviewComponentsComponent, - OSFTestingModule, - ...MockComponents(IconComponent, ContributorsListComponent), - ], + imports: [OverviewComponentsComponent, ...MockComponents(IconComponent, ContributorsListComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: ProjectOverviewSelectors.getComponents, value: mockComponents }, diff --git a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts index a5ebbf43c..909464450 100644 --- a/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts +++ b/src/app/features/project/overview/components/overview-parent-project/overview-parent-project.component.spec.ts @@ -8,6 +8,7 @@ import { ComponentCardComponent } from '../component-card/component-card.compone import { OverviewParentProjectComponent } from './overview-parent-project.component'; import { MOCK_NODE_WITH_ADMIN } from '@testing/mocks/node.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; describe('OverviewParentProjectComponent', () => { @@ -30,7 +31,7 @@ describe('OverviewParentProjectComponent', () => { await TestBed.configureTestingModule({ imports: [OverviewParentProjectComponent, ...MockComponents(ComponentCardComponent)], - providers: [{ provide: Router, useValue: routerMock }], + providers: [provideOSFCore(), { provide: Router, useValue: routerMock }], }).compileComponents(); fixture = TestBed.createComponent(OverviewParentProjectComponent); diff --git a/src/app/features/project/overview/components/overview-supplements/overview-supplements.component.spec.ts b/src/app/features/project/overview/components/overview-supplements/overview-supplements.component.spec.ts index c730cfb43..ebfeb3d7e 100644 --- a/src/app/features/project/overview/components/overview-supplements/overview-supplements.component.spec.ts +++ b/src/app/features/project/overview/components/overview-supplements/overview-supplements.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { OverviewSupplementsComponent } from './overview-supplements.component'; import { MOCK_NODE_PREPRINTS } from '@testing/mocks/node-preprint.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('OverviewSupplementsComponent', () => { let component: OverviewSupplementsComponent; @@ -11,7 +11,8 @@ describe('OverviewSupplementsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [OverviewSupplementsComponent, OSFTestingModule], + imports: [OverviewSupplementsComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(OverviewSupplementsComponent); diff --git a/src/app/features/project/overview/components/overview-wiki/overview-wiki.component.spec.ts b/src/app/features/project/overview/components/overview-wiki/overview-wiki.component.spec.ts index f413d6c3c..a755322f2 100644 --- a/src/app/features/project/overview/components/overview-wiki/overview-wiki.component.spec.ts +++ b/src/app/features/project/overview/components/overview-wiki/overview-wiki.component.spec.ts @@ -9,7 +9,7 @@ import { WikiSelectors } from '@osf/shared/stores/wiki'; import { OverviewWikiComponent } from './overview-wiki.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -24,8 +24,9 @@ describe('OverviewWikiComponent', () => { routerMock = RouterMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [OverviewWikiComponent, OSFTestingModule, ...MockComponents(TruncatedTextComponent, MarkdownComponent)], + imports: [OverviewWikiComponent, ...MockComponents(TruncatedTextComponent, MarkdownComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: WikiSelectors.getHomeWikiLoading, value: false }, diff --git a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts index 6bd542dd6..95f106e60 100644 --- a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts +++ b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts @@ -37,7 +37,7 @@ import { OverviewSupplementsComponent } from '../overview-supplements/overview-s import { ProjectOverviewMetadataComponent } from './project-overview-metadata.component'; import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -59,7 +59,6 @@ describe('ProjectOverviewMetadataComponent', () => { await TestBed.configureTestingModule({ imports: [ ProjectOverviewMetadataComponent, - OSFTestingModule, ...MockComponents( ResourceCitationsComponent, OverviewCollectionsComponent, @@ -73,6 +72,7 @@ describe('ProjectOverviewMetadataComponent', () => { ), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: ProjectOverviewSelectors.getProject, value: mockProject }, diff --git a/src/app/features/project/overview/components/project-overview-toolbar/project-overview-toolbar.component.spec.ts b/src/app/features/project/overview/components/project-overview-toolbar/project-overview-toolbar.component.spec.ts index 76bfdb06a..6dc065832 100644 --- a/src/app/features/project/overview/components/project-overview-toolbar/project-overview-toolbar.component.spec.ts +++ b/src/app/features/project/overview/components/project-overview-toolbar/project-overview-toolbar.component.spec.ts @@ -22,7 +22,7 @@ import { TogglePublicityDialogComponent } from '../toggle-publicity-dialog/toggl import { ProjectOverviewToolbarComponent } from './project-overview-toolbar.component'; import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -57,8 +57,9 @@ describe('ProjectOverviewToolbarComponent', () => { toastService = { showSuccess: jest.fn() } as unknown as jest.Mocked; await TestBed.configureTestingModule({ - imports: [ProjectOverviewToolbarComponent, OSFTestingModule, ...MockComponents(SocialsShareButtonComponent)], + imports: [ProjectOverviewToolbarComponent, ...MockComponents(SocialsShareButtonComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: BookmarksSelectors.getBookmarksCollectionId, value: 'bookmarks-123' }, diff --git a/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.spec.ts b/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.spec.ts index 596f67dbc..8db73a8c1 100644 --- a/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.spec.ts +++ b/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.spec.ts @@ -8,7 +8,7 @@ import { ActivityLogsSelectors, ClearActivityLogs } from '@osf/shared/stores/act import { ProjectRecentActivityComponent } from './project-recent-activity.component'; import { MOCK_ACTIVITY_LOGS_WITH_DISPLAY } from '@testing/mocks/activity-log-with-display.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('ProjectRecentActivityComponent', () => { @@ -18,8 +18,9 @@ describe('ProjectRecentActivityComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProjectRecentActivityComponent, OSFTestingModule], + imports: [ProjectRecentActivityComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: ActivityLogsSelectors.getActivityLogs, value: [] }, @@ -134,8 +135,9 @@ describe('ProjectRecentActivityComponent', () => { it('should return activity logs from selector', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ - imports: [ProjectRecentActivityComponent, OSFTestingModule], + imports: [ProjectRecentActivityComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: ActivityLogsSelectors.getActivityLogs, value: MOCK_ACTIVITY_LOGS_WITH_DISPLAY }, @@ -156,8 +158,9 @@ describe('ProjectRecentActivityComponent', () => { it('should return totalCount from selector', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ - imports: [ProjectRecentActivityComponent, OSFTestingModule], + imports: [ProjectRecentActivityComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: ActivityLogsSelectors.getActivityLogs, value: [] }, @@ -178,8 +181,9 @@ describe('ProjectRecentActivityComponent', () => { it('should return isLoading from selector', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ - imports: [ProjectRecentActivityComponent, OSFTestingModule], + imports: [ProjectRecentActivityComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: ActivityLogsSelectors.getActivityLogs, value: [] }, diff --git a/src/app/features/project/overview/components/toggle-publicity-dialog/toggle-publicity-dialog.component.spec.ts b/src/app/features/project/overview/components/toggle-publicity-dialog/toggle-publicity-dialog.component.spec.ts index a0d0f58b0..2dd8e9659 100644 --- a/src/app/features/project/overview/components/toggle-publicity-dialog/toggle-publicity-dialog.component.spec.ts +++ b/src/app/features/project/overview/components/toggle-publicity-dialog/toggle-publicity-dialog.component.spec.ts @@ -7,6 +7,8 @@ import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/ import { TogglePublicityDialogComponent } from './toggle-publicity-dialog.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe.skip('TogglePublicityDialogComponent', () => { let component: TogglePublicityDialogComponent; let fixture: ComponentFixture; @@ -17,6 +19,7 @@ describe.skip('TogglePublicityDialogComponent', () => { TogglePublicityDialogComponent, ...MockComponents(ComponentsSelectionListComponent, LoadingSpinnerComponent), ], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(TogglePublicityDialogComponent); diff --git a/src/app/features/project/overview/project-overview.component.spec.ts b/src/app/features/project/overview/project-overview.component.spec.ts index 824d80e26..29eb3d952 100644 --- a/src/app/features/project/overview/project-overview.component.spec.ts +++ b/src/app/features/project/overview/project-overview.component.spec.ts @@ -2,28 +2,27 @@ import { Store } from '@ngxs/store'; import { MockComponents, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; +import { Subject } from 'rxjs'; -import { HttpTestingController } from '@angular/common/http/testing'; -import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { provideServerRendering } from '@angular/ssr'; +import { ReviewAction } from '@osf/features/moderation/models'; import { ClearCollectionModeration, CollectionsModerationSelectors, + GetSubmissionsReviewActions, } from '@osf/features/moderation/store/collections-moderation'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; -import { Mode } from '@osf/shared/enums/mode.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { SignpostingService } from '@osf/shared/services/signposting.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { AddonsSelectors, ClearConfiguredAddons } from '@osf/shared/stores/addons'; import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; -import { ClearCollections, CollectionsSelectors } from '@osf/shared/stores/collections'; +import { ClearCollections, CollectionsSelectors, GetCollectionProvider } from '@osf/shared/stores/collections'; import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { GetLinkedResources } from '@osf/shared/stores/node-links'; import { ClearWiki } from '@osf/shared/stores/wiki'; @@ -42,40 +41,99 @@ import { ProjectOverviewComponent } from './project-overview.component'; import { ClearProjectOverview, GetComponents, GetProjectById, ProjectOverviewSelectors } from './store'; import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; -import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; -import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; +import { ViewOnlyLinkHelperMock } from '@testing/providers/view-only-link-helper.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + routerUrl?: string; + queryParams?: Record; +} describe('ProjectOverviewComponent', () => { - let fixture: ComponentFixture; let component: ProjectOverviewComponent; - let store: jest.Mocked; - let routerMock: ReturnType; - let activatedRouteMock: ReturnType; + let fixture: ComponentFixture; + let store: Store; + let routerMock: RouterMockType; let customDialogServiceMock: ReturnType; - let toastService: jest.Mocked; - - const mockProject: ProjectOverviewModel = { - ...MOCK_PROJECT_OVERVIEW, - id: 'project-123', - title: 'Test Project', - parentId: 'parent-123', - rootParentId: 'root-123', - isPublic: true, + let toastServiceMock: ToastServiceMockType; + let signpostingServiceMock: { + addSignposting: jest.Mock; + removeSignpostingLinkTags: jest.Mock; }; - beforeEach(async () => { - routerMock = RouterMockBuilder.create().withUrl('/test').build(); - activatedRouteMock = ActivatedRouteMockBuilder.create().withParams({ id: 'project-123' }).build(); - customDialogServiceMock = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); - toastService = { showSuccess: jest.fn() } as unknown as jest.Mocked; - - await TestBed.configureTestingModule({ + const mockProject = MOCK_PROJECT_OVERVIEW as ProjectOverviewModel; + + const defaultSignals: SignalOverride[] = [ + { selector: CollectionsModerationSelectors.getCollectionSubmissions, value: [] }, + { selector: CollectionsSelectors.getCollectionProvider, value: null }, + { selector: CollectionsModerationSelectors.getCurrentReviewAction, value: null }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: ProjectOverviewSelectors.getProject, value: null }, + { selector: ProjectOverviewSelectors.getProjectLoading, value: false }, + { selector: ProjectOverviewSelectors.isProjectAnonymous, value: false }, + { selector: ProjectOverviewSelectors.hasWriteAccess, value: false }, + { selector: ProjectOverviewSelectors.hasAdminAccess, value: false }, + { selector: ProjectOverviewSelectors.isWikiEnabled, value: false }, + { selector: ProjectOverviewSelectors.getParentProject, value: null }, + { selector: ProjectOverviewSelectors.getParentProjectLoading, value: false }, + { selector: AddonsSelectors.getAddonsResourceReference, value: [] }, + { selector: AddonsSelectors.getConfiguredCitationAddons, value: [] }, + { selector: AddonsSelectors.getOperationInvocation, value: null }, + { selector: ProjectOverviewSelectors.getStorage, value: null }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const routeBuilder = ActivatedRouteMockBuilder.create(); + if (overrides.routeParams) { + routeBuilder.withParams(overrides.routeParams); + } else { + routeBuilder.withParams({ id: 'project-1', collectionId: 'collection-1' }); + } + if (overrides.queryParams) { + routeBuilder.withQueryParams(overrides.queryParams); + } + if (overrides.hasParent === false) { + routeBuilder.withNoParent(); + } + const activatedRouteMock = routeBuilder.build(); + + routerMock = RouterMockBuilder.create() + .withUrl(overrides.routerUrl ?? '/project/project-1') + .build(); + + const decisionClose$ = new Subject<{ action?: string }>(); + customDialogServiceMock = CustomDialogServiceMockBuilder.create() + .withOpen( + jest.fn().mockReturnValue({ + onClose: decisionClose$, + close: jest.fn(), + destroy: jest.fn(), + }) + ) + .build(); + + toastServiceMock = ToastServiceMock.simple(); + signpostingServiceMock = { + addSignposting: jest.fn(), + removeSignpostingLinkTags: jest.fn(), + }; + const viewOnlyLinkHelperMock = ViewOnlyLinkHelperMock.simple(); + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + + TestBed.configureTestingModule({ imports: [ ProjectOverviewComponent, - OSFTestingModule, ...MockComponents( SubHeaderComponent, LoadingSpinnerComponent, @@ -92,222 +150,156 @@ describe('ProjectOverviewComponent', () => { ), ], providers: [ - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: ProjectOverviewSelectors.getProjectLoading, value: false }, - { selector: ProjectOverviewSelectors.isProjectAnonymous, value: false }, - { selector: ProjectOverviewSelectors.hasWriteAccess, value: true }, - { selector: ProjectOverviewSelectors.hasAdminAccess, value: true }, - { selector: ProjectOverviewSelectors.isWikiEnabled, value: true }, - { selector: ProjectOverviewSelectors.getParentProject, value: null }, - { selector: ProjectOverviewSelectors.getParentProjectLoading, value: false }, - { selector: ProjectOverviewSelectors.getStorage, value: null }, - { selector: ProjectOverviewSelectors.isStorageLoading, value: false }, - { selector: CollectionsModerationSelectors.getCollectionSubmissions, value: [] }, - { selector: CollectionsModerationSelectors.getCurrentReviewAction, value: null }, - { selector: CollectionsModerationSelectors.getCurrentReviewActionLoading, value: false }, - { selector: CollectionsSelectors.getCollectionProvider, value: null }, - { selector: CollectionsSelectors.getCollectionProviderLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: AddonsSelectors.getAddonsResourceReference, value: [] }, - { selector: AddonsSelectors.getConfiguredCitationAddons, value: [] }, - { selector: AddonsSelectors.getOperationInvocation, value: null }, - ], - }), - MockProvider(Router, routerMock), + provideOSFCore(), MockProvider(ActivatedRoute, activatedRouteMock), + MockProvider(Router, routerMock), MockProvider(CustomDialogService, customDialogServiceMock), - MockProvider(ToastService, toastService), + MockProvider(ToastService, toastServiceMock), + MockProvider(SignpostingService, signpostingServiceMock), + MockProvider(ViewOnlyLinkHelperService, viewOnlyLinkHelperMock), + provideMockStore({ signals }), ], - }).compileComponents(); + }); - store = TestBed.inject(Store) as jest.Mocked; - store.dispatch = jest.fn().mockReturnValue(of(true)); + store = TestBed.inject(Store); fixture = TestBed.createComponent(ProjectOverviewComponent); component = fixture.componentInstance; + fixture.detectChanges(); + + return { decisionClose$ }; + } + + it('should create', () => { + setup(); + + expect(component).toBeTruthy(); }); - it('should dispatch actions when projectId exists in route params', () => { - component.ngOnInit(); + it('should dispatch init actions and add signposting on init when project id exists', () => { + setup(); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetProjectById)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetBookmarksCollectionId)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetComponents)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetLinkedResources)); + expect(store.dispatch).toHaveBeenCalledWith(new GetProjectById('project-1')); + expect(store.dispatch).toHaveBeenCalledWith(new GetBookmarksCollectionId()); + expect(store.dispatch).toHaveBeenCalledWith(new GetComponents('project-1')); + expect(store.dispatch).toHaveBeenCalledWith(new GetLinkedResources('project-1')); + expect(signpostingServiceMock.addSignposting).toHaveBeenCalledWith('project-1'); }); - it('should dispatch actions when projectId exists in parent route params', () => { - activatedRouteMock.snapshot!.params = {}; - Object.defineProperty(activatedRouteMock, 'parent', { - value: { snapshot: { params: { id: 'parent-project-123' } } }, - writable: true, - configurable: true, + it('should not dispatch init project actions when project id is missing', () => { + setup({ + hasParent: false, + routeParams: {}, }); - component.ngOnInit(); - - expect(store.dispatch).toHaveBeenCalledWith(expect.any(GetProjectById)); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(GetProjectById)); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(GetComponents)); + expect(signpostingServiceMock.addSignposting).not.toHaveBeenCalled(); }); - it('should return true for isModerationMode when query param mode is moderation', () => { - activatedRouteMock.snapshot!.queryParams = { mode: Mode.Moderation }; - fixture.detectChanges(); + it('should dispatch collection provider action in moderation collections route', () => { + setup({ + routerUrl: '/collections/abc/project/project-1', + queryParams: { mode: 'moderation' }, + routeParams: { id: 'project-1', collectionId: 'collection-77' }, + }); - expect(component.isModerationMode()).toBe(true); + expect(store.dispatch).toHaveBeenCalledWith(new GetCollectionProvider('collection-77')); }); - it('should return false for isModerationMode when query param mode is not moderation', () => { - activatedRouteMock.snapshot!.queryParams = { mode: 'other' }; - fixture.detectChanges(); + it('should dispatch current review action when provider and project are available', () => { + setup({ + routerUrl: '/collections/abc/project/project-1', + queryParams: { mode: 'moderation' }, + selectorOverrides: [ + { + selector: CollectionsSelectors.getCollectionProvider, + value: { id: 'provider-1', primaryCollection: { id: 'primary-1' } }, + }, + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + ], + }); - expect(component.isModerationMode()).toBe(false); + expect(store.dispatch).toHaveBeenCalledWith(new GetSubmissionsReviewActions('project-1', 'primary-1')); }); - it('should dispatch cleanup actions on component destroy', () => { - fixture.destroy(); + it('should open decision dialog and on action show toast and navigate back preserving status', () => { + const { decisionClose$ } = setup({ + queryParams: { status: 'pending' }, + }); + (routerMock.navigate as jest.Mock).mockClear(); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(ClearProjectOverview)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(ClearWiki)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(ClearCollections)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(ClearCollectionModeration)); - expect(store.dispatch).toHaveBeenCalledWith(expect.any(ClearConfiguredAddons)); - }); -}); + component.handleOpenMakeDecisionDialog(); + decisionClose$.next({ action: 'accept' }); -describe('ProjectOverviewComponent SSR Tests', () => { - let component: ProjectOverviewComponent; - let fixture: ComponentFixture; - let httpMock: HttpTestingController; - let mockActivatedRoute: ReturnType; - let mockRouter: ReturnType; - let store: Store; + expect(customDialogServiceMock.open).toHaveBeenCalled(); + expect(toastServiceMock.showSuccess).toHaveBeenCalledWith('moderation.makeDecision.acceptSuccess'); + expect(routerMock.navigate).toHaveBeenCalledWith(['../'], { + relativeTo: expect.any(Object), + queryParams: { status: 'pending' }, + }); + }); - const mockProject: ProjectOverviewModel = { - ...MOCK_PROJECT_OVERVIEW, - id: 'project-123', - title: 'Test Project', - parentId: 'parent-123', - rootParentId: 'root-123', - isPublic: true, - }; + it('should not show toast or navigate back when decision dialog closes without action', () => { + const { decisionClose$ } = setup(); + (routerMock.navigate as jest.Mock).mockClear(); - beforeEach(async () => { - mockRouter = RouterMockBuilder.create().withUrl('/projects/project-123').build(); - const parentRoute = { - params: of({ id: 'project-123' }), - snapshot: { params: { id: 'project-123' }, queryParams: {} }, - } as any; - mockActivatedRoute = Object.assign( - ActivatedRouteMockBuilder.create().withParams({ id: 'project-123' }).withQueryParams({}).build(), - { parent: parentRoute } - ); - - await TestBed.configureTestingModule({ - imports: [ - ProjectOverviewComponent, - OSFTestingModule, - ...MockComponents( - SubHeaderComponent, - LoadingSpinnerComponent, - OverviewWikiComponent, - OverviewComponentsComponent, - LinkedResourcesComponent, - ProjectRecentActivityComponent, - ProjectOverviewToolbarComponent, - ProjectOverviewMetadataComponent, - FilesWidgetComponent, - ViewOnlyLinkMessageComponent, - OverviewParentProjectComponent, - CitationAddonCardComponent - ), - ], - providers: [ - provideServerRendering(), - { provide: PLATFORM_ID, useValue: 'server' }, - MockProvider(ActivatedRoute, mockActivatedRoute), - MockProvider(Router, mockRouter), - MockProvider(CustomDialogService, CustomDialogServiceMockBuilder.create().build()), - MockProvider(ToastService, { showSuccess: jest.fn() }), - MockProvider(ViewOnlyLinkHelperService, { hasViewOnlyParam: jest.fn().mockReturnValue(false) }), - provideMockStore({ - signals: [ - { selector: ProjectOverviewSelectors.getProject, value: mockProject }, - { selector: ProjectOverviewSelectors.getProjectLoading, value: false }, - { selector: ProjectOverviewSelectors.isProjectAnonymous, value: false }, - { selector: ProjectOverviewSelectors.hasWriteAccess, value: true }, - { selector: ProjectOverviewSelectors.hasAdminAccess, value: true }, - { selector: ProjectOverviewSelectors.isWikiEnabled, value: true }, - { selector: ProjectOverviewSelectors.getParentProject, value: null }, - { selector: ProjectOverviewSelectors.getParentProjectLoading, value: false }, - { selector: ProjectOverviewSelectors.getStorage, value: null }, - { selector: ProjectOverviewSelectors.isStorageLoading, value: false }, - { selector: CollectionsModerationSelectors.getCollectionSubmissions, value: [] }, - { selector: CollectionsModerationSelectors.getCurrentReviewAction, value: null }, - { selector: CollectionsModerationSelectors.getCurrentReviewActionLoading, value: false }, - { selector: CollectionsSelectors.getCollectionProvider, value: null }, - { selector: CollectionsSelectors.getCollectionProviderLoading, value: false }, - { selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }, - { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, - { selector: AddonsSelectors.getAddonsResourceReference, value: [] }, - { selector: AddonsSelectors.getConfiguredCitationAddons, value: [] }, - { selector: AddonsSelectors.getOperationInvocation, value: null }, - ], - }), - ], - }).compileComponents(); + component.handleOpenMakeDecisionDialog(); + decisionClose$.next({}); - httpMock = TestBed.inject(HttpTestingController); - store = TestBed.inject(Store); - fixture = TestBed.createComponent(ProjectOverviewComponent); - component = fixture.componentInstance; - document.head.innerHTML = ''; + expect(toastServiceMock.showSuccess).not.toHaveBeenCalled(); + expect(routerMock.navigate).not.toHaveBeenCalled(); }); - it('should render ProjectOverviewComponent server-side without errors', () => { - expect(() => { - fixture.detectChanges(); - }).not.toThrow(); - expect(component).toBeTruthy(); - }); + it('should navigate back without query params when status is missing', () => { + setup({ + queryParams: {}, + }); - it('should not access browser-only APIs during SSR', () => { - const platformId = TestBed.inject(PLATFORM_ID); - expect(platformId).toBe('server'); - fixture.detectChanges(); - expect(component).toBeTruthy(); - }); + component.goBack(); - it('should execute constructor effects without errors in SSR context', () => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); - fixture.detectChanges(); - expect(dispatchSpy).toHaveBeenCalled(); - expect(component).toBeTruthy(); + expect(routerMock.navigate).toHaveBeenCalledWith(['../'], { + relativeTo: expect.any(Object), + queryParams: {}, + }); }); - it('should add signposting tags during SSR', () => { - fixture.detectChanges(); + it('should remove signposting tags on destroy', () => { + setup(); - const linkTags = Array.from(document.head.querySelectorAll('link[rel="linkset"]')); - expect(linkTags.length).toBe(2); - expect(linkTags[0].getAttribute('href')).toBe('http://localhost:4200/metadata/project-123/?format=linkset'); - expect(linkTags[0].getAttribute('type')).toBe('application/linkset'); - expect(linkTags[1].getAttribute('href')).toBe('http://localhost:4200/metadata/project-123/?format=linkset-json'); - expect(linkTags[1].getAttribute('type')).toBe('application/linkset+json'); + component.ngOnDestroy(); + + expect(signpostingServiceMock.removeSignpostingLinkTags).toHaveBeenCalled(); }); - it('should not call browser-only actions in ngOnDestroy during SSR', () => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); + it('should dispatch cleanup actions when fixture is destroyed', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); - fixture.detectChanges(); - dispatchSpy.mockClear(); fixture.destroy(); - expect(dispatchSpy).not.toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new ClearProjectOverview()); + expect(store.dispatch).toHaveBeenCalledWith(new ClearWiki()); + expect(store.dispatch).toHaveBeenCalledWith(new ClearCollections()); + expect(store.dispatch).toHaveBeenCalledWith(new ClearCollectionModeration()); + expect(store.dispatch).toHaveBeenCalledWith(new ClearConfiguredAddons()); }); - afterEach(() => { - httpMock.verify(); + it('should compute showDecisionButton correctly for collection route and removable statuses', () => { + const reviewAction: ReviewAction = { + id: '1', + fromState: 'pending', + toState: 'accepted', + trigger: 'accept', + dateModified: '', + creator: null, + comment: '', + }; + + setup({ + routerUrl: '/collections/abc/project/project-1', + selectorOverrides: [{ selector: CollectionsModerationSelectors.getCurrentReviewAction, value: reviewAction }], + }); + + expect(component.showDecisionButton()).toBe(true); }); }); diff --git a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.spec.ts b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.spec.ts index 4d90a86ee..e6c8a3631 100644 --- a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.spec.ts +++ b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.spec.ts @@ -1,8 +1,6 @@ import { provideStore } from '@ngxs/store'; -import { MockComponents } from 'ng-mocks'; - -import { MessageService } from 'primeng/api'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; @@ -12,14 +10,14 @@ import { ActivatedRoute, Router } from '@angular/router'; import { StorageItemSelectorComponent } from '@osf/shared/components/addons/storage-item-selector/storage-item-selector.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { ToastService } from '@osf/shared/services/toast.service'; import { AddonsState } from '@osf/shared/stores/addons'; import { ConfigureAddonComponent } from './configure-addon.component'; import { getConfiguredAddonsData } from '@testing/data/addons/addons.configured.data'; import { getAddonsOperationInvocation } from '@testing/data/addons/addons.operation-invocation.data'; -import { ToastServiceMock } from '@testing/mocks/toast.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { environment } from 'src/environments/environment'; describe.skip('Component: Configure Addon', () => { @@ -61,15 +59,11 @@ describe.skip('Component: Configure Addon', () => { } as unknown as Router; await TestBed.configureTestingModule({ - imports: [ - OSFTestingModule, - ConfigureAddonComponent, - ...MockComponents(SubHeaderComponent, StorageItemSelectorComponent), - ], + imports: [ConfigureAddonComponent, ...MockComponents(SubHeaderComponent, StorageItemSelectorComponent)], providers: [ + provideOSFCore(), provideStore([AddonsState]), - ToastServiceMock, - MessageService, + MockProvider(ToastService), { provide: Router, useValue: mockRouter }, { provide: ActivatedRoute, @@ -160,10 +154,11 @@ describe.skip('Component: Configure Addon', () => { } as unknown as Router; await TestBed.configureTestingModule({ - imports: [OSFTestingModule, ConfigureAddonComponent], + imports: [ConfigureAddonComponent], providers: [ + provideOSFCore(), provideStore([AddonsState]), - ToastServiceMock, + MockProvider(ToastService), { provide: Router, useValue: mockRouter }, { provide: ActivatedRoute, diff --git a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts index 6c025c5e2..9df4875c1 100644 --- a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts +++ b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ConfirmAccountConnectionModalComponent } from './confirm-account-connection-modal.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe.skip('ConfirmAccountConnectionModalComponent', () => { let component: ConfirmAccountConnectionModalComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe.skip('ConfirmAccountConnectionModalComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ConfirmAccountConnectionModalComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ConfirmAccountConnectionModalComponent); diff --git a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts index 90806b9ff..dd00f5088 100644 --- a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts +++ b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts @@ -1,12 +1,11 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { TranslateService } from '@ngx-translate/core'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { ActivatedRoute, Navigation, Router, UrlTree } from '@angular/router'; import { AddonSetupAccountFormComponent } from '@osf/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component'; @@ -18,6 +17,8 @@ import { AddonsSelectors } from '@osf/shared/stores/addons'; import { ConnectConfiguredAddonComponent } from './connect-configured-addon.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe.skip('ConnectAddonComponent', () => { let component: ConnectConfiguredAddonComponent; let fixture: ComponentFixture; @@ -49,10 +50,9 @@ describe.skip('ConnectAddonComponent', () => { imports: [ ConnectConfiguredAddonComponent, ...MockComponents(SubHeaderComponent, AddonSetupAccountFormComponent, StorageItemSelectorComponent), - MockPipe(TranslatePipe), ], providers: [ - provideNoopAnimations(), + provideOSFCore(), MockProvider(Store, { selectSignal: jest.fn().mockImplementation((selector) => { if (selector === AddonsSelectors.getAddonsUserReference) { diff --git a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts index f86043d50..33946cd17 100644 --- a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts +++ b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DisconnectAddonModalComponent } from './disconnect-addon-modal.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe.skip('DisconnectAddonModalComponent', () => { let component: DisconnectAddonModalComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe.skip('DisconnectAddonModalComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [DisconnectAddonModalComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(DisconnectAddonModalComponent); diff --git a/src/app/features/project/project-addons/project-addons.component.spec.ts b/src/app/features/project/project-addons/project-addons.component.spec.ts index 000118b8b..697303f0e 100644 --- a/src/app/features/project/project-addons/project-addons.component.spec.ts +++ b/src/app/features/project/project-addons/project-addons.component.spec.ts @@ -15,7 +15,7 @@ import { AddonsState } from '@osf/shared/stores/addons'; import { ProjectAddonsComponent } from './project-addons.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe.skip('Component: Addons', () => { let component: ProjectAddonsComponent; @@ -25,7 +25,6 @@ describe.skip('Component: Addons', () => { await TestBed.configureTestingModule({ imports: [ ProjectAddonsComponent, - OSFTestingModule, ...MockComponents( AddonCardListComponent, AddonsToolbarComponent, @@ -35,6 +34,7 @@ describe.skip('Component: Addons', () => { ), ], providers: [ + provideOSFCore(), provideStore([UserState, AddonsState]), { provide: UserSelectors, diff --git a/src/app/features/project/project.component.spec.ts b/src/app/features/project/project.component.spec.ts index 88b558a8c..e552126b4 100644 --- a/src/app/features/project/project.component.spec.ts +++ b/src/app/features/project/project.component.spec.ts @@ -20,10 +20,10 @@ import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { GetProjectById, GetProjectIdentifiers, GetProjectLicense, ProjectOverviewSelectors } from './overview/store'; import { ProjectComponent } from './project.component'; -import { DataciteMockFactory } from '@testing/mocks/datacite.service.mock'; import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { AnalyticsServiceMockFactory } from '@testing/providers/analytics.service.mock'; +import { DataciteServiceMock } from '@testing/providers/datacite.service.mock'; import { HelpScoutServiceMockFactory } from '@testing/providers/help-scout.service.mock'; import { MetaTagsServiceMockFactory } from '@testing/providers/meta-tags.service.mock'; import { MetaTagsBuilderServiceMockFactory } from '@testing/providers/meta-tags-builder.service.mock'; @@ -51,7 +51,7 @@ function setup(overrides: SetupOverrides = {}) { const analyticsService = AnalyticsServiceMockFactory(); const metaTagsService = MetaTagsServiceMockFactory(); const metaTagsBuilderService = MetaTagsBuilderServiceMockFactory(); - const dataciteService = DataciteMockFactory(); + const dataciteService = DataciteServiceMock.simple(); const prerenderReadyService = PrerenderReadyServiceMockFactory(); const routerBuilder = RouterMockBuilder.create(); const routerMock = routerBuilder.build(); diff --git a/src/app/features/project/registrations/registrations.component.spec.ts b/src/app/features/project/registrations/registrations.component.spec.ts index b00a06e57..9b956577e 100644 --- a/src/app/features/project/registrations/registrations.component.spec.ts +++ b/src/app/features/project/registrations/registrations.component.spec.ts @@ -15,7 +15,7 @@ import { RegistrationsComponent } from './registrations.component'; import { GetRegistrations, RegistrationsSelectors } from './store'; import { MOCK_REGISTRATION } from '@testing/mocks/registration.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -51,7 +51,6 @@ describe('RegistrationsComponent', () => { await TestBed.configureTestingModule({ imports: [ RegistrationsComponent, - OSFTestingModule, ...MockComponents( RegistrationCardComponent, SubHeaderComponent, @@ -60,6 +59,7 @@ describe('RegistrationsComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(ENVIRONMENT, mockEnvironment), diff --git a/src/app/features/project/settings/components/delete-project-dialog/delete-project-dialog.component.spec.ts b/src/app/features/project/settings/components/delete-project-dialog/delete-project-dialog.component.spec.ts index 3d9593542..5ef802c91 100644 --- a/src/app/features/project/settings/components/delete-project-dialog/delete-project-dialog.component.spec.ts +++ b/src/app/features/project/settings/components/delete-project-dialog/delete-project-dialog.component.spec.ts @@ -11,7 +11,7 @@ import { SettingsSelectors } from '../../store'; import { DeleteProjectDialogComponent } from './delete-project-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('DeleteProjectDialogComponent', () => { @@ -20,8 +20,9 @@ describe('DeleteProjectDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DeleteProjectDialogComponent, OSFTestingModule], + imports: [DeleteProjectDialogComponent], providers: [ + provideOSFCore(), MockProvider(ToastService), MockProvider(DynamicDialogRef), provideMockStore({ diff --git a/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts index 59acd3066..73e76eca4 100644 --- a/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts +++ b/src/app/features/project/settings/components/project-detail-setting-accordion/project-detail-setting-accordion.component.spec.ts @@ -6,7 +6,7 @@ import { SelectComponent } from '@osf/shared/components/select/select.component' import { ProjectDetailSettingAccordionComponent } from './project-detail-setting-accordion.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ProjectDetailSettingAccordionComponent', () => { let component: ProjectDetailSettingAccordionComponent; @@ -14,7 +14,8 @@ describe('ProjectDetailSettingAccordionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProjectDetailSettingAccordionComponent, OSFTestingModule, MockComponent(SelectComponent)], + imports: [ProjectDetailSettingAccordionComponent, MockComponent(SelectComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ProjectDetailSettingAccordionComponent); diff --git a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts index a47fe699b..4dd085654 100644 --- a/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts +++ b/src/app/features/project/settings/components/project-setting-notifications/project-setting-notifications.component.spec.ts @@ -11,7 +11,7 @@ import { ProjectDetailSettingAccordionComponent } from '../project-detail-settin import { ProjectSettingNotificationsComponent } from './project-setting-notifications.component'; import { MOCK_NOTIFICATION_SUBSCRIPTIONS } from '@testing/mocks/notification-subscription.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ProjectSettingNotificationsComponent', () => { let component: ProjectSettingNotificationsComponent; @@ -23,10 +23,10 @@ describe('ProjectSettingNotificationsComponent', () => { await TestBed.configureTestingModule({ imports: [ ProjectSettingNotificationsComponent, - OSFTestingModule, MockComponent(ProjectDetailSettingAccordionComponent), MockPipe(NotificationDescriptionPipe), ], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ProjectSettingNotificationsComponent); diff --git a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts index c5b4ca7fc..22d1343c9 100644 --- a/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-access-requests-card/settings-access-requests-card.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SettingsAccessRequestsCardComponent } from './settings-access-requests-card.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SettingsAccessRequestsCardComponent', () => { let component: SettingsAccessRequestsCardComponent; @@ -10,7 +10,8 @@ describe('SettingsAccessRequestsCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsAccessRequestsCardComponent, OSFTestingModule], + imports: [SettingsAccessRequestsCardComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SettingsAccessRequestsCardComponent); diff --git a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts index f13917010..f6abd95b2 100644 --- a/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts +++ b/src/app/features/project/settings/components/settings-project-affiliation/settings-project-affiliation.component.spec.ts @@ -6,7 +6,7 @@ import { InstitutionsSelectors } from '@osf/shared/stores/institutions'; import { SettingsProjectAffiliationComponent } from './settings-project-affiliation.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('SettingsProjectAffiliationComponent', () => { @@ -17,8 +17,9 @@ describe('SettingsProjectAffiliationComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsProjectAffiliationComponent, OSFTestingModule], + imports: [SettingsProjectAffiliationComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: [] }], }), @@ -67,8 +68,9 @@ describe('SettingsProjectAffiliationComponent', () => { beforeEach(() => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ - imports: [SettingsProjectAffiliationComponent, OSFTestingModule], + imports: [SettingsProjectAffiliationComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: userInstitutions }], }), @@ -123,8 +125,9 @@ describe('SettingsProjectAffiliationComponent', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ - imports: [SettingsProjectAffiliationComponent, OSFTestingModule], + imports: [SettingsProjectAffiliationComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: userInstitutions }], }), @@ -145,8 +148,9 @@ describe('SettingsProjectAffiliationComponent', () => { it('should return empty Set when no user institutions', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ - imports: [SettingsProjectAffiliationComponent, OSFTestingModule], + imports: [SettingsProjectAffiliationComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: InstitutionsSelectors.getUserInstitutions, value: [] }], }), diff --git a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts index 7429159fa..b340da9f4 100644 --- a/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-project-form-card/settings-project-form-card.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockDirective, MockPipe } from 'ng-mocks'; +import { MockComponent, MockDirective } from 'ng-mocks'; import { Textarea } from 'primeng/textarea'; @@ -13,7 +12,7 @@ import { NodeDetailsModel } from '../../models'; import { SettingsProjectFormCardComponent } from './settings-project-form-card.component'; import { MOCK_NODE_DETAILS } from '@testing/mocks/node-details.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SettingsProjectFormCardComponent', () => { let component: SettingsProjectFormCardComponent; @@ -23,13 +22,8 @@ describe('SettingsProjectFormCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - SettingsProjectFormCardComponent, - OSFTestingModule, - MockComponent(TextInputComponent), - MockPipe(TranslatePipe), - MockDirective(Textarea), - ], + imports: [SettingsProjectFormCardComponent, MockComponent(TextInputComponent), MockDirective(Textarea)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SettingsProjectFormCardComponent); diff --git a/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts b/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts index e28b6b845..3a44b73e4 100644 --- a/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-storage-location-card/settings-storage-location-card.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SettingsStorageLocationCardComponent } from './settings-storage-location-card.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SettingsStorageLocationCardComponent', () => { let component: SettingsStorageLocationCardComponent; @@ -13,7 +13,8 @@ describe('SettingsStorageLocationCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsStorageLocationCardComponent, OSFTestingModule], + imports: [SettingsStorageLocationCardComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SettingsStorageLocationCardComponent); diff --git a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts index 72f72e818..2bb5f8298 100644 --- a/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-view-only-links-card/settings-view-only-links-card.component.spec.ts @@ -8,7 +8,7 @@ import { PaginatedViewOnlyLinksModel } from '@shared/models/view-only-links/view import { SettingsViewOnlyLinksCardComponent } from './settings-view-only-links-card.component'; import { MOCK_PAGINATED_VIEW_ONLY_LINKS, MOCK_VIEW_ONLY_LINK } from '@testing/mocks/view-only-link.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SettingsViewOnlyLinksCardComponent', () => { let component: SettingsViewOnlyLinksCardComponent; @@ -19,7 +19,8 @@ describe('SettingsViewOnlyLinksCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsViewOnlyLinksCardComponent, OSFTestingModule, MockComponent(ViewOnlyTableComponent)], + imports: [SettingsViewOnlyLinksCardComponent, MockComponent(ViewOnlyTableComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SettingsViewOnlyLinksCardComponent); diff --git a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts index 97bb732b3..1814c92ea 100644 --- a/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts +++ b/src/app/features/project/settings/components/settings-wiki-card/settings-wiki-card.component.spec.ts @@ -6,7 +6,7 @@ import { ProjectDetailSettingAccordionComponent } from '../project-detail-settin import { SettingsWikiCardComponent } from './settings-wiki-card.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SettingsWikiCardComponent', () => { let component: SettingsWikiCardComponent; @@ -19,7 +19,8 @@ describe('SettingsWikiCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsWikiCardComponent, OSFTestingModule, MockComponent(ProjectDetailSettingAccordionComponent)], + imports: [SettingsWikiCardComponent, MockComponent(ProjectDetailSettingAccordionComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SettingsWikiCardComponent); diff --git a/src/app/features/project/settings/settings.component.spec.ts b/src/app/features/project/settings/settings.component.spec.ts index 6efcd5fb4..b673778bc 100644 --- a/src/app/features/project/settings/settings.component.spec.ts +++ b/src/app/features/project/settings/settings.component.spec.ts @@ -23,7 +23,7 @@ import { SettingsComponent } from './settings.component'; import { SettingsSelectors } from './store'; import { MOCK_VIEW_ONLY_LINK } from '@testing/mocks/view-only-link.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -69,7 +69,6 @@ describe.skip('SettingsComponent', () => { await TestBed.configureTestingModule({ imports: [ SettingsComponent, - OSFTestingModule, ...MockComponents( SubHeaderComponent, SettingsProjectFormCardComponent, @@ -83,6 +82,7 @@ describe.skip('SettingsComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(Router, routerMock), MockProvider(CustomConfirmationService, customConfirmationServiceMock), diff --git a/src/app/features/project/wiki/wiki.component.spec.ts b/src/app/features/project/wiki/wiki.component.spec.ts index b9478e146..38ba1c09d 100644 --- a/src/app/features/project/wiki/wiki.component.spec.ts +++ b/src/app/features/project/wiki/wiki.component.spec.ts @@ -33,7 +33,7 @@ import { ViewOnlyLinkMessageComponent } from '@shared/components/view-only-link- import { WikiComponent } from './wiki.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -90,7 +90,6 @@ describe('WikiComponent', () => { await TestBed.configureTestingModule({ imports: [ WikiComponent, - OSFTestingModule, ...MockComponents( SubHeaderComponent, WikiListComponent, @@ -101,6 +100,7 @@ describe('WikiComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(ToastService, toastServiceMock), diff --git a/src/app/features/registries/components/confirm-continue-editing-dialog/confirm-continue-editing-dialog.component.spec.ts b/src/app/features/registries/components/confirm-continue-editing-dialog/confirm-continue-editing-dialog.component.spec.ts index 5d1af3d25..d12f0110c 100644 --- a/src/app/features/registries/components/confirm-continue-editing-dialog/confirm-continue-editing-dialog.component.spec.ts +++ b/src/app/features/registries/components/confirm-continue-editing-dialog/confirm-continue-editing-dialog.component.spec.ts @@ -11,8 +11,8 @@ import { HandleSchemaResponse } from '@osf/features/registries/store'; import { ConfirmContinueEditingDialogComponent } from './confirm-continue-editing-dialog.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('ConfirmContinueEditingDialogComponent', () => { diff --git a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.spec.ts b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.spec.ts index 781dbc459..35ed8529b 100644 --- a/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.spec.ts +++ b/src/app/features/registries/components/confirm-registration-dialog/confirm-registration-dialog.component.spec.ts @@ -13,8 +13,8 @@ import { RegisterDraft, RegistriesSelectors } from '@osf/features/registries/sto import { ConfirmRegistrationDialogComponent } from './confirm-registration-dialog.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('ConfirmRegistrationDialogComponent', () => { diff --git a/src/app/features/registries/components/drafts/drafts.component.spec.ts b/src/app/features/registries/components/drafts/drafts.component.spec.ts index 7325770a2..baf6cf64d 100644 --- a/src/app/features/registries/components/drafts/drafts.component.spec.ts +++ b/src/app/features/registries/components/drafts/drafts.component.spec.ts @@ -4,7 +4,7 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { StepperComponent } from '@osf/shared/components/stepper/stepper.component'; @@ -227,13 +227,13 @@ describe('DraftsComponent', () => { expect(c).toBeTruthy(); }); - it('should hide loader after schema blocks are fetched', fakeAsync(() => { + it('should hide loader after schema blocks are fetched', async () => { fixture.detectChanges(); - tick(); + await fixture.whenStable(); const loaderService = TestBed.inject(LoaderService); expect(loaderService.hide).toHaveBeenCalled(); - })); + }); it('should not fetch schema blocks when draft has no registrationSchemaId', () => { const { fixture: f } = setup({ diff --git a/src/app/features/registries/components/new-registration/new-registration.component.spec.ts b/src/app/features/registries/components/new-registration/new-registration.component.spec.ts index 80246925c..b78e8f010 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.spec.ts +++ b/src/app/features/registries/components/new-registration/new-registration.component.spec.ts @@ -2,7 +2,7 @@ import { Store } from '@ngxs/store'; import { MockComponent, MockProvider } from 'ng-mocks'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@core/store/user'; @@ -74,6 +74,10 @@ describe('NewRegistrationComponent', () => { fixture.detectChanges(); }; + afterEach(() => { + jest.useRealTimers(); + }); + it('should create', () => { setup(); expect(component).toBeTruthy(); @@ -169,44 +173,47 @@ describe('NewRegistrationComponent', () => { expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(CreateDraft)); }); - it('should dispatch getProjects after debounced filter', fakeAsync(() => { + it('should dispatch getProjects after debounced filter', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.onProjectFilter('abc'); - tick(300); + jest.advanceTimersByTime(300); expect(store.dispatch).toHaveBeenCalledWith(new GetProjects('user-1', 'abc')); - })); + }); - it('should not dispatch duplicate getProjects for same filter value', fakeAsync(() => { + it('should not dispatch duplicate getProjects for same filter value', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.onProjectFilter('abc'); - tick(300); + jest.advanceTimersByTime(300); component.onProjectFilter('abc'); - tick(300); + jest.advanceTimersByTime(300); const getProjectsCalls = (store.dispatch as jest.Mock).mock.calls.filter( ([action]: [unknown]) => action instanceof GetProjects ); expect(getProjectsCalls.length).toBe(1); - })); + }); - it('should debounce rapid filter calls and dispatch only the last value', fakeAsync(() => { + it('should debounce rapid filter calls and dispatch only the last value', () => { + jest.useFakeTimers(); setup(); (store.dispatch as jest.Mock).mockClear(); component.onProjectFilter('a'); component.onProjectFilter('ab'); component.onProjectFilter('abc'); - tick(300); + jest.advanceTimersByTime(300); const getProjectsCalls = (store.dispatch as jest.Mock).mock.calls.filter( ([action]: [unknown]) => action instanceof GetProjects ); expect(getProjectsCalls.length).toBe(1); expect(getProjectsCalls[0][0]).toEqual(new GetProjects('user-1', 'abc')); - })); + }); }); diff --git a/src/app/features/registries/components/registry-services/registry-services.component.html b/src/app/features/registries/components/registry-services/registry-services.component.html index 5c94dde52..5e9829e01 100644 --- a/src/app/features/registries/components/registry-services/registry-services.component.html +++ b/src/app/features/registries/components/registry-services/registry-services.component.html @@ -16,11 +16,8 @@

{{ 'registries.services.title' | translate }}

diff --git a/src/app/features/registries/components/registry-services/registry-services.component.spec.ts b/src/app/features/registries/components/registry-services/registry-services.component.spec.ts index a5878279c..4d17b3209 100644 --- a/src/app/features/registries/components/registry-services/registry-services.component.spec.ts +++ b/src/app/features/registries/components/registry-services/registry-services.component.spec.ts @@ -1,7 +1,5 @@ -import { MockProvider } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { RegistryServicesComponent } from './registry-services.component'; @@ -14,7 +12,7 @@ describe('RegistryServicesComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [RegistryServicesComponent], - providers: [provideOSFCore(), MockProvider(ActivatedRoute)], + providers: [provideOSFCore(), provideRouter([])], }); fixture = TestBed.createComponent(RegistryServicesComponent); @@ -37,16 +35,10 @@ describe('RegistryServicesComponent', () => { expect(buttons.length).toBeGreaterThan(0); }); - it('should open email via mailto when openEmail is called', () => { - const originalHref = window.location.href; - Object.defineProperty(window, 'location', { - value: { href: originalHref }, - writable: true, - configurable: true, - }); - - component.openEmail(); + it('should render contact mailto anchor', () => { + const compiled = fixture.nativeElement as HTMLElement; + const mailtoAnchor = compiled.querySelector('a[href="mailto:contact@osf.io"]'); - expect(window.location.href).toBe('mailto:contact@osf.io'); + expect(mailtoAnchor).toBeTruthy(); }); }); diff --git a/src/app/features/registries/components/registry-services/registry-services.component.ts b/src/app/features/registries/components/registry-services/registry-services.component.ts index 0c57afbd6..95d34388c 100644 --- a/src/app/features/registries/components/registry-services/registry-services.component.ts +++ b/src/app/features/registries/components/registry-services/registry-services.component.ts @@ -1,7 +1,5 @@ import { TranslatePipe } from '@ngx-translate/core'; -import { Button } from 'primeng/button'; - import { ChangeDetectionStrategy, Component } from '@angular/core'; import { RouterLink } from '@angular/router'; @@ -9,15 +7,11 @@ import { RegistryServiceIcons } from '@shared/constants/registry-services-icons. @Component({ selector: 'osf-registry-services', - imports: [TranslatePipe, RouterLink, Button], + imports: [TranslatePipe, RouterLink], templateUrl: './registry-services.component.html', styleUrl: './registry-services.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class RegistryServicesComponent { registryServices = RegistryServiceIcons; - - openEmail() { - window.location.href = 'mailto:contact@osf.io'; - } } diff --git a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts index e698bf519..8c6a26a28 100644 --- a/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts +++ b/src/app/features/registries/components/select-components-dialog/select-components-dialog.component.spec.ts @@ -9,8 +9,8 @@ import { ProjectShortInfoModel } from '../../models/project-short-info.model'; import { SelectComponentsDialogComponent } from './select-components-dialog.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; describe('SelectComponentsDialogComponent', () => { let component: SelectComponentsDialogComponent; diff --git a/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts b/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts index 8ad13dace..a682d2cb7 100644 --- a/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts +++ b/src/app/features/registry/components/add-resource-dialog/add-resource-dialog.component.spec.ts @@ -16,8 +16,8 @@ import { ResourceFormComponent } from '../resource-form/resource-form.component' import { AddResourceDialogComponent } from './add-resource-dialog.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock'; const MOCK_RESOURCE: RegistryResource = { diff --git a/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts b/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts index e334b961c..7053c50eb 100644 --- a/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts +++ b/src/app/features/registry/components/edit-resource-dialog/edit-resource-dialog.component.spec.ts @@ -17,8 +17,8 @@ import { ResourceFormComponent } from '../resource-form/resource-form.component' import { EditResourceDialogComponent } from './edit-resource-dialog.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; const MOCK_RESOURCE: RegistryResource = { diff --git a/src/app/features/registry/components/registration-withdraw-dialog/registration-withdraw-dialog.component.spec.ts b/src/app/features/registry/components/registration-withdraw-dialog/registration-withdraw-dialog.component.spec.ts index c9f30842c..448255367 100644 --- a/src/app/features/registry/components/registration-withdraw-dialog/registration-withdraw-dialog.component.spec.ts +++ b/src/app/features/registry/components/registration-withdraw-dialog/registration-withdraw-dialog.component.spec.ts @@ -12,8 +12,8 @@ import { TextInputComponent } from '@osf/shared/components/text-input/text-input import { RegistrationWithdrawDialogComponent } from './registration-withdraw-dialog.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; function setup(registryId = 'reg-123') { diff --git a/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts b/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts index 5a6f91404..dd8bd1aba 100644 --- a/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts +++ b/src/app/features/registry/components/registry-make-decision/registry-make-decision.component.spec.ts @@ -18,9 +18,9 @@ import { RegistrySelectors } from '../../store/registry'; import { RegistryMakeDecisionComponent } from './registry-make-decision.component'; -import { provideDynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; const MOCK_REGISTRY_ACCEPTED = { diff --git a/src/app/features/registry/registry.component.spec.ts b/src/app/features/registry/registry.component.spec.ts index 2c8ab38a9..2f0be494a 100644 --- a/src/app/features/registry/registry.component.spec.ts +++ b/src/app/features/registry/registry.component.spec.ts @@ -22,10 +22,10 @@ import { RegistrySelectors } from './store/registry'; import { RegistrationOverviewModel } from './models'; import { RegistryComponent } from './registry.component'; -import { DataciteMockFactory } from '@testing/mocks/datacite.service.mock'; import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { AnalyticsServiceMockFactory } from '@testing/providers/analytics.service.mock'; +import { DataciteServiceMock } from '@testing/providers/datacite.service.mock'; import { HelpScoutServiceMockFactory } from '@testing/providers/help-scout.service.mock'; import { MetaTagsServiceMockFactory } from '@testing/providers/meta-tags.service.mock'; import { MetaTagsBuilderServiceMockFactory } from '@testing/providers/meta-tags-builder.service.mock'; @@ -51,7 +51,7 @@ function setup(overrides: SetupOverrides = {}) { const helpScoutService = HelpScoutServiceMockFactory(); const metaTagsService = MetaTagsServiceMockFactory(); const metaTagsBuilderService = MetaTagsBuilderServiceMockFactory(); - const dataciteService = DataciteMockFactory(); + const dataciteService = DataciteServiceMock.simple(); const prerenderReadyService = PrerenderReadyServiceMockFactory(); const analyticsService = AnalyticsServiceMockFactory(); const routerBuilder = RouterMockBuilder.create(); diff --git a/src/app/features/search/search.component.spec.ts b/src/app/features/search/search.component.spec.ts index 1c6eec014..a550442dd 100644 --- a/src/app/features/search/search.component.spec.ts +++ b/src/app/features/search/search.component.spec.ts @@ -7,6 +7,8 @@ import { SEARCH_TAB_OPTIONS } from '@osf/shared/constants/search-tab-options.con import { SearchComponent } from './search.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('SearchComponent', () => { let component: SearchComponent; let fixture: ComponentFixture; @@ -14,6 +16,7 @@ describe('SearchComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SearchComponent, MockComponent(GlobalSearchComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SearchComponent); diff --git a/src/app/features/settings/account-settings/account-settings.component.spec.ts b/src/app/features/settings/account-settings/account-settings.component.spec.ts index f2e84d1b8..f070c0704 100644 --- a/src/app/features/settings/account-settings/account-settings.component.spec.ts +++ b/src/app/features/settings/account-settings/account-settings.component.spec.ts @@ -1,17 +1,14 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; +import { GetEmails } from '@core/store/user-emails'; import { UserSelectors } from '@osf/core/store/user'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { ToastService } from '@osf/shared/services/toast.service'; -import { RegionsSelectors } from '@osf/shared/stores/regions'; +import { FetchRegions } from '@osf/shared/stores/regions'; import { AccountSettingsComponent } from './account-settings.component'; import { @@ -24,41 +21,26 @@ import { ShareIndexingComponent, TwoFactorAuthComponent, } from './components'; -import { AccountSettingsSelectors } from './store'; +import { GetAccountSettings, GetExternalIdentities, GetUserInstitutions } from './store'; -import { MockCustomConfirmationServiceProvider } from '@testing/mocks/custom-confirmation.service.mock'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; describe('AccountSettingsComponent', () => { let component: AccountSettingsComponent; let fixture: ComponentFixture; - const store = MOCK_STORE; - - beforeEach(async () => { - store.selectSignal.mockImplementation((selector) => { - switch (selector) { - case UserSelectors.getCurrentUser: - return () => MOCK_USER; - - case AccountSettingsSelectors.getAccountSettings: - return () => null; + let store: Store; - case AccountSettingsSelectors.getExternalIdentities: - return () => null; + const defaultSignals: SignalOverride[] = [{ selector: UserSelectors.getCurrentUser, value: MOCK_USER }]; - case RegionsSelectors.getRegions: - return () => null; - - case AccountSettingsSelectors.getUserInstitutions: - return () => null; - - default: - return () => []; - } - }); - await TestBed.configureTestingModule({ + function setup(overrides: BaseSetupOverrides = {}) { + TestBed.configureTestingModule({ imports: [ AccountSettingsComponent, ...MockComponents( @@ -72,35 +54,61 @@ describe('AccountSettingsComponent', () => { DeactivateAccountComponent, AffiliatedInstitutionsComponent ), - MockPipe(TranslatePipe), ], providers: [ - MockCustomConfirmationServiceProvider, - TranslateServiceMock, - MockProvider(ToastService), - provideNoopAnimations(), - provideHttpClient(), - provideHttpClientTesting(), - MockProvider(Store, store), + provideOSFCore(), + provideMockStore({ + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(AccountSettingsComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should not dispatch actions when currentUser is null', () => { - store.selectSignal.mockImplementation((selector) => - selector === UserSelectors.getCurrentUser ? () => null : () => [] - ); + it('should dispatch initial account settings actions when user exists', () => { + setup(); - store.dispatch.mockClear(); + expect(store.dispatch).toHaveBeenCalledTimes(5); + expect(store.dispatch).toHaveBeenCalledWith(new GetAccountSettings()); + expect(store.dispatch).toHaveBeenCalledWith(new GetEmails()); + expect(store.dispatch).toHaveBeenCalledWith(new GetExternalIdentities()); + expect(store.dispatch).toHaveBeenCalledWith(new FetchRegions()); + expect(store.dispatch).toHaveBeenCalledWith(new GetUserInstitutions()); + }); + + it('should not dispatch initial actions when current user is null', () => { + setup({ + selectorOverrides: [{ selector: UserSelectors.getCurrentUser, value: null }], + }); expect(store.dispatch).not.toHaveBeenCalled(); }); + + it('should render settings section when current user has id', () => { + setup(); + + const section = fixture.debugElement.query(By.css('section')); + + expect(section).toBeTruthy(); + }); + + it('should hide settings section when current user is null', () => { + setup({ + selectorOverrides: [{ selector: UserSelectors.getCurrentUser, value: null }], + }); + + const section = fixture.debugElement.query(By.css('section')); + + expect(section).toBeNull(); + }); }); diff --git a/src/app/features/settings/account-settings/components/add-email/add-email.component.spec.ts b/src/app/features/settings/account-settings/components/add-email/add-email.component.spec.ts index d11698206..a4a19cbd7 100644 --- a/src/app/features/settings/account-settings/components/add-email/add-email.component.spec.ts +++ b/src/app/features/settings/account-settings/components/add-email/add-email.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore, Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProviders } from 'ng-mocks'; +import { MockComponent, MockProviders } from 'ng-mocks'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -17,7 +16,7 @@ import { AccountSettingsState } from '../../store'; import { AddEmailComponent } from './add-email.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AddEmailComponent', () => { let component: AddEmailComponent; @@ -26,11 +25,11 @@ describe('AddEmailComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AddEmailComponent, MockComponent(TextInputComponent), MockPipe(TranslatePipe)], + imports: [AddEmailComponent, MockComponent(TextInputComponent)], providers: [ + provideOSFCore(), provideStore([AccountSettingsState, UserEmailsState]), MockProviders(DynamicDialogRef, ToastService), - TranslateServiceMock, provideHttpClient(), provideHttpClientTesting(), ], diff --git a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts index 64cabfb37..3e0a93835 100644 --- a/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts +++ b/src/app/features/settings/account-settings/components/affiliated-institutions/affiliated-institutions.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore, Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProviders } from 'ng-mocks'; +import { MockComponent, MockProviders } from 'ng-mocks'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -19,6 +18,7 @@ import { AccountSettingsState } from '../../store'; import { AffiliatedInstitutionsComponent } from './affiliated-institutions.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AffiliatedInstitutionsComponent', () => { let component: AffiliatedInstitutionsComponent; @@ -28,8 +28,9 @@ describe('AffiliatedInstitutionsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AffiliatedInstitutionsComponent, MockComponent(ReadonlyInputComponent), MockPipe(TranslatePipe)], + imports: [AffiliatedInstitutionsComponent, MockComponent(ReadonlyInputComponent)], providers: [ + provideOSFCore(), provideStore([AccountSettingsState, UserState]), MockProviders(CustomConfirmationService, ToastService, DynamicDialogRef), provideHttpClient(), diff --git a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.spec.ts b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.spec.ts index a436a5bd4..0caa5a62f 100644 --- a/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.spec.ts +++ b/src/app/features/settings/account-settings/components/cancel-deactivation/cancel-deactivation.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -13,14 +12,17 @@ import { AccountSettingsState } from '../../store'; import { CancelDeactivationComponent } from './cancel-deactivation.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('CancelDeactivationComponent', () => { let component: CancelDeactivationComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CancelDeactivationComponent, MockPipe(TranslatePipe)], + imports: [CancelDeactivationComponent], providers: [ + provideOSFCore(), provideStore([AccountSettingsState]), provideHttpClient(), provideHttpClientTesting(), diff --git a/src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts b/src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts index cdfbe1f58..661e75b8e 100644 --- a/src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts +++ b/src/app/features/settings/account-settings/components/change-password/change-password.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore, Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { of, throwError } from 'rxjs'; @@ -17,7 +16,7 @@ import { AccountSettingsState } from '../../store'; import { ChangePasswordComponent } from './change-password.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ChangePasswordComponent', () => { let component: ChangePasswordComponent; @@ -26,12 +25,12 @@ describe('ChangePasswordComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ChangePasswordComponent, MockComponent(PasswordInputHintComponent), MockPipe(TranslatePipe)], + imports: [ChangePasswordComponent, MockComponent(PasswordInputHintComponent)], providers: [ + provideOSFCore(), provideStore([AccountSettingsState]), provideHttpClient(), provideHttpClientTesting(), - TranslateServiceMock, MockProvider(LoaderService), MockProvider(ToastService), ], diff --git a/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts index 84f6bf70d..261022032 100644 --- a/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts +++ b/src/app/features/settings/account-settings/components/confirmation-sent-dialog/confirmation-sent-dialog.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProviders } from 'ng-mocks'; +import { MockProviders } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -9,14 +8,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ConfirmationSentDialogComponent } from './confirmation-sent-dialog.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ConfirmationSentDialogComponent', () => { let component: ConfirmationSentDialogComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConfirmationSentDialogComponent, MockPipe(TranslatePipe)], + imports: [ConfirmationSentDialogComponent], providers: [ + provideOSFCore(), MockProviders(DynamicDialogRef, DynamicDialogConfig), provideHttpClient(), provideHttpClientTesting(), diff --git a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts index 648e2d22a..ad4dc2914 100644 --- a/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts +++ b/src/app/features/settings/account-settings/components/connected-emails/connected-emails.component.spec.ts @@ -1,7 +1,7 @@ import { provideStore, Store } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProviders } from 'ng-mocks'; +import { TranslateService } from '@ngx-translate/core'; +import { MockComponent, MockProvider, MockProviders } from 'ng-mocks'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -24,8 +24,8 @@ import { ConfirmationSentDialogComponent } from '../confirmation-sent-dialog/con import { ConnectedEmailsComponent } from './connected-emails.component'; -import { MockCustomConfirmationServiceProvider } from '@testing/mocks/custom-confirmation.service.mock'; import { MOCK_USER } from '@testing/mocks/data.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ConnectedEmailsComponent', () => { let component: ConnectedEmailsComponent; @@ -46,13 +46,14 @@ describe('ConnectedEmailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConnectedEmailsComponent, MockComponent(ReadonlyInputComponent), MockPipe(TranslatePipe)], + imports: [ConnectedEmailsComponent, MockComponent(ReadonlyInputComponent)], providers: [ + provideOSFCore(), provideStore([UserState, UserEmailsState]), provideHttpClient(), provideHttpClientTesting(), MockProviders(DialogService, TranslateService, DestroyRef, LoaderService, ToastService), - MockCustomConfirmationServiceProvider, + MockProvider(CustomConfirmationService), ], }).compileComponents(); diff --git a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts index ce53a417a..d926537be 100644 --- a/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts +++ b/src/app/features/settings/account-settings/components/connected-identities/connected-identities.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore, Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProviders } from 'ng-mocks'; +import { MockComponent, MockProviders } from 'ng-mocks'; import { of } from 'rxjs'; @@ -19,6 +18,8 @@ import { AccountSettingsState } from '../../store/account-settings.state'; import { ConnectedIdentitiesComponent } from './connected-identities.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + const mockIdentity: ExternalIdentity = { id: 'id1', externalId: 'externalId1', @@ -33,8 +34,9 @@ describe('ConnectedIdentitiesComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ConnectedIdentitiesComponent, MockComponent(ReadonlyInputComponent), MockPipe(TranslatePipe)], + imports: [ConnectedIdentitiesComponent, MockComponent(ReadonlyInputComponent)], providers: [ + provideOSFCore(), provideStore([AccountSettingsState]), provideHttpClient(), provideHttpClientTesting(), diff --git a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.spec.ts b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.spec.ts index b2e591f8b..23ab1f967 100644 --- a/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.spec.ts +++ b/src/app/features/settings/account-settings/components/deactivate-account/deactivate-account.component.spec.ts @@ -1,121 +1,165 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockPipe, MockProvider, MockProviders } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; -import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; -import { of, Subject } from 'rxjs'; +import { Subject } from 'rxjs'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { AccountSettingsSelectors } from '../../store'; +import { AccountSettingsSelectors, CancelDeactivationRequest, DeactivateAccount } from '../../store'; import { CancelDeactivationComponent } from '../cancel-deactivation/cancel-deactivation.component'; import { DeactivationWarningComponent } from '../deactivation-warning/deactivation-warning.component'; import { DeactivateAccountComponent } from './deactivate-account.component'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { CustomDialogServiceMock, CustomDialogServiceMockType } from '@testing/providers/custom-dialog-provider.mock'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('DeactivateAccountComponent', () => { let component: DeactivateAccountComponent; let fixture: ComponentFixture; - let dialogService: DialogService; - let translateService: TranslateService; - - const MOCK_ACCOUNT_SETTINGS = { - twoFactorEnabled: false, - twoFactorConfirmed: false, - subscribeOsfGeneralEmail: false, - subscribeOsfHelpEmail: false, - deactivationRequested: false, - contactedDeactivation: false, - secret: '', - }; - - beforeEach(async () => { - const store = MOCK_STORE; - - store.selectSignal.mockImplementation((selector) => { - if (selector === AccountSettingsSelectors.getAccountSettings) { - return () => MOCK_ACCOUNT_SETTINGS; - } - - return () => null; - }); - - store.dispatch.mockImplementation(() => { - return of(); - }); - - await TestBed.configureTestingModule({ - imports: [DeactivateAccountComponent, MockPipe(TranslatePipe)], + let store: Store; + let customDialogService: CustomDialogServiceMockType; + let loaderService: LoaderServiceMock; + let toastService: ToastServiceMockType; + let dialogRef: DynamicDialogRef; + + const defaultSignals: SignalOverride[] = [ + { + selector: AccountSettingsSelectors.getAccountSettings, + value: { + twoFactorEnabled: false, + twoFactorConfirmed: false, + subscribeOsfGeneralEmail: true, + subscribeOsfHelpEmail: true, + deactivationRequested: false, + contactedDeactivation: false, + secret: '', + }, + }, + ]; + + function setup(overrides: BaseSetupOverrides = {}) { + customDialogService = CustomDialogServiceMock.simple(); + loaderService = new LoaderServiceMock(); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [DeactivateAccountComponent], providers: [ - provideNoopAnimations(), - MockProvider(Store, store), - provideHttpClient(), - provideHttpClientTesting(), - MockProviders(DynamicDialogRef, DialogService, TranslateService, ToastService), + provideOSFCore(), + MockProvider(CustomDialogService, customDialogService), + MockProvider(LoaderService, loaderService), + MockProvider(ToastService, toastService), + provideDynamicDialogRefMock(), + provideMockStore({ + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); + customDialogService.open.mockReturnValue(dialogRef); fixture = TestBed.createComponent(DeactivateAccountComponent); component = fixture.componentInstance; - - dialogService = TestBed.inject(DialogService); - translateService = TestBed.inject(TranslateService); - fixture.detectChanges(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should open DeactivationWarning dialog and on confirm show toast', () => { - jest.spyOn(translateService, 'instant').mockReturnValue('Deactivate header'); + it('should open deactivation warning dialog', () => { + setup(); - const onCloseSubject = new Subject(); - const dialogRefMock: Partial = { onClose: onCloseSubject }; - const openSpy = jest.spyOn(dialogService, 'open').mockReturnValue(dialogRefMock as DynamicDialogRef); + component.deactivateAccount(); + + expect(customDialogService.open).toHaveBeenCalledWith(DeactivationWarningComponent, { + header: 'settings.accountSettings.deactivateAccount.dialog.deactivate.title', + width: '552px', + }); + }); + + it('should not dispatch deactivate action when dialog closes with false', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); component.deactivateAccount(); + (dialogRef.onClose as Subject).next(false); - expect(openSpy).toHaveBeenCalledWith( - DeactivationWarningComponent, - expect.objectContaining({ - width: '552px', - header: 'Deactivate header', - modal: true, - }) + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(DeactivateAccount)); + expect(loaderService.show).not.toHaveBeenCalled(); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + }); + + it('should dispatch deactivate action and show success when dialog closes with true', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + + component.deactivateAccount(); + (dialogRef.onClose as Subject).next(true); + + expect(loaderService.show).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new DeactivateAccount()); + expect(toastService.showSuccess).toHaveBeenCalledWith( + 'settings.accountSettings.deactivateAccount.successDeactivation' ); + expect(loaderService.hide).toHaveBeenCalled(); + }); + + it('should open cancel deactivation dialog', () => { + setup(); + + component.cancelDeactivation(); - onCloseSubject.next(true); + expect(customDialogService.open).toHaveBeenCalledWith(CancelDeactivationComponent, { + header: 'settings.accountSettings.deactivateAccount.dialog.undo.title', + width: '552px', + }); }); - it('should open CancelDeactivation dialog and on confirm dispatch action', () => { - jest.spyOn(translateService, 'instant').mockReturnValue('Cancel header'); + it('should not dispatch cancel action when dialog closes with false', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); - const onCloseSubject = new Subject(); - const dialogRefMock: Partial = { onClose: onCloseSubject }; - const openSpy = jest.spyOn(dialogService, 'open').mockReturnValue(dialogRefMock as DynamicDialogRef); + component.cancelDeactivation(); + (dialogRef.onClose as Subject).next(false); + + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(CancelDeactivationRequest)); + expect(loaderService.show).not.toHaveBeenCalled(); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + }); + + it('should dispatch cancel action and show success when dialog closes with true', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); component.cancelDeactivation(); + (dialogRef.onClose as Subject).next(true); - expect(openSpy).toHaveBeenCalledWith( - CancelDeactivationComponent, - expect.objectContaining({ - width: '552px', - header: 'Cancel header', - modal: true, - }) + expect(loaderService.show).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new CancelDeactivationRequest()); + expect(toastService.showSuccess).toHaveBeenCalledWith( + 'settings.accountSettings.deactivateAccount.successCancelDeactivation' ); - - onCloseSubject.next(true); + expect(loaderService.hide).toHaveBeenCalled(); }); }); diff --git a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.spec.ts b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.spec.ts index 69478f15e..21587323f 100644 --- a/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.spec.ts +++ b/src/app/features/settings/account-settings/components/deactivation-warning/deactivation-warning.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -7,6 +6,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeactivationWarningComponent } from './deactivation-warning.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('DeactivationWarningComponent', () => { let component: DeactivationWarningComponent; let fixture: ComponentFixture; @@ -14,8 +15,8 @@ describe('DeactivationWarningComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DeactivationWarningComponent, MockPipe(TranslatePipe)], - providers: [MockProvider(DynamicDialogRef)], + imports: [DeactivationWarningComponent], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef)], }).compileComponents(); fixture = TestBed.createComponent(DeactivationWarningComponent); diff --git a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts index f5c6abcf1..ea9fc33d3 100644 --- a/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts +++ b/src/app/features/settings/account-settings/components/default-storage-location/default-storage-location.component.spec.ts @@ -1,88 +1,126 @@ -import { provideStore } from '@ngxs/store'; +import { Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserSelectors, UserState } from '@osf/core/store/user'; +import { UserSelectors } from '@osf/core/store/user'; +import { IdNameModel } from '@osf/shared/models/common/id-name.model'; import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { RegionsSelectors, RegionsState } from '@osf/shared/stores/regions'; +import { RegionsSelectors } from '@osf/shared/stores/regions'; -import { AccountSettingsState } from '../../store'; +import { UpdateRegion } from '../../store'; import { DefaultStorageLocationComponent } from './default-storage-location.component'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; +import { MOCK_USER } from '@testing/mocks/data.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('DefaultStorageLocationComponent', () => { let component: DefaultStorageLocationComponent; let fixture: ComponentFixture; - let loaderService: LoaderService; + let store: Store; + let loaderService: LoaderServiceMock; + let toastService: ToastServiceMockType; - const mockUser = { id: 'id1', defaultRegionId: 'region1' }; - const mockRegions = [ - { id: 'region1', name: 'Test Region' }, - { id: 'region2', name: 'Another Region' }, + const regions: IdNameModel[] = [ + { id: 'us', name: 'United States' }, + { id: 'ca', name: 'Canada' }, ]; - beforeEach(async () => { - const mockLoaderService = { - show: jest.fn(), - hide: jest.fn(), - }; - - MOCK_STORE.selectSignal.mockImplementation((selector) => { - if (selector === UserSelectors.getCurrentUser) { - return () => signal(mockUser); - } - if (selector === RegionsSelectors.getRegions) { - return () => signal(mockRegions); - } - return () => signal(null); - }); + const defaultSignals: SignalOverride[] = [ + { selector: UserSelectors.getCurrentUser, value: MOCK_USER }, + { selector: RegionsSelectors.getRegions, value: regions }, + ]; - await TestBed.configureTestingModule({ - imports: [DefaultStorageLocationComponent, MockPipe(TranslatePipe)], + function setup(overrides: BaseSetupOverrides = {}) { + loaderService = new LoaderServiceMock(); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [DefaultStorageLocationComponent], providers: [ - MockProvider(ToastService), - MockProvider(LoaderService, mockLoaderService), - provideStore([AccountSettingsState, UserState, RegionsState]), - provideHttpClient(), - provideHttpClientTesting(), + provideOSFCore(), + MockProvider(LoaderService, loaderService), + MockProvider(ToastService, toastService), + provideMockStore({ + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(DefaultStorageLocationComponent); component = fixture.componentInstance; - loaderService = TestBed.inject(LoaderService); - fixture.detectChanges(); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should not execute update when selectedRegion has no id', () => { - MOCK_STORE.selectSignal.mockImplementation((selector) => { - if (selector === UserSelectors.getCurrentUser) { - return () => signal({ id: 'id1', defaultRegionId: 'non-existent' }); - } - return () => signal(undefined); + it('should set selected region from current user default region', () => { + setup(); + + expect(component.selectedRegion()).toEqual({ id: 'us', name: 'United States' }); + }); + + it('should keep selected region undefined when user is null', () => { + setup({ + selectorOverrides: [{ selector: UserSelectors.getCurrentUser, value: null }], }); + expect(component.selectedRegion()).toBeUndefined(); + }); + + it('should not update location when selected region has no id', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.selectedRegion.set(undefined); + component.updateLocation(); expect(loaderService.show).not.toHaveBeenCalled(); - expect(MOCK_STORE.dispatch).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(UpdateRegion)); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + }); + + it('should update region and show success toast when selected region exists', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.selectedRegion.set({ id: 'ca', name: 'Canada' }); + + component.updateLocation(); + + expect(loaderService.show).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new UpdateRegion('ca')); + expect(toastService.showSuccess).toHaveBeenCalledWith( + 'settings.accountSettings.defaultStorageLocation.successUpdate' + ); + expect(loaderService.hide).toHaveBeenCalled(); + }); + + it('should keep selected region undefined when default region is not found', () => { + setup({ + selectorOverrides: [ + { + selector: UserSelectors.getCurrentUser, + value: { ...MOCK_USER, defaultRegionId: 'unknown' }, + }, + ], + }); + + expect(component.selectedRegion()).toBeUndefined(); }); }); diff --git a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts index a8c409736..cd9344fe4 100644 --- a/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts +++ b/src/app/features/settings/account-settings/components/share-indexing/share-indexing.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; @@ -12,14 +11,17 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { ShareIndexingComponent } from './share-indexing.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ShareIndexingComponent', () => { let component: ShareIndexingComponent; let fixture: ComponentFixture; beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ShareIndexingComponent, MockPipe(TranslatePipe)], + imports: [ShareIndexingComponent], providers: [ + provideOSFCore(), provideStore([UserState]), MockProvider(ToastService), provideHttpClient(), diff --git a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts index 8cfb0500f..2b0da2865 100644 --- a/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts +++ b/src/app/features/settings/account-settings/components/two-factor-auth/two-factor-auth.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore, Store } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProviders } from 'ng-mocks'; +import { MockComponent, MockProvider, MockProviders } from 'ng-mocks'; import { MessageService } from 'primeng/api'; import { DialogService } from 'primeng/dynamicdialog'; @@ -10,7 +9,7 @@ import { of } from 'rxjs'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UserState } from '@osf/core/store/user'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; @@ -19,7 +18,7 @@ import { AccountSettingsState } from '../../store'; import { TwoFactorAuthComponent } from './two-factor-auth.component'; -import { MockCustomConfirmationServiceProvider } from '@testing/mocks/custom-confirmation.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { QRCodeComponent } from 'angularx-qrcode'; describe('TwoFactorAuthComponent', () => { @@ -30,13 +29,14 @@ describe('TwoFactorAuthComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TwoFactorAuthComponent, MockComponent(QRCodeComponent), MockPipe(TranslatePipe)], + imports: [TwoFactorAuthComponent, MockComponent(QRCodeComponent)], providers: [ + provideOSFCore(), provideStore([UserState, AccountSettingsState]), provideHttpClient(), provideHttpClientTesting(), - MockProviders(TranslateService, DialogService, MessageService), - MockCustomConfirmationServiceProvider, + MockProviders(DialogService, MessageService), + MockProvider(CustomConfirmationService), ], }).compileComponents(); @@ -84,11 +84,11 @@ describe('TwoFactorAuthComponent', () => { expect(store.dispatch).not.toHaveBeenCalled(); }); - it('should call disableTwoFactorAuth when disableTwoFactor is called', fakeAsync(() => { - jest.spyOn(store, 'dispatch').mockReturnValue(of()); + it('should call disableTwoFactorAuth when disableTwoFactor is called', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch').mockReturnValue(of()); component.disableTwoFactor(); - expect(store.dispatch).toHaveBeenCalled(); - })); + expect(dispatchSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.spec.ts b/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.spec.ts index f07616d89..d98a0fc27 100644 --- a/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.spec.ts +++ b/src/app/features/settings/developer-apps/components/developer-app-add-edit-form/developer-app-add-edit-form.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProviders } from 'ng-mocks'; +import { MockComponent, MockProviders } from 'ng-mocks'; import { DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -16,7 +15,7 @@ import { DeveloperAppsState } from '../../store'; import { DeveloperAppAddEditFormComponent } from './developer-app-add-edit-form.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('CreateDeveloperAppComponent', () => { let component: DeveloperAppAddEditFormComponent; @@ -24,12 +23,12 @@ describe('CreateDeveloperAppComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DeveloperAppAddEditFormComponent, MockComponent(TextInputComponent), MockPipe(TranslatePipe)], + imports: [DeveloperAppAddEditFormComponent, MockComponent(TextInputComponent)], providers: [ + provideOSFCore(), provideHttpClient(), provideHttpClientTesting(), provideStore([DeveloperAppsState]), - TranslateServiceMock, MockProviders(DynamicDialogRef, ToastService), ], }).compileComponents(); diff --git a/src/app/features/settings/developer-apps/developer-apps-container.component.spec.ts b/src/app/features/settings/developer-apps/developer-apps-container.component.spec.ts index 5223da761..9ce430ea0 100644 --- a/src/app/features/settings/developer-apps/developer-apps-container.component.spec.ts +++ b/src/app/features/settings/developer-apps/developer-apps-container.component.spec.ts @@ -1,5 +1,5 @@ -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { TranslateService } from '@ngx-translate/core'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -13,7 +13,7 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { DeveloperAppsContainerComponent } from './developer-apps-container.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('DeveloperAppsContainerComponent', () => { let component: DeveloperAppsContainerComponent; @@ -28,8 +28,8 @@ describe('DeveloperAppsContainerComponent', () => { dialogRefMock = { onClose: new Subject() }; await TestBed.configureTestingModule({ - imports: [DeveloperAppsContainerComponent, MockComponent(SubHeaderComponent), MockPipe(TranslatePipe)], - providers: [MockProvider(DialogService), MockProvider(ToastService), TranslateServiceMock], + imports: [DeveloperAppsContainerComponent, MockComponent(SubHeaderComponent)], + providers: [MockProvider(DialogService), MockProvider(ToastService), provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(DeveloperAppsContainerComponent); diff --git a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts index baa7c445f..343603030 100644 --- a/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts +++ b/src/app/features/settings/developer-apps/pages/developer-app-details/developer-app-details.component.spec.ts @@ -1,9 +1,6 @@ import { provideStore } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; - -import { ConfirmationService } from 'primeng/api'; +import { MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; @@ -19,6 +16,8 @@ import { DeveloperAppsState } from '../../store'; import { DeveloperAppDetailsComponent } from './developer-app-details.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('DeveloperAppDetailsComponent', () => { let component: DeveloperAppDetailsComponent; let fixture: ComponentFixture; @@ -32,13 +31,12 @@ describe('DeveloperAppDetailsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DeveloperAppDetailsComponent, MockPipe(TranslatePipe)], + imports: [DeveloperAppDetailsComponent], providers: [ - ConfirmationService, + provideOSFCore(), provideHttpClient(), provideHttpClientTesting(), provideStore([DeveloperAppsState]), - MockProvider(TranslateService), MockProvider(ActivatedRoute, { params: of({ id: 'test-client-id' }) }), MockProvider(Router, mockRouter), MockProvider(CustomConfirmationService), diff --git a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts index 0d67ee7da..5f634c21d 100644 --- a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts +++ b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts @@ -1,7 +1,6 @@ import { provideStore } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockPipe, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; import { ConfirmationService } from 'primeng/api'; @@ -17,6 +16,7 @@ import { DeveloperAppsState } from '../../store'; import { DeveloperAppsListComponent } from './developer-apps-list.component'; import { MOCK_DEVELOPER_APP } from '@testing/mocks/developer-app.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('DeveloperApplicationsListComponent', () => { let component: DeveloperAppsListComponent; @@ -25,13 +25,13 @@ describe('DeveloperApplicationsListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DeveloperAppsListComponent, MockPipe(TranslatePipe)], + imports: [DeveloperAppsListComponent], providers: [ + provideOSFCore(), provideStore([DeveloperAppsState]), provideHttpClient(), provideHttpClientTesting(), MockProvider(ConfirmationService), - MockProvider(TranslateService), MockProvider(CustomConfirmationService), MockProvider(ToastService), ], diff --git a/src/app/features/settings/notifications/notifications.component.spec.ts b/src/app/features/settings/notifications/notifications.component.spec.ts index 20bbb53af..b11c6803a 100644 --- a/src/app/features/settings/notifications/notifications.component.spec.ts +++ b/src/app/features/settings/notifications/notifications.component.spec.ts @@ -1,146 +1,218 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormBuilder } from '@angular/forms'; -import { UserSelectors } from '@osf/core/store/user'; +import { UserSelectors } from '@core/store/user'; import { InfoIconComponent } from '@osf/shared/components/info-icon/info-icon.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { SubscriptionEvent } from '@osf/shared/enums/subscriptions/subscription-event.enum'; import { SubscriptionFrequency } from '@osf/shared/enums/subscriptions/subscription-frequency.enum'; +import { NotificationSubscription } from '@osf/shared/models/notifications/notification-subscription.model'; import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { AccountSettings } from '../account-settings/models'; -import { AccountSettingsSelectors } from '../account-settings/store'; +import { AccountSettingsSelectors, GetAccountSettings, UpdateAccountSettings } from '../account-settings/store'; +import { EmailPreferencesFormControls } from './models'; import { NotificationsComponent } from './notifications.component'; -import { NotificationSubscriptionSelectors } from './store'; +import { + GetAllGlobalNotificationSubscriptions, + NotificationSubscriptionSelectors, + UpdateNotificationSubscription, +} from './store'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; -import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +interface SetupOverrides { + selectorOverrides?: SignalOverride[]; + detectChanges?: boolean; +} describe('NotificationsComponent', () => { let component: NotificationsComponent; let fixture: ComponentFixture; - let loaderService: LoaderService; - let toastServiceMock: ReturnType; + let store: Store; + let loaderService: LoaderServiceMock; + let toastService: ToastServiceMockType; - const mockUserSettings: Partial = { + const mockEmailPreferences: AccountSettings = { + twoFactorEnabled: false, + twoFactorConfirmed: false, subscribeOsfGeneralEmail: true, subscribeOsfHelpEmail: false, + deactivationRequested: false, + contactedDeactivation: false, + secret: '', }; - const mockNotificationSubscriptions = [ - { id: 'id1', event: SubscriptionEvent.GlobalFileUpdated, frequency: SubscriptionFrequency.Daily }, + const mockNotificationSubscriptions: NotificationSubscription[] = [ { - id: 'id2', + id: '1_global_file_updated', event: SubscriptionEvent.GlobalFileUpdated, + frequency: SubscriptionFrequency.Daily, + }, + { + id: '1_global_reviews', + event: SubscriptionEvent.GlobalReviews, frequency: SubscriptionFrequency.Instant, }, ]; - beforeEach(async () => { - toastServiceMock = ToastServiceMockBuilder.create().build(); - - const mockLoaderService = { - show: jest.fn(), - hide: jest.fn(), - }; - - MOCK_STORE.selectSignal.mockImplementation((selector) => { - if (selector === UserSelectors.getCurrentUser) { - return signal(MOCK_USER); - } - if (selector === AccountSettingsSelectors.getAccountSettings) { - return signal(mockUserSettings); - } - if (selector === NotificationSubscriptionSelectors.getAllGlobalNotificationSubscriptions) { - return signal(mockNotificationSubscriptions); - } - if (selector === AccountSettingsSelectors.areAccountSettingsLoading) { - return signal(false); - } - if (selector === NotificationSubscriptionSelectors.isLoading) { - return signal(false); - } - return signal(null); - }); + const defaultSignals: SignalOverride[] = [ + { selector: UserSelectors.getCurrentUser, value: MOCK_USER }, + { selector: AccountSettingsSelectors.getAccountSettings, value: mockEmailPreferences }, + { + selector: NotificationSubscriptionSelectors.getAllGlobalNotificationSubscriptions, + value: mockNotificationSubscriptions, + }, + { selector: AccountSettingsSelectors.areAccountSettingsLoading, value: false }, + { selector: NotificationSubscriptionSelectors.isLoading, value: false }, + ]; - MOCK_STORE.dispatch.mockImplementation(() => of()); + function setup(overrides: SetupOverrides = {}) { + loaderService = new LoaderServiceMock(); + toastService = ToastServiceMock.simple(); - await TestBed.configureTestingModule({ - imports: [ - NotificationsComponent, - ...MockComponents(InfoIconComponent, SubHeaderComponent), - MockPipe(TranslatePipe), - ], + TestBed.configureTestingModule({ + imports: [NotificationsComponent, ...MockComponents(InfoIconComponent, SubHeaderComponent)], providers: [ - provideHttpClient(), - provideHttpClientTesting(), - TranslateServiceMock, - MockProvider(Store, MOCK_STORE), - MockProvider(LoaderService, mockLoaderService), - MockProvider(ToastService, toastServiceMock), - FormBuilder, + provideOSFCore(), + MockProvider(LoaderService, loaderService), + MockProvider(ToastService, toastService), + provideMockStore({ + signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides), + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(NotificationsComponent); component = fixture.componentInstance; - loaderService = TestBed.inject(LoaderService); - - fixture.detectChanges(); - }); + if (overrides.detectChanges !== false) { + fixture.detectChanges(); + } + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should not call loader hide when no user exists', () => { - MOCK_STORE.selectSignal.mockImplementation((selector) => { - if (selector === UserSelectors.getCurrentUser) { - return signal(null); - } + it('should patch email preferences form from selector data', () => { + setup(); + + expect(component.emailPreferencesForm.get(EmailPreferencesFormControls.SubscribeOsfGeneralEmail)?.value).toBe(true); + expect(component.emailPreferencesForm.get(EmailPreferencesFormControls.SubscribeOsfHelpEmail)?.value).toBe(false); + }); + + it('should patch notification subscriptions form from selector data', () => { + setup(); + + expect(component.notificationSubscriptionsForm.get(SubscriptionEvent.GlobalFileUpdated)?.value).toBe( + SubscriptionFrequency.Daily + ); + expect(component.notificationSubscriptionsForm.get(SubscriptionEvent.GlobalReviews)?.value).toBe( + SubscriptionFrequency.Instant + ); + }); + + it('should dispatch notification and account settings fetches on init when data is missing', () => { + setup({ + selectorOverrides: [ + { selector: AccountSettingsSelectors.getAccountSettings, value: null }, + { selector: NotificationSubscriptionSelectors.getAllGlobalNotificationSubscriptions, value: [] }, + ], + detectChanges: false, + }); + + component.ngOnInit(); + + expect(store.dispatch).toHaveBeenCalledWith(new GetAllGlobalNotificationSubscriptions()); + expect(store.dispatch).toHaveBeenCalledWith(new GetAccountSettings()); + }); + + it('should not dispatch initial fetches on init when data already exists', () => { + setup({ detectChanges: false }); + (store.dispatch as jest.Mock).mockClear(); + + component.ngOnInit(); - return signal(null); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(GetAllGlobalNotificationSubscriptions)); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(GetAccountSettings)); + }); + + it('should not submit email preferences when current user is missing', () => { + setup({ + selectorOverrides: [{ selector: UserSelectors.getCurrentUser, value: null }], }); + (store.dispatch as jest.Mock).mockClear(); + component.emailPreferencesFormSubmit(); - expect(loaderService.hide).not.toHaveBeenCalled(); + expect(loaderService.show).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(UpdateAccountSettings)); + expect(toastService.showSuccess).not.toHaveBeenCalled(); }); - it('should handle subscription completion correctly', () => { - const mockDispatch = jest.fn().mockReturnValue(of({})); - MOCK_STORE.dispatch.mockImplementation(mockDispatch); + it('should submit email preferences and show success toast when current user exists', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + component.emailPreferencesForm.patchValue({ + subscribeOsfGeneralEmail: false, + subscribeOsfHelpEmail: true, + }); component.emailPreferencesFormSubmit(); - const subscription = mockDispatch.mock.results[0].value; - subscription.subscribe(() => { - expect(loaderService.hide).toHaveBeenCalledTimes(1); - }); + expect(loaderService.show).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith( + new UpdateAccountSettings({ + subscribeOsfGeneralEmail: false, + subscribeOsfHelpEmail: true, + }) + ); + expect(loaderService.hide).toHaveBeenCalled(); + expect(toastService.showSuccess).toHaveBeenCalledWith('settings.notifications.emailPreferences.successUpdate'); }); - it('should call dispatch only once per subscription change', () => { - const mockDispatch = jest.fn().mockReturnValue(of({})); - MOCK_STORE.dispatch.mockImplementation(mockDispatch); - const event = SubscriptionEvent.GlobalFileUpdated; - const frequency = SubscriptionFrequency.Daily; + it('should not update subscription when current user is missing', () => { + setup({ + selectorOverrides: [{ selector: UserSelectors.getCurrentUser, value: null }], + }); + (store.dispatch as jest.Mock).mockClear(); + + component.onSubscriptionChange(SubscriptionEvent.GlobalFileUpdated, SubscriptionFrequency.Instant); - component.onSubscriptionChange(event, frequency); + expect(loaderService.show).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(UpdateNotificationSubscription)); + expect(toastService.showSuccess).not.toHaveBeenCalled(); + }); - expect(mockDispatch).toHaveBeenCalledTimes(1); + it('should update notification subscription and show success toast', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); + + component.onSubscriptionChange(SubscriptionEvent.GlobalFileUpdated, SubscriptionFrequency.Instant); + + expect(loaderService.show).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith( + new UpdateNotificationSubscription({ + id: '1_global_file_updated', + frequency: SubscriptionFrequency.Instant, + }) + ); + expect(loaderService.hide).toHaveBeenCalled(); + expect(toastService.showSuccess).toHaveBeenCalledWith( + 'settings.notifications.notificationPreferences.successUpdate' + ); }); }); diff --git a/src/app/features/settings/profile-settings/components/citation-preview/citation-preview.component.spec.ts b/src/app/features/settings/profile-settings/components/citation-preview/citation-preview.component.spec.ts index 792a425ed..e932997b3 100644 --- a/src/app/features/settings/profile-settings/components/citation-preview/citation-preview.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/citation-preview/citation-preview.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipes } from 'ng-mocks'; +import { MockPipe } from 'ng-mocks'; import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -9,6 +8,7 @@ import { CitationFormatPipe } from '@shared/pipes/citation-format.pipe'; import { CitationPreviewComponent } from './citation-preview.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('CitationPreviewComponent', () => { let component: CitationPreviewComponent; @@ -19,7 +19,8 @@ describe('CitationPreviewComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CitationPreviewComponent, MockPipes(TranslatePipe, CitationFormatPipe)], + imports: [CitationPreviewComponent, MockPipe(CitationFormatPipe)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(CitationPreviewComponent); diff --git a/src/app/features/settings/profile-settings/components/education-form/education-form.component.spec.ts b/src/app/features/settings/profile-settings/components/education-form/education-form.component.spec.ts index b93bda627..3b471a6a6 100644 --- a/src/app/features/settings/profile-settings/components/education-form/education-form.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/education-form/education-form.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -10,6 +9,7 @@ import { TextInputComponent } from '@osf/shared/components/text-input/text-input import { EducationFormComponent } from './education-form.component'; import { MOCK_EDUCATION } from '@testing/mocks/user-employment-education.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EducationFormComponent', () => { let component: EducationFormComponent; @@ -28,7 +28,8 @@ describe('EducationFormComponent', () => { }); await TestBed.configureTestingModule({ - imports: [EducationFormComponent, MockPipe(TranslatePipe), MockComponent(TextInputComponent)], + imports: [EducationFormComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(EducationFormComponent); diff --git a/src/app/features/settings/profile-settings/components/education/education.component.spec.ts b/src/app/features/settings/profile-settings/components/education/education.component.spec.ts index 958a758f4..2ec36be36 100644 --- a/src/app/features/settings/profile-settings/components/education/education.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/education/education.component.spec.ts @@ -1,154 +1,210 @@ import { Store } from '@ngxs/store'; -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UpdateProfileSettingsEducation, UserSelectors } from '@core/store/user'; +import { UpdateProfileSettingsEducation, UserSelectors } from '@osf/core/store/user'; +import { Education } from '@osf/shared/models/user/education.model'; +import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { EducationFormComponent } from '../education-form/education-form.component'; - import { EducationComponent } from './education.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMock, - MockCustomConfirmationServiceProvider, -} from '@testing/mocks/custom-confirmation.service.mock'; -import { MOCK_EDUCATION } from '@testing/mocks/education.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; + CustomConfirmationServiceMockType, +} from '@testing/providers/custom-confirmation-provider.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('EducationComponent', () => { let component: EducationComponent; let fixture: ComponentFixture; - - const mockStore = { - selectSignal: jest.fn().mockImplementation((selector) => { - if (selector === UserSelectors.getEducation) { - return () => MOCK_EDUCATION; - } - return () => null; - }), - dispatch: jest.fn().mockReturnValue(of({})), - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [EducationComponent, MockComponent(EducationFormComponent)], - providers: [ - TranslateServiceMock, - MockCustomConfirmationServiceProvider, - MockProvider(ToastService), - MockProvider(Store, mockStore), - provideHttpClient(), - provideHttpClientTesting(), - ], - }).compileComponents(); - - fixture = TestBed.createComponent(EducationComponent); - component = fixture.componentInstance; - - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should handle invalid index in removeEducation method', () => { - const initialLength = component.educations.length; - - component.removeEducation(100); - - expect(component.educations.length).toBe(initialLength); - }); - - it('should not add education when form is invalid', () => { - component.educations.at(0).get('institution')?.setValue(''); - component.educations.at(0).get('institution')?.updateValueAndValidity(); - const initialLength = component.educations.length; - - expect(component.educationForm.invalid).toBe(true); - - component.addEducation(); - - expect(component.educations.length).toBe(initialLength); - }); - - it('should add new education form when form is valid', () => { - const initialLength = component.educations.length; - - component.addEducation(); - - expect(component.educations.length).toBe(initialLength + 1); - - const newEducation = component.educations.at(initialLength); - expect(newEducation).toBeDefined(); - expect(newEducation.get('institution')?.value).toBe(''); - expect(newEducation.get('department')?.value).toBe(''); - expect(newEducation.get('degree')?.value).toBe(''); - expect(newEducation.get('startDate')?.value).toBe(null); - expect(newEducation.get('endDate')?.value).toBe(null); - expect(newEducation.get('ongoing')?.value).toBe(false); - }); - - it('should detect changes when form field is modified', () => { - component.educations.at(0).get('institution')?.setValue('New Institution'); - - component.discardChanges(); - - expect(CustomConfirmationServiceMock.confirmDelete).toHaveBeenCalled(); - }); - - it('should mark all fields as touched when form is invalid', () => { - component.educations.at(0).get('institution')?.setValue(''); - component.educations.at(1).get('degree')?.setValue(''); - - component.saveEducation(); - - expect(component.educationForm.touched).toBe(true); - expect(component.educations.at(0).get('institution')?.touched).toBe(true); - expect(component.educations.at(1).get('degree')?.touched).toBe(true); + let store: Store; + let loaderService: LoaderServiceMock; + let confirmationService: CustomConfirmationServiceMockType; + let toastService: ToastServiceMockType; + + const initialEducation: Education[] = [ + { + institution: 'Test University', + department: 'Computer Science', + degree: 'MSc', + startMonth: 9, + startYear: 2020, + endMonth: 6, + endYear: 2022, + ongoing: false, + }, + ]; + + describe('with default education data', () => { + beforeEach(() => { + loaderService = new LoaderServiceMock(); + confirmationService = CustomConfirmationServiceMock.simple(); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [EducationComponent], + providers: [ + provideOSFCore(), + MockProvider(LoaderService, loaderService), + MockProvider(CustomConfirmationService, confirmationService), + MockProvider(ToastService, toastService), + provideMockStore({ + signals: [{ selector: UserSelectors.getEducation, value: initialEducation }], + }), + ], + }); + + store = TestBed.inject(Store); + fixture = TestBed.createComponent(EducationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize form from selector education data', () => { + expect(component.educations.length).toBe(1); + expect(component.educations.at(0).get('institution')?.value).toBe('Test University'); + expect(component.educations.at(0).get('degree')?.value).toBe('MSc'); + }); + + it('should remove education by index', () => { + component.removeEducation(0); + + expect(component.educations.length).toBe(0); + }); + + it('should mark form touched and not add when current form is invalid', () => { + const initialLength = component.educations.length; + component.educations.at(0).patchValue({ + institution: '', + }); + + component.addEducation(); + + expect(component.educations.length).toBe(initialLength); + expect(component.educationForm.touched).toBe(true); + }); + + it('should add new education form group when form is valid', () => { + const initialLength = component.educations.length; + + component.addEducation(); + + expect(component.educations.length).toBe(initialLength + 1); + }); + + it('should return true for hasFormChanges when item count differs', () => { + component.addEducation(); + + expect(component.hasFormChanges()).toBe(true); + }); + + it('should skip discard confirmation when there are no changes', () => { + component.discardChanges(); + + expect(confirmationService.confirmDelete).not.toHaveBeenCalled(); + }); + + it('should show discard confirmation and reset values on confirm', () => { + component.educations.at(0).patchValue({ + institution: 'Changed University', + }); + + component.discardChanges(); + + expect(confirmationService.confirmDelete).toHaveBeenCalled(); + const { onConfirm } = confirmationService.confirmDelete.mock.calls[0][0]; + onConfirm(); + + expect(component.educations.at(0).get('institution')?.value).toBe('Test University'); + expect(toastService.showSuccess).toHaveBeenCalledWith('settings.profileSettings.changesDiscarded'); + }); + + it('should not save when form is invalid', () => { + component.educations.at(0).patchValue({ + institution: '', + }); + (store.dispatch as jest.Mock).mockClear(); + + component.saveEducation(); + + expect(loaderService.show).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(UpdateProfileSettingsEducation)); + }); + + it('should save education and show success toast when form is valid', () => { + (store.dispatch as jest.Mock).mockClear(); + + component.saveEducation(); + + expect(loaderService.show).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith( + new UpdateProfileSettingsEducation([ + { + institution: 'Test University', + department: 'Computer Science', + degree: 'MSc', + startMonth: 9, + startYear: 2020, + endMonth: 6, + endYear: 2022, + ongoing: false, + }, + ]) + ); + expect(loaderService.hide).toHaveBeenCalled(); + expect(toastService.showSuccess).toHaveBeenCalledWith('settings.profileSettings.education.successUpdate'); + }); }); - it('should map form data to correct education format', () => { - const education = component.educations.at(0); - education.get('institution')?.setValue('Test University'); - education.get('department')?.setValue('Engineering'); - education.get('degree')?.setValue('Bachelor'); - education.get('startDate')?.setValue(new Date(2020, 0)); - education.get('endDate')?.setValue(new Date(2024, 5)); - education.get('ongoing')?.setValue(false); - - component.saveEducation(); - - expect(mockStore.dispatch).toHaveBeenCalledWith( - new UpdateProfileSettingsEducation([ - { - institution: 'Test University', - department: 'Engineering', - degree: 'Bachelor', - startYear: 2020, - startMonth: 1, - endYear: 2024, - endMonth: 6, - ongoing: false, - }, - { - institution: 'Advanced University', - department: 'Software Engineering', - degree: 'Master of Science', - startYear: 2020, - startMonth: 9, - endYear: 2025, - endMonth: 8, - ongoing: false, - }, - ]) - ); + describe('with empty education data', () => { + beforeEach(() => { + loaderService = new LoaderServiceMock(); + confirmationService = CustomConfirmationServiceMock.simple(); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [EducationComponent], + providers: [ + provideOSFCore(), + MockProvider(LoaderService, loaderService), + MockProvider(CustomConfirmationService, confirmationService), + MockProvider(ToastService, toastService), + provideMockStore({ + signals: [{ selector: UserSelectors.getEducation, value: [] }], + }), + ], + }); + + store = TestBed.inject(Store); + fixture = TestBed.createComponent(EducationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should return false for hasFormChanges when initial and current are empty', () => { + expect(component.hasFormChanges()).toBe(false); + }); + + it('should return true for hasFormChanges when user adds values and initial is empty', () => { + component.addEducation(); + component.educations.at(0).patchValue({ + institution: 'New University', + startDate: new Date(2022, 1, 1), + endDate: new Date(2023, 1, 1), + }); + + expect(component.hasFormChanges()).toBe(true); + }); }); }); diff --git a/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.spec.ts b/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.spec.ts index 6b2f33d0b..ba37a7ca9 100644 --- a/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/employment-form/employment-form.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -10,6 +9,7 @@ import { TextInputComponent } from '@osf/shared/components/text-input/text-input import { EmploymentFormComponent } from './employment-form.component'; import { MOCK_EDUCATION, MOCK_EMPLOYMENT } from '@testing/mocks/user-employment-education.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EmploymentFormComponent', () => { let component: EmploymentFormComponent; @@ -28,7 +28,8 @@ describe('EmploymentFormComponent', () => { }); await TestBed.configureTestingModule({ - imports: [EmploymentFormComponent, MockPipe(TranslatePipe), MockComponent(TextInputComponent)], + imports: [EmploymentFormComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(EmploymentFormComponent); diff --git a/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts b/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts index 21bba0011..8647f5f9a 100644 --- a/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/employment/employment.component.spec.ts @@ -1,53 +1,66 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; - -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UpdateProfileSettingsEmployment, UserSelectors } from '@core/store/user'; +import { UpdateProfileSettingsEmployment, UserSelectors } from '@osf/core/store/user'; +import { Employment } from '@osf/shared/models/user/employment.model'; +import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { EmploymentFormComponent } from '../employment-form/employment-form.component'; - import { EmploymentComponent } from './employment.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMock, - MockCustomConfirmationServiceProvider, -} from '@testing/mocks/custom-confirmation.service.mock'; -import { MOCK_EMPLOYMENT } from '@testing/mocks/employment.mock'; + CustomConfirmationServiceMockType, +} from '@testing/providers/custom-confirmation-provider.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('EmploymentComponent', () => { let component: EmploymentComponent; let fixture: ComponentFixture; - - const mockStore = { - selectSignal: jest.fn().mockImplementation((selector) => { - if (selector === UserSelectors.getEmployment) { - return () => MOCK_EMPLOYMENT; - } - return () => null; - }), - dispatch: jest.fn().mockReturnValue(of({})), - }; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [EmploymentComponent, MockPipe(TranslatePipe), MockComponent(EmploymentFormComponent)], + let store: Store; + let loaderService: LoaderServiceMock; + let confirmationService: CustomConfirmationServiceMockType; + let toastService: ToastServiceMockType; + + const initialEmployment: Employment[] = [ + { + title: 'Engineer', + institution: 'OSF', + department: 'Platform', + startMonth: 1, + startYear: 2021, + endMonth: 12, + endYear: 2023, + ongoing: false, + }, + ]; + + beforeEach(() => { + loaderService = new LoaderServiceMock(); + confirmationService = CustomConfirmationServiceMock.simple(); + toastService = ToastServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [EmploymentComponent], providers: [ - MockCustomConfirmationServiceProvider, - MockProvider(ToastService), - provideHttpClient(), - provideHttpClientTesting(), - MockProvider(Store, mockStore), + provideOSFCore(), + MockProvider(LoaderService, loaderService), + MockProvider(CustomConfirmationService, confirmationService), + MockProvider(ToastService, toastService), + provideMockStore({ + signals: [{ selector: UserSelectors.getEmployment, value: initialEmployment }], + }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(EmploymentComponent); component = fixture.componentInstance; fixture.detectChanges(); @@ -57,87 +70,110 @@ describe('EmploymentComponent', () => { expect(component).toBeTruthy(); }); - it('should handle invalid index in removePosition method', () => { - const initialLength = component.positions.length; + it('should initialize form from selector employment data', () => { + expect(component.positions.length).toBe(1); + expect(component.positions.at(0).get('title')?.value).toBe('Engineer'); + expect(component.positions.at(0).get('institution')?.value).toBe('OSF'); + }); - component.removePosition(100); + it('should remove position by index', () => { + component.removePosition(0); - expect(component.positions.length).toBe(initialLength); + expect(component.positions.length).toBe(0); }); - it('should not add position when form is invalid', () => { - component.positions.at(0).get('title')?.setValue(''); - component.positions.at(0).get('title')?.updateValueAndValidity(); + it('should mark form touched and not add when current form is invalid', () => { const initialLength = component.positions.length; - - expect(component.employmentForm.invalid).toBe(true); + component.positions.at(0).patchValue({ + title: '', + }); component.addPosition(); expect(component.positions.length).toBe(initialLength); + expect(component.employmentForm.touched).toBe(true); }); - it('should add new employment form when form is valid', () => { + it('should add new position form group when form is valid', () => { const initialLength = component.positions.length; component.addPosition(); expect(component.positions.length).toBe(initialLength + 1); + }); + + it('should return false for hasFormChanges when initial and current match', () => { + expect(component.hasFormChanges()).toBe(false); + }); - const newEmployment = component.positions.at(initialLength); - expect(newEmployment).toBeDefined(); - expect(newEmployment.get('title')?.value).toBe(''); - expect(newEmployment.get('institution')?.value).toBe(''); - expect(newEmployment.get('department')?.value).toBe(''); - expect(newEmployment.get('startDate')?.value).toBe(null); - expect(newEmployment.get('endDate')?.value).toBe(null); - expect(newEmployment.get('ongoing')?.value).toBe(false); + it('should return true for hasFormChanges when item count differs', () => { + component.addPosition(); + + expect(component.hasFormChanges()).toBe(true); }); - it('should detect changes when form field is modified', () => { - component.positions.at(0).get('institution')?.setValue('New Institution'); + it('should return true for hasFormChanges when values are changed', () => { + component.positions.at(0).patchValue({ + title: 'Senior Engineer', + }); + + expect(component.hasFormChanges()).toBe(true); + }); + it('should skip discard confirmation when there are no changes', () => { component.discardChanges(); - expect(CustomConfirmationServiceMock.confirmDelete).toHaveBeenCalled(); + expect(confirmationService.confirmDelete).not.toHaveBeenCalled(); }); - it('should mark all fields as touched when form is invalid', () => { - component.positions.at(0).get('institution')?.setValue(''); - component.positions.at(1).get('title')?.setValue(''); + it('should show discard confirmation and reset values on confirm', () => { + component.positions.at(0).patchValue({ + title: 'Changed', + }); + + component.discardChanges(); + + expect(confirmationService.confirmDelete).toHaveBeenCalled(); + const { onConfirm } = confirmationService.confirmDelete.mock.calls[0][0]; + onConfirm(); + + expect(component.positions.at(0).get('title')?.value).toBe('Engineer'); + expect(toastService.showSuccess).toHaveBeenCalledWith('settings.profileSettings.changesDiscarded'); + }); + + it('should not save when form is invalid', () => { + component.positions.at(0).patchValue({ + institution: '', + }); + (store.dispatch as jest.Mock).mockClear(); component.saveEmployment(); - expect(component.employmentForm.touched).toBe(true); - expect(component.positions.at(0).get('institution')?.touched).toBe(true); - expect(component.positions.at(1).get('title')?.touched).toBe(true); + expect(loaderService.show).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(UpdateProfileSettingsEmployment)); }); - it('should map form data to correct employment format', () => { - const employment = component.positions.at(0); - employment.get('title')?.setValue('Software Engineer Intern'); - employment.get('institution')?.setValue('Test University'); - employment.get('department')?.setValue('Engineering'); - employment.get('startDate')?.setValue(new Date(2020, 0)); - employment.get('endDate')?.setValue(new Date(2024, 5)); - employment.get('ongoing')?.setValue(false); + it('should save employment and show success toast when form is valid', () => { + (store.dispatch as jest.Mock).mockClear(); component.saveEmployment(); - expect(mockStore.dispatch).toHaveBeenCalledWith( + expect(loaderService.show).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith( new UpdateProfileSettingsEmployment([ { - title: 'Software Engineer Intern', - institution: 'Test University', - department: 'Engineering', - startYear: 2020, + title: 'Engineer', + institution: 'OSF', + department: 'Platform', startMonth: 1, - endYear: 2024, - endMonth: 6, + startYear: 2021, + endMonth: 12, + endYear: 2023, ongoing: false, }, - expect.any(Object), ]) ); + expect(loaderService.hide).toHaveBeenCalled(); + expect(toastService.showSuccess).toHaveBeenCalledWith('settings.profileSettings.employment.successUpdate'); }); }); diff --git a/src/app/features/settings/profile-settings/components/name-form/name-form.component.spec.ts b/src/app/features/settings/profile-settings/components/name-form/name-form.component.spec.ts index 00792f1c5..5570048c8 100644 --- a/src/app/features/settings/profile-settings/components/name-form/name-form.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/name-form/name-form.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, FormGroup } from '@angular/forms'; @@ -9,6 +8,8 @@ import { TextInputComponent } from '@osf/shared/components/text-input/text-input import { NameFormComponent } from './name-form.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('NameFormComponent', () => { let component: NameFormComponent; let fixture: ComponentFixture; @@ -23,7 +24,8 @@ describe('NameFormComponent', () => { suffix: new FormControl('Jr.', { nonNullable: true }), }); await TestBed.configureTestingModule({ - imports: [NameFormComponent, MockComponent(TextInputComponent), MockPipe(TranslatePipe)], + imports: [NameFormComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(NameFormComponent); diff --git a/src/app/features/settings/profile-settings/components/name/name.component.spec.ts b/src/app/features/settings/profile-settings/components/name/name.component.spec.ts index e4290c6e9..da58b7968 100644 --- a/src/app/features/settings/profile-settings/components/name/name.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/name/name.component.spec.ts @@ -1,12 +1,9 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { provideHttpClient } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { UpdateProfileSettingsUser, UserSelectors } from '@core/store/user'; @@ -18,8 +15,8 @@ import { NameFormComponent } from '../name-form/name-form.component'; import { NameComponent } from './name.component'; -import { MockCustomConfirmationServiceProvider } from '@testing/mocks/custom-confirmation.service.mock'; import { MOCK_USER } from '@testing/mocks/data.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('NameComponent', () => { let component: NameComponent; @@ -40,13 +37,11 @@ describe('NameComponent', () => { jest.clearAllMocks(); await TestBed.configureTestingModule({ - imports: [NameComponent, MockPipe(TranslatePipe), ...MockComponents(CitationPreviewComponent, NameFormComponent)], + imports: [NameComponent, ...MockComponents(CitationPreviewComponent, NameFormComponent)], providers: [ - MockCustomConfirmationServiceProvider, + provideOSFCore(), + MockProvider(CustomConfirmationService), MockProvider(ToastService), - provideHttpClient(), - provideHttpClientTesting(), - MockProvider(TranslatePipe), MockProvider(Store, mockStore), ], }).compileComponents(); diff --git a/src/app/features/settings/profile-settings/components/social-form/social-form.component.spec.ts b/src/app/features/settings/profile-settings/components/social-form/social-form.component.spec.ts index d4b65de16..039147fe9 100644 --- a/src/app/features/settings/profile-settings/components/social-form/social-form.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/social-form/social-form.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SocialFormComponent } from './social-form.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe.skip('SocialFormComponent', () => { let component: SocialFormComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe.skip('SocialFormComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SocialFormComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SocialFormComponent); diff --git a/src/app/features/settings/profile-settings/components/social/social.component.spec.ts b/src/app/features/settings/profile-settings/components/social/social.component.spec.ts index 06a51c93d..c657275f3 100644 --- a/src/app/features/settings/profile-settings/components/social/social.component.spec.ts +++ b/src/app/features/settings/profile-settings/components/social/social.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponent, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -12,8 +11,8 @@ import { SocialFormComponent } from '../social-form/social-form.component'; import { SocialComponent } from './social.component'; -import { MockCustomConfirmationServiceProvider } from '@testing/mocks/custom-confirmation.service.mock'; import { MOCK_USER } from '@testing/mocks/data.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('SocialComponent', () => { @@ -24,14 +23,15 @@ describe('SocialComponent', () => { jest.clearAllMocks(); await TestBed.configureTestingModule({ - imports: [SocialComponent, MockComponent(SocialFormComponent), MockPipe(TranslatePipe)], + imports: [SocialComponent, MockComponent(SocialFormComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: UserSelectors.getSocialLinks, value: MOCK_USER.social }], }), MockProvider(ToastService), MockProvider(LoaderService), - { provide: CustomConfirmationService, useValue: MockCustomConfirmationServiceProvider }, + MockProvider(CustomConfirmationService), ], }).compileComponents(); diff --git a/src/app/features/settings/profile-settings/profile-settings.component.spec.ts b/src/app/features/settings/profile-settings/profile-settings.component.spec.ts index 59ceb5b48..f0d55742d 100644 --- a/src/app/features/settings/profile-settings/profile-settings.component.spec.ts +++ b/src/app/features/settings/profile-settings/profile-settings.component.spec.ts @@ -1,5 +1,4 @@ -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { BehaviorSubject } from 'rxjs'; @@ -13,6 +12,8 @@ import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens'; import { EducationComponent, EmploymentComponent, NameComponent, SocialComponent } from './components'; import { ProfileSettingsComponent } from './profile-settings.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ProfileSettingsComponent', () => { let component: ProfileSettingsComponent; let fixture: ComponentFixture; @@ -24,7 +25,6 @@ describe('ProfileSettingsComponent', () => { await TestBed.configureTestingModule({ imports: [ ProfileSettingsComponent, - MockPipe(TranslatePipe), ...MockComponents( SubHeaderComponent, EducationComponent, @@ -34,7 +34,7 @@ describe('ProfileSettingsComponent', () => { SelectComponent ), ], - providers: [MockProvider(IS_MEDIUM, isMedium), MockProvider(TranslateService)], + providers: [provideOSFCore(), MockProvider(IS_MEDIUM, isMedium)], }).compileComponents(); fixture = TestBed.createComponent(ProfileSettingsComponent); diff --git a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts index e4720655a..18675f021 100644 --- a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts +++ b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts @@ -1,12 +1,10 @@ import { Store } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; import { ActivatedRoute, Navigation, Router, UrlTree } from '@angular/router'; import { AddonSetupAccountFormComponent } from '@osf/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component'; @@ -17,6 +15,7 @@ import { AddonsSelectors } from '@shared/stores/addons'; import { ConnectAddonComponent } from './connect-addon.component'; import { MOCK_ADDON } from '@testing/mocks/addon.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe.skip('ConnectAddonComponent', () => { let component: ConnectAddonComponent; @@ -38,10 +37,9 @@ describe.skip('ConnectAddonComponent', () => { imports: [ ConnectAddonComponent, ...MockComponents(SubHeaderComponent, AddonTermsComponent, AddonSetupAccountFormComponent), - MockPipe(TranslatePipe), ], providers: [ - provideNoopAnimations(), + provideOSFCore(), MockProvider(Store, { selectSignal: jest.fn().mockImplementation((selector) => { if (selector === AddonsSelectors.getAddonsUserReference) { @@ -58,7 +56,6 @@ describe.skip('ConnectAddonComponent', () => { getCurrentNavigation: () => mockNavigation as Navigation, navigate: jest.fn(), }), - MockProvider(TranslateService), MockProvider(ActivatedRoute), ], }).compileComponents(); diff --git a/src/app/features/settings/settings-addons/settings-addons.component.spec.ts b/src/app/features/settings/settings-addons/settings-addons.component.spec.ts index 053693d79..b196faf4d 100644 --- a/src/app/features/settings/settings-addons/settings-addons.component.spec.ts +++ b/src/app/features/settings/settings-addons/settings-addons.component.spec.ts @@ -14,7 +14,7 @@ import { AddonsSelectors } from '@shared/stores/addons'; import { SettingsAddonsComponent } from './settings-addons.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe.skip('AddonsComponent', () => { let component: SettingsAddonsComponent; @@ -33,7 +33,7 @@ describe.skip('AddonsComponent', () => { ), ], providers: [ - TranslateServiceMock, + provideOSFCore(), MockProvider(Store, { selectSignal: jest.fn().mockImplementation((selector) => { if (selector === UserSelectors.getCurrentUser) { diff --git a/src/app/features/settings/settings-container.component.spec.ts b/src/app/features/settings/settings-container.component.spec.ts index b504e3372..0e0968880 100644 --- a/src/app/features/settings/settings-container.component.spec.ts +++ b/src/app/features/settings/settings-container.component.spec.ts @@ -5,7 +5,7 @@ import { HelpScoutService } from '@core/services/help-scout.service'; import { SettingsContainerComponent } from './settings-container.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('Component: Settings', () => { let fixture: ComponentFixture; @@ -13,8 +13,9 @@ describe('Component: Settings', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SettingsContainerComponent, OSFTestingModule], + imports: [SettingsContainerComponent], providers: [ + provideOSFCore(), { provide: HelpScoutService, useValue: { diff --git a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts index aa56cc5a4..e86a7c765 100644 --- a/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts +++ b/src/app/features/settings/tokens/components/token-add-edit-form/token-add-edit-form.component.spec.ts @@ -1,268 +1,200 @@ import { Store } from '@ngxs/store'; -import { TranslateService } from '@ngx-translate/core'; -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; -import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; - -import { of } from 'rxjs'; +import { DynamicDialogRef } from 'primeng/dynamicdialog'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { TokenCreatedDialogComponent } from '@osf/features/settings/tokens/components'; -import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; -import { InputLimits } from '@osf/shared/constants/input-limits.const'; +import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { TokenFormControls, TokenModel } from '../../models'; -import { CreateToken, TokensSelectors } from '../../store'; +import { ScopeModel, TokenFormControls, TokenModel } from '../../models'; +import { CreateToken, TokensSelectors, UpdateToken } from '../../store'; +import { TokenCreatedDialogComponent } from '../token-created-dialog/token-created-dialog.component'; import { TokenAddEditFormComponent } from './token-add-edit-form.component'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; -import { MOCK_SCOPES } from '@testing/mocks/scope.mock'; -import { MOCK_TOKEN } from '@testing/mocks/token.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; -import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { CustomDialogServiceMock, CustomDialogServiceMockType } from '@testing/providers/custom-dialog-provider.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; + +interface SetupOverrides extends BaseSetupOverrides { + isEditMode?: boolean; + initialValues?: TokenModel | null; +} describe('TokenAddEditFormComponent', () => { let component: TokenAddEditFormComponent; let fixture: ComponentFixture; - let dialogService: Partial; - let dialogRef: Partial; - let activatedRoute: Partial; - let router: Partial; - let toastService: jest.Mocked; - let translateService: jest.Mocked; - let toastServiceMock: ReturnType; - - const mockTokens: TokenModel[] = [MOCK_TOKEN]; - - const fillForm = (tokenName: string = MOCK_TOKEN.name, scopes: string[] = MOCK_TOKEN.scopes): void => { - component.tokenForm.patchValue({ - [TokenFormControls.TokenName]: tokenName, - [TokenFormControls.Scopes]: scopes, - }); + let store: Store; + let mockRouter: RouterMockType; + let mockToastService: ToastServiceMockType; + let mockCustomDialogService: CustomDialogServiceMockType; + let dialogRef: { close: jest.Mock }; + + const tokenFromState: TokenModel = { + id: 'token-1', + tokenId: 'secret-token-value', + name: 'Created Token', + scopes: ['osf.full_read'], }; - beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === TokensSelectors.getScopes) return () => MOCK_SCOPES; - if (selector === TokensSelectors.isTokensLoading) return () => false; - if (selector === TokensSelectors.getTokens) return () => mockTokens; - if (selector === TokensSelectors.getTokenById) { - return () => (id: string) => mockTokens.find((token) => token.id === id); - } - return () => null; - }); - - dialogService = { - open: jest.fn(), - }; - - dialogRef = { - close: jest.fn(), - }; - - activatedRoute = { - params: of({ id: MOCK_TOKEN.id }), - }; - - router = { - navigate: jest.fn(), - }; - - toastServiceMock = ToastServiceMockBuilder.create().build(); - - await TestBed.configureTestingModule({ - imports: [TokenAddEditFormComponent, OSFTestingStoreModule, MockComponent(TextInputComponent)], + const defaultSignals: SignalOverride[] = [ + { + selector: TokensSelectors.getScopes, + value: [{ id: 'osf.full_read', description: 'Read access' }] as ScopeModel[], + }, + { selector: TokensSelectors.isTokensLoading, value: false }, + { selector: TokensSelectors.getTokens, value: [tokenFromState] as TokenModel[] }, + ]; + + function setup(overrides: SetupOverrides = {}) { + const route = ActivatedRouteMockBuilder.create() + .withParams({ id: overrides.routeParams?.['id'] ?? 'token-1' }) + .build(); + mockRouter = RouterMockBuilder.create().withUrl('/settings/tokens/token-1').build(); + mockToastService = ToastServiceMock.simple(); + mockCustomDialogService = CustomDialogServiceMock.simple(); + dialogRef = { close: jest.fn() }; + + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + + TestBed.configureTestingModule({ + imports: [TokenAddEditFormComponent], providers: [ - TranslateServiceMock, - MockProvider(Store, MOCK_STORE), - MockProvider(DialogService, dialogService), + provideOSFCore(), + MockProvider(ActivatedRoute, route), + MockProvider(Router, mockRouter), + MockProvider(ToastService, mockToastService), + MockProvider(CustomDialogService, mockCustomDialogService), MockProvider(DynamicDialogRef, dialogRef), - MockProvider(ActivatedRoute, activatedRoute), - MockProvider(Router, router), - MockProvider(ToastService, toastServiceMock), + provideMockStore({ signals }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(TokenAddEditFormComponent); component = fixture.componentInstance; - toastService = TestBed.inject(ToastService) as jest.Mocked; - translateService = TestBed.inject(TranslateService) as jest.Mocked; + if (overrides.isEditMode !== undefined) { + fixture.componentRef.setInput('isEditMode', overrides.isEditMode); + } + if (overrides.initialValues !== undefined) { + fixture.componentRef.setInput('initialValues', overrides.initialValues); + } fixture.detectChanges(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); it('should patch form with initial values on init', () => { - fixture.componentRef.setInput('initialValues', MOCK_TOKEN); - const patchSpy = jest.spyOn(component.tokenForm, 'patchValue'); - - component.ngOnInit(); - - expect(patchSpy).toHaveBeenCalledWith( - expect.objectContaining({ - [TokenFormControls.TokenName]: MOCK_TOKEN.name, - [TokenFormControls.Scopes]: MOCK_TOKEN.scopes, - }) - ); - expect(component.tokenForm.get(TokenFormControls.TokenName)?.value).toBe(MOCK_TOKEN.name); - expect(component.tokenForm.get(TokenFormControls.Scopes)?.value).toEqual(MOCK_TOKEN.scopes); - }); - - it('should not patch form when initialValues are not provided', () => { - fixture.componentRef.setInput('initialValues', null); - - fillForm('Existing Name', ['read']); - - component.ngOnInit(); - - expect(component.tokenForm.get(TokenFormControls.TokenName)?.value).toBe('Existing Name'); - expect(component.tokenForm.get(TokenFormControls.Scopes)?.value).toEqual(['read']); - }); - - it('should not submit when form is invalid', () => { - fillForm('', []); - - const markAllAsTouchedSpy = jest.spyOn(component.tokenForm, 'markAllAsTouched'); - const markAsDirtySpy = jest.spyOn(component.tokenForm.get(TokenFormControls.TokenName)!, 'markAsDirty'); - const markScopesAsDirtySpy = jest.spyOn(component.tokenForm.get(TokenFormControls.Scopes)!, 'markAsDirty'); - - component.handleSubmitForm(); - - expect(markAllAsTouchedSpy).toHaveBeenCalled(); - expect(markAsDirtySpy).toHaveBeenCalled(); - expect(markScopesAsDirtySpy).toHaveBeenCalled(); - expect(MOCK_STORE.dispatch).not.toHaveBeenCalled(); - }); - - it('should return early when tokenName is missing', () => { - fillForm('', ['read']); - - component.handleSubmitForm(); - - expect(MOCK_STORE.dispatch).not.toHaveBeenCalled(); - }); - - it('should return early when scopes is missing', () => { - fillForm('Test Token', []); - - component.handleSubmitForm(); + const initialValues: TokenModel = { + id: 'token-2', + tokenId: 'token-value-2', + name: 'Existing Token', + scopes: ['osf.full_write'], + }; + setup({ initialValues }); - expect(MOCK_STORE.dispatch).not.toHaveBeenCalled(); + expect(component.tokenForm.get(TokenFormControls.TokenName)?.value).toBe('Existing Token'); + expect(component.tokenForm.get(TokenFormControls.Scopes)?.value).toEqual(['osf.full_write']); }); - it('should create token when not in edit mode', () => { - fixture.componentRef.setInput('isEditMode', false); - fillForm('Test Token', ['read', 'write']); - - MOCK_STORE.dispatch.mockReturnValue(of(undefined)); - - component.handleSubmitForm(); + it('should disable form when loading is true', () => { + setup({ + selectorOverrides: [{ selector: TokensSelectors.isTokensLoading, value: true }], + }); - expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new CreateToken('Test Token', ['read', 'write'])); + expect(component.tokenForm.disabled).toBe(true); }); - it('should show success toast and close dialog after creating token', () => { - fixture.componentRef.setInput('isEditMode', false); - fillForm('Test Token', ['read', 'write']); - - MOCK_STORE.dispatch.mockReturnValue(of(undefined)); + it('should mark controls as touched and dirty when submitting invalid form', () => { + setup(); component.handleSubmitForm(); - expect(toastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successCreate'); - expect(dialogRef.close).toHaveBeenCalled(); + expect(component.tokenForm.invalid).toBe(true); + expect(component.tokenForm.get(TokenFormControls.TokenName)?.touched).toBe(true); + expect(component.tokenForm.get(TokenFormControls.TokenName)?.dirty).toBe(true); + expect(component.tokenForm.get(TokenFormControls.Scopes)?.touched).toBe(true); + expect(component.tokenForm.get(TokenFormControls.Scopes)?.dirty).toBe(true); + expect(store.dispatch).not.toHaveBeenCalled(); }); - it('should show success toast and navigate after updating token', () => { - fixture.componentRef.setInput('isEditMode', true); - fillForm('Updated Token', ['read', 'write']); + it('should create token and open created dialog when submitting valid form in create mode', () => { + setup(); + (store.dispatch as jest.Mock).mockClear(); - MOCK_STORE.dispatch.mockReturnValue(of(undefined)); + component.tokenForm.patchValue({ + [TokenFormControls.TokenName]: 'New API Token', + [TokenFormControls.Scopes]: ['osf.full_read'], + }); component.handleSubmitForm(); - expect(toastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successEdit'); - expect(router.navigate).toHaveBeenCalledWith(['settings/tokens']); - }); - - it('should open dialog with correct configuration', () => { - const tokenName = 'Test Token'; - const tokenValue = 'test-token-value'; - - component.showTokenCreatedDialog(tokenName, tokenValue); - - expect(dialogService.open).toHaveBeenCalledWith( + expect(store.dispatch).toHaveBeenCalledWith(new CreateToken('New API Token', ['osf.full_read'])); + expect(mockToastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successCreate'); + expect(dialogRef.close).toHaveBeenCalledWith(); + expect(mockCustomDialogService.open).toHaveBeenCalledWith( TokenCreatedDialogComponent, expect.objectContaining({ - width: '500px', header: 'settings.tokens.createdDialog.title', - closeOnEscape: true, - modal: true, - closable: true, + width: '500px', data: { - tokenName, - tokenValue, + tokenName: 'Created Token', + tokenValue: 'secret-token-value', }, }) ); }); - it('should use TranslateService.instant for dialog header', () => { - component.showTokenCreatedDialog('Name', 'Value'); - expect(translateService.instant).toHaveBeenCalledWith('settings.tokens.createdDialog.title'); - }); + it('should update token and navigate when submitting valid form in edit mode', () => { + setup({ isEditMode: true, routeParams: { id: 'token-9' } }); + (store.dispatch as jest.Mock).mockClear(); - it('should read tokens via selectSignal after create', () => { - fixture.componentRef.setInput('isEditMode', false); - fillForm('Test Token', ['read']); - - const selectSpy = jest.spyOn(MOCK_STORE, 'selectSignal'); - MOCK_STORE.dispatch.mockReturnValue(of(undefined)); + component.tokenForm.patchValue({ + [TokenFormControls.TokenName]: 'Updated Token', + [TokenFormControls.Scopes]: ['osf.full_read'], + }); component.handleSubmitForm(); - expect(selectSpy).toHaveBeenCalledWith(TokensSelectors.getTokens); - }); - - it('should expose the same inputLimits as InputLimits.fullName', () => { - expect(component.inputLimits).toBe(InputLimits.fullName); - }); - - it('should require token name', () => { - const tokenNameControl = component.tokenForm.get(TokenFormControls.TokenName); - expect(tokenNameControl?.hasError('required')).toBe(true); - }); - - it('should require scopes', () => { - const scopesControl = component.tokenForm.get(TokenFormControls.Scopes); - expect(scopesControl?.hasError('required')).toBe(true); - }); - - it('should be valid when both fields are filled', () => { - fillForm('Test Token', ['read']); - - expect(component.tokenForm.valid).toBe(true); + expect(store.dispatch).toHaveBeenCalledWith(new UpdateToken('token-9', 'Updated Token', ['osf.full_read'])); + expect(mockRouter.navigate).toHaveBeenCalledWith(['settings/tokens']); + expect(mockToastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successEdit'); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should have correct input limits for token name', () => { - expect(component.inputLimits).toBeDefined(); - }); + it('should open created-token dialog with provided values', () => { + setup(); - it('should expose tokenId from route params', () => { - expect(component.tokenId()).toBe(MOCK_TOKEN.id); - }); + component.showTokenCreatedDialog('Dialog Token', 'dialog-token-value'); - it('should expose scopes from store via tokenScopes signal', () => { - expect(component.tokenScopes()).toEqual(MOCK_SCOPES); + expect(mockCustomDialogService.open).toHaveBeenCalledWith( + TokenCreatedDialogComponent, + expect.objectContaining({ + header: 'settings.tokens.createdDialog.title', + width: '500px', + data: { + tokenName: 'Dialog Token', + tokenValue: 'dialog-token-value', + }, + }) + ); }); }); diff --git a/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.spec.ts b/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.spec.ts index 21ebd2cb5..a82661e08 100644 --- a/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.spec.ts +++ b/src/app/features/settings/tokens/components/token-created-dialog/token-created-dialog.component.spec.ts @@ -10,7 +10,7 @@ import { CopyButtonComponent } from '@osf/shared/components/copy-button/copy-but import { TokenCreatedDialogComponent } from './token-created-dialog.component'; import { MOCK_TOKEN } from '@testing/mocks/token.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('TokenCreatedDialogComponent', () => { let component: TokenCreatedDialogComponent; @@ -18,8 +18,9 @@ describe('TokenCreatedDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TokenCreatedDialogComponent, OSFTestingModule, MockComponent(CopyButtonComponent)], + imports: [TokenCreatedDialogComponent, MockComponent(CopyButtonComponent)], providers: [ + provideOSFCore(), MockProvider(DynamicDialogRef, { close: jest.fn() }), MockProvider(DynamicDialogConfig, { data: { diff --git a/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts b/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts index a092cc94a..65c33e885 100644 --- a/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts +++ b/src/app/features/settings/tokens/pages/token-details/token-details.component.spec.ts @@ -2,111 +2,137 @@ import { Store } from '@ngxs/store'; import { MockComponents, MockProvider } from 'ng-mocks'; -import { of } from 'rxjs'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; -import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { ToastService } from '@osf/shared/services/toast.service'; import { TokenAddEditFormComponent } from '../../components'; import { TokenModel } from '../../models'; -import { TokensSelectors } from '../../store'; +import { DeleteToken, GetTokenById, TokensSelectors } from '../../store'; import { TokenDetailsComponent } from './token-details.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('TokenDetailsComponent', () => { let component: TokenDetailsComponent; let fixture: ComponentFixture; - let confirmationService: Partial; - let mockCustomDialogService: ReturnType; + let store: Store; + let mockRouter: RouterMockType; + let mockToastService: ToastServiceMockType; + let confirmationService: { confirmDelete: jest.Mock }; const mockToken: TokenModel = { - id: '1', - tokenId: '2', + id: 'token-1', + tokenId: 'token-value-1', name: 'Test Token', - scopes: ['read', 'write'], + scopes: ['osf.full_read'], }; - const storeMock = { - dispatch: jest.fn().mockReturnValue(of({})), - selectSnapshot: jest.fn().mockImplementation((selector: unknown) => { - if (selector === TokensSelectors.getTokenById) { - return (id: string) => (id === mockToken.id ? mockToken : null); - } - return null; - }), - selectSignal: jest.fn().mockImplementation((selector: unknown) => { - if (selector === TokensSelectors.isTokensLoading) return () => false; - if (selector === TokensSelectors.getTokenById) - return () => (id: string) => (id === mockToken.id ? mockToken : null); - return () => null; - }), - } as unknown as jest.Mocked; - - beforeEach(async () => { - mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); - - confirmationService = { - confirmDelete: jest.fn(), - }; - - await TestBed.configureTestingModule({ + const defaultSignals: SignalOverride[] = [ + { selector: TokensSelectors.isTokensLoading, value: false }, + { + selector: TokensSelectors.getTokenById, + value: (id: string | null) => (id === mockToken.id ? mockToken : null), + }, + ]; + + function setup(overrides: BaseSetupOverrides = {}) { + const route = ActivatedRouteMockBuilder.create() + .withParams(overrides.routeParams ?? { id: 'token-1' }) + .build(); + mockRouter = RouterMockBuilder.create().withUrl('/settings/tokens/token-1').build(); + mockToastService = ToastServiceMock.simple(); + confirmationService = { confirmDelete: jest.fn() }; + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + + TestBed.configureTestingModule({ imports: [ TokenDetailsComponent, - OSFTestingModule, ...MockComponents(TokenAddEditFormComponent, IconComponent, LoadingSpinnerComponent), ], providers: [ - MockProvider(Store, storeMock), + provideOSFCore(), + MockProvider(ActivatedRoute, route), + MockProvider(Router, mockRouter), + MockProvider(ToastService, mockToastService), MockProvider(CustomConfirmationService, confirmationService), - MockProvider(CustomDialogService, mockCustomDialogService), - { - provide: ActivatedRoute, - useValue: { - params: of({ id: mockToken.id }), - snapshot: { - paramMap: new Map(Object.entries({ id: mockToken.id })), - params: { id: mockToken.id }, - queryParams: {}, - }, - }, - }, + provideMockStore({ signals }), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(TokenDetailsComponent); component = fixture.componentInstance; - fixture.detectChanges(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should dispatch GetTokenById on init when tokenId exists', () => { + it('should set token from selector by route id', () => { + setup(); + + expect(component.tokenId()).toBe('token-1'); + expect(component.token()).toEqual(mockToken); + }); + + it('should dispatch getTokenById on init when token id exists', () => { + setup(); + + expect(store.dispatch).toHaveBeenCalledWith(new GetTokenById('token-1')); + }); + + it('should not dispatch getTokenById when token id is missing', () => { + setup({ routeParams: {} }); + (store.dispatch as jest.Mock).mockClear(); + + component.tokenId.set(''); component.ngOnInit(); - expect(storeMock.dispatch).toHaveBeenCalled(); + + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(GetTokenById)); }); - it('should confirm and delete token on deleteToken()', () => { - (confirmationService.confirmDelete as jest.Mock).mockImplementation(({ onConfirm }: any) => onConfirm()); + it('should call confirmation service with expected payload on deleteToken', () => { + setup(); component.deleteToken(); expect(confirmationService.confirmDelete).toHaveBeenCalledWith( expect.objectContaining({ headerKey: 'settings.tokens.confirmation.delete.title', + headerParams: { name: 'Test Token' }, messageKey: 'settings.tokens.confirmation.delete.message', + onConfirm: expect.any(Function), }) ); - expect(storeMock.dispatch).toHaveBeenCalled(); + }); + + it('should dispatch delete action and show success flow after confirm', () => { + setup(); + + component.deleteToken(); + const confirmArg = (confirmationService.confirmDelete as jest.Mock).mock.calls[0][0]; + confirmArg.onConfirm(); + + expect(store.dispatch).toHaveBeenCalledWith(new DeleteToken('token-1')); + expect(mockToastService.showSuccess).toHaveBeenCalledWith('settings.tokens.toastMessage.successDelete'); + expect(mockRouter.navigate).toHaveBeenCalledWith(['settings/tokens']); }); }); diff --git a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts index 2f7941111..44c538fb0 100644 --- a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts +++ b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts @@ -1,14 +1,6 @@ -import { TranslatePipe } from '@ngx-translate/core'; -import { MockPipe } from 'ng-mocks'; - -import { Button } from 'primeng/button'; -import { Card } from 'primeng/card'; -import { Skeleton } from 'primeng/skeleton'; - import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { RouterLink } from '@angular/router'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -17,6 +9,8 @@ import { TokenModel } from '../../models'; import { TokensListComponent } from './tokens-list.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + jest.mock('../../store', () => ({ TokensSelectors: { isTokensLoading: function isTokensLoading() {}, @@ -56,8 +50,9 @@ describe('TokensListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TokensListComponent, MockPipe(TranslatePipe), Button, Card, Skeleton, RouterLink], + imports: [TokensListComponent], providers: [ + provideOSFCore(), { provide: CustomConfirmationService, useValue: mockConfirmationService }, { provide: ToastService, useValue: mockToastService }, ], diff --git a/src/app/features/settings/tokens/services/tokens.service.spec.ts b/src/app/features/settings/tokens/services/tokens.service.spec.ts index 1c29bf6f2..ed8b77998 100644 --- a/src/app/features/settings/tokens/services/tokens.service.spec.ts +++ b/src/app/features/settings/tokens/services/tokens.service.spec.ts @@ -1,136 +1,227 @@ -import { of } from 'rxjs'; - -import { TestBed } from '@angular/core/testing'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { inject, TestBed } from '@angular/core/testing'; import { JsonApiResponse } from '@osf/shared/models/common/json-api.model'; -import { JsonApiService } from '@osf/shared/services/json-api.service'; -import { ScopeMapper, TokenMapper } from '../mappers'; import { ScopeJsonApi, ScopeModel, TokenGetResponseJsonApi, TokenModel } from '../models'; import { TokensService } from './tokens.service'; -import { environment } from 'src/environments/environment'; - -jest.mock('../mappers/scope.mapper'); -jest.mock('../mappers/token.mapper'); +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; +import { EnvironmentTokenMock } from '@testing/providers/environment.token.mock'; describe('TokensService', () => { let service: TokensService; - let jsonApiServiceMock: jest.Mocked; + const apiBase = `${EnvironmentTokenMock.useValue.apiDomainUrl}/v2`; beforeEach(() => { - jsonApiServiceMock = { - get: jest.fn(), - post: jest.fn(), - patch: jest.fn(), - delete: jest.fn(), - } as unknown as jest.Mocked; - TestBed.configureTestingModule({ - providers: [TokensService, { provide: JsonApiService, useValue: jsonApiServiceMock }], + providers: [provideOSFCore(), provideOSFHttp(), TokensService], }); - service = TestBed.inject(TokensService); }); - it('getScopes should map response using ScopeMapper', (done) => { - const mockResponse = { data: [{ type: 'scope' }] as ScopeJsonApi[] }; - const mappedScopes: ScopeModel[] = [{ name: 'mock-scope' }] as any; - - (ScopeMapper.fromResponse as jest.Mock).mockReturnValue(mappedScopes); - jsonApiServiceMock.get.mockReturnValue(of(mockResponse)); - - service.getScopes().subscribe((result) => { - expect(jsonApiServiceMock.get).toHaveBeenCalledWith(`${environment.apiDomainUrl}/v2/scopes/`); - expect(result).toBe(mappedScopes); - done(); - }); + it('should expose apiUrl from environment', () => { + expect(service.apiUrl).toBe(`${apiBase}`); }); - it('getTokens should map each response using TokenMapper.fromGetResponse', (done) => { - const mockData: TokenGetResponseJsonApi[] = [ - { id: '1' } as TokenGetResponseJsonApi, - { id: '2' } as TokenGetResponseJsonApi, - ]; - const mapped = [{ id: '1' }, { id: '2' }] as TokenModel[]; - - (TokenMapper.fromGetResponse as jest.Mock).mockImplementation((item) => ({ id: item.id })); - - jsonApiServiceMock.get.mockReturnValue(of({ data: mockData })); - - service.getTokens().subscribe((tokens) => { - expect(tokens).toEqual(mapped); - expect(TokenMapper.fromGetResponse).toHaveBeenCalledTimes(2); - done(); + it('should getScopes and map response', inject([HttpTestingController], (httpMock: HttpTestingController) => { + const response: JsonApiResponse = { + data: [ + { + id: 'osf.full_read', + type: 'scopes', + attributes: { description: 'Read access' }, + }, + ], + }; + let result: ScopeModel[] = []; + + service.getScopes().subscribe((value) => (result = value)); + + const req = httpMock.expectOne(`${apiBase}/scopes/`); + expect(req.request.method).toBe('GET'); + req.flush(response); + + expect(result).toEqual([{ id: 'osf.full_read', description: 'Read access' }]); + httpMock.verify(); + })); + + it('should getTokens and map response', inject([HttpTestingController], (httpMock: HttpTestingController) => { + const response: JsonApiResponse = { + data: [ + { + id: 'token-1', + attributes: { name: 'Token One', token_id: 'token-value-1' }, + embeds: { + scopes: { + data: [{ id: 'osf.full_read' }, { id: 'osf.full_write' }], + }, + }, + }, + ], + }; + let result: TokenModel[] = []; + + service.getTokens().subscribe((value) => (result = value)); + + const req = httpMock.expectOne(`${apiBase}/tokens/`); + expect(req.request.method).toBe('GET'); + req.flush(response); + + expect(result).toEqual([ + { + id: 'token-1', + tokenId: 'token-value-1', + name: 'Token One', + scopes: ['osf.full_read', 'osf.full_write'], + }, + ]); + httpMock.verify(); + })); + + it('should getTokenById and map response', inject([HttpTestingController], (httpMock: HttpTestingController) => { + const response: JsonApiResponse = { + data: { + id: 'token-2', + attributes: { name: 'Token Two', token_id: 'token-value-2' }, + embeds: { + scopes: { + data: [{ id: 'osf.full_read' }], + }, + }, + }, + }; + let result: TokenModel | undefined; + + service.getTokenById('token-2').subscribe((value) => (result = value)); + + const req = httpMock.expectOne(`${apiBase}/tokens/token-2/`); + expect(req.request.method).toBe('GET'); + req.flush(response); + + expect(result).toEqual({ + id: 'token-2', + tokenId: 'token-value-2', + name: 'Token Two', + scopes: ['osf.full_read'], }); - }); - - it('getTokenById should map response using TokenMapper.fromGetResponse', (done) => { - const tokenId = 'abc'; - const mockApiResponse = { data: { id: tokenId } as TokenGetResponseJsonApi }; - const mappedToken = { id: tokenId } as TokenModel; - - (TokenMapper.fromGetResponse as jest.Mock).mockReturnValue(mappedToken); - jsonApiServiceMock.get.mockReturnValue(of(mockApiResponse)); - - service.getTokenById(tokenId).subscribe((token) => { - expect(jsonApiServiceMock.get).toHaveBeenCalledWith(`${environment.apiDomainUrl}/v2/tokens/${tokenId}/`); - expect(token).toBe(mappedToken); - done(); + httpMock.verify(); + })); + + it('should createToken with mapped request and mapped response', inject( + [HttpTestingController], + (httpMock: HttpTestingController) => { + const response: JsonApiResponse = { + data: { + id: 'token-3', + attributes: { name: 'Created Token', token_id: 'token-value-3' }, + embeds: { + scopes: { + data: [{ id: 'osf.full_read' }, { id: 'osf.full_write' }], + }, + }, + }, + }; + let result: TokenModel | undefined; + + service.createToken('Created Token', ['osf.full_read', 'osf.full_write']).subscribe((value) => (result = value)); + + const req = httpMock.expectOne(`${apiBase}/tokens/`); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({ + data: { + attributes: { + name: 'Created Token', + scopes: 'osf.full_read osf.full_write', + }, + type: 'tokens', + }, + }); + req.flush(response); + + expect(result).toEqual({ + id: 'token-3', + tokenId: 'token-value-3', + name: 'Created Token', + scopes: ['osf.full_read', 'osf.full_write'], + }); + httpMock.verify(); + } + )); + + it('should updateToken with mapped request and mapped response', inject( + [HttpTestingController], + (httpMock: HttpTestingController) => { + const response = { + data: { + id: 'token-4', + attributes: { name: 'Updated Token', token_id: 'token-value-4' }, + embeds: { + scopes: { + data: [{ id: 'osf.full_write' }], + }, + }, + } as TokenGetResponseJsonApi, + }; + let result: TokenModel | undefined; + + service.updateToken('token-4', 'Updated Token', ['osf.full_write']).subscribe((value) => (result = value)); + + const req = httpMock.expectOne(`${apiBase}/tokens/token-4/`); + expect(req.request.method).toBe('PATCH'); + expect(req.request.body).toEqual({ + data: { + attributes: { + name: 'Updated Token', + scopes: 'osf.full_write', + }, + type: 'tokens', + }, + }); + req.flush(response); + + expect(result).toEqual({ + id: 'token-4', + tokenId: 'token-value-4', + name: 'Updated Token', + scopes: ['osf.full_write'], + }); + httpMock.verify(); + } + )); + + it('should deleteToken with DELETE method', inject([HttpTestingController], (httpMock: HttpTestingController) => { + let completed = false; + + service.deleteToken('token-5').subscribe({ + complete: () => { + completed = true; + }, }); - }); - - it('createToken should map response using TokenMapper.fromCreateResponse', (done) => { - const name = 'new token'; - const scopes = ['read']; - const requestBody = { name, scopes }; - const apiResponse = { data: { id: 'xyz' } } as JsonApiResponse; - const mapped = { id: 'xyz' } as TokenModel; + const req = httpMock.expectOne(`${apiBase}/tokens/token-5/`); + expect(req.request.method).toBe('DELETE'); + req.flush(null); - (TokenMapper.toRequest as jest.Mock).mockReturnValue(requestBody); - (TokenMapper.fromGetResponse as jest.Mock).mockReturnValue(mapped); - jsonApiServiceMock.post.mockReturnValue(of(apiResponse)); + expect(completed).toBe(true); + httpMock.verify(); + })); - service.createToken(name, scopes).subscribe((token) => { - expect(jsonApiServiceMock.post).toHaveBeenCalledWith(`${environment.apiDomainUrl}/v2/tokens/`, requestBody); - expect(token).toEqual(mapped); - done(); - }); - }); + it('should propagate errors from getTokenById', inject([HttpTestingController], (httpMock: HttpTestingController) => { + let errorStatus: number | undefined; - it('updateToken should map response using TokenMapper.fromCreateResponse', (done) => { - const tokenId = '123'; - const name = 'updated'; - const scopes = ['write']; - const requestBody = { name, scopes }; - - const apiResponse = { id: tokenId } as TokenGetResponseJsonApi; - const mapped = { id: tokenId } as TokenModel; - - (TokenMapper.toRequest as jest.Mock).mockReturnValue(requestBody); - (TokenMapper.fromGetResponse as jest.Mock).mockReturnValue(mapped); - jsonApiServiceMock.patch.mockReturnValue(of(apiResponse)); - - service.updateToken(tokenId, name, scopes).subscribe((token) => { - expect(jsonApiServiceMock.patch).toHaveBeenCalledWith( - `${environment.apiDomainUrl}/v2/tokens/${tokenId}/`, - requestBody - ); - expect(token).toEqual(mapped); - done(); + service.getTokenById('token-error').subscribe({ + next: () => {}, + error: (error) => { + errorStatus = error.status; + }, }); - }); - it('deleteToken should call jsonApiService.delete with correct URL', (done) => { - const tokenId = 'delete-me'; - jsonApiServiceMock.delete.mockReturnValue(of(void 0)); + const req = httpMock.expectOne(`${apiBase}/tokens/token-error/`); + req.flush({ errors: [{ detail: 'boom' }] }, { status: 500, statusText: 'Server Error' }); - service.deleteToken(tokenId).subscribe((result) => { - expect(jsonApiServiceMock.delete).toHaveBeenCalledWith(`${environment.apiDomainUrl}/v2/tokens/${tokenId}/`); - expect(result).toBeUndefined(); - done(); - }); - }); + expect(errorStatus).toBe(500); + httpMock.verify(); + })); }); diff --git a/src/app/features/settings/tokens/tokens.component.spec.ts b/src/app/features/settings/tokens/tokens.component.spec.ts index 81759e221..6f778ae33 100644 --- a/src/app/features/settings/tokens/tokens.component.spec.ts +++ b/src/app/features/settings/tokens/tokens.component.spec.ts @@ -1,62 +1,88 @@ import { Store } from '@ngxs/store'; -import { MockComponent, MockProvider } from 'ng-mocks'; - -import { DialogService } from 'primeng/dynamicdialog'; +import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { TokenAddEditFormComponent } from './components'; import { GetScopes } from './store'; import { TokensComponent } from './tokens.component'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; -import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; -import { DialogServiceMockBuilder } from '@testing/providers/dialog-provider.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { CustomDialogServiceMock, CustomDialogServiceMockType } from '@testing/providers/custom-dialog-provider.mock'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +interface SetupOverrides { + url?: string; +} describe('TokensComponent', () => { let component: TokensComponent; let fixture: ComponentFixture; - let mockCustomDialogService: ReturnType; - let mockDialogService: ReturnType; + let store: Store; + let customDialogService: CustomDialogServiceMockType; + let routerMock: RouterMockType; - beforeEach(async () => { - mockCustomDialogService = CustomDialogServiceMockBuilder.create().withOpen(jest.fn()).build(); - mockDialogService = DialogServiceMockBuilder.create().withOpenMock().build(); + function setup(overrides: SetupOverrides = {}) { + customDialogService = CustomDialogServiceMock.simple(); - await TestBed.configureTestingModule({ - imports: [TokensComponent, OSFTestingModule, MockComponent(SubHeaderComponent)], + routerMock = RouterMockBuilder.create() + .withUrl(overrides.url ?? '/settings/tokens') + .build(); + + TestBed.configureTestingModule({ + imports: [TokensComponent, ...MockComponents(SubHeaderComponent)], providers: [ - MockProvider(Store, MOCK_STORE), - MockProvider(CustomDialogService, mockCustomDialogService), - MockProvider(DialogService, mockDialogService), + provideOSFCore(), + MockProvider(Router, routerMock), + MockProvider(CustomDialogService, customDialogService), + provideMockStore(), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); fixture = TestBed.createComponent(TokensComponent); component = fixture.componentInstance; - (MOCK_STORE.dispatch as jest.Mock).mockClear(); fixture.detectChanges(); - }); + } it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should dispatch getScopes on init', () => { - expect(MOCK_STORE.dispatch).toHaveBeenCalledWith(new GetScopes()); + it('should dispatch get scopes on init', () => { + setup(); + + expect(store.dispatch).toHaveBeenCalledWith(new GetScopes()); }); - it('should open create token dialog with correct config', () => { + it('should open create token dialog with expected config', () => { + setup(); + component.createToken(); - expect(mockCustomDialogService.open).toHaveBeenCalledWith( - expect.any(Function), - expect.objectContaining({ - header: 'settings.tokens.form.createTitle', - }) - ); + + expect(customDialogService.open).toHaveBeenCalledWith(TokenAddEditFormComponent, { + header: 'settings.tokens.form.createTitle', + width: '800px', + }); + }); + + it('should set isBaseRoute true initially for base tokens route', () => { + setup({ url: '/settings/tokens' }); + + expect(component.isBaseRoute()).toBe(true); + }); + + it('should set isBaseRoute false initially for nested tokens route', () => { + setup({ url: '/settings/tokens/token-1' }); + + expect(component.isBaseRoute()).toBe(false); }); }); diff --git a/src/app/features/static/privacy-policy/privacy-policy.component.spec.ts b/src/app/features/static/privacy-policy/privacy-policy.component.spec.ts index 54a75e804..0884896d4 100644 --- a/src/app/features/static/privacy-policy/privacy-policy.component.spec.ts +++ b/src/app/features/static/privacy-policy/privacy-policy.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PrivacyPolicyComponent } from './privacy-policy.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('PrivacyPolicyComponent', () => { let component: PrivacyPolicyComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe('PrivacyPolicyComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [PrivacyPolicyComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(PrivacyPolicyComponent); diff --git a/src/app/features/static/terms-of-use/terms-of-use.component.spec.ts b/src/app/features/static/terms-of-use/terms-of-use.component.spec.ts index 0aac8e8cf..d36984ea0 100644 --- a/src/app/features/static/terms-of-use/terms-of-use.component.spec.ts +++ b/src/app/features/static/terms-of-use/terms-of-use.component.spec.ts @@ -3,6 +3,8 @@ import { By } from '@angular/platform-browser'; import { TermsOfUseComponent } from './terms-of-use.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('TermsOfUseComponent', () => { let component: TermsOfUseComponent; let fixture: ComponentFixture; @@ -10,6 +12,7 @@ describe('TermsOfUseComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TermsOfUseComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(TermsOfUseComponent); diff --git a/src/app/shared/components/add-project-form/add-project-form.component.spec.ts b/src/app/shared/components/add-project-form/add-project-form.component.spec.ts index cdaf249de..91b92a9c6 100644 --- a/src/app/shared/components/add-project-form/add-project-form.component.spec.ts +++ b/src/app/shared/components/add-project-form/add-project-form.component.spec.ts @@ -21,7 +21,7 @@ import { ProjectSelectorComponent } from '../project-selector/project-selector.c import { AddProjectFormComponent } from './add-project-form.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('AddProjectFormComponent', () => { @@ -54,10 +54,10 @@ describe('AddProjectFormComponent', () => { await TestBed.configureTestingModule({ imports: [ AddProjectFormComponent, - OSFTestingModule, ...MockComponents(AffiliatedInstitutionSelectComponent, ProjectSelectorComponent), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { diff --git a/src/app/shared/components/addons/addon-card-list/addon-card-list.component.spec.ts b/src/app/shared/components/addons/addon-card-list/addon-card-list.component.spec.ts index 3b1edca54..e40a87642 100644 --- a/src/app/shared/components/addons/addon-card-list/addon-card-list.component.spec.ts +++ b/src/app/shared/components/addons/addon-card-list/addon-card-list.component.spec.ts @@ -6,7 +6,7 @@ import { AddonCardComponent } from '../addon-card/addon-card.component'; import { AddonCardListComponent } from './addon-card-list.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AddonCardListComponent', () => { let component: AddonCardListComponent; @@ -14,7 +14,8 @@ describe('AddonCardListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AddonCardListComponent, MockComponent(AddonCardComponent), OSFTestingModule], + imports: [AddonCardListComponent, MockComponent(AddonCardComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(AddonCardListComponent); diff --git a/src/app/shared/components/addons/addon-card/addon-card.component.spec.ts b/src/app/shared/components/addons/addon-card/addon-card.component.spec.ts index e75532ca7..dafc58ed9 100644 --- a/src/app/shared/components/addons/addon-card/addon-card.component.spec.ts +++ b/src/app/shared/components/addons/addon-card/addon-card.component.spec.ts @@ -9,7 +9,7 @@ import { AddonModel } from '@shared/models/addons/addon.model'; import { AddonCardComponent } from './addon-card.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -36,9 +36,10 @@ describe('AddonCardComponent', () => { customConfirmationServiceMock = CustomConfirmationServiceMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [AddonCardComponent, OSFTestingModule], + imports: [AddonCardComponent], providers: [ - provideMockStore({}), + provideOSFCore(), + provideMockStore(), MockProvider(Router, mockRouter), MockProvider(CustomConfirmationService, customConfirmationServiceMock), ], diff --git a/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts b/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts index defb44471..1185fb7b4 100644 --- a/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts +++ b/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts @@ -10,7 +10,7 @@ import { AddonSetupAccountFormComponent } from './addon-setup-account-form.compo import { MOCK_ADDON } from '@testing/mocks/addon.mock'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AddonSetupAccountFormComponent', () => { let component: AddonSetupAccountFormComponent; @@ -24,8 +24,8 @@ describe('AddonSetupAccountFormComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AddonSetupAccountFormComponent, OSFTestingModule], - providers: [MockProvider(AddonFormService, mockAddonFormService)], + imports: [AddonSetupAccountFormComponent], + providers: [provideOSFCore(), MockProvider(AddonFormService, mockAddonFormService)], }).compileComponents(); fixture = TestBed.createComponent(AddonSetupAccountFormComponent); diff --git a/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts b/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts index fbfeaea93..8376248c1 100644 --- a/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts +++ b/src/app/shared/components/addons/addon-terms/addon-terms.component.spec.ts @@ -8,7 +8,7 @@ import { AddonTerm } from '@osf/shared/models/addons/addon-utils.model'; import { AddonTermsComponent } from './addon-terms.component'; import { MOCK_ADDON } from '@testing/mocks/addon.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; jest.mock('@shared/helpers/addon-type.helper.ts', () => ({ isCitationAddon: jest.fn(), @@ -24,7 +24,8 @@ describe('AddonTermsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AddonTermsComponent, OSFTestingModule], + imports: [AddonTermsComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(AddonTermsComponent); diff --git a/src/app/shared/components/addons/addons-toolbar/addons-toolbar.component.spec.ts b/src/app/shared/components/addons/addons-toolbar/addons-toolbar.component.spec.ts index 2c394c908..2c3ea2ed9 100644 --- a/src/app/shared/components/addons/addons-toolbar/addons-toolbar.component.spec.ts +++ b/src/app/shared/components/addons/addons-toolbar/addons-toolbar.component.spec.ts @@ -9,7 +9,7 @@ import { SelectComponent } from '../../select/select.component'; import { AddonsToolbarComponent } from './addons-toolbar.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('AddonsToolbarComponent', () => { @@ -21,8 +21,8 @@ describe('AddonsToolbarComponent', () => { activatedRouteMock = ActivatedRouteMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [AddonsToolbarComponent, OSFTestingModule, ...MockComponents(SearchInputComponent, SelectComponent)], - providers: [MockProvider(ActivatedRoute, activatedRouteMock)], + imports: [AddonsToolbarComponent, ...MockComponents(SearchInputComponent, SelectComponent)], + providers: [provideOSFCore(), MockProvider(ActivatedRoute, activatedRouteMock)], }).compileComponents(); fixture = TestBed.createComponent(AddonsToolbarComponent); diff --git a/src/app/shared/components/addons/resource-type-info-dialog/resource-type-info-dialog.component.spec.ts b/src/app/shared/components/addons/resource-type-info-dialog/resource-type-info-dialog.component.spec.ts index ca67edef6..0a8e90a50 100644 --- a/src/app/shared/components/addons/resource-type-info-dialog/resource-type-info-dialog.component.spec.ts +++ b/src/app/shared/components/addons/resource-type-info-dialog/resource-type-info-dialog.component.spec.ts @@ -6,7 +6,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ResourceTypeInfoDialogComponent } from './resource-type-info-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResourceTypeInfoDialogComponent', () => { let component: ResourceTypeInfoDialogComponent; @@ -14,8 +14,8 @@ describe('ResourceTypeInfoDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ResourceTypeInfoDialogComponent, OSFTestingModule], - providers: [MockProvider(DynamicDialogRef)], + imports: [ResourceTypeInfoDialogComponent], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef)], }).compileComponents(); fixture = TestBed.createComponent(ResourceTypeInfoDialogComponent); diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts index f3961bd7d..2d0c562b1 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.spec.ts @@ -14,7 +14,7 @@ import { SelectComponent } from '../../select/select.component'; import { StorageItemSelectorComponent } from './storage-item-selector.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { DialogServiceMockBuilder } from '@testing/providers/dialog-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -29,12 +29,9 @@ describe('StorageItemSelectorComponent', () => { mockOperationInvocation = signal(null); await TestBed.configureTestingModule({ - imports: [ - StorageItemSelectorComponent, - OSFTestingModule, - ...MockComponents(GoogleFilePickerComponent, SelectComponent), - ], + imports: [StorageItemSelectorComponent, ...MockComponents(GoogleFilePickerComponent, SelectComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { diff --git a/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts b/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts index cddac3c53..9b3e7f809 100644 --- a/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts +++ b/src/app/shared/components/affiliated-institution-select/affiliated-institution-select.component.spec.ts @@ -5,7 +5,7 @@ import { Institution } from '@osf/shared/models/institutions/institutions.model' import { AffiliatedInstitutionSelectComponent } from './affiliated-institution-select.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AffiliatedInstitutionSelectComponent', () => { let component: AffiliatedInstitutionSelectComponent; @@ -15,7 +15,8 @@ describe('AffiliatedInstitutionSelectComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AffiliatedInstitutionSelectComponent, OSFTestingModule], + imports: [AffiliatedInstitutionSelectComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(AffiliatedInstitutionSelectComponent); diff --git a/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts b/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts index 4724ab97a..1f9cfcb12 100644 --- a/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts +++ b/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts @@ -1,11 +1,12 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { Institution } from '@osf/shared/models/institutions/institutions.model'; import { AffiliatedInstitutionsViewComponent } from './affiliated-institutions-view.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AffiliatedInstitutionsViewComponent', () => { let component: AffiliatedInstitutionsViewComponent; @@ -15,7 +16,8 @@ describe('AffiliatedInstitutionsViewComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AffiliatedInstitutionsViewComponent, OSFTestingModule], + imports: [AffiliatedInstitutionsViewComponent], + providers: [provideOSFCore(), provideRouter([])], }).compileComponents(); fixture = TestBed.createComponent(AffiliatedInstitutionsViewComponent); diff --git a/src/app/shared/components/bar-chart/bar-chart.component.spec.ts b/src/app/shared/components/bar-chart/bar-chart.component.spec.ts index dee95b220..419bbf4ff 100644 --- a/src/app/shared/components/bar-chart/bar-chart.component.spec.ts +++ b/src/app/shared/components/bar-chart/bar-chart.component.spec.ts @@ -6,7 +6,7 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { BarChartComponent } from './bar-chart.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('BarChartComponent', () => { let component: BarChartComponent; @@ -14,7 +14,8 @@ describe('BarChartComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [BarChartComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], + imports: [BarChartComponent, MockComponent(LoadingSpinnerComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(BarChartComponent); diff --git a/src/app/shared/components/component-checkbox-item/component-checkbox-item.component.spec.ts b/src/app/shared/components/component-checkbox-item/component-checkbox-item.component.spec.ts index 11f2c131a..85eecda20 100644 --- a/src/app/shared/components/component-checkbox-item/component-checkbox-item.component.spec.ts +++ b/src/app/shared/components/component-checkbox-item/component-checkbox-item.component.spec.ts @@ -6,6 +6,8 @@ import { InfoIconComponent } from '../info-icon/info-icon.component'; import { ComponentCheckboxItemComponent } from './component-checkbox-item.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ComponentCheckboxItemComponent', () => { let component: ComponentCheckboxItemComponent; let fixture: ComponentFixture; @@ -13,6 +15,7 @@ describe('ComponentCheckboxItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ComponentCheckboxItemComponent, MockComponent(InfoIconComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ComponentCheckboxItemComponent); diff --git a/src/app/shared/components/components-selection-list/components-selection-list.component.spec.ts b/src/app/shared/components/components-selection-list/components-selection-list.component.spec.ts index db5bc73b8..293254e39 100644 --- a/src/app/shared/components/components-selection-list/components-selection-list.component.spec.ts +++ b/src/app/shared/components/components-selection-list/components-selection-list.component.spec.ts @@ -8,7 +8,7 @@ import { ComponentCheckboxItemComponent } from '../component-checkbox-item/compo import { ComponentsSelectionListComponent } from './components-selection-list.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ComponentsSelectionListComponent', () => { let component: ComponentsSelectionListComponent; @@ -22,7 +22,8 @@ describe('ComponentsSelectionListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ComponentsSelectionListComponent, OSFTestingModule, MockComponent(ComponentCheckboxItemComponent)], + imports: [ComponentsSelectionListComponent, MockComponent(ComponentCheckboxItemComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ComponentsSelectionListComponent); diff --git a/src/app/shared/components/confirm-email/confirm-email.component.spec.ts b/src/app/shared/components/confirm-email/confirm-email.component.spec.ts index 63d1a186d..c60ebba1a 100644 --- a/src/app/shared/components/confirm-email/confirm-email.component.spec.ts +++ b/src/app/shared/components/confirm-email/confirm-email.component.spec.ts @@ -1,128 +1,182 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; -import { signal } from '@angular/core'; +import { throwError } from 'rxjs'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { UserEmailsSelectors } from '@core/store/user-emails'; +import { DeleteEmail, UserEmailsSelectors, VerifyEmail } from '@core/store/user-emails'; +import { AccountEmailModel } from '@osf/shared/models/emails/account-email.model'; import { ToastService } from '@osf/shared/services/toast.service'; -import { AccountEmailModel } from '@shared/models/emails/account-email.model'; - -import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; - -import { ConfirmEmailComponent } from './confirm-email.component'; +import { ConfirmEmailComponent } from '@shared/components/confirm-email/confirm-email.component'; -import { DynamicDialogRefMock } from '@testing/mocks/dynamic-dialog-ref.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; -import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; +import { ToastServiceMock, ToastServiceMockType } from '@testing/providers/toast-provider.mock'; describe('ConfirmEmailComponent', () => { let component: ConfirmEmailComponent; let fixture: ComponentFixture; - let mockToastService: ReturnType; - - const mockEmail: AccountEmailModel = { - id: 'email-123', - emailAddress: 'test@example.com', - confirmed: false, - verified: false, - primary: false, - isMerge: false, - }; - - beforeEach(async () => { - jest.useFakeTimers(); - - mockToastService = ToastServiceMockBuilder.create().build(); - - await TestBed.configureTestingModule({ - imports: [ConfirmEmailComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], + let store: Store; + let dialogRef: DynamicDialogRef; + let toastService: ToastServiceMockType; + + interface SetupOverrides { + email?: AccountEmailModel; + } + + function buildEmail(overrides: Partial = {}): AccountEmailModel { + return { + id: 'email-1', + emailAddress: 'user@example.com', + confirmed: false, + verified: false, + primary: false, + isMerge: false, + ...overrides, + }; + } + + function setup(overrides: SetupOverrides = {}) { + toastService = ToastServiceMock.simple(); + const email = overrides.email ?? buildEmail(); + + TestBed.configureTestingModule({ + imports: [ConfirmEmailComponent], providers: [ + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { data: [email] }), + MockProvider(ToastService, toastService), provideMockStore({ - signals: [{ selector: UserEmailsSelectors.isEmailsSubmitting, value: signal(false) }], + signals: [{ selector: UserEmailsSelectors.isEmailsSubmitting, value: false }], }), - DynamicDialogRefMock, - MockProvider(DynamicDialogConfig, { - data: [mockEmail], - }), - MockProvider(ToastService, mockToastService), ], - }).compileComponents(); + }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(ConfirmEmailComponent); component = fixture.componentInstance; fixture.detectChanges(); - }); - - afterEach(() => { - jest.useRealTimers(); - }); + } it('should create', () => { + setup(); expect(component).toBeTruthy(); }); - it('should return email from config data', () => { - expect(component.email).toEqual(mockEmail); - expect(component.email.id).toBe('email-123'); - expect(component.email.emailAddress).toBe('test@example.com'); + it('should expose email from dialog config data', () => { + const email = buildEmail({ id: 'email-2' }); + setup({ email }); + expect(component.email).toEqual(email); }); - it('should have isSubmitting signal from store', () => { - expect(component.isSubmitting()).toBe(false); + it('should dispatch delete email and show success for add flow', () => { + const email = buildEmail({ isMerge: false }); + setup({ email }); + + component.closeDialog(); + + expect(store.dispatch).toHaveBeenCalledWith(new DeleteEmail(email.id)); + expect(toastService.showSuccess).toHaveBeenCalledWith('home.confirmEmail.add.emailNotAdded', { + name: email.emailAddress, + }); + expect(dialogRef.close).toHaveBeenCalled(); }); - it('should show success toast with email address', () => { + it('should show error for delete email failure in add flow', () => { + const email = buildEmail({ isMerge: false }); + setup({ email }); + (store.dispatch as jest.Mock).mockReturnValueOnce(throwError(() => new Error('delete failed'))); + component.closeDialog(); - jest.runAllTimers(); - expect(mockToastService.showSuccess).toHaveBeenCalledWith('home.confirmEmail.add.emailNotAdded', { - name: mockEmail.emailAddress, + expect(toastService.showError).toHaveBeenCalledWith('home.confirmEmail.add.denyError', { + name: email.emailAddress, }); + expect(dialogRef.close).toHaveBeenCalled(); }); - it('should close dialog after successful deletion', () => { - const mockDialogRef = TestBed.inject(DynamicDialogRef); + it('should dispatch delete email and show success for merge flow', () => { + const email = buildEmail({ isMerge: true }); + setup({ email }); component.closeDialog(); - jest.runAllTimers(); - expect(mockDialogRef.close).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new DeleteEmail(email.id)); + expect(toastService.showSuccess).toHaveBeenCalledWith('home.confirmEmail.merge.emailNotAdded', { + name: email.emailAddress, + }); + expect(dialogRef.close).toHaveBeenCalled(); }); - it('should call verifyEmail action without errors', () => { - expect(() => component.verifyEmail()).not.toThrow(); + it('should show error for delete email failure in merge flow', () => { + const email = buildEmail({ isMerge: true }); + setup({ email }); + (store.dispatch as jest.Mock).mockReturnValueOnce(throwError(() => new Error('delete failed'))); + + component.closeDialog(); + + expect(toastService.showError).toHaveBeenCalledWith('home.confirmEmail.merge.denyError', { + name: email.emailAddress, + }); + expect(dialogRef.close).toHaveBeenCalled(); }); - it('should show success toast on successful verification', () => { + it('should dispatch verify email and show success for add flow', () => { + const email = buildEmail({ isMerge: false }); + setup({ email }); + component.verifyEmail(); - jest.runAllTimers(); - expect(mockToastService.showSuccess).toHaveBeenCalledWith('home.confirmEmail.add.emailVerified', { - name: mockEmail.emailAddress, + expect(store.dispatch).toHaveBeenCalledWith(new VerifyEmail(email.id)); + expect(toastService.showSuccess).toHaveBeenCalledWith('home.confirmEmail.add.emailVerified', { + name: email.emailAddress, }); + expect(dialogRef.close).toHaveBeenCalled(); }); - it('should close dialog after successful verification', () => { - const mockDialogRef = TestBed.inject(DynamicDialogRef); + it('should show error for verify email failure in add flow', () => { + const email = buildEmail({ isMerge: false }); + setup({ email }); + (store.dispatch as jest.Mock).mockReturnValueOnce(throwError(() => new Error('verify failed'))); component.verifyEmail(); - jest.runAllTimers(); - expect(mockDialogRef.close).toHaveBeenCalled(); + expect(toastService.showError).toHaveBeenCalledWith('home.confirmEmail.add.verifyError', { + name: email.emailAddress, + }); + expect(dialogRef.close).toHaveBeenCalled(); }); - it('should close dialog on error without showing success toast', () => { - const mockDialogRef = TestBed.inject(DynamicDialogRef); + it('should dispatch verify email and show success for merge flow', () => { + const email = buildEmail({ isMerge: true }); + setup({ email }); - mockToastService.showSuccess.mockClear(); - (mockDialogRef.close as jest.Mock).mockClear(); + component.verifyEmail(); + + expect(store.dispatch).toHaveBeenCalledWith(new VerifyEmail(email.id)); + expect(toastService.showSuccess).toHaveBeenCalledWith('home.confirmEmail.merge.emailVerified', { + name: email.emailAddress, + }); + expect(dialogRef.close).toHaveBeenCalled(); + }); + + it('should show error for verify email failure in merge flow', () => { + const email = buildEmail({ isMerge: true }); + setup({ email }); + (store.dispatch as jest.Mock).mockReturnValueOnce(throwError(() => new Error('verify failed'))); component.verifyEmail(); - jest.runAllTimers(); - expect(mockDialogRef.close).toHaveBeenCalled(); + expect(toastService.showError).toHaveBeenCalledWith('home.confirmEmail.merge.verifyError', { + name: email.emailAddress, + }); + expect(dialogRef.close).toHaveBeenCalled(); }); }); diff --git a/src/app/shared/components/confirm-email/confirm-email.component.ts b/src/app/shared/components/confirm-email/confirm-email.component.ts index 0c34d813c..24a286a63 100644 --- a/src/app/shared/components/confirm-email/confirm-email.component.ts +++ b/src/app/shared/components/confirm-email/confirm-email.component.ts @@ -7,7 +7,6 @@ import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormsModule } from '@angular/forms'; import { DeleteEmail, UserEmailsSelectors, VerifyEmail } from '@core/store/user-emails'; import { AccountEmailModel } from '@osf/shared/models/emails/account-email.model'; @@ -17,7 +16,7 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp @Component({ selector: 'osf-confirm-email', - imports: [Button, FormsModule, TranslatePipe, LoadingSpinnerComponent], + imports: [Button, TranslatePipe, LoadingSpinnerComponent], templateUrl: './confirm-email.component.html', styleUrl: './confirm-email.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/shared/components/contributors-list-shortener/contributors-list-shortener.component.spec.ts b/src/app/shared/components/contributors-list-shortener/contributors-list-shortener.component.spec.ts index 28390cbf9..679188000 100644 --- a/src/app/shared/components/contributors-list-shortener/contributors-list-shortener.component.spec.ts +++ b/src/app/shared/components/contributors-list-shortener/contributors-list-shortener.component.spec.ts @@ -4,6 +4,8 @@ import { By } from '@angular/platform-browser'; import { ContributorsListShortenerComponent } from './contributors-list-shortener.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('ContributorsListShortenerComponent', () => { let component: ContributorsListShortenerComponent; let fixture: ComponentFixture; @@ -12,6 +14,7 @@ describe('ContributorsListShortenerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ContributorsListShortenerComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ContributorsListShortenerComponent); diff --git a/src/app/shared/components/contributors-list/contributors-list.component.spec.ts b/src/app/shared/components/contributors-list/contributors-list.component.spec.ts index b45612c08..1090284de 100644 --- a/src/app/shared/components/contributors-list/contributors-list.component.spec.ts +++ b/src/app/shared/components/contributors-list/contributors-list.component.spec.ts @@ -1,28 +1,51 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; -import { ContributorsListComponent } from './contributors-list.component'; +import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; +import { ContributorModel } from '@osf/shared/models/contributors/contributor.model'; -import { MOCK_CONTRIBUTOR } from '@testing/mocks/contributors.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ContributorsListComponent', () => { let component: ContributorsListComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ContributorsListComponent, OSFTestingModule], - }).compileComponents(); - + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ContributorsListComponent], + providers: [provideOSFCore(), provideRouter([])], + }); fixture = TestBed.createComponent(ContributorsListComponent); component = fixture.componentInstance; - - fixture.componentRef.setInput('contributors', [MOCK_CONTRIBUTOR]); - + fixture.componentRef.setInput('contributors', []); fixture.detectChanges(); }); + function setContributors(contributors: ContributorModel[] | Partial[]) { + fixture.componentRef.setInput('contributors', contributors); + fixture.detectChanges(); + } + it('should create', () => { expect(component).toBeTruthy(); }); + + it('should have false default values for optional inputs', () => { + expect(component.isLoading()).toBe(false); + expect(component.hasLoadMore()).toBe(false); + expect(component.readonly()).toBe(false); + expect(component.anonymous()).toBe(false); + }); + + it('should accept contributors input', () => { + const contributors: Partial[] = [{ id: '1', userId: 'u1', fullName: 'User One' }]; + setContributors(contributors); + expect(component.contributors()).toEqual(contributors); + }); + + it('should emit load more event', () => { + const emitSpy = jest.spyOn(component.loadMoreContributors, 'emit'); + component.loadMoreContributors.emit(); + expect(emitSpy).toHaveBeenCalled(); + }); }); diff --git a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.spec.ts b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.spec.ts index 943aa2603..6e353a4e9 100644 --- a/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.spec.ts +++ b/src/app/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component.spec.ts @@ -1,183 +1,181 @@ import { Store } from '@ngxs/store'; -import { MockComponents } from 'ng-mocks'; +import { TranslatePipe } from '@ngx-translate/core'; +import { MockPipe, MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { PaginatorState } from 'primeng/paginator'; -import { signal } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AddContributorDialogComponent } from '@osf/shared/components/contributors/add-contributor-dialog/add-contributor-dialog.component'; import { AddContributorType } from '@osf/shared/enums/contributors/add-contributor-type.enum'; import { AddDialogState } from '@osf/shared/enums/contributors/add-dialog-state.enum'; -import { AddContributorItemComponent } from '@shared/components/contributors/add-contributor-item/add-contributor-item.component'; -import { ContributorsSelectors } from '@shared/stores/contributors'; - -import { ComponentsSelectionListComponent } from '../../components-selection-list/components-selection-list.component'; -import { CustomPaginatorComponent } from '../../custom-paginator/custom-paginator.component'; -import { LoadingSpinnerComponent } from '../../loading-spinner/loading-spinner.component'; -import { SearchInputComponent } from '../../search-input/search-input.component'; - -import { AddContributorDialogComponent } from './add-contributor-dialog.component'; - -import { - MOCK_COMPONENT_CHECKBOX_ITEM, - MOCK_COMPONENT_CHECKBOX_ITEM_CURRENT, - MOCK_CONTRIBUTOR_ADD, - MOCK_CONTRIBUTOR_ADD_DISABLED, -} from '@testing/mocks/contributors.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { ComponentCheckboxItemModel } from '@shared/models/component-checkbox-item.model'; +import { ContributorAddModel } from '@shared/models/contributors/contributor-add.model'; +import { ClearUsers, ContributorsSelectors, SearchUsers, SearchUsersPageChange } from '@shared/stores/contributors'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('AddContributorDialogComponent', () => { let component: AddContributorDialogComponent; let fixture: ComponentFixture; - let dialogRef: jest.Mocked; - let dialogConfig: DynamicDialogConfig; let store: Store; - - beforeEach(async () => { - dialogRef = { - close: jest.fn(), - } as any; - - dialogConfig = { - data: {}, - } as DynamicDialogConfig; - - await TestBed.configureTestingModule({ - imports: [ - AddContributorDialogComponent, - OSFTestingModule, - ...MockComponents( - SearchInputComponent, - LoadingSpinnerComponent, - CustomPaginatorComponent, - AddContributorItemComponent, - ComponentsSelectionListComponent - ), - ], + let dialogRef: DynamicDialogRef; + + interface SetupOverrides { + data?: { + components: ComponentCheckboxItemModel[]; + resourceName: string; + parentResourceName: string; + allowAddingContributorsFromParentProject: boolean; + }; + usersNextLink?: string | null; + usersPreviousLink?: string | null; + } + + const defaultComponents: ComponentCheckboxItemModel[] = [ + { id: 'root', title: 'Root', checked: true, disabled: false, isCurrent: true }, + { id: 'child-1', title: 'Child 1', checked: true, disabled: false }, + { id: 'child-2', title: 'Child 2', checked: false, disabled: false }, + ]; + + const defaultDialogData = { + components: defaultComponents, + resourceName: 'Project A', + parentResourceName: 'Parent Project', + allowAddingContributorsFromParentProject: true, + }; + + function setup(overrides: SetupOverrides = {}) { + const usersNextLink = 'usersNextLink' in overrides ? overrides.usersNextLink : 'next-link'; + const usersPreviousLink = 'usersPreviousLink' in overrides ? overrides.usersPreviousLink : 'prev-link'; + + TestBed.configureTestingModule({ + imports: [AddContributorDialogComponent], providers: [ + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { data: overrides.data ?? defaultDialogData }), provideMockStore({ signals: [ - { selector: ContributorsSelectors.getUsers, value: signal([]) }, + { selector: ContributorsSelectors.getUsers, value: [] }, { selector: ContributorsSelectors.isUsersLoading, value: false }, { selector: ContributorsSelectors.getUsersTotalCount, value: 0 }, - { selector: ContributorsSelectors.getUsersNextLink, value: signal(null) }, - { selector: ContributorsSelectors.getUsersPreviousLink, value: signal(null) }, + { selector: ContributorsSelectors.getUsersNextLink, value: usersNextLink }, + { selector: ContributorsSelectors.getUsersPreviousLink, value: usersPreviousLink }, ], }), - { provide: DynamicDialogRef, useValue: dialogRef }, - { provide: DynamicDialogConfig, useValue: dialogConfig }, ], - }).compileComponents(); + }); + + TestBed.overrideComponent(AddContributorDialogComponent, { + remove: { imports: [TranslatePipe] }, + add: { imports: [MockPipe(TranslatePipe)] }, + }); store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(AddContributorDialogComponent); component = fixture.componentInstance; - }); + fixture.detectChanges(); + (store.dispatch as jest.Mock).mockClear(); + } it('should create', () => { + setup(); expect(component).toBeTruthy(); }); - it('should initialize with default values', () => { - expect(component.currentState()).toBe(AddDialogState.Search); - expect(component.isInitialState()).toBe(true); - expect(component.selectedUsers()).toEqual([]); - }); - it('should initialize dialog data from config', () => { - const mockComponents = [MOCK_COMPONENT_CHECKBOX_ITEM]; - dialogConfig.data = { - components: mockComponents, - resourceName: 'Test Resource', - parentResourceName: 'Parent Resource', - allowAddingContributorsFromParentProject: true, - }; - - fixture = TestBed.createComponent(AddContributorDialogComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - - expect(component.components()).toEqual(mockComponents); - expect(component.resourceName()).toBe('Test Resource'); + setup(); + expect(component.components()).toEqual(defaultComponents); + expect(component.resourceName()).toBe('Project A'); + expect(component.parentResourceName()).toBe('Parent Project'); + expect(component.allowAddingContributorsFromParentProject()).toBe(true); }); - it('should compute contributorNames correctly', () => { - component.selectedUsers.set([MOCK_CONTRIBUTOR_ADD, MOCK_CONTRIBUTOR_ADD_DISABLED]); - expect(component.contributorNames()).toBe('John Doe, Jane Smith'); + it('should clear users on destroy', () => { + setup(); + component.ngOnDestroy(); + expect(store.dispatch).toHaveBeenCalledWith(new ClearUsers()); }); - it('should compute state flags correctly', () => { + it('should move from search to details state on addContributor', () => { + setup(); component.currentState.set(AddDialogState.Search); - expect(component.isSearchState()).toBe(true); - expect(component.isDetailsState()).toBe(false); - - component.currentState.set(AddDialogState.Details); - expect(component.isDetailsState()).toBe(true); - expect(component.isSearchState()).toBe(false); - }); - - it('should compute hasComponents correctly', () => { - component.components.set([MOCK_COMPONENT_CHECKBOX_ITEM, MOCK_COMPONENT_CHECKBOX_ITEM_CURRENT]); - expect(component.hasComponents()).toBe(true); - - component.components.set([MOCK_COMPONENT_CHECKBOX_ITEM]); - expect(component.hasComponents()).toBe(false); + component.addContributor(); + expect(component.currentState()).toBe(AddDialogState.Details); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should compute buttonLabel based on state and components', () => { - component.currentState.set(AddDialogState.Search); - expect(component.buttonLabel()).toBe('common.buttons.next'); - + it('should close with registered contributors in details state when no additional components', () => { + setup({ + data: { + ...defaultDialogData, + components: [{ id: 'root', title: 'Root', checked: true, disabled: false, isCurrent: true }], + }, + }); + const users: ContributorAddModel[] = [ + { id: '1', fullName: 'A User', permission: 'write', isBibliographic: true, disabled: false }, + { id: '2', fullName: 'Disabled User', permission: 'read', isBibliographic: true, disabled: true }, + ]; + component.selectedUsers.set(users); component.currentState.set(AddDialogState.Details); - component.components.set([]); - expect(component.buttonLabel()).toBe('common.buttons.done'); - component.components.set([MOCK_COMPONENT_CHECKBOX_ITEM, MOCK_COMPONENT_CHECKBOX_ITEM_CURRENT]); - expect(component.buttonLabel()).toBe('common.buttons.next'); + component.addContributor(); - component.currentState.set(AddDialogState.Components); - expect(component.buttonLabel()).toBe('common.buttons.done'); + expect(component.currentState()).toBe(AddDialogState.Search); + expect(dialogRef.close).toHaveBeenCalledWith({ + data: [{ id: '1', fullName: 'A User', permission: 'write', isBibliographic: true, disabled: false }], + type: AddContributorType.Registered, + childNodeIds: undefined, + }); }); - it('should transition states and close dialog appropriately', () => { - component.currentState.set(AddDialogState.Search); - component.addContributor(); - expect(component.currentState()).toBe(AddDialogState.Details); - + it('should move from details to components when there are multiple components', () => { + setup(); component.currentState.set(AddDialogState.Details); - component.components.set([MOCK_COMPONENT_CHECKBOX_ITEM, MOCK_COMPONENT_CHECKBOX_ITEM_CURRENT]); component.addContributor(); expect(component.currentState()).toBe(AddDialogState.Components); + expect(dialogRef.close).not.toHaveBeenCalled(); + }); + + it('should close with selected child component ids in components state', () => { + setup(); + component.selectedUsers.set([ + { id: '1', fullName: 'A User', permission: 'write', isBibliographic: true, disabled: false }, + ]); + component.currentState.set(AddDialogState.Components); - component.currentState.set(AddDialogState.Details); - component.components.set([]); - component.selectedUsers.set([MOCK_CONTRIBUTOR_ADD]); component.addContributor(); + expect(dialogRef.close).toHaveBeenCalledWith({ - data: [MOCK_CONTRIBUTOR_ADD], + data: [{ id: '1', fullName: 'A User', permission: 'write', isBibliographic: true, disabled: false }], type: AddContributorType.Registered, - childNodeIds: undefined, + childNodeIds: ['child-1'], }); - - component.currentState.set(AddDialogState.Components); - component.components.set([{ ...MOCK_COMPONENT_CHECKBOX_ITEM, checked: true }]); - component.addContributor(); - expect(dialogRef.close).toHaveBeenCalledTimes(2); }); - it('should close dialog with correct data for different actions', () => { - component.selectedUsers.set([MOCK_CONTRIBUTOR_ADD]); + it('should close with parent project type', () => { + setup(); + component.selectedUsers.set([ + { id: '1', fullName: 'A User', permission: 'write', isBibliographic: true, disabled: false }, + ]); component.addSourceProjectContributors(); + expect(dialogRef.close).toHaveBeenCalledWith({ - data: [MOCK_CONTRIBUTOR_ADD], + data: [{ id: '1', fullName: 'A User', permission: 'write', isBibliographic: true, disabled: false }], type: AddContributorType.ParentProject, - childNodeIds: undefined, + childNodeIds: ['child-1'], }); + }); + it('should close with unregistered type', () => { + setup(); component.addUnregistered(); expect(dialogRef.close).toHaveBeenCalledWith({ data: [], @@ -185,122 +183,58 @@ describe('AddContributorDialogComponent', () => { }); }); - it('should handle pagination correctly', () => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); + it('should search first page with trimmed value and reset pagination', () => { + setup(); + component.currentPage.set(3); + component.first.set(20); + component.searchControl.setValue(' alice '); - component.pageChanged({ first: 0 } as PaginatorState); - expect(dispatchSpy).not.toHaveBeenCalled(); + component.pageChanged({ page: 0, first: 0 } as PaginatorState); - component.searchControl.setValue('test'); - component.pageChanged({ page: 0, first: 0, rows: 10 } as PaginatorState); - expect(dispatchSpy).toHaveBeenCalled(); + expect(store.dispatch).toHaveBeenCalledWith(new SearchUsers('alice')); expect(component.currentPage()).toBe(1); expect(component.first()).toBe(0); }); - it('should navigate to next page when link is available', () => { - const nextLink = 'http://api.example.com/users?page=3'; - const originalSelect = store.select.bind(store); - (store.select as jest.Mock) = jest.fn((selector) => { - if (selector === ContributorsSelectors.getUsersNextLink) { - return signal(nextLink); - } - return originalSelect(selector); - }); + it('should dispatch page change action when moving to next page with link', () => { + setup({ usersNextLink: 'next-link' }); + component.currentPage.set(1); - Object.defineProperty(component, 'usersNextLink', { - get: () => signal(nextLink), - configurable: true, - }); + component.pageChanged({ page: 1, first: 10 } as PaginatorState); - const dispatchSpy = jest.spyOn(store, 'dispatch'); - component.currentPage.set(2); - component.pageChanged({ page: 2, first: 20, rows: 10 } as PaginatorState); - - expect(dispatchSpy).toHaveBeenCalled(); - expect(component.currentPage()).toBe(3); - expect(component.first()).toBe(20); + expect(store.dispatch).toHaveBeenCalledWith(new SearchUsersPageChange('next-link')); + expect(component.currentPage()).toBe(2); + expect(component.first()).toBe(10); }); - it('should debounce and filter search input', fakeAsync(() => { - fixture.detectChanges(); - const dispatchSpy = jest.spyOn(store, 'dispatch'); - - component.searchControl.setValue('t'); - tick(200); - component.searchControl.setValue('test'); - tick(500); - - expect(dispatchSpy).toHaveBeenCalledTimes(1); - expect(component.isInitialState()).toBe(false); - expect(component.selectedUsers()).toEqual([]); - })); - - it('should not search empty or whitespace values', fakeAsync(() => { - fixture.detectChanges(); - const dispatchSpy = jest.spyOn(store, 'dispatch'); - - component.searchControl.setValue(''); - tick(500); - expect(dispatchSpy).not.toHaveBeenCalled(); - - component.searchControl.setValue(' '); - tick(500); - expect(dispatchSpy).not.toHaveBeenCalled(); - })); - - it('should reset pagination on search', fakeAsync(() => { - fixture.detectChanges(); - component.currentPage.set(3); - component.first.set(20); + it('should not dispatch page change when page link is missing', () => { + setup({ usersNextLink: null }); + component.currentPage.set(1); - component.searchControl.setValue('test'); - tick(500); + component.pageChanged({ page: 1, first: 10 } as PaginatorState); + expect(store.dispatch).not.toHaveBeenCalled(); expect(component.currentPage()).toBe(1); expect(component.first()).toBe(0); - })); - - it('should update selectedUsers from checked users', () => { - const checkedUsers = [MOCK_CONTRIBUTOR_ADD]; - const usersSignal = signal(checkedUsers); - - Object.defineProperty(component, 'users', { - get: () => usersSignal, - configurable: true, - }); - - fixture.detectChanges(); - usersSignal.set(checkedUsers); - fixture.detectChanges(); - - expect(component.selectedUsers().length).toBeGreaterThan(0); }); - it('should filter disabled users and include childNodeIds', () => { - component.selectedUsers.set([MOCK_CONTRIBUTOR_ADD, MOCK_CONTRIBUTOR_ADD_DISABLED]); - component.components.set([]); - component['closeDialogWithData'](); + it('should debounce and deduplicate search control dispatches', () => { + jest.useFakeTimers(); + setup(); + component.selectedUsers.set([ + { id: '1', fullName: 'A User', permission: 'write', isBibliographic: true, disabled: false }, + ]); - expect(dialogRef.close).toHaveBeenCalledWith({ - data: [MOCK_CONTRIBUTOR_ADD], - type: AddContributorType.Registered, - childNodeIds: undefined, - }); + component.searchControl.setValue('john'); + jest.advanceTimersByTime(500); - component.components.set([{ ...MOCK_COMPONENT_CHECKBOX_ITEM, checked: true }]); - component['closeDialogWithData'](AddContributorType.ParentProject); + component.searchControl.setValue('john'); + jest.advanceTimersByTime(500); - expect(dialogRef.close).toHaveBeenCalledWith({ - data: [MOCK_CONTRIBUTOR_ADD], - type: AddContributorType.ParentProject, - childNodeIds: [MOCK_COMPONENT_CHECKBOX_ITEM.id], - }); - }); - - it('should clear users on destroy', () => { - const dispatchSpy = jest.spyOn(store, 'dispatch'); - component.ngOnDestroy(); - expect(dispatchSpy).toHaveBeenCalled(); + const dispatchMock = store.dispatch as jest.Mock; + expect(dispatchMock.mock.calls.filter((call) => call[0] instanceof SearchUsers).length).toBe(1); + expect(component.isInitialState()).toBe(false); + expect(component.selectedUsers()).toEqual([]); + jest.useRealTimers(); }); }); diff --git a/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.spec.ts b/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.spec.ts index e8fe8fb39..dcc79c302 100644 --- a/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.spec.ts +++ b/src/app/shared/components/contributors/add-contributor-item/add-contributor-item.component.spec.ts @@ -4,7 +4,7 @@ import { ContributorAddModel } from '@osf/shared/models/contributors/contributor import { AddContributorItemComponent } from './add-contributor-item.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AddContributorItemComponent', () => { let component: AddContributorItemComponent; @@ -20,7 +20,8 @@ describe('AddContributorItemComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AddContributorItemComponent, OSFTestingModule], + imports: [AddContributorItemComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(AddContributorItemComponent); diff --git a/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.spec.ts b/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.spec.ts index 110925251..c622e20c3 100644 --- a/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.spec.ts +++ b/src/app/shared/components/contributors/add-unregistered-contributor-dialog/add-unregistered-contributor-dialog.component.spec.ts @@ -13,7 +13,7 @@ import { TextInputComponent } from '../../text-input/text-input.component'; import { AddUnregisteredContributorDialogComponent } from './add-unregistered-contributor-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('AddUnregisteredContributorDialogComponent', () => { let component: AddUnregisteredContributorDialogComponent; @@ -23,8 +23,8 @@ describe('AddUnregisteredContributorDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [AddUnregisteredContributorDialogComponent, OSFTestingModule, MockComponent(TextInputComponent)], - providers: [MockProviders(DynamicDialogRef)], + imports: [AddUnregisteredContributorDialogComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore(), MockProviders(DynamicDialogRef)], }).compileComponents(); fixture = TestBed.createComponent(AddUnregisteredContributorDialogComponent); diff --git a/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts b/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts index ec825b314..789327985 100644 --- a/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts +++ b/src/app/shared/components/contributors/contributors-table/contributors-table.component.spec.ts @@ -15,7 +15,7 @@ import { SelectComponent } from '../../select/select.component'; import { ContributorsTableComponent } from './contributors-table.component'; import { MOCK_CONTRIBUTOR, MOCK_CONTRIBUTOR_WITHOUT_HISTORY } from '@testing/mocks/contributors.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { DialogServiceMockBuilder } from '@testing/providers/dialog-provider.mock'; describe('ContributorsTableComponent', () => { @@ -38,12 +38,8 @@ describe('ContributorsTableComponent', () => { mockDialogService = DialogServiceMockBuilder.create().withOpenMock().build(); await TestBed.configureTestingModule({ - imports: [ - ContributorsTableComponent, - OSFTestingModule, - ...MockComponents(SelectComponent, IconComponent, InfoIconComponent), - ], - providers: [MockProvider(DialogService, mockDialogService)], + imports: [ContributorsTableComponent, ...MockComponents(SelectComponent, IconComponent, InfoIconComponent)], + providers: [provideOSFCore(), MockProvider(DialogService, mockDialogService)], }).compileComponents(); fixture = TestBed.createComponent(ContributorsTableComponent); diff --git a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts index c3790ed9e..27e3eb9b8 100644 --- a/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts +++ b/src/app/shared/components/contributors/remove-contributor-dialog/remove-contributor-dialog.component.spec.ts @@ -4,7 +4,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { RemoveContributorDialogComponent } from './remove-contributor-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RemoveContributorDialogComponent', () => { let component: RemoveContributorDialogComponent; @@ -15,8 +15,9 @@ describe('RemoveContributorDialogComponent', () => { dialogRef = { close: jest.fn() } as any; await TestBed.configureTestingModule({ - imports: [RemoveContributorDialogComponent, OSFTestingModule], + imports: [RemoveContributorDialogComponent], providers: [ + provideOSFCore(), { provide: DynamicDialogRef, useValue: dialogRef }, { provide: DynamicDialogConfig, diff --git a/src/app/shared/components/contributors/request-access-table/request-access-table.component.spec.ts b/src/app/shared/components/contributors/request-access-table/request-access-table.component.spec.ts index 7b505fe83..afb10defb 100644 --- a/src/app/shared/components/contributors/request-access-table/request-access-table.component.spec.ts +++ b/src/app/shared/components/contributors/request-access-table/request-access-table.component.spec.ts @@ -13,7 +13,7 @@ import { SelectComponent } from '../../select/select.component'; import { RequestAccessTableComponent } from './request-access-table.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { DialogServiceMockBuilder } from '@testing/providers/dialog-provider.mock'; describe('RequestAccessTableComponent', () => { @@ -53,8 +53,8 @@ describe('RequestAccessTableComponent', () => { mockDialogService = DialogServiceMockBuilder.create().withOpenMock().build(); await TestBed.configureTestingModule({ - imports: [RequestAccessTableComponent, OSFTestingModule, MockComponent(SelectComponent)], - providers: [MockProvider(DialogService, mockDialogService)], + imports: [RequestAccessTableComponent, MockComponent(SelectComponent)], + providers: [provideOSFCore(), MockProvider(DialogService, mockDialogService)], }).compileComponents(); fixture = TestBed.createComponent(RequestAccessTableComponent); diff --git a/src/app/shared/components/copy-button/copy-button.component.spec.ts b/src/app/shared/components/copy-button/copy-button.component.spec.ts index db190f212..a3af77e66 100644 --- a/src/app/shared/components/copy-button/copy-button.component.spec.ts +++ b/src/app/shared/components/copy-button/copy-button.component.spec.ts @@ -7,7 +7,7 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { CopyButtonComponent } from './copy-button.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('CopyButtonComponent', () => { let component: CopyButtonComponent; @@ -17,8 +17,8 @@ describe('CopyButtonComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [CopyButtonComponent, OSFTestingModule], - providers: [MockProviders(Clipboard, ToastService)], + imports: [CopyButtonComponent], + providers: [provideOSFCore(), MockProviders(Clipboard, ToastService)], }).compileComponents(); fixture = TestBed.createComponent(CopyButtonComponent); diff --git a/src/app/shared/components/custom-paginator/custom-paginator.component.spec.ts b/src/app/shared/components/custom-paginator/custom-paginator.component.spec.ts index b81ef2248..81724abbe 100644 --- a/src/app/shared/components/custom-paginator/custom-paginator.component.spec.ts +++ b/src/app/shared/components/custom-paginator/custom-paginator.component.spec.ts @@ -4,6 +4,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CustomPaginatorComponent } from './custom-paginator.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('CustomPaginatorComponent', () => { let component: CustomPaginatorComponent; let fixture: ComponentFixture; @@ -11,6 +13,7 @@ describe('CustomPaginatorComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CustomPaginatorComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(CustomPaginatorComponent); diff --git a/src/app/shared/components/data-resources/data-resources.component.spec.ts b/src/app/shared/components/data-resources/data-resources.component.spec.ts index 1b07b61d4..8e4cdfc60 100644 --- a/src/app/shared/components/data-resources/data-resources.component.spec.ts +++ b/src/app/shared/components/data-resources/data-resources.component.spec.ts @@ -7,7 +7,7 @@ import { IconComponent } from '../icon/icon.component'; import { DataResourcesComponent } from './data-resources.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('DataResourcesComponent', () => { @@ -19,8 +19,8 @@ describe('DataResourcesComponent', () => { activatedRouteMock = ActivatedRouteMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [DataResourcesComponent, OSFTestingModule, MockComponent(IconComponent)], - providers: [MockProvider(ActivatedRoute, activatedRouteMock)], + imports: [DataResourcesComponent, MockComponent(IconComponent)], + providers: [provideOSFCore(), MockProvider(ActivatedRoute, activatedRouteMock)], }).compileComponents(); fixture = TestBed.createComponent(DataResourcesComponent); diff --git a/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts b/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts index 20b8545ad..76391769c 100644 --- a/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts +++ b/src/app/shared/components/doughnut-chart/doughnut-chart.component.spec.ts @@ -7,7 +7,7 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { DoughnutChartComponent } from './doughnut-chart.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('DoughnutChartComponent', () => { let component: DoughnutChartComponent; @@ -15,8 +15,8 @@ describe('DoughnutChartComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [DoughnutChartComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], - providers: [MockProvider(PLATFORM_ID, 'server')], + imports: [DoughnutChartComponent, MockComponent(LoadingSpinnerComponent)], + providers: [provideOSFCore(), MockProvider(PLATFORM_ID, 'server')], }).compileComponents(); fixture = TestBed.createComponent(DoughnutChartComponent); diff --git a/src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts b/src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts index 2c7034e56..bce2d46ca 100644 --- a/src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts +++ b/src/app/shared/components/education-history-dialog/education-history-dialog.component.spec.ts @@ -8,7 +8,7 @@ import { EducationHistoryComponent } from '../education-history/education-histor import { EducationHistoryDialogComponent } from './education-history-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EducationHistoryDialogComponent', () => { let component: EducationHistoryDialogComponent; @@ -16,8 +16,8 @@ describe('EducationHistoryDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EducationHistoryDialogComponent, OSFTestingModule, MockComponent(EducationHistoryComponent)], - providers: [MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], + imports: [EducationHistoryDialogComponent, MockComponent(EducationHistoryComponent)], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(EducationHistoryDialogComponent); diff --git a/src/app/shared/components/education-history/education-history.component.spec.ts b/src/app/shared/components/education-history/education-history.component.spec.ts index 34e09a370..c54b3e622 100644 --- a/src/app/shared/components/education-history/education-history.component.spec.ts +++ b/src/app/shared/components/education-history/education-history.component.spec.ts @@ -7,7 +7,7 @@ import { MonthYearPipe } from '@osf/shared/pipes/month-year.pipe'; import { EducationHistoryComponent } from './education-history.component'; import { MOCK_EDUCATION } from '@testing/mocks/user-employment-education.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EducationHistoryComponent', () => { let component: EducationHistoryComponent; @@ -15,7 +15,8 @@ describe('EducationHistoryComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EducationHistoryComponent, OSFTestingModule, MockPipe(MonthYearPipe)], + imports: [EducationHistoryComponent, MockPipe(MonthYearPipe)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(EducationHistoryComponent); diff --git a/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts index ed86a2705..a99c32658 100644 --- a/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts +++ b/src/app/shared/components/employment-history-dialog/employment-history-dialog.component.spec.ts @@ -8,7 +8,7 @@ import { EmploymentHistoryComponent } from '../employment-history/employment-his import { EmploymentHistoryDialogComponent } from './employment-history-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EmploymentHistoryDialogComponent', () => { let component: EmploymentHistoryDialogComponent; @@ -16,8 +16,8 @@ describe('EmploymentHistoryDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EmploymentHistoryDialogComponent, OSFTestingModule, MockComponent(EmploymentHistoryComponent)], - providers: [MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], + imports: [EmploymentHistoryDialogComponent, MockComponent(EmploymentHistoryComponent)], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig)], }).compileComponents(); fixture = TestBed.createComponent(EmploymentHistoryDialogComponent); diff --git a/src/app/shared/components/employment-history/employment-history.component.spec.ts b/src/app/shared/components/employment-history/employment-history.component.spec.ts index 8a84fb151..846eb5cd4 100644 --- a/src/app/shared/components/employment-history/employment-history.component.spec.ts +++ b/src/app/shared/components/employment-history/employment-history.component.spec.ts @@ -8,7 +8,7 @@ import { MonthYearPipe } from '@osf/shared/pipes/month-year.pipe'; import { EmploymentHistoryComponent } from './employment-history.component'; import { MOCK_EMPLOYMENT } from '@testing/mocks/user-employment-education.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('EmploymentHistoryComponent', () => { let component: EmploymentHistoryComponent; @@ -16,7 +16,8 @@ describe('EmploymentHistoryComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [EmploymentHistoryComponent, OSFTestingModule, MockPipe(MonthYearPipe)], + imports: [EmploymentHistoryComponent, MockPipe(MonthYearPipe)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(EmploymentHistoryComponent); diff --git a/src/app/shared/components/file-menu/file-menu.component.spec.ts b/src/app/shared/components/file-menu/file-menu.component.spec.ts index 7e4358881..6e1ca2024 100644 --- a/src/app/shared/components/file-menu/file-menu.component.spec.ts +++ b/src/app/shared/components/file-menu/file-menu.component.spec.ts @@ -1,5 +1,6 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockProvider } from 'ng-mocks'; +import { MenuItem } from 'primeng/api'; import { TieredMenu } from 'primeng/tieredmenu'; import { ComponentFixture, TestBed } from '@angular/core/testing'; @@ -7,220 +8,177 @@ import { Router } from '@angular/router'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; import { MenuManagerService } from '@osf/shared/services/menu-manager.service'; -import { FileMenuFlags } from '@shared/models/files/file-menu-action.model'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; +import { FileMenuComponent } from '@shared/components/file-menu/file-menu.component'; +import { FileMenuAction, FileMenuFlags } from '@shared/models/files/file-menu-action.model'; -import { FileMenuComponent } from './file-menu.component'; - -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { RouterMock, RouterMockType } from '@testing/providers/router-provider.mock'; +import { ViewOnlyLinkHelperMock, ViewOnlyLinkHelperMockType } from '@testing/providers/view-only-link-helper.mock'; describe('FileMenuComponent', () => { let component: FileMenuComponent; let fixture: ComponentFixture; - let router: Router; - let menuManager: MenuManagerService; - let mockMenu: TieredMenu; + let menuManager: Pick; + let viewOnlyService: ViewOnlyLinkHelperMockType; + + interface SetupOverrides { + isFolder?: boolean; + hasViewOnly?: boolean; + allowedActions?: Partial; + } + + const ALL_ACTIONS: FileMenuFlags = { + [FileMenuType.Download]: true, + [FileMenuType.Copy]: true, + [FileMenuType.Move]: true, + [FileMenuType.Delete]: true, + [FileMenuType.Rename]: true, + [FileMenuType.Share]: true, + [FileMenuType.Embed]: true, + }; + + function toFlags(overrides: Partial = {}): FileMenuFlags { + return { ...ALL_ACTIONS, ...overrides }; + } + + function getMenuIds(items: MenuItem[]): string[] { + return items.map((item) => item.id as string); + } + + function setup(overrides: SetupOverrides = {}) { + const routerMock: RouterMockType = RouterMock.create().build(); + viewOnlyService = ViewOnlyLinkHelperMock.simple(overrides.hasViewOnly ?? false); + menuManager = { + openMenu: jest.fn(), + onMenuHide: jest.fn(), + }; - beforeEach(async () => { - mockMenu = { - toggle: jest.fn(), - hide: jest.fn(), - } as any; + TestBed.configureTestingModule({ + imports: [FileMenuComponent], + providers: [ + provideOSFCore(), + MockProvider(Router, routerMock), + MockProvider(ViewOnlyLinkHelperService, viewOnlyService), + MockProvider(MenuManagerService, menuManager), + ], + }); - await TestBed.configureTestingModule({ - imports: [FileMenuComponent, OSFTestingModule, MockComponent(TieredMenu)], - providers: [MockProvider(MenuManagerService)], - }).compileComponents(); + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(() => ({ + matches: false, + media: '', + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); fixture = TestBed.createComponent(FileMenuComponent); component = fixture.componentInstance; - router = TestBed.inject(Router); - menuManager = TestBed.inject(MenuManagerService); + fixture.componentRef.setInput('isFolder', overrides.isFolder ?? false); + fixture.componentRef.setInput('allowedActions', toFlags(overrides.allowedActions)); + fixture.detectChanges(); + } - Object.defineProperty(component, 'menu', { - value: () => mockMenu, - writable: true, - configurable: true, - }); + it('should create', () => { + setup(); + expect(component).toBeTruthy(); }); - it('should have default values', () => { - expect(component.isFolder()).toBe(false); - expect(component.allowedActions()).toEqual({}); + it('should include all allowed actions for files without view-only', () => { + setup({ isFolder: false, hasViewOnly: false }); + expect(getMenuIds(component.menuItems())).toEqual([ + FileMenuType.Download, + FileMenuType.Share, + FileMenuType.Embed, + FileMenuType.Rename, + FileMenuType.Move, + FileMenuType.Copy, + FileMenuType.Delete, + ]); }); - describe('menuItems computed - View Only Mode', () => { - beforeEach(() => { - jest.spyOn(router, 'url', 'get').mockReturnValue('/test?view_only=true'); - Object.defineProperty(window, 'location', { - value: { search: '?view_only=true' }, - writable: true, - }); - }); - - it('should filter menu items for files in view-only mode', () => { - const allowedActions: FileMenuFlags = { - [FileMenuType.Download]: true, - [FileMenuType.Embed]: true, - [FileMenuType.Share]: true, - [FileMenuType.Copy]: true, - [FileMenuType.Rename]: false, - [FileMenuType.Move]: false, - [FileMenuType.Delete]: false, - }; - - fixture.componentRef.setInput('isFolder', false); - fixture.componentRef.setInput('allowedActions', allowedActions); - fixture.detectChanges(); - - const menuItems = component.menuItems(); - const menuItemIds = menuItems.map((item) => item.id); - - expect(menuItemIds).toContain(FileMenuType.Download); - expect(menuItemIds).toContain(FileMenuType.Embed); - expect(menuItemIds).toContain(FileMenuType.Share); - expect(menuItemIds).toContain(FileMenuType.Copy); - expect(menuItemIds).not.toContain(FileMenuType.Rename); - expect(menuItemIds).not.toContain(FileMenuType.Move); - expect(menuItemIds).not.toContain(FileMenuType.Delete); - }); - - it('should return empty array when no allowed actions in view-only mode', () => { - const allowedActions: FileMenuFlags = { - [FileMenuType.Download]: false, - [FileMenuType.Embed]: false, - [FileMenuType.Share]: false, - [FileMenuType.Copy]: false, - [FileMenuType.Rename]: false, - [FileMenuType.Move]: false, - [FileMenuType.Delete]: false, - }; - - fixture.componentRef.setInput('isFolder', false); - fixture.componentRef.setInput('allowedActions', allowedActions); - fixture.detectChanges(); - - expect(component.menuItems()).toEqual([]); - }); + it('should exclude share and embed for folders without view-only', () => { + setup({ isFolder: true, hasViewOnly: false }); + expect(getMenuIds(component.menuItems())).toEqual([ + FileMenuType.Download, + FileMenuType.Rename, + FileMenuType.Move, + FileMenuType.Copy, + FileMenuType.Delete, + ]); }); - describe('menuItems computed - Normal Mode', () => { - beforeEach(() => { - jest.spyOn(router, 'url', 'get').mockReturnValue('/test'); - Object.defineProperty(window, 'location', { - value: { search: '' }, - writable: true, - }); - }); - - it('should filter menu items for files in normal mode', () => { - const allowedActions: FileMenuFlags = { - [FileMenuType.Download]: true, - [FileMenuType.Embed]: true, - [FileMenuType.Share]: true, - [FileMenuType.Copy]: true, - [FileMenuType.Rename]: true, - [FileMenuType.Move]: true, - [FileMenuType.Delete]: true, - }; - - fixture.componentRef.setInput('isFolder', false); - fixture.componentRef.setInput('allowedActions', allowedActions); - fixture.detectChanges(); - - const menuItems = component.menuItems(); - const menuItemIds = menuItems.map((item) => item.id); - - expect(menuItemIds).toContain(FileMenuType.Download); - expect(menuItemIds).toContain(FileMenuType.Embed); - expect(menuItemIds).toContain(FileMenuType.Share); - expect(menuItemIds).toContain(FileMenuType.Copy); - expect(menuItemIds).toContain(FileMenuType.Rename); - expect(menuItemIds).toContain(FileMenuType.Move); - expect(menuItemIds).toContain(FileMenuType.Delete); - }); + it('should allow only download, embed, share and copy for files in view-only', () => { + setup({ isFolder: false, hasViewOnly: true }); + expect(getMenuIds(component.menuItems())).toEqual([ + FileMenuType.Download, + FileMenuType.Share, + FileMenuType.Embed, + FileMenuType.Copy, + ]); + }); - it('should filter menu items for folders in normal mode, excluding Share and Embed', () => { - const allowedActions: FileMenuFlags = { - [FileMenuType.Download]: true, - [FileMenuType.Embed]: true, - [FileMenuType.Share]: true, - [FileMenuType.Copy]: true, - [FileMenuType.Rename]: true, - [FileMenuType.Move]: true, - [FileMenuType.Delete]: true, - }; - - fixture.componentRef.setInput('isFolder', true); - fixture.componentRef.setInput('allowedActions', allowedActions); - fixture.detectChanges(); - - const menuItems = component.menuItems(); - const menuItemIds = menuItems.map((item) => item.id); - - expect(menuItemIds).toContain(FileMenuType.Download); - expect(menuItemIds).toContain(FileMenuType.Copy); - expect(menuItemIds).toContain(FileMenuType.Rename); - expect(menuItemIds).toContain(FileMenuType.Move); - expect(menuItemIds).toContain(FileMenuType.Delete); - expect(menuItemIds).not.toContain(FileMenuType.Embed); - expect(menuItemIds).not.toContain(FileMenuType.Share); - }); + it('should allow only download and copy for folders in view-only', () => { + setup({ isFolder: true, hasViewOnly: true }); + expect(getMenuIds(component.menuItems())).toEqual([FileMenuType.Download, FileMenuType.Copy]); + }); - it('should return empty array when no allowed actions in normal mode', () => { - const allowedActions: FileMenuFlags = { + it('should filter out disabled actions', () => { + setup({ + isFolder: false, + hasViewOnly: false, + allowedActions: { [FileMenuType.Download]: false, - [FileMenuType.Embed]: false, - [FileMenuType.Share]: false, - [FileMenuType.Copy]: false, - [FileMenuType.Rename]: false, [FileMenuType.Move]: false, - [FileMenuType.Delete]: false, - }; - - fixture.componentRef.setInput('isFolder', false); - fixture.componentRef.setInput('allowedActions', allowedActions); - fixture.detectChanges(); - - expect(component.menuItems()).toEqual([]); + [FileMenuType.Share]: false, + }, }); + expect(getMenuIds(component.menuItems())).toEqual([ + FileMenuType.Embed, + FileMenuType.Rename, + FileMenuType.Copy, + FileMenuType.Delete, + ]); }); - it('should update isFolder input', () => { - fixture.componentRef.setInput('isFolder', true); - fixture.detectChanges(); - expect(component.isFolder()).toBe(true); + it('should emit download action from menu command', () => { + setup(); + const emitSpy = jest.spyOn(component.action, 'emit'); + const item = component.menuItems().find((menuItem) => menuItem.id === FileMenuType.Download); + item?.command?.({} as never); + expect(emitSpy).toHaveBeenCalledWith({ value: FileMenuType.Download, data: undefined } as FileMenuAction); }); - it('should update allowedActions input', () => { - const allowedActions: FileMenuFlags = { - [FileMenuType.Download]: true, - [FileMenuType.Embed]: false, - [FileMenuType.Share]: false, - [FileMenuType.Copy]: false, - [FileMenuType.Rename]: false, - [FileMenuType.Move]: false, - [FileMenuType.Delete]: false, - }; - - fixture.componentRef.setInput('allowedActions', allowedActions); - fixture.detectChanges(); - expect(component.allowedActions()).toEqual(allowedActions); + it('should emit share twitter action with data from menu command', () => { + setup(); + const emitSpy = jest.spyOn(component.action, 'emit'); + const shareItem = component.menuItems().find((menuItem) => menuItem.id === FileMenuType.Share); + const twitterItem = shareItem?.items?.find((menuItem) => menuItem.id === `${FileMenuType.Share}-twitter`); + twitterItem?.command?.({} as never); + expect(emitSpy).toHaveBeenCalledWith({ + value: FileMenuType.Share, + data: { type: 'twitter' }, + } as FileMenuAction); }); - it('should call menuManager.openMenu when onMenuToggle is called', () => { - const openMenuSpy = jest.spyOn(menuManager, 'openMenu'); - const mockEvent = new Event('click'); - - component.onMenuToggle(mockEvent); - - expect(openMenuSpy).toHaveBeenCalledWith(mockMenu, mockEvent); + it('should delegate menu toggle to menu manager', () => { + setup(); + const menuMock = {} as TieredMenu; + const event = new Event('click'); + jest.spyOn(component, 'menu').mockReturnValue(menuMock); + component.onMenuToggle(event); + expect(menuManager.openMenu).toHaveBeenCalledWith(menuMock, event); }); - it('should call menuManager.onMenuHide when onMenuHide is called', () => { - const onMenuHideSpy = jest.spyOn(menuManager, 'onMenuHide'); - + it('should notify menu manager on hide', () => { + setup(); component.onMenuHide(); - - expect(onMenuHideSpy).toHaveBeenCalled(); + expect(menuManager.onMenuHide).toHaveBeenCalled(); }); }); diff --git a/src/app/shared/components/file-select-destination/file-select-destination.component.spec.ts b/src/app/shared/components/file-select-destination/file-select-destination.component.spec.ts index a6f8e9198..200e8c9d7 100644 --- a/src/app/shared/components/file-select-destination/file-select-destination.component.spec.ts +++ b/src/app/shared/components/file-select-destination/file-select-destination.component.spec.ts @@ -6,6 +6,8 @@ import { SelectComponent } from '../select/select.component'; import { FileSelectDestinationComponent } from './file-select-destination.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe.skip('FileSelectDestinationComponent', () => { let component: FileSelectDestinationComponent; let fixture: ComponentFixture; @@ -13,6 +15,7 @@ describe.skip('FileSelectDestinationComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [FileSelectDestinationComponent, MockComponent(SelectComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(FileSelectDestinationComponent); diff --git a/src/app/shared/components/file-upload-dialog/file-upload-dialog.component.spec.ts b/src/app/shared/components/file-upload-dialog/file-upload-dialog.component.spec.ts index 88d76cde2..0cdf223ca 100644 --- a/src/app/shared/components/file-upload-dialog/file-upload-dialog.component.spec.ts +++ b/src/app/shared/components/file-upload-dialog/file-upload-dialog.component.spec.ts @@ -1,21 +1,18 @@ -import { MockComponent } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.component'; - import { FileUploadDialogComponent } from './file-upload-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('FileUploadDialogComponent', () => { let component: FileUploadDialogComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FileUploadDialogComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], - }).compileComponents(); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FileUploadDialogComponent], + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(FileUploadDialogComponent); component = fixture.componentInstance; @@ -26,153 +23,23 @@ describe('FileUploadDialogComponent', () => { expect(component).toBeTruthy(); }); - it('should have default values', () => { + it('should have default model and input values', () => { expect(component.visible()).toBe(false); expect(component.fileName()).toBe(''); expect(component.progress()).toBe(0); }); - it('should accept fileName input', () => { - fixture.componentRef.setInput('fileName', 'test-file.pdf'); - fixture.detectChanges(); - - expect(component.fileName()).toBe('test-file.pdf'); - }); - - it('should accept progress input', () => { - fixture.componentRef.setInput('progress', 50); - fixture.detectChanges(); - - expect(component.progress()).toBe(50); - }); - - it('should accept progress input at 0', () => { - fixture.componentRef.setInput('progress', 0); - fixture.detectChanges(); - - expect(component.progress()).toBe(0); - }); - - it('should accept progress input at 100', () => { - fixture.componentRef.setInput('progress', 100); - fixture.detectChanges(); - - expect(component.progress()).toBe(100); - }); - - it('should update visible model', () => { - component.visible.set(true); - fixture.detectChanges(); - - expect(component.visible()).toBe(true); - }); - - it('should toggle visible state', () => { - expect(component.visible()).toBe(false); - - component.visible.set(true); - expect(component.visible()).toBe(true); - - component.visible.set(false); - expect(component.visible()).toBe(false); - }); - - it('should handle multiple inputs together', () => { - fixture.componentRef.setInput('fileName', 'document.docx'); - fixture.componentRef.setInput('progress', 75); - component.visible.set(true); - fixture.detectChanges(); - - expect(component.fileName()).toBe('document.docx'); - expect(component.progress()).toBe(75); - expect(component.visible()).toBe(true); - }); - - it('should handle long file names', () => { - const longFileName = 'very-long-file-name-with-many-characters-that-might-cause-display-issues.pdf'; - fixture.componentRef.setInput('fileName', longFileName); - fixture.detectChanges(); - - expect(component.fileName()).toBe(longFileName); - }); - - it('should handle file names with special characters', () => { - const specialFileName = 'file (1) [copy] - final_version.txt'; - fixture.componentRef.setInput('fileName', specialFileName); - fixture.detectChanges(); - - expect(component.fileName()).toBe(specialFileName); - }); - - it('should handle progress updates during upload', () => { - const progressValues = [0, 25, 50, 75, 100]; - - progressValues.forEach((progress) => { - fixture.componentRef.setInput('progress', progress); - fixture.detectChanges(); - expect(component.progress()).toBe(progress); - }); - }); - - it('should handle empty file name', () => { - fixture.componentRef.setInput('fileName', ''); - fixture.detectChanges(); - - expect(component.fileName()).toBe(''); - }); - - it('should update fileName during upload', () => { - fixture.componentRef.setInput('fileName', 'initial-file.pdf'); - fixture.detectChanges(); - expect(component.fileName()).toBe('initial-file.pdf'); - - fixture.componentRef.setInput('fileName', 'updated-file.pdf'); - fixture.detectChanges(); - expect(component.fileName()).toBe('updated-file.pdf'); - }); - - it('should handle progress reset', () => { - fixture.componentRef.setInput('progress', 100); - fixture.detectChanges(); - expect(component.progress()).toBe(100); - - fixture.componentRef.setInput('progress', 0); - fixture.detectChanges(); - expect(component.progress()).toBe(0); - }); - - it('should maintain state across multiple operations', () => { - fixture.componentRef.setInput('fileName', 'first-file.pdf'); - fixture.componentRef.setInput('progress', 50); + it('should update visible model value', () => { component.visible.set(true); - fixture.detectChanges(); - - expect(component.fileName()).toBe('first-file.pdf'); - expect(component.progress()).toBe(50); - expect(component.visible()).toBe(true); - - fixture.componentRef.setInput('fileName', 'second-file.pdf'); - fixture.componentRef.setInput('progress', 25); - fixture.detectChanges(); - - expect(component.fileName()).toBe('second-file.pdf'); - expect(component.progress()).toBe(25); expect(component.visible()).toBe(true); }); - it('should handle visibility changes independently of other inputs', () => { - fixture.componentRef.setInput('fileName', 'test.pdf'); - fixture.componentRef.setInput('progress', 50); + it('should accept fileName and progress input values', () => { + fixture.componentRef.setInput('fileName', 'my-file.pdf'); + fixture.componentRef.setInput('progress', 65); fixture.detectChanges(); - component.visible.set(true); - expect(component.visible()).toBe(true); - expect(component.fileName()).toBe('test.pdf'); - expect(component.progress()).toBe(50); - - component.visible.set(false); - expect(component.visible()).toBe(false); - expect(component.fileName()).toBe('test.pdf'); - expect(component.progress()).toBe(50); + expect(component.fileName()).toBe('my-file.pdf'); + expect(component.progress()).toBe(65); }); }); diff --git a/src/app/shared/components/files-tree/files-tree.component.spec.ts b/src/app/shared/components/files-tree/files-tree.component.spec.ts index e868ab604..500a8d835 100644 --- a/src/app/shared/components/files-tree/files-tree.component.spec.ts +++ b/src/app/shared/components/files-tree/files-tree.component.spec.ts @@ -5,6 +5,7 @@ import { DialogService } from 'primeng/dynamicdialog'; import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { FileKind } from '@osf/shared/enums/file-kind.enum'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; @@ -20,15 +21,15 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { FilesTreeComponent } from './files-tree.component'; -import { DataciteMockFactory } from '@testing/mocks/datacite.service.mock'; import { OSF_FILE_MOCK } from '@testing/mocks/osf-file.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { DataciteServiceMock, DataciteServiceMockType } from '@testing/providers/datacite.service.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('FilesTreeComponent', () => { let component: FilesTreeComponent; let fixture: ComponentFixture; - let dataciteMock: jest.Mocked; + let dataciteMock: DataciteServiceMockType; const mockFolderFile: FileFolderModel = { ...OSF_FILE_MOCK, @@ -42,10 +43,13 @@ describe('FilesTreeComponent', () => { }; beforeEach(async () => { - dataciteMock = DataciteMockFactory(); + dataciteMock = DataciteServiceMock.simple(); + await TestBed.configureTestingModule({ - imports: [FilesTreeComponent, OSFTestingModule, ...MockComponents(LoadingSpinnerComponent, FileMenuComponent)], + imports: [FilesTreeComponent, ...MockComponents(LoadingSpinnerComponent, FileMenuComponent)], providers: [ + provideOSFCore(), + provideRouter([]), provideMockStore({ signals: [{ selector: CurrentResourceSelectors.getCurrentResource, value: signal(null) }], }), diff --git a/src/app/shared/components/filter-chips/filter-chips.component.spec.ts b/src/app/shared/components/filter-chips/filter-chips.component.spec.ts index 1fec74fc0..18c90b884 100644 --- a/src/app/shared/components/filter-chips/filter-chips.component.spec.ts +++ b/src/app/shared/components/filter-chips/filter-chips.component.spec.ts @@ -8,7 +8,7 @@ import { import { FilterChipsComponent } from './filter-chips.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('FilterChipsComponent', () => { let component: FilterChipsComponent; @@ -39,7 +39,8 @@ describe('FilterChipsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FilterChipsComponent, OSFTestingModule], + imports: [FilterChipsComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(FilterChipsComponent); diff --git a/src/app/shared/components/form-select/form-select.component.spec.ts b/src/app/shared/components/form-select/form-select.component.spec.ts index e055630c5..1b792aaff 100644 --- a/src/app/shared/components/form-select/form-select.component.spec.ts +++ b/src/app/shared/components/form-select/form-select.component.spec.ts @@ -6,7 +6,7 @@ import { SelectOption } from '@osf/shared/models/select-option.model'; import { FormSelectComponent } from './form-select.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('FormSelectComponent', () => { let component: FormSelectComponent; @@ -22,7 +22,8 @@ describe('FormSelectComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FormSelectComponent, OSFTestingModule], + imports: [FormSelectComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(FormSelectComponent); diff --git a/src/app/shared/components/full-screen-loader/full-screen-loader.component.spec.ts b/src/app/shared/components/full-screen-loader/full-screen-loader.component.spec.ts index 058b2f9ec..0feec5d0b 100644 --- a/src/app/shared/components/full-screen-loader/full-screen-loader.component.spec.ts +++ b/src/app/shared/components/full-screen-loader/full-screen-loader.component.spec.ts @@ -5,6 +5,7 @@ import { LoaderService } from '@osf/shared/services/loader.service'; import { FullScreenLoaderComponent } from './full-screen-loader.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; describe('FullScreenLoaderComponent', () => { @@ -16,6 +17,7 @@ describe('FullScreenLoaderComponent', () => { await TestBed.configureTestingModule({ imports: [FullScreenLoaderComponent], providers: [ + provideOSFCore(), { provide: LoaderService, useClass: LoaderServiceMock, diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.html b/src/app/shared/components/funder-awards-list/funder-awards-list.component.html similarity index 100% rename from src/app/shared/funder-awards-list/funder-awards-list.component.html rename to src/app/shared/components/funder-awards-list/funder-awards-list.component.html diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.scss b/src/app/shared/components/funder-awards-list/funder-awards-list.component.scss similarity index 100% rename from src/app/shared/funder-awards-list/funder-awards-list.component.scss rename to src/app/shared/components/funder-awards-list/funder-awards-list.component.scss diff --git a/src/app/shared/components/funder-awards-list/funder-awards-list.component.spec.ts b/src/app/shared/components/funder-awards-list/funder-awards-list.component.spec.ts new file mode 100644 index 000000000..7ad11d6a5 --- /dev/null +++ b/src/app/shared/components/funder-awards-list/funder-awards-list.component.spec.ts @@ -0,0 +1,66 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; + +import { Funder } from '@osf/features/metadata/models'; + +import { FunderAwardsListComponent } from './funder-awards-list.component'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; + +describe('FunderAwardsListComponent', () => { + let component: FunderAwardsListComponent; + let fixture: ComponentFixture; + + const fundersMock: Funder[] = [ + { + funderName: 'National Science Foundation', + funderIdentifier: 'https://ror.org/021nxhr62', + funderIdentifierType: 'ROR', + awardNumber: 'NSF-123', + awardUri: 'https://example.org/nsf-123', + awardTitle: 'Grant 123', + }, + { + funderName: 'National Institutes of Health', + funderIdentifier: 'https://ror.org/04zaypm56', + funderIdentifierType: 'ROR', + awardNumber: 'NIH-456', + awardUri: 'https://example.org/nih-456', + awardTitle: 'Grant 456', + }, + ]; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [FunderAwardsListComponent], + providers: [provideOSFCore(), provideRouter([])], + }); + fixture = TestBed.createComponent(FunderAwardsListComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have default input values', () => { + expect(component.funders()).toEqual([]); + expect(component.registryId()).toBeNull(); + expect(component.isLoading()).toBe(false); + }); + + it('should update isLoading input', () => { + fixture.componentRef.setInput('isLoading', true); + expect(component.isLoading()).toBe(true); + }); + + it('should update registryId input', () => { + fixture.componentRef.setInput('registryId', 'abc123'); + expect(component.registryId()).toBe('abc123'); + }); + + it('should update funders input', () => { + fixture.componentRef.setInput('funders', fundersMock); + expect(component.funders()).toEqual(fundersMock); + }); +}); diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.ts b/src/app/shared/components/funder-awards-list/funder-awards-list.component.ts similarity index 100% rename from src/app/shared/funder-awards-list/funder-awards-list.component.ts rename to src/app/shared/components/funder-awards-list/funder-awards-list.component.ts diff --git a/src/app/shared/components/generic-filter/generic-filter.component.spec.ts b/src/app/shared/components/generic-filter/generic-filter.component.spec.ts index 1152ec047..3e5d8deb4 100644 --- a/src/app/shared/components/generic-filter/generic-filter.component.spec.ts +++ b/src/app/shared/components/generic-filter/generic-filter.component.spec.ts @@ -10,7 +10,7 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { GenericFilterComponent } from './generic-filter.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('GenericFilterComponent', () => { let component: GenericFilterComponent; @@ -25,7 +25,8 @@ describe('GenericFilterComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [GenericFilterComponent, OSFTestingModule, MockComponent(LoadingSpinnerComponent)], + imports: [GenericFilterComponent, MockComponent(LoadingSpinnerComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(GenericFilterComponent); diff --git a/src/app/shared/components/global-search/global-search.component.spec.ts b/src/app/shared/components/global-search/global-search.component.spec.ts index 6e4887249..f3fa74dcc 100644 --- a/src/app/shared/components/global-search/global-search.component.spec.ts +++ b/src/app/shared/components/global-search/global-search.component.spec.ts @@ -20,7 +20,7 @@ import { SearchInputComponent } from '../search-input/search-input.component'; import { GlobalSearchComponent } from './global-search.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -51,7 +51,6 @@ describe('GlobalSearchComponent', () => { await TestBed.configureTestingModule({ imports: [ GlobalSearchComponent, - OSFTestingModule, ...MockComponents( FilterChipsComponent, SearchInputComponent, @@ -60,6 +59,7 @@ describe('GlobalSearchComponent', () => { ), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: GlobalSearchSelectors.getResources, value: signal([]) }, diff --git a/src/app/shared/components/google-file-picker/google-file-picker.component.spec.ts b/src/app/shared/components/google-file-picker/google-file-picker.component.spec.ts index 9a33c8ec0..1034476eb 100644 --- a/src/app/shared/components/google-file-picker/google-file-picker.component.spec.ts +++ b/src/app/shared/components/google-file-picker/google-file-picker.component.spec.ts @@ -10,7 +10,7 @@ import { GoogleFilePickerDownloadService } from '@osf/shared/services/google-fil import { GoogleFilePickerComponent } from './google-file-picker.component'; -import { OSFTestingModule, OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('Component: Google File Picker', () => { let component: GoogleFilePickerComponent; @@ -104,8 +104,9 @@ describe('Component: Google File Picker', () => { }; await TestBed.configureTestingModule({ - imports: [OSFTestingModule, GoogleFilePickerComponent], + imports: [GoogleFilePickerComponent], providers: [ + provideOSFCore(), { provide: SENTRY_TOKEN, useValue: { captureException: jest.fn() } }, { provide: GoogleFilePickerDownloadService, useValue: googlePickerServiceSpy }, { @@ -225,8 +226,9 @@ describe('Component: Google File Picker', () => { }; await TestBed.configureTestingModule({ - imports: [OSFTestingStoreModule, GoogleFilePickerComponent], + imports: [GoogleFilePickerComponent], providers: [ + provideOSFCore(), { provide: SENTRY_TOKEN, useValue: { captureException: jest.fn() } }, { provide: GoogleFilePickerDownloadService, useValue: googlePickerServiceSpy }, { @@ -306,8 +308,9 @@ describe('Component: Google File Picker', () => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ - imports: [OSFTestingStoreModule, GoogleFilePickerComponent], + imports: [GoogleFilePickerComponent], providers: [ + provideOSFCore(), { provide: SENTRY_TOKEN, useValue: { captureException: jest.fn() } }, { provide: GoogleFilePickerDownloadService, useValue: googlePickerServiceSpy }, { provide: Store, useValue: errorStoreMock }, @@ -333,8 +336,9 @@ describe('Component: Google File Picker', () => { describe('picker not configured', () => { it('should disable picker when apiKey or appId is missing', async () => { await TestBed.configureTestingModule({ - imports: [OSFTestingModule, GoogleFilePickerComponent], + imports: [GoogleFilePickerComponent], providers: [ + provideOSFCore(), { provide: SENTRY_TOKEN, useValue: { captureException: jest.fn() } }, { provide: GoogleFilePickerDownloadService, useValue: googlePickerServiceSpy }, { provide: Store, useValue: storeMock }, @@ -359,8 +363,9 @@ describe('Component: Google File Picker', () => { it('should not open picker when not configured', async () => { await TestBed.configureTestingModule({ - imports: [OSFTestingModule, GoogleFilePickerComponent], + imports: [GoogleFilePickerComponent], providers: [ + provideOSFCore(), { provide: SENTRY_TOKEN, useValue: { captureException: jest.fn() } }, { provide: GoogleFilePickerDownloadService, useValue: googlePickerServiceSpy }, { provide: Store, useValue: storeMock }, diff --git a/src/app/shared/components/icon/icon.component.spec.ts b/src/app/shared/components/icon/icon.component.spec.ts index 1b259c789..649b73d4a 100644 --- a/src/app/shared/components/icon/icon.component.spec.ts +++ b/src/app/shared/components/icon/icon.component.spec.ts @@ -4,6 +4,8 @@ import { By } from '@angular/platform-browser'; import { IconComponent } from './icon.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('IconComponent', () => { let component: IconComponent; let fixture: ComponentFixture; @@ -12,6 +14,7 @@ describe('IconComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [IconComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(IconComponent); diff --git a/src/app/shared/components/info-icon/info-icon.component.spec.ts b/src/app/shared/components/info-icon/info-icon.component.spec.ts index 1a6b7623a..6420ffd16 100644 --- a/src/app/shared/components/info-icon/info-icon.component.spec.ts +++ b/src/app/shared/components/info-icon/info-icon.component.spec.ts @@ -3,12 +3,11 @@ import { MockPipe } from 'ng-mocks'; import { ComponentRef } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; - -import { TooltipPosition } from '@osf/shared/models/tooltip-position.model'; import { InfoIconComponent } from './info-icon.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('InfoIconComponent', () => { let component: InfoIconComponent; let fixture: ComponentFixture; @@ -17,6 +16,7 @@ describe('InfoIconComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [InfoIconComponent, MockPipe(TranslatePipe)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(InfoIconComponent); @@ -34,16 +34,4 @@ describe('InfoIconComponent', () => { expect(component.tooltipText()).toBe('This is a tooltip'); }); - - it('should handle different tooltip positions', () => { - const positions: TooltipPosition[] = ['top', 'bottom', 'left', 'right']; - - positions.forEach((position) => { - componentRef.setInput('tooltipPosition', position); - fixture.detectChanges(); - - const iconElement = fixture.debugElement.query(By.css('i')); - expect(iconElement.nativeElement.getAttribute('ng-reflect-tooltip-position')).toBe(position); - }); - }); }); diff --git a/src/app/shared/components/license-display/license-display.component.spec.ts b/src/app/shared/components/license-display/license-display.component.spec.ts index 336af6cfd..6246551ba 100644 --- a/src/app/shared/components/license-display/license-display.component.spec.ts +++ b/src/app/shared/components/license-display/license-display.component.spec.ts @@ -8,7 +8,7 @@ import { InterpolatePipe } from '@osf/shared/pipes/interpolate.pipe'; import { LicenseDisplayComponent } from './license-display.component'; import { MOCK_LICENSE } from '@testing/mocks/license.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('LicenseDisplayComponent', () => { let component: LicenseDisplayComponent; @@ -27,7 +27,8 @@ describe('LicenseDisplayComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LicenseDisplayComponent, MockPipe(InterpolatePipe), OSFTestingModule], + imports: [LicenseDisplayComponent, MockPipe(InterpolatePipe)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(LicenseDisplayComponent); diff --git a/src/app/shared/components/license/license.component.spec.ts b/src/app/shared/components/license/license.component.spec.ts index eb9ee62d9..cf61d767a 100644 --- a/src/app/shared/components/license/license.component.spec.ts +++ b/src/app/shared/components/license/license.component.spec.ts @@ -10,7 +10,7 @@ import { TruncatedTextComponent } from '../truncated-text/truncated-text.compone import { LicenseComponent } from './license.component'; import { MOCK_LICENSE } from '@testing/mocks/license.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('LicenseComponent', () => { let component: LicenseComponent; @@ -34,7 +34,8 @@ describe('LicenseComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LicenseComponent, ...MockComponents(TextInputComponent, TruncatedTextComponent), OSFTestingModule], + imports: [LicenseComponent, ...MockComponents(TextInputComponent, TruncatedTextComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(LicenseComponent); diff --git a/src/app/shared/components/line-chart/line-chart.component.spec.ts b/src/app/shared/components/line-chart/line-chart.component.spec.ts index 9e148757f..070dee9c4 100644 --- a/src/app/shared/components/line-chart/line-chart.component.spec.ts +++ b/src/app/shared/components/line-chart/line-chart.component.spec.ts @@ -11,7 +11,7 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { LineChartComponent } from './line-chart.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('LineChartComponent', () => { let component: LineChartComponent; @@ -19,8 +19,8 @@ describe('LineChartComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [LineChartComponent, OSFTestingModule, MockModule(ChartModule), MockComponent(LoadingSpinnerComponent)], - providers: [MockProvider(PLATFORM_ID, 'browser')], + imports: [LineChartComponent, MockModule(ChartModule), MockComponent(LoadingSpinnerComponent)], + providers: [provideOSFCore(), MockProvider(PLATFORM_ID, 'browser')], }).compileComponents(); fixture = TestBed.createComponent(LineChartComponent); diff --git a/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts b/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts index 24d148f97..55cac58e0 100644 --- a/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts +++ b/src/app/shared/components/loading-spinner/loading-spinner.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { LoadingSpinnerComponent } from './loading-spinner.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('LoadingSpinnerComponent', () => { let component: LoadingSpinnerComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe('LoadingSpinnerComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [LoadingSpinnerComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(LoadingSpinnerComponent); diff --git a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.spec.ts b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.spec.ts index 9fdd56e65..97285589f 100644 --- a/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.spec.ts +++ b/src/app/shared/components/make-decision-dialog/make-decision-dialog.component.spec.ts @@ -13,7 +13,7 @@ import { CollectionsSelectors } from '@osf/shared/stores/collections'; import { MakeDecisionDialogComponent } from './make-decision-dialog.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('MakeDecisionDialogComponent', () => { @@ -42,9 +42,10 @@ describe('MakeDecisionDialogComponent', () => { }; await TestBed.configureTestingModule({ - imports: [MakeDecisionDialogComponent, OSFTestingModule], + imports: [MakeDecisionDialogComponent], schemas: [NO_ERRORS_SCHEMA], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: CollectionsSelectors.getCollectionProvider, value: signal(mockCollectionProvider) }, diff --git a/src/app/shared/components/markdown/markdown.component.spec.ts b/src/app/shared/components/markdown/markdown.component.spec.ts index 6fb6cda0c..7e368fb65 100644 --- a/src/app/shared/components/markdown/markdown.component.spec.ts +++ b/src/app/shared/components/markdown/markdown.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { MarkdownComponent } from './markdown.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('MarkdownComponent', () => { let component: MarkdownComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe('MarkdownComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [MarkdownComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MarkdownComponent); diff --git a/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts b/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts index 38a68f4ad..ec626b26c 100644 --- a/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts +++ b/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts @@ -1,6 +1,5 @@ import { MockComponents } from 'ng-mocks'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { CedarTemplateFormComponent } from '@osf/features/metadata/components'; @@ -14,7 +13,7 @@ import { MetadataTabsComponent } from './metadata-tabs.component'; import { CEDAR_METADATA_DATA_TEMPLATE_JSON_API_MOCK } from '@testing/mocks/cedar-metadata-data-template-json-api.mock'; import { MOCK_CEDAR_METADATA_RECORD_DATA } from '@testing/mocks/cedar-metadata-record.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MetadataTabsComponent', () => { let component: MetadataTabsComponent; @@ -32,12 +31,8 @@ describe('MetadataTabsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - MetadataTabsComponent, - OSFTestingModule, - ...MockComponents(LoadingSpinnerComponent, CedarTemplateFormComponent), - ], - schemas: [NO_ERRORS_SCHEMA], + imports: [MetadataTabsComponent, ...MockComponents(LoadingSpinnerComponent, CedarTemplateFormComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MetadataTabsComponent); diff --git a/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts b/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts index ffb134af6..8f24e9c6f 100644 --- a/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts +++ b/src/app/shared/components/my-projects-table/my-projects-table.component.spec.ts @@ -12,7 +12,7 @@ import { IconComponent } from '../icon/icon.component'; import { MyProjectsTableComponent } from './my-projects-table.component'; import { MOCK_CONTRIBUTOR } from '@testing/mocks/contributors.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('MyProjectsTableComponent', () => { let component: MyProjectsTableComponent; @@ -44,7 +44,7 @@ describe('MyProjectsTableComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [MyProjectsTableComponent, ...MockComponents(IconComponent, ContributorsListShortenerComponent)], - providers: [TranslateServiceMock], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(MyProjectsTableComponent); diff --git a/src/app/shared/components/password-input-hint/password-input-hint.component.spec.ts b/src/app/shared/components/password-input-hint/password-input-hint.component.spec.ts index 18924152f..8d9eee03b 100644 --- a/src/app/shared/components/password-input-hint/password-input-hint.component.spec.ts +++ b/src/app/shared/components/password-input-hint/password-input-hint.component.spec.ts @@ -3,7 +3,7 @@ import { FormControl, Validators } from '@angular/forms'; import { PasswordInputHintComponent } from './password-input-hint.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('PasswordInputHintComponent', () => { let component: PasswordInputHintComponent; @@ -11,7 +11,8 @@ describe('PasswordInputHintComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PasswordInputHintComponent, OSFTestingModule], + imports: [PasswordInputHintComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(PasswordInputHintComponent); diff --git a/src/app/shared/components/pie-chart/pie-chart.component.spec.ts b/src/app/shared/components/pie-chart/pie-chart.component.spec.ts index 0990a79d6..920eed204 100644 --- a/src/app/shared/components/pie-chart/pie-chart.component.spec.ts +++ b/src/app/shared/components/pie-chart/pie-chart.component.spec.ts @@ -11,7 +11,7 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { PieChartComponent } from './pie-chart.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('PieChartComponent', () => { let component: PieChartComponent; @@ -19,8 +19,8 @@ describe('PieChartComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PieChartComponent, OSFTestingModule, MockModule(ChartModule), MockComponent(LoadingSpinnerComponent)], - providers: [MockProvider(PLATFORM_ID, 'browser')], + imports: [PieChartComponent, MockModule(ChartModule), MockComponent(LoadingSpinnerComponent)], + providers: [provideOSFCore(), MockProvider(PLATFORM_ID, 'browser')], }).compileComponents(); fixture = TestBed.createComponent(PieChartComponent); diff --git a/src/app/shared/components/project-selector/project-selector.component.spec.ts b/src/app/shared/components/project-selector/project-selector.component.spec.ts index 4c3cfe41a..d23dc7532 100644 --- a/src/app/shared/components/project-selector/project-selector.component.spec.ts +++ b/src/app/shared/components/project-selector/project-selector.component.spec.ts @@ -10,7 +10,7 @@ import { ProjectsState } from '@shared/stores/projects'; import { ProjectSelectorComponent } from './project-selector.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ProjectSelectorComponent', () => { let component: ProjectSelectorComponent; @@ -18,8 +18,8 @@ describe('ProjectSelectorComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProjectSelectorComponent, OSFTestingModule], - providers: [MockProvider(ToastService), provideStore([ProjectsState, UserState])], + imports: [ProjectSelectorComponent], + providers: [provideOSFCore(), MockProvider(ToastService), provideStore([ProjectsState, UserState])], }).compileComponents(); fixture = TestBed.createComponent(ProjectSelectorComponent); diff --git a/src/app/shared/components/readonly-input/readonly-input.component.spec.ts b/src/app/shared/components/readonly-input/readonly-input.component.spec.ts index fd7642eb4..1b94d9077 100644 --- a/src/app/shared/components/readonly-input/readonly-input.component.spec.ts +++ b/src/app/shared/components/readonly-input/readonly-input.component.spec.ts @@ -3,7 +3,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReadonlyInputComponent } from './readonly-input.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ReadonlyInputComponent', () => { let component: ReadonlyInputComponent; @@ -14,7 +14,8 @@ describe('ReadonlyInputComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ReadonlyInputComponent, OSFTestingModule], + imports: [ReadonlyInputComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ReadonlyInputComponent); diff --git a/src/app/shared/components/recent-activity/recent-activity-list.component.spec.ts b/src/app/shared/components/recent-activity/recent-activity-list.component.spec.ts index 7ac02a83b..a63799a59 100644 --- a/src/app/shared/components/recent-activity/recent-activity-list.component.spec.ts +++ b/src/app/shared/components/recent-activity/recent-activity-list.component.spec.ts @@ -12,7 +12,7 @@ import { makeActivityLogWithDisplay, MOCK_ACTIVITY_LOGS_WITH_DISPLAY, } from '@testing/mocks/activity-log-with-display.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RecentActivityListComponent', () => { let component: RecentActivityListComponent; @@ -20,7 +20,8 @@ describe('RecentActivityListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RecentActivityListComponent, OSFTestingModule, MockComponent(CustomPaginatorComponent)], + imports: [RecentActivityListComponent, MockComponent(CustomPaginatorComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(RecentActivityListComponent); diff --git a/src/app/shared/components/registration-card/registration-card.component.spec.ts b/src/app/shared/components/registration-card/registration-card.component.spec.ts index adc8720ef..98ef2d4e7 100644 --- a/src/app/shared/components/registration-card/registration-card.component.spec.ts +++ b/src/app/shared/components/registration-card/registration-card.component.spec.ts @@ -17,7 +17,7 @@ import { StatusBadgeComponent } from '../status-badge/status-badge.component'; import { RegistrationCardComponent } from './registration-card.component'; import { MOCK_REGISTRATION } from '@testing/mocks/registration.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('RegistrationCardComponent', () => { @@ -30,10 +30,10 @@ describe('RegistrationCardComponent', () => { await TestBed.configureTestingModule({ imports: [ RegistrationCardComponent, - OSFTestingModule, ...MockComponents(StatusBadgeComponent, DataResourcesComponent, IconComponent, ContributorsListComponent), ], providers: [ + provideOSFCore(), provideMockStore({ signals: [{ selector: RegistriesSelectors.getSchemaResponse, value: signal(null) }], }), diff --git a/src/app/shared/components/resource-card/components/file-secondary-metadata/file-secondary-metadata.component.spec.ts b/src/app/shared/components/resource-card/components/file-secondary-metadata/file-secondary-metadata.component.spec.ts index fd9c5984e..06a54b740 100644 --- a/src/app/shared/components/resource-card/components/file-secondary-metadata/file-secondary-metadata.component.spec.ts +++ b/src/app/shared/components/resource-card/components/file-secondary-metadata/file-secondary-metadata.component.spec.ts @@ -6,7 +6,7 @@ import { ResourceModel } from '@shared/models/search/resource.model'; import { FileSecondaryMetadataComponent } from './file-secondary-metadata.component'; import { MOCK_RESOURCE } from '@testing/mocks/resource.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('FileSecondaryMetadataComponent', () => { let component: FileSecondaryMetadataComponent; @@ -19,7 +19,8 @@ describe('FileSecondaryMetadataComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [FileSecondaryMetadataComponent, OSFTestingModule], + imports: [FileSecondaryMetadataComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(FileSecondaryMetadataComponent); diff --git a/src/app/shared/components/resource-card/components/preprint-secondary-metadata/preprint-secondary-metadata.component.spec.ts b/src/app/shared/components/resource-card/components/preprint-secondary-metadata/preprint-secondary-metadata.component.spec.ts index c2f9776c4..06cd109c4 100644 --- a/src/app/shared/components/resource-card/components/preprint-secondary-metadata/preprint-secondary-metadata.component.spec.ts +++ b/src/app/shared/components/resource-card/components/preprint-secondary-metadata/preprint-secondary-metadata.component.spec.ts @@ -6,7 +6,7 @@ import { ResourceModel } from '@shared/models/search/resource.model'; import { PreprintSecondaryMetadataComponent } from './preprint-secondary-metadata.component'; import { MOCK_RESOURCE } from '@testing/mocks/resource.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('PreprintSecondaryMetadataComponent', () => { let component: PreprintSecondaryMetadataComponent; @@ -19,7 +19,8 @@ describe('PreprintSecondaryMetadataComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [PreprintSecondaryMetadataComponent, OSFTestingModule], + imports: [PreprintSecondaryMetadataComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(PreprintSecondaryMetadataComponent); diff --git a/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.spec.ts b/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.spec.ts index beb0316e1..86b6d3f2a 100644 --- a/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.spec.ts +++ b/src/app/shared/components/resource-card/components/project-secondary-metadata/project-secondary-metadata.component.spec.ts @@ -6,7 +6,7 @@ import { ResourceModel } from '@shared/models/search/resource.model'; import { ProjectSecondaryMetadataComponent } from './project-secondary-metadata.component'; import { MOCK_RESOURCE } from '@testing/mocks/resource.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ProjectSecondaryMetadataComponent', () => { let component: ProjectSecondaryMetadataComponent; @@ -19,7 +19,8 @@ describe('ProjectSecondaryMetadataComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ProjectSecondaryMetadataComponent, OSFTestingModule], + imports: [ProjectSecondaryMetadataComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ProjectSecondaryMetadataComponent); diff --git a/src/app/shared/components/resource-card/components/registration-secondary-metadata/registration-secondary-metadata.component.spec.ts b/src/app/shared/components/resource-card/components/registration-secondary-metadata/registration-secondary-metadata.component.spec.ts index ea861554c..f285062a8 100644 --- a/src/app/shared/components/resource-card/components/registration-secondary-metadata/registration-secondary-metadata.component.spec.ts +++ b/src/app/shared/components/resource-card/components/registration-secondary-metadata/registration-secondary-metadata.component.spec.ts @@ -6,7 +6,7 @@ import { ResourceModel } from '@shared/models/search/resource.model'; import { RegistrationSecondaryMetadataComponent } from './registration-secondary-metadata.component'; import { MOCK_RESOURCE } from '@testing/mocks/resource.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('RegistrationSecondaryMetadataComponent', () => { let component: RegistrationSecondaryMetadataComponent; @@ -19,7 +19,8 @@ describe('RegistrationSecondaryMetadataComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [RegistrationSecondaryMetadataComponent, OSFTestingModule], + imports: [RegistrationSecondaryMetadataComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(RegistrationSecondaryMetadataComponent); diff --git a/src/app/shared/components/resource-card/components/user-secondary-metadata/user-secondary-metadata.component.spec.ts b/src/app/shared/components/resource-card/components/user-secondary-metadata/user-secondary-metadata.component.spec.ts index ef77b5c06..8c15dfc20 100644 --- a/src/app/shared/components/resource-card/components/user-secondary-metadata/user-secondary-metadata.component.spec.ts +++ b/src/app/shared/components/resource-card/components/user-secondary-metadata/user-secondary-metadata.component.spec.ts @@ -6,7 +6,7 @@ import { ResourceModel } from '@shared/models/search/resource.model'; import { UserSecondaryMetadataComponent } from './user-secondary-metadata.component'; import { MOCK_AGENT_RESOURCE } from '@testing/mocks/resource.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('UserSecondaryMetadataComponent', () => { let component: UserSecondaryMetadataComponent; @@ -19,7 +19,8 @@ describe('UserSecondaryMetadataComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [UserSecondaryMetadataComponent, OSFTestingModule], + imports: [UserSecondaryMetadataComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(UserSecondaryMetadataComponent); diff --git a/src/app/shared/components/resource-card/resource-card.component.spec.ts b/src/app/shared/components/resource-card/resource-card.component.spec.ts index ebec3c9f3..8fc491c5d 100644 --- a/src/app/shared/components/resource-card/resource-card.component.spec.ts +++ b/src/app/shared/components/resource-card/resource-card.component.spec.ts @@ -20,7 +20,7 @@ import { ResourceCardComponent } from './resource-card.component'; import { MOCK_USER_RELATED_COUNTS } from '@testing/mocks/data.mock'; import { MOCK_AGENT_RESOURCE, MOCK_RESOURCE } from '@testing/mocks/resource.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResourceCardComponent', () => { let component: ResourceCardComponent; @@ -35,7 +35,6 @@ describe('ResourceCardComponent', () => { await TestBed.configureTestingModule({ imports: [ ResourceCardComponent, - OSFTestingModule, ...MockComponents( DataResourcesComponent, UserSecondaryMetadataComponent, @@ -46,6 +45,7 @@ describe('ResourceCardComponent', () => { ), ], providers: [ + provideOSFCore(), MockProvider(ResourceCardService, { getUserRelatedCounts: jest.fn().mockReturnValue(of(mockUserCounts)), }), diff --git a/src/app/shared/components/resource-citations/resource-citations.component.spec.ts b/src/app/shared/components/resource-citations/resource-citations.component.spec.ts index df5fc86f8..03faa49f8 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.spec.ts +++ b/src/app/shared/components/resource-citations/resource-citations.component.spec.ts @@ -11,7 +11,7 @@ import { CitationsSelectors } from '@shared/stores/citations'; import { ResourceCitationsComponent } from './resource-citations.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMockBuilder } from '@testing/providers/toast-provider.mock'; @@ -35,8 +35,9 @@ describe('ResourceCitationsComponent', () => { mockRouter = RouterMockBuilder.create().build(); await TestBed.configureTestingModule({ - imports: [ResourceCitationsComponent, OSFTestingModule], + imports: [ResourceCitationsComponent], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: CitationsSelectors.getDefaultCitations, value: signal([]) }, diff --git a/src/app/shared/components/resource-doi/resource-doi.component.spec.ts b/src/app/shared/components/resource-doi/resource-doi.component.spec.ts index 5dbf0fb17..4976b5e61 100644 --- a/src/app/shared/components/resource-doi/resource-doi.component.spec.ts +++ b/src/app/shared/components/resource-doi/resource-doi.component.spec.ts @@ -5,6 +5,7 @@ import { IdentifierModel } from '@osf/shared/models/identifiers/identifier.model import { ResourceDoiComponent } from './resource-doi.component'; import { MOCK_PROJECT_IDENTIFIERS } from '@testing/mocks/project-overview.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResourceDoiComponent', () => { let component: ResourceDoiComponent; @@ -23,6 +24,7 @@ describe('ResourceDoiComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ResourceDoiComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ResourceDoiComponent); diff --git a/src/app/shared/components/resource-license/resource-license.component.spec.ts b/src/app/shared/components/resource-license/resource-license.component.spec.ts index c2765d03e..068ebbf70 100644 --- a/src/app/shared/components/resource-license/resource-license.component.spec.ts +++ b/src/app/shared/components/resource-license/resource-license.component.spec.ts @@ -4,7 +4,7 @@ import { By } from '@angular/platform-browser'; import { ResourceLicenseComponent } from './resource-license.component'; import { MOCK_LICENSE } from '@testing/mocks/license.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResourceLicenseComponent', () => { let component: ResourceLicenseComponent; @@ -12,7 +12,8 @@ describe('ResourceLicenseComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ResourceLicenseComponent, OSFTestingModule], + imports: [ResourceLicenseComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ResourceLicenseComponent); diff --git a/src/app/shared/components/search-filters/search-filters.component.spec.ts b/src/app/shared/components/search-filters/search-filters.component.spec.ts index 1177919c0..c906d0d32 100644 --- a/src/app/shared/components/search-filters/search-filters.component.spec.ts +++ b/src/app/shared/components/search-filters/search-filters.component.spec.ts @@ -14,7 +14,7 @@ import { LoadingSpinnerComponent } from '../loading-spinner/loading-spinner.comp import { SearchFiltersComponent } from './search-filters.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SearchFiltersComponent', () => { let component: SearchFiltersComponent; @@ -55,11 +55,8 @@ describe('SearchFiltersComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ - SearchFiltersComponent, - OSFTestingModule, - ...MockComponents(GenericFilterComponent, LoadingSpinnerComponent), - ], + imports: [SearchFiltersComponent, ...MockComponents(GenericFilterComponent, LoadingSpinnerComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SearchFiltersComponent); diff --git a/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts b/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts index 01c26344d..bd66bf8e2 100644 --- a/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts +++ b/src/app/shared/components/search-help-tutorial/search-help-tutorial.component.spec.ts @@ -5,6 +5,8 @@ import { TutorialStep } from '@shared/models/tutorial-step.model'; import { SearchHelpTutorialComponent } from './search-help-tutorial.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('SearchHelpTutorialComponent', () => { let component: SearchHelpTutorialComponent; let fixture: ComponentFixture; @@ -19,6 +21,7 @@ describe('SearchHelpTutorialComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SearchHelpTutorialComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SearchHelpTutorialComponent); diff --git a/src/app/shared/components/search-input/search-input.component.spec.ts b/src/app/shared/components/search-input/search-input.component.spec.ts index cbe1ef082..313a1ddf2 100644 --- a/src/app/shared/components/search-input/search-input.component.spec.ts +++ b/src/app/shared/components/search-input/search-input.component.spec.ts @@ -7,6 +7,8 @@ import { IconComponent } from '../icon/icon.component'; import { SearchInputComponent } from './search-input.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('SearchInputComponent', () => { let component: SearchInputComponent; let fixture: ComponentFixture; @@ -14,6 +16,7 @@ describe('SearchInputComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SearchInputComponent, MockComponent(IconComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SearchInputComponent); diff --git a/src/app/shared/components/search-results-container/search-results-container.component.spec.ts b/src/app/shared/components/search-results-container/search-results-container.component.spec.ts index 91206fd6e..fa22f8fd7 100644 --- a/src/app/shared/components/search-results-container/search-results-container.component.spec.ts +++ b/src/app/shared/components/search-results-container/search-results-container.component.spec.ts @@ -11,7 +11,7 @@ import { SelectComponent } from '../select/select.component'; import { SearchResultsContainerComponent } from './search-results-container.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SearchResultsContainerComponent', () => { let component: SearchResultsContainerComponent; @@ -22,9 +22,9 @@ describe('SearchResultsContainerComponent', () => { await TestBed.configureTestingModule({ imports: [ SearchResultsContainerComponent, - OSFTestingModule, ...MockComponents(ResourceCardComponent, SelectComponent, LoadingSpinnerComponent), ], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SearchResultsContainerComponent); diff --git a/src/app/shared/components/select/select.component.spec.ts b/src/app/shared/components/select/select.component.spec.ts index 273c86ec2..eb3a5eeed 100644 --- a/src/app/shared/components/select/select.component.spec.ts +++ b/src/app/shared/components/select/select.component.spec.ts @@ -1,11 +1,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Primitive } from '@shared/helpers'; +import { Primitive } from '@osf/shared/helpers/types.helper'; import { SelectOption } from '@shared/models/select-option.model'; import { SelectComponent } from './select.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SelectComponent', () => { let component: SelectComponent; @@ -25,7 +25,8 @@ describe('SelectComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SelectComponent, OSFTestingModule], + imports: [SelectComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SelectComponent); diff --git a/src/app/shared/components/socials-share-button/socials-share-button.component.spec.ts b/src/app/shared/components/socials-share-button/socials-share-button.component.spec.ts index 036d5acea..5111c8e42 100644 --- a/src/app/shared/components/socials-share-button/socials-share-button.component.spec.ts +++ b/src/app/shared/components/socials-share-button/socials-share-button.component.spec.ts @@ -10,6 +10,8 @@ import { IconComponent } from '../icon/icon.component'; import { SocialsShareButtonComponent } from './socials-share-button.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('SocialsShareButtonComponent', () => { let component: SocialsShareButtonComponent; let fixture: ComponentFixture; @@ -18,7 +20,7 @@ describe('SocialsShareButtonComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SocialsShareButtonComponent, MockComponent(IconComponent), MockPipe(TranslatePipe)], - providers: [MockProvider(SocialShareService)], + providers: [provideOSFCore(), MockProvider(SocialShareService)], }).compileComponents(); fixture = TestBed.createComponent(SocialsShareButtonComponent); diff --git a/src/app/shared/components/statistic-card/statistic-card.component.spec.ts b/src/app/shared/components/statistic-card/statistic-card.component.spec.ts index d18f40e72..7f1d53807 100644 --- a/src/app/shared/components/statistic-card/statistic-card.component.spec.ts +++ b/src/app/shared/components/statistic-card/statistic-card.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { StatisticCardComponent } from './statistic-card.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('StatisticCardComponent', () => { let component: StatisticCardComponent; let fixture: ComponentFixture; @@ -9,6 +11,7 @@ describe('StatisticCardComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [StatisticCardComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(StatisticCardComponent); diff --git a/src/app/shared/components/status-badge/status-badge.component.spec.ts b/src/app/shared/components/status-badge/status-badge.component.spec.ts index 732b6d463..3d4caf4ce 100644 --- a/src/app/shared/components/status-badge/status-badge.component.spec.ts +++ b/src/app/shared/components/status-badge/status-badge.component.spec.ts @@ -4,6 +4,8 @@ import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; import { StatusBadgeComponent } from './status-badge.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('StatusBadgeComponent', () => { let component: StatusBadgeComponent; let fixture: ComponentFixture; @@ -11,6 +13,7 @@ describe('StatusBadgeComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [StatusBadgeComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(StatusBadgeComponent); diff --git a/src/app/shared/components/stepper/stepper.component.spec.ts b/src/app/shared/components/stepper/stepper.component.spec.ts index aa1550679..0d272f369 100644 --- a/src/app/shared/components/stepper/stepper.component.spec.ts +++ b/src/app/shared/components/stepper/stepper.component.spec.ts @@ -8,6 +8,8 @@ import { IconComponent } from '../icon/icon.component'; import { StepperComponent } from './stepper.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('StepperComponent', () => { let component: StepperComponent; let fixture: ComponentFixture; @@ -23,6 +25,7 @@ describe('StepperComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [StepperComponent, MockComponent(IconComponent)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(StepperComponent); diff --git a/src/app/shared/components/sub-header/sub-header.component.spec.ts b/src/app/shared/components/sub-header/sub-header.component.spec.ts index fadd15d00..9a4917ccd 100644 --- a/src/app/shared/components/sub-header/sub-header.component.spec.ts +++ b/src/app/shared/components/sub-header/sub-header.component.spec.ts @@ -8,6 +8,8 @@ import { FixSpecialCharPipe } from '@osf/shared/pipes/fix-special-char.pipe'; import { SubHeaderComponent } from './sub-header.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('SubHeaderComponent', () => { let component: SubHeaderComponent; let fixture: ComponentFixture; @@ -15,6 +17,7 @@ describe('SubHeaderComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [SubHeaderComponent, ...MockPipes(SafeHtmlPipe, FixSpecialCharPipe)], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SubHeaderComponent); diff --git a/src/app/shared/components/subjects-list/subjects-list.component.spec.ts b/src/app/shared/components/subjects-list/subjects-list.component.spec.ts index 9033cb5ea..94b2721c6 100644 --- a/src/app/shared/components/subjects-list/subjects-list.component.spec.ts +++ b/src/app/shared/components/subjects-list/subjects-list.component.spec.ts @@ -4,7 +4,7 @@ import { By } from '@angular/platform-browser'; import { SubjectsListComponent } from './subjects-list.component'; import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('SubjectsListComponent', () => { let component: SubjectsListComponent; @@ -12,7 +12,8 @@ describe('SubjectsListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SubjectsListComponent, OSFTestingModule], + imports: [SubjectsListComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(SubjectsListComponent); diff --git a/src/app/shared/components/subjects/subjects.component.spec.ts b/src/app/shared/components/subjects/subjects.component.spec.ts index 29e8a7866..167436c9b 100644 --- a/src/app/shared/components/subjects/subjects.component.spec.ts +++ b/src/app/shared/components/subjects/subjects.component.spec.ts @@ -10,7 +10,7 @@ import { SearchInputComponent } from '../search-input/search-input.component'; import { SubjectsComponent } from './subjects.component'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('SubjectsComponent', () => { @@ -43,8 +43,9 @@ describe('SubjectsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [SubjectsComponent, OSFTestingStoreModule, MockComponent(SearchInputComponent)], + imports: [SubjectsComponent, MockComponent(SearchInputComponent)], providers: [ + provideOSFCore(), provideMockStore({ signals: [ { selector: SubjectsSelectors.getSubjects, value: signal(mockSubjects) }, diff --git a/src/app/shared/components/tags-input/tags-input.component.spec.ts b/src/app/shared/components/tags-input/tags-input.component.spec.ts index 5813db194..8999e525e 100644 --- a/src/app/shared/components/tags-input/tags-input.component.spec.ts +++ b/src/app/shared/components/tags-input/tags-input.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TagsInputComponent } from './tags-input.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('TagsInputComponent', () => { let component: TagsInputComponent; @@ -10,7 +10,8 @@ describe('TagsInputComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TagsInputComponent, OSFTestingModule], + imports: [TagsInputComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(TagsInputComponent); diff --git a/src/app/shared/components/tags-list/tags-list.component.spec.ts b/src/app/shared/components/tags-list/tags-list.component.spec.ts index 7ace3f459..ce63c1b19 100644 --- a/src/app/shared/components/tags-list/tags-list.component.spec.ts +++ b/src/app/shared/components/tags-list/tags-list.component.spec.ts @@ -3,7 +3,7 @@ import { By } from '@angular/platform-browser'; import { TagsListComponent } from './tags-list.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('TagsListComponent', () => { let component: TagsListComponent; @@ -11,7 +11,8 @@ describe('TagsListComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TagsListComponent, OSFTestingModule], + imports: [TagsListComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(TagsListComponent); diff --git a/src/app/shared/components/text-input/text-input.component.spec.ts b/src/app/shared/components/text-input/text-input.component.spec.ts index a820f858b..209784445 100644 --- a/src/app/shared/components/text-input/text-input.component.spec.ts +++ b/src/app/shared/components/text-input/text-input.component.spec.ts @@ -5,7 +5,7 @@ import { INPUT_VALIDATION_MESSAGES } from '@osf/shared/constants/input-validatio import { TextInputComponent } from './text-input.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('TextInputComponent', () => { let component: TextInputComponent; @@ -13,7 +13,8 @@ describe('TextInputComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [TextInputComponent, OSFTestingModule], + imports: [TextInputComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(TextInputComponent); diff --git a/src/app/shared/components/toast/toast.component.spec.ts b/src/app/shared/components/toast/toast.component.spec.ts index 0a64d913c..ca2b99a2a 100644 --- a/src/app/shared/components/toast/toast.component.spec.ts +++ b/src/app/shared/components/toast/toast.component.spec.ts @@ -8,7 +8,7 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { ToastComponent } from './toast.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ToastComponent', () => { let component: ToastComponent; @@ -17,7 +17,7 @@ describe('ToastComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ToastComponent, MockModule(ToastModule)], - providers: [TranslateServiceMock, MockProvider(ToastService)], + providers: [provideOSFCore(), MockProvider(ToastService)], }).compileComponents(); fixture = TestBed.createComponent(ToastComponent); diff --git a/src/app/shared/components/truncated-text/truncated-text.component.spec.ts b/src/app/shared/components/truncated-text/truncated-text.component.spec.ts index cf1b72f87..7defba4d9 100644 --- a/src/app/shared/components/truncated-text/truncated-text.component.spec.ts +++ b/src/app/shared/components/truncated-text/truncated-text.component.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { TruncatedTextComponent } from './truncated-text.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('TruncatedTextComponent', () => { let component: TruncatedTextComponent; @@ -11,7 +11,7 @@ describe('TruncatedTextComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [TruncatedTextComponent], - providers: [TranslateServiceMock], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(TruncatedTextComponent); diff --git a/src/app/shared/components/view-only-link-message/view-only-link-message.component.spec.ts b/src/app/shared/components/view-only-link-message/view-only-link-message.component.spec.ts index bcec43b12..16428d8b4 100644 --- a/src/app/shared/components/view-only-link-message/view-only-link-message.component.spec.ts +++ b/src/app/shared/components/view-only-link-message/view-only-link-message.component.spec.ts @@ -1,136 +1,55 @@ +import { MockProvider } from 'ng-mocks'; + +import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { ViewOnlyLinkMessageComponent } from './view-only-link-message.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; describe('ViewOnlyLinkMessageComponent', () => { let component: ViewOnlyLinkMessageComponent; let fixture: ComponentFixture; + let routerMock: RouterMockType; + + function setup(platformId: 'browser' | 'server', navigateMock?: jest.Mock>) { + routerMock = navigateMock + ? RouterMockBuilder.create().withNavigate(navigateMock).build() + : RouterMockBuilder.create().build(); - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [ViewOnlyLinkMessageComponent, OSFTestingModule], - }).compileComponents(); + TestBed.configureTestingModule({ + imports: [ViewOnlyLinkMessageComponent], + providers: [provideOSFCore(), MockProvider(Router, routerMock), MockProvider(PLATFORM_ID, platformId)], + }); fixture = TestBed.createComponent(ViewOnlyLinkMessageComponent); component = fixture.componentInstance; - fixture.detectChanges(); - }); + } it('should create', () => { + setup('server'); expect(component).toBeTruthy(); }); - describe('handleLeaveViewOnlyView', () => { - let originalLocation: Location; - let mockPushState: jest.SpyInstance; - let mockReload: jest.SpyInstance; - - beforeEach(() => { - originalLocation = window.location; - - delete (window as any).location; - window.location = { - ...originalLocation, - href: 'https://example.com/project/abc123?view_only=test123&other=param', - reload: jest.fn(), - } as any; - - mockPushState = jest.spyOn(window.history, 'pushState').mockImplementation(() => {}); - mockReload = window.location.reload as jest.Mock; - }); - - afterEach(() => { - window.location = originalLocation; - mockPushState.mockRestore(); - }); - - it('should remove view_only parameter from URL', () => { - component.handleLeaveViewOnlyView(); - - expect(mockPushState).toHaveBeenCalled(); - const [, , newUrl] = mockPushState.mock.calls[0]; - - expect(newUrl).not.toContain('view_only'); - expect(newUrl).toContain('other=param'); - }); + it('should not navigate outside browser platform', () => { + setup('server'); - it('should call window.history.pushState with correct parameters', () => { - component.handleLeaveViewOnlyView(); + component.handleLeaveViewOnlyView(); - expect(mockPushState).toHaveBeenCalledWith(null, '', expect.any(String)); - }); - - it('should call window.location.reload', () => { - component.handleLeaveViewOnlyView(); - - expect(mockReload).toHaveBeenCalled(); - }); - - it('should handle URL without view_only parameter', () => { - window.location = { - ...originalLocation, - href: 'https://example.com/project/abc123?other=param', - reload: jest.fn(), - } as any; - - mockReload = window.location.reload as jest.Mock; - - expect(() => component.handleLeaveViewOnlyView()).not.toThrow(); - expect(mockPushState).toHaveBeenCalled(); - expect(mockReload).toHaveBeenCalled(); - }); - - it('should handle URL with only view_only parameter', () => { - window.location = { - ...originalLocation, - href: 'https://example.com/project/abc123?view_only=test123', - reload: jest.fn(), - } as any; - - mockReload = window.location.reload as jest.Mock; - - component.handleLeaveViewOnlyView(); - - expect(mockPushState).toHaveBeenCalled(); - const [, , newUrl] = mockPushState.mock.calls[0]; - - expect(newUrl).not.toContain('view_only'); - expect(newUrl).not.toContain('?'); - expect(mockReload).toHaveBeenCalled(); - }); - - it('should preserve other query parameters', () => { - window.location = { - ...originalLocation, - href: 'https://example.com/project/abc123?view_only=test123¶m1=value1¶m2=value2', - reload: jest.fn(), - } as any; - - mockReload = window.location.reload as jest.Mock; - - component.handleLeaveViewOnlyView(); - - const [, , newUrl] = mockPushState.mock.calls[0]; - - expect(newUrl).toContain('param1=value1'); - expect(newUrl).toContain('param2=value2'); - expect(newUrl).not.toContain('view_only'); - }); + expect(routerMock.navigate).not.toHaveBeenCalled(); + }); - it('should handle URL without query parameters', () => { - window.location = { - ...originalLocation, - href: 'https://example.com/project/abc123', - reload: jest.fn(), - } as any; + it('should navigate in browser platform', () => { + const navigateMock = jest.fn, [unknown[], unknown?]>(() => new Promise(() => {})); + setup('browser', navigateMock); - mockReload = window.location.reload as jest.Mock; + component.handleLeaveViewOnlyView(); - expect(() => component.handleLeaveViewOnlyView()).not.toThrow(); - expect(mockPushState).toHaveBeenCalled(); - expect(mockReload).toHaveBeenCalled(); + expect(navigateMock).toHaveBeenCalledWith([], { + queryParams: { view_only: null }, + queryParamsHandling: 'merge', }); }); }); diff --git a/src/app/shared/components/view-only-link-message/view-only-link-message.component.ts b/src/app/shared/components/view-only-link-message/view-only-link-message.component.ts index 643790381..25b52ea7d 100644 --- a/src/app/shared/components/view-only-link-message/view-only-link-message.component.ts +++ b/src/app/shared/components/view-only-link-message/view-only-link-message.component.ts @@ -5,6 +5,7 @@ import { Message } from 'primeng/message'; import { isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, inject, PLATFORM_ID } from '@angular/core'; +import { Router } from '@angular/router'; @Component({ selector: 'osf-view-only-link-message', @@ -14,17 +15,19 @@ import { ChangeDetectionStrategy, Component, inject, PLATFORM_ID } from '@angula changeDetection: ChangeDetectionStrategy.OnPush, }) export class ViewOnlyLinkMessageComponent { - private platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); + private readonly router = inject(Router); handleLeaveViewOnlyView(): void { - if (!isPlatformBrowser(this.platformId)) { + if (!this.isBrowser) { return; } - const currentUrl = new URL(window.location.href); - currentUrl.searchParams.delete('view_only'); - - window.history.pushState(null, '', currentUrl.toString()); - window.location.reload(); + this.router + .navigate([], { + queryParams: { view_only: null }, + queryParamsHandling: 'merge', + }) + .then(() => window.location.reload()); } } diff --git a/src/app/shared/components/view-only-table/view-only-table.component.spec.ts b/src/app/shared/components/view-only-table/view-only-table.component.spec.ts index 47a22eb90..928d4458e 100644 --- a/src/app/shared/components/view-only-table/view-only-table.component.spec.ts +++ b/src/app/shared/components/view-only-table/view-only-table.component.spec.ts @@ -8,8 +8,8 @@ import { CopyButtonComponent } from '../copy-button/copy-button.component'; import { ViewOnlyTableComponent } from './view-only-table.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; import { MOCK_PAGINATED_VIEW_ONLY_LINKS, MOCK_VIEW_ONLY_LINK } from '@testing/mocks/view-only-link.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ViewOnlyTableComponent', () => { let component: ViewOnlyTableComponent; @@ -21,7 +21,7 @@ describe('ViewOnlyTableComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ViewOnlyTableComponent, MockComponent(CopyButtonComponent)], - providers: [TranslateServiceMock], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ViewOnlyTableComponent); diff --git a/src/app/shared/components/wiki/add-wiki-dialog/add-wiki-dialog.component.spec.ts b/src/app/shared/components/wiki/add-wiki-dialog/add-wiki-dialog.component.spec.ts index be22defa6..d5ae412b0 100644 --- a/src/app/shared/components/wiki/add-wiki-dialog/add-wiki-dialog.component.spec.ts +++ b/src/app/shared/components/wiki/add-wiki-dialog/add-wiki-dialog.component.spec.ts @@ -1,5 +1,3 @@ -import { Store } from '@ngxs/store'; - import { MockComponent, MockProvider } from 'ng-mocks'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; @@ -13,25 +11,26 @@ import { TextInputComponent } from '../../text-input/text-input.component'; import { AddWikiDialogComponent } from './add-wiki-dialog.component'; -import { MOCK_STORE } from '@testing/mocks/mock-store.mock'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('AddWikiDialogComponent', () => { let component: AddWikiDialogComponent; let fixture: ComponentFixture; - beforeEach(async () => { - (MOCK_STORE.selectSignal as jest.Mock).mockImplementation((selector) => { - if (selector === WikiSelectors.getWikiSubmitting) { - return () => false; - } - return () => null; - }); - - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [AddWikiDialogComponent, MockComponent(TextInputComponent)], providers: [ - TranslateServiceMock, + provideOSFCore(), + provideMockStore({ + signals: [ + { + selector: WikiSelectors.getWikiSubmitting, + value: false, + }, + ], + }), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig, { data: { @@ -39,9 +38,8 @@ describe('AddWikiDialogComponent', () => { }, }), MockProvider(ToastService), - MockProvider(Store, MOCK_STORE), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(AddWikiDialogComponent); component = fixture.componentInstance; diff --git a/src/app/shared/components/wiki/compare-section/compare-section.component.spec.ts b/src/app/shared/components/wiki/compare-section/compare-section.component.spec.ts index 2dc8dc08a..1d2880187 100644 --- a/src/app/shared/components/wiki/compare-section/compare-section.component.spec.ts +++ b/src/app/shared/components/wiki/compare-section/compare-section.component.spec.ts @@ -1,194 +1,104 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { WikiVersion } from '@shared/models/wiki/wiki.model'; +import { WikiVersion } from '@osf/shared/models/wiki/wiki.model'; import { CompareSectionComponent } from './compare-section.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; +import * as Diff from 'diff'; describe('CompareSectionComponent', () => { let component: CompareSectionComponent; let fixture: ComponentFixture; - let translateServiceMock: any; - const mockVersions: WikiVersion[] = [ + const versions: WikiVersion[] = [ { - id: 'version-1', - createdAt: '2024-01-01T10:00:00Z', - createdBy: 'John Doe', + id: 'v3', + createdAt: '2024-01-03T10:30:00.000Z', + createdBy: 'Alice', }, { - id: 'version-2', - createdAt: '2024-01-02T10:00:00Z', - createdBy: 'Jane Smith', - }, - { - id: 'version-3', - createdAt: '2024-01-03T10:00:00Z', - createdBy: 'Bob Johnson', + id: 'v2', + createdAt: '2024-01-02T10:30:00.000Z', + createdBy: undefined, }, ]; - const mockVersionContent = 'Original content'; - const mockPreviewContent = 'Updated content with changes'; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [CompareSectionComponent, OSFTestingModule], - providers: [TranslateServiceMock], - }).compileComponents(); - - translateServiceMock = TestBed.inject(TranslateServiceMock.provide); - translateServiceMock.instant.mockReturnValue('Current'); + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CompareSectionComponent], + providers: [provideOSFCore()], + }); fixture = TestBed.createComponent(CompareSectionComponent); component = fixture.componentInstance; - - fixture.componentRef.setInput('versions', mockVersions); - fixture.componentRef.setInput('versionContent', mockVersionContent); - fixture.componentRef.setInput('previewContent', mockPreviewContent); + fixture.componentRef.setInput('versions', versions); + fixture.componentRef.setInput('versionContent', 'old text'); + fixture.componentRef.setInput('previewContent', 'new text'); fixture.componentRef.setInput('isLoading', false); fixture.detectChanges(); }); - it('should set versions input', () => { - expect(component.versions()).toEqual(mockVersions); - }); - - it('should set versionContent input', () => { - expect(component.versionContent()).toBe(mockVersionContent); - }); - - it('should set previewContent input', () => { - expect(component.previewContent()).toBe(mockPreviewContent); - }); - - it('should set isLoading input', () => { - expect(component.isLoading()).toBe(false); - }); - - it('should handle empty versions array', () => { - fixture.componentRef.setInput('versions', []); - fixture.detectChanges(); - - expect(component.versions()).toEqual([]); - expect(component.selectedVersion).toBeUndefined(); - }); - - it('should initialize with first version selected and emit selectVersion', () => { - expect(component.selectedVersion).toBe(mockVersions[0].id); + it('should create', () => { + expect(component).toBeTruthy(); }); - it('should not emit when no versions available', () => { + it('should emit first version id on init and set selectedVersion', () => { const emitSpy = jest.spyOn(component.selectVersion, 'emit'); - - fixture.componentRef.setInput('versions', []); - fixture.detectChanges(); - - expect(component.selectedVersion).toBeUndefined(); - expect(emitSpy).not.toHaveBeenCalled(); - }); - - it('should map versions correctly', () => { - const mappedVersions = component.mappedVersions(); - - expect(mappedVersions).toHaveLength(3); - expect(mappedVersions[0].value).toBe('version-1'); - expect(mappedVersions[0].label).toContain('(Current)'); - expect(mappedVersions[0].label).toContain('John Doe'); - expect(mappedVersions[1].value).toBe('version-2'); - expect(mappedVersions[1].label).toContain('(2)'); - expect(mappedVersions[1].label).toContain('Jane Smith'); - expect(mappedVersions[2].value).toBe('version-3'); - expect(mappedVersions[2].label).toContain('(1)'); - expect(mappedVersions[2].label).toContain('Bob Johnson'); - }); - - it('should handle version with undefined createdBy', () => { - const versionsWithUndefinedCreator: WikiVersion[] = [ + const nextVersions: WikiVersion[] = [ { - id: 'version-1', - createdAt: '2024-01-01T10:00:00Z', - createdBy: undefined, + id: 'v9', + createdAt: '2024-01-09T10:30:00.000Z', + createdBy: 'Bob', }, + ...versions, ]; - fixture.componentRef.setInput('versions', versionsWithUndefinedCreator); + fixture.componentRef.setInput('versions', nextVersions); fixture.detectChanges(); - const mappedVersions = component.mappedVersions(); - expect(mappedVersions).toHaveLength(1); - expect(mappedVersions[0].value).toBe('version-1'); - expect(mappedVersions[0].label).toContain('(Current)'); - expect(mappedVersions[0].label).toContain('1/1/2024'); + expect(component.selectedVersion).toBe('v9'); + expect(emitSpy).toHaveBeenCalledWith('v9'); }); - it('should handle single version', () => { - const singleVersion = [mockVersions[0]]; - fixture.componentRef.setInput('versions', singleVersion); - fixture.detectChanges(); + it('should map versions with current label and unknown author fallback', () => { + const mapped = component.mappedVersions(); - const mappedVersions = component.mappedVersions(); - expect(mappedVersions).toHaveLength(1); - expect(mappedVersions[0].label).toContain('(Current)'); + expect(mapped.length).toBe(2); + expect(mapped[0].value).toBe('v3'); + expect(mapped[0].label).toContain('(project.wiki.version.current)'); + expect(mapped[0].label).toContain('Alice'); + expect(mapped[1].label).toContain('project.wiki.version.unknownAuthor'); }); - it('should compute content diff correctly', () => { - const content = component.content(); + it('should update selectedVersion and emit on version change', () => { + const emitSpy = jest.spyOn(component.selectVersion, 'emit'); - expect(content).toContain('Original'); - expect(content).toContain('Updated'); - expect(content).toContain('content'); - expect(content).toContain('with changes'); - }); + component.onVersionChange('v2'); - it('should handle identical content', () => { - fixture.componentRef.setInput('previewContent', mockVersionContent); - fixture.detectChanges(); - - const content = component.content(); - expect(content).toBe(mockVersionContent); + expect(component.selectedVersion).toBe('v2'); + expect(emitSpy).toHaveBeenCalledWith('v2'); }); - it('should handle empty version content', () => { - fixture.componentRef.setInput('versionContent', ''); - fixture.detectChanges(); - - const content = component.content(); - expect(content).toContain('Updated content with changes'); - }); + it('should render diff words with added and removed wrappers', () => { + jest.spyOn(Diff, 'diffWords').mockReturnValue([ + { value: 'same ', added: false, removed: false, count: 1 }, + { value: 'removed ', added: false, removed: true, count: 1 }, + { value: 'added', added: true, removed: false, count: 1 }, + ]); - it('should handle empty preview content', () => { - fixture.componentRef.setInput('previewContent', ''); + fixture.componentRef.setInput('versionContent', 'same removed'); + fixture.componentRef.setInput('previewContent', 'same added'); fixture.detectChanges(); - const content = component.content(); - expect(content).toContain('Original content'); - }); - - it('should update selectedVersion and emit selectVersion', () => { - const emitSpy = jest.spyOn(component.selectVersion, 'emit'); - const versionId = 'version-2'; - - component.onVersionChange(versionId); - - expect(component.selectedVersion).toBe(versionId); - expect(emitSpy).toHaveBeenCalledWith(versionId); - expect(emitSpy).toHaveBeenCalledTimes(1); + expect(component.content()).toBe('same removed added'); }); - it('should emit correct version id when called multiple times', () => { - const emitSpy = jest.spyOn(component.selectVersion, 'emit'); - - component.onVersionChange('version-2'); - component.onVersionChange('version-3'); - component.onVersionChange('version-1'); + it('should render loading skeletons when isLoading is true', () => { + fixture.componentRef.setInput('isLoading', true); + fixture.detectChanges(); - expect(component.selectedVersion).toBe('version-1'); - expect(emitSpy).toHaveBeenCalledTimes(3); - expect(emitSpy).toHaveBeenNthCalledWith(1, 'version-2'); - expect(emitSpy).toHaveBeenNthCalledWith(2, 'version-3'); - expect(emitSpy).toHaveBeenNthCalledWith(3, 'version-1'); + const skeletons = fixture.nativeElement.querySelectorAll('p-skeleton'); + expect(skeletons.length).toBeGreaterThan(0); }); }); diff --git a/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts b/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts index e6c5faf38..e2029b09f 100644 --- a/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts +++ b/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts @@ -9,7 +9,7 @@ import { WikiSyntaxHelpDialogComponent } from '../wiki-syntax-help-dialog/wiki-s import { EditSectionComponent } from './edit-section.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; describe('EditSectionComponent', () => { @@ -35,8 +35,8 @@ describe('EditSectionComponent', () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); await TestBed.configureTestingModule({ - imports: [EditSectionComponent, OSFTestingModule, MockModule(LMarkdownEditorModule)], - providers: [MockProvider(CustomDialogService, mockCustomDialogService)], + imports: [EditSectionComponent, MockModule(LMarkdownEditorModule)], + providers: [provideOSFCore(), MockProvider(CustomDialogService, mockCustomDialogService)], }).compileComponents(); fixture = TestBed.createComponent(EditSectionComponent); diff --git a/src/app/shared/components/wiki/rename-wiki-dialog/rename-wiki-dialog.component.spec.ts b/src/app/shared/components/wiki/rename-wiki-dialog/rename-wiki-dialog.component.spec.ts index 56399a64d..7caecf037 100644 --- a/src/app/shared/components/wiki/rename-wiki-dialog/rename-wiki-dialog.component.spec.ts +++ b/src/app/shared/components/wiki/rename-wiki-dialog/rename-wiki-dialog.component.spec.ts @@ -11,7 +11,7 @@ import { TextInputComponent } from '../../text-input/text-input.component'; import { RenameWikiDialogComponent } from './rename-wiki-dialog.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('RenameWikiDialogComponent', () => { @@ -22,7 +22,7 @@ describe('RenameWikiDialogComponent', () => { await TestBed.configureTestingModule({ imports: [RenameWikiDialogComponent, MockComponent(TextInputComponent)], providers: [ - TranslateServiceMock, + provideOSFCore(), MockProvider(DynamicDialogRef), MockProvider(DynamicDialogConfig, { data: { diff --git a/src/app/shared/components/wiki/view-section/view-section.component.spec.ts b/src/app/shared/components/wiki/view-section/view-section.component.spec.ts index e1cb969be..b55fb8523 100644 --- a/src/app/shared/components/wiki/view-section/view-section.component.spec.ts +++ b/src/app/shared/components/wiki/view-section/view-section.component.spec.ts @@ -4,8 +4,7 @@ import { WikiVersion } from '@shared/models/wiki/wiki.model'; import { ViewSectionComponent } from './view-section.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ViewSectionComponent', () => { let component: ViewSectionComponent; @@ -29,8 +28,8 @@ describe('ViewSectionComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [ViewSectionComponent, OSFTestingModule], - providers: [TranslateServiceMock], + imports: [ViewSectionComponent], + providers: [provideOSFCore()], }).compileComponents(); fixture = TestBed.createComponent(ViewSectionComponent); diff --git a/src/app/shared/components/wiki/wiki-list/wiki-list.component.spec.ts b/src/app/shared/components/wiki/wiki-list/wiki-list.component.spec.ts index 818a3a412..8f3c8af72 100644 --- a/src/app/shared/components/wiki/wiki-list/wiki-list.component.spec.ts +++ b/src/app/shared/components/wiki/wiki-list/wiki-list.component.spec.ts @@ -13,7 +13,7 @@ import { ComponentWiki } from '@osf/shared/stores/wiki'; import { WikiListComponent } from './wiki-list.component'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMockBuilder } from '@testing/providers/custom-confirmation-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -47,8 +47,9 @@ describe('WikiListComponent', () => { mockRouter = RouterMockBuilder.create().withUrl('/project/abc123/wiki').build(); await TestBed.configureTestingModule({ - imports: [WikiListComponent, OSFTestingModule], + imports: [WikiListComponent], providers: [ + provideOSFCore(), MockProvider(CustomDialogService), MockProvider(CustomConfirmationService, mockCustomConfirmationService), MockProvider(Router, mockRouter), diff --git a/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.spec.ts b/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.spec.ts index e5b1465de..682dd1142 100644 --- a/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.spec.ts +++ b/src/app/shared/components/wiki/wiki-syntax-help-dialog/wiki-syntax-help-dialog.component.spec.ts @@ -6,7 +6,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { WikiSyntaxHelpDialogComponent } from './wiki-syntax-help-dialog.component'; -import { TranslateServiceMock } from '@testing/mocks/translate.service.mock'; +import { provideOSFCore } from '@testing/osf.testing.provider'; describe('WikiSyntaxHelpDialogComponent', () => { let component: WikiSyntaxHelpDialogComponent; @@ -15,7 +15,7 @@ describe('WikiSyntaxHelpDialogComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [WikiSyntaxHelpDialogComponent], - providers: [TranslateServiceMock, MockProvider(DynamicDialogRef)], + providers: [provideOSFCore(), MockProvider(DynamicDialogRef)], }).compileComponents(); fixture = TestBed.createComponent(WikiSyntaxHelpDialogComponent); diff --git a/src/app/shared/funder-awards-list/funder-awards-list.component.spec.ts b/src/app/shared/funder-awards-list/funder-awards-list.component.spec.ts deleted file mode 100644 index d066f8152..000000000 --- a/src/app/shared/funder-awards-list/funder-awards-list.component.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { provideRouter } from '@angular/router'; - -import { FunderAwardsListComponent } from './funder-awards-list.component'; - -import { MOCK_FUNDERS } from '@testing/mocks/funder.mock'; -import { OSFTestingModule } from '@testing/osf.testing.module'; - -describe('FunderAwardsListComponent', () => { - let component: FunderAwardsListComponent; - let fixture: ComponentFixture; - - const MOCK_REGISTRY_ID = 'test-registry-123'; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [FunderAwardsListComponent, OSFTestingModule], - providers: [provideRouter([])], - }).compileComponents(); - - fixture = TestBed.createComponent(FunderAwardsListComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should not render the list or label if funders array is empty', () => { - fixture.componentRef.setInput('funders', []); - fixture.detectChanges(); - const label = fixture.debugElement.query(By.css('p')); - const links = fixture.debugElement.queryAll(By.css('a')); - expect(label).toBeNull(); - expect(links.length).toBe(0); - }); - - it('should render a list of funders when data is provided', () => { - fixture.componentRef.setInput('funders', MOCK_FUNDERS); - fixture.componentRef.setInput('registryId', MOCK_REGISTRY_ID); - fixture.detectChanges(); - const links = fixture.debugElement.queryAll(By.css('a')); - expect(links.length).toBe(2); - const firstItemText = links[0].nativeElement.textContent; - expect(firstItemText).toContain('National Science Foundation'); - expect(firstItemText).toContain('NSF-1234567'); - }); - - it('should generate the correct router link', () => { - fixture.componentRef.setInput('funders', MOCK_FUNDERS); - fixture.componentRef.setInput('registryId', MOCK_REGISTRY_ID); - fixture.detectChanges(); - const linkDebugEl = fixture.debugElement.query(By.css('a')); - const href = linkDebugEl.nativeElement.getAttribute('href'); - expect(href).toContain(`/${MOCK_REGISTRY_ID}/metadata/osf`); - }); - - it('should open links in a new tab', () => { - fixture.componentRef.setInput('funders', MOCK_FUNDERS); - fixture.detectChanges(); - const linkDebugEl = fixture.debugElement.query(By.css('a')); - expect(linkDebugEl.attributes['target']).toBe('_blank'); - }); -}); diff --git a/src/app/shared/services/activity-logs/activity-logs.service.spec.ts b/src/app/shared/services/activity-logs/activity-logs.service.spec.ts index 2cb8979f4..5243945a4 100644 --- a/src/app/shared/services/activity-logs/activity-logs.service.spec.ts +++ b/src/app/shared/services/activity-logs/activity-logs.service.spec.ts @@ -1,3 +1,5 @@ +import { MockProvider } from 'ng-mocks'; + import { HttpTestingController } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; @@ -11,20 +13,22 @@ import { buildRegistrationLogsUrl, getActivityLogsResponse, } from '@testing/data/activity-logs/activity-logs.data'; -import { EnvironmentTokenMock } from '@testing/mocks/environment.token.mock'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; +import { EnvironmentTokenMock } from '@testing/providers/environment.token.mock'; describe('Service: ActivityLogs', () => { let service: ActivityLogsService; const environment = EnvironmentTokenMock; const apiBase = environment.useValue.apiDomainUrl; + const activityLogDisplayServiceMock = { getActivityDisplay: jest.fn().mockReturnValue('FMT') }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [OSFTestingStoreModule], providers: [ + provideOSFCore(), + provideOSFHttp(), ActivityLogsService, - { provide: ActivityLogDisplayService, useValue: { getActivityDisplay: jest.fn().mockReturnValue('FMT') } }, + MockProvider(ActivityLogDisplayService, activityLogDisplayServiceMock), ], }); service = TestBed.inject(ActivityLogsService); diff --git a/src/app/shared/services/addons/addons.service.spec.ts b/src/app/shared/services/addons/addons.service.spec.ts index 46aa097e8..561440064 100644 --- a/src/app/shared/services/addons/addons.service.spec.ts +++ b/src/app/shared/services/addons/addons.service.spec.ts @@ -6,15 +6,15 @@ import { AddonsService } from './addons.service'; import { getAddonsAuthorizedStorageData } from '@testing/data/addons/addons.authorized-storage.data'; import { getConfiguredAddonsData } from '@testing/data/addons/addons.configured.data'; import { getAddonsExternalStorageData } from '@testing/data/addons/addons.external-storage.data'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('Service: Addons', () => { let service: AddonsService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [OSFTestingStoreModule], - providers: [AddonsService], + providers: [provideOSFCore(), provideOSFHttp(), provideMockStore(), AddonsService], }); service = TestBed.inject(AddonsService); diff --git a/src/app/shared/services/banners.service.spec.ts b/src/app/shared/services/banners.service.spec.ts index 5787eae31..fd71cd0c1 100644 --- a/src/app/shared/services/banners.service.spec.ts +++ b/src/app/shared/services/banners.service.spec.ts @@ -6,15 +6,14 @@ import { BannerModel } from '@core/components/osf-banners/models/banner.model'; import { BannersService } from './banners.service'; import { getScheduledBannerData } from '@testing/data/banners/scheduled.banner.data'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; describe('Service: Banners', () => { let service: BannersService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [OSFTestingModule], - providers: [BannersService], + providers: [provideOSFCore(), provideOSFHttp(), BannersService], }); service = TestBed.inject(BannersService); diff --git a/src/app/shared/services/datacite/datacite.service.spec.ts b/src/app/shared/services/datacite/datacite.service.spec.ts index 229f0db72..139835c55 100644 --- a/src/app/shared/services/datacite/datacite.service.spec.ts +++ b/src/app/shared/services/datacite/datacite.service.spec.ts @@ -1,227 +1,203 @@ -import { Observable, take } from 'rxjs'; +import { of } from 'rxjs'; -import { provideHttpClient } from '@angular/common/http'; -import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { PLATFORM_ID } from '@angular/core'; import { TestBed } from '@angular/core/testing'; +import { BYPASS_ERROR_INTERCEPTOR } from '@core/interceptors/error-interceptor.tokens'; import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; import { DataciteEvent } from '@osf/shared/enums/datacite/datacite-event.enum'; -import { IdentifierModel } from '@shared/models/identifiers/identifier.model'; +import { EnvironmentModel } from '@osf/shared/models/environment.model'; +import { IdentifierModel } from '@osf/shared/models/identifiers/identifier.model'; +import { IdentifiersResponseJsonApi } from '@osf/shared/models/identifiers/identifier-json-api.model'; import { DataciteService } from './datacite.service'; -function buildObservable(doi: string) { - return new Observable<{ identifiers?: IdentifierModel[] } | null>((subscriber) => { - subscriber.next({}); - subscriber.next({ identifiers: [] }); - subscriber.next({ - identifiers: [ - { - category: 'doi', - value: doi, - id: '', - type: 'identifier', - }, - ], - }); - subscriber.next({ - identifiers: [ - { - category: 'doi', - value: 'other doi', - id: '', - type: 'identifier', - }, - ], - }); - subscriber.complete(); - }); -} - -function assertSuccess( - httpMock: HttpTestingController, - dataciteTrackerAddress: string, - dataciteTrackerRepoId: string, - doi: string, - event: DataciteEvent -) { - assertSendBeacon(dataciteTrackerAddress, dataciteTrackerRepoId, doi, event); - const req = httpMock.expectOne(dataciteTrackerAddress); - expect(req.request.method).toBe('POST'); - expect(req.request.body).toEqual({ - n: event, - u: window.location.href, - i: dataciteTrackerRepoId, - p: doi, - }); - expect(req.request.headers.get('Content-Type')).toBe('application/json'); - req.flush({}); -} - -function assertSendBeacon( - dataciteTrackerAddress: string, - dataciteTrackerRepoId: string, - doi: string, - event: DataciteEvent -) { - expect(navigator.sendBeacon).toBeCalledTimes(1); - expect(navigator.sendBeacon).toHaveBeenCalledWith( - dataciteTrackerAddress, - JSON.stringify({ - n: event, - u: window.location.href, - i: dataciteTrackerRepoId, - p: doi, - }) - ); -} +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; -describe('DataciteService', () => { +describe('Service: Datacite', () => { let service: DataciteService; - let sentry: jest.Mocked; let httpMock: HttpTestingController; - - const dataciteTrackerAddress = 'https://tracker.test'; - const apiDomainUrl = 'https://osf.io'; - const dataciteTrackerRepoId = 'repo-123'; - describe('with proper configuration', () => { - beforeEach(() => { - Object.defineProperty(navigator, 'sendBeacon', { - configurable: true, - value: jest.fn(() => false), - }); - TestBed.configureTestingModule({ - providers: [ - DataciteService, - provideHttpClient(), - provideHttpClientTesting(), - { provide: SENTRY_TOKEN, useValue: sentry }, - { - provide: ENVIRONMENT, - useValue: { - apiDomainUrl, - dataciteTrackerRepoId, - dataciteTrackerAddress, - }, - }, - ], - }); - - service = TestBed.inject(DataciteService); - httpMock = TestBed.inject(HttpTestingController); + let environment: EnvironmentModel; + + const trackerAddress = 'https://analytics.datacite.org/api/metric'; + const trackerRepoId = 'repo-id'; + const doi = '10.1234/example-doi'; + + const trackable = (identifiers: IdentifierModel[]) => of<{ identifiers?: IdentifierModel[] } | null>({ identifiers }); + const setSendBeacon = (value: boolean) => { + const mock = jest.fn().mockReturnValue(value); + Object.defineProperty(window.navigator, 'sendBeacon', { + value: mock, + configurable: true, + writable: true, }); + return mock; + }; - afterEach(() => { - httpMock.verify(); + const setup = (platformId: 'browser' | 'server' = 'browser') => { + TestBed.configureTestingModule({ + providers: [provideOSFCore(), provideOSFHttp(), DataciteService, { provide: PLATFORM_ID, useValue: platformId }], }); - it('logIdentifiableView should POST with correct payload', () => { - const doi = '10.1234/abcd'; - const observable = buildObservable(doi); - service.logIdentifiableView(observable).subscribe(); - assertSuccess(httpMock, dataciteTrackerAddress, dataciteTrackerRepoId, doi, DataciteEvent.VIEW); - }); + service = TestBed.inject(DataciteService); + httpMock = TestBed.inject(HttpTestingController); + environment = TestBed.inject(ENVIRONMENT); + environment.dataciteTrackerAddress = trackerAddress; + environment.dataciteTrackerRepoId = trackerRepoId; + }; - it('logIdentifiableView should notPOST without correct payload', () => { - const doi = '10.1234/abcd'; - const observable = buildObservable(doi).pipe(take(2)); - service.logIdentifiableView(observable).subscribe(); - httpMock.expectNone(dataciteTrackerAddress); - }); + afterEach(() => { + httpMock.verify(); + }); - it('logIdentifiableDownload should POST with correct payload', () => { - const doi = '10.1234/abcd'; - const observable = buildObservable(doi); - service.logIdentifiableDownload(observable).subscribe(); - assertSuccess(httpMock, dataciteTrackerAddress, dataciteTrackerRepoId, doi, DataciteEvent.DOWNLOAD); - }); - it('logFileView should GET identifiers and POST with correct payload', () => { - const doi = '10.1234/fileview'; - const targetId = 'file-1'; - const targetType = 'files'; - - service.logFileView(targetId, targetType).subscribe(); - - const reqGet = httpMock.expectOne(`${apiDomainUrl}/v2/${targetType}/${targetId}/identifiers`); - expect(reqGet.request.method).toBe('GET'); - reqGet.flush({ - data: [ - { - id: 'id-1', - type: 'identifier', - attributes: { category: 'doi', value: doi }, - }, - ], - }); + it('should expose environment values', () => { + setup(); - assertSuccess(httpMock, dataciteTrackerAddress, dataciteTrackerRepoId, doi, DataciteEvent.VIEW); - }); + expect(service.apiDomainUrl).toBe(environment.apiDomainUrl); + expect(service.dataciteTrackerAddress).toBe(trackerAddress); + expect(service.dataciteTrackerRepoId).toBe(trackerRepoId); + }); - it('logFileDownload should GET identifiers and POST with correct payload', () => { - const doi = '10.1234/filedownload'; - const targetId = 'file-2'; - const targetType = 'files'; - - service.logFileDownload(targetId, targetType).subscribe(); - - const reqGet = httpMock.expectOne(`${apiDomainUrl}/v2/${targetType}/${targetId}/identifiers`); - expect(reqGet.request.method).toBe('GET'); - reqGet.flush({ - data: [ - { - id: 'id-2', - type: 'identifier', - attributes: { category: 'doi', value: doi }, - }, - ], + it('should log identifiable view with sendBeacon when doi exists', () => { + setup(); + const sendBeaconSpy = setSendBeacon(true); + + let emitted = false; + service + .logIdentifiableView( + trackable([ + { id: '1', type: 'identifiers', category: 'doi', value: doi }, + { id: '2', type: 'identifiers', category: 'doi', value: '10.9999/second-doi' }, + ]) + ) + .subscribe(() => { + emitted = true; }); - assertSuccess(httpMock, dataciteTrackerAddress, dataciteTrackerRepoId, doi, DataciteEvent.DOWNLOAD); - }); + expect(emitted).toBe(true); + expect(sendBeaconSpy).toHaveBeenCalledTimes(1); + expect(sendBeaconSpy).toHaveBeenCalledWith( + trackerAddress, + JSON.stringify({ + n: DataciteEvent.VIEW, + u: window.location.href, + i: trackerRepoId, + p: doi, + }) + ); + }); - it('navigator success', () => { - (navigator.sendBeacon as jest.Mock).mockReturnValueOnce(true); + it('should fallback to http post when sendBeacon fails', () => { + setup(); + const sendBeaconSpy = setSendBeacon(false); + let emitted = false; - const doi = 'qwerty'; - const event = DataciteEvent.VIEW; - service.logIdentifiableView(buildObservable(doi)).subscribe(); + service + .logIdentifiableDownload(trackable([{ id: '1', type: 'identifiers', category: 'doi', value: doi }])) + .subscribe(() => { + emitted = true; + }); - httpMock.expectNone(dataciteTrackerAddress); - assertSendBeacon(dataciteTrackerAddress, dataciteTrackerRepoId, doi, event); + const req = httpMock.expectOne(trackerAddress); + expect(req.request.method).toBe('POST'); + expect(req.request.headers.get('Content-Type')).toBe('application/json'); + expect(req.request.context.get(BYPASS_ERROR_INTERCEPTOR)).toBe(true); + expect(req.request.body).toEqual({ + n: DataciteEvent.DOWNLOAD, + u: window.location.href, + i: trackerRepoId, + p: doi, }); + req.flush({}); + + expect(sendBeaconSpy).toHaveBeenCalledTimes(1); + expect(emitted).toBe(true); }); - describe('on local setup (without dataciteTrackerRepoId configured)', () => { - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - DataciteService, - provideHttpClient(), - provideHttpClientTesting(), - { - provide: ENVIRONMENT, - useValue: { - dataciteTrackerRepoId: null, - dataciteTrackerAddress: dataciteTrackerAddress, - }, - }, - ], + it('should not log when identifiable has no doi', () => { + setup(); + const sendBeaconSpy = setSendBeacon(true); + let emitted = false; + let completed = false; + + service + .logIdentifiableView(trackable([{ id: '1', type: 'identifiers', category: 'ark', value: 'ark:/99999/x' }])) + .subscribe({ + next: () => { + emitted = true; + }, + complete: () => { + completed = true; + }, }); - service = TestBed.inject(DataciteService); - httpMock = TestBed.inject(HttpTestingController); - }); + expect(emitted).toBe(false); + expect(completed).toBe(true); + expect(sendBeaconSpy).not.toHaveBeenCalled(); + }); - it('logIdentifiableView should POST with correct payload', () => { - const doi = '10.1234/abcd'; - const observable = buildObservable(doi); - service.logIdentifiableView(observable).subscribe(); - httpMock.expectNone(dataciteTrackerAddress); + it('should fetch file identifiers and log doi on download', () => { + setup(); + const sendBeaconSpy = setSendBeacon(true); + let emitted = false; + + service.logFileDownload('file-123', 'files').subscribe(() => { + emitted = true; }); - afterEach(() => { - httpMock.verify(); + const req = httpMock.expectOne(`${environment.apiDomainUrl}/v2/files/file-123/identifiers`); + expect(req.request.method).toBe('GET'); + const response: IdentifiersResponseJsonApi = { + data: [ + { + id: 'id-1', + type: 'identifiers', + attributes: { category: 'doi', value: doi }, + embeds: null, + relationships: null, + links: null, + }, + ], + links: {}, + meta: { + total: 1, + per_page: 10, + version: '2.0', + }, + }; + req.flush(response); + + expect(emitted).toBe(true); + expect(sendBeaconSpy).toHaveBeenCalledWith( + trackerAddress, + JSON.stringify({ + n: DataciteEvent.DOWNLOAD, + u: window.location.href, + i: trackerRepoId, + p: doi, + }) + ); + }); + + it('should not log when tracker repo id is missing', () => { + setup(); + environment.dataciteTrackerRepoId = null; + const sendBeaconSpy = setSendBeacon(true); + let emitted = false; + let completed = false; + + service.logIdentifiableView(trackable([{ id: '1', type: 'identifiers', category: 'doi', value: doi }])).subscribe({ + next: () => { + emitted = true; + }, + complete: () => { + completed = true; + }, }); + + expect(emitted).toBe(false); + expect(completed).toBe(true); + expect(sendBeaconSpy).not.toHaveBeenCalled(); }); }); diff --git a/src/app/shared/services/files.service.spec.ts b/src/app/shared/services/files.service.spec.ts index 8dda0117b..6a3f77dd6 100644 --- a/src/app/shared/services/files.service.spec.ts +++ b/src/app/shared/services/files.service.spec.ts @@ -5,15 +5,14 @@ import { FilesService } from './files.service'; import { getConfiguredAddonsData } from '@testing/data/addons/addons.configured.data'; import { getResourceReferencesData } from '@testing/data/files/resource-references.data'; -import { OSFTestingStoreModule } from '@testing/osf.testing.module'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; describe.skip('Service: Files', () => { let service: FilesService; beforeEach(() => { TestBed.configureTestingModule({ - imports: [OSFTestingStoreModule], - providers: [FilesService], + providers: [provideOSFCore(), provideOSFHttp(), FilesService], }); service = TestBed.inject(FilesService); diff --git a/src/app/shared/services/google-file-picker.download.service.spec.ts b/src/app/shared/services/google-file-picker.download.service.spec.ts index 1d23bc2a2..ea33d19d7 100644 --- a/src/app/shared/services/google-file-picker.download.service.spec.ts +++ b/src/app/shared/services/google-file-picker.download.service.spec.ts @@ -1,132 +1,167 @@ -import { DOCUMENT } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { PLATFORM_ID } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { GoogleFilePickerDownloadService } from './google-file-picker.download.service'; -describe('Service: Google File Picker Download', () => { +describe('Service: GoogleFilePickerDownload', () => { let service: GoogleFilePickerDownloadService; - let mockDocument: Document; - let mockScriptElement: any; - - beforeEach(() => { - mockScriptElement = { - set src(url) { - this._src = url; - }, - get src() { - return this._src; - }, - onload: jest.fn(), - onerror: jest.fn(), - }; - - mockDocument = { - createElement: jest.fn(() => mockScriptElement), - body: { - appendChild: jest.fn((node: Node) => node), - } as any, - querySelector: jest.fn(), - } as any; + let documentRef: Document; + const setup = (platformId: 'browser' | 'server' = 'browser') => { TestBed.configureTestingModule({ - providers: [GoogleFilePickerDownloadService, { provide: DOCUMENT, useValue: mockDocument }], + providers: [GoogleFilePickerDownloadService, { provide: PLATFORM_ID, useValue: platformId }], }); service = TestBed.inject(GoogleFilePickerDownloadService); + documentRef = TestBed.inject(DOCUMENT); + }; + + const removeGoogleScript = () => { + const existing = documentRef.querySelector('script[src="https://apis.google.com/js/api.js"]'); + if (existing) { + existing.remove(); + } + }; + + afterEach(() => { + removeGoogleScript(); + (window as any).gapi = undefined; }); - it('should load the script and complete the observable', (done) => { - const observable = service.loadScript(); - - observable.subscribe({ - next: () => { - expect(mockDocument.createElement).toHaveBeenCalledWith('script'); - expect(mockScriptElement.src).toBe('https://apis.google.com/js/api.js'); - expect(mockScriptElement.async).toBeTruthy(); - expect(mockScriptElement.defer).toBeTruthy(); - expect(mockDocument.body.appendChild).toHaveBeenCalledWith(mockScriptElement); - }, - complete: () => { - expect(true).toBe(true); - done(); - }, - error: () => { - fail('Should not call error on script load success'); - }, - }); + it('should complete immediately when script already exists', () => { + setup(); + const script = documentRef.createElement('script'); + script.src = 'https://apis.google.com/js/api.js'; + documentRef.body.appendChild(script); + const appendSpy = jest.spyOn(documentRef.body, 'appendChild'); + + const next = jest.fn(); + const complete = jest.fn(); + service.loadScript().subscribe({ next, complete }); - mockScriptElement.onload(); + expect(next).toHaveBeenCalledTimes(1); + expect(complete).toHaveBeenCalledTimes(1); + expect(appendSpy).not.toHaveBeenCalled(); }); - it('should emit error when script fails to load', (done) => { - const service = new GoogleFilePickerDownloadService(); + it('should append script and complete on successful load', () => { + setup(); - service.loadScript().subscribe({ - next: () => fail('Should not emit next on error'), - error: (err) => { - expect(err).toBe('Failed to load Google Picker script'); - done(); - }, - }); + const next = jest.fn(); + const complete = jest.fn(); + service.loadScript().subscribe({ next, complete }); + + const script = documentRef.querySelector('script[src="https://apis.google.com/js/api.js"]') as HTMLScriptElement; + expect(script).toBeTruthy(); + + script.onload?.(new Event('load')); + + expect(next).toHaveBeenCalledTimes(1); + expect(complete).toHaveBeenCalledTimes(1); }); - describe('loadGapiModules', () => { - beforeEach(() => { - (globalThis as any).gapi = { - load: jest.fn(), - }; - }); + it('should emit error when script fails to load', () => { + setup(); - afterEach(() => { - jest.resetAllMocks(); - }); + const error = jest.fn(); + service.loadScript().subscribe({ error }); - it('should complete when GAPI loads successfully', (done) => { - service.loadGapiModules().subscribe({ - next: () => {}, - complete: () => { - expect(globalThis.gapi.load).toHaveBeenCalledWith( - 'client:picker', - expect.objectContaining({ - callback: expect.any(Function), - onerror: expect.any(Function), - timeout: 5000, - ontimeout: expect.any(Function), - }) - ); - done(); - }, - error: () => fail('Should not error'), - }); - - const config = (globalThis.gapi.load as jest.Mock).mock.calls[0][1]; - config.callback(); - }); + const script = documentRef.querySelector('script[src="https://apis.google.com/js/api.js"]') as HTMLScriptElement; + expect(script).toBeTruthy(); - it('should emit error when GAPI fails to load', (done) => { - service.loadGapiModules().subscribe({ - next: () => fail('Should not emit next'), - error: (err) => { - expect(err).toBe('Failed to load GAPI modules'); - done(); - }, - }); - - const config = (globalThis.gapi.load as jest.Mock).mock.calls[0][1]; - config.onerror(); - }); + script.onerror?.(new Event('error')); - it('should emit error on GAPI timeout', (done) => { - service.loadGapiModules().subscribe({ - next: () => fail('Should not emit next'), - error: (err) => { - expect(err).toBe('GAPI load timeout'); - done(); - }, - }); - - const config = (globalThis.gapi.load as jest.Mock).mock.calls[0][1]; - config.ontimeout(); - }); + expect(error).toHaveBeenCalledWith('Failed to load Google Picker script'); + }); + + it('should complete immediately on second load after successful first load', () => { + setup(); + const appendSpy = jest.spyOn(documentRef.body, 'appendChild'); + + service.loadScript().subscribe(); + const script = documentRef.querySelector('script[src="https://apis.google.com/js/api.js"]') as HTMLScriptElement; + script.onload?.(new Event('load')); + removeGoogleScript(); + appendSpy.mockClear(); + + const next = jest.fn(); + const complete = jest.fn(); + service.loadScript().subscribe({ next, complete }); + + expect(next).toHaveBeenCalledTimes(1); + expect(complete).toHaveBeenCalledTimes(1); + expect(appendSpy).not.toHaveBeenCalled(); + }); + + it('should error when loading gapi modules outside browser', () => { + setup('server'); + + const error = jest.fn(); + service.loadGapiModules().subscribe({ error }); + + expect(error).toHaveBeenCalledWith('GAPI not available'); + }); + + it('should error when gapi is not available in browser', () => { + setup('browser'); + + const error = jest.fn(); + service.loadGapiModules().subscribe({ error }); + + expect(error).toHaveBeenCalledWith('GAPI not available'); + }); + + it('should load gapi modules successfully', () => { + setup('browser'); + const loadMock = jest.fn( + (api: string, config: { callback: () => void; onerror: () => void; timeout: number; ontimeout: () => void }) => { + config.callback(); + } + ); + window.gapi = { load: loadMock } as unknown as typeof window.gapi; + + const next = jest.fn(); + const complete = jest.fn(); + service.loadGapiModules().subscribe({ next, complete }); + + expect(loadMock).toHaveBeenCalledWith( + 'client:picker', + expect.objectContaining({ + timeout: 5000, + }) + ); + expect(next).toHaveBeenCalledTimes(1); + expect(complete).toHaveBeenCalledTimes(1); + }); + + it('should emit error when gapi load fails', () => { + setup('browser'); + const loadMock = jest.fn( + (api: string, config: { callback: () => void; onerror: () => void; timeout: number; ontimeout: () => void }) => { + config.onerror(); + } + ); + window.gapi = { load: loadMock } as unknown as typeof window.gapi; + + const error = jest.fn(); + service.loadGapiModules().subscribe({ error }); + + expect(error).toHaveBeenCalledWith('Failed to load GAPI modules'); + }); + + it('should emit error on gapi load timeout', () => { + setup('browser'); + const loadMock = jest.fn( + (api: string, config: { callback: () => void; onerror: () => void; timeout: number; ontimeout: () => void }) => { + config.ontimeout(); + } + ); + window.gapi = { load: loadMock } as unknown as typeof window.gapi; + + const error = jest.fn(); + service.loadGapiModules().subscribe({ error }); + + expect(error).toHaveBeenCalledWith('GAPI load timeout'); }); }); diff --git a/src/app/shared/services/google-file-picker.download.service.ts b/src/app/shared/services/google-file-picker.download.service.ts index 62324780b..f37416817 100644 --- a/src/app/shared/services/google-file-picker.download.service.ts +++ b/src/app/shared/services/google-file-picker.download.service.ts @@ -26,7 +26,7 @@ export class GoogleFilePickerDownloadService { * * @returns Observable that emits once the script is loaded, or errors if loading fails. */ - public loadScript(): Observable { + loadScript(): Observable { return new Observable((observer: Subscriber) => { const existingScript = this.document.querySelector(`script[src="${this.scriptUrl}"]`); if (existingScript || this.scriptLoaded) { @@ -52,7 +52,7 @@ export class GoogleFilePickerDownloadService { /** * Loads GAPI modules (client:picker). */ - public loadGapiModules(): Observable { + loadGapiModules(): Observable { return new Observable((observer: Subscriber) => { if (!isPlatformBrowser(this.platformId) || !window.gapi) { observer.error('GAPI not available'); diff --git a/src/app/shared/services/signposting.service.spec.ts b/src/app/shared/services/signposting.service.spec.ts index e1f160546..e428db8ee 100644 --- a/src/app/shared/services/signposting.service.spec.ts +++ b/src/app/shared/services/signposting.service.spec.ts @@ -1,68 +1,105 @@ -import { RendererFactory2, RESPONSE_INIT } from '@angular/core'; +import { DOCUMENT } from '@angular/common'; +import { RESPONSE_INIT } from '@angular/core'; import { TestBed } from '@angular/core/testing'; +import { LINKSET_JSON_TYPE, LINKSET_TYPE } from '@osf/shared/models/signposting.model'; + import { SignpostingService } from './signposting.service'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('Service: Signposting', () => { let service: SignpostingService; - let mockResponseInit: ResponseInit; - let createdLinks: Record[]; - let mockAppendChild: jest.Mock; - - beforeEach(() => { - createdLinks = []; - mockAppendChild = jest.fn(); - mockResponseInit = { headers: new Headers() }; + let documentRef: Document; + const setup = (responseInit?: ResponseInit) => { TestBed.configureTestingModule({ providers: [ + provideOSFCore(), SignpostingService, - { provide: RESPONSE_INIT, useValue: mockResponseInit }, - { - provide: RendererFactory2, - useValue: { - createRenderer: () => ({ - createElement: jest.fn().mockImplementation(() => { - const link: Record = {}; - createdLinks.push(link); - return link; - }), - setAttribute: jest.fn().mockImplementation((el, attr, value) => { - el[attr] = value; - }), - appendChild: mockAppendChild, - }), - }, - }, + ...(responseInit ? [{ provide: RESPONSE_INIT, useValue: responseInit }] : []), ], }); service = TestBed.inject(SignpostingService); + documentRef = TestBed.inject(DOCUMENT); + service.removeSignpostingLinkTags(); + }; + + afterEach(() => { + if (service) { + service.removeSignpostingLinkTags(); + } + }); + + it('should add linkset signposting tags', () => { + setup(); + + service.addSignposting('abc123'); + + const linksetTags = Array.from(documentRef.head.querySelectorAll('link[rel="linkset"]')); + const hrefs = linksetTags.map((tag) => tag.getAttribute('href')); + const types = linksetTags.map((tag) => tag.getAttribute('type')); + + expect(linksetTags).toHaveLength(2); + expect(hrefs).toContain('http://localhost:4200/metadata/abc123/?format=linkset'); + expect(hrefs).toContain('http://localhost:4200/metadata/abc123/?format=linkset-json'); + expect(types).toContain(LINKSET_TYPE); + expect(types).toContain(LINKSET_JSON_TYPE); + }); + + it('should add metadata signposting tag', () => { + setup(); + + service.addMetadataSignposting('abc123'); + + const describesTags = Array.from(documentRef.head.querySelectorAll('link[rel="describes"]')); + + expect(describesTags).toHaveLength(1); + expect(describesTags[0].getAttribute('href')).toBe('http://localhost:4200/abc123/'); + expect(describesTags[0].getAttribute('type')).toBe('text/html'); + }); + + it('should remove only signposting tags', () => { + setup(); + const extraLink = documentRef.createElement('link'); + extraLink.setAttribute('rel', 'stylesheet'); + documentRef.head.appendChild(extraLink); + service.addSignposting('abc123'); + service.addMetadataSignposting('abc123'); + + service.removeSignpostingLinkTags(); + + expect(documentRef.head.querySelectorAll('link[rel="linkset"]')).toHaveLength(0); + expect(documentRef.head.querySelectorAll('link[rel="describes"]')).toHaveLength(0); + expect(documentRef.head.querySelectorAll('link[rel="stylesheet"]')).toHaveLength(1); }); - it('should set headers using addSignposting', () => { - service.addSignposting('abcde'); - const linkHeader = (mockResponseInit.headers as Headers).get('Link'); - expect(linkHeader).toBe( - '; rel="linkset"; type="application/linkset", ; rel="linkset"; type="application/linkset+json"' + it('should set link headers for addSignposting when response init headers are plain object', () => { + const responseInit: ResponseInit = { headers: {} }; + setup(responseInit); + + service.addSignposting('abc123'); + + expect(responseInit.headers).toBeInstanceOf(Headers); + const linkHeader = (responseInit.headers as Headers).get('Link'); + + expect(linkHeader).toContain( + '; rel="linkset"; type="application/linkset"' + ); + expect(linkHeader).toContain( + '; rel="linkset"; type="application/linkset+json"' ); }); - it('should add link tags using addSignposting', () => { - service.addSignposting('abcde'); - - expect(createdLinks).toEqual([ - { - rel: 'linkset', - href: 'https://staging3.osf.io/metadata/abcde/?format=linkset', - type: 'application/linkset', - }, - { - rel: 'linkset', - href: 'https://staging3.osf.io/metadata/abcde/?format=linkset-json', - type: 'application/linkset+json', - }, - ]); - expect(mockAppendChild).toHaveBeenCalledTimes(2); + it('should set link headers for addMetadataSignposting when response init headers are Headers', () => { + const headers = new Headers(); + const responseInit: ResponseInit = { headers }; + setup(responseInit); + + service.addMetadataSignposting('abc123'); + + expect(responseInit.headers).toBe(headers); + expect(headers.get('Link')).toBe('; rel="describes"; type="text/html"'); }); }); diff --git a/src/app/shared/stores/activity-logs/activity-logs.state.spec.ts b/src/app/shared/stores/activity-logs/activity-logs.state.spec.ts index d3536146b..2b24ef7f7 100644 --- a/src/app/shared/stores/activity-logs/activity-logs.state.spec.ts +++ b/src/app/shared/stores/activity-logs/activity-logs.state.spec.ts @@ -15,7 +15,7 @@ import { buildRegistrationLogsUrl, getActivityLogsResponse, } from '@testing/data/activity-logs/activity-logs.data'; -import { EnvironmentTokenMock } from '@testing/mocks/environment.token.mock'; +import { EnvironmentTokenMock } from '@testing/providers/environment.token.mock'; describe('State: ActivityLogs', () => { let store: Store; diff --git a/src/app/shared/stores/addons/addons.state.spec.ts b/src/app/shared/stores/addons/addons.state.spec.ts index 2dd88b3f4..af52bb6c9 100644 --- a/src/app/shared/stores/addons/addons.state.spec.ts +++ b/src/app/shared/stores/addons/addons.state.spec.ts @@ -17,15 +17,14 @@ import { AddonsState } from './addons.state'; import { getAddonsAuthorizedStorageData } from '@testing/data/addons/addons.authorized-storage.data'; import { getConfiguredAddonsData } from '@testing/data/addons/addons.configured.data'; import { getAddonsExternalStorageData } from '@testing/data/addons/addons.external-storage.data'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; describe('State: Addons', () => { let store: Store; beforeEach(() => { TestBed.configureTestingModule({ - imports: [OSFTestingModule], - providers: [provideStore([AddonsState]), AddonsService], + providers: [provideOSFCore(), provideOSFHttp(), provideStore([AddonsState]), AddonsService], }); store = TestBed.inject(Store); diff --git a/src/app/shared/stores/banners/banners.state.spec.ts b/src/app/shared/stores/banners/banners.state.spec.ts index bd6f9d6c7..59e8acaa9 100644 --- a/src/app/shared/stores/banners/banners.state.spec.ts +++ b/src/app/shared/stores/banners/banners.state.spec.ts @@ -11,15 +11,14 @@ import { BannersSelector } from './banners.selectors'; import { BannersState } from './banners.state'; import { getScheduledBannerData } from '@testing/data/banners/scheduled.banner.data'; -import { OSFTestingModule } from '@testing/osf.testing.module'; +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; describe('State: Banners', () => { let store: Store; beforeEach(() => { TestBed.configureTestingModule({ - imports: [OSFTestingModule], - providers: [provideStore([BannersState]), BannersService], + providers: [provideOSFCore(), provideOSFHttp(), provideStore([BannersState]), BannersService], }); store = TestBed.inject(Store); diff --git a/src/testing/mocks/custom-confirmation.service.mock.ts b/src/testing/mocks/custom-confirmation.service.mock.ts deleted file mode 100644 index ba0ffc956..000000000 --- a/src/testing/mocks/custom-confirmation.service.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; - -export const CustomConfirmationServiceMock = { - confirmDelete: jest.fn(), - confirmAccept: jest.fn(), - confirmContinue: jest.fn(), -}; - -export const MockCustomConfirmationServiceProvider = { - provide: CustomConfirmationService, - useValue: CustomConfirmationServiceMock, -}; diff --git a/src/testing/mocks/datacite.service.mock.ts b/src/testing/mocks/datacite.service.mock.ts deleted file mode 100644 index 69ab8d025..000000000 --- a/src/testing/mocks/datacite.service.mock.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { of } from 'rxjs'; - -import { DataciteService } from '@shared/services/datacite/datacite.service'; - -export function DataciteMockFactory() { - return { - logFileDownload: jest.fn().mockReturnValue(of(void 0)), - logFileView: jest.fn().mockReturnValue(of(void 0)), - logIdentifiableView: jest.fn().mockReturnValue(of(void 0)), - logIdentifiableDownload: jest.fn().mockReturnValue(of(void 0)), - } as unknown as jest.Mocked; -} diff --git a/src/testing/mocks/mock-store.mock.ts b/src/testing/mocks/mock-store.mock.ts deleted file mode 100644 index db678a7b4..000000000 --- a/src/testing/mocks/mock-store.mock.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const MOCK_STORE = { - selectSignal: jest.fn(), - selectSnapshot: jest.fn(), - dispatch: jest.fn(), -}; diff --git a/src/testing/mocks/store.mock.ts b/src/testing/mocks/store.mock.ts deleted file mode 100644 index 34bf9f298..000000000 --- a/src/testing/mocks/store.mock.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Store } from '@ngxs/store'; - -import { of } from 'rxjs'; - -/** - * A simple Jest-based mock for the Angular NGXS `Store`. - * - * @remarks - * This mock provides a no-op implementation of the `dispatch` method and an empty `select` observable. - * Useful when the store is injected but no store behavior is required for the test. - * - * @example - * ```ts - * TestBed.configureTestingModule({ - * providers: [ - * { provide: Store, useValue: storeMock } - * ] - * }); - * ``` - * - * @property dispatch - A Jest mock function that returns an observable of `true` when called. - * @property select - A function returning an observable emitting `undefined`, acting as a placeholder selector. - */ -export const StoreMock = { - provide: Store, - useValue: { - select: jest.fn().mockReturnValue(of([])), - selectSignal: jest.fn().mockReturnValue(of([])), - dispatch: jest.fn().mockReturnValue(of({})), - } as unknown as jest.Mocked, -}; diff --git a/src/testing/mocks/toast.service.mock.ts b/src/testing/mocks/toast.service.mock.ts deleted file mode 100644 index 7e8435814..000000000 --- a/src/testing/mocks/toast.service.mock.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ToastService } from '@osf/shared/services/toast.service'; - -/** - * A mock implementation of a toast (notification) service for testing purposes. - * - * @remarks - * This mock allows tests to verify that toast messages would have been triggered without - * actually displaying them. The methods are replaced with Jest spies so you can assert - * calls like `expect(toastService.showSuccess).toHaveBeenCalledWith(...)`. - * - * @example - * ```ts - * TestBed.configureTestingModule({ - * providers: [{ provide: ToastService, useValue: toastServiceMock }] - * }); - * - * it('should show success toast', () => { - * someComponent.doSomething(); - * expect(toastServiceMock.showSuccess).toHaveBeenCalledWith('Operation successful'); - * }); - * ``` - * - * @property showSuccess - Mocked method for displaying a success message. - * @property showError - Mocked method for displaying an error message. - * @property showWarng - Mocked method for displaying a warning message. - */ -export const ToastServiceMock = { - provide: ToastService, - useValue: { - showSuccess: jest.fn(), - showError: jest.fn(), - showWarn: jest.fn(), - }, -}; diff --git a/src/testing/mocks/translate.service.mock.ts b/src/testing/mocks/translate.service.mock.ts deleted file mode 100644 index fa13d46b1..000000000 --- a/src/testing/mocks/translate.service.mock.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { TranslateService } from '@ngx-translate/core'; - -import { of } from 'rxjs'; - -export const TranslateServiceMock = { - provide: TranslateService, - useValue: { - get: jest.fn().mockImplementation((key: string) => of(key)), - instant: jest.fn().mockImplementation((key: string) => key), - onLangChange: of({ lang: 'en' }), - onTranslationChange: of({ translations: {} }), - onDefaultLangChange: of({ lang: 'en' }), - setDefaultLang: jest.fn(), - use: jest.fn(), - getDefaultLang: jest.fn().mockReturnValue('en'), - getBrowserLang: jest.fn().mockReturnValue('en'), - addLangs: jest.fn(), - getLangs: jest.fn().mockReturnValue(['en']), - stream: jest.fn().mockImplementation((key: string) => of(key)), - currentLang: 'en', - defaultLang: 'en', - }, -}; diff --git a/src/testing/mocks/translation.service.mock.ts b/src/testing/mocks/translation.service.mock.ts deleted file mode 100644 index fc579f3f3..000000000 --- a/src/testing/mocks/translation.service.mock.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { TranslateService } from '@ngx-translate/core'; - -import { of } from 'rxjs'; - -/** - * Mock implementation of the TranslationService used for unit testing. - * - * This mock provides stubbed implementations for common translation methods, enabling components - * to be tested without relying on actual i18n infrastructure. - * - * Each method is implemented as a Jest mock function, so tests can assert on calls, arguments, and return values. - * - * @property get - Simulates retrieval of translated values as an observable. - * @property instant - Simulates synchronous translation of a key. - * @property use - Simulates switching the current language. - * @property stream - Simulates a translation stream for reactive bindings. - * @property setDefaultLang - Simulates setting the default fallback language. - * @property getBrowserCultureLang - Simulates detection of the user's browser culture. - * @property getBrowserLang - Simulates detection of the user's browser language. - */ -export const TranslationServiceMock = { - provide: TranslateService, - useValue: { - get: jest.fn().mockImplementation((key) => of(key || '')), - instant: jest.fn().mockImplementation((key) => key || ''), - stream: jest.fn().mockImplementation((key) => of(key || '')), - use: jest.fn(), - onLangChange: of({}), - onTranslationChange: of({ - lang: 'en', - translations: {}, - }), - onDefaultLangChange: of({ - lang: 'en', - translations: {}, - }), - }, -}; diff --git a/src/testing/osf.testing.module.ts b/src/testing/osf.testing.module.ts deleted file mode 100644 index a4e376233..000000000 --- a/src/testing/osf.testing.module.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { TranslateModule } from '@ngx-translate/core'; - -import { CommonModule } from '@angular/common'; -import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; -import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { NgModule } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; -import { NoopAnimationsModule, provideNoopAnimations } from '@angular/platform-browser/animations'; -import { provideRouter } from '@angular/router'; - -import { DynamicDialogRefMock } from './mocks/dynamic-dialog-ref.mock'; -import { EnvironmentTokenMock } from './mocks/environment.token.mock'; -import { StoreMock } from './mocks/store.mock'; -import { ToastServiceMock } from './mocks/toast.service.mock'; -import { TranslationServiceMock } from './mocks/translation.service.mock'; - -/** - * Shared testing module used across OSF-related unit tests. - * - * This module imports and declares no actual components or services. Its purpose is to provide - * a lightweight Angular module that includes permissive schemas to suppress Angular template - * validation errors related to unknown elements and attributes. - * - * This is useful for testing components that contain custom elements or web components, or when - * mocking child components not included in the test's declarations or imports. - */ -@NgModule({ - imports: [NoopAnimationsModule, BrowserModule, CommonModule, TranslateModule.forRoot()], - providers: [ - provideNoopAnimations(), - provideRouter([]), - provideHttpClient(withInterceptorsFromDi()), - provideHttpClientTesting(), - TranslationServiceMock, - DynamicDialogRefMock, - EnvironmentTokenMock, - ToastServiceMock, - ], -}) -export class OSFTestingModule {} - -/** - * Angular testing module that includes the OSFTestingModule and a mock Store provider. - * - * This module is intended for unit tests that require NGXS `Store` injection, - * and it uses `StoreMock` to mock store behavior without requiring a real NGXS store setup. - * - * @remarks - * - Combines permissive schemas (via OSFTestingModule) and store mocking. - * - Keeps unit tests lightweight and focused by avoiding full store configuration. - */ -@NgModule({ - /** - * Imports the shared OSF testing module to allow custom elements and suppress schema errors. - */ - imports: [OSFTestingModule], - - /** - * Provides a mocked NGXS Store instance for test environments. - * @see StoreMock - A mock provider simulating Store behaviors like select, dispatch, etc. - */ - providers: [StoreMock], -}) -export class OSFTestingStoreModule {} diff --git a/src/testing/osf.testing.provider.ts b/src/testing/osf.testing.provider.ts index 5667c36a0..950adcc5f 100644 --- a/src/testing/osf.testing.provider.ts +++ b/src/testing/osf.testing.provider.ts @@ -1,20 +1,14 @@ -import { TranslateModule } from '@ngx-translate/core'; - import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; -import { importProvidersFrom } from '@angular/core'; -import { provideNoopAnimations } from '@angular/platform-browser/animations'; +import { provideZonelessChangeDetection } from '@angular/core'; + +import { provideTranslation } from '@core/helpers/i18n.helper'; -import { EnvironmentTokenMock } from './mocks/environment.token.mock'; -import { TranslationServiceMock } from './mocks/translation.service.mock'; +import { EnvironmentTokenMock } from './providers/environment.token.mock'; +import { TranslateServiceMock } from './providers/translate.service.mock'; export function provideOSFCore() { - return [ - provideNoopAnimations(), - importProvidersFrom(TranslateModule.forRoot()), - TranslationServiceMock, - EnvironmentTokenMock, - ]; + return [provideZonelessChangeDetection(), provideTranslation, TranslateServiceMock, EnvironmentTokenMock]; } export function provideOSFHttp() { diff --git a/src/testing/mocks/dynamic-dialog-ref.mock.ts b/src/testing/providers/dynamic-dialog-ref.mock.ts similarity index 100% rename from src/testing/mocks/dynamic-dialog-ref.mock.ts rename to src/testing/providers/dynamic-dialog-ref.mock.ts diff --git a/src/testing/mocks/environment.token.mock.ts b/src/testing/providers/environment.token.mock.ts similarity index 100% rename from src/testing/mocks/environment.token.mock.ts rename to src/testing/providers/environment.token.mock.ts diff --git a/src/testing/providers/route-provider.mock.ts b/src/testing/providers/route-provider.mock.ts index 477e3200c..e41c35bd4 100644 --- a/src/testing/providers/route-provider.mock.ts +++ b/src/testing/providers/route-provider.mock.ts @@ -7,6 +7,7 @@ export class ActivatedRouteMockBuilder { private queryParamsObj: Record = {}; private dataObj: Record = {}; private firstChildBuilder: ActivatedRouteMockBuilder | null = null; + private parentRoute: Partial | null = null; private hasParent = true; private params$ = new BehaviorSubject>({}); @@ -43,6 +44,13 @@ export class ActivatedRouteMockBuilder { withNoParent(): ActivatedRouteMockBuilder { this.hasParent = false; + this.parentRoute = null; + return this; + } + + withParentRoute(parentRoute: Partial): ActivatedRouteMockBuilder { + this.hasParent = true; + this.parentRoute = parentRoute; return this; } @@ -63,12 +71,13 @@ export class ActivatedRouteMockBuilder { const firstChild = this.firstChildBuilder ? this.firstChildBuilder.build() : null; const parent = this.hasParent - ? ({ + ? (this.parentRoute ?? + ({ params: this.params$.asObservable(), snapshot: { params: this.paramsObj, }, - } as any) + } as any)) : null; const route: Partial = { diff --git a/src/testing/providers/translate.service.mock.ts b/src/testing/providers/translate.service.mock.ts new file mode 100644 index 000000000..ab17804ca --- /dev/null +++ b/src/testing/providers/translate.service.mock.ts @@ -0,0 +1,40 @@ +import { TranslateService } from '@ngx-translate/core'; + +import { of } from 'rxjs'; + +export const TranslateServiceMock = { + provide: TranslateService, + useValue: { + onTranslationChange: of({ lang: 'en', translations: {} }), + onLangChange: of({ lang: 'en', translations: {} }), + onFallbackLangChange: of({ lang: 'en', translations: {} }), + onDefaultLangChange: of({ lang: 'en', translations: {} }), + + get: jest.fn().mockImplementation((key: string | string[]) => of(key)), + instant: jest.fn().mockImplementation((key: string | string[]) => key), + stream: jest.fn().mockImplementation((key: string | string[]) => of(key)), + getStreamOnTranslationChange: jest.fn().mockImplementation((key: string | string[]) => of(key)), + + use: jest.fn().mockReturnValue(of({})), + setFallbackLang: jest.fn().mockReturnValue(of({})), + setDefaultLang: jest.fn().mockReturnValue(of({})), + reloadLang: jest.fn().mockReturnValue(of({})), + getParsedResult: jest.fn().mockImplementation((key) => key), + + getLangs: jest.fn().mockReturnValue(['en']), + getCurrentLang: jest.fn().mockReturnValue('en'), + getFallbackLang: jest.fn().mockReturnValue('en'), + getDefaultLang: jest.fn().mockReturnValue('en'), + getBrowserLang: jest.fn().mockReturnValue('en'), + getBrowserCultureLang: jest.fn().mockReturnValue('en-US'), + + currentLang: 'en', + defaultLang: 'en', + langs: ['en'], + + addLangs: jest.fn(), + resetLang: jest.fn(), + set: jest.fn(), + setTranslation: jest.fn(), + }, +}; From f29e5c413303038ad5098134d105a8a4182640c8 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Mar 2026 16:56:29 +0200 Subject: [PATCH 02/10] test(package): removed structured clone package --- package-lock.json | 1 - package.json | 1 - src/@types/structured-clone.d.ts | 5 ----- src/testing/data/activity-logs/activity-logs.data.ts | 2 -- src/testing/data/addons/addons.authorized-storage.data.ts | 2 -- src/testing/data/addons/addons.configured.data.ts | 2 -- src/testing/data/addons/addons.external-storage.data.ts | 2 -- src/testing/data/addons/addons.operation-invocation.data.ts | 2 -- src/testing/data/banners/scheduled.banner.data.ts | 2 -- src/testing/data/dashboard/dasboard.data.ts | 2 -- src/testing/data/files/node.data.ts | 2 -- src/testing/data/files/resource-references.data.ts | 2 -- src/testing/mocks/activity-log-with-display.mock.ts | 2 -- 13 files changed, 27 deletions(-) delete mode 100644 src/@types/structured-clone.d.ts diff --git a/package-lock.json b/package-lock.json index 3b663b420..e47a2bbfc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85,7 +85,6 @@ "ng-mocks": "^14.15.1", "prettier": "3.8.1", "source-map-explorer": "^2.5.3", - "structured-clone": "^0.2.2", "ts-jest": "^29.4.6", "typescript": "~5.9.3", "typescript-eslint": "^8.56.1", diff --git a/package.json b/package.json index a7b689005..f3df90be8 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,6 @@ "ng-mocks": "^14.15.1", "prettier": "3.8.1", "source-map-explorer": "^2.5.3", - "structured-clone": "^0.2.2", "ts-jest": "^29.4.6", "typescript": "~5.9.3", "typescript-eslint": "^8.56.1", diff --git a/src/@types/structured-clone.d.ts b/src/@types/structured-clone.d.ts deleted file mode 100644 index e1b423af7..000000000 --- a/src/@types/structured-clone.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// src/types/structured-clone.d.ts -declare module 'structured-clone' { - function structuredClone(value: T): T; - export = structuredClone; -} diff --git a/src/testing/data/activity-logs/activity-logs.data.ts b/src/testing/data/activity-logs/activity-logs.data.ts index bd430eb4c..5726607eb 100644 --- a/src/testing/data/activity-logs/activity-logs.data.ts +++ b/src/testing/data/activity-logs/activity-logs.data.ts @@ -1,5 +1,3 @@ -import structuredClone from 'structured-clone'; - export const ACTIVITY_LOGS_EMBEDS_QS = 'embed%5B%5D=original_node&embed%5B%5D=user&embed%5B%5D=linked_node&embed%5B%5D=linked_registration&embed%5B%5D=template_node'; diff --git a/src/testing/data/addons/addons.authorized-storage.data.ts b/src/testing/data/addons/addons.authorized-storage.data.ts index 2a7ea5d94..98ee981a9 100644 --- a/src/testing/data/addons/addons.authorized-storage.data.ts +++ b/src/testing/data/addons/addons.authorized-storage.data.ts @@ -1,5 +1,3 @@ -import structuredClone from 'structured-clone'; - const AuthorizedStorage = { data: [ { diff --git a/src/testing/data/addons/addons.configured.data.ts b/src/testing/data/addons/addons.configured.data.ts index 9e50cce1c..97f535fde 100644 --- a/src/testing/data/addons/addons.configured.data.ts +++ b/src/testing/data/addons/addons.configured.data.ts @@ -1,7 +1,5 @@ import { AddonMapper } from '@osf/shared/mappers/addon.mapper'; -import structuredClone from 'structured-clone'; - const ConfiguredAddons = { data: [ { diff --git a/src/testing/data/addons/addons.external-storage.data.ts b/src/testing/data/addons/addons.external-storage.data.ts index a50c9d7e3..75f5d834c 100644 --- a/src/testing/data/addons/addons.external-storage.data.ts +++ b/src/testing/data/addons/addons.external-storage.data.ts @@ -1,5 +1,3 @@ -import structuredClone from 'structured-clone'; - const ExternalStorage = { links: { first: 'https://addons.staging4.osf.io/v1/external-storage-services?page%5Bnumber%5D=1', diff --git a/src/testing/data/addons/addons.operation-invocation.data.ts b/src/testing/data/addons/addons.operation-invocation.data.ts index e3c57a659..b713700ae 100644 --- a/src/testing/data/addons/addons.operation-invocation.data.ts +++ b/src/testing/data/addons/addons.operation-invocation.data.ts @@ -1,5 +1,3 @@ -import structuredClone from 'structured-clone'; - const OperationInvocation = { data: { type: 'addon-operation-invocations', diff --git a/src/testing/data/banners/scheduled.banner.data.ts b/src/testing/data/banners/scheduled.banner.data.ts index ac8553267..d999c7f8c 100644 --- a/src/testing/data/banners/scheduled.banner.data.ts +++ b/src/testing/data/banners/scheduled.banner.data.ts @@ -1,5 +1,3 @@ -import structuredClone from 'structured-clone'; - const ScheduledBannerData = { data: { id: '', diff --git a/src/testing/data/dashboard/dasboard.data.ts b/src/testing/data/dashboard/dasboard.data.ts index c2e133b71..e296f94e2 100644 --- a/src/testing/data/dashboard/dasboard.data.ts +++ b/src/testing/data/dashboard/dasboard.data.ts @@ -1,7 +1,5 @@ import { MyResourcesItem } from '@osf/shared/models/my-resources/my-resources.model'; -import structuredClone from 'structured-clone'; - const ProjectsMock = { data: [ { diff --git a/src/testing/data/files/node.data.ts b/src/testing/data/files/node.data.ts index 1f8208b74..2532fe923 100644 --- a/src/testing/data/files/node.data.ts +++ b/src/testing/data/files/node.data.ts @@ -1,7 +1,5 @@ import { FilesMapper } from '@osf/shared/mappers/files/files.mapper'; -import structuredClone from 'structured-clone'; - const NodeFiles = { data: [ { diff --git a/src/testing/data/files/resource-references.data.ts b/src/testing/data/files/resource-references.data.ts index d82c2856b..d201b5729 100644 --- a/src/testing/data/files/resource-references.data.ts +++ b/src/testing/data/files/resource-references.data.ts @@ -1,5 +1,3 @@ -import structuredClone from 'structured-clone'; - const ResourceReferences = { data: [ { diff --git a/src/testing/mocks/activity-log-with-display.mock.ts b/src/testing/mocks/activity-log-with-display.mock.ts index 293e7228f..a64c8b367 100644 --- a/src/testing/mocks/activity-log-with-display.mock.ts +++ b/src/testing/mocks/activity-log-with-display.mock.ts @@ -1,7 +1,5 @@ import { ActivityLogWithDisplay } from '@osf/shared/models/activity-logs/activity-log-with-display.model'; -import structuredClone from 'structured-clone'; - export function makeActivityLogWithDisplay(overrides: Partial = {}): ActivityLogWithDisplay { return structuredClone({ id: 'log1', From bb8941908ab34e71b07d773a2b50aace1056b270 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Mar 2026 17:05:12 +0200 Subject: [PATCH 03/10] test(router): updated provider for router link --- .../footer/footer.component.spec.ts | 11 +++----- .../tos-consent-banner.component.spec.ts | 6 +---- .../institutions-list.component.spec.ts | 7 +---- ...metadata-collection-item.component.spec.ts | 13 ++++----- .../moderators-table.component.spec.ts | 7 ++--- .../my-reviewing-navigation.component.spec.ts | 2 +- .../browse-by-subjects.component.spec.ts | 7 ++--- .../preprint-provider-hero.component.spec.ts | 9 ++----- .../preprint-services.component.spec.ts | 3 ++- .../title-and-abstract-step.component.spec.ts | 5 ++-- .../select-preprint-service.component.spec.ts | 11 +++----- .../profile-information.component.spec.ts | 27 ++++--------------- .../overview-collections.component.spec.ts | 9 ++++--- .../registry-revisions.component.spec.ts | 7 ++--- .../registry-statuses.component.spec.ts | 7 +++-- .../short-registration-info.component.spec.ts | 7 +++-- .../developer-apps-list.component.spec.ts | 8 +++--- .../connect-addon.component.spec.ts | 25 ++++------------- .../tokens-list/tokens-list.component.spec.ts | 8 +++--- ...addon-setup-account-form.component.spec.ts | 9 ++++--- ...liated-institutions-view.component.spec.ts | 6 ++--- .../registration-card.component.spec.ts | 12 ++++----- 22 files changed, 73 insertions(+), 133 deletions(-) diff --git a/src/app/core/components/footer/footer.component.spec.ts b/src/app/core/components/footer/footer.component.spec.ts index 2a6396c93..892901692 100644 --- a/src/app/core/components/footer/footer.component.spec.ts +++ b/src/app/core/components/footer/footer.component.spec.ts @@ -1,7 +1,7 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, provideRouter } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { SOCIAL_ICONS } from '@core/constants/social-icons.constant'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; @@ -9,7 +9,6 @@ import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { FooterComponent } from './footer.component'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('FooterComponent', () => { let component: FooterComponent; @@ -18,11 +17,7 @@ describe('FooterComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [FooterComponent, MockComponent(IconComponent)], - providers: [ - provideOSFCore(), - provideRouter([]), - MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), - ], + providers: [provideOSFCore(), provideRouter([])], }); fixture = TestBed.createComponent(FooterComponent); diff --git a/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts b/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts index be08f6e16..ec80f4172 100644 --- a/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts +++ b/src/app/core/components/osf-banners/tos-consent-banner/tos-consent-banner.component.spec.ts @@ -1,10 +1,8 @@ import { Store } from '@ngxs/store'; -import { MockProvider } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { ActivatedRoute, provideRouter } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { AcceptTermsOfServiceByUser, UserSelectors } from '@core/store/user'; import { UserModel } from '@osf/shared/models/user/user.model'; @@ -13,7 +11,6 @@ import { TosConsentBannerComponent } from './tos-consent-banner.component'; import { MOCK_USER } from '@testing/mocks/data.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { BaseSetupOverrides, mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock'; describe('TosConsentBannerComponent', () => { @@ -27,7 +24,6 @@ describe('TosConsentBannerComponent', () => { providers: [ provideOSFCore(), provideRouter([]), - MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), provideMockStore({ signals: mergeSignalOverrides( [ diff --git a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts index 85ae36670..a25229411 100644 --- a/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts +++ b/src/app/features/institutions/pages/institutions-list/institutions-list.component.spec.ts @@ -4,7 +4,7 @@ import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl } from '@angular/forms'; -import { ActivatedRoute, provideRouter } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { ScheduledBannerComponent } from '@core/components/osf-banners/scheduled-banner/scheduled-banner.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; @@ -16,7 +16,6 @@ import { InstitutionsListComponent } from './institutions-list.component'; import { MOCK_INSTITUTION } from '@testing/mocks/institution.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('InstitutionsListComponent', () => { @@ -35,10 +34,6 @@ describe('InstitutionsListComponent', () => { providers: [ provideOSFCore(), provideRouter([]), - { - provide: ActivatedRoute, - useValue: ActivatedRouteMockBuilder.create().build(), - }, provideMockStore({ signals: [ { selector: InstitutionsSelectors.getInstitutions, value: mockInstitutions }, diff --git a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts index 4b4e94882..8755c43b3 100644 --- a/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts +++ b/src/app/features/metadata/components/metadata-collection-item/metadata-collection-item.component.spec.ts @@ -1,7 +1,5 @@ -import { MockProvider } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { CollectionSubmissionReviewState } from '@osf/shared/enums/collection-submission-review-state.enum'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; @@ -9,7 +7,6 @@ import { CollectionSubmission } from '@osf/shared/models/collections/collections import { MetadataCollectionItemComponent } from './metadata-collection-item.component'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('MetadataCollectionItemComponent', () => { let component: MetadataCollectionItemComponent; @@ -33,11 +30,11 @@ describe('MetadataCollectionItemComponent', () => { gradeLevels: 'Graduate', }; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [MetadataCollectionItemComponent], - providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())], - }).compileComponents(); + providers: [provideOSFCore(), provideRouter([])], + }); fixture = TestBed.createComponent(MetadataCollectionItemComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts b/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts index 5e8ba1c3a..e92b977c7 100644 --- a/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts +++ b/src/app/features/moderation/components/moderators-table/moderators-table.component.spec.ts @@ -1,6 +1,7 @@ import { MockComponent, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { SelectComponent } from '@osf/shared/components/select/select.component'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; @@ -34,10 +35,10 @@ describe('ModeratorsTableComponent', () => { beforeEach(async () => { mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); - await TestBed.configureTestingModule({ + TestBed.configureTestingModule({ imports: [ModeratorsTableComponent, MockComponent(SelectComponent)], - providers: [provideOSFCore(), MockProvider(CustomDialogService, mockCustomDialogService)], - }).compileComponents(); + providers: [provideOSFCore(), provideRouter([]), MockProvider(CustomDialogService, mockCustomDialogService)], + }); fixture = TestBed.createComponent(ModeratorsTableComponent); component = fixture.componentInstance; diff --git a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts index eed9eaaa6..1fe30f1c7 100644 --- a/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts +++ b/src/app/features/moderation/components/my-reviewing-navigation/my-reviewing-navigation.component.spec.ts @@ -14,7 +14,7 @@ describe('MyReviewingNavigationComponent', () => { const mockProvider = MOCK_PROVIDER; - beforeEach(async () => { + beforeEach(() => { TestBed.configureTestingModule({ imports: [MyReviewingNavigationComponent], providers: [provideOSFCore(), provideRouter([])], diff --git a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts index b09b902fa..7ebfaa005 100644 --- a/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts +++ b/src/app/features/preprints/components/browse-by-subjects/browse-by-subjects.component.spec.ts @@ -1,7 +1,5 @@ -import { MockProvider } from 'ng-mocks'; - import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { ResourceType } from '@shared/enums/resource-type.enum'; import { SubjectModel } from '@shared/models/subject/subject.model'; @@ -10,7 +8,6 @@ import { BrowseBySubjectsComponent } from './browse-by-subjects.component'; import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('BrowseBySubjectsComponent', () => { let component: BrowseBySubjectsComponent; @@ -26,7 +23,7 @@ describe('BrowseBySubjectsComponent', () => { }) { TestBed.configureTestingModule({ imports: [BrowseBySubjectsComponent], - providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())], + providers: [provideOSFCore(), provideRouter([])], }); fixture = TestBed.createComponent(BrowseBySubjectsComponent); diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts index 4b3444fa5..ebe658558 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts @@ -2,7 +2,7 @@ import { MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl } from '@angular/forms'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { PreprintProviderDetails } from '@osf/features/preprints/models'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; @@ -17,7 +17,6 @@ import { CustomDialogServiceMockBuilder, CustomDialogServiceMockType, } from '@testing/providers/custom-dialog-provider.mock'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; describe('PreprintProviderHeroComponent', () => { let component: PreprintProviderHeroComponent; @@ -35,11 +34,7 @@ describe('PreprintProviderHeroComponent', () => { TestBed.configureTestingModule({ imports: [PreprintProviderHeroComponent], - providers: [ - provideOSFCore(), - MockProvider(CustomDialogService, customDialogMock), - MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), - ], + providers: [provideOSFCore(), provideRouter([]), MockProvider(CustomDialogService, customDialogMock)], }); fixture = TestBed.createComponent(PreprintProviderHeroComponent); diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts b/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts index 552053787..319125d0e 100644 --- a/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { PreprintProviderShortInfo } from '@osf/features/preprints/models'; @@ -16,7 +17,7 @@ describe('PreprintServicesComponent', () => { function setup(providers: PreprintProviderShortInfo[]) { TestBed.configureTestingModule({ imports: [PreprintServicesComponent], - providers: [provideOSFCore()], + providers: [provideOSFCore(), provideRouter([])], }); fixture = TestBed.createComponent(PreprintServicesComponent); diff --git a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts index 95edb3149..7f4123181 100644 --- a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts @@ -3,7 +3,7 @@ import { MockComponent, MockDirective, MockProvider } from 'ng-mocks'; import { Textarea } from 'primeng/textarea'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { TitleAndAbstractStepComponent } from '@osf/features/preprints/components'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; @@ -12,7 +12,6 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; import { ToastServiceMock } from '@testing/providers/toast-provider.mock'; @@ -29,7 +28,7 @@ describe('TitleAndAbstractStepComponent', () => { imports: [TitleAndAbstractStepComponent, MockComponent(TextInputComponent), MockDirective(Textarea)], providers: [ provideOSFCore(), - MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), + provideRouter([]), MockProvider(ToastService, mockToastService), provideMockStore({ signals: [ diff --git a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts index 12b7e4720..ed7c70636 100644 --- a/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts +++ b/src/app/features/preprints/pages/select-preprint-service/select-preprint-service.component.spec.ts @@ -1,9 +1,9 @@ import { Store } from '@ngxs/store'; -import { MockComponents, MockProvider } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; @@ -14,7 +14,6 @@ import { SelectPreprintServiceComponent } from './select-preprint-service.compon import { PREPRINT_PROVIDER_SHORT_INFO_MOCK } from '@testing/mocks/preprint-provider-short-info.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { mergeSignalOverrides, provideMockStore, SignalOverride } from '@testing/providers/store-provider.mock'; describe('SelectPreprintServiceComponent', () => { @@ -35,11 +34,7 @@ describe('SelectPreprintServiceComponent', () => { TestBed.configureTestingModule({ imports: [SelectPreprintServiceComponent, ...MockComponents(SubHeaderComponent)], - providers: [ - provideOSFCore(), - MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), - provideMockStore({ signals }), - ], + providers: [provideOSFCore(), provideRouter([]), provideMockStore({ signals })], }); store = TestBed.inject(Store); diff --git a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts index 71b6c776b..8b93acd2b 100644 --- a/src/app/features/profile/components/profile-information/profile-information.component.spec.ts +++ b/src/app/features/profile/components/profile-information/profile-information.component.spec.ts @@ -3,7 +3,7 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { EducationHistoryComponent } from '@osf/shared/components/education-history/education-history.component'; import { EmploymentHistoryComponent } from '@osf/shared/components/employment-history/employment-history.component'; @@ -25,11 +25,11 @@ describe('ProfileInformationComponent', () => { const mockUser: UserModel = MOCK_USER; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [ProfileInformationComponent, ...MockComponents(EmploymentHistoryComponent, EducationHistoryComponent)], - providers: [provideOSFCore(), MockProvider(ActivatedRoute), MockProvider(IS_MEDIUM, of(false))], - }).compileComponents(); + providers: [provideOSFCore(), provideRouter([]), MockProvider(IS_MEDIUM, of(false))], + }); fixture = TestBed.createComponent(ProfileInformationComponent); component = fixture.componentInstance; @@ -179,21 +179,4 @@ describe('ProfileInformationComponent', () => { fixture.detectChanges(); expect(component.currentUserInstitutions()).toEqual(mockInstitutions); }); - - it('should not render institution logos when currentUserInstitutions is undefined', () => { - fixture.componentRef.setInput('currentUserInstitutions', undefined); - fixture.detectChanges(); - const logos = fixture.nativeElement.querySelectorAll('img.fit-contain'); - expect(logos.length).toBe(0); - }); - - it('should render institution logos when currentUserInstitutions is provided', () => { - const institutions: Institution[] = [MOCK_INSTITUTION]; - fixture.componentRef.setInput('currentUserInstitutions', institutions); - fixture.detectChanges(); - - const logos = fixture.nativeElement.querySelectorAll('img.fit-contain'); - expect(logos.length).toBe(institutions.length); - expect(logos[0].alt).toBe(institutions[0].name); - }); }); diff --git a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts index 2192db5a5..4126b4e3e 100644 --- a/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts +++ b/src/app/features/project/overview/components/overview-collections/overview-collections.component.spec.ts @@ -1,4 +1,5 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { collectionFilterNames } from '@osf/features/collections/constants'; import { CollectionSubmission } from '@osf/shared/models/collections/collections.model'; @@ -18,11 +19,11 @@ describe('OverviewCollectionsComponent', () => { let component: OverviewCollectionsComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [OverviewCollectionsComponent], - providers: [provideOSFCore()], - }).compileComponents(); + providers: [provideOSFCore(), provideRouter([])], + }); fixture = TestBed.createComponent(OverviewCollectionsComponent); component = fixture.componentInstance; diff --git a/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts b/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts index 7f235d7f6..becc25851 100644 --- a/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts +++ b/src/app/features/registry/components/registry-revisions/registry-revisions.component.spec.ts @@ -1,7 +1,5 @@ -import { MockProvider } from 'ng-mocks'; - import { TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; @@ -11,7 +9,6 @@ import { RegistryRevisionsComponent } from './registry-revisions.component'; import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { createMockSchemaResponse } from '@testing/mocks/schema-response.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; const MOCK_REGISTRY = MOCK_REGISTRATION_OVERVIEW_MODEL; const MOCK_RESPONSES = [ @@ -22,7 +19,7 @@ const MOCK_RESPONSES = [ function setup() { TestBed.configureTestingModule({ imports: [RegistryRevisionsComponent], - providers: [provideOSFCore(), MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build())], + providers: [provideOSFCore(), provideRouter([])], }); const fixture = TestBed.createComponent(RegistryRevisionsComponent); diff --git a/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts b/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts index 5b94caebd..3f63858fd 100644 --- a/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts +++ b/src/app/features/registry/components/registry-statuses/registry-statuses.component.spec.ts @@ -3,7 +3,7 @@ import { Store } from '@ngxs/store'; import { MockProvider } from 'ng-mocks'; import { TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; import { RegistryStatus } from '@osf/shared/enums/registry-status.enum'; @@ -18,7 +18,6 @@ import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-ov import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomConfirmationServiceMock } from '@testing/providers/custom-confirmation-provider.mock'; import { CustomDialogServiceMock } from '@testing/providers/custom-dialog-provider.mock'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; const MOCK_REGISTRY = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, embargoEndDate: '2024-01-01T00:00:00Z' }; @@ -37,10 +36,10 @@ function setup(overrides: SetupOverrides = {}) { imports: [RegistryStatusesComponent], providers: [ provideOSFCore(), - MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), + provideRouter([]), + provideMockStore(), MockProvider(CustomDialogService, mockDialogService), MockProvider(CustomConfirmationService, mockConfirmationService), - provideMockStore(), ], }); diff --git a/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts b/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts index d51619d47..0593afd9e 100644 --- a/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts +++ b/src/app/features/registry/components/short-registration-info/short-registration-info.component.spec.ts @@ -1,11 +1,11 @@ import { Store } from '@ngxs/store'; -import { MockComponent, MockProvider } from 'ng-mocks'; +import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; @@ -16,7 +16,6 @@ import { ShortRegistrationInfoComponent } from './short-registration-info.compon import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; -import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('ShortRegistrationInfoComponent', () => { @@ -25,7 +24,7 @@ describe('ShortRegistrationInfoComponent', () => { imports: [ShortRegistrationInfoComponent, MockComponent(ContributorsListComponent)], providers: [ provideOSFCore(), - MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), + provideRouter([]), provideMockStore({ signals: [ { selector: ContributorsSelectors.getBibliographicContributors, value: [] }, diff --git a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts index 5f634c21d..4d0093ef8 100644 --- a/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts +++ b/src/app/features/settings/developer-apps/pages/developer-apps-list/developer-apps-list.component.spec.ts @@ -7,6 +7,7 @@ import { ConfirmationService } from 'primeng/api'; import { provideHttpClient } from '@angular/common/http'; import { provideHttpClientTesting } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -23,11 +24,12 @@ describe('DeveloperApplicationsListComponent', () => { let fixture: ComponentFixture; let customConfirmationService: CustomConfirmationService; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [DeveloperAppsListComponent], providers: [ provideOSFCore(), + provideRouter([]), provideStore([DeveloperAppsState]), provideHttpClient(), provideHttpClientTesting(), @@ -35,7 +37,7 @@ describe('DeveloperApplicationsListComponent', () => { MockProvider(CustomConfirmationService), MockProvider(ToastService), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(DeveloperAppsListComponent); component = fixture.componentInstance; diff --git a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts index 18675f021..26f9c60d3 100644 --- a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts +++ b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts @@ -5,7 +5,7 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute, Navigation, Router, UrlTree } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { AddonSetupAccountFormComponent } from '@osf/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component'; import { AddonTermsComponent } from '@osf/shared/components/addons/addon-terms/addon-terms.component'; @@ -21,25 +21,15 @@ describe.skip('ConnectAddonComponent', () => { let component: ConnectAddonComponent; let fixture: ComponentFixture; - beforeEach(async () => { - const mockNavigation: Partial = { - id: 1, - initialUrl: new UrlTree(), - extractedUrl: new UrlTree(), - trigger: 'imperative', - previousNavigation: null, - extras: { - state: { addon: MOCK_ADDON }, - }, - }; - - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [ ConnectAddonComponent, ...MockComponents(SubHeaderComponent, AddonTermsComponent, AddonSetupAccountFormComponent), ], providers: [ provideOSFCore(), + provideRouter([]), MockProvider(Store, { selectSignal: jest.fn().mockImplementation((selector) => { if (selector === AddonsSelectors.getAddonsUserReference) { @@ -52,13 +42,8 @@ describe.skip('ConnectAddonComponent', () => { }), dispatch: jest.fn().mockReturnValue(of({})), }), - MockProvider(Router, { - getCurrentNavigation: () => mockNavigation as Navigation, - navigate: jest.fn(), - }), - MockProvider(ActivatedRoute), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(ConnectAddonComponent); component = fixture.componentInstance; diff --git a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts index 44c538fb0..f19e21ed4 100644 --- a/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts +++ b/src/app/features/settings/tokens/pages/tokens-list/tokens-list.component.spec.ts @@ -1,6 +1,7 @@ import { of } from 'rxjs'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideRouter } from '@angular/router'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -48,15 +49,16 @@ describe('TokensListComponent', () => { showSuccess: jest.fn(), }; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [TokensListComponent], providers: [ provideOSFCore(), + provideRouter([]), { provide: CustomConfirmationService, useValue: mockConfirmationService }, { provide: ToastService, useValue: mockToastService }, ], - }).compileComponents(); + }); fixture = TestBed.createComponent(TokensListComponent); component = fixture.componentInstance; diff --git a/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts b/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts index 1185fb7b4..256a8bff3 100644 --- a/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts +++ b/src/app/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component.spec.ts @@ -2,6 +2,7 @@ import { MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { FormControl, FormGroup } from '@angular/forms'; +import { provideRouter } from '@angular/router'; import { AddonFormControls } from '@osf/shared/enums/addon-form-controls.enum'; import { AddonFormService } from '@shared/services/addons/addon-form.service'; @@ -22,11 +23,11 @@ describe('AddonSetupAccountFormComponent', () => { generateAuthorizedAddonPayload: jest.fn(), }; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [AddonSetupAccountFormComponent], - providers: [provideOSFCore(), MockProvider(AddonFormService, mockAddonFormService)], - }).compileComponents(); + providers: [provideOSFCore(), provideRouter([]), MockProvider(AddonFormService, mockAddonFormService)], + }); fixture = TestBed.createComponent(AddonSetupAccountFormComponent); component = fixture.componentInstance; diff --git a/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts b/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts index 1f9cfcb12..9fd8167fc 100644 --- a/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts +++ b/src/app/shared/components/affiliated-institutions-view/affiliated-institutions-view.component.spec.ts @@ -14,11 +14,11 @@ describe('AffiliatedInstitutionsViewComponent', () => { const mockInstitutions: Institution[] = [MOCK_INSTITUTION]; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [AffiliatedInstitutionsViewComponent], providers: [provideOSFCore(), provideRouter([])], - }).compileComponents(); + }); fixture = TestBed.createComponent(AffiliatedInstitutionsViewComponent); component = fixture.componentInstance; diff --git a/src/app/shared/components/registration-card/registration-card.component.spec.ts b/src/app/shared/components/registration-card/registration-card.component.spec.ts index 98ef2d4e7..f7124ce38 100644 --- a/src/app/shared/components/registration-card/registration-card.component.spec.ts +++ b/src/app/shared/components/registration-card/registration-card.component.spec.ts @@ -1,8 +1,8 @@ -import { MockComponents, MockProvider } from 'ng-mocks'; +import { MockComponents } from 'ng-mocks'; import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { RegistriesSelectors } from '@osf/features/registries/store'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; @@ -26,20 +26,20 @@ describe('RegistrationCardComponent', () => { const mockRegistrationData: RegistrationCard = MOCK_REGISTRATION; - beforeEach(async () => { - await TestBed.configureTestingModule({ + beforeEach(() => { + TestBed.configureTestingModule({ imports: [ RegistrationCardComponent, ...MockComponents(StatusBadgeComponent, DataResourcesComponent, IconComponent, ContributorsListComponent), ], providers: [ provideOSFCore(), + provideRouter([]), provideMockStore({ signals: [{ selector: RegistriesSelectors.getSchemaResponse, value: signal(null) }], }), - MockProvider(ActivatedRoute), ], - }).compileComponents(); + }); fixture = TestBed.createComponent(RegistrationCardComponent); component = fixture.componentInstance; From 813196ca967cff1520c10e80cf75a06ca30b11ec Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Mar 2026 19:02:34 +0200 Subject: [PATCH 04/10] test(zonejs): removed zonejs --- package-lock.json | 3 +-- package.json | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index f7e25618e..6cf892e0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,8 +86,7 @@ "source-map-explorer": "^2.5.3", "ts-jest": "^29.4.6", "typescript": "~5.9.3", - "typescript-eslint": "^8.56.1", - "zone.js": "^0.16.1" + "typescript-eslint": "^8.56.1" } }, "node_modules/@aduh95/viz.js": { diff --git a/package.json b/package.json index fcec3f6c3..26509a6ad 100644 --- a/package.json +++ b/package.json @@ -111,8 +111,7 @@ "source-map-explorer": "^2.5.3", "ts-jest": "^29.4.6", "typescript": "~5.9.3", - "typescript-eslint": "^8.56.1", - "zone.js": "^0.16.1" + "typescript-eslint": "^8.56.1" }, "lint-staged": { "**/*.{ts,html,scss}": [ From cbbd66497767fccb9a6358ed00df4d09f6c685f0 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 23 Mar 2026 19:13:53 +0200 Subject: [PATCH 05/10] fix(coverage): updated check coverage thresholds --- .github/scripts/check-coverage-thresholds.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/scripts/check-coverage-thresholds.js b/.github/scripts/check-coverage-thresholds.js index 02c3995fd..3ae4ee0e4 100644 --- a/.github/scripts/check-coverage-thresholds.js +++ b/.github/scripts/check-coverage-thresholds.js @@ -1,6 +1,5 @@ -const fs = require('fs'); const coverage = require('../../coverage/coverage-summary.json'); -const jestConfig = require('../../jest.config.js'); +const jestConfig = require('../../jest.config.ts'); const summary = coverage.total; const thresholds = jestConfig.coverageThreshold.global; @@ -29,7 +28,7 @@ for (const key of ['branches', 'functions', 'lines', 'statements']) { ); errors.push( formatErrorsWithAlignedStars( - `Please update the coverageThreshold.global.${key} in the jest.config.js to ---> ${current} <---`, + `Please update the coverageThreshold.global.${key} in the jest.config.ts to ---> ${current} <---`, true ) ); @@ -41,9 +40,9 @@ for (const key of ['branches', 'functions', 'lines', 'statements']) { if (failed) { const stars = '*'.repeat(warnMessage.length + 8); console.log('\n\nCongratulations! You have successfully run the coverage check and added tests.'); - console.log('\n\nThe jest.config.js file is not insync with your new test additions.'); - console.log('Please update the coverage thresholds in jest.config.js.'); - console.log('You will need to commit again once you have updated the jst.config.js file.'); + console.log('\n\nThe jest.config.ts file is not insync with your new test additions.'); + console.log('Please update the coverage thresholds in jest.config.ts.'); + console.log('You will need to commit again once you have updated the jst.config.ts file.'); console.log('This is only necessary until we hit 100% coverage.'); console.log(`\n\n${stars}`); errors.forEach((err) => { From c0f6b3b8a1f6e817963e65dc30ea683a5bf06f82 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 24 Mar 2026 13:05:59 +0200 Subject: [PATCH 06/10] test(import): updated test imports for components --- .../admin-institutions/components/index.ts | 1 - .../admin-institutions/pages/index.ts | 5 --- .../institutions-preprints.component.spec.ts | 2 +- .../institutions-preprints.component.ts | 2 +- .../institutions-projects.component.spec.ts | 2 +- .../institutions-projects.component.ts | 2 +- ...stitutions-registrations.component.spec.ts | 2 +- .../institutions-registrations.component.ts | 2 +- .../institutions-users.component.spec.ts | 3 +- .../institutions-users.component.ts | 2 +- src/app/features/admin-institutions/routes.ts | 13 +++--- .../admin-institutions/services/index.ts | 1 - src/app/features/auth/pages/index.ts | 3 -- .../reset-password.component.spec.ts | 3 +- .../pages/sign-up/sign-up.component.spec.ts | 23 +++++----- .../auth/pages/sign-up/sign-up.component.ts | 8 ++-- .../components/add-to-collection/index.ts | 5 --- .../collections-discover.component.spec.ts | 3 +- ...collections-main-content.component.spec.ts | 9 ++-- .../collections-main-content.component.ts | 2 +- ...llections-search-results.component.spec.ts | 3 +- .../features/collections/components/index.ts | 7 --- src/app/features/metadata/components/index.ts | 13 ------ src/app/features/metadata/dialogs/index.ts | 8 ---- .../metadata/metadata.component.spec.ts | 28 ++++++------ .../features/metadata/metadata.component.ts | 44 +++++++++---------- .../add-metadata.component.spec.ts | 2 +- .../add-metadata/add-metadata.component.ts | 2 +- src/app/features/metadata/pages/index.ts | 1 - ...n-moderation-submissions.component.spec.ts | 2 +- ...lection-submissions-list.component.spec.ts | 3 +- .../features/moderation/components/index.ts | 19 -------- .../preprint-submissions.component.spec.ts | 3 +- .../preprint-submissions.component.ts | 2 +- .../registry-submissions.component.spec.ts | 2 +- .../registry-submissions.component.ts | 2 +- .../my-preprint-reviewing.component.spec.ts | 6 +-- .../my-preprint-reviewing.component.ts | 3 +- .../advisory-board.component.spec.ts | 3 ++ .../features/preprints/components/index.ts | 23 ---------- ...preprint-provider-footer.component.spec.ts | 3 ++ .../preprint-provider-hero.component.spec.ts | 1 + .../preprint-provider-hero.component.ts | 5 ++- .../preprints-help-dialog.component.ts | 2 +- .../title-and-abstract-step.component.spec.ts | 3 +- .../create-new-version.component.spec.ts | 3 +- .../create-new-version.component.ts | 3 +- .../preprint-details.component.spec.ts | 22 +++++----- .../preprint-details.component.ts | 24 +++++----- ...eprint-provider-discover.component.spec.ts | 2 +- .../preprint-provider-discover.component.ts | 2 +- ...eprint-provider-overview.component.spec.ts | 10 ++--- .../preprint-provider-overview.component.ts | 10 ++--- .../preprints-landing.component.spec.ts | 4 +- .../preprints-landing.component.ts | 4 +- .../submit-preprint-stepper.component.spec.ts | 14 +++--- .../submit-preprint-stepper.component.ts | 14 +++--- .../update-preprint-stepper.component.spec.ts | 14 +++--- .../update-preprint-stepper.component.ts | 14 +++--- .../preprints/preprints.component.spec.ts | 2 +- src/app/features/preprints/services/index.ts | 5 --- .../store/my-preprints/my-preprints.state.ts | 2 +- .../preprint-providers.state.ts | 3 +- .../preprint-stepper.state.ts | 11 +++-- .../store/preprint/preprint.state.ts | 2 +- .../registry-provider-hero.component.spec.ts | 1 + .../registry-provider-hero.component.ts | 2 +- .../metadata-tabs.component.spec.ts | 2 +- .../metadata-tabs/metadata-tabs.component.ts | 2 +- .../edit-section.component.spec.ts | 9 +++- 70 files changed, 188 insertions(+), 276 deletions(-) delete mode 100644 src/app/features/admin-institutions/components/index.ts delete mode 100644 src/app/features/admin-institutions/pages/index.ts delete mode 100644 src/app/features/admin-institutions/services/index.ts delete mode 100644 src/app/features/auth/pages/index.ts delete mode 100644 src/app/features/collections/components/add-to-collection/index.ts delete mode 100644 src/app/features/collections/components/index.ts delete mode 100644 src/app/features/metadata/components/index.ts delete mode 100644 src/app/features/metadata/dialogs/index.ts delete mode 100644 src/app/features/metadata/pages/index.ts delete mode 100644 src/app/features/moderation/components/index.ts delete mode 100644 src/app/features/preprints/components/index.ts delete mode 100644 src/app/features/preprints/services/index.ts diff --git a/src/app/features/admin-institutions/components/index.ts b/src/app/features/admin-institutions/components/index.ts deleted file mode 100644 index 48b38339b..000000000 --- a/src/app/features/admin-institutions/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AdminTableComponent } from './admin-table/admin-table.component'; diff --git a/src/app/features/admin-institutions/pages/index.ts b/src/app/features/admin-institutions/pages/index.ts deleted file mode 100644 index c5679acff..000000000 --- a/src/app/features/admin-institutions/pages/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { InstitutionsPreprintsComponent } from './institutions-preprints/institutions-preprints.component'; -export { InstitutionsProjectsComponent } from './institutions-projects/institutions-projects.component'; -export { InstitutionsRegistrationsComponent } from './institutions-registrations/institutions-registrations.component'; -export { InstitutionsSummaryComponent } from './institutions-summary/institutions-summary.component'; -export { InstitutionsUsersComponent } from './institutions-users/institutions-users.component'; diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts index 3d74762b2..5943d684b 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.spec.ts @@ -8,7 +8,6 @@ import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { AdminTableComponent } from '@osf/features/admin-institutions/components'; import { DownloadType } from '@osf/features/admin-institutions/enums'; import * as downloadHelper from '@osf/features/admin-institutions/helpers'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; @@ -25,6 +24,7 @@ import { SetSortBy, } from '@osf/shared/stores/global-search'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; import { FiltersSectionComponent } from '../../components/filters-section/filters-section.component'; import { InstitutionsPreprintsComponent } from './institutions-preprints.component'; diff --git a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts index 271e7e1bf..c80773de5 100644 --- a/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-preprints/institutions-preprints.component.ts @@ -22,7 +22,7 @@ import { SetSortBy, } from '@osf/shared/stores/global-search'; -import { AdminTableComponent } from '../../components'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; import { FiltersSectionComponent } from '../../components/filters-section/filters-section.component'; import { preprintsTableColumns } from '../../constants'; import { DownloadType } from '../../enums'; diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts index 4fb81b5ca..5a21c1316 100644 --- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.spec.ts @@ -9,7 +9,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@core/store/user'; -import { AdminTableComponent } from '@osf/features/admin-institutions/components'; import { DownloadType } from '@osf/features/admin-institutions/enums'; import * as downloadHelper from '@osf/features/admin-institutions/helpers'; import { TableColumn, TableIconClickEvent } from '@osf/features/admin-institutions/models'; @@ -29,6 +28,7 @@ import { SetSortBy, } from '@osf/shared/stores/global-search'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; import { FiltersSectionComponent } from '../../components/filters-section/filters-section.component'; import { InstitutionsProjectsComponent } from './institutions-projects.component'; diff --git a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts index 11c826559..9d964436f 100644 --- a/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-projects/institutions-projects.component.ts @@ -37,7 +37,7 @@ import { SetSortBy, } from '@osf/shared/stores/global-search'; -import { AdminTableComponent } from '../../components'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; import { FiltersSectionComponent } from '../../components/filters-section/filters-section.component'; import { RequestAccessErrorDialogComponent } from '../../components/request-access-error-dialog/request-access-error-dialog.component'; import { projectTableColumns } from '../../constants'; diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts index e82a551e9..a6ad5ee2b 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.spec.ts @@ -8,7 +8,6 @@ import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { AdminTableComponent } from '@osf/features/admin-institutions/components'; import { DownloadType } from '@osf/features/admin-institutions/enums'; import * as downloadHelper from '@osf/features/admin-institutions/helpers'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; @@ -25,6 +24,7 @@ import { SetSortBy, } from '@osf/shared/stores/global-search'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; import { FiltersSectionComponent } from '../../components/filters-section/filters-section.component'; import { InstitutionsRegistrationsComponent } from './institutions-registrations.component'; diff --git a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts index 099951102..d68ab8fae 100644 --- a/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-registrations/institutions-registrations.component.ts @@ -23,7 +23,7 @@ import { SetSortBy, } from '@osf/shared/stores/global-search'; -import { AdminTableComponent } from '../../components'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; import { FiltersSectionComponent } from '../../components/filters-section/filters-section.component'; import { registrationTableColumns } from '../../constants'; import { DownloadType } from '../../enums'; diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts index 4b8a93fe3..cd04b61ad 100644 --- a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts +++ b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.spec.ts @@ -6,7 +6,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@osf/core/store/user'; -import { AdminTableComponent } from '@osf/features/admin-institutions/components'; import { DownloadType } from '@osf/features/admin-institutions/enums'; import { InstitutionsAdminSelectors } from '@osf/features/admin-institutions/store'; import { SelectComponent } from '@osf/shared/components/select/select.component'; @@ -15,6 +14,8 @@ import { ToastService } from '@osf/shared/services/toast.service'; import { SortOrder } from '@shared/enums/sort-order.enum'; import { SearchFilters } from '@shared/models/search-filters.model'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; + import { InstitutionsUsersComponent } from './institutions-users.component'; import { diff --git a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts index b620ed107..c311be47a 100644 --- a/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts +++ b/src/app/features/admin-institutions/pages/institutions-users/institutions-users.component.ts @@ -21,7 +21,7 @@ import { SearchFilters } from '@osf/shared/models/search-filters.model'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { AdminTableComponent } from '../../components'; +import { AdminTableComponent } from '../../components/admin-table/admin-table.component'; import { departmentOptions, userTableColumns } from '../../constants'; import { SendEmailDialogComponent } from '../../dialogs'; import { DownloadType } from '../../enums'; diff --git a/src/app/features/admin-institutions/routes.ts b/src/app/features/admin-institutions/routes.ts index 8c77a13ff..421b95aa4 100644 --- a/src/app/features/admin-institutions/routes.ts +++ b/src/app/features/admin-institutions/routes.ts @@ -1,13 +1,10 @@ import { Routes } from '@angular/router'; -import { - InstitutionsPreprintsComponent, - InstitutionsProjectsComponent, - InstitutionsRegistrationsComponent, - InstitutionsSummaryComponent, - InstitutionsUsersComponent, -} from '@osf/features/admin-institutions/pages'; - +import { InstitutionsPreprintsComponent } from './pages/institutions-preprints/institutions-preprints.component'; +import { InstitutionsProjectsComponent } from './pages/institutions-projects/institutions-projects.component'; +import { InstitutionsRegistrationsComponent } from './pages/institutions-registrations/institutions-registrations.component'; +import { InstitutionsSummaryComponent } from './pages/institutions-summary/institutions-summary.component'; +import { InstitutionsUsersComponent } from './pages/institutions-users/institutions-users.component'; import { AdminInstitutionsComponent } from './admin-institutions.component'; export const routes: Routes = [ diff --git a/src/app/features/admin-institutions/services/index.ts b/src/app/features/admin-institutions/services/index.ts deleted file mode 100644 index 6febec8a5..000000000 --- a/src/app/features/admin-institutions/services/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './institutions-admin.service'; diff --git a/src/app/features/auth/pages/index.ts b/src/app/features/auth/pages/index.ts deleted file mode 100644 index 22c2f0914..000000000 --- a/src/app/features/auth/pages/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { ForgotPasswordComponent } from './forgot-password/forgot-password.component'; -export { ResetPasswordComponent } from './reset-password/reset-password.component'; -export { SignUpComponent } from './sign-up/sign-up.component'; diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts b/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts index 7e0d9e3ae..1fa6e2530 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts +++ b/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts @@ -6,9 +6,10 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { AuthService } from '@core/services/auth.service'; -import { ResetPasswordComponent } from '@osf/features/auth/pages'; import { PasswordInputHintComponent } from '@osf/shared/components/password-input-hint/password-input-hint.component'; +import { ResetPasswordComponent } from './reset-password.component'; + import { provideOSFCore } from '@testing/osf.testing.provider'; describe('ResetPasswordComponent', () => { diff --git a/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts b/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts index 3e1e27167..649f99ea0 100644 --- a/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts +++ b/src/app/features/auth/pages/sign-up/sign-up.component.spec.ts @@ -1,10 +1,12 @@ -import { MockComponent, MockProvider } from 'ng-mocks'; +import { NgxCaptchaModule } from 'ngx-captcha'; +import { MockComponents, MockModule, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; +import { provideRouter } from '@angular/router'; import { AuthService } from '@core/services/auth.service'; import { PasswordInputHintComponent } from '@osf/shared/components/password-input-hint/password-input-hint.component'; +import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; import { ToastService } from '@osf/shared/services/toast.service'; import { SignUpComponent } from './sign-up.component'; @@ -15,16 +17,15 @@ describe('SignUpComponent', () => { let component: SignUpComponent; let fixture: ComponentFixture; - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [SignUpComponent, MockComponent(PasswordInputHintComponent)], - providers: [ - provideOSFCore(), - MockProvider(ActivatedRoute), - MockProvider(ToastService), - MockProvider(AuthService), + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + SignUpComponent, + ...MockComponents(TextInputComponent, PasswordInputHintComponent), + MockModule(NgxCaptchaModule), ], - }).compileComponents(); + providers: [provideOSFCore(), provideRouter([]), MockProvider(ToastService), MockProvider(AuthService)], + }); fixture = TestBed.createComponent(SignUpComponent); component = fixture.componentInstance; diff --git a/src/app/features/auth/pages/sign-up/sign-up.component.ts b/src/app/features/auth/pages/sign-up/sign-up.component.ts index cb1276016..283df47df 100644 --- a/src/app/features/auth/pages/sign-up/sign-up.component.ts +++ b/src/app/features/auth/pages/sign-up/sign-up.component.ts @@ -27,17 +27,17 @@ import { SignUpForm } from '../../models'; @Component({ selector: 'osf-sign-up', imports: [ - ReactiveFormsModule, Button, - Password, Checkbox, Divider, + Password, NgOptimizedImage, + ReactiveFormsModule, RouterLink, - PasswordInputHintComponent, - TranslatePipe, NgxCaptchaModule, TextInputComponent, + PasswordInputHintComponent, + TranslatePipe, ], templateUrl: './sign-up.component.html', styleUrl: './sign-up.component.scss', diff --git a/src/app/features/collections/components/add-to-collection/index.ts b/src/app/features/collections/components/add-to-collection/index.ts deleted file mode 100644 index 3661b3214..000000000 --- a/src/app/features/collections/components/add-to-collection/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { AddToCollectionConfirmationDialogComponent } from './add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component'; -export { CollectionMetadataStepComponent } from './collection-metadata-step/collection-metadata-step.component'; -export { ProjectContributorsStepComponent } from './project-contributors-step/project-contributors-step.component'; -export { ProjectMetadataStepComponent } from './project-metadata-step/project-metadata-step.component'; -export { SelectProjectStepComponent } from './select-project-step/select-project-step.component'; diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts index c887e126c..47abd6ff2 100644 --- a/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts +++ b/src/app/features/collections/components/collections-discover/collections-discover.component.spec.ts @@ -4,13 +4,14 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; -import { CollectionsMainContentComponent } from '@osf/features/collections/components'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { CollectionsSelectors } from '@shared/stores/collections'; +import { CollectionsMainContentComponent } from '../collections-main-content'; + import { CollectionsDiscoverComponent } from './collections-discover.component'; import { MOCK_PROVIDER } from '@testing/mocks/provider.mock'; diff --git a/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts b/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts index 152890ecb..f92a0e604 100644 --- a/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts +++ b/src/app/features/collections/components/collections-main-content/collections-main-content.component.spec.ts @@ -2,13 +2,12 @@ import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { - CollectionsFilterChipsComponent, - CollectionsFiltersComponent, - CollectionsSearchResultsComponent, -} from '@osf/features/collections/components'; import { CollectionsSelectors } from '@shared/stores/collections'; +import { CollectionsFilterChipsComponent } from '../collections-filter-chips/collections-filter-chips.component'; +import { CollectionsFiltersComponent } from '../collections-filters/collections-filters.component'; +import { CollectionsSearchResultsComponent } from '../collections-search-results/collections-search-results.component'; + import { CollectionsMainContentComponent } from './collections-main-content.component'; import { MOCK_COLLECTIONS_SELECTED_FILTERS } from '@testing/mocks/collections-filters.mock'; diff --git a/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts b/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts index 3509e6d64..170971d87 100644 --- a/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts +++ b/src/app/features/collections/components/collections-main-content/collections-main-content.component.ts @@ -10,11 +10,11 @@ import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@a import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; -import { CollectionsFilterChipsComponent } from '@osf/features/collections/components'; import { collectionsSortOptions } from '@osf/features/collections/constants'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; import { CollectionsSelectors, SetSortBy } from '@shared/stores/collections'; +import { CollectionsFilterChipsComponent } from '../collections-filter-chips/collections-filter-chips.component'; import { CollectionsFiltersComponent } from '../collections-filters/collections-filters.component'; import { CollectionsSearchResultsComponent } from '../collections-search-results/collections-search-results.component'; diff --git a/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts b/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts index 4944150d6..124cccc82 100644 --- a/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts +++ b/src/app/features/collections/components/collections-search-results/collections-search-results.component.spec.ts @@ -2,10 +2,11 @@ import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CollectionsSearchResultCardComponent } from '@osf/features/collections/components'; import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; import { CollectionsSelectors } from '@shared/stores/collections'; +import { CollectionsSearchResultCardComponent } from '../collections-search-result-card/collections-search-result-card.component'; + import { CollectionsSearchResultsComponent } from './collections-search-results.component'; import { MOCK_COLLECTION_SUBMISSION_WITH_GUID } from '@testing/mocks/submission.mock'; diff --git a/src/app/features/collections/components/index.ts b/src/app/features/collections/components/index.ts deleted file mode 100644 index 4afafff2e..000000000 --- a/src/app/features/collections/components/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { AddToCollectionComponent } from './add-to-collection/add-to-collection.component'; -export { CollectionsFilterChipsComponent } from './collections-filter-chips/collections-filter-chips.component'; -export { CollectionsFiltersComponent } from './collections-filters/collections-filters.component'; -export { CollectionsHelpDialogComponent } from './collections-help-dialog/collections-help-dialog.component'; -export { CollectionsMainContentComponent } from './collections-main-content/collections-main-content.component'; -export { CollectionsSearchResultCardComponent } from './collections-search-result-card/collections-search-result-card.component'; -export { CollectionsSearchResultsComponent } from './collections-search-results/collections-search-results.component'; diff --git a/src/app/features/metadata/components/index.ts b/src/app/features/metadata/components/index.ts deleted file mode 100644 index b4eeb2184..000000000 --- a/src/app/features/metadata/components/index.ts +++ /dev/null @@ -1,13 +0,0 @@ -export { CedarTemplateFormComponent } from './cedar-template-form/cedar-template-form.component'; -export { MetadataAffiliatedInstitutionsComponent } from './metadata-affiliated-institutions/metadata-affiliated-institutions.component'; -export { MetadataContributorsComponent } from './metadata-contributors/metadata-contributors.component'; -export { MetadataDateInfoComponent } from './metadata-date-info/metadata-date-info.component'; -export { MetadataDescriptionComponent } from './metadata-description/metadata-description.component'; -export { MetadataFundingComponent } from './metadata-funding/metadata-funding.component'; -export { MetadataLicenseComponent } from './metadata-license/metadata-license.component'; -export { MetadataPublicationDoiComponent } from './metadata-publication-doi/metadata-publication-doi.component'; -export { MetadataRegistrationDoiComponent } from './metadata-registration-doi/metadata-registration-doi.component'; -export { MetadataResourceInformationComponent } from './metadata-resource-information/metadata-resource-information.component'; -export { MetadataSubjectsComponent } from './metadata-subjects/metadata-subjects.component'; -export { MetadataTagsComponent } from './metadata-tags/metadata-tags.component'; -export { MetadataTitleComponent } from './metadata-title/metadata-title.component'; diff --git a/src/app/features/metadata/dialogs/index.ts b/src/app/features/metadata/dialogs/index.ts deleted file mode 100644 index c3cd29a34..000000000 --- a/src/app/features/metadata/dialogs/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { AffiliatedInstitutionsDialogComponent } from './affiliated-institutions-dialog/affiliated-institutions-dialog.component'; -export { ContributorsDialogComponent } from './contributors-dialog/contributors-dialog.component'; -export { DescriptionDialogComponent } from './description-dialog/description-dialog.component'; -export { FundingDialogComponent } from './funding-dialog/funding-dialog.component'; -export { LicenseDialogComponent } from './license-dialog/license-dialog.component'; -export { PublicationDoiDialogComponent } from './publication-doi-dialog/publication-doi-dialog.component'; -export { ResourceInformationDialogComponent } from './resource-information-dialog/resource-information-dialog.component'; -export { ResourceInfoTooltipComponent } from './resource-tooltip-info/resource-tooltip-info.component'; diff --git a/src/app/features/metadata/metadata.component.spec.ts b/src/app/features/metadata/metadata.component.spec.ts index 501952227..0a6b8b529 100644 --- a/src/app/features/metadata/metadata.component.spec.ts +++ b/src/app/features/metadata/metadata.component.spec.ts @@ -3,21 +3,6 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { - MetadataAffiliatedInstitutionsComponent, - MetadataContributorsComponent, - MetadataDateInfoComponent, - MetadataDescriptionComponent, - MetadataFundingComponent, - MetadataLicenseComponent, - MetadataPublicationDoiComponent, - MetadataRegistrationDoiComponent, - MetadataResourceInformationComponent, - MetadataSubjectsComponent, - MetadataTagsComponent, - MetadataTitleComponent, -} from '@osf/features/metadata/components'; -import { MetadataSelectors } from '@osf/features/metadata/store'; import { MetadataTabsComponent } from '@osf/shared/components/metadata-tabs/metadata-tabs.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; @@ -26,7 +11,20 @@ import { CustomDialogService } from '@osf/shared/services/custom-dialog.service' import { ToastService } from '@osf/shared/services/toast.service'; import { RegistrationProviderSelectors } from '@osf/shared/stores/registration-provider'; +import { MetadataAffiliatedInstitutionsComponent } from './components/metadata-affiliated-institutions/metadata-affiliated-institutions.component'; +import { MetadataContributorsComponent } from './components/metadata-contributors/metadata-contributors.component'; +import { MetadataDateInfoComponent } from './components/metadata-date-info/metadata-date-info.component'; +import { MetadataDescriptionComponent } from './components/metadata-description/metadata-description.component'; +import { MetadataFundingComponent } from './components/metadata-funding/metadata-funding.component'; +import { MetadataLicenseComponent } from './components/metadata-license/metadata-license.component'; +import { MetadataPublicationDoiComponent } from './components/metadata-publication-doi/metadata-publication-doi.component'; +import { MetadataRegistrationDoiComponent } from './components/metadata-registration-doi/metadata-registration-doi.component'; +import { MetadataResourceInformationComponent } from './components/metadata-resource-information/metadata-resource-information.component'; +import { MetadataSubjectsComponent } from './components/metadata-subjects/metadata-subjects.component'; +import { MetadataTagsComponent } from './components/metadata-tags/metadata-tags.component'; +import { MetadataTitleComponent } from './components/metadata-title/metadata-title.component'; import { MetadataComponent } from './metadata.component'; +import { MetadataSelectors } from './store'; import { MOCK_PROJECT_METADATA } from '@testing/mocks/project-metadata.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; diff --git a/src/app/features/metadata/metadata.component.ts b/src/app/features/metadata/metadata.component.ts index 5e4dc966d..ad6f68623 100644 --- a/src/app/features/metadata/metadata.component.ts +++ b/src/app/features/metadata/metadata.component.ts @@ -50,33 +50,29 @@ import { import { MetadataTabsModel } from '@shared/models/metadata-tabs.model'; import { SubjectModel } from '@shared/models/subject/subject.model'; +import { MetadataAffiliatedInstitutionsComponent } from './components/metadata-affiliated-institutions/metadata-affiliated-institutions.component'; import { MetadataCollectionsComponent } from './components/metadata-collections/metadata-collections.component'; +import { MetadataContributorsComponent } from './components/metadata-contributors/metadata-contributors.component'; +import { MetadataDateInfoComponent } from './components/metadata-date-info/metadata-date-info.component'; +import { MetadataDescriptionComponent } from './components/metadata-description/metadata-description.component'; +import { MetadataFundingComponent } from './components/metadata-funding/metadata-funding.component'; +import { MetadataLicenseComponent } from './components/metadata-license/metadata-license.component'; +import { MetadataPublicationDoiComponent } from './components/metadata-publication-doi/metadata-publication-doi.component'; +import { MetadataRegistrationDoiComponent } from './components/metadata-registration-doi/metadata-registration-doi.component'; import { MetadataRegistryInfoComponent } from './components/metadata-registry-info/metadata-registry-info.component'; +import { MetadataResourceInformationComponent } from './components/metadata-resource-information/metadata-resource-information.component'; +import { MetadataSubjectsComponent } from './components/metadata-subjects/metadata-subjects.component'; +import { MetadataTagsComponent } from './components/metadata-tags/metadata-tags.component'; +import { MetadataTitleComponent } from './components/metadata-title/metadata-title.component'; +import { AffiliatedInstitutionsDialogComponent } from './dialogs/affiliated-institutions-dialog/affiliated-institutions-dialog.component'; +import { ContributorsDialogComponent } from './dialogs/contributors-dialog/contributors-dialog.component'; +import { DescriptionDialogComponent } from './dialogs/description-dialog/description-dialog.component'; import { EditTitleDialogComponent } from './dialogs/edit-title-dialog/edit-title-dialog.component'; -import { - MetadataAffiliatedInstitutionsComponent, - MetadataContributorsComponent, - MetadataDateInfoComponent, - MetadataDescriptionComponent, - MetadataFundingComponent, - MetadataLicenseComponent, - MetadataPublicationDoiComponent, - MetadataRegistrationDoiComponent, - MetadataResourceInformationComponent, - MetadataSubjectsComponent, - MetadataTagsComponent, - MetadataTitleComponent, -} from './components'; -import { - AffiliatedInstitutionsDialogComponent, - ContributorsDialogComponent, - DescriptionDialogComponent, - FundingDialogComponent, - LicenseDialogComponent, - PublicationDoiDialogComponent, - ResourceInformationDialogComponent, - ResourceInfoTooltipComponent, -} from './dialogs'; +import { FundingDialogComponent } from './dialogs/funding-dialog/funding-dialog.component'; +import { LicenseDialogComponent } from './dialogs/license-dialog/license-dialog.component'; +import { PublicationDoiDialogComponent } from './dialogs/publication-doi-dialog/publication-doi-dialog.component'; +import { ResourceInformationDialogComponent } from './dialogs/resource-information-dialog/resource-information-dialog.component'; +import { ResourceInfoTooltipComponent } from './dialogs/resource-tooltip-info/resource-tooltip-info.component'; import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData, diff --git a/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts b/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts index b0cb6f095..f006ff8d1 100644 --- a/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts +++ b/src/app/features/metadata/pages/add-metadata/add-metadata.component.spec.ts @@ -3,12 +3,12 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { CedarTemplateFormComponent } from '@osf/features/metadata/components'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { ToastService } from '@osf/shared/services/toast.service'; +import { CedarTemplateFormComponent } from '../../components/cedar-template-form/cedar-template-form.component'; import { MetadataSelectors } from '../../store'; import { AddMetadataComponent } from './add-metadata.component'; diff --git a/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts b/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts index 63cd38edf..c60deb5be 100644 --- a/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts +++ b/src/app/features/metadata/pages/add-metadata/add-metadata.component.ts @@ -24,7 +24,7 @@ import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { IS_MEDIUM } from '@osf/shared/helpers/breakpoints.tokens'; import { ToastService } from '@osf/shared/services/toast.service'; -import { CedarTemplateFormComponent } from '../../components'; +import { CedarTemplateFormComponent } from '../../components/cedar-template-form/cedar-template-form.component'; import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData, CedarRecordDataBinding } from '../../models'; import { CreateCedarMetadataRecord, diff --git a/src/app/features/metadata/pages/index.ts b/src/app/features/metadata/pages/index.ts deleted file mode 100644 index 264da8299..000000000 --- a/src/app/features/metadata/pages/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AddMetadataComponent } from './add-metadata/add-metadata.component'; diff --git a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts index 0738975f2..c96432dbc 100644 --- a/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts +++ b/src/app/features/moderation/components/collection-moderation-submissions/collection-moderation-submissions.component.spec.ts @@ -3,7 +3,6 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { CollectionSubmissionsListComponent } from '@osf/features/moderation/components'; import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; @@ -12,6 +11,7 @@ import { CollectionsSelectors } from '@osf/shared/stores/collections'; import { SubmissionReviewStatus } from '../../enums'; import { CollectionsModerationSelectors } from '../../store/collections-moderation'; +import { CollectionSubmissionsListComponent } from '../collection-submissions-list/collection-submissions-list.component'; import { CollectionModerationSubmissionsComponent } from './collection-moderation-submissions.component'; diff --git a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts index 384c818c7..e39d440ce 100644 --- a/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts +++ b/src/app/features/moderation/components/collection-submissions-list/collection-submissions-list.component.spec.ts @@ -2,9 +2,8 @@ import { MockComponent } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CollectionSubmissionItemComponent } from '@osf/features/moderation/components'; - import { CollectionsModerationSelectors } from '../../store/collections-moderation'; +import { CollectionSubmissionItemComponent } from '../collection-submission-item/collection-submission-item.component'; import { CollectionSubmissionsListComponent } from './collection-submissions-list.component'; diff --git a/src/app/features/moderation/components/index.ts b/src/app/features/moderation/components/index.ts deleted file mode 100644 index 4884d20bb..000000000 --- a/src/app/features/moderation/components/index.ts +++ /dev/null @@ -1,19 +0,0 @@ -export { AddModeratorDialogComponent } from './add-moderator-dialog/add-moderator-dialog.component'; -export { BulkUploadComponent } from './bulk-upload/bulk-upload.component'; -export { CollectionModerationSubmissionsComponent } from './collection-moderation-submissions/collection-moderation-submissions.component'; -export { InviteModeratorDialogComponent } from './invite-moderator-dialog/invite-moderator-dialog.component'; -export { ModeratorsListComponent } from './moderators-list/moderators-list.component'; -export { ModeratorsTableComponent } from './moderators-table/moderators-table.component'; -export { MyReviewingNavigationComponent } from './my-reviewing-navigation/my-reviewing-navigation.component'; -export { NotificationSettingsComponent } from './notification-settings/notification-settings.component'; -export { PreprintModerationSettingsComponent } from './preprint-moderation-settings/preprint-moderation-settings.component'; -export { PreprintRecentActivityListComponent } from './preprint-recent-activity-list/preprint-recent-activity-list.component'; -export { PreprintSubmissionItemComponent } from './preprint-submission-item/preprint-submission-item.component'; -export { PreprintSubmissionsComponent } from './preprint-submissions/preprint-submissions.component'; -export { PreprintWithdrawalSubmissionsComponent } from './preprint-withdrawal-submissions/preprint-withdrawal-submissions.component'; -export { RegistryPendingSubmissionsComponent } from './registry-pending-submissions/registry-pending-submissions.component'; -export { RegistrySettingsComponent } from './registry-settings/registry-settings.component'; -export { RegistrySubmissionItemComponent } from './registry-submission-item/registry-submission-item.component'; -export { RegistrySubmissionsComponent } from './registry-submissions/registry-submissions.component'; -export { CollectionSubmissionItemComponent } from '@osf/features/moderation/components/collection-submission-item/collection-submission-item.component'; -export { CollectionSubmissionsListComponent } from '@osf/features/moderation/components/collection-submissions-list/collection-submissions-list.component'; diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts index 6af4d0c30..5d86b7937 100644 --- a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.spec.ts @@ -18,7 +18,8 @@ import { PreprintModerationSelectors, } from '../../store/preprint-moderation'; import { PreprintSubmissionItemComponent } from '../preprint-submission-item/preprint-submission-item.component'; -import { PreprintSubmissionsComponent } from '..'; + +import { PreprintSubmissionsComponent } from './preprint-submissions.component'; import { MOCK_PREPRINT_SUBMISSIONS } from '@testing/mocks/preprint-submission.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; diff --git a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts index af97cb132..b62f78a23 100644 --- a/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts +++ b/src/app/features/moderation/components/preprint-submissions/preprint-submissions.component.ts @@ -13,7 +13,6 @@ import { toSignal } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; -import { PreprintSubmissionItemComponent } from '@osf/features/moderation/components'; import { PREPRINT_SORT_OPTIONS, SUBMISSION_REVIEW_OPTIONS } from '@osf/features/moderation/constants'; import { PreprintSubmissionsSort, SubmissionReviewStatus } from '@osf/features/moderation/enums'; import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; @@ -29,6 +28,7 @@ import { LoadMorePreprintSubmissionContributors, PreprintModerationSelectors, } from '../../store/preprint-moderation'; +import { PreprintSubmissionItemComponent } from '../preprint-submission-item/preprint-submission-item.component'; @Component({ selector: 'osf-preprint-submissions', diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts index ecb4a78f3..7570ebec6 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.spec.ts @@ -3,7 +3,6 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { RegistrySubmissionItemComponent } from '@osf/features/moderation/components'; import { RegistryModeration } from '@osf/features/moderation/models'; import { CustomPaginatorComponent } from '@osf/shared/components/custom-paginator/custom-paginator.component'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; @@ -12,6 +11,7 @@ import { SelectComponent } from '@osf/shared/components/select/select.component' import { RegistrySort, SubmissionReviewStatus } from '../../enums'; import { RegistryModerationSelectors } from '../../store/registry-moderation'; +import { RegistrySubmissionItemComponent } from '../registry-submission-item/registry-submission-item.component'; import { RegistrySubmissionsComponent } from './registry-submissions.component'; diff --git a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts index 3271d565f..64ed44bc7 100644 --- a/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts +++ b/src/app/features/moderation/components/registry-submissions/registry-submissions.component.ts @@ -31,7 +31,7 @@ import { LoadMoreRegistrySubmissionContributors, RegistryModerationSelectors, } from '../../store/registry-moderation'; -import { RegistrySubmissionItemComponent } from '..'; +import { RegistrySubmissionItemComponent } from '../registry-submission-item/registry-submission-item.component'; @Component({ selector: 'osf-registry-submissions', diff --git a/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts b/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts index cbde21198..c30d551b6 100644 --- a/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts +++ b/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.spec.ts @@ -2,12 +2,10 @@ import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { - MyReviewingNavigationComponent, - PreprintRecentActivityListComponent, -} from '@osf/features/moderation/components'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; +import { MyReviewingNavigationComponent } from '../../components/my-reviewing-navigation/my-reviewing-navigation.component'; +import { PreprintRecentActivityListComponent } from '../../components/preprint-recent-activity-list/preprint-recent-activity-list.component'; import { PreprintModerationSelectors } from '../../store/preprint-moderation'; import { MyPreprintReviewingComponent } from './my-preprint-reviewing.component'; diff --git a/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.ts b/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.ts index a80e3772d..9bf4a9a54 100644 --- a/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.ts +++ b/src/app/features/moderation/pages/my-preprint-reviewing/my-preprint-reviewing.component.ts @@ -9,7 +9,8 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; -import { MyReviewingNavigationComponent, PreprintRecentActivityListComponent } from '../../components'; +import { MyReviewingNavigationComponent } from '../../components/my-reviewing-navigation/my-reviewing-navigation.component'; +import { PreprintRecentActivityListComponent } from '../../components/preprint-recent-activity-list/preprint-recent-activity-list.component'; import { GetPreprintProviders, GetPreprintReviewActions, diff --git a/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts b/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts index 8119d7b99..3de69e900 100644 --- a/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts +++ b/src/app/features/preprints/components/advisory-board/advisory-board.component.spec.ts @@ -2,6 +2,8 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AdvisoryBoardComponent } from './advisory-board.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('AdvisoryBoardComponent', () => { let component: AdvisoryBoardComponent; let fixture: ComponentFixture; @@ -12,6 +14,7 @@ describe('AdvisoryBoardComponent', () => { beforeEach(() => { TestBed.configureTestingModule({ imports: [AdvisoryBoardComponent], + providers: [provideOSFCore()], }); fixture = TestBed.createComponent(AdvisoryBoardComponent); diff --git a/src/app/features/preprints/components/index.ts b/src/app/features/preprints/components/index.ts deleted file mode 100644 index 17ba1617f..000000000 --- a/src/app/features/preprints/components/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -export { AdvisoryBoardComponent } from './advisory-board/advisory-board.component'; -export { BrowseBySubjectsComponent } from './browse-by-subjects/browse-by-subjects.component'; -export { AdditionalInfoComponent } from './preprint-details/additional-info/additional-info.component'; -export { GeneralInformationComponent } from './preprint-details/general-information/general-information.component'; -export { ModerationStatusBannerComponent } from './preprint-details/moderation-status-banner/moderation-status-banner.component'; -export { PreprintFileSectionComponent } from './preprint-details/preprint-file-section/preprint-file-section.component'; -export { PreprintMakeDecisionComponent } from './preprint-details/preprint-make-decision/preprint-make-decision.component'; -export { PreprintMetricsInfoComponent } from './preprint-details/preprint-metrics-info/preprint-metrics-info.component'; -export { PreprintTombstoneComponent } from './preprint-details/preprint-tombstone/preprint-tombstone.component'; -export { PreprintWarningBannerComponent } from './preprint-details/preprint-warning-banner/preprint-warning-banner.component'; -export { PreprintWithdrawDialogComponent } from './preprint-details/preprint-withdraw-dialog/preprint-withdraw-dialog.component'; -export { ShareAndDownloadComponent } from './preprint-details/share-and-download/share-and-download.component'; -export { StatusBannerComponent } from './preprint-details/status-banner/status-banner.component'; -export { PreprintProviderFooterComponent } from './preprint-provider-footer/preprint-provider-footer.component'; -export { PreprintProviderHeroComponent } from './preprint-provider-hero/preprint-provider-hero.component'; -export { PreprintServicesComponent } from './preprint-services/preprint-services.component'; -export { PreprintsHelpDialogComponent } from './preprints-help-dialog/preprints-help-dialog.component'; -export { AuthorAssertionsStepComponent } from './stepper/author-assertion-step/author-assertions-step.component'; -export { FileStepComponent } from './stepper/file-step/file-step.component'; -export { PreprintsMetadataStepComponent } from './stepper/preprints-metadata-step/preprints-metadata-step.component'; -export { ReviewStepComponent } from './stepper/review-step/review-step.component'; -export { SupplementsStepComponent } from './stepper/supplements-step/supplements-step.component'; -export { TitleAndAbstractStepComponent } from './stepper/title-and-abstract-step/title-and-abstract-step.component'; diff --git a/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts b/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts index 14c6b329f..ee7326662 100644 --- a/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts +++ b/src/app/features/preprints/components/preprint-provider-footer/preprint-provider-footer.component.spec.ts @@ -2,12 +2,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { PreprintProviderFooterComponent } from './preprint-provider-footer.component'; +import { provideOSFCore } from '@testing/osf.testing.provider'; + describe('PreprintProviderFooterComponent', () => { let fixture: ComponentFixture; beforeEach(() => { TestBed.configureTestingModule({ imports: [PreprintProviderFooterComponent], + providers: [provideOSFCore()], }); fixture = TestBed.createComponent(PreprintProviderFooterComponent); diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts index ebe658558..ac1c18563 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.spec.ts @@ -108,6 +108,7 @@ describe('PreprintProviderHeroComponent', () => { expect(customDialogMock.open).toHaveBeenCalledWith(PreprintsHelpDialogComponent, { header: 'preprints.helpDialog.header', + width: '560px', }); }); }); diff --git a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts index 2fa2bc2aa..475c7380e 100644 --- a/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts +++ b/src/app/features/preprints/components/preprint-provider-hero/preprint-provider-hero.component.ts @@ -36,6 +36,9 @@ export class PreprintProviderHeroComponent { } openHelpDialog(): void { - this.customDialogService.open(PreprintsHelpDialogComponent, { header: 'preprints.helpDialog.header' }); + this.customDialogService.open(PreprintsHelpDialogComponent, { + header: 'preprints.helpDialog.header', + width: '560px', + }); } } diff --git a/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.ts b/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.ts index 55826b4cc..aeb422f2f 100644 --- a/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.ts +++ b/src/app/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component.ts @@ -3,7 +3,7 @@ import { TranslatePipe } from '@ngx-translate/core'; import { ChangeDetectionStrategy, Component } from '@angular/core'; @Component({ - selector: 'osf-collections-help-dialog', + selector: 'osf-preprints-help-dialog', imports: [TranslatePipe], templateUrl: './preprints-help-dialog.component.html', styleUrl: './preprints-help-dialog.component.scss', diff --git a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts index 7f4123181..3b617414f 100644 --- a/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts +++ b/src/app/features/preprints/components/stepper/title-and-abstract-step/title-and-abstract-step.component.spec.ts @@ -5,11 +5,12 @@ import { Textarea } from 'primeng/textarea'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { provideRouter } from '@angular/router'; -import { TitleAndAbstractStepComponent } from '@osf/features/preprints/components'; import { PreprintStepperSelectors } from '@osf/features/preprints/store/preprint-stepper'; import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; import { ToastService } from '@osf/shared/services/toast.service'; +import { TitleAndAbstractStepComponent } from './title-and-abstract-step.component'; + import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { provideMockStore } from '@testing/providers/store-provider.mock'; diff --git a/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts b/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts index eaeaa8b2d..5b173d43b 100644 --- a/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts +++ b/src/app/features/preprints/pages/create-new-version/create-new-version.component.spec.ts @@ -13,7 +13,8 @@ import { BrandService } from '@osf/shared/services/brand.service'; import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; -import { FileStepComponent, ReviewStepComponent } from '../../components'; +import { FileStepComponent } from '../../components/stepper/file-step/file-step.component'; +import { ReviewStepComponent } from '../../components/stepper/review-step/review-step.component'; import { createNewVersionStepsConst } from '../../constants'; import { PreprintSteps } from '../../enums'; import { PreprintProviderDetails } from '../../models'; diff --git a/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts b/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts index 70176a373..af3a4aeac 100644 --- a/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts +++ b/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts @@ -27,7 +27,8 @@ import { BrandService } from '@osf/shared/services/brand.service'; import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; -import { FileStepComponent, ReviewStepComponent } from '../../components'; +import { FileStepComponent } from '../../components/stepper/file-step/file-step.component'; +import { ReviewStepComponent } from '../../components/stepper/review-step/review-step.component'; import { createNewVersionStepsConst } from '../../constants'; import { PreprintSteps } from '../../enums'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts index 6f1496379..662d24659 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts @@ -20,18 +20,16 @@ import { MetaTagsBuilderService } from '@osf/shared/services/meta-tags-builder.s import { ToastService } from '@osf/shared/services/toast.service'; import { ContributorsSelectors } from '@osf/shared/stores/contributors'; -import { - AdditionalInfoComponent, - GeneralInformationComponent, - ModerationStatusBannerComponent, - PreprintFileSectionComponent, - PreprintMakeDecisionComponent, - PreprintMetricsInfoComponent, - PreprintTombstoneComponent, - PreprintWarningBannerComponent, - ShareAndDownloadComponent, - StatusBannerComponent, -} from '../../components'; +import { AdditionalInfoComponent } from '../../components/preprint-details/additional-info/additional-info.component'; +import { GeneralInformationComponent } from '../../components/preprint-details/general-information/general-information.component'; +import { ModerationStatusBannerComponent } from '../../components/preprint-details/moderation-status-banner/moderation-status-banner.component'; +import { PreprintFileSectionComponent } from '../../components/preprint-details/preprint-file-section/preprint-file-section.component'; +import { PreprintMakeDecisionComponent } from '../../components/preprint-details/preprint-make-decision/preprint-make-decision.component'; +import { PreprintMetricsInfoComponent } from '../../components/preprint-details/preprint-metrics-info/preprint-metrics-info.component'; +import { PreprintTombstoneComponent } from '../../components/preprint-details/preprint-tombstone/preprint-tombstone.component'; +import { PreprintWarningBannerComponent } from '../../components/preprint-details/preprint-warning-banner/preprint-warning-banner.component'; +import { ShareAndDownloadComponent } from '../../components/preprint-details/share-and-download/share-and-download.component'; +import { StatusBannerComponent } from '../../components/preprint-details/status-banner/status-banner.component'; import { ReviewsState } from '../../enums'; import { FetchPreprintDetails, diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts index 279e56a90..a2f82df3f 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts @@ -38,19 +38,17 @@ import { SignpostingService } from '@osf/shared/services/signposting.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { ContributorsSelectors } from '@osf/shared/stores/contributors'; -import { - AdditionalInfoComponent, - GeneralInformationComponent, - ModerationStatusBannerComponent, - PreprintFileSectionComponent, - PreprintMakeDecisionComponent, - PreprintMetricsInfoComponent, - PreprintTombstoneComponent, - PreprintWarningBannerComponent, - PreprintWithdrawDialogComponent, - ShareAndDownloadComponent, - StatusBannerComponent, -} from '../../components'; +import { AdditionalInfoComponent } from '../../components/preprint-details/additional-info/additional-info.component'; +import { GeneralInformationComponent } from '../../components/preprint-details/general-information/general-information.component'; +import { ModerationStatusBannerComponent } from '../../components/preprint-details/moderation-status-banner/moderation-status-banner.component'; +import { PreprintFileSectionComponent } from '../../components/preprint-details/preprint-file-section/preprint-file-section.component'; +import { PreprintMakeDecisionComponent } from '../../components/preprint-details/preprint-make-decision/preprint-make-decision.component'; +import { PreprintMetricsInfoComponent } from '../../components/preprint-details/preprint-metrics-info/preprint-metrics-info.component'; +import { PreprintTombstoneComponent } from '../../components/preprint-details/preprint-tombstone/preprint-tombstone.component'; +import { PreprintWarningBannerComponent } from '../../components/preprint-details/preprint-warning-banner/preprint-warning-banner.component'; +import { PreprintWithdrawDialogComponent } from '../../components/preprint-details/preprint-withdraw-dialog/preprint-withdraw-dialog.component'; +import { ShareAndDownloadComponent } from '../../components/preprint-details/share-and-download/share-and-download.component'; +import { StatusBannerComponent } from '../../components/preprint-details/status-banner/status-banner.component'; import { PreprintRequestMachineState, ProviderReviewsWorkflow, ReviewsState } from '../../enums'; import { FetchPreprintDetails, diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts index aba5aa33b..a94a5e159 100644 --- a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts @@ -12,7 +12,7 @@ import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { SetDefaultFilterValue, SetResourceType } from '@osf/shared/stores/global-search'; -import { PreprintProviderHeroComponent } from '../../components'; +import { PreprintProviderHeroComponent } from '../../components/preprint-provider-hero/preprint-provider-hero.component'; import { PreprintProviderDetails } from '../../models'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts index ff52d233e..62a14a939 100644 --- a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts @@ -11,7 +11,7 @@ import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { SetDefaultFilterValue, SetResourceType } from '@osf/shared/stores/global-search'; -import { PreprintProviderHeroComponent } from '../../components'; +import { PreprintProviderHeroComponent } from '../../components/preprint-provider-hero/preprint-provider-hero.component'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; @Component({ diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts index f524e9436..551c7e57f 100644 --- a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts @@ -9,12 +9,10 @@ import { BrandService } from '@osf/shared/services/brand.service'; import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; -import { - AdvisoryBoardComponent, - BrowseBySubjectsComponent, - PreprintProviderFooterComponent, - PreprintProviderHeroComponent, -} from '../../components'; +import { AdvisoryBoardComponent } from '../../components/advisory-board/advisory-board.component'; +import { BrowseBySubjectsComponent } from '../../components/browse-by-subjects/browse-by-subjects.component'; +import { PreprintProviderFooterComponent } from '../../components/preprint-provider-footer/preprint-provider-footer.component'; +import { PreprintProviderHeroComponent } from '../../components/preprint-provider-hero/preprint-provider-hero.component'; import { PreprintProviderDetails } from '../../models'; import { GetHighlightedSubjectsByProviderId, diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts index 5eb4560c2..1395b3dfc 100644 --- a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts @@ -10,12 +10,10 @@ import { BrandService } from '@osf/shared/services/brand.service'; import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; -import { - AdvisoryBoardComponent, - BrowseBySubjectsComponent, - PreprintProviderFooterComponent, - PreprintProviderHeroComponent, -} from '../../components'; +import { AdvisoryBoardComponent } from '../../components/advisory-board/advisory-board.component'; +import { BrowseBySubjectsComponent } from '../../components/browse-by-subjects/browse-by-subjects.component'; +import { PreprintProviderFooterComponent } from '../../components/preprint-provider-footer/preprint-provider-footer.component'; +import { PreprintProviderHeroComponent } from '../../components/preprint-provider-hero/preprint-provider-hero.component'; import { GetHighlightedSubjectsByProviderId, GetPreprintProviderById, diff --git a/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts b/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts index cfd20dc03..5784af23b 100644 --- a/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts +++ b/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts @@ -10,7 +10,9 @@ import { SearchInputComponent } from '@osf/shared/components/search-input/search import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { BrandService } from '@osf/shared/services/brand.service'; -import { AdvisoryBoardComponent, BrowseBySubjectsComponent, PreprintServicesComponent } from '../../components'; +import { AdvisoryBoardComponent } from '../../components/advisory-board/advisory-board.component'; +import { BrowseBySubjectsComponent } from '../../components/browse-by-subjects/browse-by-subjects.component'; +import { PreprintServicesComponent } from '../../components/preprint-services/preprint-services.component'; import { PreprintProviderDetails } from '../../models'; import { GetHighlightedSubjectsByProviderId, diff --git a/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.ts b/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.ts index 0f66c9315..8de2db2c1 100644 --- a/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.ts +++ b/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.ts @@ -16,7 +16,9 @@ import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { normalizeQuotes } from '@osf/shared/helpers/normalize-quotes'; import { BrandService } from '@osf/shared/services/brand.service'; -import { AdvisoryBoardComponent, BrowseBySubjectsComponent, PreprintServicesComponent } from '../../components'; +import { AdvisoryBoardComponent } from '../../components/advisory-board/advisory-board.component'; +import { BrowseBySubjectsComponent } from '../../components/browse-by-subjects/browse-by-subjects.component'; +import { PreprintServicesComponent } from '../../components/preprint-services/preprint-services.component'; import { GetHighlightedSubjectsByProviderId, GetPreprintProviderById, diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts index 9803536df..1c040d046 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts @@ -13,14 +13,12 @@ import { BrandService } from '@osf/shared/services/brand.service'; import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; -import { - AuthorAssertionsStepComponent, - FileStepComponent, - PreprintsMetadataStepComponent, - ReviewStepComponent, - SupplementsStepComponent, - TitleAndAbstractStepComponent, -} from '../../components'; +import { AuthorAssertionsStepComponent } from '../../components/stepper/author-assertion-step/author-assertions-step.component'; +import { FileStepComponent } from '../../components/stepper/file-step/file-step.component'; +import { PreprintsMetadataStepComponent } from '../../components/stepper/preprints-metadata-step/preprints-metadata-step.component'; +import { ReviewStepComponent } from '../../components/stepper/review-step/review-step.component'; +import { SupplementsStepComponent } from '../../components/stepper/supplements-step/supplements-step.component'; +import { TitleAndAbstractStepComponent } from '../../components/stepper/title-and-abstract-step/title-and-abstract-step.component'; import { submitPreprintSteps } from '../../constants'; import { PreprintSteps } from '../../enums'; import { PreprintProviderDetails } from '../../models'; diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts index d5f06bd12..6d91a28cb 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts @@ -29,14 +29,12 @@ import { BrandService } from '@osf/shared/services/brand.service'; import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; -import { - AuthorAssertionsStepComponent, - FileStepComponent, - PreprintsMetadataStepComponent, - ReviewStepComponent, - SupplementsStepComponent, - TitleAndAbstractStepComponent, -} from '../../components'; +import { AuthorAssertionsStepComponent } from '../../components/stepper/author-assertion-step/author-assertions-step.component'; +import { FileStepComponent } from '../../components/stepper/file-step/file-step.component'; +import { PreprintsMetadataStepComponent } from '../../components/stepper/preprints-metadata-step/preprints-metadata-step.component'; +import { ReviewStepComponent } from '../../components/stepper/review-step/review-step.component'; +import { SupplementsStepComponent } from '../../components/stepper/supplements-step/supplements-step.component'; +import { TitleAndAbstractStepComponent } from '../../components/stepper/title-and-abstract-step/title-and-abstract-step.component'; import { submitPreprintSteps } from '../../constants'; import { PreprintSteps } from '../../enums'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts index 95c980ceb..8c90328fd 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts @@ -13,14 +13,12 @@ import { BrandService } from '@osf/shared/services/brand.service'; import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; import { HeaderStyleService } from '@osf/shared/services/header-style.service'; -import { - AuthorAssertionsStepComponent, - FileStepComponent, - PreprintsMetadataStepComponent, - ReviewStepComponent, - SupplementsStepComponent, - TitleAndAbstractStepComponent, -} from '../../components'; +import { AuthorAssertionsStepComponent } from '../../components/stepper/author-assertion-step/author-assertions-step.component'; +import { FileStepComponent } from '../../components/stepper/file-step/file-step.component'; +import { PreprintsMetadataStepComponent } from '../../components/stepper/preprints-metadata-step/preprints-metadata-step.component'; +import { ReviewStepComponent } from '../../components/stepper/review-step/review-step.component'; +import { SupplementsStepComponent } from '../../components/stepper/supplements-step/supplements-step.component'; +import { TitleAndAbstractStepComponent } from '../../components/stepper/title-and-abstract-step/title-and-abstract-step.component'; import { submitPreprintSteps } from '../../constants'; import { PreprintSteps, ReviewsState } from '../../enums'; import { PreprintProviderDetails } from '../../models'; diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts index eacb8c11e..86c13a68f 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts @@ -28,14 +28,12 @@ import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { CanDeactivateComponent } from '@shared/models/can-deactivate.interface'; import { StepOption } from '@shared/models/step-option.model'; -import { - AuthorAssertionsStepComponent, - FileStepComponent, - PreprintsMetadataStepComponent, - ReviewStepComponent, - SupplementsStepComponent, - TitleAndAbstractStepComponent, -} from '../../components'; +import { AuthorAssertionsStepComponent } from '../../components/stepper/author-assertion-step/author-assertions-step.component'; +import { FileStepComponent } from '../../components/stepper/file-step/file-step.component'; +import { PreprintsMetadataStepComponent } from '../../components/stepper/preprints-metadata-step/preprints-metadata-step.component'; +import { ReviewStepComponent } from '../../components/stepper/review-step/review-step.component'; +import { SupplementsStepComponent } from '../../components/stepper/supplements-step/supplements-step.component'; +import { TitleAndAbstractStepComponent } from '../../components/stepper/title-and-abstract-step/title-and-abstract-step.component'; import { submitPreprintSteps } from '../../constants'; import { PreprintSteps, ProviderReviewsWorkflow, ReviewsState } from '../../enums'; import { GetPreprintProviderById, PreprintProvidersSelectors } from '../../store/preprint-providers'; diff --git a/src/app/features/preprints/preprints.component.spec.ts b/src/app/features/preprints/preprints.component.spec.ts index 7ae801441..5f2848b2b 100644 --- a/src/app/features/preprints/preprints.component.spec.ts +++ b/src/app/features/preprints/preprints.component.spec.ts @@ -9,7 +9,7 @@ import { PreprintsComponent } from './preprints.component'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { HelpScoutServiceMockFactory } from '@testing/providers/help-scout.service.mock'; -describe('Component: Preprint', () => { +describe('PreprintsComponent', () => { let fixture: ComponentFixture; let helpScoutService: HelpScoutService; diff --git a/src/app/features/preprints/services/index.ts b/src/app/features/preprints/services/index.ts deleted file mode 100644 index 33746a055..000000000 --- a/src/app/features/preprints/services/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { PreprintFilesService } from './preprint-files.service'; -export { PreprintLicensesService } from './preprint-licenses.service'; -export { PreprintProvidersService } from './preprint-providers.service'; -export { PreprintsService } from './preprints.service'; -export { PreprintsProjectsService } from './preprints-projects.service'; diff --git a/src/app/features/preprints/store/my-preprints/my-preprints.state.ts b/src/app/features/preprints/store/my-preprints/my-preprints.state.ts index c65af78ce..2643dd4b6 100644 --- a/src/app/features/preprints/store/my-preprints/my-preprints.state.ts +++ b/src/app/features/preprints/store/my-preprints/my-preprints.state.ts @@ -8,7 +8,7 @@ import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; -import { PreprintsService } from '../../services'; +import { PreprintsService } from '../../services/preprints.service'; import { FetchMyPreprints } from './my-preprints.actions'; import { DEFAULT_MY_PREPRINTS_STATE, MyPreprintsStateModel } from './my-preprints.model'; diff --git a/src/app/features/preprints/store/preprint-providers/preprint-providers.state.ts b/src/app/features/preprints/store/preprint-providers/preprint-providers.state.ts index e70522ff2..6d9125da5 100644 --- a/src/app/features/preprints/store/preprint-providers/preprint-providers.state.ts +++ b/src/app/features/preprints/store/preprint-providers/preprint-providers.state.ts @@ -7,10 +7,11 @@ import { catchError } from 'rxjs/operators'; import { inject, Injectable } from '@angular/core'; import { SetCurrentProvider } from '@core/store/provider'; -import { PreprintProvidersService } from '@osf/features/preprints/services'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; +import { PreprintProvidersService } from '../../services/preprint-providers.service'; + import { GetHighlightedSubjectsByProviderId, GetPreprintProviderById, diff --git a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts index cc817558d..4322e296b 100644 --- a/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts +++ b/src/app/features/preprints/store/preprint-stepper/preprint-stepper.state.ts @@ -8,17 +8,16 @@ import { HttpEventType } from '@angular/common/http'; import { inject, Injectable } from '@angular/core'; import { PreprintModel } from '@osf/features/preprints/models'; -import { - PreprintFilesService, - PreprintLicensesService, - PreprintsProjectsService, - PreprintsService, -} from '@osf/features/preprints/services'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; import { FileModel } from '@osf/shared/models/files/file.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; import { FilesService } from '@osf/shared/services/files.service'; +import { PreprintFilesService } from '../../services/preprint-files.service'; +import { PreprintLicensesService } from '../../services/preprint-licenses.service'; +import { PreprintsService } from '../../services/preprints.service'; +import { PreprintsProjectsService } from '../../services/preprints-projects.service'; + import { ConnectProject, CopyFileFromProject, diff --git a/src/app/features/preprints/store/preprint/preprint.state.ts b/src/app/features/preprints/store/preprint/preprint.state.ts index 966b95b02..d8d279d6a 100644 --- a/src/app/features/preprints/store/preprint/preprint.state.ts +++ b/src/app/features/preprints/store/preprint/preprint.state.ts @@ -9,7 +9,7 @@ import { inject, Injectable } from '@angular/core'; import { handleSectionError } from '@osf/shared/helpers/state-error.handler'; import { FilesService } from '@osf/shared/services/files.service'; -import { PreprintsService } from '../../services'; +import { PreprintsService } from '../../services/preprints.service'; import { FetchPreprintDetails, diff --git a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts index bc9d6e446..c7cb4586e 100644 --- a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts +++ b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.spec.ts @@ -34,6 +34,7 @@ describe('RegistryProviderHeroComponent', () => { brand: null, iri: '', reviewsWorkflow: '', + allowSubmissions: false, }; beforeEach(() => { diff --git a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts index 186b57eed..d5c09d830 100644 --- a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts +++ b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts @@ -8,7 +8,7 @@ import { ChangeDetectionStrategy, Component, effect, inject, input, OnDestroy, o import { FormControl } from '@angular/forms'; import { Router } from '@angular/router'; -import { PreprintsHelpDialogComponent } from '@osf/features/preprints/components'; +import { PreprintsHelpDialogComponent } from '@osf/features/preprints/components/preprints-help-dialog/preprints-help-dialog.component'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { BrandService } from '@osf/shared/services/brand.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; diff --git a/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts b/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts index ec626b26c..dee04ca5e 100644 --- a/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts +++ b/src/app/shared/components/metadata-tabs/metadata-tabs.component.spec.ts @@ -2,7 +2,7 @@ import { MockComponents } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { CedarTemplateFormComponent } from '@osf/features/metadata/components'; +import { CedarTemplateFormComponent } from '@osf/features/metadata/components/cedar-template-form/cedar-template-form.component'; import { CedarMetadataDataTemplateJsonApi, CedarRecordDataBinding } from '@osf/features/metadata/models'; import { MetadataResourceEnum } from '@osf/shared/enums/metadata-resource.enum'; import { MetadataTabsModel } from '@shared/models/metadata-tabs.model'; diff --git a/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts b/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts index f93a7bd32..4325b30b4 100644 --- a/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts +++ b/src/app/shared/components/metadata-tabs/metadata-tabs.component.ts @@ -4,7 +4,7 @@ import { TabsModule } from 'primeng/tabs'; import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { CedarTemplateFormComponent } from '@osf/features/metadata/components'; +import { CedarTemplateFormComponent } from '@osf/features/metadata/components/cedar-template-form/cedar-template-form.component'; import { CedarMetadataDataTemplateJsonApi, CedarMetadataRecordData, diff --git a/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts b/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts index e2029b09f..c7bc684be 100644 --- a/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts +++ b/src/app/shared/components/wiki/edit-section/edit-section.component.spec.ts @@ -12,6 +12,13 @@ import { EditSectionComponent } from './edit-section.component'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; +jest.mock('ace-builds/src-noconflict/ext-language_tools'); + +(globalThis as any).ace = { + define: jest.fn(), + require: jest.fn().mockReturnValue({ snippetCompleter: {} }), +}; + describe('EditSectionComponent', () => { let component: EditSectionComponent; let fixture: ComponentFixture; @@ -191,7 +198,7 @@ describe('EditSectionComponent', () => { expect((component as any).editorInstance).toBe(mockEditorInstance); expect(mockEditorInstance.setShowPrintMargin).toHaveBeenCalledWith(false); - expect((global as any).ace.require).toHaveBeenCalledWith('ace/ext/language_tools'); + expect((globalThis as any).ace.require).toHaveBeenCalledWith('ace/ext/language_tools'); expect(mockEditorInstance.setOptions).toHaveBeenCalledWith({ enableBasicAutocompletion: false, enableLiveAutocompletion: false, From d65d6040462c3968666501134f3910df17ff4ba7 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 24 Mar 2026 13:25:51 +0200 Subject: [PATCH 07/10] test(configs): updated tests configs --- jest.config.ts | 29 +- package-lock.json | 6456 ++++++++++++++++++++++++++------------------- package.json | 1 - setup-jest.ts | 44 +- 4 files changed, 3710 insertions(+), 2820 deletions(-) diff --git a/jest.config.ts b/jest.config.ts index 1a258b4ee..568c34a4d 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -1,11 +1,18 @@ -module.exports = { +import type { Config } from 'jest'; + +const config: Config = { preset: 'jest-preset-angular', setupFilesAfterEnv: ['/setup-jest.ts'], globalSetup: '/jest.global-setup.ts', - collectCoverage: false, + testEnvironment: 'jsdom', clearMocks: true, restoreMocks: true, - coverageReporters: ['json-summary', 'lcov', 'clover'], + collectCoverage: false, + coverageDirectory: 'coverage', + coverageReporters: ['json-summary', 'lcov', 'clover', 'text-summary'], + moduleFileExtensions: ['ts', 'js', 'html', 'json', 'mjs'], + extensionsToTreatAsEsm: ['.ts'], + testMatch: ['/src/**/*.spec.ts'], moduleNameMapper: { '^@osf/(.*)$': '/src/app/$1', '^@core/(.*)$': '/src/app/core/$1', @@ -27,11 +34,8 @@ module.exports = { ], }, transformIgnorePatterns: [ - 'node_modules/(?!.*\\.mjs$|@ngxs|@angular|@ngrx|parse5|entities|chart.js|@mdit|@citation-js|@traptitech|@sentry|@primeng|@newrelic)', + 'node_modules/(?!(@angular|@ngxs|@ngx-translate|angular-google-tag-manager|ngx-cookie-service|ngx-markdown-editor|angularx-qrcode|ngx-captcha|@sentry|@newrelic|@centerforopenscience|@mdit|@traptitech|@citation-js|primeng|@primeuix|markdown-it|markdown-it-anchor|markdown-it-toc-done-right|markdown-it-video|chart\\.js)/)', ], - testEnvironment: 'jsdom', - moduleFileExtensions: ['ts', 'js', 'html', 'json', 'mjs'], - coverageDirectory: 'coverage', collectCoverageFrom: [ 'src/app/**/*.{ts,js}', '!src/app/core/theme/**', @@ -40,10 +44,8 @@ module.exports = { '!src/app/**/*.routes.{ts,js}', '!src/app/**/*.route.{ts,js}', '!src/app/**/mappers/**', - '!src/app/shared/mappers/**', '!src/app/**/*.model.{ts,js}', '!src/app/**/models/*.{ts,js}', - '!src/app/shared/models/**', '!src/app/**/*.enum.{ts,js}', '!src/app/**/*.type.{ts,js}', '!src/app/**/*.spec.{ts,js}', @@ -51,13 +53,12 @@ module.exports = { '!src/app/**/index.ts', '!src/app/**/public-api.ts', ], - extensionsToTreatAsEsm: ['.ts'], coverageThreshold: { global: { branches: 43.3, - functions: 42.7, - lines: 69.3, - statements: 69.8, + functions: 43.8, + lines: 70.18, + statements: 70.6, }, }, watchPathIgnorePatterns: [ @@ -70,3 +71,5 @@ module.exports = { ], testPathIgnorePatterns: ['/src/environments'], }; + +export default config; diff --git a/package-lock.json b/package-lock.json index 6cf892e0a..3e8425c51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,6 @@ "tslib": "^2.3.0" }, "devDependencies": { - "@angular-builders/jest": "^21.0.3", "@angular-devkit/build-angular": "^21.2.1", "@angular-eslint/eslint-plugin": "^21.3.0", "@angular-eslint/eslint-plugin-template": "^21.3.0", @@ -91,6 +90,8 @@ }, "node_modules/@aduh95/viz.js": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@aduh95/viz.js/-/viz.js-3.4.0.tgz", + "integrity": "sha512-KI2nVf9JdwWCXqK6RVf+9/096G7VWN4Z84mnynlyZKao2xQENW8WNEjLmvdlxS5X8PNWXFC1zqwm7tveOXw/4A==", "dev": true, "license": "MIT" }, @@ -305,6 +306,8 @@ }, "node_modules/@ampproject/remapping": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -315,70 +318,6 @@ "node": ">=6.0.0" } }, - "node_modules/@angular-builders/common": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-5.0.3.tgz", - "integrity": "sha512-Dro3574mu4/xqmjdA3159+TXDhgTbIJpEY/iBETSKUvHJiCgHel+R3eT105RpHN5o7NaD2rau5Zk2wuZqOk35Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-devkit/core": "^21.0.0", - "ts-node": "^10.0.0", - "tsconfig-paths": "^4.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@angular-builders/common/node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@angular-builders/common/node_modules/tsconfig-paths": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", - "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "json5": "^2.2.2", - "minimist": "^1.2.6", - "strip-bom": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@angular-builders/jest": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@angular-builders/jest/-/jest-21.0.3.tgz", - "integrity": "sha512-RYIsJQJkke4Dns+lYBYzn0JcmABCQKvTWkqMibi5v8dgtNS8pgPS8pE5x8DSmgraqJikL3ukqaUQmdeL6r38aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@angular-builders/common": "5.0.3", - "@angular-devkit/architect": ">=0.2100.0 < 0.2200.0", - "@angular-devkit/core": "^21.0.0", - "jest-preset-angular": "^16.0.0", - "lodash": "^4.17.15" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular-devkit/build-angular": "^21.0.0", - "@angular/compiler-cli": "^21.0.0", - "@angular/core": "^21.0.0", - "@angular/platform-browser-dynamic": "^21.0.0", - "jest": "^30.0.0" - } - }, "node_modules/@angular-devkit/architect": { "version": "0.2102.3", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.3.tgz", @@ -579,33 +518,6 @@ } } }, - "node_modules/@angular-devkit/core/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@angular-devkit/core/node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, "node_modules/@angular-devkit/schematics": { "version": "21.2.3", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.3.tgz", @@ -625,16 +537,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/@angular-eslint/builder": { "version": "21.3.1", "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.1.tgz", @@ -859,16 +761,6 @@ } } }, - "node_modules/@angular/build/node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, "node_modules/@angular/cdk": { "version": "21.2.3", "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.2.3.tgz", @@ -885,30 +777,6 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, - "node_modules/@angular/cdk/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/@angular/cdk/node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/@angular/cli": { "version": "21.2.3", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.3.tgz", @@ -944,105 +812,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular/cli/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@angular/cli/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular/cli/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/cli/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@angular/cli/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@angular/cli/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, "node_modules/@angular/common": { "version": "21.2.5", "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.5.tgz", @@ -1104,138 +873,39 @@ } } }, - "node_modules/@angular/compiler-cli/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, + "node_modules/@angular/core": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.5.tgz", + "integrity": "sha512-JgHU134Adb1wrpyGC9ozcv3hiRAgaFTvJFn1u9OU/AVXyxu4meMmVh2hp5QhAvPnv8XQdKWWIkAY+dbpPE6zKA==", "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@angular/compiler-cli/node_modules/cliui": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", - "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", - "dev": true, - "license": "ISC", "dependencies": { - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "tslib": "^2.3.0" }, "engines": { - "node": ">=20" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@angular/compiler": "21.2.5", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0 || ~0.16.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } } }, - "node_modules/@angular/compiler-cli/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@angular/compiler-cli/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, + "node_modules/@angular/forms": { + "version": "21.2.5", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.5.tgz", + "integrity": "sha512-pqRuK+a1ZAFZbs8/dZoorFJah2IWaf/SH8axHUpaDJ7fyNrwNEcpczyObdxZ00lOgORpKAhWo/q0hlVS+In8cw==", "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@angular/compiler-cli/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/@angular/compiler-cli/node_modules/yargs": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", - "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^9.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "string-width": "^7.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^22.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@angular/compiler-cli/node_modules/yargs-parser": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", - "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=23" - } - }, - "node_modules/@angular/core": { - "version": "21.2.5", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.5.tgz", - "integrity": "sha512-JgHU134Adb1wrpyGC9ozcv3hiRAgaFTvJFn1u9OU/AVXyxu4meMmVh2hp5QhAvPnv8XQdKWWIkAY+dbpPE6zKA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@angular/compiler": "21.2.5", - "rxjs": "^6.5.3 || ^7.4.0", - "zone.js": "~0.15.0 || ~0.16.0" - }, - "peerDependenciesMeta": { - "@angular/compiler": { - "optional": true - }, - "zone.js": { - "optional": true - } - } - }, - "node_modules/@angular/forms": { - "version": "21.2.5", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.5.tgz", - "integrity": "sha512-pqRuK+a1ZAFZbs8/dZoorFJah2IWaf/SH8axHUpaDJ7fyNrwNEcpczyObdxZ00lOgORpKAhWo/q0hlVS+In8cw==", - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.0.0", - "tslib": "^2.3.0" + "@standard-schema/spec": "^1.0.0", + "tslib": "^2.3.0" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" @@ -1347,6 +1017,8 @@ }, "node_modules/@arr/every": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", + "integrity": "sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==", "dev": true, "license": "MIT", "engines": { @@ -1354,25 +1026,70 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", - "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@csstools/css-calc": "^2.1.3", - "@csstools/css-color-parser": "^3.0.9", - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3", - "lru-cache": "^10.4.3" + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT", + "peer": true }, "node_modules/@babel/code-frame": { "version": "7.29.0", @@ -1432,11 +1149,15 @@ }, "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -1492,6 +1213,8 @@ }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -1522,6 +1245,8 @@ }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -1530,6 +1255,8 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "dev": true, "license": "MIT", "dependencies": { @@ -1546,6 +1273,8 @@ }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -1571,6 +1300,8 @@ }, "node_modules/@babel/helper-globals": { "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", "engines": { @@ -1579,6 +1310,8 @@ }, "node_modules/@babel/helper-member-expression-to-functions": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "dependencies": { @@ -1623,6 +1356,8 @@ }, "node_modules/@babel/helper-optimise-call-expression": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "license": "MIT", "dependencies": { @@ -1644,6 +1379,8 @@ }, "node_modules/@babel/helper-remap-async-to-generator": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, "license": "MIT", "dependencies": { @@ -1678,6 +1415,8 @@ }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", "dependencies": { @@ -1703,6 +1442,8 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -1711,6 +1452,8 @@ }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -1719,6 +1462,8 @@ }, "node_modules/@babel/helper-validator-option": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", "engines": { @@ -1726,13 +1471,15 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1770,6 +1517,8 @@ }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1785,6 +1534,8 @@ }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, "license": "MIT", "dependencies": { @@ -1799,6 +1550,8 @@ }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, "license": "MIT", "dependencies": { @@ -1813,6 +1566,8 @@ }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, "license": "MIT", "dependencies": { @@ -1846,6 +1601,8 @@ }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, "license": "MIT", "engines": { @@ -2112,6 +1869,8 @@ }, "node_modules/@babel/plugin-syntax-unicode-sets-regex": { "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, "license": "MIT", "dependencies": { @@ -2127,6 +1886,8 @@ }, "node_modules/@babel/plugin-transform-arrow-functions": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, "license": "MIT", "dependencies": { @@ -2177,6 +1938,8 @@ }, "node_modules/@babel/plugin-transform-block-scoped-functions": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "license": "MIT", "dependencies": { @@ -2279,6 +2042,8 @@ }, "node_modules/@babel/plugin-transform-destructuring": { "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "dev": true, "license": "MIT", "dependencies": { @@ -2311,6 +2076,8 @@ }, "node_modules/@babel/plugin-transform-duplicate-keys": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, "license": "MIT", "dependencies": { @@ -2342,6 +2109,8 @@ }, "node_modules/@babel/plugin-transform-dynamic-import": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", "dev": true, "license": "MIT", "dependencies": { @@ -2389,6 +2158,8 @@ }, "node_modules/@babel/plugin-transform-export-namespace-from": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2403,6 +2174,8 @@ }, "node_modules/@babel/plugin-transform-for-of": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "license": "MIT", "dependencies": { @@ -2418,6 +2191,8 @@ }, "node_modules/@babel/plugin-transform-function-name": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2450,6 +2225,8 @@ }, "node_modules/@babel/plugin-transform-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, "license": "MIT", "dependencies": { @@ -2480,6 +2257,8 @@ }, "node_modules/@babel/plugin-transform-member-expression-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2494,6 +2273,8 @@ }, "node_modules/@babel/plugin-transform-modules-amd": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, "license": "MIT", "dependencies": { @@ -2545,6 +2326,8 @@ }, "node_modules/@babel/plugin-transform-modules-umd": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, "license": "MIT", "dependencies": { @@ -2577,6 +2360,8 @@ }, "node_modules/@babel/plugin-transform-new-target": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2643,6 +2428,8 @@ }, "node_modules/@babel/plugin-transform-object-super": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, "license": "MIT", "dependencies": { @@ -2691,6 +2478,8 @@ }, "node_modules/@babel/plugin-transform-parameters": { "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, "license": "MIT", "dependencies": { @@ -2704,12 +2493,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -2738,6 +2529,8 @@ }, "node_modules/@babel/plugin-transform-property-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2785,6 +2578,8 @@ }, "node_modules/@babel/plugin-transform-reserved-words": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, "license": "MIT", "dependencies": { @@ -2830,6 +2625,8 @@ }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2861,6 +2658,8 @@ }, "node_modules/@babel/plugin-transform-sticky-regex": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, "license": "MIT", "dependencies": { @@ -2875,6 +2674,8 @@ }, "node_modules/@babel/plugin-transform-template-literals": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "license": "MIT", "dependencies": { @@ -2889,6 +2690,8 @@ }, "node_modules/@babel/plugin-transform-typeof-symbol": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "license": "MIT", "dependencies": { @@ -2903,6 +2706,8 @@ }, "node_modules/@babel/plugin-transform-unicode-escapes": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, "license": "MIT", "dependencies": { @@ -2934,6 +2739,8 @@ }, "node_modules/@babel/plugin-transform-unicode-regex": { "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, "license": "MIT", "dependencies": { @@ -3049,23 +2856,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/preset-env/node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", @@ -3092,6 +2882,8 @@ }, "node_modules/@babel/preset-modules": { "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", "dev": true, "license": "MIT", "dependencies": { @@ -3168,8 +2960,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, "node_modules/@centerforopenscience/markdown-it-atrules": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@centerforopenscience/markdown-it-atrules/-/markdown-it-atrules-0.1.1.tgz", + "integrity": "sha512-r3ZTJESVCpdU9tOQe2PVcUkJmrPA/1itvCtHpsJrhXMj/nY7JcESjgMSFEe432DcofXDzRF8kXW1XWQuH1BfMw==", "license": "MIT", "dependencies": { "np": "^3.0.4" @@ -3177,6 +2985,8 @@ }, "node_modules/@citation-js/core": { "version": "0.7.21", + "resolved": "https://registry.npmjs.org/@citation-js/core/-/core-0.7.21.tgz", + "integrity": "sha512-Vobv2/Yfnn6C6BVO/pvj7madQ7Mfzl83/jAWwixbemGF6ZThhGMz8++FD9hWHyHXDMYuLGa6fK68c2VsolZmTA==", "license": "MIT", "dependencies": { "@citation-js/date": "^0.5.0", @@ -3190,6 +3000,8 @@ }, "node_modules/@citation-js/date": { "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz", + "integrity": "sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -3197,13 +3009,17 @@ }, "node_modules/@citation-js/name": { "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz", + "integrity": "sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==", "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/@citation-js/plugin-csl": { - "version": "0.7.21", + "version": "0.7.22", + "resolved": "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.22.tgz", + "integrity": "sha512-/rGdtbeP3nS4uZDdEbQUHT8PrUcIs0da2t+sWMKYXoOhXQqfw3oJJ7p4tUD+R8lptyIR5Eq20/DFk/kQDdLpYg==", "license": "MIT", "dependencies": { "@citation-js/date": "^0.5.0", @@ -3238,85 +3054,185 @@ "node": ">=v18" } }, - "node_modules/@commitlint/config-conventional": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", - "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", + "node_modules/@commitlint/cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "conventional-changelog-conventionalcommits": "^9.2.0" - }, "engines": { - "node": ">=v18" + "node": ">=8" } }, - "node_modules/@commitlint/config-validator": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", - "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", + "node_modules/@commitlint/cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "@commitlint/types": "^20.5.0", - "ajv": "^8.11.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": ">=v18" + "node": ">=12" } }, - "node_modules/@commitlint/ensure": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", - "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", + "node_modules/@commitlint/cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", - "dependencies": { - "@commitlint/types": "^20.5.0", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" - }, "engines": { - "node": ">=v18" + "node": ">=8" } }, - "node_modules/@commitlint/execute-rule": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", - "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "node_modules/@commitlint/cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=v18" + "node": ">=8" } }, - "node_modules/@commitlint/format": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", - "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", + "node_modules/@commitlint/cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", - "picocolors": "^1.1.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=v18" + "node": ">=8" } }, - "node_modules/@commitlint/is-ignored": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", - "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", + "node_modules/@commitlint/cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/types": "^20.5.0", - "semver": "^7.6.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@commitlint/cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", + "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.5.0", + "conventional-changelog-conventionalcommits": "^9.2.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-20.5.0.tgz", + "integrity": "sha512-T/Uh6iJUzyx7j35GmHWdIiGRQB+ouZDk0pwAaYq4SXgB54KZhFdJ0vYmxiW6AMYICTIWuyMxDBl1jK74oFp/Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.5.0", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", + "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.5.0", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-20.0.0.tgz", + "integrity": "sha512-xyCoOShoPuPL44gVa+5EdZsBVao/pNzpQhkzq3RdtlFdKZtjWcLlUFQHSWBuhk5utKYykeJPSz2i8ABHQA+ZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-20.5.0.tgz", + "integrity": "sha512-TI9EwFU/qZWSK7a5qyXMpKPPv3qta7FO4tKW+Wt2al7sgMbLWTsAcDpX1cU8k16TRdsiiet9aOw0zpvRXNJu7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.5.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "20.5.0", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-20.5.0.tgz", + "integrity": "sha512-JWLarAsurHJhPozbuAH6GbP4p/hdOCoqS9zJMfqwswne+/GPs5V0+rrsfOkP68Y8PSLphwtFXV0EzJ+GTXTTGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^20.5.0", + "semver": "^7.6.0" }, "engines": { "node": ">=v18" @@ -3359,19 +3275,6 @@ "node": ">=v18" } }, - "node_modules/@commitlint/load/node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@commitlint/message": { "version": "20.4.3", "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-20.4.3.tgz", @@ -3486,34 +3389,36 @@ } }, "node_modules/@compodoc/compodoc": { - "version": "1.1.32", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@compodoc/compodoc/-/compodoc-1.2.1.tgz", + "integrity": "sha512-won7I0OeFM0zSi+cwVilwtLA7VpY9NzsBch6+gGElYQdbM4/01XgA//leF8EsNzhxuQb+kP86MqJ3loZhz+Big==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular-devkit/schematics": "20.3.4", - "@babel/core": "7.28.4", - "@babel/plugin-transform-private-methods": "7.27.1", - "@babel/preset-env": "7.28.3", + "@angular-devkit/schematics": "21.1.0", + "@babel/core": "7.28.6", + "@babel/plugin-transform-private-methods": "7.28.6", + "@babel/preset-env": "7.28.6", "@compodoc/live-server": "^1.2.3", "@compodoc/ngd-transformer": "^2.1.3", "@polka/send-type": "^0.5.2", - "body-parser": "^2.2.0", + "body-parser": "^2.2.2", "bootstrap.native": "^5.1.6", "cheerio": "1.1.2", - "chokidar": "^4.0.3", + "chokidar": "^5.0.0", "colors": "1.4.0", - "commander": "^14.0.1", + "commander": "^14.0.2", "cosmiconfig": "^9.0.0", "decache": "^4.6.2", "es6-shim": "^0.35.8", "fancy-log": "^2.0.0", "fast-glob": "^3.3.3", - "fs-extra": "^11.3.2", - "glob": "^11.0.3", + "fs-extra": "^11.3.3", + "glob": "^13.0.0", "handlebars": "^4.7.8", "html-entities": "^2.6.0", - "i18next": "25.5.3", + "i18next": "25.7.4", "json5": "^2.2.3", "lodash": "^4.17.21", "loglevel": "^1.9.2", @@ -3527,11 +3432,11 @@ "picocolors": "^1.1.1", "polka": "^0.5.2", "prismjs": "^1.30.0", - "semver": "^7.7.2", + "semver": "^7.7.3", "sirv": "^3.0.2", "svg-pan-zoom": "^3.6.2", - "tablesort": "^5.6.0", - "ts-morph": "^27.0.0", + "tablesort": "^5.7.0", + "ts-morph": "^27.0.2", "uuid": "11.1.0", "vis-network": "^10.0.2" }, @@ -3543,7 +3448,9 @@ } }, "node_modules/@compodoc/compodoc/node_modules/@angular-devkit/core": { - "version": "20.3.4", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.1.0.tgz", + "integrity": "sha512-dPfVy0CictDjWffRv4pGTPOFjdlJL3ZkGUqxzaosUjMbJW+Ai9cNn1VNr7zxYZ4kem3BxLBh1thzDsCPrkXlZA==", "dev": true, "license": "MIT", "dependencies": { @@ -3560,7 +3467,7 @@ "yarn": ">= 1.13.0" }, "peerDependencies": { - "chokidar": "^4.0.0" + "chokidar": "^5.0.0" }, "peerDependenciesMeta": { "chokidar": { @@ -3569,14 +3476,16 @@ } }, "node_modules/@compodoc/compodoc/node_modules/@angular-devkit/schematics": { - "version": "20.3.4", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.1.0.tgz", + "integrity": "sha512-sVgTntCZCOV7mOpHzj6V14KOAoy4B9Ur9yHNRFZVgL2yD77TYRrJ0qwq+l7Im9fSjMCar6csjboqCvyAEpfV1g==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "20.3.4", + "@angular-devkit/core": "21.1.0", "jsonc-parser": "3.3.1", - "magic-string": "0.30.17", - "ora": "8.2.0", + "magic-string": "0.30.21", + "ora": "9.0.0", "rxjs": "7.8.2" }, "engines": { @@ -3586,19 +3495,21 @@ } }, "node_modules/@compodoc/compodoc/node_modules/@babel/core": { - "version": "7.28.4", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -3616,6 +3527,8 @@ }, "node_modules/@compodoc/compodoc/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -3623,74 +3536,76 @@ } }, "node_modules/@compodoc/compodoc/node_modules/@babel/preset-env": { - "version": "7.28.3", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.6.tgz", + "integrity": "sha512-GaTI4nXDrs7l0qaJ6Rg06dtOXTBCG6TMDB44zbqofCIC4PqC7SEvmFFtpxzCDw9W5aJ7RKVshgXTLvLdBFV/qw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", + "@babel/compat-data": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.6", + "@babel/plugin-transform-async-to-generator": "^7.28.6", "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.28.6", "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", "@babel/plugin-transform-export-namespace-from": "^7.27.1", "@babel/plugin-transform-for-of": "^7.27.1", "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", "@babel/plugin-transform-member-expression-literals": "^7.27.1", "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", "@babel/plugin-transform-modules-umd": "^7.27.1", "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.6", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", "@babel/plugin-transform-reserved-words": "^7.27.1", "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", "@babel/plugin-transform-sticky-regex": "^7.27.1", "@babel/plugin-transform-template-literals": "^7.27.1", "@babel/plugin-transform-typeof-symbol": "^7.27.1", "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.14", "babel-plugin-polyfill-corejs3": "^0.13.0", @@ -3707,45 +3622,60 @@ }, "node_modules/@compodoc/compodoc/node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" } }, - "node_modules/@compodoc/compodoc/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/@compodoc/compodoc/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@compodoc/compodoc/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 14.16.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/@compodoc/compodoc/node_modules/convert-source-map": { "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@compodoc/compodoc/node_modules/emoji-regex": { - "version": "10.6.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, "license": "MIT" }, "node_modules/@compodoc/compodoc/node_modules/log-symbols": { - "version": "6.0.0", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { "node": ">=18" @@ -3754,70 +3684,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@compodoc/compodoc/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@compodoc/compodoc/node_modules/ora": { - "version": "8.2.0", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-9.0.0.tgz", + "integrity": "sha512-m0pg2zscbYgWbqRR6ABga5c3sZdEon7bSgjnlXC64kxtxLOyjRcbbUkLj7HFyy/FTD+P2xdBWu8snGhYI0jc4A==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.3.0", + "chalk": "^5.6.2", "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", + "cli-spinners": "^3.2.0", "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" + "string-width": "^8.1.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@compodoc/compodoc/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@compodoc/compodoc/node_modules/source-map": { - "version": "0.7.6", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/@compodoc/compodoc/node_modules/string-width": { - "version": "7.2.0", + "node_modules/@compodoc/compodoc/node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, "engines": { "node": ">=18" }, @@ -3827,6 +3723,8 @@ }, "node_modules/@compodoc/live-server": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@compodoc/live-server/-/live-server-1.2.3.tgz", + "integrity": "sha512-hDmntVCyjjaxuJzPzBx68orNZ7TW4BtHWMnXlIVn5dqhK7vuFF/11hspO1cMmc+2QTYgqde1TBcb3127S7Zrow==", "dev": true, "license": "MIT", "dependencies": { @@ -3854,6 +3752,8 @@ }, "node_modules/@compodoc/live-server/node_modules/chokidar": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, "license": "MIT", "dependencies": { @@ -3877,6 +3777,8 @@ }, "node_modules/@compodoc/live-server/node_modules/define-lazy-prop": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, "license": "MIT", "engines": { @@ -3885,7 +3787,9 @@ }, "node_modules/@compodoc/live-server/node_modules/glob-parent": { "version": "5.1.2", - "dev": true, + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -3896,6 +3800,8 @@ }, "node_modules/@compodoc/live-server/node_modules/is-docker": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", "bin": { @@ -3910,6 +3816,8 @@ }, "node_modules/@compodoc/live-server/node_modules/is-wsl": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", "dependencies": { @@ -3921,6 +3829,8 @@ }, "node_modules/@compodoc/live-server/node_modules/open": { "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3936,7 +3846,9 @@ } }, "node_modules/@compodoc/live-server/node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -3948,6 +3860,8 @@ }, "node_modules/@compodoc/live-server/node_modules/readdirp": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, "license": "MIT", "dependencies": { @@ -3959,6 +3873,8 @@ }, "node_modules/@compodoc/ngd-core": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@compodoc/ngd-core/-/ngd-core-2.1.1.tgz", + "integrity": "sha512-Z+wE6wWZYVnudRYg6qunDlyh3Orw39Ib66Gvrz5kX5u7So+iu3tr6sQJdqH6yGS3hAjig5avlfhWLlgsb6/x1Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3972,6 +3888,8 @@ }, "node_modules/@compodoc/ngd-transformer": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@compodoc/ngd-transformer/-/ngd-transformer-2.1.3.tgz", + "integrity": "sha512-oWxJza7CpWR8/FeWYfE6j+jgncnGBsTWnZLt5rD2GUpsGSQTuGrsFPnmbbaVLgRS5QIVWBJYke7QFBr/7qVMWg==", "dev": true, "license": "MIT", "dependencies": { @@ -4011,34 +3929,10 @@ } } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -4051,14 +3945,15 @@ } ], "license": "MIT-0", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "dev": true, "funding": [ { @@ -4071,18 +3966,19 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, "funding": [ { @@ -4095,22 +3991,23 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -4123,17 +4020,44 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.1.tgz", + "integrity": "sha512-BvqN0AMWNAnLk9G8jnUT77D+mUbY/H2b3uDTvg2isJkHaOufUE2R3AOwxWo7VBQKT1lOdwdvorddo2B/lk64+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peer": true, + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -4146,12 +4070,15 @@ } ], "license": "MIT", + "peer": true, "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@discoveryjs/json-ext": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", "dev": true, "license": "MIT", "engines": { @@ -4669,6 +4596,8 @@ }, "node_modules/@eslint-community/regexpp": { "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -4690,45 +4619,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@eslint/config-helpers": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", @@ -4800,6 +4690,25 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@fortawesome/fontawesome-free": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", @@ -4842,6 +4751,8 @@ }, "node_modules/@humanfs/core": { "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4850,6 +4761,8 @@ }, "node_modules/@humanfs/node": { "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -4862,6 +4775,8 @@ }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4874,6 +4789,8 @@ }, "node_modules/@humanwhocodes/retry": { "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -5234,27 +5151,10 @@ } } }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, "license": "ISC", "dependencies": { @@ -5271,6 +5171,8 @@ }, "node_modules/@isaacs/cliui/node_modules/ansi-styles": { "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -5280,8 +5182,35 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5408,6 +5337,8 @@ }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "license": "MIT", "engines": { @@ -5432,39 +5363,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/console/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/console/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@jest/core": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz", @@ -5512,39 +5410,6 @@ } } }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@jest/diff-sequences": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", @@ -5727,37 +5592,21 @@ } } }, - "node_modules/@jest/reporters/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@jest/reporters/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT" }, - "node_modules/@jest/reporters/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@jest/reporters/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "balanced-match": "^1.0.0" } }, "node_modules/@jest/reporters/node_modules/glob": { @@ -5782,22 +5631,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@jest/reporters/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/@jest/reporters/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -5805,6 +5638,22 @@ "dev": true, "license": "ISC" }, + "node_modules/@jest/reporters/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@jest/reporters/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -5851,39 +5700,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/snapshot-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/snapshot-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@jest/source-map": { "version": "30.0.1", "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", @@ -5957,39 +5773,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@jest/transform/node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6016,41 +5799,10 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, "license": "MIT", "dependencies": { @@ -6060,6 +5812,8 @@ }, "node_modules/@jridgewell/remapping": { "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6069,6 +5823,8 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "license": "MIT", "engines": { @@ -6088,11 +5844,15 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -6532,6 +6292,8 @@ }, "node_modules/@kurkle/color": { "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, "node_modules/@leichtgewicht/ip-codec": { @@ -7142,9 +6904,9 @@ } }, "node_modules/@newrelic/browser-agent": { - "version": "1.310.1", - "resolved": "https://registry.npmjs.org/@newrelic/browser-agent/-/browser-agent-1.310.1.tgz", - "integrity": "sha512-ggBr+oBY1bfDtmpnMvwFU1brG9J0hMAlYNZGxpJa8hP4Z7auzd+GyICPrAwp8cQWHOP5OUoMalzxhx8RTtlWug==", + "version": "1.311.0", + "resolved": "https://registry.npmjs.org/@newrelic/browser-agent/-/browser-agent-1.311.0.tgz", + "integrity": "sha512-4nCcuzeXUK6AMdIqNXLGUN0P4flhvMLUVXleX12CZ7VmsRMhqpD6DozxUtOftfQ5rlGJMqJnp2LDnaQHgtVpiA==", "license": "Apache-2.0", "dependencies": { "@newrelic/rrweb": "1.0.1", @@ -7157,6 +6919,8 @@ }, "node_modules/@newrelic/rrdom": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/rrdom/-/rrdom-1.0.1.tgz", + "integrity": "sha512-nfO0ZnyqIta4gnKmcoAyP03o7Jc+EAj0TyJPq91gwNXkzbHSazDM0uWXj2KCCXFqx3KLu68cFbkcJXb8/piqTw==", "license": "MIT", "dependencies": { "@newrelic/rrweb-snapshot": "^1.0.1" @@ -7164,6 +6928,8 @@ }, "node_modules/@newrelic/rrweb": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb/-/rrweb-1.0.1.tgz", + "integrity": "sha512-qr6JbjamTPYfkJazf+Rpd4AGPgWuJ2V4L2rdpPXzA5GiBx8eFkujJpDV+20hNzxhIYvoLxC1tqfejDVq9qNM7g==", "license": "MIT", "dependencies": { "@newrelic/rrdom": "^1.0.1", @@ -7178,6 +6944,8 @@ }, "node_modules/@newrelic/rrweb-snapshot": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb-snapshot/-/rrweb-snapshot-1.0.1.tgz", + "integrity": "sha512-nOx5UqbRkc0g5rbX1JeVUi3Qnb7QDvGsuxKVo2ZbxHVrCjW8nwM/jsHKpNVZ+1mf95Nmhoxzu4pZIOXp4Dt4ZQ==", "license": "MIT", "dependencies": { "postcss": "^8.4.38" @@ -7185,10 +6953,14 @@ }, "node_modules/@newrelic/rrweb-types": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb-types/-/rrweb-types-1.0.1.tgz", + "integrity": "sha512-NPllHLTkmXyRNMwItuCl3kOQvWUH7Y6homnxnHDgINLsM2ohRYuQyN32UssAV1zi7JqDgCszsK4X/47+J8hyKg==", "license": "MIT" }, "node_modules/@newrelic/rrweb-utils": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb-utils/-/rrweb-utils-1.0.1.tgz", + "integrity": "sha512-2twM2sR6LQWWUZOqXyWR27eREqIyIfx4PKivAt9vsYrxh1M32dWs6zYv4f/d397FweVkWT00cvxo3w03kKnQcw==", "license": "MIT" }, "node_modules/@ngtools/webpack": { @@ -7266,6 +7038,8 @@ }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", "dependencies": { @@ -7278,6 +7052,8 @@ }, "node_modules/@nodelib/fs.stat": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", "engines": { @@ -7286,6 +7062,8 @@ }, "node_modules/@nodelib/fs.walk": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { @@ -7438,101 +7216,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@npmcli/package-json/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@npmcli/package-json/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@npmcli/package-json/node_modules/spdx-expression-parse": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", - "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, "node_modules/@npmcli/promise-spawn": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-9.0.1.tgz", @@ -8112,11 +7795,15 @@ }, "node_modules/@polka/send-type": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@polka/send-type/-/send-type-0.5.2.tgz", + "integrity": "sha512-jGXalKihnhGQmMQ+xxfxrRfI2cWs38TIZuwgYpnbQDD4r9TkOiU3ocjAS+6CqqMNQNAu9Ul2iHU5YFRDODak2w==", "dev": true, "license": "MIT" }, "node_modules/@polka/url": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-0.5.0.tgz", + "integrity": "sha512-oZLYFEAzUKyi3SKnXvj32ZCEGH6RDnao7COuCVhDydMS9NrCSVXhM79VaKyP5+Zc33m0QXEd2DN3UkU7OsHcfw==", "dev": true, "license": "MIT" }, @@ -8143,25 +7830,7 @@ "node": ">=12.11.0" } }, - "node_modules/@primeuix/motion/node_modules/@primeuix/utils": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz", - "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==", - "license": "MIT", - "engines": { - "node": ">=12.11.0" - } - }, - "node_modules/@primeuix/styles": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz", - "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==", - "license": "MIT", - "dependencies": { - "@primeuix/styled": "^0.7.4" - } - }, - "node_modules/@primeuix/styles/node_modules/@primeuix/styled": { + "node_modules/@primeuix/styled": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", @@ -8173,13 +7842,13 @@ "node": ">=12.11.0" } }, - "node_modules/@primeuix/styles/node_modules/@primeuix/utils": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz", - "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==", + "node_modules/@primeuix/styles": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-2.0.3.tgz", + "integrity": "sha512-2ykAB6BaHzR/6TwF8ShpJTsZrid6cVIEBVlookSdvOdmlWuevGu5vWOScgIwqWwlZcvkFYAGR/SUV3OHCTBMdw==", "license": "MIT", - "engines": { - "node": ">=12.11.0" + "dependencies": { + "@primeuix/styled": "^0.7.4" } }, "node_modules/@primeuix/themes": { @@ -8191,19 +7860,7 @@ "@primeuix/styled": "^0.7.4" } }, - "node_modules/@primeuix/themes/node_modules/@primeuix/styled": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", - "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", - "license": "MIT", - "dependencies": { - "@primeuix/utils": "^0.6.1" - }, - "engines": { - "node": ">=12.11.0" - } - }, - "node_modules/@primeuix/themes/node_modules/@primeuix/utils": { + "node_modules/@primeuix/utils": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz", "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==", @@ -8792,6 +8449,8 @@ }, "node_modules/@samverschueren/stream-to-observable": { "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz", + "integrity": "sha512-c/qwwcHyafOQuVQJj0IlBjf5yYgBI7YPJ77k4fOJYesb41jio65eaJODRUmfYKhTOFBrIZ66kgvGPlNbjuoRdQ==", "license": "MIT", "dependencies": { "any-observable": "^0.3.0" @@ -9038,6 +8697,8 @@ }, "node_modules/@sindresorhus/is": { "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", "license": "MIT", "engines": { "node": ">=6" @@ -9071,6 +8732,8 @@ }, "node_modules/@szmarczak/http-timer": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", "license": "MIT", "dependencies": { "defer-to-connect": "^1.0.1" @@ -9080,7 +8743,9 @@ } }, "node_modules/@thednp/event-listener": { - "version": "2.0.10", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@thednp/event-listener/-/event-listener-2.0.14.tgz", + "integrity": "sha512-DctB5pHYVg1Gw19wtweh+4YDJIlEkoi1LNMTaPVNaK25VB9nnMY16TgtMmbxakeUIjMchQDog5wBEY9gyUeJrg==", "dev": true, "license": "MIT", "engines": { @@ -9089,11 +8754,13 @@ } }, "node_modules/@thednp/position-observer": { - "version": "1.1.0", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@thednp/position-observer/-/position-observer-1.1.2.tgz", + "integrity": "sha512-1YTnd6j30iYTcEPZigntdbNtfPvCnuJMOJ0YxBrsRklgjWOHvcOMiqusobVvcbscI4OgY/8aywrGoudS7VhUKA==", "dev": true, "license": "MIT", "dependencies": { - "@thednp/shorty": "^2.0.11" + "@thednp/shorty": "^2.0.13" }, "engines": { "node": ">=16", @@ -9101,7 +8768,9 @@ } }, "node_modules/@thednp/shorty": { - "version": "2.0.11", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@thednp/shorty/-/shorty-2.0.13.tgz", + "integrity": "sha512-gteebgdf01ugz7IyVQQhyAHtmHJufd/b6m7SQSsZgqRvCpWYNvGDkdbGHG30jI8Nm1CM+yPh4csaZNQsE1mYLQ==", "dev": true, "license": "MIT", "engines": { @@ -9111,6 +8780,8 @@ }, "node_modules/@traptitech/markdown-it-katex": { "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@traptitech/markdown-it-katex/-/markdown-it-katex-3.6.0.tgz", + "integrity": "sha512-CnJzTWxsgLGXFdSrWRaGz7GZ1kUUi8g3E9HzJmeveX1YwVJavrKYqysktfHZQsujdnRqV5O7g8FPKEA/aeTkOQ==", "license": "MIT", "dependencies": { "katex": "^0.16.0" @@ -9118,6 +8789,8 @@ }, "node_modules/@ts-morph/common": { "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.28.1.tgz", + "integrity": "sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==", "dev": true, "license": "MIT", "dependencies": { @@ -9126,54 +8799,12 @@ "tinyglobby": "^0.2.14" } }, - "node_modules/@ts-morph/common/node_modules/minimatch": { - "version": "10.1.1", + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", - "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", - "dev": true, - "license": "MIT", + "license": "MIT", "engines": { "node": "^16.14.0 || >=18.0.0" } @@ -9192,45 +8823,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@tufjs/models/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@tufjs/models/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@tufjs/models/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.1", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", @@ -9331,6 +8923,8 @@ }, "node_modules/@types/css-font-loading-module": { "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz", + "integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==", "license": "MIT" }, "node_modules/@types/eslint": { @@ -9364,6 +8958,8 @@ }, "node_modules/@types/estree": { "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -9394,11 +8990,15 @@ }, "node_modules/@types/gapi": { "version": "0.0.47", + "resolved": "https://registry.npmjs.org/@types/gapi/-/gapi-0.0.47.tgz", + "integrity": "sha512-/ZsLuq6BffMgbKMtZyDZ8vwQvTyKhKQ1G2K6VyWCgtHHhfSSXbk4+4JwImZiTjWNXfI2q1ZStAwFFHSkNoTkHA==", "dev": true, "license": "MIT" }, "node_modules/@types/gapi.auth2": { "version": "0.0.61", + "resolved": "https://registry.npmjs.org/@types/gapi.auth2/-/gapi.auth2-0.0.61.tgz", + "integrity": "sha512-cn+omiRoE/LTxZncnVl1QhcLggOT0sJ8Yz9RXIsw5R2zLyRf+0o6kaZzJ/Gr3Sxz6i7J/+PbXAF8yeZipCaiWw==", "dev": true, "license": "MIT", "dependencies": { @@ -9422,6 +9022,8 @@ }, "node_modules/@types/http-proxy": { "version": "1.17.17", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", + "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", "dev": true, "license": "MIT", "dependencies": { @@ -9478,6 +9080,32 @@ "parse5": "^7.0.0" } }, + "node_modules/@types/jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@types/jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -9487,10 +9115,14 @@ }, "node_modules/@types/linkify-it": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", "license": "MIT" }, "node_modules/@types/markdown-it": { "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "license": "MIT", "dependencies": { "@types/linkify-it": "^5", @@ -9499,6 +9131,8 @@ }, "node_modules/@types/mdurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, "node_modules/@types/mime": { @@ -9622,17 +9256,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -9645,22 +9279,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -9676,14 +9310,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -9698,14 +9332,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9716,9 +9350,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -9733,15 +9367,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -9758,9 +9392,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -9772,16 +9406,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -9799,56 +9433,17 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9863,13 +9458,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -10358,6 +9953,8 @@ }, "node_modules/@xstate/fsm": { "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz", + "integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==", "license": "MIT" }, "node_modules/@xtuc/ieee754": { @@ -10376,6 +9973,8 @@ }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true, "license": "BSD-2-Clause" }, @@ -10390,12 +9989,13 @@ } }, "node_modules/accepts": { - "version": "1.3.8", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" @@ -10443,21 +10043,10 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/adjust-sourcemap-loader": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", "dev": true, "license": "MIT", "dependencies": { @@ -10470,6 +10059,8 @@ }, "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "license": "MIT", "dependencies": { @@ -10492,7 +10083,9 @@ } }, "node_modules/ajv": { - "version": "8.17.1", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -10508,6 +10101,8 @@ }, "node_modules/ajv-formats": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10615,6 +10210,8 @@ }, "node_modules/ansi-align": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha512-TdlOggdA/zURfMYa7ABC66j+oqfMew58KpJMbUlH3bcZP1b+cBHIHDDn5uH9INsxrHBPjsqM0tDB4jPTF/vgJA==", "license": "ISC", "dependencies": { "string-width": "^2.0.0" @@ -10622,6 +10219,8 @@ }, "node_modules/ansi-align/node_modules/ansi-regex": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "license": "MIT", "engines": { "node": ">=4" @@ -10629,6 +10228,8 @@ }, "node_modules/ansi-align/node_modules/is-fullwidth-code-point": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "license": "MIT", "engines": { "node": ">=4" @@ -10636,6 +10237,8 @@ }, "node_modules/ansi-align/node_modules/string-width": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "license": "MIT", "dependencies": { "is-fullwidth-code-point": "^2.0.0", @@ -10647,6 +10250,8 @@ }, "node_modules/ansi-align/node_modules/strip-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "license": "MIT", "dependencies": { "ansi-regex": "^3.0.0" @@ -10657,6 +10262,8 @@ }, "node_modules/ansi-colors": { "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, "license": "MIT", "engines": { @@ -10694,6 +10301,8 @@ }, "node_modules/ansi-regex": { "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -10704,13 +10313,16 @@ } }, "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -10718,6 +10330,8 @@ }, "node_modules/any-observable": { "version": "0.3.0", + "resolved": "https://registry.npmjs.org/any-observable/-/any-observable-0.3.0.tgz", + "integrity": "sha512-/FQM1EDkTsf63Ub2C6O7GuYFDsSXUwsaZDurV0np41ocwq0jthUAYCmhBX9f+KwlaCgIuWyr/4WlUQUBfKfZog==", "license": "MIT", "engines": { "node": ">=6" @@ -10725,6 +10339,8 @@ }, "node_modules/anymatch": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "license": "ISC", "dependencies": { @@ -10736,7 +10352,9 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -10748,6 +10366,8 @@ }, "node_modules/apache-crypt": { "version": "1.2.6", + "resolved": "https://registry.npmjs.org/apache-crypt/-/apache-crypt-1.2.6.tgz", + "integrity": "sha512-072WetlM4blL8PREJVeY+WHiUh1R5VNt2HfceGS8aKqttPHcmqE5pkKuXPz/ULmJOFkc8Hw3kfKl6vy7Qka6DA==", "dev": true, "license": "MIT", "dependencies": { @@ -10759,25 +10379,24 @@ }, "node_modules/apache-md5": { "version": "1.1.8", + "resolved": "https://registry.npmjs.org/apache-md5/-/apache-md5-1.1.8.tgz", + "integrity": "sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true, - "license": "MIT" - }, "node_modules/argparse": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, "node_modules/aria-query": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -10786,6 +10405,8 @@ }, "node_modules/array-find-index": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10807,6 +10428,8 @@ }, "node_modules/array-union": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", "license": "MIT", "dependencies": { "array-uniq": "^1.0.1" @@ -10817,6 +10440,8 @@ }, "node_modules/array-uniq": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10824,6 +10449,8 @@ }, "node_modules/arrify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10846,6 +10473,8 @@ }, "node_modules/async": { "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true, "license": "MIT" }, @@ -10888,6 +10517,8 @@ }, "node_modules/axobject-query": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -10916,39 +10547,6 @@ "@babel/core": "^7.11.0 || ^8.0.0-0" } }, - "node_modules/babel-jest/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/babel-jest/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/babel-loader": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", @@ -11016,6 +10614,8 @@ }, "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "license": "ISC", "bin": { @@ -11094,11 +10694,19 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-arraybuffer": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", "license": "MIT", "engines": { "node": ">= 0.6.0" @@ -11106,6 +10714,8 @@ }, "node_modules/base64-js": { "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "funding": [ { "type": "github", @@ -11123,15 +10733,22 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.9", + "version": "2.10.10", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.10.tgz", + "integrity": "sha512-sUoJ3IMxx4AyRqO4MLeHlnGDkyXRoUG0/AI9fjK+vS72ekpV0yWVY7O0BVjmBcRtkNcsAO2QDZ4tdKKGoI6YaQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/basic-auth": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "dev": true, "license": "MIT", "dependencies": { @@ -11141,18 +10758,17 @@ "node": ">= 0.8" } }, - "node_modules/basic-auth/node_modules/safe-buffer": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, "node_modules/batch": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true, "license": "MIT" }, "node_modules/bcryptjs": { "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "dev": true, "license": "MIT" }, @@ -11177,38 +10793,21 @@ "node": ">=18.0.0" } }, - "node_modules/beasties/node_modules/css-select": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", - "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "peer": true, "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^7.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "nth-check": "^2.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/beasties/node_modules/css-what": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", - "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "require-from-string": "^2.0.2" } }, "node_modules/big.js": { "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, "license": "MIT", "engines": { @@ -11217,6 +10816,8 @@ }, "node_modules/binary-extensions": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, "license": "MIT", "engines": { @@ -11227,7 +10828,9 @@ } }, "node_modules/body-parser": { - "version": "2.2.1", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -11236,7 +10839,7 @@ "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", + "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" }, @@ -11261,6 +10864,8 @@ }, "node_modules/boolbase": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "dev": true, "license": "ISC" }, @@ -11285,13 +10890,15 @@ } }, "node_modules/bootstrap.native": { - "version": "5.1.6", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/bootstrap.native/-/bootstrap.native-5.1.9.tgz", + "integrity": "sha512-pkWQu9U+OHSs5Sob7quE8yiE1jqkeajsvpOn42+2oRSEGdZJFwx4C+Siy+QEaYVYzwjKgrQZj7d4DWcK92iXPg==", "dev": true, "license": "MIT", "dependencies": { - "@thednp/event-listener": "^2.0.10", - "@thednp/position-observer": "^1.1.0", - "@thednp/shorty": "^2.0.11" + "@thednp/event-listener": "^2.0.14", + "@thednp/position-observer": "^1.1.2", + "@thednp/shorty": "^2.0.13" }, "engines": { "node": ">=16", @@ -11300,6 +10907,8 @@ }, "node_modules/boxen": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", "license": "MIT", "dependencies": { "ansi-align": "^2.0.0", @@ -11316,6 +10925,8 @@ }, "node_modules/boxen/node_modules/ansi-regex": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "license": "MIT", "engines": { "node": ">=4" @@ -11323,6 +10934,8 @@ }, "node_modules/boxen/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -11333,6 +10946,8 @@ }, "node_modules/boxen/node_modules/camelcase": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", "license": "MIT", "engines": { "node": ">=4" @@ -11340,6 +10955,8 @@ }, "node_modules/boxen/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -11352,6 +10969,8 @@ }, "node_modules/boxen/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -11359,10 +10978,14 @@ }, "node_modules/boxen/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/boxen/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -11370,6 +10993,8 @@ }, "node_modules/boxen/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -11377,6 +11002,8 @@ }, "node_modules/boxen/node_modules/is-fullwidth-code-point": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "license": "MIT", "engines": { "node": ">=4" @@ -11384,6 +11011,8 @@ }, "node_modules/boxen/node_modules/string-width": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "license": "MIT", "dependencies": { "is-fullwidth-code-point": "^2.0.0", @@ -11395,6 +11024,8 @@ }, "node_modules/boxen/node_modules/strip-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "license": "MIT", "dependencies": { "ansi-regex": "^3.0.0" @@ -11405,6 +11036,8 @@ }, "node_modules/boxen/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -11414,15 +11047,22 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "license": "MIT", "dependencies": { @@ -11434,6 +11074,8 @@ }, "node_modules/browserslist": { "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -11466,6 +11108,8 @@ }, "node_modules/bs-logger": { "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", "dev": true, "license": "MIT", "dependencies": { @@ -11487,6 +11131,8 @@ }, "node_modules/btoa": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", "dev": true, "license": "(MIT OR Apache-2.0)", "bin": { @@ -11498,6 +11144,8 @@ }, "node_modules/buffer": { "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "funding": [ { "type": "github", @@ -11520,11 +11168,15 @@ }, "node_modules/buffer-from": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true, "license": "MIT" }, "node_modules/builtins": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==", "license": "MIT" }, "node_modules/bundle-name": { @@ -11545,6 +11197,8 @@ }, "node_modules/bytes": { "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -11582,73 +11236,16 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/cacache/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/cacache/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/cacache/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/cacache/node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", - "dev": true, - "license": "BlueOak-1.0.0", + "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, - "node_modules/cacache/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacache/node_modules/p-map": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", @@ -11662,25 +11259,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cacache/node_modules/path-scurry": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^11.0.0", - "minipass": "^7.1.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/cacheable-request": { "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", "license": "MIT", "dependencies": { "clone-response": "^1.0.2", @@ -11697,6 +11279,8 @@ }, "node_modules/cacheable-request/node_modules/get-stream": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -11710,10 +11294,14 @@ }, "node_modules/cacheable-request/node_modules/json-buffer": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", "license": "MIT" }, "node_modules/cacheable-request/node_modules/keyv": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", "license": "MIT", "dependencies": { "json-buffer": "3.0.0" @@ -11721,6 +11309,8 @@ }, "node_modules/cacheable-request/node_modules/lowercase-keys": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", "license": "MIT", "engines": { "node": ">=8" @@ -11728,6 +11318,8 @@ }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -11739,6 +11331,8 @@ }, "node_modules/call-bound": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -11753,6 +11347,8 @@ }, "node_modules/callsite": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz", + "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==", "dev": true, "engines": { "node": "*" @@ -11760,6 +11356,8 @@ }, "node_modules/callsites": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", "engines": { @@ -11768,6 +11366,8 @@ }, "node_modules/camelcase": { "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { @@ -11776,6 +11376,8 @@ }, "node_modules/camelcase-keys": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha512-Ej37YKYbFUI8QiYlvj9YHb6/Z60dZyPJW0Cs8sFilMbd2lP0bw3ylAq9yJkK4lcTA2dID5fG8LjmJYbO7kWb7Q==", "license": "MIT", "dependencies": { "camelcase": "^4.1.0", @@ -11788,6 +11390,8 @@ }, "node_modules/camelcase-keys/node_modules/camelcase": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", "license": "MIT", "engines": { "node": ">=4" @@ -11816,6 +11420,8 @@ }, "node_modules/capture-stack-trace": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz", + "integrity": "sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11826,6 +11432,8 @@ }, "node_modules/cedar-artifact-viewer": { "version": "0.9.5", + "resolved": "https://registry.npmjs.org/cedar-artifact-viewer/-/cedar-artifact-viewer-0.9.5.tgz", + "integrity": "sha512-o23pXLrLBB6ZgZZW79SaE+c41CEGSASZ9YC0qKd8BK8b2EmLwiH18dEQv5pXYSxKKo3Ue7WdnyLoRNEZ+yo9mQ==", "license": "ISC" }, "node_modules/cedar-embeddable-editor": { @@ -11835,11 +11443,17 @@ "license": "ISC" }, "node_modules/chalk": { - "version": "5.6.2", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" @@ -11864,6 +11478,8 @@ }, "node_modules/chart.js": { "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", "license": "MIT", "dependencies": { "@kurkle/color": "^0.3.0" @@ -11874,6 +11490,8 @@ }, "node_modules/cheerio": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", "dev": true, "license": "MIT", "dependencies": { @@ -11898,6 +11516,8 @@ }, "node_modules/cheerio-select": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -11912,6 +11532,62 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -11966,6 +11642,8 @@ }, "node_modules/citeproc": { "version": "2.4.63", + "resolved": "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz", + "integrity": "sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==", "license": "CPAL-1.0 OR AGPL-1.0" }, "node_modules/cjs-module-lexer": { @@ -11977,6 +11655,8 @@ }, "node_modules/cli-boxes": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha512-3Fo5wu8Ytle8q9iCzS4D2MWVL2X7JVWRiS1BnXbTFDhS9c/REkM9vd1AmabsoZoY5/dGi5TT9iKL8Kb6DeBRQg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -11984,6 +11664,8 @@ }, "node_modules/cli-cursor": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, "license": "MIT", "dependencies": { @@ -11997,11 +11679,13 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.2", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", + "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" + "node": ">=18.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -12024,23 +11708,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", - "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -12052,88 +11719,71 @@ } }, "node_modules/cliui": { - "version": "8.0.1", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", "dev": true, "license": "ISC", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=12" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">=20" } }, "node_modules/cliui/node_modules/ansi-styles": { - "version": "4.3.0", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/cliui/node_modules/wrap-ansi": { - "version": "7.0.0", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -12141,6 +11791,8 @@ }, "node_modules/clone-deep": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12154,6 +11806,8 @@ }, "node_modules/clone-deep/node_modules/is-plain-object": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "license": "MIT", "dependencies": { @@ -12165,6 +11819,8 @@ }, "node_modules/clone-response": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -12186,11 +11842,15 @@ }, "node_modules/code-block-writer": { "version": "13.0.3", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", + "integrity": "sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==", "dev": true, "license": "MIT" }, "node_modules/code-point-at": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12205,6 +11865,8 @@ }, "node_modules/color-convert": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12216,11 +11878,15 @@ }, "node_modules/color-name": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, "node_modules/color-support": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true, "license": "ISC", "bin": { @@ -12236,6 +11902,8 @@ }, "node_modules/colors": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", "dev": true, "license": "MIT", "engines": { @@ -12243,7 +11911,9 @@ } }, "node_modules/commander": { - "version": "14.0.2", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { @@ -12334,12 +12004,37 @@ "node": ">= 0.6" } }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, "node_modules/configstore": { "version": "3.1.5", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.5.tgz", + "integrity": "sha512-nlOhI4+fdzoK5xmJ+NY+1gZK56bwEaWZr8fYuXohZ9Vkc1o3a4T/R3M+yE/w7x/ZVJ1zF8c+oaOvF0dztdUgmA==", "license": "BSD-2-Clause", "dependencies": { "dot-prop": "^4.2.1", @@ -12355,6 +12050,8 @@ }, "node_modules/configstore/node_modules/dot-prop": { "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", + "integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==", "license": "MIT", "dependencies": { "is-obj": "^1.0.0" @@ -12365,6 +12062,8 @@ }, "node_modules/configstore/node_modules/is-obj": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12372,6 +12071,8 @@ }, "node_modules/configstore/node_modules/make-dir": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "license": "MIT", "dependencies": { "pify": "^3.0.0" @@ -12382,10 +12083,14 @@ }, "node_modules/configstore/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/configstore/node_modules/write-file-atomic": { "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", "license": "ISC", "dependencies": { "graceful-fs": "^4.1.11", @@ -12395,6 +12100,8 @@ }, "node_modules/connect": { "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, "license": "MIT", "dependencies": { @@ -12419,6 +12126,8 @@ }, "node_modules/connect/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -12427,6 +12136,8 @@ }, "node_modules/connect/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, @@ -12445,6 +12156,8 @@ }, "node_modules/content-type": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -12495,6 +12208,8 @@ }, "node_modules/convert-source-map": { "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true, "license": "MIT" }, @@ -12639,6 +12354,8 @@ }, "node_modules/create-error-class": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw==", "license": "MIT", "dependencies": { "capture-stack-trace": "^1.0.0" @@ -12647,15 +12364,10 @@ "node": ">=0.10.0" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { @@ -12669,6 +12381,8 @@ }, "node_modules/crypto-random-string": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==", "license": "MIT", "engines": { "node": ">=4" @@ -12711,22 +12425,41 @@ } }, "node_modules/css-select": { - "version": "5.2.2", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" }, "funding": { "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css-what": { - "version": "6.2.2", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -12763,8 +12496,146 @@ "node": ">=18" } }, + "node_modules/cssstyle/node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/cssstyle/node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/cssstyle/node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/currently-unhandled": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", "license": "MIT", "dependencies": { "array-find-index": "^1.0.1" @@ -12774,25 +12645,41 @@ } }, "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" } }, "node_modules/date-fns": { "version": "1.30.1", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-1.30.1.tgz", + "integrity": "sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==", "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -12808,6 +12695,8 @@ }, "node_modules/decache": { "version": "4.6.2", + "resolved": "https://registry.npmjs.org/decache/-/decache-4.6.2.tgz", + "integrity": "sha512-2LPqkLeu8XWHU8qNCS3kcF6sCcb5zIzvWaAHYSvPfwhdd7mHuah29NssMzrTYyHN4F5oFy2ko9OBYxegtU0FEw==", "dev": true, "license": "MIT", "dependencies": { @@ -12816,6 +12705,8 @@ }, "node_modules/decamelize": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12823,6 +12714,8 @@ }, "node_modules/decamelize-keys": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", "license": "MIT", "dependencies": { "decamelize": "^1.1.0", @@ -12837,6 +12730,8 @@ }, "node_modules/decamelize-keys/node_modules/map-obj": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -12851,6 +12746,8 @@ }, "node_modules/decompress-response": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", "license": "MIT", "dependencies": { "mimic-response": "^1.0.0" @@ -12876,6 +12773,8 @@ }, "node_modules/deep-extend": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "license": "MIT", "engines": { "node": ">=4.0.0" @@ -12883,6 +12782,8 @@ }, "node_modules/deep-is": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, "license": "MIT" }, @@ -12928,6 +12829,8 @@ }, "node_modules/defer-to-connect": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", "license": "MIT" }, "node_modules/define-lazy-prop": { @@ -12945,6 +12848,8 @@ }, "node_modules/del": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", + "integrity": "sha512-7yjqSoVSlJzA4t/VUwazuEagGeANEKB3f/aNI//06pfKgwoCb7f6Q1gETN1sZzYaj6chTQ0AhIwDiPdfOjko4A==", "license": "MIT", "dependencies": { "globby": "^6.1.0", @@ -12958,65 +12863,10 @@ "node": ">=4" } }, - "node_modules/del/node_modules/brace-expansion": { - "version": "1.1.12", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/del/node_modules/glob": { - "version": "7.2.3", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/del/node_modules/globby": { - "version": "6.1.0", - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/depd": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13072,6 +12922,8 @@ }, "node_modules/dijkstrajs": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", "dev": true, "license": "MIT" }, @@ -13090,6 +12942,8 @@ }, "node_modules/dom-serializer": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, "license": "MIT", "dependencies": { @@ -13103,6 +12957,8 @@ }, "node_modules/domelementtype": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "dev": true, "funding": [ { @@ -13114,6 +12970,8 @@ }, "node_modules/domhandler": { "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -13128,6 +12986,8 @@ }, "node_modules/domutils": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -13141,6 +13001,8 @@ }, "node_modules/dot": { "version": "2.0.0-beta.1", + "resolved": "https://registry.npmjs.org/dot/-/dot-2.0.0-beta.1.tgz", + "integrity": "sha512-kxM7fSnNQTXOmaeGuBSXM8O3fEsBb7XSDBllkGbRwa0lJSJTxxDE/4eSNGLKZUmlFw0f1vJ5qSV2BljrgQtgIA==", "dev": true, "license": "MIT" }, @@ -13159,6 +13021,8 @@ }, "node_modules/dunder-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -13171,24 +13035,34 @@ }, "node_modules/duplexer": { "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", "dev": true, "license": "MIT" }, "node_modules/duplexer3": { "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", "license": "BSD-3-Clause" }, "node_modules/eastasianwidth": { "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true, "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, "node_modules/ejs": { "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -13202,12 +13076,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", + "version": "1.5.322", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.322.tgz", + "integrity": "sha512-vFU34OcrvMcH66T+dYC3G4nURmgfDVewMIu6Q2urXpumAPSMmzvcn04KVVV8Opikq8Vs5nUbO/8laNhNRqSzYw==", "dev": true, "license": "ISC" }, "node_modules/elegant-spinner": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", + "integrity": "sha512-B+ZM+RXvRqQaAmkMlO/oSe5nMUOaUnyfGYCEHoR8wrXsZR2mA0XVibsxV1bvTwxdRWah1PkQqso2EzhILGHtEQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13227,12 +13105,16 @@ } }, "node_modules/emoji-regex": { - "version": "9.2.2", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, "license": "MIT" }, "node_modules/emojis-list": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", "dev": true, "license": "MIT", "engines": { @@ -13241,6 +13123,8 @@ }, "node_modules/encodeurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -13248,6 +13132,8 @@ }, "node_modules/encoding-sniffer": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", "dev": true, "license": "MIT", "dependencies": { @@ -13260,6 +13146,8 @@ }, "node_modules/encoding-sniffer/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -13271,6 +13159,8 @@ }, "node_modules/end-of-stream": { "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -13292,6 +13182,8 @@ }, "node_modules/entities": { "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -13302,6 +13194,8 @@ }, "node_modules/env-paths": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, "license": "MIT", "engines": { @@ -13344,6 +13238,8 @@ }, "node_modules/error-ex": { "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -13351,6 +13247,8 @@ }, "node_modules/es-define-property": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -13358,6 +13256,8 @@ }, "node_modules/es-errors": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -13372,6 +13272,8 @@ }, "node_modules/es-object-atoms": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -13382,6 +13284,8 @@ }, "node_modules/es6-shim": { "version": "0.35.8", + "resolved": "https://registry.npmjs.org/es6-shim/-/es6-shim-0.35.8.tgz", + "integrity": "sha512-Twf7I2v4/1tLoIXMT8HlqaBSS5H2wQTs2wx3MNYCI8K1R1/clXyCazrcVCPm/FuO9cyV8+leEaZOWD5C253NDg==", "dev": true, "license": "MIT" }, @@ -13442,6 +13346,8 @@ }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -13450,10 +13356,14 @@ }, "node_modules/escape-html": { "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", "engines": { @@ -13521,6 +13431,8 @@ }, "node_modules/eslint-config-prettier": { "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", "bin": { @@ -13566,6 +13478,8 @@ }, "node_modules/eslint-plugin-simple-import-sort": { "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -13609,6 +13523,8 @@ }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "license": "Apache-2.0", "engines": { @@ -13635,29 +13551,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/eslint/node_modules/eslint-visitor-keys": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", @@ -13673,6 +13566,8 @@ }, "node_modules/eslint/node_modules/ignore": { "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, "license": "MIT", "engines": { @@ -13686,28 +13581,12 @@ "dev": true, "license": "MIT" }, - "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", @@ -13785,6 +13664,8 @@ }, "node_modules/esutils": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -13793,6 +13674,8 @@ }, "node_modules/etag": { "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -13800,6 +13683,8 @@ }, "node_modules/event-stream": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", "dev": true, "license": "MIT", "dependencies": { @@ -13814,6 +13699,8 @@ }, "node_modules/eventemitter3": { "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "dev": true, "license": "MIT" }, @@ -13978,19 +13865,6 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "license": "MIT", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -14012,42 +13886,10 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/external-editor": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", "license": "MIT", "dependencies": { "chardet": "^0.4.0", @@ -14060,10 +13902,14 @@ }, "node_modules/external-editor/node_modules/chardet": { "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg==", "license": "MIT" }, "node_modules/external-editor/node_modules/iconv-lite": { "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -14074,6 +13920,8 @@ }, "node_modules/fancy-log": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-2.0.0.tgz", + "integrity": "sha512-9CzxZbACXMUXW13tS0tI8XsGGmxWzO2DmYrGuBJOJ8k8q2K7hwfJA5qHjuPPe8wtsco33YR9wc+Rlr5wYFvhSA==", "dev": true, "license": "MIT", "dependencies": { @@ -14085,6 +13933,8 @@ }, "node_modules/fast-deep-equal": { "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, "license": "MIT" }, @@ -14097,6 +13947,8 @@ }, "node_modules/fast-glob": { "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -14112,6 +13964,8 @@ }, "node_modules/fast-glob/node_modules/glob-parent": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { @@ -14123,16 +13977,22 @@ }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, "license": "MIT" }, "node_modules/fast-uri": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, "funding": [ { @@ -14147,7 +14007,9 @@ "license": "BSD-3-Clause" }, "node_modules/fastq": { - "version": "1.19.1", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -14156,6 +14018,8 @@ }, "node_modules/faye-websocket": { "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -14177,6 +14041,8 @@ }, "node_modules/fdir": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", "engines": { @@ -14193,6 +14059,8 @@ }, "node_modules/fetch-ponyfill": { "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz", + "integrity": "sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==", "license": "MIT", "dependencies": { "node-fetch": "~2.6.1" @@ -14200,10 +14068,14 @@ }, "node_modules/fflate": { "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "license": "MIT" }, "node_modules/figures": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" @@ -14214,6 +14086,8 @@ }, "node_modules/figures/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -14221,6 +14095,8 @@ }, "node_modules/file-entry-cache": { "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14231,15 +14107,36 @@ } }, "node_modules/filelist": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { "minimatch": "^5.0.1" } }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -14251,6 +14148,8 @@ }, "node_modules/fill-range": { "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "license": "MIT", "dependencies": { @@ -14262,6 +14161,8 @@ }, "node_modules/finalhandler": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, "license": "MIT", "dependencies": { @@ -14279,6 +14180,8 @@ }, "node_modules/finalhandler/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -14287,6 +14190,8 @@ }, "node_modules/finalhandler/node_modules/encodeurl": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, "license": "MIT", "engines": { @@ -14295,11 +14200,15 @@ }, "node_modules/finalhandler/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/finalhandler/node_modules/on-finished": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, "license": "MIT", "dependencies": { @@ -14311,6 +14220,8 @@ }, "node_modules/finalhandler/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, "license": "MIT", "engines": { @@ -14336,6 +14247,8 @@ }, "node_modules/flat": { "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, "license": "BSD-3-Clause", "bin": { @@ -14344,6 +14257,8 @@ }, "node_modules/flat-cache": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "license": "MIT", "dependencies": { @@ -14355,12 +14270,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -14390,6 +14309,8 @@ }, "node_modules/foreground-child": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, "license": "ISC", "dependencies": { @@ -14437,11 +14358,15 @@ }, "node_modules/from": { "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", "dev": true, "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.3.2", + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", "dev": true, "license": "MIT", "dependencies": { @@ -14468,6 +14393,8 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, "node_modules/fsevents": { @@ -14487,6 +14414,8 @@ }, "node_modules/function-bind": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14494,6 +14423,8 @@ }, "node_modules/gensync": { "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "license": "MIT", "engines": { @@ -14502,6 +14433,8 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, "license": "ISC", "engines": { @@ -14523,6 +14456,8 @@ }, "node_modules/get-intrinsic": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -14555,6 +14490,8 @@ }, "node_modules/get-proto": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -14596,25 +14533,23 @@ }, "node_modules/github-url-from-git": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/github-url-from-git/-/github-url-from-git-1.5.0.tgz", + "integrity": "sha512-WWOec4aRI7YAykQ9+BHmzjyNlkfJFG8QLXnDTsLz/kZefq7qkzdfo4p6fkYYMIq1aj+gZcQs/1HQhQh3DPPxlQ==", "license": "MIT" }, "node_modules/glob": { - "version": "11.1.0", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -14622,6 +14557,8 @@ }, "node_modules/glob-parent": { "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", "dependencies": { @@ -14655,20 +14592,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.1.1", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -14697,6 +14620,8 @@ }, "node_modules/global-dirs": { "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", "license": "MIT", "dependencies": { "ini": "^1.3.4" @@ -14707,10 +14632,88 @@ }, "node_modules/global-dirs/node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, + "node_modules/globby": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", + "license": "MIT", + "dependencies": { + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/globby/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/globby/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/globby/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globby/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/gopd": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -14721,6 +14724,8 @@ }, "node_modules/got": { "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", "license": "MIT", "dependencies": { "@sindresorhus/is": "^0.14.0", @@ -14741,6 +14746,8 @@ }, "node_modules/got/node_modules/get-stream": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -14751,10 +14758,14 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, "node_modules/gzip-size": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", "dev": true, "license": "MIT", "dependencies": { @@ -14776,6 +14787,8 @@ }, "node_modules/handlebars": { "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, "license": "MIT", "dependencies": { @@ -14796,6 +14809,8 @@ }, "node_modules/handlebars/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -14804,6 +14819,8 @@ }, "node_modules/has-ansi": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", "license": "MIT", "dependencies": { "ansi-regex": "^2.0.0" @@ -14814,6 +14831,8 @@ }, "node_modules/has-ansi/node_modules/ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -14821,6 +14840,8 @@ }, "node_modules/has-flag": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", "engines": { @@ -14829,6 +14850,8 @@ }, "node_modules/has-symbols": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -14839,6 +14862,8 @@ }, "node_modules/has-yarn": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-1.0.0.tgz", + "integrity": "sha512-UAI4b48aqrdez88CwMfC9s+gcJ25O1qg0/hS5eKOsIF5tOw2EYcgGsryYF6TEI5G8SeCYzFBt5Z04D/BDABYSQ==", "license": "MIT", "engines": { "node": ">=4" @@ -14846,6 +14871,8 @@ }, "node_modules/hasown": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -14900,13 +14927,6 @@ "wbuf": "^1.1.0" } }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, - "license": "MIT" - }, "node_modules/hpack.js/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -14923,13 +14943,6 @@ "util-deprecate": "~1.0.1" } }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT" - }, "node_modules/hpack.js/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -14941,20 +14954,23 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "whatwg-encoding": "^3.1.1" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/html-entities": { "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", "dev": true, "funding": [ { @@ -14976,7 +14992,9 @@ "license": "MIT" }, "node_modules/htmlparser2": { - "version": "10.0.0", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -14989,12 +15007,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.2.2", + "entities": "^7.0.1" } }, "node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -15006,6 +15026,8 @@ }, "node_modules/http-auth": { "version": "4.1.9", + "resolved": "https://registry.npmjs.org/http-auth/-/http-auth-4.1.9.tgz", + "integrity": "sha512-kvPYxNGc9EKGTXvOMnTBQw2RZfuiSihK/mLw/a4pbtRueTE45S55Lw/3k5CktIf7Ak0veMKEIteDj4YkNmCzmQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15020,6 +15042,8 @@ }, "node_modules/http-auth-connect": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/http-auth-connect/-/http-auth-connect-1.0.6.tgz", + "integrity": "sha512-yaO0QSCPqGCjPrl3qEEHjJP+lwZ6gMpXLuCBE06eWwcXomkI5TARtu0kxf9teFuBj6iaV3Ybr15jaWUvbzNzHw==", "dev": true, "license": "MIT", "engines": { @@ -15028,6 +15052,8 @@ }, "node_modules/http-auth/node_modules/uuid": { "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", "dev": true, "license": "MIT", "bin": { @@ -15036,6 +15062,8 @@ }, "node_modules/http-cache-semantics": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", "license": "BSD-2-Clause" }, "node_modules/http-deceiver": { @@ -15047,6 +15075,8 @@ }, "node_modules/http-errors": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "license": "MIT", "dependencies": { "depd": "~2.0.0", @@ -15065,11 +15095,15 @@ }, "node_modules/http-parser-js": { "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", "dev": true, "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15097,6 +15131,8 @@ }, "node_modules/http-proxy-middleware": { "version": "3.0.5", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", + "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", "dev": true, "license": "MIT", "dependencies": { @@ -15137,6 +15173,8 @@ }, "node_modules/husky": { "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, "license": "MIT", "bin": { @@ -15160,7 +15198,9 @@ } }, "node_modules/i18next": { - "version": "25.5.3", + "version": "25.7.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.4.tgz", + "integrity": "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw==", "dev": true, "funding": [ { @@ -15178,7 +15218,7 @@ ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.6" + "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" @@ -15220,6 +15260,8 @@ }, "node_modules/ieee754": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", "funding": [ { "type": "github", @@ -15259,45 +15301,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/ignore-walk/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/ignore-walk/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/ignore-walk/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", @@ -15321,6 +15324,8 @@ }, "node_modules/import-fresh": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -15336,6 +15341,8 @@ }, "node_modules/import-fresh/node_modules/resolve-from": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", "engines": { @@ -15344,6 +15351,8 @@ }, "node_modules/import-lazy": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", "license": "MIT", "engines": { "node": ">=4" @@ -15382,6 +15391,8 @@ }, "node_modules/imurmurhash": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "license": "MIT", "engines": { "node": ">=0.8.19" @@ -15389,6 +15400,8 @@ }, "node_modules/indent-string": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha512-BYqTHXTGUIvg7t1r4sJNKcbDZkL92nkXA8YtRpbjFHRHGDL/NtUeiBJMeE60kIFN/Mg8ESaWQvftaYMGJzQZCQ==", "license": "MIT", "engines": { "node": ">=4" @@ -15396,6 +15409,9 @@ }, "node_modules/inflight": { "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -15404,6 +15420,8 @@ }, "node_modules/inherits": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, "node_modules/ini": { @@ -15418,6 +15436,8 @@ }, "node_modules/inquirer": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", "license": "MIT", "dependencies": { "ansi-escapes": "^3.0.0", @@ -15440,6 +15460,8 @@ }, "node_modules/inquirer/node_modules/ansi-escapes": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "license": "MIT", "engines": { "node": ">=4" @@ -15447,6 +15469,8 @@ }, "node_modules/inquirer/node_modules/ansi-regex": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "license": "MIT", "engines": { "node": ">=4" @@ -15454,6 +15478,8 @@ }, "node_modules/inquirer/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -15464,6 +15490,8 @@ }, "node_modules/inquirer/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -15476,6 +15504,8 @@ }, "node_modules/inquirer/node_modules/cli-cursor": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" @@ -15486,10 +15516,14 @@ }, "node_modules/inquirer/node_modules/cli-width": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "license": "ISC" }, "node_modules/inquirer/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -15497,10 +15531,14 @@ }, "node_modules/inquirer/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/inquirer/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -15508,6 +15546,8 @@ }, "node_modules/inquirer/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -15515,6 +15555,8 @@ }, "node_modules/inquirer/node_modules/is-fullwidth-code-point": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "license": "MIT", "engines": { "node": ">=4" @@ -15522,6 +15564,8 @@ }, "node_modules/inquirer/node_modules/mimic-fn": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "license": "MIT", "engines": { "node": ">=4" @@ -15529,10 +15573,14 @@ }, "node_modules/inquirer/node_modules/mute-stream": { "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", "license": "ISC" }, "node_modules/inquirer/node_modules/onetime": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" @@ -15543,6 +15591,8 @@ }, "node_modules/inquirer/node_modules/restore-cursor": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "license": "MIT", "dependencies": { "onetime": "^2.0.0", @@ -15554,6 +15604,8 @@ }, "node_modules/inquirer/node_modules/rxjs": { "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", "license": "Apache-2.0", "dependencies": { "symbol-observable": "1.0.1" @@ -15564,10 +15616,14 @@ }, "node_modules/inquirer/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/inquirer/node_modules/string-width": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "license": "MIT", "dependencies": { "is-fullwidth-code-point": "^2.0.0", @@ -15579,6 +15635,8 @@ }, "node_modules/inquirer/node_modules/strip-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "license": "MIT", "dependencies": { "ansi-regex": "^3.0.0" @@ -15589,6 +15647,8 @@ }, "node_modules/inquirer/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -15599,6 +15659,8 @@ }, "node_modules/inquirer/node_modules/symbol-observable": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha512-Kb3PrPYz4HanVF1LVGuAdW6LoVgIwjUYJGzFe7NDrBLCN4lsV/5J0MFurV+ygS4bRVwrCEt2c7MQ1R2a72oJDw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15616,6 +15678,8 @@ }, "node_modules/ip-regex": { "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", "license": "MIT", "engines": { "node": ">=8" @@ -15632,10 +15696,14 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, "license": "MIT", "dependencies": { @@ -15647,6 +15715,8 @@ }, "node_modules/is-ci": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", + "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", "license": "MIT", "dependencies": { "ci-info": "^1.5.0" @@ -15657,10 +15727,14 @@ }, "node_modules/is-ci/node_modules/ci-info": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", + "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -15690,6 +15764,8 @@ }, "node_modules/is-extglob": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", "engines": { @@ -15724,6 +15800,8 @@ }, "node_modules/is-glob": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", "dependencies": { @@ -15767,6 +15845,8 @@ }, "node_modules/is-installed-globally": { "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha512-ERNhMg+i/XgDwPIPF3u24qpajVreaiSuvpb1Uu0jugw7KKcxGyCX8cgp8P5fwTmAuXku6beDHHECdKArjlg7tw==", "license": "MIT", "dependencies": { "global-dirs": "^0.1.0", @@ -15804,6 +15884,8 @@ }, "node_modules/is-npm": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz", + "integrity": "sha512-9r39FIr3d+KD9SbX0sfMsHzb5PP3uimOiwr3YupUaUFG4W0l1U57Rx3utpttV7qz5U3jmrO5auUa04LU9pyHsg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15811,6 +15893,8 @@ }, "node_modules/is-number": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, "license": "MIT", "engines": { @@ -15829,6 +15913,8 @@ }, "node_modules/is-observable": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-observable/-/is-observable-1.1.0.tgz", + "integrity": "sha512-NqCa4Sa2d+u7BWc6CukaObG3Fh+CU9bvixbpcXYhy2VvYS7vVGIdAgnIS5Ks3A/cqk4rebLJ9s8zBstT2aKnIA==", "license": "MIT", "dependencies": { "symbol-observable": "^1.1.0" @@ -15837,15 +15923,10 @@ "node": ">=4" } }, - "node_modules/is-observable/node_modules/symbol-observable": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-path-cwd": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15853,6 +15934,8 @@ }, "node_modules/is-path-in-cwd": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", "license": "MIT", "dependencies": { "is-path-inside": "^1.0.0" @@ -15863,6 +15946,8 @@ }, "node_modules/is-path-inside": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g==", "license": "MIT", "dependencies": { "path-is-inside": "^1.0.1" @@ -15872,14 +15957,22 @@ } }, "node_modules/is-plain-obj": { - "version": "1.1.0", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-plain-object": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "dev": true, "license": "MIT", "engines": { @@ -15895,10 +15988,14 @@ }, "node_modules/is-promise": { "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==", "license": "MIT" }, "node_modules/is-redirect": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", + "integrity": "sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15906,6 +16003,8 @@ }, "node_modules/is-retry-allowed": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15913,6 +16012,8 @@ }, "node_modules/is-scoped": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-scoped/-/is-scoped-1.0.0.tgz", + "integrity": "sha512-iT1y0qJcdqXnHe6SCtN9cOBPRiarw8Cy1EZkawW50dxO/7oHC6AYvs1tH4QbBbi7UC/vYY3BnRmbE0bFLwvUog==", "license": "MIT", "dependencies": { "scoped-regex": "^1.0.0" @@ -15923,6 +16024,8 @@ }, "node_modules/is-stream": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, "license": "MIT", "engines": { @@ -15947,6 +16050,8 @@ }, "node_modules/is-url-superb": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-3.0.0.tgz", + "integrity": "sha512-3faQP+wHCGDQT1qReM5zCPx2mxoal6DzbzquFlCYJLWyy4WPTved33ea2xFbX37z4NoriEwZGIYhFtx8RUB5wQ==", "license": "MIT", "dependencies": { "url-regex": "^5.0.0" @@ -15978,12 +16083,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, "license": "MIT", "engines": { @@ -15992,6 +16108,8 @@ }, "node_modules/issue-regex": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/issue-regex/-/issue-regex-2.0.0.tgz", + "integrity": "sha512-flaQ/45dMqCYSMzBQI/h3bcto6T70uN7kjNnI8n3gQU6no5p+QcnMWBNXkraED0YvbUymxKaqdvgPa09RZQM5A==", "license": "MIT", "engines": { "node": ">=6" @@ -15999,6 +16117,8 @@ }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -16007,6 +16127,8 @@ }, "node_modules/istanbul-lib-instrument": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -16065,21 +16187,25 @@ } }, "node_modules/jackspeak": { - "version": "4.1.1", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": "20 || >=22" - }, "funding": { "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/jake": { "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -16168,39 +16294,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-circus/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-cli": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz", @@ -16234,37 +16327,104 @@ } } }, - "node_modules/jest-cli/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-cli/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", "dependencies": { - "color-convert": "^2.0.1" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, + "engines": { + "node": ">=12" + } + }, + "node_modules/jest-cli/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", "engines": { "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/jest-cli/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-cli/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" } }, "node_modules/jest-config": { @@ -16318,37 +16478,21 @@ } } }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-config/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT" }, - "node_modules/jest-config/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-config/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "balanced-match": "^1.0.0" } }, "node_modules/jest-config/node_modules/glob": { @@ -16373,22 +16517,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-config/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest-config/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -16396,6 +16524,22 @@ "dev": true, "license": "ISC" }, + "node_modules/jest-config/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-config/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -16429,39 +16573,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-docblock": { "version": "30.2.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", @@ -16492,60 +16603,190 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-environment-jsdom": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" }, "engines": { - "node": ">=8" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-environment-jsdom/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/jest-each/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-environment-jsdom/node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jest-environment-jsdom/node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jest-environment-jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" } }, - "node_modules/jest-environment-jsdom": { - "version": "30.3.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", - "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", + "node_modules/jest-environment-jsdom/node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.3.0", - "@jest/environment-jsdom-abstract": "30.3.0", - "jsdom": "^26.1.0" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } + "node": ">=18" } }, "node_modules/jest-environment-node": { @@ -16622,39 +16863,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-message-util": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", @@ -16676,39 +16884,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-mock": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", @@ -16816,39 +16991,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-resolve/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-resolve/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-runner": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz", @@ -16883,39 +17025,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-runner/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-runner/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-runner/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -16971,37 +17080,21 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-runtime/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/jest-runtime/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT" }, - "node_modules/jest-runtime/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/jest-runtime/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "balanced-match": "^1.0.0" } }, "node_modules/jest-runtime/node_modules/glob": { @@ -17026,22 +17119,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-runtime/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jest-runtime/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -17049,6 +17126,22 @@ "dev": true, "license": "ISC" }, + "node_modules/jest-runtime/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/jest-runtime/node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -17073,63 +17166,30 @@ "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.27.4", - "@babel/generator": "^7.27.5", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1", - "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.3.0", - "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.3.0", - "@jest/transform": "30.3.0", - "@jest/types": "30.3.0", - "babel-preset-current-node-syntax": "^1.2.0", - "chalk": "^4.1.2", - "expect": "30.3.0", - "graceful-fs": "^4.2.11", - "jest-diff": "30.3.0", - "jest-matcher-utils": "30.3.0", - "jest-message-util": "30.3.0", - "jest-util": "30.3.0", - "pretty-format": "30.3.0", - "semver": "^7.7.2", - "synckit": "^0.11.8" - }, - "engines": { - "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.3.0", + "@jest/transform": "30.3.0", + "@jest/types": "30.3.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.3.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.3.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-util": "30.3.0", + "pretty-format": "30.3.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-util": { @@ -17150,39 +17210,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-validate": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz", @@ -17201,22 +17228,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/jest-validate/node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -17230,23 +17241,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-validate/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-watcher": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz", @@ -17267,39 +17261,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-watcher/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-watcher/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/jest-worker": { "version": "30.3.0", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz", @@ -17355,11 +17316,15 @@ }, "node_modules/js-tokens": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true, "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -17370,35 +17335,37 @@ } }, "node_modules/jsdom": { - "version": "26.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", - "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.5.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.1.1", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.1", - "ws": "^8.18.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -17409,8 +17376,43 @@ } } }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/jsdom/node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/jsdom/node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20" + } + }, "node_modules/jsesc": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -17422,11 +17424,15 @@ }, "node_modules/json-buffer": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, "license": "MIT" }, "node_modules/json-parse-better-errors": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "license": "MIT" }, "node_modules/json-parse-even-better-errors": { @@ -17441,6 +17447,8 @@ }, "node_modules/json-schema-traverse": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, "license": "MIT" }, @@ -17453,11 +17461,15 @@ }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "license": "MIT", "bin": { @@ -17469,11 +17481,15 @@ }, "node_modules/jsonc-parser": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "dev": true, "license": "MIT" }, "node_modules/jsonfile": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", "dependencies": { @@ -17495,6 +17511,8 @@ }, "node_modules/karma-source-map-support": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", "dev": true, "license": "MIT", "dependencies": { @@ -17502,7 +17520,9 @@ } }, "node_modules/katex": { - "version": "0.16.27", + "version": "0.16.40", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.40.tgz", + "integrity": "sha512-1DJcK/L05k1Y9Gf7wMcyuqFOL6BiY3vY0CFcAM/LPRN04NALxcl6u7lOWNsp3f/bCHWxigzQl6FbR95XJ4R84Q==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -17517,6 +17537,8 @@ }, "node_modules/katex/node_modules/commander": { "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "license": "MIT", "engines": { "node": ">= 12" @@ -17532,6 +17554,8 @@ }, "node_modules/keyv": { "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", "dependencies": { @@ -17540,6 +17564,8 @@ }, "node_modules/kind-of": { "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, "license": "MIT", "engines": { @@ -17548,6 +17574,8 @@ }, "node_modules/latest-version": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha512-Be1YRHWWlZaSsrz2U+VInk+tO0EwLIyV+23RhWLINJYwg/UIikxjlj3MhH37/6/EDCAusjajvMkMMUXRaMWl/w==", "license": "MIT", "dependencies": { "package-json": "^4.0.0" @@ -17681,6 +17709,8 @@ }, "node_modules/levn": { "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -17693,6 +17723,8 @@ }, "node_modules/license-webpack-plugin": { "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", "dev": true, "license": "ISC", "dependencies": { @@ -17709,11 +17741,15 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true, "license": "MIT" }, "node_modules/linkify-it": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", "license": "MIT", "dependencies": { "uc.micro": "^2.0.0" @@ -17743,18 +17779,10 @@ "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/commander": { - "version": "14.0.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", - "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, "node_modules/listr": { "version": "0.14.3", + "resolved": "https://registry.npmjs.org/listr/-/listr-0.14.3.tgz", + "integrity": "sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA==", "license": "MIT", "dependencies": { "@samverschueren/stream-to-observable": "^0.3.0", @@ -17773,6 +17801,8 @@ }, "node_modules/listr-input": { "version": "0.1.3", + "resolved": "https://registry.npmjs.org/listr-input/-/listr-input-0.1.3.tgz", + "integrity": "sha512-dvjSD1MrWGXxxPixpMQlSBmkyqhJrPxGo30un25k/vlvFOWZj70AauU+YkEh7CA8vmpkE6Wde37DJDmqYqF39g==", "license": "MIT", "dependencies": { "inquirer": "^3.3.0", @@ -17785,6 +17815,8 @@ }, "node_modules/listr-input/node_modules/ansi-escapes": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "license": "MIT", "engines": { "node": ">=4" @@ -17792,6 +17824,8 @@ }, "node_modules/listr-input/node_modules/ansi-regex": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "license": "MIT", "engines": { "node": ">=4" @@ -17799,6 +17833,8 @@ }, "node_modules/listr-input/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -17809,6 +17845,8 @@ }, "node_modules/listr-input/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -17821,6 +17859,8 @@ }, "node_modules/listr-input/node_modules/cli-cursor": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" @@ -17831,10 +17871,14 @@ }, "node_modules/listr-input/node_modules/cli-width": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "license": "ISC" }, "node_modules/listr-input/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -17842,10 +17886,14 @@ }, "node_modules/listr-input/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/listr-input/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -17853,6 +17901,8 @@ }, "node_modules/listr-input/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -17860,6 +17910,8 @@ }, "node_modules/listr-input/node_modules/inquirer": { "version": "3.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", + "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", "license": "MIT", "dependencies": { "ansi-escapes": "^3.0.0", @@ -17880,6 +17932,8 @@ }, "node_modules/listr-input/node_modules/is-fullwidth-code-point": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "license": "MIT", "engines": { "node": ">=4" @@ -17887,6 +17941,8 @@ }, "node_modules/listr-input/node_modules/mimic-fn": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "license": "MIT", "engines": { "node": ">=4" @@ -17894,10 +17950,14 @@ }, "node_modules/listr-input/node_modules/mute-stream": { "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha512-r65nCZhrbXXb6dXOACihYApHw2Q6pV0M3V0PSxd74N0+D8nzAdEAITq2oAjA1jVnKI+tGvEBUpqiMh0+rW6zDQ==", "license": "ISC" }, "node_modules/listr-input/node_modules/onetime": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" @@ -17908,6 +17968,8 @@ }, "node_modules/listr-input/node_modules/restore-cursor": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "license": "MIT", "dependencies": { "onetime": "^2.0.0", @@ -17919,6 +17981,8 @@ }, "node_modules/listr-input/node_modules/rxjs": { "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", "license": "Apache-2.0", "dependencies": { "symbol-observable": "1.0.1" @@ -17929,10 +17993,14 @@ }, "node_modules/listr-input/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/listr-input/node_modules/string-width": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "license": "MIT", "dependencies": { "is-fullwidth-code-point": "^2.0.0", @@ -17944,6 +18012,8 @@ }, "node_modules/listr-input/node_modules/strip-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "license": "MIT", "dependencies": { "ansi-regex": "^3.0.0" @@ -17954,6 +18024,8 @@ }, "node_modules/listr-input/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -17964,6 +18036,8 @@ }, "node_modules/listr-input/node_modules/symbol-observable": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha512-Kb3PrPYz4HanVF1LVGuAdW6LoVgIwjUYJGzFe7NDrBLCN4lsV/5J0MFurV+ygS4bRVwrCEt2c7MQ1R2a72oJDw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17971,6 +18045,8 @@ }, "node_modules/listr-silent-renderer": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz", + "integrity": "sha512-L26cIFm7/oZeSNVhWB6faeorXhMg4HNlb/dS/7jHhr708jxlXrtrBWo4YUxZQkc6dGoxEAe6J/D3juTRBUzjtA==", "license": "MIT", "engines": { "node": ">=4" @@ -17978,6 +18054,8 @@ }, "node_modules/listr-update-renderer": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz", + "integrity": "sha512-tKRsZpKz8GSGqoI/+caPmfrypiaq+OQCbd+CovEC24uk1h952lVj5sC7SqyFUm+OaJ5HN/a1YLt5cit2FMNsFA==", "license": "MIT", "dependencies": { "chalk": "^1.1.3", @@ -17998,6 +18076,8 @@ }, "node_modules/listr-update-renderer/node_modules/ansi-escapes": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "license": "MIT", "engines": { "node": ">=4" @@ -18005,6 +18085,8 @@ }, "node_modules/listr-update-renderer/node_modules/ansi-regex": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18012,6 +18094,8 @@ }, "node_modules/listr-update-renderer/node_modules/ansi-styles": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18019,6 +18103,8 @@ }, "node_modules/listr-update-renderer/node_modules/chalk": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "license": "MIT", "dependencies": { "ansi-styles": "^2.2.1", @@ -18033,6 +18119,8 @@ }, "node_modules/listr-update-renderer/node_modules/cli-cursor": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" @@ -18043,6 +18131,8 @@ }, "node_modules/listr-update-renderer/node_modules/cli-truncate": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-0.2.1.tgz", + "integrity": "sha512-f4r4yJnbT++qUPI9NR4XLDLq41gQ+uqnPItWG0F5ZkehuNiTTa3EY0S4AqTSUOeJ7/zU41oWPQSNkW5BqPL9bg==", "license": "MIT", "dependencies": { "slice-ansi": "0.0.4", @@ -18054,6 +18144,8 @@ }, "node_modules/listr-update-renderer/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -18061,6 +18153,8 @@ }, "node_modules/listr-update-renderer/node_modules/figures": { "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5", @@ -18072,6 +18166,8 @@ }, "node_modules/listr-update-renderer/node_modules/is-fullwidth-code-point": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", "license": "MIT", "dependencies": { "number-is-nan": "^1.0.0" @@ -18082,6 +18178,8 @@ }, "node_modules/listr-update-renderer/node_modules/log-symbols": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-1.0.2.tgz", + "integrity": "sha512-mmPrW0Fh2fxOzdBbFv4g1m6pR72haFLPJ2G5SJEELf1y+iaQrDG6cWCPjy54RHYbZAt7X+ls690Kw62AdWXBzQ==", "license": "MIT", "dependencies": { "chalk": "^1.0.0" @@ -18092,6 +18190,8 @@ }, "node_modules/listr-update-renderer/node_modules/log-update": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-2.3.0.tgz", + "integrity": "sha512-vlP11XfFGyeNQlmEn9tJ66rEW1coA/79m5z6BCkudjbAGE83uhAcGYrBFwfs3AdLiLzGRusRPAbSPK9xZteCmg==", "license": "MIT", "dependencies": { "ansi-escapes": "^3.0.0", @@ -18104,6 +18204,8 @@ }, "node_modules/listr-update-renderer/node_modules/mimic-fn": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "license": "MIT", "engines": { "node": ">=4" @@ -18111,6 +18213,8 @@ }, "node_modules/listr-update-renderer/node_modules/onetime": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" @@ -18121,6 +18225,8 @@ }, "node_modules/listr-update-renderer/node_modules/restore-cursor": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "license": "MIT", "dependencies": { "onetime": "^2.0.0", @@ -18132,10 +18238,14 @@ }, "node_modules/listr-update-renderer/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/listr-update-renderer/node_modules/slice-ansi": { "version": "0.0.4", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", + "integrity": "sha512-up04hB2hR92PgjpyU3y/eg91yIBILyjVY26NvvciY3EVVPjybkMszMpXQ9QAkcS3I5rtJBDLoTxxg+qvW8c7rw==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18143,6 +18253,8 @@ }, "node_modules/listr-update-renderer/node_modules/string-width": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", "license": "MIT", "dependencies": { "code-point-at": "^1.0.0", @@ -18155,6 +18267,8 @@ }, "node_modules/listr-update-renderer/node_modules/strip-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", "license": "MIT", "dependencies": { "ansi-regex": "^2.0.0" @@ -18165,6 +18279,8 @@ }, "node_modules/listr-update-renderer/node_modules/supports-color": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -18172,6 +18288,8 @@ }, "node_modules/listr-update-renderer/node_modules/wrap-ansi": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-3.0.1.tgz", + "integrity": "sha512-iXR3tDXpbnTpzjKSylUJRkLuOrEC7hwEB221cgn6wtF8wpmz28puFXAEfPT5zrjM3wahygB//VuWEr1vTkDcNQ==", "license": "MIT", "dependencies": { "string-width": "^2.1.1", @@ -18183,6 +18301,8 @@ }, "node_modules/listr-update-renderer/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "license": "MIT", "engines": { "node": ">=4" @@ -18190,6 +18310,8 @@ }, "node_modules/listr-update-renderer/node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "license": "MIT", "engines": { "node": ">=4" @@ -18197,6 +18319,8 @@ }, "node_modules/listr-update-renderer/node_modules/wrap-ansi/node_modules/string-width": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "license": "MIT", "dependencies": { "is-fullwidth-code-point": "^2.0.0", @@ -18208,6 +18332,8 @@ }, "node_modules/listr-update-renderer/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "license": "MIT", "dependencies": { "ansi-regex": "^3.0.0" @@ -18218,6 +18344,8 @@ }, "node_modules/listr-verbose-renderer": { "version": "0.5.0", + "resolved": "https://registry.npmjs.org/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz", + "integrity": "sha512-04PDPqSlsqIOaaaGZ+41vq5FejI9auqTInicFRndCBgE3bXG8D6W1I+mWhk+1nqbHmyhla/6BUrd5OSiHwKRXw==", "license": "MIT", "dependencies": { "chalk": "^2.4.1", @@ -18231,6 +18359,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -18241,6 +18371,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -18253,6 +18385,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/cli-cursor": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", "license": "MIT", "dependencies": { "restore-cursor": "^2.0.0" @@ -18263,6 +18397,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -18270,10 +18406,14 @@ }, "node_modules/listr-verbose-renderer/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/listr-verbose-renderer/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -18281,6 +18421,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -18288,6 +18430,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/mimic-fn": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "license": "MIT", "engines": { "node": ">=4" @@ -18295,6 +18439,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/onetime": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "license": "MIT", "dependencies": { "mimic-fn": "^1.0.0" @@ -18305,6 +18451,8 @@ }, "node_modules/listr-verbose-renderer/node_modules/restore-cursor": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", "license": "MIT", "dependencies": { "onetime": "^2.0.0", @@ -18316,10 +18464,14 @@ }, "node_modules/listr-verbose-renderer/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/listr-verbose-renderer/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -18330,6 +18482,8 @@ }, "node_modules/listr/node_modules/is-stream": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18337,6 +18491,8 @@ }, "node_modules/listr/node_modules/p-map": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", + "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", "license": "MIT", "engines": { "node": ">=6" @@ -18344,6 +18500,8 @@ }, "node_modules/listr/node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" @@ -18354,6 +18512,8 @@ }, "node_modules/listr/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, "node_modules/listr2": { @@ -18468,6 +18628,8 @@ }, "node_modules/load-json-file": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", @@ -18481,6 +18643,8 @@ }, "node_modules/load-json-file/node_modules/parse-json": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "license": "MIT", "dependencies": { "error-ex": "^1.3.1", @@ -18492,6 +18656,8 @@ }, "node_modules/load-json-file/node_modules/strip-bom": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "license": "MIT", "engines": { "node": ">=4" @@ -18513,6 +18679,8 @@ }, "node_modules/loader-utils": { "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "dev": true, "license": "MIT", "engines": { @@ -18536,7 +18704,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.camelcase": { @@ -18548,6 +18718,8 @@ }, "node_modules/lodash.debounce": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", "dev": true, "license": "MIT" }, @@ -18560,6 +18732,8 @@ }, "node_modules/lodash.memoize": { "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true, "license": "MIT" }, @@ -18593,10 +18767,14 @@ }, "node_modules/lodash.zip": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.zip/-/lodash.zip-4.2.0.tgz", + "integrity": "sha512-C7IOaBBK/0gMORRBd8OETNx3kmOkgIWIPvyDpZSCTwUrpYmgZwJkjZeOD8ww4xbOUOs4/attY+pciKvadNfFbg==", "license": "MIT" }, "node_modules/log-symbols": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", "license": "MIT", "dependencies": { "chalk": "^2.0.1" @@ -18607,6 +18785,8 @@ }, "node_modules/log-symbols/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -18617,6 +18797,8 @@ }, "node_modules/log-symbols/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -18629,6 +18811,8 @@ }, "node_modules/log-symbols/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -18636,10 +18820,14 @@ }, "node_modules/log-symbols/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/log-symbols/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -18647,6 +18835,8 @@ }, "node_modules/log-symbols/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -18654,6 +18844,8 @@ }, "node_modules/log-symbols/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -18773,6 +18965,8 @@ }, "node_modules/loglevel": { "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", "dev": true, "license": "MIT", "engines": { @@ -18785,11 +18979,15 @@ }, "node_modules/loglevel-plugin-prefix": { "version": "0.8.4", + "resolved": "https://registry.npmjs.org/loglevel-plugin-prefix/-/loglevel-plugin-prefix-0.8.4.tgz", + "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", "dev": true, "license": "MIT" }, "node_modules/loud-rejection": { "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", "license": "MIT", "dependencies": { "currently-unhandled": "^0.4.1", @@ -18801,10 +18999,14 @@ }, "node_modules/loud-rejection/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/lowercase-keys": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -18812,6 +19014,8 @@ }, "node_modules/lru-cache": { "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { @@ -18820,11 +19024,15 @@ }, "node_modules/lunr": { "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true, "license": "MIT" }, "node_modules/macos-release": { "version": "2.5.1", + "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-2.5.1.tgz", + "integrity": "sha512-DXqXhEM7gW59OjZO8NIjBCz9AQ1BEMrfiOAl4AYByHCtVHRF4KoGNO8mqQeM8lRCtQe/UnJ4imO/d2HdkKsd+A==", "dev": true, "license": "MIT", "engines": { @@ -18835,11 +19043,13 @@ } }, "node_modules/magic-string": { - "version": "0.30.17", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/make-dir": { @@ -18860,6 +19070,8 @@ }, "node_modules/make-error": { "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true, "license": "ISC" }, @@ -18887,16 +19099,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/make-fetch-happen/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -18909,6 +19111,8 @@ }, "node_modules/map-obj": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha512-TzQSV2DiMYgoF5RycneKVUzIa9bQsj/B3tTgsE3dOGqlzHnGIDaC7XBE7grnA+8kZPnfqSGFe95VHc2oc0VFUQ==", "license": "MIT", "engines": { "node": ">=4" @@ -18916,6 +19120,8 @@ }, "node_modules/map-stream": { "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", "dev": true, "license": "MIT" }, @@ -18938,6 +19144,8 @@ }, "node_modules/markdown-it-anchor": { "version": "9.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-9.2.0.tgz", + "integrity": "sha512-sa2ErMQ6kKOA4l31gLGYliFQrMKkqSO0ZJgGhDHKijPf0pNFM9vghjAh3gn26pS4JDRs7Iwa9S36gxm3vgZTzg==", "license": "Unlicense", "peerDependencies": { "@types/markdown-it": "*", @@ -18946,14 +19154,20 @@ }, "node_modules/markdown-it-toc-done-right": { "version": "4.2.0", + "resolved": "https://registry.npmjs.org/markdown-it-toc-done-right/-/markdown-it-toc-done-right-4.2.0.tgz", + "integrity": "sha512-UB/IbzjWazwTlNAX0pvWNlJS8NKsOQ4syrXZQ/C72j+jirrsjVRT627lCaylrKJFBQWfRsPmIVQie8x38DEhAQ==", "license": "MIT" }, "node_modules/markdown-it-video": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/markdown-it-video/-/markdown-it-video-0.6.3.tgz", + "integrity": "sha512-T4th1kwy0OcvyWSN4u3rqPGxvbDclpucnVSSaH3ZacbGsAts964dxokx9s/I3GYsrDCJs4ogtEeEeVP18DQj0Q==", "license": "MIT" }, "node_modules/marked": { "version": "7.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.3.tgz", + "integrity": "sha512-ev2uM40p0zQ/GbvqotfKcSWEa59fJwluGZj5dcaUOwDRrB1F3dncdXy8NWUApk4fi8atU3kTBOwjyjZ0ud0dxw==", "dev": true, "license": "MIT", "bin": { @@ -18965,6 +19179,8 @@ }, "node_modules/matchit": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/matchit/-/matchit-1.1.0.tgz", + "integrity": "sha512-+nGYoOlfHmxe5BW5tE0EMJppXEwdSf8uBA1GTZC7Q77kbT35+VKLYJMzVNWCHSsga1ps1tPYFtFyvxvKzWVmMA==", "dev": true, "license": "MIT", "dependencies": { @@ -18976,17 +19192,31 @@ }, "node_modules/math-intrinsics": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "license": "MIT", "engines": { "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0", + "peer": true + }, "node_modules/mdurl": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", "license": "MIT" }, "node_modules/media-typer": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -19049,11 +19279,15 @@ }, "node_modules/merge-stream": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", "dev": true, "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, "license": "MIT", "engines": { @@ -19072,6 +19306,8 @@ }, "node_modules/micromatch": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -19083,7 +19319,9 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -19107,26 +19345,34 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "dev": true, + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "dev": true, + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-fn": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, "license": "MIT", "engines": { @@ -19135,6 +19381,8 @@ }, "node_modules/mimic-function": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, "license": "MIT", "engines": { @@ -19146,6 +19394,8 @@ }, "node_modules/mimic-response": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "license": "MIT", "engines": { "node": ">=4" @@ -19180,14 +19430,16 @@ "license": "ISC" }, "node_modules/minimatch": { - "version": "9.0.5", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -19195,6 +19447,8 @@ }, "node_modules/minimist": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -19202,6 +19456,8 @@ }, "node_modules/minimist-options": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", "license": "MIT", "dependencies": { "arrify": "^1.0.1", @@ -19211,6 +19467,15 @@ "node": ">= 4" } }, + "node_modules/minimist-options/node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -19346,10 +19611,27 @@ }, "node_modules/mitt": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/morgan": { "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", "dev": true, "license": "MIT", "dependencies": { @@ -19365,6 +19647,8 @@ }, "node_modules/morgan/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -19373,11 +19657,15 @@ }, "node_modules/morgan/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, "node_modules/morgan/node_modules/on-finished": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, "license": "MIT", "dependencies": { @@ -19389,6 +19677,8 @@ }, "node_modules/mrmime": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, "license": "MIT", "engines": { @@ -19397,6 +19687,8 @@ }, "node_modules/ms": { "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, "node_modules/msgpackr": { @@ -19459,6 +19751,8 @@ }, "node_modules/nanoid": { "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -19491,6 +19785,8 @@ }, "node_modules/natural-compare": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, "license": "MIT" }, @@ -19526,9 +19822,10 @@ "node": ">=0.10.0" } }, - "node_modules/negotiator": { - "version": "0.6.3", - "dev": true, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -19536,11 +19833,15 @@ }, "node_modules/neo-async": { "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true, "license": "MIT" }, "node_modules/neotraverse": { "version": "0.6.18", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", + "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", "dev": true, "license": "MIT", "engines": { @@ -19591,6 +19892,8 @@ }, "node_modules/ngx-markdown-editor": { "version": "5.3.4", + "resolved": "https://registry.npmjs.org/ngx-markdown-editor/-/ngx-markdown-editor-5.3.4.tgz", + "integrity": "sha512-YFp06lIWlh67tbww2y7rtEkQmClC4LHmr+oejUZ0OBIdgh7/B+0v/FiEzIAshPCocC2uPF4VHuNJZG7dFn42xA==", "license": "Apache License 2.0", "dependencies": { "tslib": "^2.3.0" @@ -19603,6 +19906,8 @@ }, "node_modules/nice-try": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "license": "MIT" }, "node_modules/node-addon-api": { @@ -19615,6 +19920,8 @@ }, "node_modules/node-fetch": { "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", "license": "MIT", "dependencies": { "whatwg-url": "^5.0.0" @@ -19633,14 +19940,20 @@ }, "node_modules/node-fetch/node_modules/tr46": { "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, "node_modules/node-fetch/node_modules/webidl-conversions": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, "node_modules/node-fetch/node_modules/whatwg-url": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -19722,7 +20035,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -19744,6 +20059,8 @@ }, "node_modules/normalize-package-data": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", @@ -19754,10 +20071,14 @@ }, "node_modules/normalize-package-data/node_modules/hosted-git-info": { "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "license": "ISC" }, "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "license": "ISC", "bin": { "semver": "bin/semver" @@ -19765,6 +20086,8 @@ }, "node_modules/normalize-path": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", "engines": { @@ -19773,6 +20096,8 @@ }, "node_modules/normalize-url": { "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", "license": "MIT", "engines": { "node": ">=8" @@ -19780,6 +20105,8 @@ }, "node_modules/np": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/np/-/np-3.1.0.tgz", + "integrity": "sha512-3HTje97SzbsvK9g61C72PpDk9AloaaTn0K7xHbx7jMrs9vJtCZqu7TWUGxrcYGiKRO/uFRn5SiRZfYB/gpL9Iw==", "license": "MIT", "dependencies": { "@samverschueren/stream-to-observable": "^0.3.0", @@ -19814,6 +20141,8 @@ }, "node_modules/np/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -19824,6 +20153,8 @@ }, "node_modules/np/node_modules/camelcase": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha512-FxAv7HpHrXbh3aPo4o2qxHay2lkLY3x5Mw3KeE4KQE8ysVfziWeRZDwcjauvwBSGEC/nXUPzZy8zeh4HokqOnw==", "license": "MIT", "engines": { "node": ">=4" @@ -19831,6 +20162,8 @@ }, "node_modules/np/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -19843,6 +20176,8 @@ }, "node_modules/np/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -19850,10 +20185,14 @@ }, "node_modules/np/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/np/node_modules/cross-spawn": { "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "license": "MIT", "dependencies": { "nice-try": "^1.0.4", @@ -19868,6 +20207,8 @@ }, "node_modules/np/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -19875,6 +20216,8 @@ }, "node_modules/np/node_modules/execa": { "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", "license": "MIT", "dependencies": { "cross-spawn": "^6.0.0", @@ -19891,6 +20234,8 @@ }, "node_modules/np/node_modules/get-stream": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "license": "MIT", "engines": { "node": ">=4" @@ -19898,6 +20243,8 @@ }, "node_modules/np/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -19905,6 +20252,8 @@ }, "node_modules/np/node_modules/is-stream": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -19912,6 +20261,8 @@ }, "node_modules/np/node_modules/meow": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz", + "integrity": "sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig==", "license": "MIT", "dependencies": { "camelcase-keys": "^4.0.0", @@ -19930,6 +20281,8 @@ }, "node_modules/np/node_modules/npm-run-path": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "license": "MIT", "dependencies": { "path-key": "^2.0.0" @@ -19940,6 +20293,8 @@ }, "node_modules/np/node_modules/path-key": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "license": "MIT", "engines": { "node": ">=4" @@ -19947,6 +20302,8 @@ }, "node_modules/np/node_modules/rxjs": { "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^1.9.0" @@ -19957,6 +20314,8 @@ }, "node_modules/np/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "license": "ISC", "bin": { "semver": "bin/semver" @@ -19964,6 +20323,8 @@ }, "node_modules/np/node_modules/shebang-command": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" @@ -19974,6 +20335,8 @@ }, "node_modules/np/node_modules/shebang-regex": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -19981,10 +20344,14 @@ }, "node_modules/np/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/np/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -19995,10 +20362,14 @@ }, "node_modules/np/node_modules/tslib": { "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, "node_modules/np/node_modules/which": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -20009,6 +20380,8 @@ }, "node_modules/np/node_modules/yargs-parser": { "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", "license": "ISC", "dependencies": { "camelcase": "^4.1.0" @@ -20042,6 +20415,8 @@ }, "node_modules/npm-name": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/npm-name/-/npm-name-5.5.0.tgz", + "integrity": "sha512-l7/uyVfEi2e3ho+ovaJZC0xlbwzXNUz3RxkxpfcnLuoGKAuYoo9YoJ/uy18PsTD8IziugGHks4t/mGmBJEZ4Qg==", "license": "MIT", "dependencies": { "got": "^9.6.0", @@ -20058,6 +20433,8 @@ }, "node_modules/npm-name/node_modules/is-scoped": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-scoped/-/is-scoped-2.1.0.tgz", + "integrity": "sha512-Cv4OpPTHAK9kHYzkzCrof3VJh7H/PrG2MBUMvvJebaaUMbqhm0YAtXnvh0I3Hnj2tMZWwrRROWLSgfJrKqWmlQ==", "license": "MIT", "dependencies": { "scoped-regex": "^2.0.0" @@ -20068,6 +20445,8 @@ }, "node_modules/npm-name/node_modules/scoped-regex": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/scoped-regex/-/scoped-regex-2.1.0.tgz", + "integrity": "sha512-g3WxHrqSWCZHGHlSrF51VXFdjImhwvH8ZO/pryFH56Qi0cDsZfylQa/t0jCzVQFNbNvM00HfHjkDPEuarKDSWQ==", "license": "MIT", "engines": { "node": ">=8" @@ -20161,6 +20540,8 @@ }, "node_modules/npm-run-path": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "license": "MIT", "dependencies": { @@ -20172,6 +20553,8 @@ }, "node_modules/nth-check": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -20183,6 +20566,8 @@ }, "node_modules/number-is-nan": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20206,6 +20591,8 @@ }, "node_modules/object-inspect": { "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -20223,6 +20610,8 @@ }, "node_modules/on-finished": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -20233,6 +20622,8 @@ }, "node_modules/on-headers": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", "dev": true, "license": "MIT", "engines": { @@ -20241,6 +20632,8 @@ }, "node_modules/once": { "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "license": "ISC", "dependencies": { "wrappy": "1" @@ -20248,6 +20641,8 @@ }, "node_modules/onetime": { "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, "license": "MIT", "dependencies": { @@ -20283,6 +20678,8 @@ }, "node_modules/opencollective-postinstall": { "version": "2.0.3", + "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", + "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", "dev": true, "license": "MIT", "bin": { @@ -20291,6 +20688,8 @@ }, "node_modules/optionator": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", "dependencies": { @@ -20328,17 +20727,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/cli-spinners": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-3.4.0.tgz", - "integrity": "sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==", + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.20" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/ora/node_modules/log-symbols": { @@ -20358,36 +20757,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ora/node_modules/stdin-discarder": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", - "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/string-width": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", - "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ordered-binary": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.6.1.tgz", @@ -20398,6 +20767,8 @@ }, "node_modules/os-name": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/os-name/-/os-name-4.0.1.tgz", + "integrity": "sha512-xl9MAoU97MH1Xt5K9ERft2YfCAoaO6msy1OBA0ozxEC0x0TmIoE6K3QvgJMMZA9yKGLmHXNY/YZoDbiGDj4zYw==", "dev": true, "license": "MIT", "dependencies": { @@ -20413,6 +20784,8 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20420,6 +20793,8 @@ }, "node_modules/p-cancelable": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "license": "MIT", "engines": { "node": ">=6" @@ -20427,6 +20802,8 @@ }, "node_modules/p-finally": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", "license": "MIT", "engines": { "node": ">=4" @@ -20466,6 +20843,8 @@ }, "node_modules/p-map": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", "license": "MIT", "engines": { "node": ">=4" @@ -20501,6 +20880,8 @@ }, "node_modules/p-timeout": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "license": "MIT", "dependencies": { "p-finally": "^1.0.0" @@ -20511,6 +20892,8 @@ }, "node_modules/p-try": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, "license": "MIT", "engines": { @@ -20519,6 +20902,8 @@ }, "node_modules/package-json": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha512-q/R5GrMek0vzgoomq6rm9OX+3PQve8sLwTirmK30YB3Cu0Bbt9OX9M/SIUnroN5BGJkzwGsFwDaRGD9EwBOlCA==", "license": "MIT", "dependencies": { "got": "^6.7.1", @@ -20532,11 +20917,15 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/package-json/node_modules/get-stream": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "license": "MIT", "engines": { "node": ">=4" @@ -20544,6 +20933,8 @@ }, "node_modules/package-json/node_modules/got": { "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha512-Y/K3EDuiQN9rTZhBvPRWMLXIKdeD1Rj0nzunfoi0Yyn5WBEbzxXKU9Ub2X41oZBagVWOBU3MuDonFMgPWQFnwg==", "license": "MIT", "dependencies": { "create-error-class": "^3.0.0", @@ -20564,6 +20955,8 @@ }, "node_modules/package-json/node_modules/is-stream": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20571,6 +20964,8 @@ }, "node_modules/package-json/node_modules/prepend-http": { "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20578,6 +20973,8 @@ }, "node_modules/package-json/node_modules/registry-auth-token": { "version": "3.4.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", + "integrity": "sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A==", "license": "MIT", "dependencies": { "rc": "^1.1.6", @@ -20586,6 +20983,8 @@ }, "node_modules/package-json/node_modules/registry-url": { "version": "3.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz", + "integrity": "sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==", "license": "MIT", "dependencies": { "rc": "^1.0.1" @@ -20596,6 +20995,8 @@ }, "node_modules/package-json/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "license": "ISC", "bin": { "semver": "bin/semver" @@ -20603,6 +21004,8 @@ }, "node_modules/package-json/node_modules/url-parse-lax": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", "license": "MIT", "dependencies": { "prepend-http": "^1.0.1" @@ -20645,6 +21048,8 @@ }, "node_modules/parent-module": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", "dependencies": { @@ -20656,6 +21061,8 @@ }, "node_modules/parse-json": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -20673,6 +21080,8 @@ }, "node_modules/parse-json/node_modules/json-parse-even-better-errors": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true, "license": "MIT" }, @@ -20687,8 +21096,9 @@ } }, "node_modules/parse5": { - "version": "7.3.0", - "dev": true, + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "license": "MIT", "dependencies": { "entities": "^6.0.0" @@ -20725,56 +21135,60 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/parse5-html-rewriting-stream/node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^6.0.0" + "domhandler": "^5.0.3", + "parse5": "^7.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { - "parse5": "^7.0.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-sax-parser": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", - "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", "dev": true, "license": "MIT", "dependencies": { - "parse5": "^8.0.0" + "parse5": "^7.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-sax-parser/node_modules/entities": { + "node_modules/parse5-parser-stream/node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", @@ -20787,10 +21201,10 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/parse5-sax-parser/node_modules/parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", - "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "dev": true, "license": "MIT", "dependencies": { @@ -20800,9 +21214,23 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parse5-sax-parser": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", + "integrity": "sha512-/dQ8UzHZwnrzs3EvDj6IkKrD/jIZyTlB+8XrHJvcjNgRdmWruNdN9i9RK/JtxakmlUdPwKubKPTCqvbTgzGhrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "dev": true, + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -20813,6 +21241,8 @@ }, "node_modules/parseurl": { "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -20820,6 +21250,8 @@ }, "node_modules/path-browserify": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", "dev": true, "license": "MIT" }, @@ -20835,6 +21267,8 @@ }, "node_modules/path-is-absolute": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20842,10 +21276,14 @@ }, "node_modules/path-is-inside": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", "license": "(WTFPL OR MIT)" }, "node_modules/path-key": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", "engines": { @@ -20854,10 +21292,14 @@ }, "node_modules/path-parse": { "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -20865,14 +21307,16 @@ "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -20889,8 +21333,22 @@ "url": "https://opencollective.com/express" } }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/pause-stream": { "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", "dev": true, "license": [ "MIT", @@ -20902,6 +21360,8 @@ }, "node_modules/picocolors": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -20919,6 +21379,8 @@ }, "node_modules/pify": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "license": "MIT", "engines": { "node": ">=4" @@ -20926,6 +21388,8 @@ }, "node_modules/pinkie": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20933,6 +21397,8 @@ }, "node_modules/pinkie-promise": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "license": "MIT", "dependencies": { "pinkie": "^2.0.0" @@ -21063,6 +21529,8 @@ }, "node_modules/pngjs": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", "dev": true, "license": "MIT", "engines": { @@ -21071,6 +21539,8 @@ }, "node_modules/polka": { "version": "0.5.2", + "resolved": "https://registry.npmjs.org/polka/-/polka-0.5.2.tgz", + "integrity": "sha512-FVg3vDmCqP80tOrs+OeNlgXYmFppTXdjD5E7I4ET1NjvtNmQrb1/mJibybKkb/d4NA7YWAr1ojxuhpL3FHqdlw==", "dev": true, "license": "MIT", "dependencies": { @@ -21271,6 +21741,8 @@ }, "node_modules/prelude-ls": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", "engines": { @@ -21279,6 +21751,8 @@ }, "node_modules/prepend-http": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "license": "MIT", "engines": { "node": ">=4" @@ -21328,12 +21802,29 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/primeflex": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/primeflex/-/primeflex-4.0.0.tgz", + "integrity": "sha512-UOEZCRjR36+sm5bUpDhS1xbA068l9VC6y1aTNVqQPtXuKIdPTqAWHRUxj3mKAoPrQ9W373ooJJMgNVXfiaw04g==", "license": "MIT" }, "node_modules/primeicons": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/primeicons/-/primeicons-7.0.0.tgz", + "integrity": "sha512-jK3Et9UzwzTsd6tzl2RmwrVY/b8raJ3QZLzoDACj+oTJ0oX7L9Hy+XnVwgo4QVKlKpnP/Ur13SXV/pVh4LzaDw==", "license": "MIT" }, "node_modules/primeng": { @@ -21358,29 +21849,10 @@ "rxjs": "^6.0.0 || ^7.8.1" } }, - "node_modules/primeng/node_modules/@primeuix/styled": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.4.tgz", - "integrity": "sha512-QSO/NpOQg8e9BONWRBx9y8VGMCMYz0J/uKfNJEya/RGEu7ARx0oYW0ugI1N3/KB1AAvyGxzKBzGImbwg0KUiOQ==", - "license": "MIT", - "dependencies": { - "@primeuix/utils": "^0.6.1" - }, - "engines": { - "node": ">=12.11.0" - } - }, - "node_modules/primeng/node_modules/@primeuix/utils": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.4.tgz", - "integrity": "sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==", - "license": "MIT", - "engines": { - "node": ">=12.11.0" - } - }, "node_modules/prismjs": { "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", "dev": true, "license": "MIT", "engines": { @@ -21451,10 +21923,14 @@ }, "node_modules/pseudomap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", "license": "ISC" }, "node_modules/pump": { - "version": "3.0.3", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -21473,6 +21949,8 @@ }, "node_modules/punycode.js": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", "license": "MIT", "engines": { "node": ">=6" @@ -21517,6 +21995,8 @@ }, "node_modules/qrcode": { "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -21533,6 +22013,8 @@ }, "node_modules/qrcode/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -21541,6 +22023,8 @@ }, "node_modules/qrcode/node_modules/cliui": { "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", "dev": true, "license": "ISC", "dependencies": { @@ -21549,13 +22033,10 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/qrcode/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/qrcode/node_modules/find-up": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "license": "MIT", "dependencies": { @@ -21568,6 +22049,8 @@ }, "node_modules/qrcode/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -21576,6 +22059,8 @@ }, "node_modules/qrcode/node_modules/locate-path": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "license": "MIT", "dependencies": { @@ -21587,6 +22072,8 @@ }, "node_modules/qrcode/node_modules/p-limit": { "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "license": "MIT", "dependencies": { @@ -21601,6 +22088,8 @@ }, "node_modules/qrcode/node_modules/p-locate": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "license": "MIT", "dependencies": { @@ -21612,6 +22101,8 @@ }, "node_modules/qrcode/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -21625,6 +22116,8 @@ }, "node_modules/qrcode/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -21636,11 +22129,15 @@ }, "node_modules/qrcode/node_modules/y18n": { "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "dev": true, "license": "ISC" }, "node_modules/qrcode/node_modules/yargs": { "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", "dev": true, "license": "MIT", "dependencies": { @@ -21662,6 +22159,8 @@ }, "node_modules/qrcode/node_modules/yargs-parser": { "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", "dev": true, "license": "ISC", "dependencies": { @@ -21673,7 +22172,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -21687,6 +22188,8 @@ }, "node_modules/queue-microtask": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true, "funding": [ { @@ -21706,6 +22209,8 @@ }, "node_modules/quick-lru": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha512-tRS7sTgyxMXtLum8L65daJnHUhfDUgboRdcWW2bR9vBfrj2+O5HSMbQOJfJJjIVSPFqbBCF37FpwWXGitDc5tA==", "license": "MIT", "engines": { "node": ">=4" @@ -21713,6 +22218,8 @@ }, "node_modules/range-parser": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -21720,6 +22227,8 @@ }, "node_modules/raw-body": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -21733,6 +22242,8 @@ }, "node_modules/rc": { "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", @@ -21746,10 +22257,14 @@ }, "node_modules/rc/node_modules/ini": { "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "license": "ISC" }, "node_modules/rc/node_modules/strip-json-comments": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21764,6 +22279,8 @@ }, "node_modules/read-pkg": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", "license": "MIT", "dependencies": { "load-json-file": "^4.0.0", @@ -21776,6 +22293,8 @@ }, "node_modules/read-pkg-up": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", "license": "MIT", "dependencies": { "find-up": "^2.0.0", @@ -21787,6 +22306,8 @@ }, "node_modules/read-pkg-up/node_modules/find-up": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "license": "MIT", "dependencies": { "locate-path": "^2.0.0" @@ -21797,6 +22318,8 @@ }, "node_modules/read-pkg-up/node_modules/locate-path": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "license": "MIT", "dependencies": { "p-locate": "^2.0.0", @@ -21808,6 +22331,8 @@ }, "node_modules/read-pkg-up/node_modules/p-limit": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "license": "MIT", "dependencies": { "p-try": "^1.0.0" @@ -21818,6 +22343,8 @@ }, "node_modules/read-pkg-up/node_modules/p-locate": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "license": "MIT", "dependencies": { "p-limit": "^1.1.0" @@ -21828,6 +22355,8 @@ }, "node_modules/read-pkg-up/node_modules/p-try": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "license": "MIT", "engines": { "node": ">=4" @@ -21835,21 +22364,13 @@ }, "node_modules/read-pkg-up/node_modules/path-exists": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/read-pkg/node_modules/path-type": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -21881,6 +22402,8 @@ }, "node_modules/redent": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha512-XNwrTx77JQCEMXTeb8movBKuK75MgH0RZkujNuDKCezemx/voapl9i2gCSi8WWm8+ox5ycJi1gxF22fR7c0Ciw==", "license": "MIT", "dependencies": { "indent-string": "^3.0.0", @@ -21892,16 +22415,22 @@ }, "node_modules/reflect-metadata": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", "dev": true, "license": "Apache-2.0" }, "node_modules/regenerate": { "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true, "license": "MIT" }, "node_modules/regenerate-unicode-properties": { "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "dev": true, "license": "MIT", "dependencies": { @@ -21913,11 +22442,15 @@ }, "node_modules/regex-parser": { "version": "2.3.1", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", + "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", "dev": true, "license": "MIT" }, "node_modules/regexpu-core": { "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, "license": "MIT", "dependencies": { @@ -21934,6 +22467,8 @@ }, "node_modules/registry-auth-token": { "version": "4.2.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", + "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", "license": "MIT", "dependencies": { "rc": "1.2.8" @@ -21944,6 +22479,8 @@ }, "node_modules/registry-url": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", "license": "MIT", "dependencies": { "rc": "^1.2.8" @@ -21954,11 +22491,15 @@ }, "node_modules/regjsgen": { "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", "dev": true, "license": "MIT" }, "node_modules/regjsparser": { "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -21970,6 +22511,8 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, "license": "MIT", "engines": { @@ -21978,6 +22521,8 @@ }, "node_modules/require-from-string": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { @@ -21986,11 +22531,15 @@ }, "node_modules/require-main-filename": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", "dev": true, "license": "ISC" }, "node_modules/requires-port": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true, "license": "MIT" }, @@ -22039,6 +22588,8 @@ }, "node_modules/resolve-url-loader": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -22054,6 +22605,8 @@ }, "node_modules/resolve-url-loader/node_modules/loader-utils": { "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "license": "MIT", "dependencies": { @@ -22067,6 +22620,8 @@ }, "node_modules/resolve-url-loader/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -22075,6 +22630,8 @@ }, "node_modules/responselike": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", "license": "MIT", "dependencies": { "lowercase-keys": "^1.0.0" @@ -22082,6 +22639,8 @@ }, "node_modules/restore-cursor": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, "license": "MIT", "dependencies": { @@ -22097,6 +22656,8 @@ }, "node_modules/restore-cursor/node_modules/onetime": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -22121,6 +22682,8 @@ }, "node_modules/reusify": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -22137,6 +22700,9 @@ }, "node_modules/rimraf": { "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -22145,8 +22711,16 @@ "rimraf": "bin.js" } }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, "node_modules/rimraf/node_modules/brace-expansion": { "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -22155,6 +22729,9 @@ }, "node_modules/rimraf/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -22172,7 +22749,9 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -22302,6 +22881,8 @@ }, "node_modules/run-async": { "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "license": "MIT", "engines": { "node": ">=0.12.0" @@ -22309,6 +22890,8 @@ }, "node_modules/run-parallel": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, "funding": [ { @@ -22330,41 +22913,37 @@ } }, "node_modules/rx-lite": { - "version": "4.0.8" + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", + "integrity": "sha512-Cun9QucwK6MIrp3mry/Y7hqD1oFqTYLQ4pGxaHTjIdaFDWRGGLikqp6u8LcWJnzpoALg9hap+JGk8sFIUuEGNA==" }, "node_modules/rx-lite-aggregates": { "version": "4.0.8", + "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha512-3xPNZGW93oCjiO7PtKxRK6iOVYBWBvtf9QHDfU23Oc+dLIQmAV//UnyXV/yihv81VS/UqoQPk4NegS8EFi55Hg==", "dependencies": { "rx-lite": "*" } }, "node_modules/rxjs": { "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, "node_modules/sass": { @@ -22523,6 +23102,8 @@ }, "node_modules/scoped-regex": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/scoped-regex/-/scoped-regex-1.0.0.tgz", + "integrity": "sha512-90/gFvaP4jXL0rXPD8FS7tWgmkQDlxCjs9cs3r3G5hAnrODt94kIh4SDbH/gm3HosGTik0omdSPOh0KQyGqjlg==", "license": "MIT", "engines": { "node": ">=4" @@ -22564,6 +23145,8 @@ }, "node_modules/semver-diff": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz", + "integrity": "sha512-gL8F8L4ORwsS0+iQ34yCYv///jsOq0ZL7WP55d1HnJ32o7tyFYEFQZQA22mrLIacZdU6xecaBBZ+uEiffGNyXw==", "license": "MIT", "dependencies": { "semver": "^5.0.3" @@ -22574,6 +23157,8 @@ }, "node_modules/semver-diff/node_modules/semver": { "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "license": "ISC", "bin": { "semver": "bin/semver" @@ -22605,56 +23190,57 @@ "url": "https://opencollective.com/express" } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", + "node_modules/serialize-javascript": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 0.6" + "node": ">=20.0.0" } }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.2", + "node_modules/serve-index": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", + "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", + "dev": true, "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "accepts": "~1.3.8", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.8.0", + "mime-types": "~2.1.35", + "parseurl": "~1.3.3" }, "engines": { - "node": ">=18" + "node": ">= 0.8.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/express" } }, - "node_modules/serialize-javascript": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", - "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", + "node_modules/serve-index/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, "license": "MIT", "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 0.6" } }, "node_modules/serve-index/node_modules/debug": { "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, "license": "MIT", "dependencies": { @@ -22663,6 +23249,8 @@ }, "node_modules/serve-index/node_modules/depd": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, "license": "MIT", "engines": { @@ -22670,36 +23258,66 @@ } }, "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", "dev": true, "license": "MIT", "dependencies": { "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.6" } }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", + "node_modules/serve-index/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/ms": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, "license": "MIT" }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", + "node_modules/serve-index/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "dev": true, - "license": "ISC" + "license": "MIT", + "engines": { + "node": ">= 0.6" + } }, "node_modules/serve-index/node_modules/statuses": { "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, "license": "MIT", "engines": { @@ -22727,15 +23345,21 @@ }, "node_modules/set-blocking": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true, "license": "ISC" }, "node_modules/setprototypeof": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, "license": "MIT", "dependencies": { @@ -22747,6 +23371,8 @@ }, "node_modules/shebang-command": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", "dependencies": { @@ -22758,6 +23384,8 @@ }, "node_modules/shebang-regex": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { @@ -22779,6 +23407,8 @@ }, "node_modules/side-channel": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -22796,6 +23426,8 @@ }, "node_modules/side-channel-list": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -22810,6 +23442,8 @@ }, "node_modules/side-channel-map": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -22826,6 +23460,8 @@ }, "node_modules/side-channel-weakmap": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -22843,6 +23479,8 @@ }, "node_modules/signal-exit": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, "license": "ISC", "engines": { @@ -22872,6 +23510,8 @@ }, "node_modules/sirv": { "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -22885,6 +23525,8 @@ }, "node_modules/sirv/node_modules/@polka/url": { "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", "dev": true, "license": "MIT" }, @@ -22992,15 +23634,19 @@ } }, "node_modules/source-map": { - "version": "0.7.4", + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, "license": "BSD-3-Clause", "engines": { - "node": ">= 8" + "node": ">= 12" } }, "node_modules/source-map-explorer": { "version": "2.5.3", + "resolved": "https://registry.npmjs.org/source-map-explorer/-/source-map-explorer-2.5.3.tgz", + "integrity": "sha512-qfUGs7UHsOBE5p/lGfQdaAj/5U/GWYBw2imEpD6UQNkqElYonkow8t+HBL1qqIl3CuGZx7n8/CQo4x1HwSHhsg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -23027,28 +23673,25 @@ }, "node_modules/source-map-explorer/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/source-map-explorer/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/source-map-explorer/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } + "license": "MIT" }, "node_modules/source-map-explorer/node_modules/brace-expansion": { "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -23056,23 +23699,10 @@ "concat-map": "0.0.1" } }, - "node_modules/source-map-explorer/node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/source-map-explorer/node_modules/cliui": { "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, "license": "ISC", "dependencies": { @@ -23081,13 +23711,11 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/source-map-explorer/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/source-map-explorer/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -23107,6 +23735,8 @@ }, "node_modules/source-map-explorer/node_modules/is-docker": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, "license": "MIT", "bin": { @@ -23121,6 +23751,8 @@ }, "node_modules/source-map-explorer/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -23129,6 +23761,8 @@ }, "node_modules/source-map-explorer/node_modules/is-wsl": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, "license": "MIT", "dependencies": { @@ -23139,7 +23773,9 @@ } }, "node_modules/source-map-explorer/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -23151,6 +23787,8 @@ }, "node_modules/source-map-explorer/node_modules/open": { "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -23166,6 +23804,8 @@ }, "node_modules/source-map-explorer/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -23179,6 +23819,8 @@ }, "node_modules/source-map-explorer/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -23190,6 +23832,8 @@ }, "node_modules/source-map-explorer/node_modules/wrap-ansi": { "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -23206,6 +23850,8 @@ }, "node_modules/source-map-explorer/node_modules/yargs": { "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "license": "MIT", "dependencies": { @@ -23223,6 +23869,8 @@ }, "node_modules/source-map-explorer/node_modules/yargs-parser": { "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "license": "ISC", "engines": { @@ -23231,6 +23879,8 @@ }, "node_modules/source-map-js": { "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -23238,6 +23888,8 @@ }, "node_modules/source-map-loader": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "dev": true, "license": "MIT", "dependencies": { @@ -23257,6 +23909,8 @@ }, "node_modules/source-map-loader/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -23268,6 +23922,8 @@ }, "node_modules/source-map-support": { "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, "license": "MIT", "dependencies": { @@ -23277,6 +23933,8 @@ }, "node_modules/source-map-support/node_modules/source-map": { "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -23285,18 +23943,35 @@ }, "node_modules/spdx-correct": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, + "node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/spdx-exceptions": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { - "version": "3.0.1", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -23304,7 +23979,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.22", + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", "license": "CC0-1.0" }, "node_modules/spdy": { @@ -23341,6 +24018,8 @@ }, "node_modules/split": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "license": "MIT", "dependencies": { "through": "2" @@ -23394,13 +24073,17 @@ }, "node_modules/statuses": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/stdin-discarder": { - "version": "0.2.2", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.3.1.tgz", + "integrity": "sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==", "dev": true, "license": "MIT", "engines": { @@ -23412,6 +24095,8 @@ }, "node_modules/stream-combiner": { "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", "dev": true, "license": "MIT", "dependencies": { @@ -23429,8 +24114,31 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/string-argv": { "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, "license": "MIT", "engines": { @@ -23475,16 +24183,17 @@ } }, "node_modules/string-width": { - "version": "5.1.2", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", "dev": true, "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -23493,6 +24202,8 @@ "node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -23506,19 +24217,18 @@ }, "node_modules/string-width-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -23527,6 +24237,8 @@ }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -23537,11 +24249,13 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -23553,6 +24267,8 @@ "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -23564,6 +24280,8 @@ }, "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { @@ -23582,6 +24300,8 @@ }, "node_modules/strip-eof": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -23589,6 +24309,8 @@ }, "node_modules/strip-final-newline": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, "license": "MIT", "engines": { @@ -23597,6 +24319,8 @@ }, "node_modules/strip-indent": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA==", "license": "MIT", "engines": { "node": ">=4" @@ -23617,6 +24341,8 @@ }, "node_modules/supports-color": { "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", "dependencies": { @@ -23628,6 +24354,8 @@ }, "node_modules/supports-hyperlinks": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz", + "integrity": "sha512-HHi5kVSefKaJkGYXbDuKbUGRVxqnWGn3J2e39CYcNJEfWciGq2zYtOhXLTlvrOZW1QU7VX67w7fMmWafHX9Pfw==", "license": "MIT", "dependencies": { "has-flag": "^2.0.0", @@ -23639,6 +24367,8 @@ }, "node_modules/supports-hyperlinks/node_modules/has-flag": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha512-P+1n3MnwjR/Epg9BBo1KT8qbye2g2Ou4sFumihwt6I4tsUX7jnLcX4BTOSKg/B1ZrIYMN9FcEnG4x5a7NB8Eng==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -23646,6 +24376,8 @@ }, "node_modules/supports-hyperlinks/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -23656,6 +24388,8 @@ }, "node_modules/supports-hyperlinks/node_modules/supports-color/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -23663,6 +24397,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -23673,9 +24409,20 @@ }, "node_modules/svg-pan-zoom": { "version": "3.6.2", + "resolved": "https://registry.npmjs.org/svg-pan-zoom/-/svg-pan-zoom-3.6.2.tgz", + "integrity": "sha512-JwnvRWfVKw/Xzfe6jriFyfey/lWJLq4bUh2jwoR5ChWQuQoOH8FEh1l/bEp46iHHKHEJWIyFJETbazraxNWECg==", "dev": true, "license": "BSD-2-Clause" }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -23685,6 +24432,8 @@ }, "node_modules/sync-fetch": { "version": "0.4.5", + "resolved": "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz", + "integrity": "sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==", "license": "MIT", "dependencies": { "buffer": "^5.7.1", @@ -23711,18 +24460,20 @@ } }, "node_modules/tablesort": { - "version": "5.6.0", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/tablesort/-/tablesort-5.7.0.tgz", + "integrity": "sha512-irnN1HPD08466v6DHKR1+gqZ2be2+QZBDIGTM1DFGoWywY+d38bFtfsuUqBbMGkqaMyYE1uPxE7p0AM5cmbRSA==", "dev": true, "license": "MIT", "engines": { - "node": ">= 16", - "npm": ">= 8" + "node": ">= 22", + "npm": ">= 10" } }, "node_modules/tapable": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.1.tgz", - "integrity": "sha512-b+u3CEM6FjDHru+nhUSjDofpWSBp2rINziJWgApm72wwGasQ/wKXftZe4tI2Y5HPv6OpzXSZHOFq87H4vfsgsw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, "license": "MIT", "engines": { @@ -23734,9 +24485,9 @@ } }, "node_modules/tar": { - "version": "7.5.12", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.12.tgz", - "integrity": "sha512-9TsuLcdhOn4XztcQqhNyq1KOwOOED/3k58JAvtULiYqbO8B/0IBAAIE1hj0Svmm58k27TmcigyDI0deMlgG3uw==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -23762,6 +24513,8 @@ }, "node_modules/temp": { "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", "dependencies": { @@ -23772,8 +24525,17 @@ "node": ">=6.0.0" } }, + "node_modules/temp/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/temp/node_modules/brace-expansion": { "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -23783,6 +24545,9 @@ }, "node_modules/temp/node_modules/glob": { "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -23801,7 +24566,9 @@ } }, "node_modules/temp/node_modules/minimatch": { - "version": "3.1.2", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -23811,19 +24578,11 @@ "node": "*" } }, - "node_modules/temp/node_modules/mkdirp": { - "version": "0.5.6", - "dev": true, - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, "node_modules/temp/node_modules/rimraf": { "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", "dependencies": { @@ -23835,6 +24594,8 @@ }, "node_modules/term-size": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha512-7dPUZQGy/+m3/wjVz3ZW5dobSoD/02NxJpoXUX0WIyjfVS3l0c+b/+9phIDFA7FHzkYtwtMFgeGZ/Y8jVTeqQQ==", "license": "MIT", "dependencies": { "execa": "^0.7.0" @@ -23845,6 +24606,8 @@ }, "node_modules/term-size/node_modules/cross-spawn": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "license": "MIT", "dependencies": { "lru-cache": "^4.0.1", @@ -23854,6 +24617,8 @@ }, "node_modules/term-size/node_modules/execa": { "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", "license": "MIT", "dependencies": { "cross-spawn": "^5.0.1", @@ -23870,6 +24635,8 @@ }, "node_modules/term-size/node_modules/get-stream": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "license": "MIT", "engines": { "node": ">=4" @@ -23877,6 +24644,8 @@ }, "node_modules/term-size/node_modules/is-stream": { "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -23884,6 +24653,8 @@ }, "node_modules/term-size/node_modules/lru-cache": { "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", "license": "ISC", "dependencies": { "pseudomap": "^1.0.2", @@ -23892,6 +24663,8 @@ }, "node_modules/term-size/node_modules/npm-run-path": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", "license": "MIT", "dependencies": { "path-key": "^2.0.0" @@ -23902,6 +24675,8 @@ }, "node_modules/term-size/node_modules/path-key": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "license": "MIT", "engines": { "node": ">=4" @@ -23909,6 +24684,8 @@ }, "node_modules/term-size/node_modules/shebang-command": { "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" @@ -23919,6 +24696,8 @@ }, "node_modules/term-size/node_modules/shebang-regex": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -23926,10 +24705,14 @@ }, "node_modules/term-size/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, "node_modules/term-size/node_modules/which": { "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -23940,10 +24723,14 @@ }, "node_modules/term-size/node_modules/yallist": { "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", "license": "ISC" }, "node_modules/terminal-link": { "version": "1.3.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-1.3.0.tgz", + "integrity": "sha512-nFaWG/gs3brGi3opgWU2+dyFGbQ7tueSRYOBOD8URdDXCbAGqDEZzuskCc+okCClYcJFDPwn8e2mbv4FqAnWFA==", "license": "MIT", "dependencies": { "ansi-escapes": "^3.2.0", @@ -23955,6 +24742,8 @@ }, "node_modules/terminal-link/node_modules/ansi-escapes": { "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", "license": "MIT", "engines": { "node": ">=4" @@ -24066,6 +24855,13 @@ "node": ">=8" } }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/test-exclude/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -24131,6 +24927,8 @@ }, "node_modules/through": { "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "license": "MIT" }, "node_modules/thunky": { @@ -24142,6 +24940,8 @@ }, "node_modules/timed-out": { "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -24159,6 +24959,8 @@ }, "node_modules/tinyglobby": { "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24174,33 +24976,39 @@ }, "node_modules/tlds": { "version": "1.261.0", + "resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz", + "integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==", "license": "MIT", "bin": { "tlds": "bin.js" } }, "node_modules/tldts": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", - "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "tldts-core": "^6.1.86" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.86", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", - "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/tmp": { "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" @@ -24218,6 +25026,8 @@ }, "node_modules/to-readable-stream": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", "license": "MIT", "engines": { "node": ">=6" @@ -24225,6 +25035,8 @@ }, "node_modules/to-regex-range": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24236,6 +25048,8 @@ }, "node_modules/toidentifier": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", "engines": { "node": ">=0.6" @@ -24243,6 +25057,8 @@ }, "node_modules/totalist": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, "license": "MIT", "engines": { @@ -24250,29 +25066,31 @@ } }, "node_modules/tough-cookie": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", - "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", + "peer": true, "dependencies": { - "tldts": "^6.1.32" + "tldts": "^7.0.5" }, "engines": { "node": ">=16" } }, "node_modules/tr46": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", - "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/tree-dump": { @@ -24294,6 +25112,8 @@ }, "node_modules/tree-kill": { "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", "dev": true, "license": "MIT", "bin": { @@ -24302,6 +25122,8 @@ }, "node_modules/trim-newlines": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha512-MTBWv3jhVjTU7XR3IQHllbiJs8sc75a80OEhB6or/q7pLTWgQ0bMGQXXYQSrSuXe6WiKWDZ5txXY5P59a/coVA==", "license": "MIT", "engines": { "node": ">=4" @@ -24309,6 +25131,8 @@ }, "node_modules/trouter": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/trouter/-/trouter-2.0.1.tgz", + "integrity": "sha512-kr8SKKw94OI+xTGOkfsvwZQ8mWoikZDd2n8XZHjJVZUARZT+4/VV6cacRS6CLsH9bNm+HFIPU1Zx4CnNnb4qlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -24333,6 +25157,8 @@ }, "node_modules/ts-jest": { "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", "dev": true, "license": "MIT", "dependencies": { @@ -24384,6 +25210,8 @@ }, "node_modules/ts-jest/node_modules/type-fest": { "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { @@ -24395,6 +25223,8 @@ }, "node_modules/ts-morph": { "version": "27.0.2", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-27.0.2.tgz", + "integrity": "sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==", "dev": true, "license": "MIT", "dependencies": { @@ -24402,62 +25232,10 @@ "code-block-writer": "^13.0.3" } }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node/node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/tslib": { "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, "node_modules/tsyringe": { @@ -24497,6 +25275,8 @@ }, "node_modules/type-check": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", "dependencies": { @@ -24530,40 +25310,23 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "content-type": "^1.0.5", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.2", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, "node_modules/typed-assert": { "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", "dev": true, "license": "MIT" }, @@ -24582,16 +25345,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -24607,10 +25370,14 @@ }, "node_modules/uc.micro": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, "node_modules/uglify-js": { "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, "license": "BSD-2-Clause", "optional": true, @@ -24640,6 +25407,8 @@ }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, "license": "MIT", "engines": { @@ -24648,6 +25417,8 @@ }, "node_modules/unicode-match-property-ecmascript": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, "license": "MIT", "dependencies": { @@ -24660,6 +25431,8 @@ }, "node_modules/unicode-match-property-value-ecmascript": { "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "dev": true, "license": "MIT", "engines": { @@ -24668,6 +25441,8 @@ }, "node_modules/unicode-property-aliases-ecmascript": { "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "dev": true, "license": "MIT", "engines": { @@ -24676,6 +25451,8 @@ }, "node_modules/unique-string": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==", "license": "MIT", "dependencies": { "crypto-random-string": "^1.0.0" @@ -24686,6 +25463,8 @@ }, "node_modules/universalify": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", "engines": { @@ -24694,11 +25473,15 @@ }, "node_modules/unix-crypt-td-js": { "version": "1.1.4", + "resolved": "https://registry.npmjs.org/unix-crypt-td-js/-/unix-crypt-td-js-1.1.4.tgz", + "integrity": "sha512-8rMeVYWSIyccIJscb9NdCfZKSRBKYTeVnwmiRYT2ulE3qd1RaDQ0xQDP+rI3ccIWbhu/zuo5cgN8z73belNZgw==", "dev": true, "license": "BSD-3-Clause" }, "node_modules/unpipe": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -24741,6 +25524,8 @@ }, "node_modules/unzip-response": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha512-N0XH6lqDtFH84JxptQoZYmloF4nzrQqqrAymNj+/gW60AO2AZgOcf4O/nUXJcYfyQkqvMo9lSupBZmmgvuVXlw==", "license": "MIT", "engines": { "node": ">=4" @@ -24748,6 +25533,8 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -24777,6 +25564,8 @@ }, "node_modules/update-notifier": { "version": "2.5.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz", + "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==", "license": "BSD-2-Clause", "dependencies": { "boxen": "^1.2.1", @@ -24796,6 +25585,8 @@ }, "node_modules/update-notifier/node_modules/ansi-styles": { "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", "dependencies": { "color-convert": "^1.9.0" @@ -24806,6 +25597,8 @@ }, "node_modules/update-notifier/node_modules/chalk": { "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", @@ -24818,6 +25611,8 @@ }, "node_modules/update-notifier/node_modules/color-convert": { "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { "color-name": "1.1.3" @@ -24825,10 +25620,14 @@ }, "node_modules/update-notifier/node_modules/color-name": { "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/update-notifier/node_modules/escape-string-regexp": { "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -24836,6 +25635,8 @@ }, "node_modules/update-notifier/node_modules/has-flag": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "license": "MIT", "engines": { "node": ">=4" @@ -24843,6 +25644,8 @@ }, "node_modules/update-notifier/node_modules/supports-color": { "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "license": "MIT", "dependencies": { "has-flag": "^3.0.0" @@ -24863,6 +25666,8 @@ }, "node_modules/url-parse-lax": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "license": "MIT", "dependencies": { "prepend-http": "^2.0.0" @@ -24873,6 +25678,8 @@ }, "node_modules/url-regex": { "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-5.0.0.tgz", + "integrity": "sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g==", "license": "MIT", "dependencies": { "ip-regex": "^4.1.0", @@ -24891,6 +25698,8 @@ }, "node_modules/utils-merge": { "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, "license": "MIT", "engines": { @@ -24899,6 +25708,8 @@ }, "node_modules/uuid": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "dev": true, "funding": [ "https://github.com/sponsors/broofa", @@ -24909,13 +25720,6 @@ "uuid": "dist/esm/bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true, - "license": "MIT" - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -24940,14 +25744,28 @@ }, "node_modules/validate-npm-package-license": { "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, + "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, "node_modules/validate-npm-package-name": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==", "license": "ISC", "dependencies": { "builtins": "^1.0.3" @@ -24955,6 +25773,8 @@ }, "node_modules/vary": { "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -24978,6 +25798,8 @@ }, "node_modules/vis-network": { "version": "10.0.2", + "resolved": "https://registry.npmjs.org/vis-network/-/vis-network-10.0.2.tgz", + "integrity": "sha512-qPl8GLYBeHEFqiTqp4VBbYQIJ2EA8KLr7TstA2E8nJxfEHaKCU81hQLz7hhq11NUpHbMaRzBjW5uZpVKJ45/wA==", "dev": true, "license": "(Apache-2.0 OR MIT)", "funding": { @@ -25144,16 +25966,19 @@ }, "node_modules/web-vitals": { "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { - "node": ">=12" + "node": ">=20" } }, "node_modules/webpack": { @@ -25235,33 +26060,6 @@ } } }, - "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/webpack-dev-server": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.3.tgz", @@ -25369,6 +26167,20 @@ "@types/send": "<1" } }, + "node_modules/webpack-dev-server/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-dev-server/node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -25626,6 +26438,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/webpack-dev-server/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack-dev-server/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/webpack-dev-server/node_modules/open": { "version": "10.2.0", "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", @@ -25653,9 +26498,9 @@ "license": "MIT" }, "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -25665,6 +26510,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/webpack-dev-server/node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/webpack-dev-server/node_modules/raw-body": { "version": "2.5.3", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", @@ -25694,6 +26555,27 @@ "node": ">=8.10.0" } }, + "node_modules/webpack-dev-server/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/webpack-dev-server/node_modules/send": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", @@ -25767,6 +26649,8 @@ }, "node_modules/webpack-merge": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", "dev": true, "license": "MIT", "dependencies": { @@ -25779,7 +26663,9 @@ } }, "node_modules/webpack-sources": { - "version": "3.3.3", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, "license": "MIT", "engines": { @@ -25788,6 +26674,8 @@ }, "node_modules/webpack-subresource-integrity": { "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", "dev": true, "license": "MIT", "dependencies": { @@ -25837,8 +26725,33 @@ "dev": true, "license": "MIT" }, + "node_modules/webpack/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/webpack/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/websocket-driver": { "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -25852,6 +26765,8 @@ }, "node_modules/websocket-extensions": { "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, "license": "Apache-2.0", "engines": { @@ -25860,6 +26775,9 @@ }, "node_modules/whatwg-encoding": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, "license": "MIT", "dependencies": { @@ -25871,6 +26789,8 @@ }, "node_modules/whatwg-encoding/node_modules/iconv-lite": { "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", "dependencies": { @@ -25882,6 +26802,8 @@ }, "node_modules/whatwg-mimetype": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, "license": "MIT", "engines": { @@ -25889,21 +26811,25 @@ } }, "node_modules/whatwg-url": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", - "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "tr46": "^5.1.0", - "webidl-conversions": "^7.0.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/which": { "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", "dependencies": { @@ -25918,11 +26844,15 @@ }, "node_modules/which-module": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true, "license": "ISC" }, "node_modules/widest-line": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.1.tgz", + "integrity": "sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA==", "license": "MIT", "dependencies": { "string-width": "^2.1.1" @@ -25933,6 +26863,8 @@ }, "node_modules/widest-line/node_modules/ansi-regex": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", "license": "MIT", "engines": { "node": ">=4" @@ -25940,6 +26872,8 @@ }, "node_modules/widest-line/node_modules/is-fullwidth-code-point": { "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", "license": "MIT", "engines": { "node": ">=4" @@ -25947,6 +26881,8 @@ }, "node_modules/widest-line/node_modules/string-width": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "license": "MIT", "dependencies": { "is-fullwidth-code-point": "^2.0.0", @@ -25958,6 +26894,8 @@ }, "node_modules/widest-line/node_modules/strip-ansi": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "license": "MIT", "dependencies": { "ansi-regex": "^3.0.0" @@ -25968,11 +26906,15 @@ }, "node_modules/wildcard": { "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true, "license": "MIT" }, "node_modules/windows-release": { "version": "4.0.0", + "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", + "integrity": "sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==", "dev": true, "license": "MIT", "dependencies": { @@ -25987,6 +26929,8 @@ }, "node_modules/windows-release/node_modules/execa": { "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, "license": "MIT", "dependencies": { @@ -26009,6 +26953,8 @@ }, "node_modules/windows-release/node_modules/get-stream": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "license": "MIT", "dependencies": { @@ -26023,6 +26969,8 @@ }, "node_modules/windows-release/node_modules/human-signals": { "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -26031,11 +26979,15 @@ }, "node_modules/windows-release/node_modules/signal-exit": { "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, "license": "ISC" }, "node_modules/word-wrap": { "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", "engines": { @@ -26044,11 +26996,15 @@ }, "node_modules/wordwrap": { "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "dev": true, "license": "MIT" }, "node_modules/wrap-ansi": { "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, "license": "MIT", "dependencies": { @@ -26063,6 +27019,8 @@ "node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, "license": "MIT", "dependencies": { @@ -26079,33 +27037,18 @@ }, "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -26114,6 +27057,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -26127,6 +27072,8 @@ }, "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -26138,33 +27085,18 @@ }, "node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "license": "MIT", "engines": { @@ -26173,6 +27105,8 @@ }, "node_modules/wrap-ansi/node_modules/string-width": { "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { @@ -26186,6 +27120,8 @@ }, "node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", "dependencies": { @@ -26197,6 +27133,8 @@ }, "node_modules/wrappy": { "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, "node_modules/write-file-atomic": { @@ -26254,6 +27192,8 @@ }, "node_modules/xdg-basedir": { "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==", "license": "MIT", "engines": { "node": ">=4" @@ -26261,6 +27201,8 @@ }, "node_modules/xhr2": { "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", "license": "MIT", "engines": { "node": ">= 6" @@ -26285,6 +27227,8 @@ }, "node_modules/y18n": { "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, "license": "ISC", "engines": { @@ -26293,11 +27237,15 @@ }, "node_modules/yallist": { "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { @@ -26311,83 +27259,66 @@ } }, "node_modules/yargs": { - "version": "17.7.2", + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", "dev": true, "license": "MIT", "dependencies": { - "cliui": "^8.0.1", + "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^7.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^22.0.0" }, "engines": { - "node": ">=12" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yargs-parser": { "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, "license": "ISC", "engines": { "node": ">=12" } }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "5.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", "dev": true, "license": "MIT" }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/yargs/node_modules/string-width": { - "version": "4.2.3", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" + "node": ">=18" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "node_modules/yargs/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", "dev": true, - "license": "MIT", + "license": "ISC", "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.12.0 || >=23" } }, "node_modules/yocto-queue": { @@ -26448,13 +27379,6 @@ "peerDependencies": { "zod": "^3.25 || ^4" } - }, - "node_modules/zone.js": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.16.1.tgz", - "integrity": "sha512-dpvY17vxYIW3+bNrP0ClUlaiY0CiIRK3tnoLaGoQsQcY9/I/NpzIWQ7tQNhbV7LacQMpCII6wVzuL3tuWOyfuA==", - "devOptional": true, - "license": "MIT" } } } diff --git a/package.json b/package.json index 26509a6ad..2766bbf21 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "tslib": "^2.3.0" }, "devDependencies": { - "@angular-builders/jest": "^21.0.3", "@angular-devkit/build-angular": "^21.2.1", "@angular-eslint/eslint-plugin": "^21.3.0", "@angular-eslint/eslint-plugin-template": "^21.3.0", diff --git a/setup-jest.ts b/setup-jest.ts index d8e007667..8a7c094bd 100644 --- a/setup-jest.ts +++ b/setup-jest.ts @@ -2,34 +2,10 @@ import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless'; setupZonelessTestEnv(); -// Global mocks for jsdom -const mock = () => { - let storage: Record = {}; - return { - getItem: (key: string) => (key in storage ? storage[key] : null), - setItem: (key: string, value: string) => (storage[key] = value || ''), - removeItem: (key: string) => delete storage[key], - clear: () => (storage = {}), - }; -}; - -Object.defineProperty(window, 'localStorage', { value: mock() }); -Object.defineProperty(window, 'sessionStorage', { value: mock() }); -Object.defineProperty(window, 'getComputedStyle', { - value: () => ['-webkit-appearance'], -}); - -Object.defineProperty(document.body, 'clientWidth', { value: 1024 }); -Object.defineProperty(document.body, 'clientHeight', { value: 768 }); - -// Mock ResizeObserver for Jest (PrimeNG and other UI libs may require this) class ResizeObserver { - // eslint-disable-next-line @typescript-eslint/no-empty-function - observe() {} - // eslint-disable-next-line @typescript-eslint/no-empty-function - unobserve() {} - // eslint-disable-next-line @typescript-eslint/no-empty-function - disconnect() {} + observe = jest.fn(); + unobserve = jest.fn(); + disconnect = jest.fn(); } Object.defineProperty(window, 'ResizeObserver', { @@ -38,14 +14,6 @@ Object.defineProperty(window, 'ResizeObserver', { value: ResizeObserver, }); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -(global as any).ace = { - define: jest.fn(), - require: jest.fn().mockReturnValue({ - snippetCompleter: {}, - }), -}; - jest.mock('@newrelic/browser-agent/loaders/browser-agent', () => ({ BrowserAgent: jest.fn().mockImplementation(() => ({ start: jest.fn(), @@ -54,9 +22,5 @@ jest.mock('@newrelic/browser-agent/loaders/browser-agent', () => ({ })); if (!globalThis.structuredClone) { - Object.defineProperty(globalThis, 'structuredClone', { - value: (value: T): T => JSON.parse(JSON.stringify(value)) as T, - writable: true, - configurable: true, - }); + globalThis.structuredClone = (value: T): T => JSON.parse(JSON.stringify(value)) as T; } From 9c1968e1f3a633b9a5fa95fd4d933f84ac35f8d5 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 24 Mar 2026 13:31:37 +0200 Subject: [PATCH 08/10] fix(dependency): updated dependency --- package-lock.json | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e8425c51..5492619e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7024,13 +7024,15 @@ } }, "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { - "node": ">= 16" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -21527,6 +21529,19 @@ "node": ">=16.0.0" } }, + "node_modules/pkijs/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", From f9af42f03c35feb2bf49e91093d4858b6aa85861 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 24 Mar 2026 14:19:07 +0200 Subject: [PATCH 09/10] test(configs): updated jest configs --- .github/scripts/check-coverage-thresholds.js | 8 ++++---- jest.config.ts => jest.config.js | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) rename jest.config.ts => jest.config.js (96%) diff --git a/.github/scripts/check-coverage-thresholds.js b/.github/scripts/check-coverage-thresholds.js index 3ae4ee0e4..3a300fe95 100644 --- a/.github/scripts/check-coverage-thresholds.js +++ b/.github/scripts/check-coverage-thresholds.js @@ -1,5 +1,5 @@ const coverage = require('../../coverage/coverage-summary.json'); -const jestConfig = require('../../jest.config.ts'); +const jestConfig = require('../../jest.config.js'); const summary = coverage.total; const thresholds = jestConfig.coverageThreshold.global; @@ -28,7 +28,7 @@ for (const key of ['branches', 'functions', 'lines', 'statements']) { ); errors.push( formatErrorsWithAlignedStars( - `Please update the coverageThreshold.global.${key} in the jest.config.ts to ---> ${current} <---`, + `Please update the coverageThreshold.global.${key} in the jest.config.js to ---> ${current} <---`, true ) ); @@ -40,8 +40,8 @@ for (const key of ['branches', 'functions', 'lines', 'statements']) { if (failed) { const stars = '*'.repeat(warnMessage.length + 8); console.log('\n\nCongratulations! You have successfully run the coverage check and added tests.'); - console.log('\n\nThe jest.config.ts file is not insync with your new test additions.'); - console.log('Please update the coverage thresholds in jest.config.ts.'); + console.log('\n\nThe jest.config.js file is not insync with your new test additions.'); + console.log('Please update the coverage thresholds in jest.config.js.'); console.log('You will need to commit again once you have updated the jst.config.ts file.'); console.log('This is only necessary until we hit 100% coverage.'); console.log(`\n\n${stars}`); diff --git a/jest.config.ts b/jest.config.js similarity index 96% rename from jest.config.ts rename to jest.config.js index 568c34a4d..c0c941e77 100644 --- a/jest.config.ts +++ b/jest.config.js @@ -1,6 +1,4 @@ -import type { Config } from 'jest'; - -const config: Config = { +const config = { preset: 'jest-preset-angular', setupFilesAfterEnv: ['/setup-jest.ts'], globalSetup: '/jest.global-setup.ts', @@ -72,4 +70,4 @@ const config: Config = { testPathIgnorePatterns: ['/src/environments'], }; -export default config; +module.exports = config; From 5d62bc4a19fad5e8022bbd8098a5e10734aa57bd Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 24 Mar 2026 15:16:26 +0200 Subject: [PATCH 10/10] fix(testing): updated testing docs --- docs/testing.md | 93 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/docs/testing.md b/docs/testing.md index 84357eecd..39b6f6d40 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -311,7 +311,35 @@ expect(store.dispatch).toHaveBeenCalledWith(new SpecificAction()); ## 7. Router & Route Mocking -### ActivatedRoute +Use this checklist: + +### `provideRouter([])` + +- Use when the template/component needs router infrastructure (`routerLink`, `routerLinkActive`, router directives/providers). +- Keep it local to the spec. +- Skip it for pure logic tests without router directive usage. + +### `ActivatedRouteMockBuilder` + +- Use when code reads route state (`snapshot.paramMap`, `params`, `queryParams`, `parent`, and similar route inputs). +- Use it for deterministic route inputs in unit tests. +- Works with or without `provideRouter([])` depending on template needs. + +### `RouterMockBuilder` + +- Use when you assert navigation calls (`navigate`, `navigateByUrl`, `url`/events usage). +- Best for behavior assertions, not real router integration. +- If you mock `Router`, you test whether navigation was requested, not real routing execution. +- Do not mock `Router` in specs that validate real routing behavior via `provideRouter(...)`. + +### Typical combos + +- Params only: `ActivatedRouteMockBuilder` +- Params + navigation assertions: `ActivatedRouteMockBuilder` + `RouterMockBuilder` +- Template has `routerLink` + params + navigation assertions: `provideRouter([])` + `ActivatedRouteMockBuilder` + `RouterMockBuilder` +- Need real router behavior: `provideRouter(...)` + `ActivatedRoute` setup, avoid mocking `Router` + +### `ActivatedRouteMockBuilder` examples ```typescript const mockRoute = ActivatedRouteMockBuilder.create() @@ -330,7 +358,7 @@ const mockRoute = ActivatedRouteMockBuilder.create() const mockRoute = ActivatedRouteMockBuilder.create().withParams({ id: 'reg-1' }).withNoParent().build(); ``` -### Router +### `RouterMockBuilder` examples ```typescript const mockRouter = RouterMockBuilder.create().withUrl('/registries/drafts/reg-1/metadata').build(); @@ -413,37 +441,57 @@ fixture.detectChanges(); ## 10. Async Operations -### `fakeAsync` + `tick` for debounced operations +### Zoneless change detection + +In a zoneless environment, `fixture.detectChanges()` is used for immediate synchronous rendering. For signal updates and other async logic, use `await fixture.whenStable()` before DOM assertions so the scheduler can finish. ```typescript -it('should dispatch after debounce', fakeAsync(() => { +it('should update UI after signal change', async () => { + mySignal.set(newVal); + await fixture.whenStable(); + expect(fixture.nativeElement.textContent).toContain(newVal); +}); +``` + +### Debounced operations with Jest timers + +```typescript +it('should dispatch after debounce', () => { + jest.useFakeTimers(); (store.dispatch as jest.Mock).mockClear(); + component.onProjectFilter('abc'); - tick(300); + jest.advanceTimersByTime(300); + expect(store.dispatch).toHaveBeenCalledWith(new GetProjects('user-1', 'abc')); -})); + jest.useRealTimers(); +}); -// Deduplication — only the last value dispatches -it('should debounce rapid calls', fakeAsync(() => { +it('should debounce rapid calls', () => { + jest.useFakeTimers(); (store.dispatch as jest.Mock).mockClear(); + component.onProjectFilter('a'); component.onProjectFilter('ab'); component.onProjectFilter('abc'); - tick(300); - const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [any]) => a instanceof GetProjects); + jest.advanceTimersByTime(300); + + const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [unknown]) => a instanceof GetProjects); expect(calls.length).toBe(1); -})); + jest.useRealTimers(); +}); ``` -### `done` callback for output emissions +### Output emissions with explicit async flow ```typescript -it('should emit attachFile', (done) => { - component.attachFile.subscribe((f) => { - expect(f).toEqual({ id: 'file-1' }); - done(); +it('should emit attachFile', async () => { + const emitted = new Promise((resolve) => { + component.attachFile.subscribe((file) => resolve(file)); }); + component.selectFile({ id: 'file-1' } as FileModel); + await expect(emitted).resolves.toEqual({ id: 'file-1' }); }); ``` @@ -855,13 +903,12 @@ This project strictly enforces 90%+ test coverage through GitHub Actions CI. 7. **Use `(store.dispatch as jest.Mock).mockClear()`** when `ngOnInit` dispatches and you need isolated per-test assertions. 8. **Use `WritableSignal` for dynamic state** — pass `signal()` values to `provideMockStore` when tests need to mutate state mid-test. 9. **Use `Subject` for dialog `onClose`** — gives explicit control over dialog result timing. Use `provideDynamicDialogRefMock()` where applicable. -10. **Use `fakeAsync` + `tick`** for debounced operations — specify the exact debounce duration. +10. **Use Jest fake timers** for debounced operations — `jest.useFakeTimers()`, `jest.advanceTimersByTime(ms)`, and `jest.useRealTimers()`. 11. **Use `fixture.componentRef.setInput()`** for signal inputs — never direct property assignment. -12. **Use `ngMocks.faster()`** when all tests in a file share identical `TestBed` config — reuses the compiled module for speed. Do not use if any test requires a different config: shared state will cause subtle test pollution. -13. **Use typed mock interfaces** (`ToastServiceMockType`, `RouterMockType`, etc.) — avoid `any`. -14. **Test both positive and negative paths** — confirm an action fires AND confirm it does not fire when conditions are not met. -15. **Only use `@testing/data` fixtures in HTTP flushes** — never hardcode response values inline in service or state tests. -16. **Each test should highlight the most critical aspect of the code** — if a test fails during a refactor, it should clearly signal that a core feature was impacted. +12. **Use typed mock interfaces** (`ToastServiceMockType`, `RouterMockType`, etc.) — avoid `any`. +13. **Test both positive and negative paths** — confirm an action fires AND confirm it does not fire when conditions are not met. +14. **Only use `@testing/data` fixtures in HTTP flushes** — never hardcode response values inline in service or state tests. +15. **Each test should highlight the most critical aspect of the code** — if a test fails during a refactor, it should clearly signal that a core feature was impacted. --- @@ -894,7 +941,7 @@ expect(callArgs[1].data.draftId).toBe('draft-1'); ### Filtering dispatch calls by action type ```typescript -const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [any]) => a instanceof GetProjects); +const calls = (store.dispatch as jest.Mock).mock.calls.filter(([a]: [unknown]) => a instanceof GetProjects); expect(calls.length).toBe(1); expect(calls[0][0]).toEqual(new GetProjects('user-1', 'abc')); ```