From 75afcd9f48a6d92440ee367869c832deeba9b21f Mon Sep 17 00:00:00 2001 From: Eyo Okon Eyo Date: Wed, 26 Feb 2025 16:18:58 +0100 Subject: [PATCH] start on fixing tests --- .../public/components/share_tabs.test.tsx | 152 ++++++++---- .../share/public/components/share_tabs.tsx | 11 +- .../plugins/shared/share/public/mocks.ts | 3 +- .../services/share_menu_registry.test.ts | 216 +++++++++++++++--- .../public/services/share_menu_registry.ts | 6 +- .../plugins/shared/share/public/types.ts | 5 +- 6 files changed, 314 insertions(+), 79 deletions(-) diff --git a/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx b/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx index a2f3e51687fe0..d11c4024e12c9 100644 --- a/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx +++ b/src/platform/plugins/shared/share/public/components/share_tabs.test.tsx @@ -50,13 +50,31 @@ const service = new UrlService { describe('link tab', () => { - it('should not render the link tab when the disableShareUrl prop is true', async () => { + it('should not render the link tab when it is configured as disabled', async () => { + const disabledLinkShareContext = { + ...mockShareContext, + objectTypeMeta: { + ...mockShareContext.objectTypeMeta, + config: { + ...mockShareContext.objectTypeMeta.config, + link: { + disabled: true, + }, + }, + }, + }; + const wrapper = mountWithIntl( - + ); @@ -84,33 +115,55 @@ describe('Share modal tabs', () => { describe('export tab', () => { it('should render export tab when there are share menu items that are not disabled', async () => { - const testItem = [ - { - shareMenuItem: { name: 'test', disabled: false }, - label: CSV, - generateExport: mockGenerateExport, - generateExportUrl: mockGenerateExportUrl, - }, - ]; + const shareContextWithConfiguredExportItem: IShareContext = { + ...mockShareContext, + shareMenuItems: [ + ...mockShareContext.shareMenuItems, + { + id: 'test-export', + shareType: 'integration', + groupId: 'export', + config: { + name: 'test', + disabled: false, + label: CSV, + generateExport: mockGenerateExport, + generateExportUrl: mockGenerateExportUrl, + }, + }, + ], + }; + const wrapper = mountWithIntl( - + ); expect(wrapper.find('[data-test-subj="export"]').exists()).toBeTruthy(); }); - it('should not render export tab when the license is disabled', async () => { - const testItems = [ - { - shareMenuItem: { name: 'test', disabled: true }, - label: CSV, - generateExport: mockGenerateExport, - generateExportUrl: mockGenerateExportUrl, - }, - ]; + + it('should not render export tab when it has only one item configured as disabled', async () => { + const shareContextWithConfiguredExportItem: IShareContext = { + ...mockShareContext, + shareMenuItems: [ + ...mockShareContext.shareMenuItems, + { + id: 'test-export', + shareType: 'integration', + groupId: 'export', + config: { + name: 'test', + disabled: true, + label: CSV, + generateExport: mockGenerateExport, + generateExportUrl: mockGenerateExportUrl, + }, + }, + ], + }; const wrapper = mountWithIntl( - + ); @@ -119,22 +172,39 @@ describe('Share modal tabs', () => { }); it('would render the export tab when there is at least one export type which is not disabled', async () => { - const testItem = [ - { - shareMenuItem: { name: 'test', disabled: false }, - label: CSV, - generateExport: mockGenerateExport, - generateExportUrl: mockGenerateExportUrl, - }, - { - shareMenuItem: { name: 'test', disabled: true }, - label: PNG, - generateExport: mockGenerateExport, - generateExportUrl: mockGenerateExportUrl, - }, - ]; + const shareContextWithConfiguredExportItem: IShareContext = { + ...mockShareContext, + shareMenuItems: [ + ...mockShareContext.shareMenuItems, + { + id: 'test-csv-export', + shareType: 'integration', + groupId: 'export', + config: { + name: 'test', + disabled: false, + label: CSV, + generateExport: mockGenerateExport, + generateExportUrl: mockGenerateExportUrl, + }, + }, + { + id: 'test-png-export', + shareType: 'integration', + groupId: 'export', + config: { + name: 'test', + disabled: true, + label: PNG, + generateExport: mockGenerateExport, + generateExportUrl: mockGenerateExportUrl, + }, + }, + ], + }; + const wrapper = mountWithIntl( - + ); diff --git a/src/platform/plugins/shared/share/public/components/share_tabs.tsx b/src/platform/plugins/shared/share/public/components/share_tabs.tsx index 7040345084517..0935fd9d80e0f 100644 --- a/src/platform/plugins/shared/share/public/components/share_tabs.tsx +++ b/src/platform/plugins/shared/share/public/components/share_tabs.tsx @@ -29,21 +29,24 @@ export const ShareMenuTabs = () => { const tabs: Array> = []; - // do not show the link tab if the share url is disabled + // Do not show the link tab if the share url is disabled if (!objectTypeMeta?.config.link?.disabled) { tabs.push(linkTab); } - // do not show the export tab if the license is disabled + // Do not show the export tab if there's no export type enabled if ( shareMenuItems.some( - (shareItem) => shareItem.shareType === 'integration' && shareItem.groupId === 'export' + (shareItem) => + shareItem.shareType === 'integration' && + shareItem.groupId === 'export' && + !shareItem.config.disabled ) ) { tabs.push(exportTab); } - // embed is disabled in the serverless offering, hence the need to check that we received it + // Embed is disabled in the serverless offering, hence the need to check that we received it if ( shareMenuItems.some(({ shareType }) => shareType === 'embed') && !objectTypeMeta?.config?.embed?.disabled diff --git a/src/platform/plugins/shared/share/public/mocks.ts b/src/platform/plugins/shared/share/public/mocks.ts index 5e6a7ee6f6e83..9ee2cd787a8fd 100644 --- a/src/platform/plugins/shared/share/public/mocks.ts +++ b/src/platform/plugins/shared/share/public/mocks.ts @@ -16,7 +16,7 @@ import type { BrowserShortUrlClientFactoryCreateParams } from './url_service/sho export type Setup = jest.Mocked; export type Start = jest.Mocked; -const url = new UrlService({ +export const url = new UrlService({ navigate: async () => {}, getUrl: async ({ app, path }, { absolute }) => { return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`; @@ -40,6 +40,7 @@ const url = new UrlService { const setupContract: Setup = { register: jest.fn(), + registerShareIntegration: jest.fn(), url, navigate: jest.fn(), setAnonymousAccessServiceProvider: jest.fn(), diff --git a/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts b/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts index fa9d0cf0205da..5824b74115ce2 100644 --- a/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts +++ b/src/platform/plugins/shared/share/public/services/share_menu_registry.test.ts @@ -8,53 +8,209 @@ */ import { ShareRegistry } from './share_menu_registry'; -import { ShareMenuItemV2, ShareContext } from '../types'; +import { ShareContext, ShareIntegration, ShareRegistryApiStart } from '../types'; +import { url } from '../mocks'; describe('ShareActionsRegistry', () => { - describe('setup', () => { + const startDeps: ShareRegistryApiStart = { + urlService: url, + anonymousAccessServiceProvider: () => ({ + getCapabilities: jest.fn(), + getState: jest.fn(), + }), + }; + + describe('registerShareIntegration', () => { test('throws when registering duplicate id', () => { - const setup = new ShareRegistry().setup(); - setup.register({ + const shareRegistry = new ShareRegistry(); + + shareRegistry.registerShareIntegration({ id: 'csvReports', - getShareMenuItems: () => [], + config: () => ({}), }); + expect(() => - setup.register({ + shareRegistry.registerShareIntegration({ id: 'csvReports', - getShareMenuItems: () => [], + config: () => ({}), }) ).toThrowErrorMatchingInlineSnapshot( - `"Share menu provider with id [csvReports] has already been registered. Use a unique id."` + `"Share action with type [integration] for app [*] has already been registered."` ); }); }); describe('start', () => { - describe('getActions', () => { + describe('resolveShareItemsForShareContext', () => { + test('it returns the default share actions for any requested app scope without performing any prior registrations', () => { + const resolveShareItemsForShareContext = new ShareRegistry().start(startDeps); + + const context = { + objectType: 'someRandomObjectType', + } as ShareContext; + + expect(resolveShareItemsForShareContext({ ...context, isServerless: false })).toEqual([ + expect.objectContaining({ + shareType: 'link', + }), + expect.objectContaining({ + shareType: 'embed', + }), + ]); + }); + + test('it excludes the default embed share actions for any requested app scope in serverless', () => { + const resolveShareItemsForShareContext = new ShareRegistry().start(startDeps); + + const context = { + objectType: 'someRandomObjectType', + } as ShareContext; + + expect(resolveShareItemsForShareContext({ ...context, isServerless: true })).toEqual( + expect.not.arrayContaining([ + expect.objectContaining({ + shareType: 'embed', + }), + ]) + ); + }); + test('returns a flat list of actions returned by all providers', () => { const service = new ShareRegistry(); - const registerFunction = service.setup().register; - const shareAction1 = {} as ShareMenuItemV2; - const shareAction2 = {} as ShareMenuItemV2; - const shareAction3 = {} as ShareMenuItemV2; - const provider1Callback = jest.fn(() => [shareAction1]); - const provider2Callback = jest.fn(() => [shareAction2, shareAction3]); - registerFunction({ - id: 'csvReports', - getShareMenuItems: provider1Callback, - }); - registerFunction({ - id: 'screenCaptureReports', - getShareMenuItems: provider2Callback, - }); - const context = {} as ShareContext; - expect(service.start().getShareMenuItems(context)).toEqual([ - shareAction1, - shareAction2, - shareAction3, + const registerFunction = service.registerShareIntegration.bind(service); + + const shareAction1ConfigFactory = jest.fn(() => ({})); + const shareAction1: ShareIntegration = { + id: 'shareAction1', + shareType: 'integration', + config: shareAction1ConfigFactory, + }; + + const shareAction2ConfigFactory = jest.fn(() => ({})); + const shareAction2: ShareIntegration = { + id: 'shareAction2', + shareType: 'integration', + config: shareAction2ConfigFactory, + }; + + const shareAction3ConfigFactory = jest.fn(() => ({})); + const shareAction3: ShareIntegration = { + id: 'shareAction3', + shareType: 'integration', + config: shareAction3ConfigFactory, + }; + + registerFunction(shareAction1); + registerFunction(shareAction2); + registerFunction(shareAction3); + + const context = { objectType: 'anotherRandomShareObjectType' } as ShareContext; + const isServerless = false; + + expect(() => + service.resolveShareItemsForShareContext({ ...context, isServerless }) + ).toThrow(); + + service.start(startDeps); + + expect(service.resolveShareItemsForShareContext({ ...context, isServerless })).toEqual([ + expect.objectContaining({ + shareType: 'link', + config: expect.any(Object), + }), + expect.objectContaining({ + shareType: 'embed', + config: expect.any(Object), + }), + expect.objectContaining({ + shareType: 'integration', + id: 'shareAction1', + config: expect.any(Object), + }), + expect.objectContaining({ + shareType: 'integration', + id: 'shareAction2', + config: expect.any(Object), + }), + expect.objectContaining({ + shareType: 'integration', + id: 'shareAction3', + config: expect.any(Object), + }), + ]); + + [shareAction1ConfigFactory, shareAction2ConfigFactory, shareAction3ConfigFactory].forEach( + (factory) => { + expect(factory).toHaveBeenCalledTimes(1); + expect(shareAction1ConfigFactory).toHaveBeenCalledWith( + expect.objectContaining({ + ...context, + urlService: expect.any(Object), + anonymousAccessServiceProvider: expect.any(Function), + }) + ); + } + ); + }); + + test('it returns a flat list of actions registered to the requested scope', () => { + const service = new ShareRegistry(); + const registerFunction = service.registerShareIntegration.bind(service); + const context = { objectType: 'randomObjectType' } as ShareContext; + + const isServerless = false; + + const shareAction1ConfigFactory = jest.fn(() => ({})); + const shareAction1: ShareIntegration = { + id: 'shareAction1', + shareType: 'integration', + config: shareAction1ConfigFactory, + }; + + const shareAction2ConfigFactory = jest.fn(() => ({})); + const shareAction2: ShareIntegration = { + id: 'shareAction2', + shareType: 'integration', + config: shareAction2ConfigFactory, + }; + + const shareAction3ConfigFactory = jest.fn(() => ({})); + const shareAction3: ShareIntegration = { + id: 'shareAction3', + shareType: 'integration', + config: shareAction3ConfigFactory, + }; + + registerFunction(context.objectType, shareAction1); + registerFunction('someOtherRandomObjectType', shareAction2); + registerFunction(context.objectType, shareAction3); + + expect(() => + service.resolveShareItemsForShareContext({ ...context, isServerless }) + ).toThrow(); + + service.start(startDeps); + + expect(service.resolveShareItemsForShareContext({ ...context, isServerless })).toEqual([ + expect.objectContaining({ + shareType: 'link', + config: expect.any(Object), + }), + expect.objectContaining({ + shareType: 'embed', + config: expect.any(Object), + }), + expect.objectContaining({ + shareType: 'integration', + id: 'shareAction1', + config: expect.any(Object), + }), + expect.objectContaining({ + shareType: 'integration', + id: 'shareAction3', + config: expect.any(Object), + }), ]); - expect(provider1Callback).toHaveBeenCalledWith(context); - expect(provider2Callback).toHaveBeenCalledWith(context); }); }); }); diff --git a/src/platform/plugins/shared/share/public/services/share_menu_registry.ts b/src/platform/plugins/shared/share/public/services/share_menu_registry.ts index 2af852faac085..2580edf98de43 100644 --- a/src/platform/plugins/shared/share/public/services/share_menu_registry.ts +++ b/src/platform/plugins/shared/share/public/services/share_menu_registry.ts @@ -34,7 +34,7 @@ export class ShareRegistry implements ShareRegistryPublicApi { private readonly shareOptionsStore: Record< string, - Map + Map > = { [this.globalMarker]: new Map(), }; @@ -158,7 +158,9 @@ export class ShareRegistry implements ShareRegistryPublicApi { } as ShareConfigs; }) .filter((shareAction) => { - return shareAction.config || (shareAction.shareType === 'embed' && !isServerless); + return isServerless + ? shareAction.shareType !== 'embed' && shareAction.config + : shareAction.config; }); } } diff --git a/src/platform/plugins/shared/share/public/types.ts b/src/platform/plugins/shared/share/public/types.ts index 2ad5c1baf0f0f..471bdde353dde 100644 --- a/src/platform/plugins/shared/share/public/types.ts +++ b/src/platform/plugins/shared/share/public/types.ts @@ -123,6 +123,9 @@ export interface ExportShare sortOrder?: number; label: string; exportType: string; + /** + * allows disabling the export action based on there factors, for instance licensing + */ disabled?: boolean; helpText?: ReactElement; renderCopyURLButton?: ReactElement; @@ -302,7 +305,7 @@ export interface UrlParamExtension { /** @public */ export interface ShowShareMenuOptions extends Omit { - anchorElement: HTMLElement; + anchorElement?: HTMLElement; allowShortUrl: boolean; onClose?: () => void; publicAPIEnabled?: boolean;