From 190241b2ffa1bf78d74b0ce65d8a1bc37c3daf42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 09:10:41 +0000 Subject: [PATCH 01/22] chore(deps): update actions/checkout digest to eef6144 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/api-breaking-changes-comment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/api-breaking-changes-comment.yml b/.github/workflows/api-breaking-changes-comment.yml index e28f147706..038061adac 100644 --- a/.github/workflows/api-breaking-changes-comment.yml +++ b/.github/workflows/api-breaking-changes-comment.yml @@ -90,7 +90,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4 # Identify comment to be updated - name: Find comment for API Changes From 9cc7dd6cfd05549319b7fbce22684e766c3a0639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 8 Oct 2024 17:03:22 +0200 Subject: [PATCH 02/22] implement the beginnings of mockApis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/real-rockets-divide.md | 5 + .changeset/thin-chairs-ring.md | 8 + packages/backend-test-utils/report.api.md | 119 ++++++----- .../src/next/services/mockServices.ts | 39 ++++ .../auth/bitbucket/BitbucketAuth.test.ts | 4 +- .../BitbucketServerAuth.test.ts | 4 +- .../auth/github/GithubAuth.test.ts | 4 +- .../auth/gitlab/GitlabAuth.test.ts | 4 +- .../auth/google/GoogleAuth.test.ts | 4 +- .../auth/oauth2/OAuth2.test.ts | 4 +- .../auth/okta/OktaAuth.test.ts | 4 +- .../src/app/resolveRouteBindings.test.ts | 60 +++--- .../SupportButton/SupportButton.test.tsx | 26 +-- .../extractRouteInfoFromAppNode.test.ts | 4 +- .../src/wiring/createSpecializedApp.test.tsx | 4 +- .../frontend-defaults/src/createApp.test.tsx | 26 +-- .../src/createPublicSignInApp.test.tsx | 6 +- .../src/wiring/createFrontendPlugin.test.ts | 4 +- packages/frontend-test-utils/package.json | 1 - packages/frontend-test-utils/report.api.md | 15 +- .../frontend-test-utils/src/apis/index.ts | 3 +- packages/test-utils/package.json | 5 + packages/test-utils/report.api.md | 31 ++- .../src/testUtils}/apis/ApiMock.ts | 2 +- .../testUtils/apis/ConfigApi/MockConfigApi.ts | 1 + .../test-utils/src/testUtils/apis/index.ts | 2 + .../src/testUtils/apis/mockApis.test.tsx | 40 ++++ .../test-utils/src/testUtils/apis/mockApis.ts | 188 ++++++++++++++++++ .../PreviewCatalogInfoComponent.test.tsx | 14 +- .../StepPrepareCreatePullRequest.test.tsx | 4 +- plugins/home/src/api/config.test.ts | 62 +++--- .../VisitedByType/Content.test.tsx | 76 +++---- .../SearchFilter.Autocomplete.test.tsx | 12 +- .../SearchFilter/SearchFilter.test.tsx | 12 +- .../components/SearchFilter/hooks.test.tsx | 17 +- .../SearchPagination.test.tsx | 12 +- .../src/context/SearchContext.test.tsx | 8 +- .../SearchType/SearchType.Accordion.test.tsx | 12 +- .../SearchType/SearchType.Tabs.test.tsx | 12 +- .../components/SearchType/SearchType.test.tsx | 12 +- plugins/techdocs-react/src/context.test.tsx | 5 +- plugins/techdocs/src/client.test.ts | 4 +- .../TechDocsReaderPage.test.tsx | 9 +- .../useNavigateUrl.test.tsx | 36 +--- yarn.lock | 7 +- 45 files changed, 632 insertions(+), 299 deletions(-) create mode 100644 .changeset/real-rockets-divide.md create mode 100644 .changeset/thin-chairs-ring.md rename packages/{frontend-test-utils/src => test-utils/src/testUtils}/apis/ApiMock.ts (94%) create mode 100644 packages/test-utils/src/testUtils/apis/mockApis.test.tsx create mode 100644 packages/test-utils/src/testUtils/apis/mockApis.ts diff --git a/.changeset/real-rockets-divide.md b/.changeset/real-rockets-divide.md new file mode 100644 index 0000000000..2506a89f22 --- /dev/null +++ b/.changeset/real-rockets-divide.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Minor doc string changes diff --git a/.changeset/thin-chairs-ring.md b/.changeset/thin-chairs-ring.md new file mode 100644 index 0000000000..ebcb3afa93 --- /dev/null +++ b/.changeset/thin-chairs-ring.md @@ -0,0 +1,8 @@ +--- +'@backstage/test-utils': minor +'@backstage/frontend-test-utils': patch +--- + +Added a `mockApis` export, which will replace the `MockX` API implementation classes and their related types. This is analogous with the backend's `mockServices`. + +Deprecated `MockConfigApi`, please use `mockApis.config` instead. diff --git a/packages/backend-test-utils/report.api.md b/packages/backend-test-utils/report.api.md index 1235cd7cb3..d9c1c4c721 100644 --- a/packages/backend-test-utils/report.api.md +++ b/packages/backend-test-utils/report.api.md @@ -150,7 +150,7 @@ export function mockErrorHandler(): ErrorRequestHandler< Record >; -// @public (undocumented) +// @public export namespace mockServices { // (undocumented) export function auth(options?: { @@ -491,65 +491,64 @@ export class TestDatabases { // src/next/services/mockCredentials.d.ts:116:9 - (ae-undocumented) Missing documentation for "invalidToken". // src/next/services/mockCredentials.d.ts:117:9 - (ae-undocumented) Missing documentation for "invalidHeader". // src/next/services/mockServices.d.ts:5:1 - (ae-undocumented) Missing documentation for "ServiceMock". -// src/next/services/mockServices.d.ts:13:1 - (ae-undocumented) Missing documentation for "mockServices". -// src/next/services/mockServices.d.ts:14:5 - (ae-undocumented) Missing documentation for "rootConfig". -// src/next/services/mockServices.d.ts:15:5 - (ae-undocumented) Missing documentation for "rootConfig". -// src/next/services/mockServices.d.ts:16:9 - (ae-undocumented) Missing documentation for "Options". -// src/next/services/mockServices.d.ts:19:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:20:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:22:5 - (ae-undocumented) Missing documentation for "rootLogger". -// src/next/services/mockServices.d.ts:23:5 - (ae-undocumented) Missing documentation for "rootLogger". -// src/next/services/mockServices.d.ts:24:9 - (ae-undocumented) Missing documentation for "Options". -// src/next/services/mockServices.d.ts:27:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:28:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:30:5 - (ae-undocumented) Missing documentation for "auth". -// src/next/services/mockServices.d.ts:34:5 - (ae-undocumented) Missing documentation for "auth". -// src/next/services/mockServices.d.ts:35:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:36:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:38:5 - (ae-undocumented) Missing documentation for "discovery". -// src/next/services/mockServices.d.ts:39:5 - (ae-undocumented) Missing documentation for "discovery". -// src/next/services/mockServices.d.ts:40:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:41:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:61:5 - (ae-undocumented) Missing documentation for "httpAuth". -// src/next/services/mockServices.d.ts:72:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:82:5 - (ae-undocumented) Missing documentation for "userInfo". -// src/next/services/mockServices.d.ts:90:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:92:5 - (ae-undocumented) Missing documentation for "cache". -// src/next/services/mockServices.d.ts:93:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:94:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:96:5 - (ae-undocumented) Missing documentation for "database". -// src/next/services/mockServices.d.ts:97:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:98:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:100:5 - (ae-undocumented) Missing documentation for "rootHealth". -// src/next/services/mockServices.d.ts:101:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:102:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:104:5 - (ae-undocumented) Missing documentation for "httpRouter". -// src/next/services/mockServices.d.ts:105:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:106:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:108:5 - (ae-undocumented) Missing documentation for "rootHttpRouter". -// src/next/services/mockServices.d.ts:109:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:110:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:112:5 - (ae-undocumented) Missing documentation for "lifecycle". -// src/next/services/mockServices.d.ts:113:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:114:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:116:5 - (ae-undocumented) Missing documentation for "logger". -// src/next/services/mockServices.d.ts:117:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:118:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:120:5 - (ae-undocumented) Missing documentation for "permissions". -// src/next/services/mockServices.d.ts:121:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:122:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:124:5 - (ae-undocumented) Missing documentation for "rootLifecycle". -// src/next/services/mockServices.d.ts:125:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:126:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:128:5 - (ae-undocumented) Missing documentation for "scheduler". -// src/next/services/mockServices.d.ts:129:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:130:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:132:5 - (ae-undocumented) Missing documentation for "urlReader". -// src/next/services/mockServices.d.ts:133:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:134:15 - (ae-undocumented) Missing documentation for "mock". -// src/next/services/mockServices.d.ts:136:5 - (ae-undocumented) Missing documentation for "events". -// src/next/services/mockServices.d.ts:137:15 - (ae-undocumented) Missing documentation for "factory". -// src/next/services/mockServices.d.ts:138:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:53:5 - (ae-undocumented) Missing documentation for "rootConfig". +// src/next/services/mockServices.d.ts:54:5 - (ae-undocumented) Missing documentation for "rootConfig". +// src/next/services/mockServices.d.ts:55:9 - (ae-undocumented) Missing documentation for "Options". +// src/next/services/mockServices.d.ts:58:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:59:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:61:5 - (ae-undocumented) Missing documentation for "rootLogger". +// src/next/services/mockServices.d.ts:62:5 - (ae-undocumented) Missing documentation for "rootLogger". +// src/next/services/mockServices.d.ts:63:9 - (ae-undocumented) Missing documentation for "Options". +// src/next/services/mockServices.d.ts:66:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:67:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:69:5 - (ae-undocumented) Missing documentation for "auth". +// src/next/services/mockServices.d.ts:73:5 - (ae-undocumented) Missing documentation for "auth". +// src/next/services/mockServices.d.ts:74:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:75:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:77:5 - (ae-undocumented) Missing documentation for "discovery". +// src/next/services/mockServices.d.ts:78:5 - (ae-undocumented) Missing documentation for "discovery". +// src/next/services/mockServices.d.ts:79:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:80:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:100:5 - (ae-undocumented) Missing documentation for "httpAuth". +// src/next/services/mockServices.d.ts:111:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:121:5 - (ae-undocumented) Missing documentation for "userInfo". +// src/next/services/mockServices.d.ts:129:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:131:5 - (ae-undocumented) Missing documentation for "cache". +// src/next/services/mockServices.d.ts:132:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:133:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:135:5 - (ae-undocumented) Missing documentation for "database". +// src/next/services/mockServices.d.ts:136:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:137:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:139:5 - (ae-undocumented) Missing documentation for "rootHealth". +// src/next/services/mockServices.d.ts:140:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:141:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:143:5 - (ae-undocumented) Missing documentation for "httpRouter". +// src/next/services/mockServices.d.ts:144:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:145:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:147:5 - (ae-undocumented) Missing documentation for "rootHttpRouter". +// src/next/services/mockServices.d.ts:148:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:149:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:151:5 - (ae-undocumented) Missing documentation for "lifecycle". +// src/next/services/mockServices.d.ts:152:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:153:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:155:5 - (ae-undocumented) Missing documentation for "logger". +// src/next/services/mockServices.d.ts:156:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:157:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:159:5 - (ae-undocumented) Missing documentation for "permissions". +// src/next/services/mockServices.d.ts:160:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:161:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:163:5 - (ae-undocumented) Missing documentation for "rootLifecycle". +// src/next/services/mockServices.d.ts:164:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:165:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:167:5 - (ae-undocumented) Missing documentation for "scheduler". +// src/next/services/mockServices.d.ts:168:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:169:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:171:5 - (ae-undocumented) Missing documentation for "urlReader". +// src/next/services/mockServices.d.ts:172:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:173:15 - (ae-undocumented) Missing documentation for "mock". +// src/next/services/mockServices.d.ts:175:5 - (ae-undocumented) Missing documentation for "events". +// src/next/services/mockServices.d.ts:176:15 - (ae-undocumented) Missing documentation for "factory". +// src/next/services/mockServices.d.ts:177:15 - (ae-undocumented) Missing documentation for "mock". // src/next/wiring/TestBackend.d.ts:5:1 - (ae-undocumented) Missing documentation for "TestBackendOptions". // src/next/wiring/TestBackend.d.ts:6:5 - (ae-undocumented) Missing documentation for "extensionPoints". // src/next/wiring/TestBackend.d.ts:14:5 - (ae-undocumented) Missing documentation for "features". diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index 8b0f196399..789b75de9d 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -128,7 +128,46 @@ function simpleMock( } /** + * Mock implementations of the core services, to be used in tests. + * * @public + * @remarks + * + * There are some variations among the services depending on what needs tests + * might have, but overall there are three main usage patterns: + * + * 1. Creating an actual fake service instance, often with a simplified version + * of functionality, by calling the mock service itself as a function. + * + * ```ts + * // The function often accepts parameters that control its behavior + * const foo = mockServices.foo(); + * ``` + * + * 2. Creating a mock service, where all methods are replaced with jest mocks, by + * calling the service's `mock` function. + * + * ```ts + * // You can optionally supply a subset of its methods to implement + * const foo = mockServices.foo.mock({ + * someMethod: () => 'mocked result', + * }); + * // After exercising your test, you can make assertions on the mock: + * expect(foo.someMethod).toHaveBeenCalledTimes(2); + * expect(foo.otherMethod).toHaveBeenCalledWith(testData); + * ``` + * + * 3. Creating a service factory that behaves similarly to the mock as per above. + * + * ```ts + * await startTestBackend({ + * features: [ + * mockServices.foo.factory({ + * someMethod: () => 'mocked result', + * }) + * ], + * }); + * ``` */ export namespace mockServices { export function rootConfig(options?: rootConfig.Options): RootConfigService { diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts index 1335808280..c1359ee6b2 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts @@ -17,7 +17,7 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import BitbucketAuth from './BitbucketAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -33,7 +33,7 @@ describe('BitbucketAuth', () => { jest.resetAllMocks(); }); - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); it.each([ ['team api write_repository', ['team', 'api', 'write_repository']], diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts index 5631a9a886..39f1d0dd68 100644 --- a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts @@ -17,7 +17,7 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import BitbucketServerAuth from './BitbucketServerAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -41,7 +41,7 @@ describe('BitbucketServerAuth', () => { ['PROJECT_ADMIN', 'REPO_READ', 'ACCOUNT_WRITE'], ], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const bitbucketServerAuth = BitbucketServerAuth.create({ configApi: configApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts index fc77754f6f..8a7bb9e671 100644 --- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts @@ -17,7 +17,7 @@ import { UrlPatternDiscovery } from '../../DiscoveryApi'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import GithubAuth from './GithubAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -33,7 +33,7 @@ describe('GithubAuth', () => { jest.resetAllMocks(); }); - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); it('should forward access token request to session manager', async () => { const githubAuth = GithubAuth.create({ diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts index 6dde0c0334..0818bbb253 100644 --- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts @@ -17,7 +17,7 @@ import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; import GitlabAuth from './GitlabAuth'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const getSession = jest.fn(); @@ -40,7 +40,7 @@ describe('GitlabAuth', () => { ], ['read_repository sudo', ['read_repository', 'sudo']], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const gitlabAuth = GitlabAuth.create({ configApi: configApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts index 7897eef42e..a740b3089b 100644 --- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts @@ -17,7 +17,7 @@ import GoogleAuth from './GoogleAuth'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const PREFIX = 'https://www.googleapis.com/auth/'; @@ -59,7 +59,7 @@ describe('GoogleAuth', () => { [`${PREFIX}profile`, [`${PREFIX}profile`]], [`${PREFIX}openid`, [`${PREFIX}openid`]], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const googleAuth = GoogleAuth.create({ configApi: configApi, diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts index 8ca818ebd8..a34589d746 100644 --- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts @@ -17,7 +17,7 @@ import OAuth2 from './OAuth2'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const theFuture = new Date(Date.now() + 3600000); const thePast = new Date(Date.now() - 10); @@ -35,7 +35,7 @@ jest.mock('../../../../lib/AuthSessionManager', () => ({ }, })); -const configApi = new MockConfigApi({}); +const configApi = mockApis.config(); describe('OAuth2', () => { it('should get refreshed access token', async () => { diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts index 5e7370a947..621172f8cc 100644 --- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts +++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts @@ -17,7 +17,7 @@ import OktaAuth from './OktaAuth'; import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import { UrlPatternDiscovery } from '../../DiscoveryApi'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; const PREFIX = 'okta.'; @@ -51,7 +51,7 @@ describe('OktaAuth', () => { [`${PREFIX}profile`, [`${PREFIX}profile`]], [`${PREFIX}openid`, [`${PREFIX}openid`]], ])(`should normalize scopes correctly - %p`, (scope, scopes) => { - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const auth = OktaAuth.create({ configApi: configApi, oauthRequestApi: new MockOAuthApi(), diff --git a/packages/core-app-api/src/app/resolveRouteBindings.test.ts b/packages/core-app-api/src/app/resolveRouteBindings.test.ts index fdc1a9e456..e3dbbba0af 100644 --- a/packages/core-app-api/src/app/resolveRouteBindings.test.ts +++ b/packages/core-app-api/src/app/resolveRouteBindings.test.ts @@ -20,7 +20,7 @@ import { createRouteRef, } from '@backstage/core-plugin-api'; import { collectRouteIds, resolveRouteBindings } from './resolveRouteBindings'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; describe('resolveRouteBindings', () => { it('runs happy path', () => { @@ -30,7 +30,7 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind(external, { myRoute: ref }); }, - new MockConfigApi({}), + mockApis.config(), [], ); @@ -45,7 +45,7 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind(external, { someOtherRoute: ref } as any); }, - new MockConfigApi({}), + mockApis.config(), [], ), ).toThrow('Key someOtherRoute is not an existing external route'); @@ -56,8 +56,10 @@ describe('resolveRouteBindings', () => { const myTarget = createRouteRef({ id: 'test' }); const result = resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }, }), [ createPlugin({ @@ -84,8 +86,10 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind({ mySource }, { mySource: false }); }, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'myTarget' } } }, + }, }), [ createPlugin({ @@ -106,8 +110,8 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind({ mySource }, { mySource: myTarget }); }, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': false } } }, + mockApis.config({ + data: { app: { routes: { bindings: { 'test.mySource': false } } } }, }), [ createPlugin({ @@ -128,7 +132,7 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ app: { routes: { bindings: 'derp' } } }), + mockApis.config({ data: { app: { routes: { bindings: 'derp' } } } }), [], ), ).toThrow( @@ -138,8 +142,8 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': true } } }, + mockApis.config({ + data: { app: { routes: { bindings: { 'test.mySource': true } } } }, }), [], ), @@ -150,8 +154,10 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }, }), [], ), @@ -162,8 +168,10 @@ describe('resolveRouteBindings', () => { expect(() => resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.mySource': 'test.myTarget' } } }, + }, }), [ createPlugin({ @@ -198,17 +206,17 @@ describe('resolveRouteBindings', () => { }); // defaultTarget wins only if no bind or config matches - let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [ - plugin, - ]); + let result = resolveRouteBindings(() => {}, mockApis.config(), [plugin]); expect(result.get(source)).toBe(target1); // config wins over defaultTarget result = resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.source': 'test.target2' } } }, + mockApis.config({ + data: { + app: { routes: { bindings: { 'test.source': 'test.target2' } } }, + }, }), [plugin], ); @@ -220,7 +228,7 @@ describe('resolveRouteBindings', () => { ({ bind }) => { bind(plugin.externalRoutes, { source: plugin.routes.target2 }); }, - new MockConfigApi({}), + mockApis.config(), [plugin], ); @@ -257,17 +265,15 @@ describe('collectRouteIds', () => { }); // resolves normally with no config - let result = resolveRouteBindings(() => {}, new MockConfigApi({}), [ - plugin, - ]); + let result = resolveRouteBindings(() => {}, mockApis.config(), [plugin]); expect(result.get(source)).toBe(target1); // can be disabled result = resolveRouteBindings( () => {}, - new MockConfigApi({ - app: { routes: { bindings: { 'test.source': false } } }, + mockApis.config({ + data: { app: { routes: { bindings: { 'test.source': false } } } }, }), [plugin], ); diff --git a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx index 6b0e239aa5..9ed8fb8401 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.test.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.test.tsx @@ -16,7 +16,7 @@ import { configApiRef } from '@backstage/core-plugin-api'; import { - MockConfigApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -24,17 +24,19 @@ import { act, fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { SupportButton } from './SupportButton'; -const configApi = new MockConfigApi({ - app: { - support: { - url: 'https://github.com', - items: [ - { - title: 'Github', - icon: 'github', - links: [{ title: 'Github Issues', url: '/issues' }], - }, - ], +const configApi = mockApis.config({ + data: { + app: { + support: { + url: 'https://github.com', + items: [ + { + title: 'Github', + icon: 'github', + links: [{ title: 'Github Issues', url: '/issues' }], + }, + ], + }, }, }, }); diff --git a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts index 7ffe64d6d7..eac6e9d2aa 100644 --- a/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts +++ b/packages/frontend-app-api/src/routing/extractRouteInfoFromAppNode.test.ts @@ -28,7 +28,7 @@ import { createFrontendPlugin, createRouteRef, } from '@backstage/frontend-plugin-api'; -import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils'; +import { mockApis, TestApiRegistry } from '@backstage/test-utils'; import appPlugin from '@backstage/plugin-app'; import { readAppExtensionsConfig } from '../tree/readAppExtensionsConfig'; @@ -92,7 +92,7 @@ function routeInfoFromExtensions(extensions: ExtensionDefinition[]) { builtinExtensions: [ resolveExtensionDefinition(Root, { namespace: 'root' }), ], - parameters: readAppExtensionsConfig(new MockConfigApi({})), + parameters: readAppExtensionsConfig(mockApis.config()), forbidden: new Set(['root']), }), ); diff --git a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx index 72594cefd1..6c5331a972 100644 --- a/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx +++ b/packages/frontend-app-api/src/wiring/createSpecializedApp.test.tsx @@ -29,7 +29,7 @@ import { } from '@backstage/frontend-plugin-api'; import { screen, render } from '@testing-library/react'; import { createSpecializedApp } from './createSpecializedApp'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import React from 'react'; import { configApiRef, @@ -98,7 +98,7 @@ describe('createSpecializedApp', () => { it('should forward config', () => { const app = createSpecializedApp({ - config: new MockConfigApi({ test: 'foo' }), + config: mockApis.config({ data: { test: 'foo' } }), features: [ createFrontendPlugin({ id: 'test', diff --git a/packages/frontend-defaults/src/createApp.test.tsx b/packages/frontend-defaults/src/createApp.test.tsx index 2890700653..06872a5691 100644 --- a/packages/frontend-defaults/src/createApp.test.tsx +++ b/packages/frontend-defaults/src/createApp.test.tsx @@ -26,7 +26,7 @@ import { } from '@backstage/frontend-plugin-api'; import { screen, waitFor } from '@testing-library/react'; import { CreateAppFeatureLoader, createApp } from './createApp'; -import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; +import { mockApis, renderWithEffects } from '@backstage/test-utils'; import React from 'react'; import { featureFlagsApiRef, useApi } from '@backstage/core-plugin-api'; import appPlugin from '@backstage/plugin-app'; @@ -35,12 +35,14 @@ describe('createApp', () => { it('should allow themes to be installed', async () => { const app = createApp({ configLoader: async () => ({ - config: new MockConfigApi({ - app: { - extensions: [ - { 'theme:app/light': false }, - { 'theme:app/dark': false }, - ], + config: mockApis.config({ + data: { + app: { + extensions: [ + { 'theme:app/light': false }, + { 'theme:app/dark': false }, + ], + }, }, }), }), @@ -72,7 +74,7 @@ describe('createApp', () => { it('should deduplicate features keeping the last received one', async () => { const duplicatedFeatureId = 'test'; const app = createApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendPlugin({ id: duplicatedFeatureId, @@ -135,7 +137,7 @@ describe('createApp', () => { const app = createApp({ configLoader: async () => ({ - config: new MockConfigApi({ key: 'config-value' }), + config: mockApis.config({ data: { key: 'config-value' } }), }), features: [appPlugin, loader], }); @@ -159,7 +161,7 @@ describe('createApp', () => { const app = createApp({ configLoader: async () => ({ - config: new MockConfigApi({}), + config: mockApis.config(), }), features: [loader], }); @@ -173,7 +175,7 @@ describe('createApp', () => { it('should register feature flags', async () => { const app = createApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ appPlugin.withOverrides({ extensions: [ @@ -227,7 +229,7 @@ describe('createApp', () => { let appTreeApi: AppTreeApi | undefined = undefined; const app = createApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendPlugin({ id: 'my-plugin', diff --git a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx index ac1adf9b0f..ce0f8c4b2a 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx @@ -22,7 +22,7 @@ import { import { render, screen, waitFor } from '@testing-library/react'; import React, { useEffect } from 'react'; import { createPublicSignInApp } from './createPublicSignInApp'; -import { MockConfigApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; describe('createPublicSignInApp', () => { beforeEach(() => { @@ -31,7 +31,7 @@ describe('createPublicSignInApp', () => { it('should render a sign-in page', async () => { const app = createPublicSignInApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendModule({ pluginId: 'app', @@ -59,7 +59,7 @@ describe('createPublicSignInApp', () => { .mockReturnValue(); const app = createPublicSignInApp({ - configLoader: async () => ({ config: new MockConfigApi({}) }), + configLoader: async () => ({ config: mockApis.config() }), features: [ createFrontendModule({ pluginId: 'app', diff --git a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts index 65daad0586..678eff0b4c 100644 --- a/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts +++ b/packages/frontend-plugin-api/src/wiring/createFrontendPlugin.test.ts @@ -23,7 +23,7 @@ import { JsonObject } from '@backstage/types'; import { createExtension } from './createExtension'; import { createExtensionDataRef } from './createExtensionDataRef'; import { coreExtensionData } from './coreExtensionData'; -import { MockConfigApi, renderWithEffects } from '@backstage/test-utils'; +import { mockApis, renderWithEffects } from '@backstage/test-utils'; import { createExtensionInput } from './createExtensionInput'; const nameExtensionDataRef = createExtensionDataRef().with({ @@ -128,7 +128,7 @@ function createTestAppRoot({ }) { return createApp({ features: [...features], - configLoader: async () => ({ config: new MockConfigApi(config) }), + configLoader: async () => ({ config: mockApis.config({ data: config }) }), }).createRoot(); } diff --git a/packages/frontend-test-utils/package.json b/packages/frontend-test-utils/package.json index 49869b7df9..27bb592879 100644 --- a/packages/frontend-test-utils/package.json +++ b/packages/frontend-test-utils/package.json @@ -50,7 +50,6 @@ }, "peerDependencies": { "@testing-library/react": "^16.0.0", - "@types/jest": "*", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", diff --git a/packages/frontend-test-utils/report.api.md b/packages/frontend-test-utils/report.api.md index 3229da9898..dc37ec4fb4 100644 --- a/packages/frontend-test-utils/report.api.md +++ b/packages/frontend-test-utils/report.api.md @@ -3,13 +3,12 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -/// /// import { AnalyticsApi } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/frontend-plugin-api'; import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api'; -import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ApiMock } from '@backstage/test-utils'; import { AppNode } from '@backstage/frontend-plugin-api'; import { AppNodeInstance } from '@backstage/frontend-plugin-api'; import { ErrorWithContext } from '@backstage/test-utils'; @@ -18,6 +17,7 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExtensionDefinitionParameters } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-app-api'; import { JsonObject } from '@backstage/types'; +import { mockApis } from '@backstage/test-utils'; import { MockConfigApi } from '@backstage/test-utils'; import { MockErrorApi } from '@backstage/test-utils'; import { MockErrorApiOptions } from '@backstage/test-utils'; @@ -34,14 +34,7 @@ import { TestApiProviderProps } from '@backstage/test-utils'; import { TestApiRegistry } from '@backstage/test-utils'; import { withLogCollector } from '@backstage/test-utils'; -// @public -export type ApiMock = { - factory: ApiFactory; -} & { - [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return - ? TApi[Key] & jest.MockInstance - : TApi[Key]; -}; +export { ApiMock }; // @public (undocumented) export function createExtensionTester( @@ -103,6 +96,8 @@ export class MockAnalyticsApi implements AnalyticsApi { getEvents(): AnalyticsEvent[]; } +export { mockApis }; + export { MockConfigApi }; export { MockErrorApi }; diff --git a/packages/frontend-test-utils/src/apis/index.ts b/packages/frontend-test-utils/src/apis/index.ts index 564c327aef..e5a0786cd0 100644 --- a/packages/frontend-test-utils/src/apis/index.ts +++ b/packages/frontend-test-utils/src/apis/index.ts @@ -24,7 +24,8 @@ export { MockPermissionApi, MockStorageApi, type MockStorageBucket, + mockApis, + type ApiMock, } from '@backstage/test-utils'; -export { type ApiMock } from './ApiMock'; export { MockAnalyticsApi } from './AnalyticsApi/MockAnalyticsApi'; diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 1c611fd584..47c61abae2 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -65,6 +65,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@testing-library/jest-dom": "^6.0.0", + "@types/jest": "*", "@types/react": "^18.0.0", "msw": "^1.0.0", "react": "^18.0.2", @@ -73,12 +74,16 @@ }, "peerDependencies": { "@testing-library/react": "^16.0.0", + "@types/jest": "*", "@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0", "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, "peerDependenciesMeta": { + "@types/jest": { + "optional": true + }, "@types/react": { "optional": true } diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 4a1f297c4e..a551d54c62 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -3,8 +3,11 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +/// + import { AnalyticsApi } from '@backstage/core-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; +import { ApiFactory } from '@backstage/core-plugin-api'; import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; import { AppComponents } from '@backstage/core-plugin-api'; @@ -39,6 +42,15 @@ import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +// @public +export type ApiMock = { + factory: ApiFactory; +} & { + [Key in keyof TApi]: TApi[Key] extends (...args: infer Args) => infer Return + ? TApi[Key] & jest.MockInstance + : TApi[Key]; +}; + // @public export type AsyncLogCollector = () => Promise; @@ -77,10 +89,27 @@ export class MockAnalyticsApi implements AnalyticsApi { getEvents(): AnalyticsEvent[]; } +// @public +export namespace mockApis { + export function config(options?: { data?: JsonObject }): jest.Mocked; + export namespace config { + const factory: ( + options?: + | { + data?: JsonObject | undefined; + } + | undefined, + ) => ApiFactory; + const mock: (partialImpl?: Partial | undefined) => ApiMock; + } +} + // @public @deprecated export function mockBreakpoint(options: { matches: boolean }): void; -// @public +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "config" has more than one declaration; you need to add a TSDoc member reference selector +// +// @public @deprecated export class MockConfigApi implements ConfigApi { constructor(data: JsonObject); get(key?: string): T; diff --git a/packages/frontend-test-utils/src/apis/ApiMock.ts b/packages/test-utils/src/testUtils/apis/ApiMock.ts similarity index 94% rename from packages/frontend-test-utils/src/apis/ApiMock.ts rename to packages/test-utils/src/testUtils/apis/ApiMock.ts index d11689f98a..fe66f35458 100644 --- a/packages/frontend-test-utils/src/apis/ApiMock.ts +++ b/packages/test-utils/src/testUtils/apis/ApiMock.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ApiFactory } from '@backstage/frontend-plugin-api'; +import { ApiFactory } from '@backstage/core-plugin-api'; /** * Represents a mocked version of an API, where you automatically have access to diff --git a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts index 372c295c02..9d7462889c 100644 --- a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts +++ b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts @@ -23,6 +23,7 @@ import { ConfigApi } from '@backstage/core-plugin-api'; * that can be used to mock configuration using a plain object. * * @public + * @deprecated Use {@link mockApis.config} instead * @example * ```tsx * const mockConfig = new MockConfigApi({ diff --git a/packages/test-utils/src/testUtils/apis/index.ts b/packages/test-utils/src/testUtils/apis/index.ts index e122a4b432..94738a598e 100644 --- a/packages/test-utils/src/testUtils/apis/index.ts +++ b/packages/test-utils/src/testUtils/apis/index.ts @@ -20,3 +20,5 @@ export * from './ErrorApi'; export * from './FetchApi'; export * from './PermissionApi'; export * from './StorageApi'; +export { type ApiMock } from './ApiMock'; +export { mockApis } from './mockApis'; diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx new file mode 100644 index 0000000000..b7a78867f8 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockApis } from './mockApis'; + +describe('mockApis', () => { + describe('config', () => { + const data = { backend: { baseUrl: 'http://test.com' } }; + + it('can create an instance and make assertions on it', () => { + const empty = mockApis.config(); + const notEmpty = mockApis.config({ data }); + expect(empty.getOptional('backend.baseUrl')).toBeUndefined(); + expect(empty.getOptional).toHaveBeenCalledTimes(1); + expect(notEmpty.getOptional('backend.baseUrl')).toEqual( + 'http://test.com', + ); + expect(notEmpty.getOptional).toHaveBeenCalledTimes(1); + }); + + it('can create a mock and make assertions on it', async () => { + const mock = mockApis.config.mock({ getString: () => 'replaced' }); + expect(mock.getString('a')).toEqual('replaced'); + expect(mock.getString).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts new file mode 100644 index 0000000000..d084fae332 --- /dev/null +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -0,0 +1,188 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { + ApiFactory, + ApiRef, + configApiRef, + createApiFactory, +} from '@backstage/core-plugin-api'; +import { JsonObject } from '@backstage/types'; +import { ApiMock } from './ApiMock'; + +/** @internal */ +function simpleInstance( + _ref: ApiRef, + instance: TApi, + mockSkeleton: () => jest.Mocked, +): jest.Mocked { + const mock = mockSkeleton(); + const result = Object.create(instance) as any; + for (const [key, impl] of Object.entries(mock)) { + result[key] = (impl as any).mockImplementation((instance as any)[key]); + } + return result; +} + +/** @internal */ +function simpleFactory( + ref: ApiRef, + factory: (...args: TArgs) => TApi, +): (...args: TArgs) => ApiFactory { + return (...args) => + createApiFactory({ + api: ref, + deps: {}, + factory: () => factory(...args), + }); +} + +/** @internal */ +function simpleMock( + ref: ApiRef, + mockFactory: () => jest.Mocked, +): (partialImpl?: Partial) => ApiMock { + return partialImpl => { + const mock = mockFactory(); + if (partialImpl) { + for (const [key, impl] of Object.entries(partialImpl)) { + if (typeof impl === 'function') { + (mock as any)[key].mockImplementation(impl); + } else { + (mock as any)[key] = impl; + } + } + } + return Object.assign(mock, { + factory: createApiFactory({ + api: ref, + deps: {}, + factory: () => mock, + }), + }) as ApiMock; + }; +} + +/** + * Mock implementations of the core utility APIs, to be used in tests. + * + * @public + * @remarks + * + * There are some variations among the APIs depending on what needs tests + * might have, but overall there are three main usage patterns: + * + * 1: Creating an actual fake API instance, often with a simplified version + * of functionality, by calling the mock API itself as a function. + * + * ```ts + * // The function often accepts parameters that control its behavior + * const foo = mockApis.foo(); + * ``` + * + * 2: Creating a mock API, where all methods are replaced with jest mocks, by + * calling the API's `mock` function. + * + * ```ts + * // You can optionally supply a subset of its methods to implement + * const foo = mockApis.foo.mock({ + * someMethod: () => 'mocked result', + * }); + * // After exercising your test, you can make assertions on the mock: + * expect(foo.someMethod).toHaveBeenCalledTimes(2); + * expect(foo.otherMethod).toHaveBeenCalledWith(testData); + * ``` + * + * 3: Creating an API factory that behaves similarly to the mock as per above. + * + * ```ts + * const factory = mockApis.foo.factory({ + * someMethod: () => 'mocked result', + * }); + * ``` + */ +export namespace mockApis { + const configMockSkeleton = () => ({ + has: jest.fn(), + keys: jest.fn(), + get: jest.fn(), + getOptional: jest.fn(), + getConfig: jest.fn(), + getOptionalConfig: jest.fn(), + getConfigArray: jest.fn(), + getOptionalConfigArray: jest.fn(), + getNumber: jest.fn(), + getOptionalNumber: jest.fn(), + getBoolean: jest.fn(), + getOptionalBoolean: jest.fn(), + getString: jest.fn(), + getOptionalString: jest.fn(), + getStringArray: jest.fn(), + getOptionalStringArray: jest.fn(), + }); + /** + * Fake implementation of {@link @backstage/frontend-plugin-api#ConfigApi} + * with optional data supplied. + * + * @public + * @example + * + * ```tsx + * const config = mockApis.config({ + * data: { app: { baseUrl: 'https://example.com' } }, + * }); + * + * const rendered = await renderInTestApp( + * + * + * , + * ); + * ``` + */ + export function config(options?: { data?: JsonObject }) { + return simpleInstance( + configApiRef, + new ConfigReader(options?.data, 'mock-config'), + configMockSkeleton, + ); + } + /** + * Mock helpers for {@link @backstage/frontend-plugin-api#ConfigApi}. + * + * @see {@link @backstage/frontend-plugin-api#mockApis.config} + * @public + */ + export namespace config { + /** + * Creates a factory for a fake implementation of + * {@link @backstage/frontend-plugin-api#ConfigApi} with optional + * configuration data supplied. + * + * @public + */ + export const factory = simpleFactory(configApiRef, config); + /** + * Creates a mock implementation of + * {@link @backstage/frontend-plugin-api#ConfigApi}. All methods are + * replaced with jest mock functions, and you can optionally pass in a + * subset of methods with an explicit implementation. + * + * @public + */ + export const mock = simpleMock(configApiRef, configMockSkeleton); + } +} diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx index 1ed8026a2b..f832c842a5 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/PreviewCatalogInfoComponent.test.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { configApiRef } from '@backstage/core-plugin-api'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { makeStyles } from '@material-ui/core/styles'; import { render, screen } from '@testing-library/react'; import { renderHook } from '@testing-library/react'; @@ -46,7 +46,7 @@ const entities: Entity[] = [ }, ]; -const mockConfigApi = new MockConfigApi({}); +const mockConfigApi = mockApis.config(); const apis = [[configApiRef, mockConfigApi]] as const; describe('', () => { @@ -122,10 +122,12 @@ describe('', () => { apis={[ [ configApiRef, - new MockConfigApi({ - catalog: { - import: { - entityFilename: 'anvil.yaml', + mockApis.config({ + data: { + catalog: { + import: { + entityFilename: 'anvil.yaml', + }, }, }, }), diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 6e81b27c55..acc2715fec 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -16,7 +16,7 @@ import { configApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { TestApiProvider, MockConfigApi } from '@backstage/test-utils'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import TextField from '@material-ui/core/TextField'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -43,7 +43,7 @@ describe('', () => { post: jest.fn(), }; - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const Wrapper = ({ children }: { children?: React.ReactNode }) => ( { describe('readFilterConfig', () => { it('returns filter data', async () => { - const mockConfig = new MockConfigApi({ - field: 'pathname', - operator: '==', - value: '/home', + const mockConfig = mockApis.config({ + data: { + field: 'pathname', + operator: '==', + value: '/home', + }, }); const res = readFilterConfig(mockConfig); expect(res).toEqual({ @@ -34,10 +36,12 @@ describe('config', () => { }); it('returns undefined for invalid filter', async () => { - const mockInvalidConfig = new MockConfigApi({ - myField: 'pathname', - operator: '==', - value: '3', + const mockInvalidConfig = mockApis.config({ + data: { + myField: 'pathname', + operator: '==', + value: '3', + }, }); const res = readFilterConfig(mockInvalidConfig); expect(res).toEqual(undefined); @@ -46,15 +50,19 @@ describe('config', () => { describe('createFilterByQueryParamFromConfig', () => { it('returns filter data', async () => { - const mockConfig1 = new MockConfigApi({ - field: 'id', - operator: '==', - value: '3', + const mockConfig1 = mockApis.config({ + data: { + field: 'id', + operator: '==', + value: '3', + }, }); - const mockConfig2 = new MockConfigApi({ - field: 'pathname', - operator: '==', - value: 'path', + const mockConfig2 = mockApis.config({ + data: { + field: 'pathname', + operator: '==', + value: 'path', + }, }); const res = createFilterByQueryParamFromConfig([ mockConfig1, @@ -67,15 +75,19 @@ describe('config', () => { }); it('returns only valid filters', async () => { - const mockValidConfig = new MockConfigApi({ - field: 'id', - operator: '==', - value: 3, + const mockValidConfig = mockApis.config({ + data: { + field: 'id', + operator: '==', + value: 3, + }, }); - const mockInvalidConfig = new MockConfigApi({ - myField: 'pathname', - operator: '==', - value: 'path', + const mockInvalidConfig = mockApis.config({ + data: { + myField: 'pathname', + operator: '==', + value: 'path', + }, }); const res = createFilterByQueryParamFromConfig([ mockValidConfig, diff --git a/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx b/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx index 055ebf998b..ec2d9eb068 100644 --- a/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx +++ b/plugins/home/src/homePageComponents/VisitedByType/Content.test.tsx @@ -19,7 +19,7 @@ import { Content } from './Content'; import { TestApiProvider, renderInTestApp, - MockConfigApi, + mockApis, } from '@backstage/test-utils'; import { visitsApiRef } from '../../api'; import { ContextProvider } from './Context'; @@ -138,16 +138,18 @@ describe('', () => { }); it('allows recent items to be filtered using config', async () => { - const configApiMock = new MockConfigApi({ - home: { - recentVisits: { - filterBy: [ - { - field: 'pathname', - operator: '==', - value: '/tech-radar', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + recentVisits: { + filterBy: [ + { + field: 'pathname', + operator: '==', + value: '/tech-radar', + }, + ], + }, }, }, }); @@ -206,20 +208,22 @@ describe('', () => { }); it('allows recent items to have no filter if the filter config is not valid', async () => { - const configApiMock = new MockConfigApi({ - home: { - recentVisits: { - filterBy: [ - { - operator: '==', - value: '/tech-radar', - }, - { - field: 'pathname', - operator: '==', - value: '/explore', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + recentVisits: { + filterBy: [ + { + operator: '==', + value: '/tech-radar', + }, + { + field: 'pathname', + operator: '==', + value: '/explore', + }, + ], + }, }, }, }); @@ -283,16 +287,18 @@ describe('', () => { }); it('allows top items to be filtered using config', async () => { - const configApiMock = new MockConfigApi({ - home: { - topVisits: { - filterBy: [ - { - field: 'pathname', - operator: '==', - value: '/explore', - }, - ], + const configApiMock = mockApis.config({ + data: { + home: { + topVisits: { + filterBy: [ + { + field: 'pathname', + operator: '==', + value: '/explore', + }, + ], + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx index abf5e828f6..bbc6454613 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.Autocomplete.test.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { screen, render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; @@ -35,10 +35,12 @@ const SearchContextFilterSpy = ({ name }: { name: string }) => { }; describe('SearchFilter.Autocomplete', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx index c51a4832e3..e155a093d5 100644 --- a/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/SearchFilter.test.tsx @@ -22,7 +22,7 @@ import { configApiRef } from '@backstage/core-plugin-api'; import { SearchContextProvider } from '../../context'; import { SearchFilter } from './SearchFilter'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { searchApiRef } from '../../api'; describe('SearchFilter', () => { @@ -37,10 +37,12 @@ describe('SearchFilter', () => { const values = ['value1', 'value2']; const filters = { unrelated: 'unrelated' }; - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx index 884512963f..28a6fbfd60 100644 --- a/plugins/search-react/src/components/SearchFilter/hooks.test.tsx +++ b/plugins/search-react/src/components/SearchFilter/hooks.test.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { ApiProvider } from '@backstage/core-app-api'; -import { MockConfigApi, TestApiRegistry } from '@backstage/test-utils'; +import { mockApis, TestApiRegistry } from '@backstage/test-utils'; import { act, renderHook, waitFor } from '@testing-library/react'; import { searchApiRef } from '../../api'; @@ -27,17 +28,19 @@ jest.useFakeTimers(); describe('SearchFilter.hooks', () => { describe('useDefaultFilterValue', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }), }; - const mockApis = TestApiRegistry.from( + const apis = TestApiRegistry.from( [searchApiRef, searchApiMock], [configApiRef, configApiMock], ); @@ -54,7 +57,7 @@ describe('SearchFilter.hooks', () => { filters: {}, }; return ( - + diff --git a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx index 7c86e16e95..b0a64f0061 100644 --- a/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx +++ b/plugins/search-react/src/components/SearchPagination/SearchPagination.test.tsx @@ -19,7 +19,7 @@ import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { - MockConfigApi, + mockApis, renderWithEffects, TestApiProvider, } from '@backstage/test-utils'; @@ -31,10 +31,12 @@ import { SearchPagination } from './SearchPagination'; import { configApiRef } from '@backstage/core-plugin-api'; describe('SearchPagination', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 58fde35667..2cb83e5ab6 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -22,7 +22,7 @@ import { act, renderHook, } from '@testing-library/react'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import React from 'react'; import { SearchContextProvider, @@ -37,7 +37,7 @@ describe('SearchContext', () => { } satisfies typeof searchApiRef.T; const wrapper = ({ children, initialState, config = {} }: any) => { - const configApiMock = new MockConfigApi(config); + const configApiMock = mockApis.config({ data: config }); return ( { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( { const { result } = renderHook(() => useSearch(), { wrapper: ({ children }) => { - const configApiMock = new MockConfigApi({}); + const configApiMock = mockApis.config(); return ( ({ })); describe('SearchType.Accordion', () => { - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx index c0d8549253..d06106e1ae 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.test.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { act, render } from '@testing-library/react'; import user from '@testing-library/user-event'; import { @@ -40,10 +40,12 @@ jest.mock('@backstage/plugin-search-react', () => ({ describe('SearchType.Tabs', () => { const searchApiMock = { query: jest.fn().mockResolvedValue({ results: [] }) }; - const configApiMock = new MockConfigApi({ - search: { - query: { - pageLimit: 100, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pageLimit: 100, + }, }, }, }); diff --git a/plugins/search/src/components/SearchType/SearchType.test.tsx b/plugins/search/src/components/SearchType/SearchType.test.tsx index 353b45c0fb..6a393798de 100644 --- a/plugins/search/src/components/SearchType/SearchType.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.test.tsx @@ -23,7 +23,7 @@ import { searchApiRef, } from '@backstage/plugin-search-react'; import { SearchType } from './SearchType'; -import { MockConfigApi, TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; describe('SearchType', () => { const initialState = { @@ -36,10 +36,12 @@ describe('SearchType', () => { const values = ['value1', 'value2']; const typeValues = ['preselected']; - const configApiMock = new MockConfigApi({ - search: { - query: { - pagelimit: 10, + const configApiMock = mockApis.config({ + data: { + search: { + query: { + pagelimit: 10, + }, }, }, }); diff --git a/plugins/techdocs-react/src/context.test.tsx b/plugins/techdocs-react/src/context.test.tsx index a923b3c528..6c947b6858 100644 --- a/plugins/techdocs-react/src/context.test.tsx +++ b/plugins/techdocs-react/src/context.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { renderHook, act, waitFor } from '@testing-library/react'; @@ -21,7 +22,7 @@ import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; import { MockAnalyticsApi, - MockConfigApi, + mockApis, TestApiProvider, } from '@backstage/test-utils'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; @@ -84,7 +85,7 @@ const wrapper = ({ diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 62d497d790..e2c68a7d73 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -18,7 +18,7 @@ import { UrlPatternDiscovery } from '@backstage/core-app-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; import { fetchEventSource } from '@microsoft/fetch-event-source'; -import { MockConfigApi, MockFetchApi } from '@backstage/test-utils'; +import { mockApis, MockFetchApi } from '@backstage/test-utils'; import { TechDocsStorageClient } from './client'; jest.mock('@microsoft/fetch-event-source'); @@ -34,7 +34,7 @@ const mockEntity = { describe('TechDocsStorageClient', () => { const mockBaseUrl = 'http://backstage:9191/api/techdocs'; - const configApi = new MockConfigApi({}); + const configApi = mockApis.config(); const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); const identityApi: jest.Mocked = { getCredentials: jest.fn(), diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 5e8cd7af27..5d08355ae4 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { - MockConfigApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -106,10 +107,8 @@ jest.mock('@backstage/core-components', () => ({ Page: jest.fn(), })); -const configApi = new MockConfigApi({ - app: { - baseUrl: 'http://localhost:3000', - }, +const configApi = mockApis.config({ + data: { app: { baseUrl: 'http://localhost:3000' } }, }); const Wrapper = ({ children }: { children: React.ReactNode }) => { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx index b7d4c55ed7..f2894e4d21 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPageContent/useNavigateUrl.test.tsx @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { - MockConfigApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -55,16 +56,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000'; await renderInTestApp( , @@ -75,16 +67,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000/instance'; await renderInTestApp( , @@ -95,16 +78,7 @@ describe('useNavigateUrl', () => { const baseUrl = 'http://localhost:3000'; await renderInTestApp( , diff --git a/yarn.lock b/yarn.lock index f998b87e11..5c29a9dbec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4683,7 +4683,6 @@ __metadata: zod: ^3.22.4 peerDependencies: "@testing-library/react": ^16.0.0 - "@types/jest": "*" "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 @@ -8564,6 +8563,7 @@ __metadata: "@material-ui/core": ^4.12.2 "@material-ui/icons": ^4.9.1 "@testing-library/jest-dom": ^6.0.0 + "@types/jest": "*" "@types/react": ^18.0.0 cross-fetch: ^4.0.0 i18next: ^22.4.15 @@ -8574,11 +8574,14 @@ __metadata: zen-observable: ^0.10.0 peerDependencies: "@testing-library/react": ^16.0.0 + "@types/jest": "*" "@types/react": ^16.13.1 || ^17.0.0 || ^18.0.0 react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 peerDependenciesMeta: + "@types/jest": + optional: true "@types/react": optional: true languageName: unknown @@ -17912,7 +17915,7 @@ __metadata: languageName: node linkType: hard -"@types/jest@npm:^29.5.11": +"@types/jest@npm:*, @types/jest@npm:^29.5.11": version: 29.5.13 resolution: "@types/jest@npm:29.5.13" dependencies: From 0fa49b9a5c509d15f3789f091be244e8fbbe068f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:20:05 +0000 Subject: [PATCH 03/22] chore(deps): update actions/checkout action to v4.2.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/api-breaking-changes.yml | 2 +- .github/workflows/automate_changeset_feedback.yml | 2 +- .github/workflows/automate_merge_message.yml | 2 +- .github/workflows/ci.yml | 6 +++--- .github/workflows/deploy_docker-image.yml | 2 +- .github/workflows/deploy_microsite.yml | 8 ++++---- .github/workflows/deploy_packages.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/sync_code-formatting.yml | 2 +- .github/workflows/sync_dependabot-changesets.yml | 2 +- .github/workflows/sync_release-manifest.yml | 4 ++-- .github/workflows/sync_renovate-changesets.yml | 2 +- .github/workflows/sync_snyk-github-issues.yml | 2 +- .github/workflows/sync_snyk-monitor.yml | 2 +- .github/workflows/sync_version-packages.yml | 2 +- .github/workflows/verify_accessibility.yml | 2 +- .github/workflows/verify_codeql.yml | 2 +- .github/workflows/verify_docs-quality.yml | 2 +- .github/workflows/verify_e2e-linux.yml | 2 +- .github/workflows/verify_e2e-techdocs.yml | 2 +- .github/workflows/verify_e2e-windows.yml | 2 +- .github/workflows/verify_fossa.yml | 2 +- .github/workflows/verify_microsite.yml | 2 +- .github/workflows/verify_microsite_accessibility.yml | 2 +- .github/workflows/verify_storybook.yml | 2 +- .github/workflows/verify_windows.yml | 2 +- 26 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index 1c2596bad6..58d8020672 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -18,7 +18,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: # Fetch the commit that's merged into the base rather than the target ref # This will let us diff only the contents of the PR, without fetching more history diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index 1f07a848fb..a0c0e44902 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -27,7 +27,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: # Fetch the commit that's merged into the base rather than the target ref # This will let us diff only the contents of the PR, without fetching more history diff --git a/.github/workflows/automate_merge_message.yml b/.github/workflows/automate_merge_message.yml index 4ab75b816f..4b388d7850 100644 --- a/.github/workflows/automate_merge_message.yml +++ b/.github/workflows/automate_merge_message.yml @@ -28,7 +28,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: '${{ github.event.pull_request.merge_commit_sha }}' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81d1d109b1..a488773779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -68,7 +68,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -209,7 +209,7 @@ jobs: INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }} steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: fetch master branch run: git fetch origin master diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index a45c9c35bd..d5934e8ad8 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -25,7 +25,7 @@ jobs: egress-policy: audit - name: checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: path: backstage ref: ${{ github.event.client_payload.version && env.RELEASE_VERSION || github.ref }} diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 7dad0d3b20..f7d709ce77 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -54,7 +54,7 @@ jobs: result-encoding: string - name: checkout latest release - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: refs/tags/${{ steps.find-release.outputs.result }} @@ -96,7 +96,7 @@ jobs: egress-policy: audit - name: checkout master - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: use node.js 18.x uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 @@ -170,7 +170,7 @@ jobs: # Stable docs - name: checkout latest release - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: ref: refs/tags/${{ needs.stable.outputs.release }} @@ -198,7 +198,7 @@ jobs: # Next docs - name: checkout master - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: clean: false diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index eda5e7e0c6..51b5f42831 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -68,7 +68,7 @@ jobs: INTEGRATION_TEST_AZURE_TOKEN: ${{ secrets.INTEGRATION_TEST_AZURE_TOKEN }} steps: - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 53489d8c0e..e54a32b46d 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -34,7 +34,7 @@ jobs: egress-policy: audit - name: 'Checkout code' - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: persist-credentials: false diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index f59eb048e0..2aa84bb208 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -14,7 +14,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: # Fetch changes to previous commit - required for 'only_changed' in Prettier action fetch-depth: 0 diff --git a/.github/workflows/sync_dependabot-changesets.yml b/.github/workflows/sync_dependabot-changesets.yml index adf39be8df..a89ab7629a 100644 --- a/.github/workflows/sync_dependabot-changesets.yml +++ b/.github/workflows/sync_dependabot-changesets.yml @@ -16,7 +16,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 2 ref: ${{ github.head_ref }} diff --git a/.github/workflows/sync_release-manifest.yml b/.github/workflows/sync_release-manifest.yml index 87a8837c48..4b209739c7 100644 --- a/.github/workflows/sync_release-manifest.yml +++ b/.github/workflows/sync_release-manifest.yml @@ -13,7 +13,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: # 'v' prefix is added here for the tag, we keep it out of the manifest logic ref: v${{ github.event.client_payload.version }} @@ -35,7 +35,7 @@ jobs: # Checkout backstage/versions into /backstage/versions, which is where store the output - name: Checkout versions - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: repository: backstage/versions path: versions diff --git a/.github/workflows/sync_renovate-changesets.yml b/.github/workflows/sync_renovate-changesets.yml index 200bc6eaaa..1c9b4a6186 100644 --- a/.github/workflows/sync_renovate-changesets.yml +++ b/.github/workflows/sync_renovate-changesets.yml @@ -16,7 +16,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 2 ref: ${{ github.head_ref }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 9c709508b3..1390b4ec61 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -16,7 +16,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: use node.js 18.x uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 diff --git a/.github/workflows/sync_snyk-monitor.yml b/.github/workflows/sync_snyk-monitor.yml index 1b48930410..f746a8c339 100644 --- a/.github/workflows/sync_snyk-monitor.yml +++ b/.github/workflows/sync_snyk-monitor.yml @@ -29,7 +29,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Monitor and Synchronize Snyk Policies uses: snyk/actions/node@cdb760004ba9ea4d525f2e043745dfe85bb9077e # master with: diff --git a/.github/workflows/sync_version-packages.yml b/.github/workflows/sync_version-packages.yml index aac606ae90..77fb5f951e 100644 --- a/.github/workflows/sync_version-packages.yml +++ b/.github/workflows/sync_version-packages.yml @@ -18,7 +18,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 20000 fetch-tags: true diff --git a/.github/workflows/verify_accessibility.yml b/.github/workflows/verify_accessibility.yml index cfeb8ba51f..29ff740f68 100644 --- a/.github/workflows/verify_accessibility.yml +++ b/.github/workflows/verify_accessibility.yml @@ -24,7 +24,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Use Node.js 18.x uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 with: diff --git a/.github/workflows/verify_codeql.yml b/.github/workflows/verify_codeql.yml index 63ba85906e..c77b65929b 100644 --- a/.github/workflows/verify_codeql.yml +++ b/.github/workflows/verify_codeql.yml @@ -47,7 +47,7 @@ jobs: egress-policy: audit - name: Checkout repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: # We must fetch at least the immediate parents so that if this is # a pull request then we can checkout the head. diff --git a/.github/workflows/verify_docs-quality.yml b/.github/workflows/verify_docs-quality.yml index 8e2ae20596..176136f821 100644 --- a/.github/workflows/verify_docs-quality.yml +++ b/.github/workflows/verify_docs-quality.yml @@ -16,7 +16,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 # Vale does not support file excludes, so we use the script to generate a list of files instead # The action also does not allow args or a local config file to be passed in, so the files array diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index 69a92abb9a..0c487429e9 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -45,7 +45,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Configure Git run: | diff --git a/.github/workflows/verify_e2e-techdocs.yml b/.github/workflows/verify_e2e-techdocs.yml index 15bb9b3479..f6b300af7e 100644 --- a/.github/workflows/verify_e2e-techdocs.yml +++ b/.github/workflows/verify_e2e-techdocs.yml @@ -34,7 +34,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0 with: python-version: '3.9' diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 911698867c..b6bfef51e5 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -42,7 +42,7 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Configure Git run: | diff --git a/.github/workflows/verify_fossa.yml b/.github/workflows/verify_fossa.yml index 8b3bfc9b13..5deaeafeaf 100644 --- a/.github/workflows/verify_fossa.yml +++ b/.github/workflows/verify_fossa.yml @@ -19,7 +19,7 @@ jobs: egress-policy: audit - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Install Fossa run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash" diff --git a/.github/workflows/verify_microsite.yml b/.github/workflows/verify_microsite.yml index 5527953767..1d6aac21ca 100644 --- a/.github/workflows/verify_microsite.yml +++ b/.github/workflows/verify_microsite.yml @@ -28,7 +28,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: use node.js 18.x uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 diff --git a/.github/workflows/verify_microsite_accessibility.yml b/.github/workflows/verify_microsite_accessibility.yml index 350c46309e..6626f4def9 100644 --- a/.github/workflows/verify_microsite_accessibility.yml +++ b/.github/workflows/verify_microsite_accessibility.yml @@ -19,7 +19,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Use Node.js 18.x uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 77a01f9ffd..ae3470b079 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -32,7 +32,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 with: fetch-depth: 0 # Required to retrieve git history diff --git a/.github/workflows/verify_windows.yml b/.github/workflows/verify_windows.yml index 4cb2cab220..d184bdca08 100644 --- a/.github/workflows/verify_windows.yml +++ b/.github/workflows/verify_windows.yml @@ -33,7 +33,7 @@ jobs: with: egress-policy: audit - - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: use node.js ${{ matrix.node-version }} uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 From 7f7892bc0b281e900b5ea71f6f888810b5ce3a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 10 Oct 2024 14:46:06 +0200 Subject: [PATCH 04/22] back out of the automocking of instances MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/test-utils/report.api.md | 2 +- .../src/testUtils/apis/mockApis.test.tsx | 4 +- .../test-utils/src/testUtils/apis/mockApis.ts | 60 +++++++------------ 3 files changed, 23 insertions(+), 43 deletions(-) diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index a551d54c62..15625adb21 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -91,7 +91,7 @@ export class MockAnalyticsApi implements AnalyticsApi { // @public export namespace mockApis { - export function config(options?: { data?: JsonObject }): jest.Mocked; + export function config(options?: { data?: JsonObject }): ConfigApi; export namespace config { const factory: ( options?: diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx index b7a78867f8..ebbdb41884 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -20,15 +20,13 @@ describe('mockApis', () => { describe('config', () => { const data = { backend: { baseUrl: 'http://test.com' } }; - it('can create an instance and make assertions on it', () => { + it('can create an instance', () => { const empty = mockApis.config(); const notEmpty = mockApis.config({ data }); expect(empty.getOptional('backend.baseUrl')).toBeUndefined(); - expect(empty.getOptional).toHaveBeenCalledTimes(1); expect(notEmpty.getOptional('backend.baseUrl')).toEqual( 'http://test.com', ); - expect(notEmpty.getOptional).toHaveBeenCalledTimes(1); }); it('can create a mock and make assertions on it', async () => { diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index d084fae332..eed5a88012 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -18,26 +18,13 @@ import { ConfigReader } from '@backstage/config'; import { ApiFactory, ApiRef, + ConfigApi, configApiRef, createApiFactory, } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { ApiMock } from './ApiMock'; -/** @internal */ -function simpleInstance( - _ref: ApiRef, - instance: TApi, - mockSkeleton: () => jest.Mocked, -): jest.Mocked { - const mock = mockSkeleton(); - const result = Object.create(instance) as any; - for (const [key, impl] of Object.entries(mock)) { - result[key] = (impl as any).mockImplementation((instance as any)[key]); - } - return result; -} - /** @internal */ function simpleFactory( ref: ApiRef, @@ -116,24 +103,6 @@ function simpleMock( * ``` */ export namespace mockApis { - const configMockSkeleton = () => ({ - has: jest.fn(), - keys: jest.fn(), - get: jest.fn(), - getOptional: jest.fn(), - getConfig: jest.fn(), - getOptionalConfig: jest.fn(), - getConfigArray: jest.fn(), - getOptionalConfigArray: jest.fn(), - getNumber: jest.fn(), - getOptionalNumber: jest.fn(), - getBoolean: jest.fn(), - getOptionalBoolean: jest.fn(), - getString: jest.fn(), - getOptionalString: jest.fn(), - getStringArray: jest.fn(), - getOptionalStringArray: jest.fn(), - }); /** * Fake implementation of {@link @backstage/frontend-plugin-api#ConfigApi} * with optional data supplied. @@ -153,12 +122,8 @@ export namespace mockApis { * ); * ``` */ - export function config(options?: { data?: JsonObject }) { - return simpleInstance( - configApiRef, - new ConfigReader(options?.data, 'mock-config'), - configMockSkeleton, - ); + export function config(options?: { data?: JsonObject }): ConfigApi { + return new ConfigReader(options?.data, 'mock-config'); } /** * Mock helpers for {@link @backstage/frontend-plugin-api#ConfigApi}. @@ -183,6 +148,23 @@ export namespace mockApis { * * @public */ - export const mock = simpleMock(configApiRef, configMockSkeleton); + export const mock = simpleMock(configApiRef, () => ({ + has: jest.fn(), + keys: jest.fn(), + get: jest.fn(), + getOptional: jest.fn(), + getConfig: jest.fn(), + getOptionalConfig: jest.fn(), + getConfigArray: jest.fn(), + getOptionalConfigArray: jest.fn(), + getNumber: jest.fn(), + getOptionalNumber: jest.fn(), + getBoolean: jest.fn(), + getOptionalBoolean: jest.fn(), + getString: jest.fn(), + getOptionalString: jest.fn(), + getStringArray: jest.fn(), + getOptionalStringArray: jest.fn(), + })); } } From b5b19eee634d8073d0c30517b17c156adf8db543 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 03:54:35 +0000 Subject: [PATCH 05/22] chore(deps): update actions/upload-artifact digest to b4b15b8 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/api-breaking-changes.yml | 2 +- .github/workflows/deploy_microsite.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/api-breaking-changes.yml b/.github/workflows/api-breaking-changes.yml index 1c2596bad6..7b98c9e5e0 100644 --- a/.github/workflows/api-breaking-changes.yml +++ b/.github/workflows/api-breaking-changes.yml @@ -46,7 +46,7 @@ jobs: cat ${{ github.event_path }} > event.json - name: Upload Artifacts - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 with: name: preview-spec path: | diff --git a/.github/workflows/deploy_microsite.yml b/.github/workflows/deploy_microsite.yml index 7dad0d3b20..c3c0ad7286 100644 --- a/.github/workflows/deploy_microsite.yml +++ b/.github/workflows/deploy_microsite.yml @@ -73,7 +73,7 @@ jobs: run: yarn build:api-docs - name: upload API reference - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 with: name: stable-reference path: docs/reference/ @@ -113,7 +113,7 @@ jobs: run: yarn build:api-docs - name: upload API reference - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 with: name: next-reference path: docs/reference/ @@ -130,7 +130,7 @@ jobs: working-directory: storybook - name: storybook upload - uses: actions/upload-artifact@604373da6381bf24206979c74d06a550515601b9 # v4 + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4 with: name: storybook path: storybook/dist/ From 40bfc240c706e53bdd7c68ae4453196c9d2b58e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 11 Oct 2024 09:48:21 +0200 Subject: [PATCH 06/22] fix report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/test-utils/report.api.md | 2 -- .../test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 15625adb21..38f4b5f69c 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -107,8 +107,6 @@ export namespace mockApis { // @public @deprecated export function mockBreakpoint(options: { matches: boolean }): void; -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "config" has more than one declaration; you need to add a TSDoc member reference selector -// // @public @deprecated export class MockConfigApi implements ConfigApi { constructor(data: JsonObject); diff --git a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts index 9d7462889c..3c5329962a 100644 --- a/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts +++ b/packages/test-utils/src/testUtils/apis/ConfigApi/MockConfigApi.ts @@ -23,7 +23,7 @@ import { ConfigApi } from '@backstage/core-plugin-api'; * that can be used to mock configuration using a plain object. * * @public - * @deprecated Use {@link mockApis.config} instead + * @deprecated Use {@link mockApis.(config:namespace)} instead * @example * ```tsx * const mockConfig = new MockConfigApi({ From 811ff0cddc2d5ad29051a8d3ca60cecbc6ad7874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 8 Oct 2024 17:51:14 +0200 Subject: [PATCH 07/22] implement analytics too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/thin-chairs-ring.md | 3 +- .../core-app-api/src/app/AppManager.test.tsx | 17 ++++---- .../src/routing/RouteTracker.test.tsx | 8 ++-- .../src/components/Link/Link.test.tsx | 24 ++++++------ .../src/layout/Sidebar/Items.test.tsx | 16 +++----- .../src/routing/RouteTracker.test.tsx | 1 + .../src/components/ExtensionBoundary.test.tsx | 32 +++++++-------- packages/test-utils/report.api.md | 21 ++++++++-- .../apis/AnalyticsApi/MockAnalyticsApi.ts | 1 + .../test-utils/src/testUtils/apis/mockApis.ts | 13 +++++++ .../CatalogGraphCard.test.tsx | 22 ++++++----- .../CatalogGraphPage.test.tsx | 36 +++++++++-------- .../src/hooks/useEntity.test.tsx | 28 +++++++------ .../components/Workflow/Workflow.test.tsx | 4 +- .../TemplateWizardPage.test.tsx | 39 ++++++++++++------- .../components/SearchBar/SearchBar.test.tsx | 6 +-- .../SearchResultGroup.test.tsx | 4 +- .../SearchResultList.test.tsx | 4 +- .../src/context/SearchContext.test.tsx | 8 +--- plugins/search-react/src/extensions.test.tsx | 8 ++-- .../src/ReportIssue/IssueLink.test.tsx | 20 +++++----- plugins/techdocs-react/src/context.test.tsx | 14 +++---- .../components/TechDocsNotFound.test.tsx | 20 +++++----- 23 files changed, 196 insertions(+), 153 deletions(-) diff --git a/.changeset/thin-chairs-ring.md b/.changeset/thin-chairs-ring.md index ebcb3afa93..df388a7365 100644 --- a/.changeset/thin-chairs-ring.md +++ b/.changeset/thin-chairs-ring.md @@ -5,4 +5,5 @@ Added a `mockApis` export, which will replace the `MockX` API implementation classes and their related types. This is analogous with the backend's `mockServices`. -Deprecated `MockConfigApi`, please use `mockApis.config` instead. +- Deprecated `MockAnalyticsApi`, please use `mockApis.analytics` instead. +- Deprecated `MockConfigApi`, please use `mockApis.config` instead. diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 01277a09c7..1b7d2d28cb 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import { LocalStorageFeatureFlags, NoOpAnalyticsApi } from '../apis'; +import { LocalStorageFeatureFlags } from '../apis'; import { - MockAnalyticsApi, + mockApis, renderWithEffects, withLogCollector, registerMswTestHooks, @@ -59,7 +59,7 @@ describe('Integration Test', () => { const noOpAnalyticsApi = createApiFactory( analyticsApiRef, - new NoOpAnalyticsApi(), + mockApis.analytics(), ); const noopErrorApi = createApiFactory(errorApiRef, { error$() { @@ -575,7 +575,7 @@ describe('Integration Test', () => { }); it('should track route changes via analytics api', async () => { - const mockAnalyticsApi = new MockAnalyticsApi(); + const mockAnalyticsApi = mockApis.analytics(); const apis = [createApiFactory(analyticsApiRef, mockAnalyticsApi)]; const app = new AppManager({ apis, @@ -608,26 +608,27 @@ describe('Integration Test', () => { ); // Capture initial and subsequent navigation events with expected context. - const capturedEvents = mockAnalyticsApi.getEvents(); - expect(capturedEvents[0]).toMatchObject({ + expect(mockAnalyticsApi.captureEvent).toHaveBeenCalledTimes(2); + expect(mockAnalyticsApi.captureEvent).toHaveBeenNthCalledWith(1, { action: 'navigate', subject: '/', + attributes: {}, context: { extension: 'App', pluginId: 'blob', routeRef: 'ref-1-2', }, }); - expect(capturedEvents[1]).toMatchObject({ + expect(mockAnalyticsApi.captureEvent).toHaveBeenNthCalledWith(2, { action: 'navigate', subject: '/foo', + attributes: {}, context: { extension: 'App', pluginId: 'plugin2', routeRef: 'ref-2', }, }); - expect(capturedEvents).toHaveLength(2); }); it('should throw some error when the route has duplicate params', async () => { diff --git a/packages/core-app-api/src/routing/RouteTracker.test.tsx b/packages/core-app-api/src/routing/RouteTracker.test.tsx index 25d5cfbcfb..ecbd44315d 100644 --- a/packages/core-app-api/src/routing/RouteTracker.test.tsx +++ b/packages/core-app-api/src/routing/RouteTracker.test.tsx @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { TestApiProvider } from '@backstage/test-utils'; + +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import React from 'react'; import { BackstageRouteObject } from './types'; import { fireEvent, render } from '@testing-library/react'; import { RouteTracker } from './RouteTracker'; import { Link, MemoryRouter, Route, Routes } from 'react-router-dom'; import { - AnalyticsApi, analyticsApiRef, createPlugin, createRouteRef, @@ -68,9 +68,7 @@ describe('RouteTracker', () => { }, ]; - const mockedAnalytics: jest.Mocked = { - captureEvent: jest.fn(), - }; + const mockedAnalytics = mockApis.analytics(); beforeEach(() => { jest.clearAllMocks(); diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx index fe616299c3..0a86c06d43 100644 --- a/packages/core-components/src/components/Link/Link.test.tsx +++ b/packages/core-components/src/components/Link/Link.test.tsx @@ -17,7 +17,7 @@ import React, { ComponentType } from 'react'; import { fireEvent, waitFor, screen, renderHook } from '@testing-library/react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -71,7 +71,7 @@ describe('', () => { it('captures click using analytics api', async () => { const linkText = 'Navigate!'; - const analyticsApi = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); const customOnClick = jest.fn(); await renderInTestApp( @@ -86,13 +86,15 @@ describe('', () => { // Analytics event should have been fired. await waitFor(() => { - expect(analyticsApi.getEvents()[0]).toMatchObject({ - action: 'click', - subject: linkText, - attributes: { - to: '/test', - }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: linkText, + attributes: { + to: '/test', + }, + }), + ); // Custom onClick handler should have still been fired too. expect(customOnClick).toHaveBeenCalled(); @@ -101,7 +103,7 @@ describe('', () => { it('does not capture click when noTrack is set', async () => { const linkText = 'Navigate!'; - const analyticsApi = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); const customOnClick = jest.fn(); await renderInTestApp( @@ -120,7 +122,7 @@ describe('', () => { expect(customOnClick).toHaveBeenCalled(); // But there should be no analytics event. - expect(analyticsApi.getEvents()).toHaveLength(0); + expect(analyticsApi.captureEvent).not.toHaveBeenCalled(); }); }); diff --git a/packages/core-components/src/layout/Sidebar/Items.test.tsx b/packages/core-components/src/layout/Sidebar/Items.test.tsx index 6c959bfd0d..ee0f0ea401 100644 --- a/packages/core-components/src/layout/Sidebar/Items.test.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -36,9 +36,8 @@ const useStyles = makeStyles({ }, }); -let analyticsApiMock: MockAnalyticsApi; - const handleSidebarItemClick = jest.fn(); +const analyticsApiMock = mockApis.analytics(); async function renderSidebar() { const { result } = renderHook(() => useStyles()); @@ -79,7 +78,6 @@ async function renderSidebar() { describe('Items', () => { beforeEach(async () => { jest.clearAllMocks(); - analyticsApiMock = new MockAnalyticsApi(); await renderSidebar(); }); @@ -107,8 +105,7 @@ describe('Items', () => { await screen.findByRole('button', { name: /create/i }), ); expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); - expect(analyticsApiMock.getEvents()).toHaveLength(1); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'click', subject: 'Create...', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, @@ -119,8 +116,7 @@ describe('Items', () => { it('should send link clicks to analytics', async () => { await userEvent.click(await screen.findByRole('link', { name: /docs/i })); expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); - expect(analyticsApiMock.getEvents()).toHaveLength(1); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'click', subject: 'Docs', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, @@ -132,10 +128,10 @@ describe('Items', () => { await userEvent.click( await screen.findByRole('link', { name: /explore/i }), ); - expect(handleSidebarItemClick).toHaveBeenCalledTimes(1); - expect(analyticsApiMock.getEvents()).toHaveLength(0); + expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); }); + describe('SidebarSearchField', () => { it('should be defaultPrevented when enter is pressed', async () => { const searchEvent = createEvent.keyDown( diff --git a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx index aeec03992f..372afea4c3 100644 --- a/packages/frontend-app-api/src/routing/RouteTracker.test.tsx +++ b/packages/frontend-app-api/src/routing/RouteTracker.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { TestApiProvider } from '@backstage/test-utils'; import React, { useEffect } from 'react'; import { BackstageRouteObject } from './types'; diff --git a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx index 3e260f0c9b..14aaebb778 100644 --- a/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx +++ b/packages/frontend-plugin-api/src/components/ExtensionBoundary.test.tsx @@ -17,7 +17,7 @@ import React, { useEffect } from 'react'; import { act, screen, waitFor } from '@testing-library/react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, withLogCollector, } from '@backstage/test-utils'; @@ -93,7 +93,7 @@ describe('ExtensionBoundary', () => { it('should wrap children with analytics context', async () => { const action = 'render'; const subject = 'analytics'; - const analyticsApiMock = new MockAnalyticsApi(); + const analyticsApiMock = mockApis.analytics(); const AnalyticsComponent = () => { const analytics = useAnalytics(); @@ -112,17 +112,15 @@ describe('ExtensionBoundary', () => { ); await waitFor(() => { - const event = analyticsApiMock - .getEvents() - .find(e => e.subject === subject); - - expect(event).toMatchObject({ - action, - subject, - context: { - extensionId: 'test', - }, - }); + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action, + subject, + context: expect.objectContaining({ + extensionId: 'test', + }), + }), + ); }); }); @@ -136,7 +134,7 @@ describe('ExtensionBoundary', () => { }); return null; }; - const analyticsApiMock = new MockAnalyticsApi(); + const analyticsApiMock = mockApis.analytics(); await act(async () => { renderInTestApp( @@ -147,7 +145,7 @@ describe('ExtensionBoundary', () => { ); }); - expect(analyticsApiMock.getEvents()).toEqual([ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith( expect.objectContaining({ action: 'navigate', subject: '/', @@ -156,7 +154,9 @@ describe('ExtensionBoundary', () => { extensionId: 'test', }), }), + ); + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith( expect.objectContaining({ action: 'dummy' }), - ]); + ); }); }); diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 38f4b5f69c..6e614321b6 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -81,7 +81,9 @@ export type LogCollector = AsyncLogCollector | SyncLogCollector; // @public export type LogFuncs = 'log' | 'warn' | 'error'; -// @public +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "analytics" has more than one declaration; you need to add a TSDoc member reference selector +// +// @public @deprecated export class MockAnalyticsApi implements AnalyticsApi { // (undocumented) captureEvent(event: AnalyticsEvent): void; @@ -91,6 +93,15 @@ export class MockAnalyticsApi implements AnalyticsApi { // @public export namespace mockApis { + // (undocumented) + export function analytics(): jest.Mocked; + // (undocumented) + export namespace analytics { + const // (undocumented) + factory: () => ApiFactory; + const // (undocumented) + mock: () => jest.Mocked; + } export function config(options?: { data?: JsonObject }): ConfigApi; export namespace config { const factory: ( @@ -303,8 +314,8 @@ export function wrapInTestApp( // Warnings were encountered during analysis: // // src/deprecated.d.ts:5:1 - (ae-undocumented) Missing documentation for "setupRequestMockHandlers". -// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:10:5 - (ae-undocumented) Missing documentation for "captureEvent". -// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "getEvents". +// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "captureEvent". +// src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.d.ts:12:5 - (ae-undocumented) Missing documentation for "getEvents". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:28:5 - (ae-undocumented) Missing documentation for "post". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:29:5 - (ae-undocumented) Missing documentation for "error$". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:33:5 - (ae-undocumented) Missing documentation for "getErrors". @@ -316,4 +327,8 @@ export function wrapInTestApp( // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "set". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "remove". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "observe$". +// src/testUtils/apis/mockApis.d.ts:44:5 - (ae-undocumented) Missing documentation for "analytics". +// src/testUtils/apis/mockApis.d.ts:45:5 - (ae-undocumented) Missing documentation for "analytics". +// src/testUtils/apis/mockApis.d.ts:46:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:47:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts index 6da225df1c..8c3ef4b326 100644 --- a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -21,6 +21,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; * Use getEvents in tests to verify captured events. * * @public + * @deprecated Use {@link mockApis.analytics} instead */ export class MockAnalyticsApi implements AnalyticsApi { private events: AnalyticsEvent[] = []; diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index eed5a88012..f0bd519eac 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -16,9 +16,11 @@ import { ConfigReader } from '@backstage/config'; import { + AnalyticsApi, ApiFactory, ApiRef, ConfigApi, + analyticsApiRef, configApiRef, createApiFactory, } from '@backstage/core-plugin-api'; @@ -103,6 +105,17 @@ function simpleMock( * ``` */ export namespace mockApis { + const analyticsMockSkeleton = (): jest.Mocked => ({ + captureEvent: jest.fn(), + }); + export function analytics() { + return analyticsMockSkeleton(); + } + export namespace analytics { + export const factory = simpleFactory(analyticsApiRef, analytics); + export const mock = analyticsMockSkeleton; + } + /** * Fake implementation of {@link @backstage/frontend-plugin-api#ConfigApi} * with optional data supplied. diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 916653f600..22eff03843 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -24,7 +24,7 @@ import { } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiProvider, TestApiRegistry, @@ -212,9 +212,9 @@ describe('', () => { ], })); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -228,12 +228,14 @@ describe('', () => { expect(await screen.findByText('b:d/c')).toBeInTheDocument(); await userEvent.click(await screen.findByText('b:d/c')); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'b:d/c', - attributes: { - to: '/entity/{kind}/{namespace}/{name}', - }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'b:d/c', + attributes: { + to: '/entity/{kind}/{namespace}/{name}', + }, + }), + ); }); }); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index 9a081747ad..054986e595 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -23,7 +23,7 @@ import { analyticsApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -227,9 +227,9 @@ describe.skip('', () => { }), ); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -243,10 +243,12 @@ describe.skip('', () => { await userEvent.click(screen.getByText('b:d/e')); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'b:d/e', - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'b:d/e', + }), + ); }); test('should capture analytics event when navigating to entity', async () => { @@ -256,9 +258,9 @@ describe.skip('', () => { }), ); - const analyticsSpy = new MockAnalyticsApi(); + const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { @@ -274,12 +276,14 @@ describe.skip('', () => { await user.keyboard('{Shift>}'); await user.click(screen.getByText('b:d/e')); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'b:d/e', - attributes: { - to: '/entity/b/d/e', - }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'b:d/e', + attributes: { + to: '/entity/b/d/e', + }, + }), + ); }); }); diff --git a/plugins/catalog-react/src/hooks/useEntity.test.tsx b/plugins/catalog-react/src/hooks/useEntity.test.tsx index 2923ed828d..c0ad3c2111 100644 --- a/plugins/catalog-react/src/hooks/useEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.test.tsx @@ -25,7 +25,7 @@ import { import { Entity } from '@backstage/catalog-model'; import { analyticsApiRef, useAnalytics } from '@backstage/core-plugin-api'; import { - MockAnalyticsApi, + mockApis, TestApiRegistry, withLogCollector, } from '@backstage/test-utils'; @@ -57,7 +57,7 @@ describe('useEntity', () => { }); it('should provide entityRef analytics context', () => { - const analyticsSpy = new MockAnalyticsApi(); + const analyticsSpy = mockApis.analytics(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( @@ -69,9 +69,13 @@ describe('useEntity', () => { result.current.captureEvent('test', 'value'); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - context: { entityRef: 'mykind:default/my-entity' }, - }); + expect(analyticsSpy.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + context: expect.objectContaining({ + entityRef: 'mykind:default/my-entity', + }), + }), + ); }); }); @@ -127,7 +131,7 @@ describe('useAsyncEntity', () => { }); it('should provide entityRef analytics context', () => { - const analyticsSpy = new MockAnalyticsApi(); + const analyticsSpy = mockApis.analytics(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( @@ -144,13 +148,13 @@ describe('useAsyncEntity', () => { result.current.captureEvent('test', 'value'); - expect(analyticsSpy.getEvents()[0]).toMatchObject({ - context: { entityRef: 'mykind:default/my-entity' }, - }); + expect(analyticsSpy.captureEvent.mock.calls[0][0].context.entityRef).toBe( + 'mykind:default/my-entity', + ); }); it('should omit entityRef analytics context', () => { - const analyticsSpy = new MockAnalyticsApi(); + const analyticsSpy = mockApis.analytics(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: PropsWithChildren<{}>) => ( @@ -162,6 +166,8 @@ describe('useAsyncEntity', () => { result.current.captureEvent('test', 'value'); - expect(analyticsSpy.getEvents()[0].context).not.toHaveProperty('entityRef'); + expect( + analyticsSpy.captureEvent.mock.calls[0][0].context, + ).not.toHaveProperty('entityRef'); }); }); diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx index 49f5c1feb0..243b9b7e79 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx @@ -16,7 +16,7 @@ import { ApiProvider } from '@backstage/core-app-api'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; @@ -42,7 +42,7 @@ const scaffolderApiMock: jest.Mocked = { const catalogApi = catalogApiMock.mock(); -const analyticsMock = new MockAnalyticsApi(); +const analyticsMock = mockApis.analytics(); const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [catalogApiRef, catalogApi], diff --git a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx index 750efd70d0..18211d185a 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.test.tsx @@ -17,7 +17,7 @@ import { ApiProvider } from '@backstage/core-app-api'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { - MockAnalyticsApi, + mockApis, renderInTestApp, TestApiRegistry, } from '@backstage/test-utils'; @@ -56,12 +56,12 @@ const scaffolderApiMock: jest.Mocked = { }; const catalogApi = catalogApiMock.mock(); +const analyticsApi = mockApis.analytics(); -const analyticsMock = new MockAnalyticsApi(); const apis = TestApiRegistry.from( [scaffolderApiRef, scaffolderApiMock], [catalogApiRef, catalogApi], - [analyticsApiRef, analyticsMock], + [analyticsApiRef, analyticsApi], [catalogApiRef, catalogApi], ); @@ -81,6 +81,7 @@ const entityRefResponse = { }, }, }; + describe('TemplateWizardPage', () => { it('captures expected analytics events', async () => { scaffolderApiMock.scaffold.mockResolvedValue({ taskId: 'xyz' }); @@ -130,20 +131,29 @@ describe('TemplateWizardPage', () => { }); // The "Next Step" button should have fired an event - expect(analyticsMock.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'Next Step (1)', - context: { entityRef: 'template:default/test' }, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'Next Step (1)', + context: expect.objectContaining({ + entityRef: 'template:default/test', + }), + }), + ); // And the "Create" button should have fired an event - expect(analyticsMock.getEvents()[1]).toMatchObject({ - action: 'create', - subject: 'expected-name', - context: { entityRef: 'template:default/test' }, - value: 120, - }); + expect(analyticsApi.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'create', + subject: 'expected-name', + context: expect.objectContaining({ + entityRef: 'template:default/test', + }), + value: 120, + }), + ); }); + describe('scaffolder page context menu', () => { it('should render if editUrl is set to url', async () => { catalogApi.getEntityByRef.mockResolvedValue({ @@ -175,6 +185,7 @@ describe('TemplateWizardPage', () => { ); expect(queryByTestId('menu-button')).toBeInTheDocument(); }); + it('should not render if editUrl is undefined', async () => { catalogApi.getEntityByRef.mockResolvedValue({ apiVersion: 'v1', diff --git a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx index 3f667a4ad6..0e4df4fb34 100644 --- a/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx +++ b/plugins/search-react/src/components/SearchBar/SearchBar.test.tsx @@ -20,7 +20,7 @@ import userEvent from '@testing-library/user-event'; import { configApiRef } from '@backstage/core-plugin-api'; import { ConfigReader } from '@backstage/core-app-api'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -273,7 +273,7 @@ describe('SearchBar', () => { }); it('Does not capture analytics event if not enabled in app', async () => { - const analyticsApiMock = new MockAnalyticsApi(); + const analyticsApiMock = mockApis.analytics(); await renderInTestApp( { await waitFor(() => expect(textbox).toHaveValue(value)); - expect(analyticsApiMock.getEvents()).toHaveLength(0); + expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled(); }); it('Renders custom search icon', async () => { diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx index 66513783d4..fe1e279dd3 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx @@ -24,7 +24,7 @@ import DocsIcon from '@material-ui/icons/InsertDriveFile'; import { renderInTestApp, TestApiProvider, - MockAnalyticsApi, + mockApis, } from '@backstage/test-utils'; import { createPlugin, analyticsApiRef } from '@backstage/core-plugin-api'; @@ -40,7 +40,7 @@ import { const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); describe('SearchResultGroup', () => { const results = [ diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx index 146e5822a4..86ce237135 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx @@ -20,7 +20,7 @@ import { screen, waitFor } from '@testing-library/react'; import { TestApiProvider, renderInTestApp, - MockAnalyticsApi, + mockApis, } from '@backstage/test-utils'; import { analyticsApiRef, createPlugin } from '@backstage/core-plugin-api'; @@ -31,7 +31,7 @@ import { SearchResultList } from './SearchResultList'; const query = jest.fn().mockResolvedValue({ results: [] }); const searchApiMock = { query }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); describe('SearchResultList', () => { const results = [ diff --git a/plugins/search-react/src/context/SearchContext.test.tsx b/plugins/search-react/src/context/SearchContext.test.tsx index 2cb83e5ab6..f858715adb 100644 --- a/plugins/search-react/src/context/SearchContext.test.tsx +++ b/plugins/search-react/src/context/SearchContext.test.tsx @@ -422,9 +422,7 @@ describe('SearchContext', () => { describe('analytics', () => { it('captures analytics events if enabled in app', async () => { - const analyticsApiMock = { - captureEvent: jest.fn(), - } satisfies typeof analyticsApiRef.T; + const analyticsApiMock = mockApis.analytics(); searchApiMock.query.mockResolvedValue({ results: [], @@ -481,9 +479,7 @@ describe('SearchContext', () => { }); it('captures analytics events even if number of results does not exist', async () => { - const analyticsApiMock = { - captureEvent: jest.fn(), - } satisfies typeof analyticsApiRef.T; + const analyticsApiMock = mockApis.analytics(); searchApiMock.query.mockResolvedValue({ results: [], diff --git a/plugins/search-react/src/extensions.test.tsx b/plugins/search-react/src/extensions.test.tsx index 170dca2b6d..8305f951ef 100644 --- a/plugins/search-react/src/extensions.test.tsx +++ b/plugins/search-react/src/extensions.test.tsx @@ -23,7 +23,7 @@ import ListItemText from '@material-ui/core/ListItemText'; import { renderInTestApp, TestApiProvider, - MockAnalyticsApi, + mockApis, } from '@backstage/test-utils'; import { createPlugin, @@ -38,7 +38,7 @@ import { SearchResultListItemExtensionOptions, } from './extensions'; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); const results = [ { @@ -118,7 +118,7 @@ describe('extensions', () => { screen.getByRole('link', { name: /Search Result 1/ }), ); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'discover', subject: 'Search Result 1', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, @@ -141,7 +141,7 @@ describe('extensions', () => { await userEvent.click(screen.getByRole('listitem')); - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'discover', subject: 'Search Result 1', context: { routeRef: 'unknown', pluginId: 'root', extension: 'App' }, diff --git a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx index 85ffaa5782..2bc770cc31 100644 --- a/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx +++ b/plugins/techdocs-module-addons-contrib/src/ReportIssue/IssueLink.test.tsx @@ -19,7 +19,7 @@ import { screen, fireEvent, waitFor } from '@testing-library/react'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -55,11 +55,11 @@ const defaultGitlabProps = { }; describe('FeedbackLink', () => { - const apiSpy = new MockAnalyticsApi(); + const analytics = mockApis.analytics(); it('Should open new Github issue tab', async () => { await renderInTestApp( - + , ); @@ -77,7 +77,7 @@ describe('FeedbackLink', () => { it('Should open new Gitlab issue tab', async () => { await renderInTestApp( - + , ); @@ -95,7 +95,7 @@ describe('FeedbackLink', () => { it('Should track click events', async () => { await renderInTestApp( - + , ); @@ -103,10 +103,12 @@ describe('FeedbackLink', () => { fireEvent.click(screen.getByText(/Open new Github issue/)); await waitFor(() => { - expect(apiSpy.getEvents()[0]).toMatchObject({ - action: 'click', - subject: 'Open new Github issue', - }); + expect(analytics.captureEvent).toHaveBeenCalledWith( + expect.objectContaining({ + action: 'click', + subject: 'Open new Github issue', + }), + ); }); }); }); diff --git a/plugins/techdocs-react/src/context.test.tsx b/plugins/techdocs-react/src/context.test.tsx index 6c947b6858..6605d9ceaf 100644 --- a/plugins/techdocs-react/src/context.test.tsx +++ b/plugins/techdocs-react/src/context.test.tsx @@ -20,11 +20,7 @@ import { renderHook, act, waitFor } from '@testing-library/react'; import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; -import { - MockAnalyticsApi, - mockApis, - TestApiProvider, -} from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { Entity, CompoundEntityRef } from '@backstage/catalog-model'; import { analyticsApiRef, @@ -66,7 +62,7 @@ const techdocsApiMock = { getTechDocsMetadata: jest.fn().mockResolvedValue(mockTechDocsMetadata), }; -const analyticsApiMock = new MockAnalyticsApi(); +const analyticsApiMock = mockApis.analytics(); const wrapper = ({ entityRef = { @@ -170,12 +166,12 @@ describe('useTechDocsReaderPage', () => { wrapper, }); await waitFor(() => { - expect(analyticsApiMock.getEvents()[0]).toMatchObject({ + expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({ action: 'action', subject: 'subject', - context: { + context: expect.objectContaining({ entityRef: 'component:default/test', - }, + }), }); }); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index 59c388df69..84c7275228 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -16,12 +16,11 @@ import { TechDocsNotFound } from './TechDocsNotFound'; import React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { screen, waitFor } from '@testing-library/react'; import { - MockAnalyticsApi, + mockApis, TestApiProvider, renderInTestApp, - wrapInTestApp, } from '@backstage/test-utils'; import { analyticsApiRef } from '@backstage/core-plugin-api'; @@ -58,18 +57,16 @@ describe('', () => { }); it('should trigger analytics event not-found', async () => { - const mockAnalyticsApi = new MockAnalyticsApi(); + const mockAnalyticsApi = mockApis.analytics(); - render( - wrapInTestApp( - - - , - ), + await renderInTestApp( + + + , ); await waitFor(() => { - expect(mockAnalyticsApi.getEvents()[0]).toMatchObject({ + expect(mockAnalyticsApi.captureEvent).toHaveBeenCalledWith({ action: 'not-found', subject: '/the/pathname?the=search#the-anchor', attributes: { @@ -77,6 +74,7 @@ describe('', () => { namespace: 'namespace', kind: 'kind', }, + context: expect.anything(), }); }); }); From b52715bc2e4122a229b5fb4faa450354bbf124ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 8 Oct 2024 22:49:42 +0200 Subject: [PATCH 08/22] implement identity too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- ...dentityAuthInjectorFetchMiddleware.test.ts | 17 +-- .../core-app-api/src/app/AppRouter.test.tsx | 11 +- .../components/AutoLogout/Autologout.test.tsx | 14 +- .../src/createPublicSignInApp.test.tsx | 7 +- packages/test-utils/report.api.md | 37 +++++- .../src/testUtils/apis/mockApis.test.tsx | 124 +++++++++++++++++- .../test-utils/src/testUtils/apis/mockApis.ts | 48 ++++++- .../CookieAuthRedirect.test.tsx | 8 +- .../UserListPicker/UserListPicker.test.tsx | 19 +-- .../useOwnedEntitiesCount.test.tsx | 45 +++---- .../src/hooks/useEntityListProvider.test.tsx | 24 ++-- .../src/hooks/useEntityOwnership.test.tsx | 21 +-- .../CatalogPage/DefaultCatalogPage.test.tsx | 23 +--- plugins/home/src/api/VisitsStorageApi.test.ts | 15 +-- .../home/src/api/VisitsWebStorageApi.test.ts | 14 +- .../MyGroupsSidebarItem.test.tsx | 63 ++++----- .../ListTasksPage/ListTaskPage.test.tsx | 13 +- .../columns/OwnerEntityColumn.test.tsx | 14 +- .../MyGroupsPicker/MyGroupsPicker.test.tsx | 31 ++--- plugins/signals/src/api/SignalsClient.test.ts | 11 +- plugins/techdocs/src/client.test.ts | 5 +- .../StorageApi/UserSettingsStorage.test.ts | 22 ++-- .../DefaultSettingsPage.test.tsx | 13 +- .../General/UserSettingsIdentityCard.test.tsx | 26 ++-- .../General/UserSettingsMenu.test.tsx | 7 +- .../General/UserSettingsProfileCard.test.tsx | 33 ++--- .../SettingsPage/SettingsPage.test.tsx | 19 +-- 27 files changed, 396 insertions(+), 288 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts index 1e29f2548a..f1daced2a1 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts @@ -15,8 +15,8 @@ */ import { ConfigReader } from '@backstage/config'; -import { IdentityApi } from '@backstage/core-plugin-api'; import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware'; +import { mockApis } from '@backstage/test-utils'; describe('IdentityAuthInjectorFetchMiddleware', () => { it('creates using defaults', async () => { @@ -58,10 +58,7 @@ describe('IdentityAuthInjectorFetchMiddleware', () => { }); it('injects the header only when a token is available', async () => { - const tokenFunction = jest.fn(); - const identityApi = { - getCredentials: tokenFunction, - } as unknown as IdentityApi; + const identityApi = mockApis.identity(); const middleware = new IdentityAuthInjectorFetchMiddleware( identityApi, @@ -73,27 +70,25 @@ describe('IdentityAuthInjectorFetchMiddleware', () => { const outer = middleware.apply(inner); // No token available - tokenFunction.mockResolvedValueOnce({ token: undefined }); + identityApi.getCredentials.mockResolvedValueOnce({ token: undefined }); await outer(new Request('https://example.com')); expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]); // Supply a token, header gets added - tokenFunction.mockResolvedValueOnce({ token: 'token' }); + identityApi.getCredentials.mockResolvedValueOnce({ token: 'token' }); await outer(new Request('https://example.com')); expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ ['authorization', 'Bearer token'], ]); // Token no longer available - tokenFunction.mockResolvedValueOnce({ token: undefined }); + identityApi.getCredentials.mockResolvedValueOnce({ token: undefined }); await outer(new Request('https://example.com')); expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]); }); it('does not overwrite an existing header with the same name', async () => { - const identityApi = { - getCredentials: () => ({ token: 'token' }), - } as unknown as IdentityApi; + const identityApi = mockApis.identity({ token: 'token' }); const middleware = new IdentityAuthInjectorFetchMiddleware( identityApi, diff --git a/packages/core-app-api/src/app/AppRouter.test.tsx b/packages/core-app-api/src/app/AppRouter.test.tsx index 9f3ab2ad89..b595f91794 100644 --- a/packages/core-app-api/src/app/AppRouter.test.tsx +++ b/packages/core-app-api/src/app/AppRouter.test.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { AppComponents, configApiRef, - IdentityApi, identityApiRef, SignInPageProps, useApi, @@ -30,7 +29,7 @@ import { render, screen } from '@testing-library/react'; import { AppRouter } from './AppRouter'; import useAsync from 'react-use/esm/useAsync'; import { AppContextProvider } from './AppContext'; -import { TestApiProvider } from '@backstage/test-utils'; +import { mockApis, TestApiProvider } from '@backstage/test-utils'; import { ConfigReader } from '@backstage/config'; function UserRefDisplay() { @@ -78,13 +77,7 @@ describe('AppRouter', () => { const appIdentityProxy = new AppIdentityProxy(); const SignInPage = (props: SignInPageProps) => { - props.onSignInSuccess({ - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/test', - ownershipEntityRefs: ['user:default/test'], - }), - } as IdentityApi); + props.onSignInSuccess(mockApis.identity()); return null; }; diff --git a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx index 164ad716d2..5a555cd215 100644 --- a/packages/core-components/src/components/AutoLogout/Autologout.test.tsx +++ b/packages/core-components/src/components/AutoLogout/Autologout.test.tsx @@ -13,23 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createMocks } from 'react-idle-timer'; // eslint-disable-next-line no-restricted-imports import { MessageChannel } from 'worker_threads'; import { ApiProvider } from '@backstage/core-app-api'; import { identityApiRef } from '@backstage/core-plugin-api'; -import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiRegistry, + renderInTestApp, + mockApis, +} from '@backstage/test-utils'; import React from 'react'; import { AutoLogout } from './AutoLogout'; import { cleanup } from '@testing-library/react'; -// Mock the signOut function of identityApiRef -const mockSignOut = jest.fn(); -const mockIdentityApi = { - signOut: mockSignOut, - getCredentials: jest.fn().mockReturnValue({ token: 'xxx' }), -}; +const mockIdentityApi = mockApis.identity({ token: 'xxx' }); const apis = TestApiRegistry.from([identityApiRef, mockIdentityApi]); describe('AutoLogout', () => { diff --git a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx index ce0f8c4b2a..560abc602f 100644 --- a/packages/frontend-defaults/src/createPublicSignInApp.test.tsx +++ b/packages/frontend-defaults/src/createPublicSignInApp.test.tsx @@ -15,7 +15,6 @@ */ import { - IdentityApi, SignInPageBlueprint, createFrontendModule, } from '@backstage/frontend-plugin-api'; @@ -70,9 +69,9 @@ describe('createPublicSignInApp', () => { async () => ({ onSignInSuccess }) => { useEffect(() => { - onSignInSuccess({ - getCredentials: async () => ({ token: 'mock-token' }), - } as IdentityApi); + onSignInSuccess( + mockApis.identity({ token: 'mock-token' }), + ); }, [onSignInSuccess]); return
; }, diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 6e614321b6..f570aacde4 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -100,7 +100,9 @@ export namespace mockApis { const // (undocumented) factory: () => ApiFactory; const // (undocumented) - mock: () => jest.Mocked; + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; } export function config(options?: { data?: JsonObject }): ConfigApi; export namespace config { @@ -113,6 +115,35 @@ export namespace mockApis { ) => ApiFactory; const mock: (partialImpl?: Partial | undefined) => ApiMock; } + // (undocumented) + export function identity(options?: { + userEntityRef?: string; + ownershipEntityRefs?: string[]; + token?: string; + email?: string; + displayName?: string; + picture?: string; + }): jest.Mocked; + // (undocumented) + export namespace identity { + const // (undocumented) + factory: ( + options?: + | { + userEntityRef?: string | undefined; + ownershipEntityRefs?: string[] | undefined; + token?: string | undefined; + email?: string | undefined; + displayName?: string | undefined; + picture?: string | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } } // @public @deprecated @@ -331,4 +362,8 @@ export function wrapInTestApp( // src/testUtils/apis/mockApis.d.ts:45:5 - (ae-undocumented) Missing documentation for "analytics". // src/testUtils/apis/mockApis.d.ts:46:15 - (ae-undocumented) Missing documentation for "factory". // src/testUtils/apis/mockApis.d.ts:47:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:98:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:106:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:107:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:115:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx index ebbdb41884..f9286897b4 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -17,13 +17,49 @@ import { mockApis } from './mockApis'; describe('mockApis', () => { + describe('analytics', () => { + it('can create an instance and make assertions on it', () => { + const analytics = mockApis.analytics(); + expect( + analytics.captureEvent({ + action: 'a', + subject: 'b', + context: { pluginId: 'c', extension: 'd', routeRef: 'e' }, + }), + ).toBeUndefined(); + expect(analytics.captureEvent).toHaveBeenCalledTimes(1); + }); + + it('can create a mock and make assertions on it', async () => { + expect.assertions(3); + const analytics = mockApis.analytics.mock({ + captureEvent: event => { + expect(event).toEqual({ + action: 'a', + subject: 'b', + context: { pluginId: 'c', extension: 'd', routeRef: 'e' }, + }); + }, + }); + expect( + analytics.captureEvent({ + action: 'a', + subject: 'b', + context: { pluginId: 'c', extension: 'd', routeRef: 'e' }, + }), + ).toBeUndefined(); + expect(analytics.captureEvent).toHaveBeenCalledTimes(1); + }); + }); + describe('config', () => { const data = { backend: { baseUrl: 'http://test.com' } }; it('can create an instance', () => { const empty = mockApis.config(); - const notEmpty = mockApis.config({ data }); expect(empty.getOptional('backend.baseUrl')).toBeUndefined(); + + const notEmpty = mockApis.config({ data }); expect(notEmpty.getOptional('backend.baseUrl')).toEqual( 'http://test.com', ); @@ -35,4 +71,90 @@ describe('mockApis', () => { expect(mock.getString).toHaveBeenCalledTimes(1); }); }); + + describe('identity', () => { + it('can create an instance and make assertions on it', async () => { + const empty = mockApis.identity(); + await expect(empty.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'user:default/test', + ownershipEntityRefs: ['user:default/test'], + }); + await expect(empty.getCredentials()).resolves.toEqual({}); + await expect(empty.getProfileInfo()).resolves.toEqual({}); + await expect(empty.signOut()).resolves.toBeUndefined(); + expect(empty.getBackstageIdentity).toHaveBeenCalledTimes(1); + expect(empty.getCredentials).toHaveBeenCalledTimes(1); + expect(empty.getProfileInfo).toHaveBeenCalledTimes(1); + expect(empty.signOut).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.identity({ + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + token: 'c', + email: 'd', + displayName: 'e', + picture: 'f', + }); + await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + }); + await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' }); + await expect(notEmpty.getProfileInfo()).resolves.toEqual({ + email: 'd', + displayName: 'e', + picture: 'f', + }); + await expect(notEmpty.signOut()).resolves.toBeUndefined(); + expect(notEmpty.getBackstageIdentity).toHaveBeenCalledTimes(1); + expect(notEmpty.getCredentials).toHaveBeenCalledTimes(1); + expect(notEmpty.getProfileInfo).toHaveBeenCalledTimes(1); + expect(notEmpty.signOut).toHaveBeenCalledTimes(1); + }); + + it('can create a mock and make assertions on it', async () => { + const empty = mockApis.identity.mock(); + expect(empty.getBackstageIdentity()).toBeUndefined(); + expect(empty.getCredentials()).toBeUndefined(); + expect(empty.getProfileInfo()).toBeUndefined(); + expect(empty.signOut()).toBeUndefined(); + expect(empty.getBackstageIdentity).toHaveBeenCalledTimes(1); + expect(empty.getCredentials).toHaveBeenCalledTimes(1); + expect(empty.getProfileInfo).toHaveBeenCalledTimes(1); + expect(empty.signOut).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.identity.mock({ + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + }), + getCredentials: async () => ({ token: 'c' }), + getProfileInfo: async () => ({ + email: 'd', + displayName: 'e', + picture: 'f', + }), + signOut: async () => undefined, + }); + await expect(notEmpty.getBackstageIdentity()).resolves.toEqual({ + type: 'user', + userEntityRef: 'a', + ownershipEntityRefs: ['b'], + }); + await expect(notEmpty.getCredentials()).resolves.toEqual({ token: 'c' }); + await expect(notEmpty.getProfileInfo()).resolves.toEqual({ + email: 'd', + displayName: 'e', + picture: 'f', + }); + await expect(notEmpty.signOut()).resolves.toBeUndefined(); + expect(notEmpty.getBackstageIdentity).toHaveBeenCalledTimes(1); + expect(notEmpty.getCredentials).toHaveBeenCalledTimes(1); + expect(notEmpty.getProfileInfo).toHaveBeenCalledTimes(1); + expect(notEmpty.signOut).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index f0bd519eac..e47ee4ec3e 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -20,9 +20,11 @@ import { ApiFactory, ApiRef, ConfigApi, + IdentityApi, analyticsApiRef, configApiRef, createApiFactory, + identityApiRef, } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { ApiMock } from './ApiMock'; @@ -113,7 +115,7 @@ export namespace mockApis { } export namespace analytics { export const factory = simpleFactory(analyticsApiRef, analytics); - export const mock = analyticsMockSkeleton; + export const mock = simpleMock(analyticsApiRef, analyticsMockSkeleton); } /** @@ -180,4 +182,48 @@ export namespace mockApis { getOptionalStringArray: jest.fn(), })); } + + const identityMockSkeleton = (): jest.Mocked => ({ + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), + getProfileInfo: jest.fn(), + signOut: jest.fn(), + }); + export function identity(options?: { + userEntityRef?: string; + ownershipEntityRefs?: string[]; + token?: string; + email?: string; + displayName?: string; + picture?: string; + }) { + const { + userEntityRef = 'user:default/test', + ownershipEntityRefs = ['user:default/test'], + token, + email, + displayName, + picture, + } = options ?? {}; + return simpleInstance( + identityApiRef, + { + async getBackstageIdentity() { + return { type: 'user', ownershipEntityRefs, userEntityRef }; + }, + async getCredentials() { + return { token }; + }, + async getProfileInfo() { + return { email, displayName, picture }; + }, + async signOut() {}, + }, + identityMockSkeleton, + ); + } + export namespace identity { + export const factory = simpleFactory(identityApiRef, identity); + export const mock = simpleMock(identityApiRef, identityMockSkeleton); + } } diff --git a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx index 6c0141a9d1..18c6408407 100644 --- a/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRedirect/CookieAuthRedirect.test.tsx @@ -16,12 +16,16 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiProvider, + renderInTestApp, + mockApis, +} from '@backstage/test-utils'; import { identityApiRef } from '@backstage/core-plugin-api'; import { CookieAuthRedirect } from './CookieAuthRedirect'; describe('CookieAuthRedirect', () => { - const identityApiMock = { getCredentials: jest.fn() }; + const identityApiMock = mockApis.identity.mock(); beforeEach(() => { jest.clearAllMocks(); diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 888940f30b..3f7c2034a0 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -36,13 +36,12 @@ import { catalogApiRef } from '../../api'; import { MockStorageApi, TestApiRegistry, + mockApis, renderInTestApp, } from '@backstage/test-utils'; import { ApiProvider } from '@backstage/core-app-api'; import { - ConfigApi, configApiRef, - IdentityApi, identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; @@ -61,15 +60,18 @@ const mockUser: UserEntity = { }, }; -const mockConfigApi = { - getOptionalString: () => 'Test Company', -} as Partial; +const ownershipEntityRefs = ['user:default/testuser']; + +const mockConfigApi = mockApis.config({ + data: { organization: { name: 'Test Company' } }, +}); const mockCatalogApi = catalogApiMock.mock(); -const mockIdentityApi = { - getBackstageIdentity: jest.fn(), -} as Partial>; +const mockIdentityApi = mockApis.identity({ + userEntityRef: ownershipEntityRefs[0], + ownershipEntityRefs, +}); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); @@ -81,7 +83,6 @@ const apis = TestApiRegistry.from( [starredEntitiesApiRef, mockStarredEntitiesApi], ); -const ownershipEntityRefs = ['user:default/testuser']; describe('', () => { const mockQueryEntitiesImplementation: CatalogApi['queryEntities'] = async request => { diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx index 4c620060fe..99b746c80c 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -23,11 +23,7 @@ import { useEntityList, } from '../../hooks'; import { catalogApiRef } from '../../api'; -import { - ApiRef, - IdentityApi, - identityApiRef, -} from '@backstage/core-plugin-api'; +import { ApiRef, identityApiRef } from '@backstage/core-plugin-api'; import { MemoryRouter } from 'react-router-dom'; import { useOwnedEntitiesCount } from './useOwnedEntitiesCount'; import { @@ -36,12 +32,13 @@ import { EntityUserFilter, } from '../../filters'; import { useMountEffect } from '@react-hookz/web'; +import { mockApis } from '@backstage/test-utils'; const mockCatalogApi = catalogApiMock.mock(); - -const mockGetBackstageIdentity: jest.MockedFn< - IdentityApi['getBackstageIdentity'] -> = jest.fn(); +const mockIdentityApi = mockApis.identity({ + ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], + userEntityRef: 'user:default/spiderman', +}); jest.mock('@backstage/core-plugin-api', () => { const actual = jest.requireActual('@backstage/core-plugin-api'); @@ -50,13 +47,9 @@ jest.mock('@backstage/core-plugin-api', () => { useApi: (ref: ApiRef) => { if (ref === catalogApiRef) { return mockCatalogApi; + } else if (ref === identityApiRef) { + return mockIdentityApi; } - if (ref === identityApiRef) { - return { - getBackstageIdentity: mockGetBackstageIdentity, - }; - } - return actual.useApi(ref); }, }; @@ -65,12 +58,6 @@ jest.mock('@backstage/core-plugin-api', () => { describe('useOwnedEntitiesCount', () => { beforeEach(() => { jest.clearAllMocks(); - - mockGetBackstageIdentity.mockResolvedValue({ - ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], - userEntityRef: 'user:default/spiderman', - type: 'user', - }); }); it(`shouldn't invoke queryEntities when filters are loading`, async () => { @@ -84,7 +71,9 @@ describe('useOwnedEntitiesCount', () => { wrapper: createWrapperWithInitialFilters({}), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await expect( waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()), @@ -114,7 +103,9 @@ describe('useOwnedEntitiesCount', () => { }), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ @@ -151,7 +142,9 @@ describe('useOwnedEntitiesCount', () => { }), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await expect( waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalled()), @@ -185,7 +178,9 @@ describe('useOwnedEntitiesCount', () => { }), }); - await waitFor(() => expect(mockGetBackstageIdentity).toHaveBeenCalled()); + await waitFor(() => + expect(mockIdentityApi.getBackstageIdentity).toHaveBeenCalled(), + ); await waitFor(() => expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 0bda2d19ec..067ca33fa3 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -18,14 +18,16 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { Entity } from '@backstage/catalog-model'; import { alertApiRef, - ConfigApi, configApiRef, errorApiRef, - IdentityApi, identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { MockStorageApi, TestApiProvider } from '@backstage/test-utils'; +import { + MockStorageApi, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { act, renderHook, waitFor } from '@testing-library/react'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; @@ -67,20 +69,14 @@ const entities: Entity[] = [ }, ]; -const mockConfigApi = { - getOptionalString: () => '', -} as Partial; +const mockConfigApi = mockApis.config(); const ownershipEntityRefs = ['user:default/guest']; -const mockIdentityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs, - }), - getCredentials: async () => ({ token: undefined }), -}; +const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs, +}); const mockCatalogApi = catalogApiMock.mock({ getEntities: jest.fn().mockResolvedValue({ items: entities }), queryEntities: jest.fn().mockResolvedValue({ diff --git a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx index 5516a249ea..684580792d 100644 --- a/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityOwnership.test.tsx @@ -15,20 +15,17 @@ */ import { ComponentEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; -import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; -import { TestApiProvider } from '@backstage/test-utils'; +import { identityApiRef } from '@backstage/core-plugin-api'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import { renderHook, waitFor } from '@testing-library/react'; import React from 'react'; import { useEntityOwnership } from './useEntityOwnership'; describe('useEntityOwnership', () => { - type MockIdentityApi = jest.Mocked>; - - const mockIdentityApi: MockIdentityApi = { - getBackstageIdentity: jest.fn(), - }; - - const identityApi = mockIdentityApi as unknown as IdentityApi; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/user1', + ownershipEntityRefs: ['user:default/user1', 'group:default/group1'], + }); const Wrapper = (props: { children?: React.ReactNode }) => ( @@ -64,12 +61,6 @@ describe('useEntityOwnership', () => { describe('useEntityOwnership', () => { it('matches ownership via ownership entity refs', async () => { - mockIdentityApi.getBackstageIdentity.mockResolvedValue({ - type: 'user', - userEntityRef: 'user:default/user1', - ownershipEntityRefs: ['user:default/user1', 'group:default/group1'], - }); - const { result } = renderHook(() => useEntityOwnership(), { wrapper: Wrapper, }); diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index f5a16e3af3..a5ced77749 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -17,12 +17,7 @@ import { QueryEntitiesInitialRequest } from '@backstage/catalog-client'; import { RELATION_OWNED_BY } from '@backstage/catalog-model'; import { TableColumn, TableProps } from '@backstage/core-components'; -import { - IdentityApi, - identityApiRef, - ProfileInfo, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { identityApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef, @@ -34,6 +29,7 @@ import { MockPermissionApi, MockStorageApi, TestApiProvider, + mockApis, renderInTestApp, } from '@backstage/test-utils'; import DashboardIcon from '@material-ui/icons/Dashboard'; @@ -166,18 +162,11 @@ describe('DefaultCatalogPage', () => { }), }); - const testProfile: Partial = { + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest', 'group:default/tools'], displayName: 'Display Name', - }; - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest', 'group:default/tools'], - }), - getCredentials: async () => ({ token: undefined }), - getProfileInfo: async () => testProfile, - }; + }); const storageApi = MockStorageApi.create(); const renderWrapped = (children: React.ReactNode) => diff --git a/plugins/home/src/api/VisitsStorageApi.test.ts b/plugins/home/src/api/VisitsStorageApi.test.ts index 5e8bc2dbfc..206cf42689 100644 --- a/plugins/home/src/api/VisitsStorageApi.test.ts +++ b/plugins/home/src/api/VisitsStorageApi.test.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; import { VisitsStorageApi } from './VisitsStorageApi'; -import { MockStorageApi } from '@backstage/test-utils'; +import { MockStorageApi, mockApis } from '@backstage/test-utils'; import { Visit, VisitsApi } from './VisitsApi'; describe('VisitsStorageApi.create', () => { @@ -26,13 +25,9 @@ describe('VisitsStorageApi.create', () => { () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; - const mockIdentityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: async () => - ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), - getCredentials: jest.fn(), - }; + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + }); beforeEach(() => { window.crypto.randomUUID = mockRandomUUID; @@ -40,7 +35,7 @@ describe('VisitsStorageApi.create', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); jest.useRealTimers(); window.localStorage.clear(); }); diff --git a/plugins/home/src/api/VisitsWebStorageApi.test.ts b/plugins/home/src/api/VisitsWebStorageApi.test.ts index 1cf1fa3129..8d8669e99e 100644 --- a/plugins/home/src/api/VisitsWebStorageApi.test.ts +++ b/plugins/home/src/api/VisitsWebStorageApi.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BackstageUserIdentity, IdentityApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import { VisitsWebStorageApi } from './VisitsWebStorageApi'; describe('VisitsWebStorageApi.create()', () => { @@ -24,13 +24,9 @@ describe('VisitsWebStorageApi.create()', () => { () => Math.floor(Math.random() * 16).toString(16), // 0x0 to 0xf ) as `${string}-${string}-${string}-${string}-${string}`; - const mockIdentityApi: IdentityApi = { - signOut: jest.fn(), - getProfileInfo: jest.fn(), - getBackstageIdentity: async () => - ({ userEntityRef: 'user:default/guest' } as BackstageUserIdentity), - getCredentials: jest.fn(), - }; + const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + }); const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; @@ -40,7 +36,7 @@ describe('VisitsWebStorageApi.create()', () => { afterEach(() => { window.localStorage.clear(); - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('instantiates with only identitiyApi', () => { diff --git a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx index 9eb9b3f70f..de46be98be 100644 --- a/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx +++ b/plugins/org/src/components/MyGroupsSidebarItem/MyGroupsSidebarItem.test.tsx @@ -14,11 +14,15 @@ * limitations under the License. */ -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import React from 'react'; import { MyGroupsSidebarItem } from './MyGroupsSidebarItem'; import GroupIcon from '@material-ui/icons/People'; -import { IdentityApi, identityApiRef } from '@backstage/core-plugin-api'; +import { identityApiRef } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; @@ -26,13 +30,10 @@ import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; describe('MyGroupsSidebarItem Test', () => { describe('For guests or users with no groups', () => { it('MyGroupsSidebarItem should be empty', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const catalogApi = catalogApiMock(); const rendered = await renderInTestApp( { describe('For users that are members of a single group', () => { it('MyGroupsSidebarItem should display a single item that links to their group', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/nigel.manning', - ownershipEntityRefs: ['user:default/nigel.manning'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/nigel.manning', + ownershipEntityRefs: ['user:default/nigel.manning'], + }); const catalogApi = catalogApiMock.mock({ getEntities: async () => ({ items: [ @@ -114,13 +112,10 @@ describe('MyGroupsSidebarItem Test', () => { describe('For users that are members of multiple groups', () => { it('MyGroupsSidebarItem should display a sub-menu with all their groups and a link to each group', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/nigel.manning', - ownershipEntityRefs: ['user:default/nigel.manning'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/nigel.manning', + ownershipEntityRefs: ['user:default/nigel.manning'], + }); const catalogApi = catalogApiMock.mock({ getEntities: async () => ({ items: [ @@ -192,13 +187,10 @@ describe('MyGroupsSidebarItem Test', () => { describe('When an additional filter is not provided', () => { it('catalogApi.getEntities() should be called with the default filter', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const mockCatalogApi = catalogApiMock.mock(); await renderInTestApp( { describe('When an additional filter is provided', () => { it('catalogApi.getEntities() should be called with an additional filter item', async () => { - const identityApi: Partial = { - getBackstageIdentity: async () => ({ - type: 'user', - userEntityRef: 'user:default/guest', - ownershipEntityRefs: ['user:default/guest'], - }), - }; + const identityApi = mockApis.identity({ + userEntityRef: 'user:default/guest', + ownershipEntityRefs: ['user:default/guest'], + }); const mockCatalogApi = catalogApiMock.mock(); await renderInTestApp( ', () => { const catalogApi = catalogApiMock.mock(); - const identityApi = { - getBackstageIdentity: jest.fn(), - getProfileInfo: jest.fn(), - getCredentials: jest.fn(), - signOut: jest.fn(), - }; + const identityApi = mockApis.identity(); const scaffolderApiMock: jest.Mocked> = { scaffold: jest.fn(), diff --git a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx index 35eb9ce0b7..bd5676ba31 100644 --- a/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx +++ b/plugins/scaffolder/src/components/ListTasksPage/columns/OwnerEntityColumn.test.tsx @@ -15,7 +15,11 @@ */ import { Entity } from '@backstage/catalog-model'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import React from 'react'; @@ -24,13 +28,7 @@ import { identityApiRef } from '@backstage/core-plugin-api'; describe('', () => { const catalogApi = catalogApiMock.mock(); - - const identityApi = { - getBackstageIdentity: jest.fn(), - getProfileInfo: jest.fn(), - getCredentials: jest.fn(), - signOut: jest.fn(), - }; + const identityApi = mockApis.identity(); it('should render the column with the user', async () => { const props = { diff --git a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx index d4a37d0752..57ce2d3023 100644 --- a/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/MyGroupsPicker/MyGroupsPicker.test.tsx @@ -18,7 +18,11 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { MyGroupsPicker } from './MyGroupsPicker'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiProvider, + mockApis, +} from '@backstage/test-utils'; import { catalogApiRef, entityPresentationApiRef, @@ -26,7 +30,6 @@ import { import { Entity } from '@backstage/catalog-model'; import { ErrorApi, - IdentityApi, errorApiRef, identityApiRef, } from '@backstage/core-plugin-api'; @@ -34,25 +37,9 @@ import userEvent from '@testing-library/user-event'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; -// Create a mock IdentityApi -const mockIdentityApi: IdentityApi = { - getProfileInfo: () => - Promise.resolve({ - displayName: 'Bob', - email: 'bob@example.com', - picture: 'https://example.com/picture.jpg', - }), - getBackstageIdentity: () => - Promise.resolve({ - id: 'Bob', - idToken: 'token', - type: 'user', - userEntityRef: 'user:default/bob', - ownershipEntityRefs: ['group:default/group1', 'group:default/group2'], - }), - getCredentials: () => Promise.resolve({ token: 'token' }), - signOut: () => Promise.resolve(), -}; +const mockIdentityApi = mockApis.identity({ + userEntityRef: 'user:default/bob', +}); describe('', () => { let entities: Entity[]; @@ -96,7 +83,7 @@ describe('', () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should only return the groups a user is part of and not the groups a user is not part of', async () => { diff --git a/plugins/signals/src/api/SignalsClient.test.ts b/plugins/signals/src/api/SignalsClient.test.ts index 2e93cd69f1..057b1fb23f 100644 --- a/plugins/signals/src/api/SignalsClient.test.ts +++ b/plugins/signals/src/api/SignalsClient.test.ts @@ -14,16 +14,14 @@ * limitations under the License. */ -import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import WS from 'jest-websocket-mock'; import { SignalClient } from './SignalClient'; describe('SignalsClient', () => { - const tokenFunction = jest.fn(); const baseUrlFunction = jest.fn(); - const identity = { - getCredentials: tokenFunction, - } as unknown as IdentityApi; + const identity = mockApis.identity({ token: '12345' }); const discoveryApi = { getBaseUrl: baseUrlFunction, } as unknown as DiscoveryApi; @@ -31,8 +29,7 @@ describe('SignalsClient', () => { let server: WS; beforeEach(async () => { - jest.resetAllMocks(); - tokenFunction.mockResolvedValue({ token: '12345' }); + jest.clearAllMocks(); baseUrlFunction.mockResolvedValue('http://localhost:1234'); server = new WS('ws://localhost:1234', { jsonProtocol: true }); }); diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index e2c68a7d73..8724e027f5 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -15,7 +15,6 @@ */ import { UrlPatternDiscovery } from '@backstage/core-app-api'; -import { IdentityApi } from '@backstage/core-plugin-api'; import { NotFoundError } from '@backstage/errors'; import { fetchEventSource } from '@microsoft/fetch-event-source'; import { mockApis, MockFetchApi } from '@backstage/test-utils'; @@ -36,9 +35,7 @@ describe('TechDocsStorageClient', () => { const mockBaseUrl = 'http://backstage:9191/api/techdocs'; const configApi = mockApis.config(); const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - const identityApi: jest.Mocked = { - getCredentials: jest.fn(), - } as unknown as jest.Mocked; + const identityApi = mockApis.identity(); const fetchApi = new MockFetchApi({ injectIdentityAuth: { identityApi } }); beforeEach(() => { diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts index 6bb0035b83..e54ac36624 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.test.ts @@ -18,10 +18,13 @@ import { DiscoveryApi, ErrorApi, FetchApi, - IdentityApi, StorageApi, } from '@backstage/core-plugin-api'; -import { MockFetchApi, registerMswTestHooks } from '@backstage/test-utils'; +import { + MockFetchApi, + mockApis, + registerMswTestHooks, +} from '@backstage/test-utils'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { UserSettingsStorage } from './UserSettingsStorage'; @@ -35,13 +38,8 @@ describe('Persistent Storage API', () => { const mockDiscoveryApi = { getBaseUrl: async () => mockBaseUrl, }; - const mockIdentityApi: Partial = { - getCredentials: async () => ({ token: 'a-token' }), - }; - const mockIdentityApiFallback: Partial = { - // This API recreates the guest mode, where the WebStorage is used as fallback - getCredentials: async () => ({}), - }; + const mockIdentityApi = mockApis.identity({ token: 'a-token' }); + const mockIdentityApiFallback = mockApis.identity(); const createPersistentStorage = ( args?: Partial<{ @@ -55,7 +53,7 @@ describe('Persistent Storage API', () => { errorApi: mockErrorApi, fetchApi: new MockFetchApi(), discoveryApi: mockDiscoveryApi, - identityApi: mockIdentityApi as IdentityApi, + identityApi: mockIdentityApi, ...args, }); }; @@ -72,13 +70,13 @@ describe('Persistent Storage API', () => { errorApi: mockErrorApi, fetchApi: new MockFetchApi(), discoveryApi: mockDiscoveryApi, - identityApi: mockIdentityApiFallback as IdentityApi, + identityApi: mockIdentityApiFallback, ...args, }); }; afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should return undefined for values which are unset', async () => { diff --git a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx index 14b7b7b48b..8a2e882aa8 100644 --- a/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx +++ b/plugins/user-settings/src/components/DefaultSettingsPage/DefaultSettingsPage.test.tsx @@ -20,16 +20,15 @@ import { DefaultSettingsPage } from './DefaultSettingsPage'; import { UserSettingsTab } from '../UserSettingsTab'; import { useOutlet } from 'react-router-dom'; import { SettingsLayout } from '../SettingsLayout'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useOutlet: jest.fn().mockReturnValue(undefined), })); -const catalogApiMock: jest.Mocked = { - getEntityByRef: jest.fn(), -} as any; +const catalogApi = catalogApiMock(); describe('', () => { beforeEach(() => { @@ -38,7 +37,7 @@ describe('', () => { it('should render the settings page with 3 tabs', async () => { const { container } = await renderInTestApp( - + , ); @@ -54,7 +53,7 @@ describe('', () => { ); const { container } = await renderInTestApp( - + , ); @@ -71,7 +70,7 @@ describe('', () => { ); const { container } = await renderInTestApp( - + , ); diff --git a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx index b1a811d7fa..6fe0ee01d7 100644 --- a/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsIdentityCard.test.tsx @@ -14,32 +14,28 @@ * limitations under the License. */ -import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils'; +import { + renderInTestApp, + TestApiRegistry, + mockApis, +} from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; import { UserSettingsIdentityCard } from './UserSettingsIdentityCard'; import { ApiProvider } from '@backstage/core-app-api'; import { identityApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; const apiRegistry = TestApiRegistry.from( [ identityApiRef, - { - getProfileInfo: jest.fn(async () => ({})), - getBackstageIdentity: jest.fn(async () => ({ - type: 'user' as const, - userEntityRef: 'foo:bar/foobar', - ownershipEntityRefs: ['user:default/test-ownership'], - })), - }, - ], - [ - catalogApiRef, - { - getEntityByRef: jest.fn(), - }, + mockApis.identity({ + userEntityRef: 'foo:bar/foobar', + ownershipEntityRefs: ['user:default/test-ownership'], + }), ], + [catalogApiRef, catalogApiMock.mock()], ); describe('', () => { diff --git a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx index c3b0aae4a7..0af33fac00 100644 --- a/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsMenu.test.tsx @@ -18,6 +18,7 @@ import { MockErrorApi, TestApiProvider, renderInTestApp, + mockApis, } from '@backstage/test-utils'; import { errorApiRef, identityApiRef } from '@backstage/core-plugin-api'; import { fireEvent, waitFor, screen } from '@testing-library/react'; @@ -35,9 +36,9 @@ describe('', () => { }); it('handles errors that occur when signing out', async () => { - const failingIdentityApi = { - signOut: jest.fn().mockRejectedValue(new Error('Logout error')), - }; + const failingIdentityApi = mockApis.identity.mock({ + signOut: () => Promise.reject(new Error('Logout error')), + }); const mockErrorApi = new MockErrorApi({ collect: true }); await renderInTestApp( ({})), - getBackstageIdentity: jest.fn(async () => ({ - type: 'user' as const, - userEntityRef: 'foo:bar/foobar', - ownershipEntityRefs: ['user:default/test-ownership'], - })), - }, - ], + [identityApiRef, mockApis.identity()], [ catalogApiRef, - { - getEntityByRef: jest.fn(async () => { - return { + catalogApiMock({ + entities: [ + { apiVersion: 'backstage.io/v1beta1', kind: 'User', metadata: { - name: 'Guest', + name: 'test', annotations: {}, }, spec: { @@ -50,9 +45,9 @@ const apiRegistry = TestApiRegistry.from( picture: 'https://example.com/avatar.png', }, }, - }; - }), - }, + }, + ], + }), ], ); diff --git a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx index acdb6c3f87..12431a44a2 100644 --- a/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx +++ b/plugins/user-settings/src/components/SettingsPage/SettingsPage.test.tsx @@ -22,20 +22,15 @@ import { useOutlet } from 'react-router-dom'; import { SettingsLayout } from '../SettingsLayout'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { - CatalogApi, - catalogApiRef, - entityRouteRef, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef, entityRouteRef } from '@backstage/plugin-catalog-react'; +import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useOutlet: jest.fn().mockReturnValue(undefined), })); -const catalogApiMock: jest.Mocked = { - getEntityByRef: jest.fn(), -} as any; +const catalogApi = catalogApiMock(); describe('', () => { beforeEach(() => { @@ -44,7 +39,7 @@ describe('', () => { it('should render the default settings page with 3 tabs', async () => { const { container } = await renderInTestApp( - + , { @@ -64,7 +59,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderInTestApp( - + , { @@ -85,7 +80,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(advancedTabRoute); const { container } = await renderInTestApp( - + , { @@ -115,7 +110,7 @@ describe('', () => { ); (useOutlet as jest.Mock).mockReturnValue(customLayout); const { container } = await renderInTestApp( - + , { From e39f72f813783c459a60ee754785631b0fc02776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 9 Oct 2024 12:09:36 +0200 Subject: [PATCH 09/22] implement permission too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/thin-chairs-ring.md | 1 + .../components/catalog/EntityPage.test.tsx | 5 +- packages/test-utils/report.api.md | 56 +++++++++-- .../apis/PermissionApi/MockPermissionApi.ts | 1 + .../src/testUtils/apis/mockApis.test.tsx | 98 +++++++++++++++++++ .../test-utils/src/testUtils/apis/mockApis.ts | 42 ++++++++ .../DefaultApiExplorerPage.test.tsx | 4 +- .../components/AboutCard/AboutCard.test.tsx | 38 +++---- .../CatalogPage/DefaultCatalogPage.test.tsx | 3 +- .../EntityContextMenu.test.tsx | 6 +- .../UnregisterEntity.test.tsx | 6 +- .../EntityLayout/EntityLayout.test.tsx | 24 ++--- .../MembersList/MembersListCard.test.tsx | 20 ++-- .../src/hooks/usePermission.test.tsx | 28 +++--- .../TemplateCard/TemplateCard.test.tsx | 31 +++--- .../TemplateListPage.test.tsx | 15 +-- .../OngoingTask/OngoingTask.test.tsx | 11 +-- 17 files changed, 279 insertions(+), 110 deletions(-) diff --git a/.changeset/thin-chairs-ring.md b/.changeset/thin-chairs-ring.md index df388a7365..dc3c7a0a25 100644 --- a/.changeset/thin-chairs-ring.md +++ b/.changeset/thin-chairs-ring.md @@ -7,3 +7,4 @@ Added a `mockApis` export, which will replace the `MockX` API implementation cla - Deprecated `MockAnalyticsApi`, please use `mockApis.analytics` instead. - Deprecated `MockConfigApi`, please use `mockApis.config` instead. +- Deprecated `MockPermissionApi`, please use `mockApis.permission` instead. diff --git a/packages/app/src/components/catalog/EntityPage.test.tsx b/packages/app/src/components/catalog/EntityPage.test.tsx index 5d2b23b007..0943a82efb 100644 --- a/packages/app/src/components/catalog/EntityPage.test.tsx +++ b/packages/app/src/components/catalog/EntityPage.test.tsx @@ -22,7 +22,7 @@ import { } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockPermissionApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -46,7 +46,6 @@ describe('EntityPage Test', () => { }, }; - const mockPermissionApi = new MockPermissionApi(); const rootRouteRef = catalogPlugin.routes.catalogIndex; describe('cicdContent', () => { @@ -55,7 +54,7 @@ describe('EntityPage Test', () => { diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index f570aacde4..4f29b2cdc1 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -144,6 +144,36 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } + // (undocumented) + export function permission(options?: { + authorize?: + | AuthorizeResult.ALLOW + | AuthorizeResult.DENY + | (( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY); + }): jest.Mocked; + // (undocumented) + export namespace permission { + const // (undocumented) + factory: ( + options?: + | { + authorize?: + | AuthorizeResult.DENY + | AuthorizeResult.ALLOW + | (( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.DENY | AuthorizeResult.ALLOW) + | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } } // @public @deprecated @@ -215,7 +245,9 @@ export interface MockFetchApiOptions { }; } -// @public +// Warning: (ae-unresolved-link) The @link reference could not be resolved: No member was found with name "permissions" +// +// @public @deprecated export class MockPermissionApi implements PermissionApi { constructor( requestHandler?: ( @@ -351,19 +383,23 @@ export function wrapInTestApp( // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:29:5 - (ae-undocumented) Missing documentation for "error$". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:33:5 - (ae-undocumented) Missing documentation for "getErrors". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:34:5 - (ae-undocumented) Missing documentation for "waitForError". -// src/testUtils/apis/PermissionApi/MockPermissionApi.d.ts:13:5 - (ae-undocumented) Missing documentation for "authorize". +// src/testUtils/apis/PermissionApi/MockPermissionApi.d.ts:14:5 - (ae-undocumented) Missing documentation for "authorize". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:19:5 - (ae-undocumented) Missing documentation for "create". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:20:5 - (ae-undocumented) Missing documentation for "forBucket". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:21:5 - (ae-undocumented) Missing documentation for "snapshot". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "set". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "remove". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "observe$". -// src/testUtils/apis/mockApis.d.ts:44:5 - (ae-undocumented) Missing documentation for "analytics". -// src/testUtils/apis/mockApis.d.ts:45:5 - (ae-undocumented) Missing documentation for "analytics". -// src/testUtils/apis/mockApis.d.ts:46:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:47:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:98:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:106:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:107:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:115:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:46:5 - (ae-undocumented) Missing documentation for "analytics". +// src/testUtils/apis/mockApis.d.ts:47:5 - (ae-undocumented) Missing documentation for "analytics". +// src/testUtils/apis/mockApis.d.ts:48:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:49:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:100:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:108:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:109:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:117:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:119:5 - (ae-undocumented) Missing documentation for "permission". +// src/testUtils/apis/mockApis.d.ts:122:5 - (ae-undocumented) Missing documentation for "permission". +// src/testUtils/apis/mockApis.d.ts:123:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:126:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts index 163af5dd53..3e5e4c9875 100644 --- a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts +++ b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts @@ -26,6 +26,7 @@ import { * {@link @backstage/plugin-permission-react#PermissionApi}. Supply a * requestHandler function to override the mock result returned for a given * request. + * @deprecated Use {@link mockApis.permissions} instead * @public */ export class MockPermissionApi implements PermissionApi { diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx index f9286897b4..f64aae9901 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + AuthorizeResult, + createPermission, +} from '@backstage/plugin-permission-common'; import { mockApis } from './mockApis'; describe('mockApis', () => { @@ -157,4 +161,98 @@ describe('mockApis', () => { expect(notEmpty.signOut).toHaveBeenCalledTimes(1); }); }); + + describe('permission', () => { + it('can create an instance and make assertions on it', async () => { + // default allow + const permission1 = mockApis.permission(); + await expect( + permission1.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); + expect(permission1.authorize).toHaveBeenCalledTimes(1); + + // static value + const permission2 = mockApis.permission({ + authorize: AuthorizeResult.DENY, + }); + await expect( + permission2.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.DENY }); + expect(permission2.authorize).toHaveBeenCalledTimes(1); + + // callback form + const permission3 = mockApis.permission({ + authorize: req => + req.permission.name === 'permission.1' + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + }); + await expect( + permission3.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); + await expect( + permission3.authorize({ + permission: createPermission({ + name: 'permission.2', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.DENY }); + expect(permission3.authorize).toHaveBeenCalledTimes(2); + }); + + it('can create a mock and make assertions on it', async () => { + const empty = mockApis.permission.mock(); + expect( + empty.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).toBeUndefined(); + expect(empty.authorize).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.permission.mock({ + authorize: async req => ({ + result: + req.permission.name === 'permission.1' + ? AuthorizeResult.ALLOW + : AuthorizeResult.DENY, + }), + }); + await expect( + notEmpty.authorize({ + permission: createPermission({ + name: 'permission.1', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); + await expect( + notEmpty.authorize({ + permission: createPermission({ + name: 'permission.2', + attributes: {}, + }), + }), + ).resolves.toEqual({ result: AuthorizeResult.DENY }); + expect(notEmpty.authorize).toHaveBeenCalledTimes(2); + }); + }); }); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index e47ee4ec3e..7ed886d577 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -26,8 +26,17 @@ import { createApiFactory, identityApiRef, } from '@backstage/core-plugin-api'; +import { + AuthorizeResult, + EvaluatePermissionRequest, +} from '@backstage/plugin-permission-common'; +import { + PermissionApi, + permissionApiRef, +} from '@backstage/plugin-permission-react'; import { JsonObject } from '@backstage/types'; import { ApiMock } from './ApiMock'; +import { MockPermissionApi } from './PermissionApi'; /** @internal */ function simpleFactory( @@ -226,4 +235,37 @@ export namespace mockApis { export const factory = simpleFactory(identityApiRef, identity); export const mock = simpleMock(identityApiRef, identityMockSkeleton); } + + const permissionMockSkeleton = (): jest.Mocked => ({ + authorize: jest.fn(), + }); + export function permission(options?: { + authorize?: + | AuthorizeResult.ALLOW + | AuthorizeResult.DENY + | (( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY); + }) { + const authorizeInput = options?.authorize; + let authorize: ( + request: EvaluatePermissionRequest, + ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY; + if (authorizeInput === undefined) { + authorize = () => AuthorizeResult.ALLOW; + } else if (typeof authorizeInput === 'function') { + authorize = authorizeInput; + } else { + authorize = () => authorizeInput; + } + return simpleInstance( + permissionApiRef, + new MockPermissionApi(authorize), + permissionMockSkeleton, + ); + } + export namespace permission { + export const factory = simpleFactory(permissionApiRef, permission); + export const mock = simpleMock(permissionApiRef, permissionMockSkeleton); + } } diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index b8d8b07436..7847ac8336 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -28,7 +28,7 @@ import { } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockPermissionApi, + mockApis, MockStorageApi, TestApiProvider, renderInTestApp, @@ -96,7 +96,7 @@ describe('DefaultApiExplorerPage', () => { new DefaultStarredEntitiesApi({ storageApi }), ], [apiDocsConfigRef, apiDocsConfig], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > {children} diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index b175593b68..87f35bbc3e 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -24,7 +24,11 @@ import { ScmIntegrationsApi, scmIntegrationsApiRef, } from '@backstage/integration-react'; -import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiProvider, + mockApis, + renderInTestApp, +} from '@backstage/test-utils'; import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { AboutCard } from './AboutCard'; @@ -37,10 +41,6 @@ import { permissionApiRef } from '@backstage/plugin-permission-react'; import { AuthorizeResult } from '@backstage/plugin-permission-common'; import { SWRConfig } from 'swr'; -const mockAuthorize = jest.fn(); - -const mockPermissionApi = { authorize: mockAuthorize }; - describe('', () => { const catalogApi = catalogApiMock.mock(); @@ -411,10 +411,6 @@ describe('', () => { }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.ALLOW, - })); - await renderInTestApp( ', () => { ScmIntegrationsApi.fromConfig(new ConfigReader({})), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [permissionApiRef, mockApis.permission()], ]} > @@ -466,10 +462,6 @@ describe('', () => { }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.DENY, - })); - await renderInTestApp( ', () => { ScmIntegrationsApi.fromConfig(new ConfigReader({})), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > @@ -766,9 +761,6 @@ describe('', () => { namespace: 'default', }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.ALLOW, - })); await renderInTestApp( ', () => { ), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [permissionApiRef, mockApis.permission()], ]} > @@ -819,9 +811,6 @@ describe('', () => { namespace: 'default', }, }; - mockAuthorize.mockImplementation(async () => ({ - result: AuthorizeResult.DENY, - })); await renderInTestApp( new Map() }}> ', () => { ), ], [catalogApiRef, catalogApi], - [permissionApiRef, mockPermissionApi], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index a5ced77749..38310bbbf5 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -26,7 +26,6 @@ import { } from '@backstage/plugin-catalog-react'; import { mockBreakpoint } from '@backstage/core-components/testUtils'; import { - MockPermissionApi, MockStorageApi, TestApiProvider, mockApis, @@ -177,7 +176,7 @@ describe('DefaultCatalogPage', () => { [identityApiRef, identityApi], [storageApiRef, storageApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > {children} diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx index f7220cb1ea..4725f35c86 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.test.tsx @@ -17,7 +17,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-react'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockPermissionApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -26,11 +26,9 @@ import { fireEvent, screen } from '@testing-library/react'; import * as React from 'react'; import { EntityContextMenu } from './EntityContextMenu'; -const mockPermissionApi = new MockPermissionApi(); - function render(children: React.ReactNode) { return renderInTestApp( - + + { }, } as Entity; - const mockApis = TestApiRegistry.from( + const apis = TestApiRegistry.from( [catalogApiRef, catalogApiMock()], [alertApiRef, {} as AlertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ); it('renders simplest case', async () => { await renderInTestApp( - + @@ -93,7 +93,7 @@ describe('EntityLayout', () => { } as Entity; await renderInTestApp( - + @@ -117,7 +117,7 @@ describe('EntityLayout', () => { it('renders default error message when entity is not found', async () => { await renderInTestApp( - + @@ -142,7 +142,7 @@ describe('EntityLayout', () => { it('renders custom message when entity is not found', async () => { await renderInTestApp( - + Oppps.. Your entity was not found
} @@ -171,7 +171,7 @@ describe('EntityLayout', () => { it('navigates when user clicks different tab', async () => { await renderInTestApp( - + @@ -211,7 +211,7 @@ describe('EntityLayout', () => { const shouldNotRenderTab = (e: Entity) => e.metadata.name === 'some-entity'; await renderInTestApp( - + @@ -254,7 +254,7 @@ describe('EntityLayout', () => { relations: [{ type: 'ownedBy', targetRef: mockTargetRef }], }; await renderInTestApp( - + @@ -327,7 +327,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => { [catalogApiRef, catalogApi], [alertApiRef, alertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -378,7 +378,7 @@ describe('EntityLayout - CleanUpAfterRemoval', () => { [catalogApiRef, catalogApi], [alertApiRef, alertApi], [starredEntitiesApiRef, new MockStarredEntitiesApi()], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 6b8261e015..04de321c50 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -22,7 +22,11 @@ import { StarredEntitiesApi, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { + mockApis, + renderInTestApp, + TestApiProvider, +} from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; import { @@ -184,7 +188,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -212,7 +216,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -238,7 +242,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -273,7 +277,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -308,7 +312,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -366,7 +370,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -406,7 +410,7 @@ describe('MemberTab Test', () => { apis={[ [catalogApiRef, mockedCatalogApiSupportingGroups], [starredEntitiesApiRef, mockedStarredEntitiesApi], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > diff --git a/plugins/permission-react/src/hooks/usePermission.test.tsx b/plugins/permission-react/src/hooks/usePermission.test.tsx index 1987382f47..e4a0c82eab 100644 --- a/plugins/permission-react/src/hooks/usePermission.test.tsx +++ b/plugins/permission-react/src/hooks/usePermission.test.tsx @@ -21,7 +21,7 @@ import { AuthorizeResult, createPermission, } from '@backstage/plugin-permission-common'; -import { TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import { PermissionApi, permissionApiRef } from '../apis'; import { SWRConfig } from 'swr'; @@ -52,36 +52,36 @@ function renderComponent(mockApi: PermissionApi) { } describe('usePermission', () => { - const mockPermissionApi = { authorize: jest.fn() }; - it('Returns loading when permissionApi has not yet responded.', () => { - mockPermissionApi.authorize.mockReturnValueOnce(new Promise(() => {})); + const permissionApi = mockApis.permission.mock({ + authorize: async () => new Promise(() => {}), + }); - const { getByText } = renderComponent(mockPermissionApi); + const { getByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(getByText('loading')).toBeTruthy(); }); it('Returns allowed when permissionApi allows authorization.', async () => { - mockPermissionApi.authorize.mockResolvedValueOnce({ - result: AuthorizeResult.ALLOW, + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.ALLOW, }); - const { findByText } = renderComponent(mockPermissionApi); + const { findByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); expect(await findByText('content')).toBeTruthy(); }); it('Returns not allowed when permissionApi denies authorization.', async () => { - mockPermissionApi.authorize.mockResolvedValueOnce({ - result: AuthorizeResult.DENY, + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.DENY, }); - const { findByText } = renderComponent(mockPermissionApi); + const { findByText } = renderComponent(permissionApi); - expect(mockPermissionApi.authorize).toHaveBeenCalledWith({ permission }); + expect(permissionApi.authorize).toHaveBeenCalledWith({ permission }); await expect(findByText('content')).rejects.toThrow(); }); }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx index febdc7c99e..5b2324b222 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DefaultStarredEntitiesApi } from '@backstage/plugin-catalog'; import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; import { - MockPermissionApi, + mockApis, MockStorageApi, renderInTestApp, TestApiProvider, @@ -54,7 +55,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -84,7 +85,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -116,7 +117,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -146,7 +147,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -182,7 +183,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -222,7 +223,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -267,7 +268,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -316,7 +317,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -359,7 +360,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -399,7 +400,7 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi()], + [permissionApiRef, mockApis.permission()], ]} > @@ -428,9 +429,6 @@ describe('TemplateCard', () => { }, }; const mockOnSelected = jest.fn(); - const mockAuthorize = jest - .fn() - .mockImplementation(async () => ({ result: AuthorizeResult.DENY })); // SWR used by the usePermission hook needs cache to be reset for each test const { queryByText } = await renderInTestApp( new Map() }}> @@ -442,7 +440,10 @@ describe('TemplateCard', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, new MockPermissionApi(mockAuthorize)], + [ + permissionApiRef, + mockApis.permission({ authorize: AuthorizeResult.DENY }), + ], ]} > diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx index 6079ec2e1d..a02c90f687 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx @@ -25,6 +25,7 @@ import { MockStorageApi, renderInTestApp, TestApiProvider, + mockApis, } from '@backstage/test-utils'; import React from 'react'; import { rootRouteRef } from '../../../routes'; @@ -55,7 +56,7 @@ describe('TemplateListPage', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -77,7 +78,7 @@ describe('TemplateListPage', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -100,7 +101,7 @@ describe('TemplateListPage', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -122,7 +123,7 @@ describe('TemplateListPage', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -145,7 +146,7 @@ describe('TemplateListPage', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -167,7 +168,7 @@ describe('TemplateListPage', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > @@ -188,7 +189,7 @@ describe('TemplateListPage', () => { storageApi: MockStorageApi.create(), }), ], - [permissionApiRef, {}], + [permissionApiRef, mockApis.permission()], ]} > { @@ -146,10 +146,9 @@ describe('OngoingTask', () => { }); it('should have cancel and start over buttons be disabled without the proper permissions', async () => { - const mockAuthorize = jest - .fn() - .mockImplementation(async () => ({ result: AuthorizeResult.DENY })); - const permissionApi: PermissionApi = { authorize: mockAuthorize }; + const permissionApi = mockApis.permission({ + authorize: AuthorizeResult.DENY, + }); const rendered = await render(permissionApi); const { getByTestId } = rendered; From 7d06a4391679890f7ce7117f00a6735ccb77d723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 9 Oct 2024 13:53:08 +0200 Subject: [PATCH 10/22] implement storage too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/thin-chairs-ring.md | 1 + .../DismissableBanner.test.tsx | 22 +- packages/test-utils/report.api.md | 43 ++- .../apis/StorageApi/MockStorageApi.test.ts | 1 + .../apis/StorageApi/MockStorageApi.ts | 18 +- .../src/testUtils/apis/mockApis.test.tsx | 284 ++++++++++++++++++ .../test-utils/src/testUtils/apis/mockApis.ts | 22 ++ .../DefaultApiExplorerPage.test.tsx | 10 +- .../CookieAuthRefreshProvider.test.tsx | 11 +- .../FavoriteEntity/FavoriteEntity.test.tsx | 8 +- .../UserListPicker/UserListPicker.test.tsx | 11 +- .../src/hooks/useEntityListProvider.test.tsx | 12 +- .../DefaultStarredEntitiesApi.test.ts | 8 +- .../apis/StarredEntitiesApi/migration.test.ts | 6 +- .../CatalogPage/DefaultCatalogPage.test.tsx | 5 +- plugins/home/src/api/VisitsStorageApi.test.ts | 12 +- .../TemplateCard/CardHeader.test.tsx | 11 +- .../TemplateCard/TemplateCard.test.tsx | 23 +- .../TemplateListPage.test.tsx | 15 +- .../components/DefaultTechDocsHome.test.tsx | 24 +- .../Grids/EntityListDocsGrid.test.tsx | 22 +- 21 files changed, 437 insertions(+), 132 deletions(-) diff --git a/.changeset/thin-chairs-ring.md b/.changeset/thin-chairs-ring.md index dc3c7a0a25..5561b6f551 100644 --- a/.changeset/thin-chairs-ring.md +++ b/.changeset/thin-chairs-ring.md @@ -8,3 +8,4 @@ Added a `mockApis` export, which will replace the `MockX` API implementation cla - Deprecated `MockAnalyticsApi`, please use `mockApis.analytics` instead. - Deprecated `MockConfigApi`, please use `mockApis.config` instead. - Deprecated `MockPermissionApi`, please use `mockApis.permission` instead. +- Deprecated `MockStorageApi`, please use `mockApis.storage` instead. diff --git a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx index 8d93186ccc..6f6da1231f 100644 --- a/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx +++ b/packages/core-components/src/components/DismissableBanner/DismissableBanner.test.tsx @@ -16,24 +16,18 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; -import { TestApiRegistry, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiRegistry, + renderInTestApp, + mockApis, +} from '@backstage/test-utils'; import { DismissableBanner } from './DismissableBanner'; -import { ApiProvider, WebStorage } from '@backstage/core-app-api'; -import { storageApiRef, StorageApi } from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { storageApiRef } from '@backstage/core-plugin-api'; import { screen } from '@testing-library/react'; describe('', () => { - let apis: TestApiRegistry; - const mockErrorApi = { post: jest.fn(), error$: jest.fn() }; - const createWebStorage = (): StorageApi => { - return WebStorage.create({ - errorApi: mockErrorApi, - }); - }; - - beforeEach(() => { - apis = TestApiRegistry.from([storageApiRef, createWebStorage()]); - }); + const apis = TestApiRegistry.from([storageApiRef, mockApis.storage()]); it('renders the message and the popover', async () => { await renderInTestApp( diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 4f29b2cdc1..0845218a33 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -174,6 +174,25 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } + // (undocumented) + export function storage(options?: { + data?: JsonObject; + }): jest.Mocked; + // (undocumented) + export namespace storage { + const // (undocumented) + factory: ( + options?: + | { + data?: JsonObject | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } } // @public @deprecated @@ -260,7 +279,9 @@ export class MockPermissionApi implements PermissionApi { ): Promise; } -// @public +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "storage" has more than one declaration; you need to add a TSDoc member reference selector +// +// @public @deprecated export class MockStorageApi implements StorageApi { // (undocumented) static create(data?: MockStorageBucket): MockStorageApi; @@ -278,7 +299,9 @@ export class MockStorageApi implements StorageApi { snapshot(key: string): StorageValueSnapshot; } -// @public +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "storage" has more than one declaration; you need to add a TSDoc member reference selector +// +// @public @deprecated export type MockStorageBucket = { [key: string]: any; }; @@ -384,12 +407,12 @@ export function wrapInTestApp( // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:33:5 - (ae-undocumented) Missing documentation for "getErrors". // src/testUtils/apis/ErrorApi/MockErrorApi.d.ts:34:5 - (ae-undocumented) Missing documentation for "waitForError". // src/testUtils/apis/PermissionApi/MockPermissionApi.d.ts:14:5 - (ae-undocumented) Missing documentation for "authorize". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:19:5 - (ae-undocumented) Missing documentation for "create". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:20:5 - (ae-undocumented) Missing documentation for "forBucket". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:21:5 - (ae-undocumented) Missing documentation for "snapshot". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "set". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "remove". -// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "observe$". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:21:5 - (ae-undocumented) Missing documentation for "create". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:22:5 - (ae-undocumented) Missing documentation for "forBucket". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:23:5 - (ae-undocumented) Missing documentation for "snapshot". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "set". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:25:5 - (ae-undocumented) Missing documentation for "remove". +// src/testUtils/apis/StorageApi/MockStorageApi.d.ts:26:5 - (ae-undocumented) Missing documentation for "observe$". // src/testUtils/apis/mockApis.d.ts:46:5 - (ae-undocumented) Missing documentation for "analytics". // src/testUtils/apis/mockApis.d.ts:47:5 - (ae-undocumented) Missing documentation for "analytics". // src/testUtils/apis/mockApis.d.ts:48:15 - (ae-undocumented) Missing documentation for "factory". @@ -402,4 +425,8 @@ export function wrapInTestApp( // src/testUtils/apis/mockApis.d.ts:122:5 - (ae-undocumented) Missing documentation for "permission". // src/testUtils/apis/mockApis.d.ts:123:15 - (ae-undocumented) Missing documentation for "factory". // src/testUtils/apis/mockApis.d.ts:126:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:128:5 - (ae-undocumented) Missing documentation for "storage". +// src/testUtils/apis/mockApis.d.ts:131:5 - (ae-undocumented) Missing documentation for "storage". +// src/testUtils/apis/mockApis.d.ts:132:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:135:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index 63aa4235e1..cdae96d5e4 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { StorageApi } from '@backstage/core-plugin-api'; import { MockStorageApi } from './MockStorageApi'; diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 30d523aed2..5ef3846bfa 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -20,12 +20,14 @@ import ObservableImpl from 'zen-observable'; /** * Type for map holding data in {@link MockStorageApi} + * @deprecated Use {@link mockApis.storage} instead * @public */ export type MockStorageBucket = { [key: string]: any }; /** * Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests + * @deprecated Use {@link mockApis.storage} instead * @public */ export class MockStorageApi implements StorageApi { @@ -44,7 +46,21 @@ export class MockStorageApi implements StorageApi { } static create(data?: MockStorageBucket) { - return new MockStorageApi('', new Map(), data); + // Translate a nested data object structure into a flat object with keys + // like `/a/b` with their corresponding leaf values + const keyValues: { [key: string]: any } = {}; + function put(value: { [key: string]: any }, namespace: string) { + for (const [key, val] of Object.entries(value)) { + if (typeof val === 'object' && val !== null) { + put(val, `${namespace}/${key}`); + } else { + const namespacedKey = `${namespace}/${key.replace(/^\//, '')}`; + keyValues[namespacedKey] = val; + } + } + } + put(data ?? {}, ''); + return new MockStorageApi('', new Map(), keyValues); } forBucket(name: string): StorageApi { diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx index f64aae9901..87b9472377 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -255,4 +255,288 @@ describe('mockApis', () => { expect(notEmpty.authorize).toHaveBeenCalledTimes(2); }); }); + + describe('storage', () => { + describe('instance deep tests', () => { + it('should return undefined for values which are unset', async () => { + const storage = mockApis.storage(); + + expect(storage.snapshot('myfakekey').value).toBeUndefined(); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'absent', + value: undefined, + newValue: undefined, + }); + }); + + it('should allow the setting and snapshotting of the simple data structures', async () => { + const storage = mockApis.storage(); + + await storage.set('myfakekey', 'helloimastring'); + await storage.set('mysecondfakekey', 1234); + await storage.set('mythirdfakekey', true); + expect(storage.snapshot('myfakekey').value).toBe('helloimastring'); + expect(storage.snapshot('mysecondfakekey').value).toBe(1234); + expect(storage.snapshot('mythirdfakekey').value).toBe(true); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: 'helloimastring', + }); + expect(storage.snapshot('mysecondfakekey')).toEqual({ + key: 'mysecondfakekey', + presence: 'present', + value: 1234, + }); + expect(storage.snapshot('mythirdfakekey')).toEqual({ + key: 'mythirdfakekey', + presence: 'present', + value: true, + }); + }); + + it('should allow setting of complex datastructures', async () => { + const storage = mockApis.storage(); + + const mockData = { + something: 'here', + is: [{ super: { complex: [{ but: 'something', why: true }] } }], + }; + + await storage.set('myfakekey', mockData); + + expect(storage.snapshot('myfakekey').value).toEqual(mockData); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: mockData, + }); + }); + + it('should subscribe to key changes when setting a new value', async () => { + const storage = mockApis.storage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.set('correctKey', mockData); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + presence: 'present', + value: mockData, + }); + }); + + it('should subscribe to key changes when deleting a value', async () => { + const storage = mockApis.storage(); + + const wrongKeyNextHandler = jest.fn(); + const selectedKeyNextHandler = jest.fn(); + const mockData = { hello: 'im a great new value' }; + + storage.set('correctKey', mockData); + + await new Promise(resolve => { + storage.observe$('correctKey').subscribe({ + next: (...args) => { + selectedKeyNextHandler(...args); + resolve(); + }, + }); + + storage.observe$('wrongKey').subscribe({ next: wrongKeyNextHandler }); + + storage.remove('correctKey'); + }); + + expect(wrongKeyNextHandler).not.toHaveBeenCalled(); + expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); + expect(selectedKeyNextHandler).toHaveBeenCalledWith({ + key: 'correctKey', + presence: 'absent', + value: undefined, + newValue: undefined, + }); + }); + + it('should be able to create different buckets for different uses', async () => { + const rootStorage = mockApis.storage(); + + const firstStorage = rootStorage.forBucket('userSettings'); + const secondStorage = rootStorage.forBucket('profileSettings'); + const keyName = 'blobby'; + + await firstStorage.set(keyName, 'boop'); + await secondStorage.set(keyName, 'deerp'); + + expect(firstStorage.snapshot(keyName)).not.toBe( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName).value).toBe('boop'); + expect(secondStorage.snapshot(keyName).value).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toEqual( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'boop', + }); + expect(secondStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'deerp', + }); + }); + + it('should not clash with other namespaces when creating buckets', async () => { + const rootStorage = mockApis.storage(); + + // when getting key test2 it will translate to /profile/something/deep/test2 + const firstStorage = rootStorage + .forBucket('profile') + .forBucket('something') + .forBucket('deep'); + // when getting key deep/test2 it will translate to /profile/something/deep/test2 + const secondStorage = rootStorage.forBucket('profile/something'); + + await firstStorage.set('test2', { error: true }); + + expect(secondStorage.snapshot('deep/test2').value).toBe(undefined); + expect(secondStorage.snapshot('deep/test2')).toMatchObject({ + presence: 'absent', + }); + }); + + it('should not reuse storage instances between different rootStorages', async () => { + const rootStorage1 = mockApis.storage(); + const rootStorage2 = mockApis.storage(); + + const firstStorage = rootStorage1.forBucket('something'); + const secondStorage = rootStorage2.forBucket('something'); + + await firstStorage.set('test2', true); + + expect(firstStorage.snapshot('test2').value).toBe(true); + expect(secondStorage.snapshot('test2').value).toBe(undefined); + expect(firstStorage.snapshot('test2')).toEqual({ + key: 'test2', + presence: 'present', + value: true, + }); + expect(secondStorage.snapshot('test2')).toEqual({ + key: 'test2', + presence: 'absent', + value: undefined, + }); + }); + + it('should freeze the snapshot value', async () => { + const storage = mockApis.storage(); + + const data = { foo: 'bar', baz: [{ foo: 'bar' }] }; + storage.set('foo', data); + + const snapshot = storage.snapshot('foo'); + expect(snapshot.value).not.toBe(data); + + if (snapshot.presence !== 'present') { + throw new Error('Invalid presence'); + } + + expect(() => { + snapshot.value.foo = 'buzz'; + }).toThrow(/Cannot assign to read only property/); + expect(() => { + snapshot.value.baz[0].foo = 'buzz'; + }).toThrow(/Cannot assign to read only property/); + expect(() => { + snapshot.value.baz.push({ foo: 'buzz' }); + }).toThrow(/Cannot add property 1, object is not extensible/); + }); + + it('should freeze observed values', async () => { + const storage = mockApis.storage(); + + const snapshotPromise = new Promise(resolve => { + storage.observe$('test').subscribe({ + next: resolve, + }); + }); + + storage.set('test', { + foo: { + bar: 'baz', + }, + }); + + const snapshot = await snapshotPromise; + expect(snapshot.presence).toBe('present'); + expect(() => { + snapshot.value!.foo.bar = 'qux'; + }).toThrow(/Cannot assign to read only property 'bar' of object/); + }); + + it('should JSON serialize stored values', async () => { + const storage = mockApis.storage(); + + storage.set('test', { + foo: { + toJSON() { + return { + bar: 'baz', + }; + }, + }, + }); + + expect(storage.snapshot('test')).toMatchObject({ + presence: 'present', + value: { + foo: { + bar: 'baz', + }, + }, + }); + }); + }); + + it('can create an instance and make assertions on it', () => { + const empty = mockApis.storage(); + expect(empty.snapshot('a')).toEqual({ key: 'a', presence: 'absent' }); + expect(empty.snapshot).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.storage({ data: { a: 1, b: { c: 2 } } }); + expect(notEmpty.snapshot('a')).toEqual({ + key: 'a', + presence: 'present', + value: 1, + }); + expect(notEmpty.forBucket('b').snapshot('c')).toEqual({ + key: 'c', + presence: 'present', + value: 2, + }); + expect(notEmpty.snapshot).toHaveBeenCalledTimes(1); // "inner" (forBucket returned) instances aren't mocked + expect(notEmpty.forBucket).toHaveBeenCalledTimes(1); + }); + + it('can create a mock and make assertions on it', () => {}); + }); }); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index 7ed886d577..7a334c69b6 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -21,10 +21,12 @@ import { ApiRef, ConfigApi, IdentityApi, + StorageApi, analyticsApiRef, configApiRef, createApiFactory, identityApiRef, + storageApiRef, } from '@backstage/core-plugin-api'; import { AuthorizeResult, @@ -37,6 +39,7 @@ import { import { JsonObject } from '@backstage/types'; import { ApiMock } from './ApiMock'; import { MockPermissionApi } from './PermissionApi'; +import { MockStorageApi } from './StorageApi'; /** @internal */ function simpleFactory( @@ -268,4 +271,23 @@ export namespace mockApis { export const factory = simpleFactory(permissionApiRef, permission); export const mock = simpleMock(permissionApiRef, permissionMockSkeleton); } + + const storageMockSkeleton = (): jest.Mocked => ({ + forBucket: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + observe$: jest.fn(), + snapshot: jest.fn(), + }); + export function storage(options?: { data?: JsonObject }) { + return simpleInstance( + storageApiRef, + MockStorageApi.create(options?.data), + storageMockSkeleton, + ); + } + export namespace storage { + export const factory = simpleFactory(storageApiRef, storage); + export const mock = simpleMock(storageApiRef, storageMockSkeleton); + } } diff --git a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx index 7847ac8336..34706e8136 100644 --- a/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx +++ b/plugins/api-docs/src/components/ApiExplorerPage/DefaultApiExplorerPage.test.tsx @@ -14,7 +14,6 @@ * limitations under the License. */ -import { ConfigReader } from '@backstage/core-app-api'; import { TableColumn, TableProps } from '@backstage/core-components'; import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { @@ -29,7 +28,6 @@ import { import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { mockApis, - MockStorageApi, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -72,17 +70,15 @@ describe('DefaultApiExplorerPage', () => { }), }); - const configApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); const apiDocsConfig = { getApiDefinitionWidget: () => undefined, }; - const storageApi = MockStorageApi.create(); + const storageApi = mockApis.storage(); const renderWrapped = (children: React.ReactNode) => renderInTestApp( diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx index bba876e6be..6ff9d8a1c5 100644 --- a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx @@ -19,7 +19,7 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { CookieAuthRefreshProvider } from './CookieAuthRefreshProvider'; import { - MockStorageApi, + mockApis, TestApiProvider, renderInTestApp, } from '@backstage/test-utils'; @@ -30,7 +30,6 @@ import { } from '@backstage/core-plugin-api'; describe('CookieAuthRefreshProvider', () => { - const storageApiMock = MockStorageApi.create(); const discoveryApiMock = { getBaseUrl: jest .fn() @@ -51,7 +50,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -76,7 +75,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -107,7 +106,7 @@ describe('CookieAuthRefreshProvider', () => { @@ -153,7 +152,7 @@ describe('CookieAuthRefreshProvider', () => { diff --git a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.test.tsx b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.test.tsx index d36700452a..ac06b3a34d 100644 --- a/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.test.tsx +++ b/plugins/catalog-react/src/components/FavoriteEntity/FavoriteEntity.test.tsx @@ -21,7 +21,7 @@ import { MockStarredEntitiesApi, starredEntitiesApiRef } from '../../apis'; import { FavoriteEntity } from './FavoriteEntity'; import { ComponentEntity } from '@backstage/catalog-model'; import { - MockStorageApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -41,14 +41,12 @@ const entity: ComponentEntity = { }, }; -const mockStorage = MockStorageApi.create(); - describe('', () => { it('should add to favorites', async () => { await renderInTestApp( @@ -79,7 +77,7 @@ describe('', () => { await renderInTestApp( diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 3f7c2034a0..7ff30dbf5f 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -34,7 +34,6 @@ import { } from '@backstage/catalog-client'; import { catalogApiRef } from '../../api'; import { - MockStorageApi, TestApiRegistry, mockApis, renderInTestApp, @@ -79,7 +78,7 @@ const apis = TestApiRegistry.from( [configApiRef, mockConfigApi], [catalogApiRef, mockCatalogApi], [identityApiRef, mockIdentityApi], - [storageApiRef, MockStorageApi.create()], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, mockStarredEntitiesApi], ); @@ -134,19 +133,13 @@ describe('', () => { beforeEach(() => { mockCatalogApi.getEntityByRef?.mockResolvedValue(mockUser); - mockIdentityApi.getBackstageIdentity?.mockResolvedValue({ - ownershipEntityRefs, - type: 'user', - userEntityRef: 'user:default/testuser', - }); - mockCatalogApi.queryEntities?.mockImplementation( mockQueryEntitiesImplementation, ); }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('renders filter groups', async () => { diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 067ca33fa3..4ef6b175ae 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -23,11 +23,7 @@ import { identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { - MockStorageApi, - TestApiProvider, - mockApis, -} from '@backstage/test-utils'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import { act, renderHook, waitFor } from '@testing-library/react'; import qs from 'qs'; import React, { PropsWithChildren } from 'react'; @@ -69,8 +65,6 @@ const entities: Entity[] = [ }, ]; -const mockConfigApi = mockApis.config(); - const ownershipEntityRefs = ['user:default/guest']; const mockIdentityApi = mockApis.identity({ @@ -104,10 +98,10 @@ const createWrapper = { describe('constructor', () => { it('should call migration', () => { const api = new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }); expect(performMigrationToTheNewBucket).toHaveBeenCalledTimes(1); expect(api).toBeDefined(); @@ -54,7 +54,7 @@ describe('DefaultStarredEntitiesApi', () => { it('should notify and toggle starred entities', async () => { const entityRef = 'component:default/mock'; - const storageApi = MockStorageApi.create(); + const storageApi = mockApis.storage(); const storageBucket = storageApi.forBucket('starredEntities'); const api = new DefaultStarredEntitiesApi({ storageApi }); @@ -85,7 +85,7 @@ describe('DefaultStarredEntitiesApi', () => { it('should read starred entities from storage', async () => { const entityRef = 'component:default/mock'; - const storageApi = MockStorageApi.create(); + const storageApi = mockApis.storage(); const storageBucket = storageApi.forBucket('starredEntities'); storageBucket.set('entityRefs', [entityRef]); const api = new DefaultStarredEntitiesApi({ storageApi }); diff --git a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts index c9083969ee..9f99c6e2dd 100644 --- a/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts +++ b/plugins/catalog/src/apis/StarredEntitiesApi/migration.test.ts @@ -15,18 +15,18 @@ */ import { StorageApi } from '@backstage/core-plugin-api'; -import { MockStorageApi } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import { performMigrationToTheNewBucket } from './migration'; describe('performMigrationToTheNewBucket', () => { let mockStorage: StorageApi; beforeEach(() => { - mockStorage = MockStorageApi.create(); + mockStorage = mockApis.storage(); }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should migrate', async () => { diff --git a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx index 38310bbbf5..07cf98359e 100644 --- a/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/DefaultCatalogPage.test.tsx @@ -26,7 +26,6 @@ import { } from '@backstage/plugin-catalog-react'; import { mockBreakpoint } from '@backstage/core-components/testUtils'; import { - MockStorageApi, TestApiProvider, mockApis, renderInTestApp, @@ -49,7 +48,6 @@ describe('DefaultCatalogPage', () => { }); afterEach(() => { window.history.replaceState = origReplaceState; - jest.clearAllMocks(); }); @@ -166,7 +164,6 @@ describe('DefaultCatalogPage', () => { ownershipEntityRefs: ['user:default/guest', 'group:default/tools'], displayName: 'Display Name', }); - const storageApi = MockStorageApi.create(); const renderWrapped = (children: React.ReactNode) => renderInTestApp( @@ -174,7 +171,7 @@ describe('DefaultCatalogPage', () => { apis={[ [catalogApiRef, catalogApi], [identityApiRef, identityApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], [permissionApiRef, mockApis.permission()], ]} diff --git a/plugins/home/src/api/VisitsStorageApi.test.ts b/plugins/home/src/api/VisitsStorageApi.test.ts index 206cf42689..55cf623149 100644 --- a/plugins/home/src/api/VisitsStorageApi.test.ts +++ b/plugins/home/src/api/VisitsStorageApi.test.ts @@ -15,7 +15,7 @@ */ import { VisitsStorageApi } from './VisitsStorageApi'; -import { MockStorageApi, mockApis } from '@backstage/test-utils'; +import { mockApis } from '@backstage/test-utils'; import { Visit, VisitsApi } from './VisitsApi'; describe('VisitsStorageApi.create', () => { @@ -42,7 +42,7 @@ describe('VisitsStorageApi.create', () => { it('instantiates', () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); expect(api).toBeTruthy(); @@ -51,7 +51,7 @@ describe('VisitsStorageApi.create', () => { describe('.save()', () => { it('saves a visit', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); const visit = { @@ -68,7 +68,7 @@ describe('VisitsStorageApi.create', () => { it('can control the number of stored entities', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, limit: 2, }); @@ -102,7 +102,7 @@ describe('VisitsStorageApi.create', () => { it('correctly bumps the hits from a previous visit', async () => { const api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); const visit = { @@ -144,7 +144,7 @@ describe('VisitsStorageApi.create', () => { beforeEach(() => { api = VisitsStorageApi.create({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), identityApi: mockIdentityApi, }); diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx index 58033b718c..9ae2a507dd 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/CardHeader.test.tsx @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { fireEvent } from '@testing-library/react'; import { CardHeader } from './CardHeader'; import { ThemeProvider } from '@material-ui/core/styles'; import { lightTheme } from '@backstage/theme'; import { - MockStorageApi, + mockApis, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -44,7 +45,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -75,7 +76,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -135,7 +136,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} @@ -164,7 +165,7 @@ describe('CardHeader', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], ]} diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx index 5b2324b222..673f1e4a9e 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.test.tsx @@ -21,7 +21,6 @@ import { } from '@backstage/plugin-catalog-react'; import { mockApis, - MockStorageApi, renderInTestApp, TestApiProvider, } from '@backstage/test-utils'; @@ -52,7 +51,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -82,7 +81,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -114,7 +113,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -144,7 +143,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -180,7 +179,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -220,7 +219,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -265,7 +264,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -314,7 +313,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -357,7 +356,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -397,7 +396,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -437,7 +436,7 @@ describe('TemplateCard', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [ diff --git a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx index a02c90f687..01274cf6a6 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateListPage/TemplateListPage.test.tsx @@ -22,7 +22,6 @@ import { import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { permissionApiRef } from '@backstage/plugin-permission-react'; import { - MockStorageApi, renderInTestApp, TestApiProvider, mockApis, @@ -53,7 +52,7 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -75,7 +74,7 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -98,7 +97,7 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -120,7 +119,7 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -143,7 +142,7 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -165,7 +164,7 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], @@ -186,7 +185,7 @@ describe('TemplateListPage', () => { [ starredEntitiesApiRef, new DefaultStarredEntitiesApi({ - storageApi: MockStorageApi.create(), + storageApi: mockApis.storage(), }), ], [permissionApiRef, mockApis.permission()], diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx index 513a56265c..4c3be25325 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.test.tsx @@ -14,22 +14,18 @@ * limitations under the License. */ -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { - ConfigApi, - configApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { + MockStarredEntitiesApi, catalogApiRef, starredEntitiesApiRef, - MockStarredEntitiesApi, } from '@backstage/plugin-catalog-react'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; import { - MockStorageApi, - renderInTestApp, TestApiRegistry, + mockApis, + renderInTestApp, } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; @@ -50,18 +46,14 @@ const mockCatalogApi = catalogApiMock({ }); describe('TechDocs Home', () => { - const configApi: ConfigApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); - const storageApi = MockStorageApi.create(); - const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); diff --git a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx index 947c81ae85..25471d0769 100644 --- a/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx +++ b/plugins/techdocs/src/home/components/Grids/EntityListDocsGrid.test.tsx @@ -14,12 +14,8 @@ * limitations under the License. */ -import { ApiProvider, ConfigReader } from '@backstage/core-app-api'; -import { - ConfigApi, - configApiRef, - storageApiRef, -} from '@backstage/core-plugin-api'; +import { ApiProvider } from '@backstage/core-app-api'; +import { configApiRef, storageApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef, starredEntitiesApiRef, @@ -30,9 +26,9 @@ import { catalogApiMock, } from '@backstage/plugin-catalog-react/testUtils'; import { - MockStorageApi, renderInTestApp, TestApiRegistry, + mockApis, } from '@backstage/test-utils'; import { screen } from '@testing-library/react'; import React from 'react'; @@ -68,21 +64,17 @@ const mockCatalogApi = catalogApiMock({ entities }); describe('Entity List Docs Grid', () => { beforeEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); - const configApi: ConfigApi = new ConfigReader({ - organization: { - name: 'My Company', - }, + const configApi = mockApis.config({ + data: { organization: { name: 'My Company' } }, }); - const storageApi = MockStorageApi.create(); - const apiRegistry = TestApiRegistry.from( [catalogApiRef, mockCatalogApi], [configApiRef, configApi], - [storageApiRef, storageApi], + [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], ); From d081861694552a84f04d060bbbbbd483bef38a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 9 Oct 2024 14:34:58 +0200 Subject: [PATCH 11/22] implement translation too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/thin-chairs-ring.md | 3 + packages/test-utils/report-alpha.api.md | 10 +- packages/test-utils/report.api.md | 54 +++-- .../apis/AnalyticsApi/MockAnalyticsApi.ts | 2 +- .../apis/PermissionApi/MockPermissionApi.ts | 2 +- .../apis/StorageApi/MockStorageApi.ts | 4 +- .../apis/TranslationApi/MockTranslationApi.ts | 5 +- .../src/testUtils/apis/mockApis.test.tsx | 223 +++++++++++++++++- .../test-utils/src/testUtils/apis/mockApis.ts | 57 +++-- .../src/hooks/useEntityListProvider.test.tsx | 3 +- 10 files changed, 301 insertions(+), 62 deletions(-) diff --git a/.changeset/thin-chairs-ring.md b/.changeset/thin-chairs-ring.md index 5561b6f551..104c78a96d 100644 --- a/.changeset/thin-chairs-ring.md +++ b/.changeset/thin-chairs-ring.md @@ -5,7 +5,10 @@ Added a `mockApis` export, which will replace the `MockX` API implementation classes and their related types. This is analogous with the backend's `mockServices`. +**DEPRECATED** several old helpers: + - Deprecated `MockAnalyticsApi`, please use `mockApis.analytics` instead. - Deprecated `MockConfigApi`, please use `mockApis.config` instead. - Deprecated `MockPermissionApi`, please use `mockApis.permission` instead. - Deprecated `MockStorageApi`, please use `mockApis.storage` instead. +- Deprecated `MockTranslationApi`, please use `mockApis.translation` instead. diff --git a/packages/test-utils/report-alpha.api.md b/packages/test-utils/report-alpha.api.md index ba098135fb..19a3786fc9 100644 --- a/packages/test-utils/report-alpha.api.md +++ b/packages/test-utils/report-alpha.api.md @@ -8,7 +8,7 @@ import { TranslationApi } from '@backstage/core-plugin-api/alpha'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; import { TranslationSnapshot } from '@backstage/core-plugin-api/alpha'; -// @alpha (undocumented) +// @alpha @deprecated (undocumented) export class MockTranslationApi implements TranslationApi { // (undocumented) static create(): MockTranslationApi; @@ -30,10 +30,10 @@ export class MockTranslationApi implements TranslationApi { // Warnings were encountered during analysis: // -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:4:1 - (ae-undocumented) Missing documentation for "MockTranslationApi". -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:6:5 - (ae-undocumented) Missing documentation for "create". -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:8:5 - (ae-undocumented) Missing documentation for "getTranslation". -// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "translation$". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:7:1 - (ae-undocumented) Missing documentation for "MockTranslationApi". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:9:5 - (ae-undocumented) Missing documentation for "create". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:11:5 - (ae-undocumented) Missing documentation for "getTranslation". +// src/testUtils/apis/TranslationApi/MockTranslationApi.d.ts:14:5 - (ae-undocumented) Missing documentation for "translation$". // (No @packageDocumentation comment for this package) ``` diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 0845218a33..167494d4f6 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -41,6 +41,7 @@ import { RenderResult } from '@testing-library/react'; import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { TranslationApi } from '@backstage/core-plugin-api/alpha'; // @public export type ApiMock = { @@ -81,8 +82,6 @@ export type LogCollector = AsyncLogCollector | SyncLogCollector; // @public export type LogFuncs = 'log' | 'warn' | 'error'; -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "analytics" has more than one declaration; you need to add a TSDoc member reference selector -// // @public @deprecated export class MockAnalyticsApi implements AnalyticsApi { // (undocumented) @@ -193,6 +192,17 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } + // (undocumented) + export function translation(): jest.Mocked; + // (undocumented) + export namespace translation { + const // (undocumented) + factory: () => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } } // @public @deprecated @@ -264,8 +274,6 @@ export interface MockFetchApiOptions { }; } -// Warning: (ae-unresolved-link) The @link reference could not be resolved: No member was found with name "permissions" -// // @public @deprecated export class MockPermissionApi implements PermissionApi { constructor( @@ -279,8 +287,6 @@ export class MockPermissionApi implements PermissionApi { ): Promise; } -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "storage" has more than one declaration; you need to add a TSDoc member reference selector -// // @public @deprecated export class MockStorageApi implements StorageApi { // (undocumented) @@ -299,8 +305,6 @@ export class MockStorageApi implements StorageApi { snapshot(key: string): StorageValueSnapshot; } -// Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "storage" has more than one declaration; you need to add a TSDoc member reference selector -// // @public @deprecated export type MockStorageBucket = { [key: string]: any; @@ -413,20 +417,24 @@ export function wrapInTestApp( // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "set". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:25:5 - (ae-undocumented) Missing documentation for "remove". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:26:5 - (ae-undocumented) Missing documentation for "observe$". -// src/testUtils/apis/mockApis.d.ts:46:5 - (ae-undocumented) Missing documentation for "analytics". // src/testUtils/apis/mockApis.d.ts:47:5 - (ae-undocumented) Missing documentation for "analytics". -// src/testUtils/apis/mockApis.d.ts:48:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:49:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:100:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:108:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:109:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:117:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:119:5 - (ae-undocumented) Missing documentation for "permission". -// src/testUtils/apis/mockApis.d.ts:122:5 - (ae-undocumented) Missing documentation for "permission". -// src/testUtils/apis/mockApis.d.ts:123:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:126:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:128:5 - (ae-undocumented) Missing documentation for "storage". -// src/testUtils/apis/mockApis.d.ts:131:5 - (ae-undocumented) Missing documentation for "storage". -// src/testUtils/apis/mockApis.d.ts:132:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:135:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:48:5 - (ae-undocumented) Missing documentation for "analytics". +// src/testUtils/apis/mockApis.d.ts:49:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:50:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:101:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:109:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:110:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:118:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:120:5 - (ae-undocumented) Missing documentation for "permission". +// src/testUtils/apis/mockApis.d.ts:123:5 - (ae-undocumented) Missing documentation for "permission". +// src/testUtils/apis/mockApis.d.ts:124:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:127:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:129:5 - (ae-undocumented) Missing documentation for "storage". +// src/testUtils/apis/mockApis.d.ts:132:5 - (ae-undocumented) Missing documentation for "storage". +// src/testUtils/apis/mockApis.d.ts:133:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:136:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:138:5 - (ae-undocumented) Missing documentation for "translation". +// src/testUtils/apis/mockApis.d.ts:139:5 - (ae-undocumented) Missing documentation for "translation". +// src/testUtils/apis/mockApis.d.ts:140:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:141:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts index 8c3ef4b326..788d2ca0d8 100644 --- a/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts +++ b/packages/test-utils/src/testUtils/apis/AnalyticsApi/MockAnalyticsApi.ts @@ -21,7 +21,7 @@ import { AnalyticsApi, AnalyticsEvent } from '@backstage/core-plugin-api'; * Use getEvents in tests to verify captured events. * * @public - * @deprecated Use {@link mockApis.analytics} instead + * @deprecated Use {@link @backstage/test-utils#mockApis.(analytics:namespace)} instead */ export class MockAnalyticsApi implements AnalyticsApi { private events: AnalyticsEvent[] = []; diff --git a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts index 3e5e4c9875..6bd3c5d4c5 100644 --- a/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts +++ b/packages/test-utils/src/testUtils/apis/PermissionApi/MockPermissionApi.ts @@ -26,7 +26,7 @@ import { * {@link @backstage/plugin-permission-react#PermissionApi}. Supply a * requestHandler function to override the mock result returned for a given * request. - * @deprecated Use {@link mockApis.permissions} instead + * @deprecated Use {@link @backstage/test-utils#mockApis.(permission:namespace)} instead * @public */ export class MockPermissionApi implements PermissionApi { diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 5ef3846bfa..2fb70834ef 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -20,14 +20,14 @@ import ObservableImpl from 'zen-observable'; /** * Type for map holding data in {@link MockStorageApi} - * @deprecated Use {@link mockApis.storage} instead + * @deprecated Use {@link @backstage/test-utils#mockApis.(storage:namespace)} instead * @public */ export type MockStorageBucket = { [key: string]: any }; /** * Mock implementation of the {@link core-plugin-api#StorageApi} to be used in tests - * @deprecated Use {@link mockApis.storage} instead + * @deprecated Use {@link @backstage/test-utils#mockApis.(storage:namespace)} instead * @public */ export class MockStorageApi implements StorageApi { diff --git a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts index 608b634cc4..46c5bfd5d3 100644 --- a/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts +++ b/packages/test-utils/src/testUtils/apis/TranslationApi/MockTranslationApi.ts @@ -30,7 +30,10 @@ import { toInternalTranslationRef } from '../../../../../core-plugin-api/src/tra const DEFAULT_LANGUAGE = 'en'; -/** @alpha */ +/** + * @alpha + * @deprecated Use `mockApis` from `@backstage/test-utils` instead + */ export class MockTranslationApi implements TranslationApi { static create() { const i18n = createI18n({ diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx index 87b9472377..662567066c 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -19,6 +19,9 @@ import { createPermission, } from '@backstage/plugin-permission-common'; import { mockApis } from './mockApis'; +import { JsonValue } from '@backstage/types'; +import { StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; describe('mockApis', () => { describe('analytics', () => { @@ -537,6 +540,224 @@ describe('mockApis', () => { expect(notEmpty.forBucket).toHaveBeenCalledTimes(1); }); - it('can create a mock and make assertions on it', () => {}); + it('can create a mock and make assertions on it', () => { + const empty = mockApis.storage.mock(); + expect(empty.snapshot('a')).toBeUndefined(); + expect(empty.snapshot).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.storage.mock({ + snapshot(k: string): StorageValueSnapshot { + return { key: k, presence: 'present', value: 'v' as T }; + }, + }); + expect(notEmpty.snapshot('a')).toEqual({ + key: 'a', + presence: 'present', + value: 'v', + }); + expect(notEmpty.snapshot).toHaveBeenCalledTimes(1); + }); + }); + + describe('translation', () => { + describe('instance deep tests', () => { + function snapshotWithMessages< + const TMessages extends { [key in string]: string }, + >(messages: TMessages) { + const translationApi = mockApis.translation(); + const ref = createTranslationRef({ + id: 'test', + messages, + }); + const snapshot = translationApi.getTranslation(ref); + if (!snapshot.ready) { + throw new Error('Translation snapshot is not ready'); + } + return snapshot; + } + + it('should format plain messages', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo', + bar: 'Bar', + baz: 'Baz', + }); + + expect(snapshot.t('foo')).toBe('Foo'); + expect(snapshot.t('bar')).toBe('Bar'); + expect(snapshot.t('baz')).toBe('Baz'); + }); + + it('should support interpolation', () => { + const snapshot = snapshotWithMessages({ + shallow: 'Foo {{ bar }}', + multiple: 'Foo {{ bar }} {{ baz }}', + deep: 'Foo {{ bar.baz }}', + }); + + // @ts-expect-error + expect(snapshot.t('shallow')).toBe('Foo {{ bar }}'); + expect(snapshot.t('shallow', { bar: 'Bar' })).toBe('Foo Bar'); + + // @ts-expect-error + expect(snapshot.t('multiple')).toBe('Foo {{ bar }} {{ baz }}'); + // @ts-expect-error + expect(snapshot.t('multiple', { bar: 'Bar' })).toBe( + 'Foo Bar {{ baz }}', + ); + expect(snapshot.t('multiple', { bar: 'Bar', baz: 'Baz' })).toBe( + 'Foo Bar Baz', + ); + + // @ts-expect-error + expect(snapshot.t('deep')).toBe('Foo {{ bar.baz }}'); + expect(snapshot.t('deep', { bar: { baz: 'Baz' } })).toBe('Foo Baz'); + }); + + // Escaping isn't as useful in React, since we don't need to escape HTML in strings + it('should not escape by default', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo {{ foo }}', + }); + + expect(snapshot.t('foo', { foo: '
' })).toBe('Foo
'); + expect( + snapshot.t('foo', { + foo: '
', + interpolation: { escapeValue: true }, + }), + ).toBe('Foo <div>'); + }); + + it('should support nesting', () => { + const snapshot = snapshotWithMessages({ + foo: 'Foo $t(bar) $t(baz)', + bar: 'Nested', + baz: 'Baz {{ qux }}', + }); + + expect(snapshot.t('foo', { qux: 'Deep' })).toBe('Foo Nested Baz Deep'); + }); + + it('should support formatting', () => { + const snapshot = snapshotWithMessages({ + plain: '= {{ x }}', + number: '= {{ x, number }}', + numberFixed: '= {{ x, number(minimumFractionDigits: 2) }}', + relativeTime: '= {{ x, relativeTime }}', + relativeSeconds: '= {{ x, relativeTime(second) }}', + relativeSecondsShort: + '= {{ x, relativeTime(range: second; style: short) }}', + list: '= {{ x, list }}', + }); + + expect(snapshot.t('plain', { x: '5' })).toBe('= 5'); + expect(snapshot.t('number', { x: 5 })).toBe('= 5'); + expect( + snapshot.t('number', { + x: 5, + formatParams: { x: { minimumFractionDigits: 1 } }, + }), + ).toBe('= 5.0'); + expect(snapshot.t('numberFixed', { x: 5 })).toBe('= 5.00'); + expect( + snapshot.t('numberFixed', { + x: 5, + formatParams: { x: { minimumFractionDigits: 3 } }, + }), + ).toBe('= 5.000'); + expect(snapshot.t('relativeTime', { x: 3 })).toBe('= in 3 days'); + expect(snapshot.t('relativeTime', { x: -3 })).toBe('= 3 days ago'); + expect( + snapshot.t('relativeTime', { + x: 15, + formatParams: { x: { range: 'weeks' } }, + }), + ).toBe('= in 15 weeks'); + expect( + snapshot.t('relativeTime', { + x: 15, + formatParams: { x: { range: 'weeks', style: 'short' } }, + }), + ).toBe('= in 15 wk.'); + expect(snapshot.t('relativeSeconds', { x: 1 })).toBe('= in 1 second'); + expect(snapshot.t('relativeSeconds', { x: 2 })).toBe('= in 2 seconds'); + expect(snapshot.t('relativeSeconds', { x: -3 })).toBe( + '= 3 seconds ago', + ); + expect(snapshot.t('relativeSeconds', { x: 0 })).toBe('= in 0 seconds'); + expect(snapshot.t('relativeSecondsShort', { x: 1 })).toBe( + '= in 1 sec.', + ); + expect(snapshot.t('relativeSecondsShort', { x: 2 })).toBe( + '= in 2 sec.', + ); + expect(snapshot.t('relativeSecondsShort', { x: -3 })).toBe( + '= 3 sec. ago', + ); + expect(snapshot.t('relativeSecondsShort', { x: 0 })).toBe( + '= in 0 sec.', + ); + expect(snapshot.t('list', { x: ['a'] })).toBe('= a'); + expect(snapshot.t('list', { x: ['a', 'b'] })).toBe('= a and b'); + expect(snapshot.t('list', { x: ['a', 'b', 'c'] })).toBe( + '= a, b, and c', + ); + }); + + it('should support plurals', () => { + const snapshot = snapshotWithMessages({ + derp_one: 'derp', + derp_other: 'derps', + derpWithCount_one: '{{ count }} derp', + derpWithCount_other: '{{ count }} derps', + }); + + expect(snapshot.t('derp', { count: 1 })).toBe('derp'); + expect(snapshot.t('derp', { count: 2 })).toBe('derps'); + expect(snapshot.t('derp', { count: 0 })).toBe('derps'); + expect(snapshot.t('derpWithCount', { count: 1 })).toBe('1 derp'); + expect(snapshot.t('derpWithCount', { count: 2 })).toBe('2 derps'); + expect(snapshot.t('derpWithCount', { count: 0 })).toBe('0 derps'); + }); + }); + + it('can create an instance and make assertions on it', () => { + const translation = mockApis.translation(); + const ref = createTranslationRef({ + id: 'test', + messages: { a: 'b' }, + }); + const result = translation.getTranslation(ref); + if (!result.ready) { + throw new Error('not ready'); + } + expect(result.t('a')).toEqual('b'); + expect(translation.getTranslation).toHaveBeenCalledTimes(1); + }); + + it('can create a mock and make assertions on it', () => { + const ref = createTranslationRef({ + id: 'test', + messages: { a: 'b' }, + }); + + const empty = mockApis.translation.mock(); + expect(empty.getTranslation(ref)).toBeUndefined(); + + const notEmpty = mockApis.translation.mock({ + getTranslation: () => + ({ + ready: true, + t: () => 'b', + } as any), + }); + const result = notEmpty.getTranslation(ref); + if (!result.ready) { + throw new Error('not ready'); + } + expect(result.t('a')).toEqual('b'); + expect(notEmpty.getTranslation).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index 7a334c69b6..cfc174c5ad 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -40,6 +40,11 @@ import { JsonObject } from '@backstage/types'; import { ApiMock } from './ApiMock'; import { MockPermissionApi } from './PermissionApi'; import { MockStorageApi } from './StorageApi'; +import { + TranslationApi, + translationApiRef, +} from '@backstage/core-plugin-api/alpha'; +import { MockTranslationApi } from './TranslationApi'; /** @internal */ function simpleFactory( @@ -208,7 +213,7 @@ export namespace mockApis { email?: string; displayName?: string; picture?: string; - }) { + }): IdentityApi { const { userEntityRef = 'user:default/test', ownershipEntityRefs = ['user:default/test'], @@ -217,22 +222,18 @@ export namespace mockApis { displayName, picture, } = options ?? {}; - return simpleInstance( - identityApiRef, - { - async getBackstageIdentity() { - return { type: 'user', ownershipEntityRefs, userEntityRef }; - }, - async getCredentials() { - return { token }; - }, - async getProfileInfo() { - return { email, displayName, picture }; - }, - async signOut() {}, + return { + async getBackstageIdentity() { + return { type: 'user', ownershipEntityRefs, userEntityRef }; }, - identityMockSkeleton, - ); + async getCredentials() { + return { token }; + }, + async getProfileInfo() { + return { email, displayName, picture }; + }, + async signOut() {}, + }; } export namespace identity { export const factory = simpleFactory(identityApiRef, identity); @@ -261,11 +262,7 @@ export namespace mockApis { } else { authorize = () => authorizeInput; } - return simpleInstance( - permissionApiRef, - new MockPermissionApi(authorize), - permissionMockSkeleton, - ); + return new MockPermissionApi(authorize); } export namespace permission { export const factory = simpleFactory(permissionApiRef, permission); @@ -280,14 +277,22 @@ export namespace mockApis { snapshot: jest.fn(), }); export function storage(options?: { data?: JsonObject }) { - return simpleInstance( - storageApiRef, - MockStorageApi.create(options?.data), - storageMockSkeleton, - ); + return MockStorageApi.create(options?.data); } export namespace storage { export const factory = simpleFactory(storageApiRef, storage); export const mock = simpleMock(storageApiRef, storageMockSkeleton); } + + const translationMockSkeleton = (): jest.Mocked => ({ + getTranslation: jest.fn(), + translation$: jest.fn(), + }); + export function translation() { + return MockTranslationApi.create(); + } + export namespace translation { + export const factory = simpleFactory(translationApiRef, translation); + export const mock = simpleMock(translationApiRef, translationMockSkeleton); + } } diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx index 4ef6b175ae..2ed944d339 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.test.tsx @@ -39,7 +39,6 @@ import { import { EntityListProvider, useEntityList } from './useEntityListProvider'; import { useMountEffect } from '@react-hookz/web'; import { translationApiRef } from '@backstage/core-plugin-api/alpha'; -import { MockTranslationApi } from '@backstage/test-utils/alpha'; import { EntityListPagination } from '../types'; const entities: Entity[] = [ @@ -104,7 +103,7 @@ const createWrapper = [storageApiRef, mockApis.storage()], [starredEntitiesApiRef, new MockStarredEntitiesApi()], [alertApiRef, { post: jest.fn() }], - [translationApiRef, MockTranslationApi.create()], + [translationApiRef, mockApis.translation()], [errorApiRef, { error$: jest.fn(), post: jest.fn() }], ]} > From 3afafea87b7143944b2279768291297c325ee075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 9 Oct 2024 16:33:39 +0200 Subject: [PATCH 12/22] implement discovery too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- ...ginProtocolResolverFetchMiddleware.test.ts | 22 ++++---- .../core-app-api/src/app/AppManager.test.tsx | 6 +- .../ProxiedSignInPage.test.tsx | 5 +- packages/test-utils/report.api.md | 55 +++++++++++++------ .../src/testUtils/apis/mockApis.test.tsx | 28 ++++++++++ .../test-utils/src/testUtils/apis/mockApis.ts | 30 ++++++++-- .../CookieAuthRefreshProvider.test.tsx | 8 +-- .../useCookieAuthRefresh.test.tsx | 10 +--- .../PodExecTerminal/PodExecTerminal.test.tsx | 9 ++- .../Pods/PodDrawer/PodDrawer.test.tsx | 12 ++-- plugins/signals/src/api/SignalsClient.test.ts | 11 ++-- .../TechDocsReaderPage.test.tsx | 8 +-- 12 files changed, 129 insertions(+), 75 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts index 3b4b7136bb..7a9ebdc4f9 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -14,15 +14,14 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { mockApis } from '@backstage/test-utils'; import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware'; describe('PluginProtocolResolverFetchMiddleware', () => { it.each([['https://passthrough.com/a']])( 'passes through regular URLs, %p', async url => { - const resolve = jest.fn(); - const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const discoveryApi = mockApis.discovery(); const middleware = new PluginProtocolResolverFetchMiddleware( discoveryApi, ); @@ -31,7 +30,7 @@ describe('PluginProtocolResolverFetchMiddleware', () => { await outer(url); expect(inner.mock.calls[0][0]).toBe(url); - expect(resolve).not.toHaveBeenCalled(); + expect(discoveryApi.getBaseUrl).not.toHaveBeenCalled(); }, ); @@ -76,30 +75,29 @@ describe('PluginProtocolResolverFetchMiddleware', () => { ])( 'resolves backstage URLs, %p', async (original, host, resolved, result) => { - const resolve = jest.fn(); - const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const discoveryApi = mockApis.discovery.mock({ + getBaseUrl: async () => resolved, + }); const middleware = new PluginProtocolResolverFetchMiddleware( discoveryApi, ); const inner = jest.fn(); const outer = middleware.apply(inner); - resolve.mockResolvedValueOnce(resolved); await outer(original); expect(inner.mock.calls[0][0]).toBe(result); - expect(resolve).toHaveBeenLastCalledWith(host); + expect(discoveryApi.getBaseUrl).toHaveBeenLastCalledWith(host); }, ); it('properly supports transferring request bodies too', async () => { - const resolve = jest.fn(); - const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const discoveryApi = mockApis.discovery.mock({ + getBaseUrl: async () => 'https://elsewhere.com', + }); const middleware = new PluginProtocolResolverFetchMiddleware(discoveryApi); const inner = jest.fn(); const outer = middleware.apply(inner); - resolve.mockResolvedValue('https://elsewhere.com'); - await outer('plugin://a', { method: 'POST', body: '123', diff --git a/packages/core-app-api/src/app/AppManager.test.tsx b/packages/core-app-api/src/app/AppManager.test.tsx index 1b7d2d28cb..d995edffdf 100644 --- a/packages/core-app-api/src/app/AppManager.test.tsx +++ b/packages/core-app-api/src/app/AppManager.test.tsx @@ -878,9 +878,9 @@ describe('Integration Test', () => { }), }), }; - const discoveryApiMock = { - getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7007/app'), - }; + const discoveryApiMock = mockApis.discovery.mock({ + getBaseUrl: async () => 'http://localhost:7007/app', + }); const app = new AppManager({ icons, diff --git a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx index e4e65b613f..ad7e650d24 100644 --- a/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx +++ b/packages/core-components/src/layout/ProxiedSignInPage/ProxiedSignInPage.test.tsx @@ -20,6 +20,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { TestApiProvider, + mockApis, registerMswTestHooks, wrapInTestApp, } from '@backstage/test-utils'; @@ -37,9 +38,7 @@ describe('ProxiedSignInPage', () => { apis={[ [ discoveryApiRef, - { - getBaseUrl: async () => 'http://example.com/api/auth', - }, + mockApis.discovery({ baseUrl: 'http://example.com' }), ], ]} > diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index 167494d4f6..cc09727d87 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -115,6 +115,25 @@ export namespace mockApis { const mock: (partialImpl?: Partial | undefined) => ApiMock; } // (undocumented) + export function discovery(options?: { baseUrl?: string }): jest.Mocked<{ + getBaseUrl(pluginId: string): Promise; + }>; + // (undocumented) + export namespace discovery { + const // (undocumented) + factory: ( + options?: + | { + baseUrl?: string | undefined; + } + | undefined, + ) => ApiFactory; + const // (undocumented) + mock: ( + partialImpl?: Partial | undefined, + ) => ApiMock; + } + // (undocumented) export function identity(options?: { userEntityRef?: string; ownershipEntityRefs?: string[]; @@ -421,20 +440,24 @@ export function wrapInTestApp( // src/testUtils/apis/mockApis.d.ts:48:5 - (ae-undocumented) Missing documentation for "analytics". // src/testUtils/apis/mockApis.d.ts:49:15 - (ae-undocumented) Missing documentation for "factory". // src/testUtils/apis/mockApis.d.ts:50:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:101:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:109:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:110:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:118:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:120:5 - (ae-undocumented) Missing documentation for "permission". -// src/testUtils/apis/mockApis.d.ts:123:5 - (ae-undocumented) Missing documentation for "permission". -// src/testUtils/apis/mockApis.d.ts:124:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:127:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:129:5 - (ae-undocumented) Missing documentation for "storage". -// src/testUtils/apis/mockApis.d.ts:132:5 - (ae-undocumented) Missing documentation for "storage". -// src/testUtils/apis/mockApis.d.ts:133:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:136:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:138:5 - (ae-undocumented) Missing documentation for "translation". -// src/testUtils/apis/mockApis.d.ts:139:5 - (ae-undocumented) Missing documentation for "translation". -// src/testUtils/apis/mockApis.d.ts:140:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:141:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:101:5 - (ae-undocumented) Missing documentation for "discovery". +// src/testUtils/apis/mockApis.d.ts:106:5 - (ae-undocumented) Missing documentation for "discovery". +// src/testUtils/apis/mockApis.d.ts:107:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:110:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:112:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:120:5 - (ae-undocumented) Missing documentation for "identity". +// src/testUtils/apis/mockApis.d.ts:121:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:129:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:131:5 - (ae-undocumented) Missing documentation for "permission". +// src/testUtils/apis/mockApis.d.ts:134:5 - (ae-undocumented) Missing documentation for "permission". +// src/testUtils/apis/mockApis.d.ts:135:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:138:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:140:5 - (ae-undocumented) Missing documentation for "storage". +// src/testUtils/apis/mockApis.d.ts:143:5 - (ae-undocumented) Missing documentation for "storage". +// src/testUtils/apis/mockApis.d.ts:144:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:147:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:149:5 - (ae-undocumented) Missing documentation for "translation". +// src/testUtils/apis/mockApis.d.ts:150:5 - (ae-undocumented) Missing documentation for "translation". +// src/testUtils/apis/mockApis.d.ts:151:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:152:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx index 662567066c..af0bbaee72 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -79,6 +79,34 @@ describe('mockApis', () => { }); }); + describe('discovery', () => { + it('can create an instance and make assertions on it', async () => { + const empty = mockApis.discovery(); + await expect(empty.getBaseUrl('catalog')).resolves.toBe( + 'http://example.com/api/catalog', + ); + expect(empty.getBaseUrl).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.discovery({ baseUrl: 'https://other.net' }); + await expect(notEmpty.getBaseUrl('catalog')).resolves.toBe( + 'https://other.net/api/catalog', + ); + expect(notEmpty.getBaseUrl).toHaveBeenCalledTimes(1); + }); + + it('can create a mock and make assertions on it', async () => { + const empty = mockApis.discovery.mock(); + expect(empty.getBaseUrl('catalog')).toBeUndefined(); + expect(empty.getBaseUrl).toHaveBeenCalledTimes(1); + + const notEmpty = mockApis.discovery.mock({ + getBaseUrl: async () => 'replaced', + }); + await expect(notEmpty.getBaseUrl('catalog')).resolves.toBe('replaced'); + expect(notEmpty.getBaseUrl).toHaveBeenCalledTimes(1); + }); + }); + describe('identity', () => { it('can create an instance and make assertions on it', async () => { const empty = mockApis.identity(); diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index cfc174c5ad..1adf172893 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -20,14 +20,20 @@ import { ApiFactory, ApiRef, ConfigApi, + DiscoveryApi, IdentityApi, StorageApi, analyticsApiRef, configApiRef, createApiFactory, + discoveryApiRef, identityApiRef, storageApiRef, } from '@backstage/core-plugin-api'; +import { + TranslationApi, + translationApiRef, +} from '@backstage/core-plugin-api/alpha'; import { AuthorizeResult, EvaluatePermissionRequest, @@ -40,10 +46,6 @@ import { JsonObject } from '@backstage/types'; import { ApiMock } from './ApiMock'; import { MockPermissionApi } from './PermissionApi'; import { MockStorageApi } from './StorageApi'; -import { - TranslationApi, - translationApiRef, -} from '@backstage/core-plugin-api/alpha'; import { MockTranslationApi } from './TranslationApi'; /** @internal */ @@ -200,6 +202,26 @@ export namespace mockApis { })); } + const discoveryMockSkeleton = (): jest.Mocked => ({ + getBaseUrl: jest.fn(), + }); + export function discovery(options?: { baseUrl?: string }) { + const baseUrl = options?.baseUrl ?? 'http://example.com'; + return simpleInstance( + discoveryApiRef, + { + async getBaseUrl(pluginId: string) { + return `${baseUrl}/api/${pluginId}`; + }, + }, + discoveryMockSkeleton, + ); + } + export namespace discovery { + export const factory = simpleFactory(discoveryApiRef, discovery); + export const mock = simpleMock(discoveryApiRef, discoveryMockSkeleton); + } + const identityMockSkeleton = (): jest.Mocked => ({ getBackstageIdentity: jest.fn(), getCredentials: jest.fn(), diff --git a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx index 6ff9d8a1c5..8b07abb163 100644 --- a/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx +++ b/plugins/auth-react/src/components/CookieAuthRefreshProvider/CookieAuthRefreshProvider.test.tsx @@ -30,11 +30,7 @@ import { } from '@backstage/core-plugin-api'; describe('CookieAuthRefreshProvider', () => { - const discoveryApiMock = { - getBaseUrl: jest - .fn() - .mockResolvedValue('http://localhost:7000/api/techdocs'), - }; + const discoveryApiMock = mockApis.discovery(); function getExpiresAtInFuture() { const tenMinutesInMilliseconds = 10 * 60 * 1000; @@ -118,7 +114,7 @@ describe('CookieAuthRefreshProvider', () => { await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7000/api/techdocs/.backstage/auth/v1/cookie', + 'http://example.com/api/techdocs/.backstage/auth/v1/cookie', { credentials: 'include' }, ), ); diff --git a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx index 422f392e93..6b59dbcb9e 100644 --- a/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx +++ b/plugins/auth-react/src/hooks/useCookieAuthRefresh/useCookieAuthRefresh.test.tsx @@ -17,15 +17,11 @@ import React from 'react'; import { renderHook, waitFor } from '@testing-library/react'; import { fetchApiRef, discoveryApiRef } from '@backstage/core-plugin-api'; -import { TestApiProvider } from '@backstage/test-utils'; +import { TestApiProvider, mockApis } from '@backstage/test-utils'; import { useCookieAuthRefresh } from './useCookieAuthRefresh'; describe('useCookieAuthRefresh', () => { - const discoveryApiMock = { - getBaseUrl: jest - .fn() - .mockResolvedValue('http://localhost:7000/api/techdocs'), - }; + const discoveryApiMock = mockApis.discovery(); const now = 1710316886171; const tenMinutesInMilliseconds = 10 * 60 * 1000; @@ -269,7 +265,7 @@ describe('useCookieAuthRefresh', () => { await waitFor(() => expect(fetchApiMock.fetch).toHaveBeenCalledWith( - 'http://localhost:7000/api/techdocs/.backstage/auth/v1/cookie', + 'http://example.com/api/techdocs/.backstage/auth/v1/cookie', { credentials: 'include' }, ), ); diff --git a/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx index 4aa6112436..0e56c9599a 100644 --- a/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx +++ b/plugins/kubernetes-react/src/components/PodExecTerminal/PodExecTerminal.test.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ -import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +import { discoveryApiRef } from '@backstage/core-plugin-api'; import { + mockApis, renderInTestApp, TestApiProvider, textContentMatcher, @@ -37,9 +38,7 @@ describe('PodExecTerminal', () => { const podName = 'pod1'; const podNamespace = 'podNamespace'; - const mockDiscoveryApi: Partial = { - getBaseUrl: () => Promise.resolve('http://localhost'), - }; + const mockDiscoveryApi = mockApis.discovery(); it('Should render an XTerm web terminal', async () => { await renderInTestApp( @@ -62,7 +61,7 @@ describe('PodExecTerminal', () => { it('Should connect to WebSocket server & render response', async () => { const server = new WS( - 'ws://localhost/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh', + 'ws://example.com/api/kubernetes/proxy/api/v1/namespaces/podNamespace/pods/pod1/exec?container=container2&stdin=true&stdout=true&stderr=true&tty=true&command=%2Fbin%2Fsh', ); await renderInTestApp( diff --git a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx index 066fa0f874..4718f8f29f 100644 --- a/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx +++ b/plugins/kubernetes-react/src/components/Pods/PodDrawer/PodDrawer.test.tsx @@ -17,19 +17,21 @@ import React from 'react'; import { screen } from '@testing-library/react'; -import { TestApiProvider, renderInTestApp } from '@backstage/test-utils'; +import { + TestApiProvider, + mockApis, + renderInTestApp, +} from '@backstage/test-utils'; import '@testing-library/jest-dom'; import { PodDrawer } from './PodDrawer'; -import { DiscoveryApi, discoveryApiRef } from '@backstage/core-plugin-api'; +import { discoveryApiRef } from '@backstage/core-plugin-api'; jest.mock('../../../hooks/useIsPodExecTerminalSupported'); describe('PodDrawer', () => { it('Should show title and container names', async () => { - const mockDiscoveryApi: Partial = { - getBaseUrl: () => Promise.resolve('http://localhost'), - }; + const mockDiscoveryApi = mockApis.discovery(); await renderInTestApp( diff --git a/plugins/signals/src/api/SignalsClient.test.ts b/plugins/signals/src/api/SignalsClient.test.ts index 057b1fb23f..b6944cfdc1 100644 --- a/plugins/signals/src/api/SignalsClient.test.ts +++ b/plugins/signals/src/api/SignalsClient.test.ts @@ -14,24 +14,21 @@ * limitations under the License. */ -import { DiscoveryApi } from '@backstage/core-plugin-api'; import { mockApis } from '@backstage/test-utils'; import WS from 'jest-websocket-mock'; import { SignalClient } from './SignalClient'; describe('SignalsClient', () => { - const baseUrlFunction = jest.fn(); const identity = mockApis.identity({ token: '12345' }); - const discoveryApi = { - getBaseUrl: baseUrlFunction, - } as unknown as DiscoveryApi; + const discoveryApi = mockApis.discovery({ baseUrl: 'http://localhost:1234' }); let server: WS; beforeEach(async () => { jest.clearAllMocks(); - baseUrlFunction.mockResolvedValue('http://localhost:1234'); - server = new WS('ws://localhost:1234', { jsonProtocol: true }); + server = new WS('ws://localhost:1234/api/signals', { + jsonProtocol: true, + }); }); afterEach(() => { diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx index 5d08355ae4..afb57e22ba 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.test.tsx @@ -81,12 +81,6 @@ const techdocsStorageApiMock: jest.Mocked = { syncEntityDocs: jest.fn(), }; -const discoveryApiMock = { - getBaseUrl: jest - .fn() - .mockResolvedValue('https://localhost:7000/api/techdocs'), -}; - const fetchApiMock = { fetch: jest.fn().mockResolvedValue({ ok: true, @@ -116,7 +110,7 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => { Date: Wed, 9 Oct 2024 16:52:06 +0200 Subject: [PATCH 13/22] docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/test-utils/report.api.md | 48 +++-------- .../test-utils/src/testUtils/apis/mockApis.ts | 79 +++++++++++++++++-- 2 files changed, 85 insertions(+), 42 deletions(-) diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index cc09727d87..ad5528d507 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -92,9 +92,7 @@ export class MockAnalyticsApi implements AnalyticsApi { // @public export namespace mockApis { - // (undocumented) export function analytics(): jest.Mocked; - // (undocumented) export namespace analytics { const // (undocumented) factory: () => ApiFactory; @@ -114,11 +112,9 @@ export namespace mockApis { ) => ApiFactory; const mock: (partialImpl?: Partial | undefined) => ApiMock; } - // (undocumented) export function discovery(options?: { baseUrl?: string }): jest.Mocked<{ getBaseUrl(pluginId: string): Promise; }>; - // (undocumented) export namespace discovery { const // (undocumented) factory: ( @@ -133,7 +129,6 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } - // (undocumented) export function identity(options?: { userEntityRef?: string; ownershipEntityRefs?: string[]; @@ -142,7 +137,6 @@ export namespace mockApis { displayName?: string; picture?: string; }): jest.Mocked; - // (undocumented) export namespace identity { const // (undocumented) factory: ( @@ -162,7 +156,6 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } - // (undocumented) export function permission(options?: { authorize?: | AuthorizeResult.ALLOW @@ -171,7 +164,6 @@ export namespace mockApis { request: EvaluatePermissionRequest, ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY); }): jest.Mocked; - // (undocumented) export namespace permission { const // (undocumented) factory: ( @@ -192,11 +184,9 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } - // (undocumented) export function storage(options?: { data?: JsonObject; }): jest.Mocked; - // (undocumented) export namespace storage { const // (undocumented) factory: ( @@ -211,9 +201,7 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } - // (undocumented) export function translation(): jest.Mocked; - // (undocumented) export namespace translation { const // (undocumented) factory: () => ApiFactory; @@ -436,28 +424,16 @@ export function wrapInTestApp( // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "set". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:25:5 - (ae-undocumented) Missing documentation for "remove". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:26:5 - (ae-undocumented) Missing documentation for "observe$". -// src/testUtils/apis/mockApis.d.ts:47:5 - (ae-undocumented) Missing documentation for "analytics". -// src/testUtils/apis/mockApis.d.ts:48:5 - (ae-undocumented) Missing documentation for "analytics". -// src/testUtils/apis/mockApis.d.ts:49:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:50:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:101:5 - (ae-undocumented) Missing documentation for "discovery". -// src/testUtils/apis/mockApis.d.ts:106:5 - (ae-undocumented) Missing documentation for "discovery". -// src/testUtils/apis/mockApis.d.ts:107:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:110:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:112:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:120:5 - (ae-undocumented) Missing documentation for "identity". -// src/testUtils/apis/mockApis.d.ts:121:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:129:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:131:5 - (ae-undocumented) Missing documentation for "permission". -// src/testUtils/apis/mockApis.d.ts:134:5 - (ae-undocumented) Missing documentation for "permission". -// src/testUtils/apis/mockApis.d.ts:135:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:138:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:140:5 - (ae-undocumented) Missing documentation for "storage". -// src/testUtils/apis/mockApis.d.ts:143:5 - (ae-undocumented) Missing documentation for "storage". -// src/testUtils/apis/mockApis.d.ts:144:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:147:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:149:5 - (ae-undocumented) Missing documentation for "translation". -// src/testUtils/apis/mockApis.d.ts:150:5 - (ae-undocumented) Missing documentation for "translation". -// src/testUtils/apis/mockApis.d.ts:151:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:152:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:59:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:60:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:128:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:131:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:153:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:161:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:180:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:183:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:200:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:203:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:218:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:219:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index 1adf172893..21bf0626ba 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -129,16 +129,26 @@ export namespace mockApis { const analyticsMockSkeleton = (): jest.Mocked => ({ captureEvent: jest.fn(), }); + /** + * Mock implementation of {@link @backstage/core-plugin-api#AnalyticsApi}. + * + * @public + */ export function analytics() { return analyticsMockSkeleton(); } + /** + * Mock implementations of {@link @backstage/core-plugin-api#AnalyticsApi}. + * + * @public + */ export namespace analytics { export const factory = simpleFactory(analyticsApiRef, analytics); export const mock = simpleMock(analyticsApiRef, analyticsMockSkeleton); } /** - * Fake implementation of {@link @backstage/frontend-plugin-api#ConfigApi} + * Fake implementation of {@link @backstage/core-plugin-api#ConfigApi} * with optional data supplied. * * @public @@ -149,7 +159,7 @@ export namespace mockApis { * data: { app: { baseUrl: 'https://example.com' } }, * }); * - * const rendered = await renderInTestApp( + * await renderInTestApp( * * * , @@ -160,15 +170,15 @@ export namespace mockApis { return new ConfigReader(options?.data, 'mock-config'); } /** - * Mock helpers for {@link @backstage/frontend-plugin-api#ConfigApi}. + * Mock helpers for {@link @backstage/core-plugin-api#ConfigApi}. * - * @see {@link @backstage/frontend-plugin-api#mockApis.config} + * @see {@link @backstage/core-plugin-api#mockApis.config} * @public */ export namespace config { /** * Creates a factory for a fake implementation of - * {@link @backstage/frontend-plugin-api#ConfigApi} with optional + * {@link @backstage/core-plugin-api#ConfigApi} with optional * configuration data supplied. * * @public @@ -176,7 +186,7 @@ export namespace mockApis { export const factory = simpleFactory(configApiRef, config); /** * Creates a mock implementation of - * {@link @backstage/frontend-plugin-api#ConfigApi}. All methods are + * {@link @backstage/core-plugin-api#ConfigApi}. All methods are * replaced with jest mock functions, and you can optionally pass in a * subset of methods with an explicit implementation. * @@ -205,6 +215,12 @@ export namespace mockApis { const discoveryMockSkeleton = (): jest.Mocked => ({ getBaseUrl: jest.fn(), }); + /** + * Fake implementation of {@link @backstage/core-plugin-api#DiscoveryApi}. By + * default returns URLs on the form `http://example.com/api/`. + * + * @public + */ export function discovery(options?: { baseUrl?: string }) { const baseUrl = options?.baseUrl ?? 'http://example.com'; return simpleInstance( @@ -217,6 +233,11 @@ export namespace mockApis { discoveryMockSkeleton, ); } + /** + * Mock implementations of {@link @backstage/core-plugin-api#DiscoveryApi}. + * + * @public + */ export namespace discovery { export const factory = simpleFactory(discoveryApiRef, discovery); export const mock = simpleMock(discoveryApiRef, discoveryMockSkeleton); @@ -228,6 +249,12 @@ export namespace mockApis { getProfileInfo: jest.fn(), signOut: jest.fn(), }); + /** + * Fake implementation of {@link @backstage/core-plugin-api#IdentityApi}. By + * default returns no token or profile info, and the user `user:default/test`. + * + * @public + */ export function identity(options?: { userEntityRef?: string; ownershipEntityRefs?: string[]; @@ -257,6 +284,11 @@ export namespace mockApis { async signOut() {}, }; } + /** + * Mock implementations of {@link @backstage/core-plugin-api#IdentityApi}. + * + * @public + */ export namespace identity { export const factory = simpleFactory(identityApiRef, identity); export const mock = simpleMock(identityApiRef, identityMockSkeleton); @@ -265,6 +297,13 @@ export namespace mockApis { const permissionMockSkeleton = (): jest.Mocked => ({ authorize: jest.fn(), }); + /** + * Fake implementation of + * {@link @backstage/plugin-permission-react#PermissionApi}. By default allows + * all actions. + * + * @public + */ export function permission(options?: { authorize?: | AuthorizeResult.ALLOW @@ -286,6 +325,12 @@ export namespace mockApis { } return new MockPermissionApi(authorize); } + /** + * Mock implementation of + * {@link @backstage/plugin-permission-react#PermissionApi}. + * + * @public + */ export namespace permission { export const factory = simpleFactory(permissionApiRef, permission); export const mock = simpleMock(permissionApiRef, permissionMockSkeleton); @@ -298,9 +343,20 @@ export namespace mockApis { observe$: jest.fn(), snapshot: jest.fn(), }); + /** + * Fake implementation of {@link @backstage/core-plugin-api#StorageApi}. + * Stores data temporarily in memory. + * + * @public + */ export function storage(options?: { data?: JsonObject }) { return MockStorageApi.create(options?.data); } + /** + * Mock implementations of {@link @backstage/core-plugin-api#StorageApi}. + * + * @public + */ export namespace storage { export const factory = simpleFactory(storageApiRef, storage); export const mock = simpleMock(storageApiRef, storageMockSkeleton); @@ -310,9 +366,20 @@ export namespace mockApis { getTranslation: jest.fn(), translation$: jest.fn(), }); + /** + * Fake implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * By default returns the default translation. + * + * @public + */ export function translation() { return MockTranslationApi.create(); } + /** + * Mock implementations of {@link @backstage/core-plugin-api/alpha#TranslationApi}. + * + * @public + */ export namespace translation { export const factory = simpleFactory(translationApiRef, translation); export const mock = simpleMock(translationApiRef, translationMockSkeleton); From bc8b624ead233b890b599eefc66d77201983efa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 10 Oct 2024 15:32:30 +0200 Subject: [PATCH 14/22] fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../test-utils/src/testUtils/apis/mockApis.ts | 76 +++++++++---------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/packages/test-utils/src/testUtils/apis/mockApis.ts b/packages/test-utils/src/testUtils/apis/mockApis.ts index 21bf0626ba..00473679d2 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.ts +++ b/packages/test-utils/src/testUtils/apis/mockApis.ts @@ -134,7 +134,7 @@ export namespace mockApis { * * @public */ - export function analytics() { + export function analytics(): AnalyticsApi { return analyticsMockSkeleton(); } /** @@ -212,26 +212,19 @@ export namespace mockApis { })); } - const discoveryMockSkeleton = (): jest.Mocked => ({ - getBaseUrl: jest.fn(), - }); /** * Fake implementation of {@link @backstage/core-plugin-api#DiscoveryApi}. By * default returns URLs on the form `http://example.com/api/`. * * @public */ - export function discovery(options?: { baseUrl?: string }) { + export function discovery(options?: { baseUrl?: string }): DiscoveryApi { const baseUrl = options?.baseUrl ?? 'http://example.com'; - return simpleInstance( - discoveryApiRef, - { - async getBaseUrl(pluginId: string) { - return `${baseUrl}/api/${pluginId}`; - }, + return { + async getBaseUrl(pluginId: string) { + return `${baseUrl}/api/${pluginId}`; }, - discoveryMockSkeleton, - ); + }; } /** * Mock implementations of {@link @backstage/core-plugin-api#DiscoveryApi}. @@ -240,15 +233,11 @@ export namespace mockApis { */ export namespace discovery { export const factory = simpleFactory(discoveryApiRef, discovery); - export const mock = simpleMock(discoveryApiRef, discoveryMockSkeleton); + export const mock = simpleMock(discoveryApiRef, () => ({ + getBaseUrl: jest.fn(), + })); } - const identityMockSkeleton = (): jest.Mocked => ({ - getBackstageIdentity: jest.fn(), - getCredentials: jest.fn(), - getProfileInfo: jest.fn(), - signOut: jest.fn(), - }); /** * Fake implementation of {@link @backstage/core-plugin-api#IdentityApi}. By * default returns no token or profile info, and the user `user:default/test`. @@ -291,12 +280,17 @@ export namespace mockApis { */ export namespace identity { export const factory = simpleFactory(identityApiRef, identity); - export const mock = simpleMock(identityApiRef, identityMockSkeleton); + export const mock = simpleMock( + identityApiRef, + (): jest.Mocked => ({ + getBackstageIdentity: jest.fn(), + getCredentials: jest.fn(), + getProfileInfo: jest.fn(), + signOut: jest.fn(), + }), + ); } - const permissionMockSkeleton = (): jest.Mocked => ({ - authorize: jest.fn(), - }); /** * Fake implementation of * {@link @backstage/plugin-permission-react#PermissionApi}. By default allows @@ -311,7 +305,7 @@ export namespace mockApis { | (( request: EvaluatePermissionRequest, ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY); - }) { + }): PermissionApi { const authorizeInput = options?.authorize; let authorize: ( request: EvaluatePermissionRequest, @@ -333,23 +327,18 @@ export namespace mockApis { */ export namespace permission { export const factory = simpleFactory(permissionApiRef, permission); - export const mock = simpleMock(permissionApiRef, permissionMockSkeleton); + export const mock = simpleMock(permissionApiRef, () => ({ + authorize: jest.fn(), + })); } - const storageMockSkeleton = (): jest.Mocked => ({ - forBucket: jest.fn(), - set: jest.fn(), - remove: jest.fn(), - observe$: jest.fn(), - snapshot: jest.fn(), - }); /** * Fake implementation of {@link @backstage/core-plugin-api#StorageApi}. * Stores data temporarily in memory. * * @public */ - export function storage(options?: { data?: JsonObject }) { + export function storage(options?: { data?: JsonObject }): StorageApi { return MockStorageApi.create(options?.data); } /** @@ -359,20 +348,22 @@ export namespace mockApis { */ export namespace storage { export const factory = simpleFactory(storageApiRef, storage); - export const mock = simpleMock(storageApiRef, storageMockSkeleton); + export const mock = simpleMock(storageApiRef, () => ({ + forBucket: jest.fn(), + set: jest.fn(), + remove: jest.fn(), + observe$: jest.fn(), + snapshot: jest.fn(), + })); } - const translationMockSkeleton = (): jest.Mocked => ({ - getTranslation: jest.fn(), - translation$: jest.fn(), - }); /** * Fake implementation of {@link @backstage/core-plugin-api/alpha#TranslationApi}. * By default returns the default translation. * * @public */ - export function translation() { + export function translation(): TranslationApi { return MockTranslationApi.create(); } /** @@ -382,6 +373,9 @@ export namespace mockApis { */ export namespace translation { export const factory = simpleFactory(translationApiRef, translation); - export const mock = simpleMock(translationApiRef, translationMockSkeleton); + export const mock = simpleMock(translationApiRef, () => ({ + getTranslation: jest.fn(), + translation$: jest.fn(), + })); } } From 34fcc9600495d8cbf262e011f6d26f24541711bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 10 Oct 2024 16:35:28 +0200 Subject: [PATCH 15/22] fixup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- ...dentityAuthInjectorFetchMiddleware.test.ts | 2 +- ...ginProtocolResolverFetchMiddleware.test.ts | 2 +- packages/test-utils/report.api.md | 40 +++++++++---------- .../src/testUtils/apis/mockApis.test.tsx | 30 +++----------- .../UserListPicker/UserListPicker.test.tsx | 2 + .../useOwnedEntitiesCount.test.tsx | 1 + .../src/hooks/useEntity.test.tsx | 4 +- .../src/hooks/usePermission.test.tsx | 2 + plugins/techdocs/src/client.test.ts | 7 ---- 9 files changed, 33 insertions(+), 57 deletions(-) diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts index f1daced2a1..50f93444ed 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts @@ -58,7 +58,7 @@ describe('IdentityAuthInjectorFetchMiddleware', () => { }); it('injects the header only when a token is available', async () => { - const identityApi = mockApis.identity(); + const identityApi = mockApis.identity.mock(); const middleware = new IdentityAuthInjectorFetchMiddleware( identityApi, diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts index 7a9ebdc4f9..bd82d58736 100644 --- a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -21,7 +21,7 @@ describe('PluginProtocolResolverFetchMiddleware', () => { it.each([['https://passthrough.com/a']])( 'passes through regular URLs, %p', async url => { - const discoveryApi = mockApis.discovery(); + const discoveryApi = mockApis.discovery.mock(); const middleware = new PluginProtocolResolverFetchMiddleware( discoveryApi, ); diff --git a/packages/test-utils/report.api.md b/packages/test-utils/report.api.md index ad5528d507..11657ce534 100644 --- a/packages/test-utils/report.api.md +++ b/packages/test-utils/report.api.md @@ -92,7 +92,7 @@ export class MockAnalyticsApi implements AnalyticsApi { // @public export namespace mockApis { - export function analytics(): jest.Mocked; + export function analytics(): AnalyticsApi; export namespace analytics { const // (undocumented) factory: () => ApiFactory; @@ -112,9 +112,7 @@ export namespace mockApis { ) => ApiFactory; const mock: (partialImpl?: Partial | undefined) => ApiMock; } - export function discovery(options?: { baseUrl?: string }): jest.Mocked<{ - getBaseUrl(pluginId: string): Promise; - }>; + export function discovery(options?: { baseUrl?: string }): DiscoveryApi; export namespace discovery { const // (undocumented) factory: ( @@ -136,7 +134,7 @@ export namespace mockApis { email?: string; displayName?: string; picture?: string; - }): jest.Mocked; + }): IdentityApi; export namespace identity { const // (undocumented) factory: ( @@ -163,7 +161,7 @@ export namespace mockApis { | (( request: EvaluatePermissionRequest, ) => AuthorizeResult.ALLOW | AuthorizeResult.DENY); - }): jest.Mocked; + }): PermissionApi; export namespace permission { const // (undocumented) factory: ( @@ -184,9 +182,7 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } - export function storage(options?: { - data?: JsonObject; - }): jest.Mocked; + export function storage(options?: { data?: JsonObject }): StorageApi; export namespace storage { const // (undocumented) factory: ( @@ -201,7 +197,7 @@ export namespace mockApis { partialImpl?: Partial | undefined, ) => ApiMock; } - export function translation(): jest.Mocked; + export function translation(): TranslationApi; export namespace translation { const // (undocumented) factory: () => ApiFactory; @@ -424,16 +420,16 @@ export function wrapInTestApp( // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:24:5 - (ae-undocumented) Missing documentation for "set". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:25:5 - (ae-undocumented) Missing documentation for "remove". // src/testUtils/apis/StorageApi/MockStorageApi.d.ts:26:5 - (ae-undocumented) Missing documentation for "observe$". -// src/testUtils/apis/mockApis.d.ts:59:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:60:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:128:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:131:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:153:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:161:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:180:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:183:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:200:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:203:15 - (ae-undocumented) Missing documentation for "mock". -// src/testUtils/apis/mockApis.d.ts:218:15 - (ae-undocumented) Missing documentation for "factory". -// src/testUtils/apis/mockApis.d.ts:219:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:58:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:59:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:125:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:128:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:150:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:158:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:177:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:180:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:197:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:200:15 - (ae-undocumented) Missing documentation for "mock". +// src/testUtils/apis/mockApis.d.ts:215:15 - (ae-undocumented) Missing documentation for "factory". +// src/testUtils/apis/mockApis.d.ts:216:15 - (ae-undocumented) Missing documentation for "mock". ``` diff --git a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx index af0bbaee72..31cb03e117 100644 --- a/packages/test-utils/src/testUtils/apis/mockApis.test.tsx +++ b/packages/test-utils/src/testUtils/apis/mockApis.test.tsx @@ -25,7 +25,7 @@ import { createTranslationRef } from '@backstage/core-plugin-api/alpha'; describe('mockApis', () => { describe('analytics', () => { - it('can create an instance and make assertions on it', () => { + it('can create an instance', () => { const analytics = mockApis.analytics(); expect( analytics.captureEvent({ @@ -34,7 +34,6 @@ describe('mockApis', () => { context: { pluginId: 'c', extension: 'd', routeRef: 'e' }, }), ).toBeUndefined(); - expect(analytics.captureEvent).toHaveBeenCalledTimes(1); }); it('can create a mock and make assertions on it', async () => { @@ -80,18 +79,16 @@ describe('mockApis', () => { }); describe('discovery', () => { - it('can create an instance and make assertions on it', async () => { + it('can create an instance', async () => { const empty = mockApis.discovery(); await expect(empty.getBaseUrl('catalog')).resolves.toBe( 'http://example.com/api/catalog', ); - expect(empty.getBaseUrl).toHaveBeenCalledTimes(1); const notEmpty = mockApis.discovery({ baseUrl: 'https://other.net' }); await expect(notEmpty.getBaseUrl('catalog')).resolves.toBe( 'https://other.net/api/catalog', ); - expect(notEmpty.getBaseUrl).toHaveBeenCalledTimes(1); }); it('can create a mock and make assertions on it', async () => { @@ -108,7 +105,7 @@ describe('mockApis', () => { }); describe('identity', () => { - it('can create an instance and make assertions on it', async () => { + it('can create an instance', async () => { const empty = mockApis.identity(); await expect(empty.getBackstageIdentity()).resolves.toEqual({ type: 'user', @@ -118,10 +115,6 @@ describe('mockApis', () => { await expect(empty.getCredentials()).resolves.toEqual({}); await expect(empty.getProfileInfo()).resolves.toEqual({}); await expect(empty.signOut()).resolves.toBeUndefined(); - expect(empty.getBackstageIdentity).toHaveBeenCalledTimes(1); - expect(empty.getCredentials).toHaveBeenCalledTimes(1); - expect(empty.getProfileInfo).toHaveBeenCalledTimes(1); - expect(empty.signOut).toHaveBeenCalledTimes(1); const notEmpty = mockApis.identity({ userEntityRef: 'a', @@ -143,10 +136,6 @@ describe('mockApis', () => { picture: 'f', }); await expect(notEmpty.signOut()).resolves.toBeUndefined(); - expect(notEmpty.getBackstageIdentity).toHaveBeenCalledTimes(1); - expect(notEmpty.getCredentials).toHaveBeenCalledTimes(1); - expect(notEmpty.getProfileInfo).toHaveBeenCalledTimes(1); - expect(notEmpty.signOut).toHaveBeenCalledTimes(1); }); it('can create a mock and make assertions on it', async () => { @@ -194,7 +183,7 @@ describe('mockApis', () => { }); describe('permission', () => { - it('can create an instance and make assertions on it', async () => { + it('can create an instance', async () => { // default allow const permission1 = mockApis.permission(); await expect( @@ -205,7 +194,6 @@ describe('mockApis', () => { }), }), ).resolves.toEqual({ result: AuthorizeResult.ALLOW }); - expect(permission1.authorize).toHaveBeenCalledTimes(1); // static value const permission2 = mockApis.permission({ @@ -219,7 +207,6 @@ describe('mockApis', () => { }), }), ).resolves.toEqual({ result: AuthorizeResult.DENY }); - expect(permission2.authorize).toHaveBeenCalledTimes(1); // callback form const permission3 = mockApis.permission({ @@ -244,7 +231,6 @@ describe('mockApis', () => { }), }), ).resolves.toEqual({ result: AuthorizeResult.DENY }); - expect(permission3.authorize).toHaveBeenCalledTimes(2); }); it('can create a mock and make assertions on it', async () => { @@ -548,10 +534,9 @@ describe('mockApis', () => { }); }); - it('can create an instance and make assertions on it', () => { + it('can create an instance', () => { const empty = mockApis.storage(); expect(empty.snapshot('a')).toEqual({ key: 'a', presence: 'absent' }); - expect(empty.snapshot).toHaveBeenCalledTimes(1); const notEmpty = mockApis.storage({ data: { a: 1, b: { c: 2 } } }); expect(notEmpty.snapshot('a')).toEqual({ @@ -564,8 +549,6 @@ describe('mockApis', () => { presence: 'present', value: 2, }); - expect(notEmpty.snapshot).toHaveBeenCalledTimes(1); // "inner" (forBucket returned) instances aren't mocked - expect(notEmpty.forBucket).toHaveBeenCalledTimes(1); }); it('can create a mock and make assertions on it', () => { @@ -750,7 +733,7 @@ describe('mockApis', () => { }); }); - it('can create an instance and make assertions on it', () => { + it('can create an instance', () => { const translation = mockApis.translation(); const ref = createTranslationRef({ id: 'test', @@ -761,7 +744,6 @@ describe('mockApis', () => { throw new Error('not ready'); } expect(result.t('a')).toEqual('b'); - expect(translation.getTranslation).toHaveBeenCalledTimes(1); }); it('can create a mock and make assertions on it', () => { diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 7ff30dbf5f..3ef5c242a1 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -66,11 +66,13 @@ const mockConfigApi = mockApis.config({ }); const mockCatalogApi = catalogApiMock.mock(); +jest.spyOn(mockCatalogApi, 'queryEntities'); const mockIdentityApi = mockApis.identity({ userEntityRef: ownershipEntityRefs[0], ownershipEntityRefs, }); +jest.spyOn(mockIdentityApi, 'getBackstageIdentity'); const mockStarredEntitiesApi = new MockStarredEntitiesApi(); diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx index 99b746c80c..8ea9f63472 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -39,6 +39,7 @@ const mockIdentityApi = mockApis.identity({ ownershipEntityRefs: ['user:default/spiderman', 'user:group/a-group'], userEntityRef: 'user:default/spiderman', }); +jest.spyOn(mockIdentityApi, 'getBackstageIdentity'); jest.mock('@backstage/core-plugin-api', () => { const actual = jest.requireActual('@backstage/core-plugin-api'); diff --git a/plugins/catalog-react/src/hooks/useEntity.test.tsx b/plugins/catalog-react/src/hooks/useEntity.test.tsx index c0ad3c2111..022584bd4c 100644 --- a/plugins/catalog-react/src/hooks/useEntity.test.tsx +++ b/plugins/catalog-react/src/hooks/useEntity.test.tsx @@ -131,7 +131,7 @@ describe('useAsyncEntity', () => { }); it('should provide entityRef analytics context', () => { - const analyticsSpy = mockApis.analytics(); + const analyticsSpy = mockApis.analytics.mock(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: React.PropsWithChildren<{}>) => ( @@ -154,7 +154,7 @@ describe('useAsyncEntity', () => { }); it('should omit entityRef analytics context', () => { - const analyticsSpy = mockApis.analytics(); + const analyticsSpy = mockApis.analytics.mock(); const apis = TestApiRegistry.from([analyticsApiRef, analyticsSpy]); const { result } = renderHook(() => useAnalytics(), { wrapper: ({ children }: PropsWithChildren<{}>) => ( diff --git a/plugins/permission-react/src/hooks/usePermission.test.tsx b/plugins/permission-react/src/hooks/usePermission.test.tsx index e4a0c82eab..4be9af3b32 100644 --- a/plugins/permission-react/src/hooks/usePermission.test.tsx +++ b/plugins/permission-react/src/hooks/usePermission.test.tsx @@ -67,6 +67,7 @@ describe('usePermission', () => { const permissionApi = mockApis.permission({ authorize: AuthorizeResult.ALLOW, }); + jest.spyOn(permissionApi, 'authorize'); const { findByText } = renderComponent(permissionApi); @@ -78,6 +79,7 @@ describe('usePermission', () => { const permissionApi = mockApis.permission({ authorize: AuthorizeResult.DENY, }); + jest.spyOn(permissionApi, 'authorize'); const { findByText } = renderComponent(permissionApi); diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index 8724e027f5..f66587a071 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -40,7 +40,6 @@ describe('TechDocsStorageClient', () => { beforeEach(() => { jest.resetAllMocks(); - identityApi.getCredentials.mockResolvedValue({ token: undefined }); }); it('should return correct base url based on defined storage', async () => { @@ -92,7 +91,6 @@ describe('TechDocsStorageClient', () => { await Promise.resolve(); onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await storageApi.syncEntityDocs(mockEntity); expect(mockFetchEventSource).toHaveBeenCalledWith( @@ -123,7 +121,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'cached', ); @@ -143,7 +140,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": true}' }); }); - identityApi.getCredentials.mockResolvedValue({}); await expect(storageApi.syncEntityDocs(mockEntity)).resolves.toEqual( 'updated', ); @@ -166,7 +162,6 @@ describe('TechDocsStorageClient', () => { onmessage?.({ id: '', event: 'finish', data: '{"updated": false}' }); }); - identityApi.getCredentials.mockResolvedValue({}); const logHandler = jest.fn(); await expect( storageApi.syncEntityDocs(mockEntity, logHandler), @@ -189,7 +184,6 @@ describe('TechDocsStorageClient', () => { }); // we await later after we emitted the error - identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); await expect(promise).rejects.toThrow(NotFoundError); @@ -204,7 +198,6 @@ describe('TechDocsStorageClient', () => { }); // we await later after we emitted the error - identityApi.getCredentials.mockResolvedValue({}); const promise = storageApi.syncEntityDocs(mockEntity).then(); mockFetchEventSource.mockImplementation(async (_url, options) => { From 6d2899cdca906c8429f089467e5153c5bb678ad9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 11 Oct 2024 11:05:17 +0200 Subject: [PATCH 16/22] docs/tooling: update backend dev bundling docs Signed-off-by: Patrik Oldsberg --- docs/tooling/cli/02-build-system.md | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/docs/tooling/cli/02-build-system.md b/docs/tooling/cli/02-build-system.md index dfd95ae6eb..96c46bed6a 100644 --- a/docs/tooling/cli/02-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -364,22 +364,19 @@ load performance. ### Backend Development -The backend development bundling is also based on Webpack, but rather than -starting up a web server, the backend is started up using the -[`RunScriptWebpackPlugin`](https://www.npmjs.com/package/run-script-webpack-plugin). -The reason for using Webpack for development of the backend is both that it is a -convenient way to handle transpilation of a large set of packages, as well us -allowing us to use hot module replacement and maintaining state while reloading -individual backend modules. This is particularly useful when running the backend -with in-memory SQLite as the database choice. +The backend development setup does not use any bundling process. It runs a +Node.js process directly, with only-the-fly transpilation from TypeScript to +JavaScript. The transpilation is done with a custom transform based on +[SWC](https://swc.rs/). -Except for executing in Node.js rather than a web server, the backend -development bundling configuration is quite similar to the frontend one. It -shares most of the Webpack configuration, including the transpilation setup. -Some differences are that it does not inject any environment variables or node -module fallbacks, and it uses -[`webpack-node-externals`](https://www.npmjs.com/package/webpack-node-externals) -to avoid bundling in dependency modules. +During development the backend Node.js process will restart whenever there is a +change to the source code. This means that any in-memory data will be lost. In +order to store data between restarts, the backend process has an IPC channel +available to store and restore data from the parent CLI process. The primary +purpose of this, which is already built-in, is to restore the contents of +databases when using SQLite for development. You can also use it for your own +purposes too, with the `DevDataStore` utility exported from the +`@backstage/backend-dev-utils` package. If you want to inspect the running Node.js process, the `--inspect` and `--inspect-brk` flags can be used, as they will be passed through as options to From 321a994358323858ee4508a696f7c0f105438e02 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 11 Oct 2024 11:44:36 +0200 Subject: [PATCH 17/22] backend-defaults: define token field on credentials as non-enumerable Signed-off-by: Patrik Oldsberg --- .changeset/clever-rats-rush.md | 5 ++ .../src/entrypoints/auth/helpers.test.ts | 67 +++++++++++++++++++ .../src/entrypoints/auth/helpers.ts | 50 +++++++++----- 3 files changed, 104 insertions(+), 18 deletions(-) create mode 100644 .changeset/clever-rats-rush.md create mode 100644 packages/backend-defaults/src/entrypoints/auth/helpers.test.ts diff --git a/.changeset/clever-rats-rush.md b/.changeset/clever-rats-rush.md new file mode 100644 index 0000000000..c34427f8f5 --- /dev/null +++ b/.changeset/clever-rats-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Sensitive internal fields on `BackstageCredentials` objects are now defined as read-only properties in order to minimize risk of leakage. diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts new file mode 100644 index 0000000000..8454aa3ecc --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.test.ts @@ -0,0 +1,67 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createCredentialsWithNonePrincipal, + createCredentialsWithServicePrincipal, + createCredentialsWithUserPrincipal, +} from './helpers'; + +describe('credentials', () => { + it('should be created', () => { + expect(createCredentialsWithServicePrincipal('my-service')).toEqual({ + $$type: '@backstage/BackstageCredentials', + version: 'v1', + principal: { + type: 'service', + subject: 'my-service', + }, + }); + + expect( + createCredentialsWithUserPrincipal('user:default/mock', 'my-token'), + ).toEqual({ + $$type: '@backstage/BackstageCredentials', + version: 'v1', + principal: { + type: 'user', + userEntityRef: 'user:default/mock', + }, + }); + + expect(createCredentialsWithNonePrincipal()).toEqual({ + $$type: '@backstage/BackstageCredentials', + version: 'v1', + principal: { + type: 'none', + }, + }); + }); + + it('should not include tokens when serialized', () => { + expect( + JSON.stringify( + createCredentialsWithServicePrincipal('my-service', 'my-token'), + ), + ).not.toMatch(/my-token/); + + expect( + JSON.stringify( + createCredentialsWithUserPrincipal('user:default/mock', 'my-token'), + ), + ).not.toMatch(/my-token/); + }); +}); diff --git a/packages/backend-defaults/src/entrypoints/auth/helpers.ts b/packages/backend-defaults/src/entrypoints/auth/helpers.ts index eebe45eb76..dad36670b9 100644 --- a/packages/backend-defaults/src/entrypoints/auth/helpers.ts +++ b/packages/backend-defaults/src/entrypoints/auth/helpers.ts @@ -28,16 +28,23 @@ export function createCredentialsWithServicePrincipal( token?: string, accessRestrictions?: BackstagePrincipalAccessRestrictions, ): InternalBackstageCredentials { - return { - $$type: '@backstage/BackstageCredentials', - version: 'v1', - token, - principal: { - type: 'service', - subject: sub, - accessRestrictions, + return Object.defineProperty( + { + $$type: '@backstage/BackstageCredentials', + version: 'v1', + principal: { + type: 'service', + subject: sub, + accessRestrictions, + }, }, - }; + 'token', + { + enumerable: false, + configurable: true, + value: token, + }, + ); } export function createCredentialsWithUserPrincipal( @@ -45,16 +52,23 @@ export function createCredentialsWithUserPrincipal( token: string, expiresAt?: Date, ): InternalBackstageCredentials { - return { - $$type: '@backstage/BackstageCredentials', - version: 'v1', - token, - expiresAt, - principal: { - type: 'user', - userEntityRef: sub, + return Object.defineProperty( + { + $$type: '@backstage/BackstageCredentials', + version: 'v1', + expiresAt, + principal: { + type: 'user', + userEntityRef: sub, + }, }, - }; + 'token', + { + enumerable: false, + configurable: true, + value: token, + }, + ); } export function createCredentialsWithNonePrincipal(): InternalBackstageCredentials { From dea0d6cd11d95ab4f0d747d7513d02115daf8a52 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 11 Oct 2024 12:00:17 +0200 Subject: [PATCH 18/22] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Patrik Oldsberg --- docs/tooling/cli/02-build-system.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tooling/cli/02-build-system.md b/docs/tooling/cli/02-build-system.md index 96c46bed6a..f1fb5f6c9e 100644 --- a/docs/tooling/cli/02-build-system.md +++ b/docs/tooling/cli/02-build-system.md @@ -365,7 +365,7 @@ load performance. ### Backend Development The backend development setup does not use any bundling process. It runs a -Node.js process directly, with only-the-fly transpilation from TypeScript to +Node.js process directly, with on-the-fly transpilation from TypeScript to JavaScript. The transpilation is done with a custom transform based on [SWC](https://swc.rs/). @@ -375,7 +375,7 @@ order to store data between restarts, the backend process has an IPC channel available to store and restore data from the parent CLI process. The primary purpose of this, which is already built-in, is to restore the contents of databases when using SQLite for development. You can also use it for your own -purposes too, with the `DevDataStore` utility exported from the +purposes, with the `DevDataStore` utility exported from the `@backstage/backend-dev-utils` package. If you want to inspect the running Node.js process, the `--inspect` and From 8fd7debe89d56c9cdffddf2d48d3b6ef559d2a49 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 11 Oct 2024 12:44:08 +0200 Subject: [PATCH 19/22] backend-defaults: pretty-print JSON responses during development Signed-off-by: Patrik Oldsberg --- .changeset/slow-walls-report.md | 13 +++++++++++++ .../core-services/root-http-router.md | 9 ++++++++- .../rootHttpRouter/rootHttpRouterServiceFactory.ts | 3 +++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/slow-walls-report.md diff --git a/.changeset/slow-walls-report.md b/.changeset/slow-walls-report.md new file mode 100644 index 0000000000..e5ab380006 --- /dev/null +++ b/.changeset/slow-walls-report.md @@ -0,0 +1,13 @@ +--- +'@backstage/backend-defaults': patch +--- + +The default root HTTP service implementation will now pretty-print JSON responses in development. + +If you are overriding the `rootHttpRouterServiceFactory` with a `configure` function that doesn't call `applyDefaults`, you can introduce this functionality by adding the following snippet inside `configure`: + +```ts +if (process.env.NODE_ENV === 'development') { + app.set('json spaces', 2); +} +``` diff --git a/docs/backend-system/core-services/root-http-router.md b/docs/backend-system/core-services/root-http-router.md index 88a10455f5..a2316b957b 100644 --- a/docs/backend-system/core-services/root-http-router.md +++ b/docs/backend-system/core-services/root-http-router.md @@ -59,7 +59,7 @@ const backend = createBackend(); backend.add( rootHttpRouterServiceFactory({ - configure: ({ app, middleware, routes, config, logger, lifecycle }) => { + configure: ({ app, middleware, routes, config, logger, healthRouter }) => { // Refer to https://expressjs.com/en/guide/writing-middleware.html on how to write express middleware const customMiddleware = { logging(): RequestHandler { @@ -88,11 +88,18 @@ backend.add( }, }; + // The default implementation pretty-prints JSON responses in development + if (process.env.NODE_ENV === 'development') { + app.set('json spaces', 2); + } + // the built in middleware is provided through an option in the configure function app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); + app.use(healthRouter); + // you can add you your own middleware in here app.use(customMiddleware.logging()); diff --git a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts index a18ddbe096..59811c15d4 100644 --- a/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -106,6 +106,9 @@ const rootHttpRouterServiceFactoryWithOptions = ( lifecycle, healthRouter, applyDefaults() { + if (process.env.NODE_ENV === 'development') { + app.set('json spaces', 2); + } app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); From a881f94e6387ea3920d205e650fbab5252237c39 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 11 Oct 2024 15:05:44 +0200 Subject: [PATCH 20/22] docs/tooling: fix headers on profiling page Signed-off-by: Patrik Oldsberg --- docs/tooling/local-dev/profiling.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tooling/local-dev/profiling.md b/docs/tooling/local-dev/profiling.md index bbc263c3d5..b1c904d6d0 100644 --- a/docs/tooling/local-dev/profiling.md +++ b/docs/tooling/local-dev/profiling.md @@ -7,7 +7,7 @@ description: Finding performance bottlenecks in your Backstage application Profiling can help you find performance bottlenecks in your code. This guide will show you how to profile both the backend and frontend of your Backstage application. -# Backend +## Backend To profile the backend, start the backend with the `--inspect` flag: @@ -27,7 +27,7 @@ You can also use the `Memory` tab to profile the backend's memory usage and find It's recommended to start profiling with short periods of time to avoid too much data being collected. -## Stress testing +### Stress testing To get more out of profiling, you might want to introduce additional load to your application with some tooling. One such tool is called [AutoCannon](https://www.npmjs.com/package/autocannon) which can be used to stress test the @@ -55,7 +55,7 @@ autocannon -H "Authorization=Bearer autocannon12345" http://localhost:7007/api/c See more command options in the AutoCannon documentation. -# Frontend +## Frontend Profiling the frontend can be done by using the `React DevTools` extension for Chrome or Firefox. The extension is available for download from the Chrome Web Store or the Firefox Add-ons website. From 511651b1a705d75f6745104763dbe1c6ab82bf2a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 15:17:05 +0000 Subject: [PATCH 21/22] Update dependency @opentelemetry/auto-instrumentations-node to v0.50.2 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/yarn.lock b/yarn.lock index 75503208bb..88d3c30197 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13027,8 +13027,8 @@ __metadata: linkType: hard "@opentelemetry/auto-instrumentations-node@npm:^0.50.0": - version: 0.50.1 - resolution: "@opentelemetry/auto-instrumentations-node@npm:0.50.1" + version: 0.50.2 + resolution: "@opentelemetry/auto-instrumentations-node@npm:0.50.2" dependencies: "@opentelemetry/instrumentation": ^0.53.0 "@opentelemetry/instrumentation-amqplib": ^0.42.0 @@ -13060,7 +13060,7 @@ __metadata: "@opentelemetry/instrumentation-mysql2": ^0.41.0 "@opentelemetry/instrumentation-nestjs-core": ^0.40.0 "@opentelemetry/instrumentation-net": ^0.39.0 - "@opentelemetry/instrumentation-pg": ^0.45.0 + "@opentelemetry/instrumentation-pg": ^0.45.1 "@opentelemetry/instrumentation-pino": ^0.42.0 "@opentelemetry/instrumentation-redis": ^0.42.0 "@opentelemetry/instrumentation-redis-4": ^0.42.1 @@ -13070,16 +13070,16 @@ __metadata: "@opentelemetry/instrumentation-tedious": ^0.14.0 "@opentelemetry/instrumentation-undici": ^0.6.0 "@opentelemetry/instrumentation-winston": ^0.40.0 - "@opentelemetry/resource-detector-alibaba-cloud": ^0.29.2 + "@opentelemetry/resource-detector-alibaba-cloud": ^0.29.3 "@opentelemetry/resource-detector-aws": ^1.6.2 "@opentelemetry/resource-detector-azure": ^0.2.11 - "@opentelemetry/resource-detector-container": ^0.4.2 + "@opentelemetry/resource-detector-container": ^0.4.3 "@opentelemetry/resource-detector-gcp": ^0.29.12 "@opentelemetry/resources": ^1.24.0 "@opentelemetry/sdk-node": ^0.53.0 peerDependencies: "@opentelemetry/api": ^1.4.1 - checksum: 978c1abc6c61d0544fae8db30faa32d5400013edd3d57298031491e87d4bf6ec7bfea58d8020b76c01c87eac63496d3abd22f32f25e8cc00d20e31c72d81f4ee + checksum: b90b4962b90722c458a00cf9efeeb0fff53091230ffc15043b935dc81d862d9c3125b5cc71fda836cb8283a4fb51ecffa59fbd2f0f4a3359420384d5dd68e26b languageName: node linkType: hard @@ -13103,7 +13103,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:1.26.0, @opentelemetry/core@npm:^1.0.0, @opentelemetry/core@npm:^1.1.0, @opentelemetry/core@npm:^1.25.0, @opentelemetry/core@npm:^1.25.1, @opentelemetry/core@npm:^1.8.0": +"@opentelemetry/core@npm:1.26.0, @opentelemetry/core@npm:^1.0.0, @opentelemetry/core@npm:^1.1.0, @opentelemetry/core@npm:^1.25.0, @opentelemetry/core@npm:^1.25.1, @opentelemetry/core@npm:^1.26.0, @opentelemetry/core@npm:^1.8.0": version: 1.26.0 resolution: "@opentelemetry/core@npm:1.26.0" dependencies: @@ -13599,18 +13599,19 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/instrumentation-pg@npm:^0.45.0": - version: 0.45.0 - resolution: "@opentelemetry/instrumentation-pg@npm:0.45.0" +"@opentelemetry/instrumentation-pg@npm:^0.45.1": + version: 0.45.1 + resolution: "@opentelemetry/instrumentation-pg@npm:0.45.1" dependencies: + "@opentelemetry/core": ^1.26.0 "@opentelemetry/instrumentation": ^0.53.0 - "@opentelemetry/semantic-conventions": ^1.27.0 + "@opentelemetry/semantic-conventions": 1.27.0 "@opentelemetry/sql-common": ^0.40.1 "@types/pg": 8.6.1 "@types/pg-pool": 2.0.6 peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 059f79eeca92e27e1eee06ab55659a4eeee389c004ee2d8542e6bcce2147faf7c0ea568a5f305b2ee8065df575c44f6de80effb5c76adc1d3f098cdb3c2e3bdf + checksum: 29e6641ca221fe1e7c1a659ac37f07573f86abfc7f2a6afafdb528d70677522c54a35ff4fd3243a965dc4a8fc27f2c17b422fbc32789c0ee8a27e67e1f47aff0 languageName: node linkType: hard @@ -13835,15 +13836,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.29.2": - version: 0.29.2 - resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.29.2" +"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.29.3": + version: 0.29.3 + resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.29.3" dependencies: + "@opentelemetry/core": ^1.26.0 "@opentelemetry/resources": ^1.10.0 "@opentelemetry/semantic-conventions": ^1.27.0 peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 817a024d28c5f3c79a65377115193ccf2171626e8d79e47210baea9db75c55c0a48d8f50fd19d9bf2ece422c3ae7c75d5d64879d4941e4a15c1757a2877be77e + checksum: e3cb36aa8b4ce7b6fc60fe15f51139493728f4c8e7af11f70cb567b5667680c16147bc382aa1a330b8bca26b392e7dddd5f66b6c393ca50aa315329baa0bea14 languageName: node linkType: hard @@ -13873,15 +13875,16 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resource-detector-container@npm:^0.4.2": - version: 0.4.2 - resolution: "@opentelemetry/resource-detector-container@npm:0.4.2" +"@opentelemetry/resource-detector-container@npm:^0.4.3": + version: 0.4.3 + resolution: "@opentelemetry/resource-detector-container@npm:0.4.3" dependencies: + "@opentelemetry/core": ^1.26.0 "@opentelemetry/resources": ^1.10.0 "@opentelemetry/semantic-conventions": ^1.27.0 peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 69b7d105b8d58e92bd4d35b7eba7a8be97fa4c89ba8006a590c03d028f84150bc5360d176933b0aba03c81ee0613cf46c74213cb52e96c05dd1497e2b4512875 + checksum: 301a98524027442030cb6ea17c75988c29925586d86acde637d9129007d78e4dd27f7a3af30998000c475d2aae2a08d3269259e5d49973a7824f41ee144285e4 languageName: node linkType: hard From 159d87c7e86a50243e8df09f6ffaed8f839e607c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 11 Oct 2024 16:00:56 +0000 Subject: [PATCH 22/22] Update dependency @types/react-dom to v18.3.1 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 88d3c30197..badd0f81f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18583,11 +18583,11 @@ __metadata: linkType: hard "@types/react-dom@npm:^18": - version: 18.3.0 - resolution: "@types/react-dom@npm:18.3.0" + version: 18.3.1 + resolution: "@types/react-dom@npm:18.3.1" dependencies: "@types/react": "*" - checksum: a0cd9b1b815a6abd2a367a9eabdd8df8dd8f13f95897b2f9e1359ea3ac6619f957c1432ece004af7d95e2a7caddbba19faa045f831f32d6263483fc5404a7596 + checksum: ad28ecce3915d30dc76adc2a1373fda1745ba429cea290e16c6628df9a05fd80b6403c8e87d78b45e6c60e51df7a67add389ab62b90070fbfdc9bda8307d9953 languageName: node linkType: hard