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] 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(), }); }); });