Skip to content

Commit

Permalink
fix issue with generating short url when coping share link
Browse files Browse the repository at this point in the history
  • Loading branch information
eokoneyo committed Nov 23, 2024
1 parent 9c4785b commit 7d1b28b
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 78 deletions.
25 changes: 25 additions & 0 deletions src/plugins/share/public/components/context/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { renderHook } from '@testing-library/react-hooks';
import { useShareTabsContext } from '.';

describe('share menu context', () => {
describe('useShareTabsContext', () => {
it('throws an error if used outside of ShareMenuProvider tree', () => {
const { result } = renderHook(() => useShareTabsContext());

expect(result.error?.message).toEqual(
expect.stringContaining(
'Failed to call `useShareTabsContext` because the context from ShareMenuProvider is missing.'
)
);
});
});
});
23 changes: 20 additions & 3 deletions src/plugins/share/public/components/context/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import { ThemeServiceSetup } from '@kbn/core-theme-browser';
import { I18nStart } from '@kbn/core/public';
import { createContext, useContext } from 'react';
import React, { type PropsWithChildren, createContext, useContext } from 'react';

import { AnonymousAccessServiceContract } from '../../../common';
import type {
Expand All @@ -35,6 +35,23 @@ export interface IShareContext extends ShareContext {
anchorElement?: HTMLElement;
}

export const ShareTabsContext = createContext<IShareContext | null>(null);
const ShareTabsContext = createContext<IShareContext | null>(null);

export const useShareTabsContext = () => useContext(ShareTabsContext);
export const ShareMenuProvider = ({
shareContext,
children,
}: PropsWithChildren<{ shareContext: IShareContext }>) => {
return <ShareTabsContext.Provider value={shareContext}>{children}</ShareTabsContext.Provider>;
};

export const useShareTabsContext = () => {
const context = useContext(ShareTabsContext);

if (!context) {
throw new Error(
'Failed to call `useShareTabsContext` because the context from ShareMenuProvider is missing. Ensure the component or React root is wrapped with ShareMenuProvider'
);
}

return context;
};
16 changes: 9 additions & 7 deletions src/plugins/share/public/components/share_tabs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import React from 'react';
import { ShareMenuTabs } from './share_tabs';
import { ShareTabsContext } from './context';
import { ShareMenuProvider } from './context';
import { mountWithIntl } from '@kbn/test-jest-helpers';
import { KibanaLocation, LocatorGetUrlParams, UrlService } from '../../common/url_service';
import {
Expand Down Expand Up @@ -77,9 +77,9 @@ describe('Share modal tabs', () => {
},
];
const wrapper = mountWithIntl(
<ShareTabsContext.Provider value={{ ...mockShareContext, shareMenuItems: testItem }}>
<ShareMenuProvider shareContext={{ ...mockShareContext, shareMenuItems: testItem }}>
<ShareMenuTabs />
</ShareTabsContext.Provider>
</ShareMenuProvider>
);
expect(wrapper.find('[data-test-subj="export"]').exists()).toBeTruthy();
});
Expand All @@ -92,11 +92,13 @@ describe('Share modal tabs', () => {
generateExportUrl: mockGenerateExportUrl,
},
];

const wrapper = mountWithIntl(
<ShareTabsContext.Provider value={{ ...mockShareContext, shareMenuItems: testItems }}>
<ShareMenuProvider shareContext={{ ...mockShareContext, shareMenuItems: testItems }}>
<ShareMenuTabs />
</ShareTabsContext.Provider>
</ShareMenuProvider>
);

expect(wrapper.find('[data-test-subj="export"]').exists()).toBeFalsy();
});

Expand All @@ -116,9 +118,9 @@ describe('Share modal tabs', () => {
},
];
const wrapper = mountWithIntl(
<ShareTabsContext.Provider value={{ ...mockShareContext, shareMenuItems: testItem }}>
<ShareMenuProvider shareContext={{ ...mockShareContext, shareMenuItems: testItem }}>
<ShareMenuTabs />
</ShareTabsContext.Provider>
</ShareMenuProvider>
);
expect(wrapper.find('[data-test-subj="export"]').exists()).toBeTruthy();
});
Expand Down
16 changes: 5 additions & 11 deletions src/plugins/share/public/components/share_tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,26 @@
*/

import React, { type FC } from 'react';
import { TabbedModal } from '@kbn/shared-ux-tabbed-modal';
import { TabbedModal, type IModalTabDeclaration } from '@kbn/shared-ux-tabbed-modal';

import { ShareTabsContext, useShareTabsContext, type IShareContext } from './context';
import { ShareMenuProvider, useShareTabsContext, type IShareContext } from './context';
import { linkTab, embedTab, exportTab } from './tabs';

export const ShareMenu: FC<{ shareContext: IShareContext }> = ({ shareContext }) => {
return (
<ShareTabsContext.Provider value={shareContext}>
<ShareMenuProvider {...{ shareContext }}>
<ShareMenuTabs />
</ShareTabsContext.Provider>
</ShareMenuProvider>
);
};

// this file is intended to replace share_context_menu
export const ShareMenuTabs = () => {
const shareContext = useShareTabsContext();

if (!shareContext) {
return null;
}

const { allowEmbed, objectTypeMeta, onClose, shareMenuItems, anchorElement } = shareContext;

const tabs = [];

tabs.push(linkTab);
const tabs: Array<IModalTabDeclaration<any>> = [linkTab];

const enabledItems = shareMenuItems.filter(({ shareMenuItem }) => !shareMenuItem?.disabled);

Expand Down
1 change: 1 addition & 0 deletions src/plugins/share/public/components/tabs/link/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const linkTabReducer: ILinkTab['reducer'] = (
const LinkTabContent: ILinkTab['content'] = ({ state, dispatch }) => {
const {
objectType,
objectTypeMeta,
objectId,
isDirty,
shareableUrl,
Expand Down
191 changes: 191 additions & 0 deletions src/plugins/share/public/components/tabs/link/link_content.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { type ComponentProps } from 'react';
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import userEvent from '@testing-library/user-event';
import { render, screen, waitFor } from '@testing-library/react';

import { urlServiceTestSetup } from '../../../../common/url_service/__tests__/setup';
import { MockLocatorDefinition } from '../../../../common/url_service/mocks';
import { BrowserShortUrlClientFactory } from '../../../url_service/short_urls/short_url_client_factory';
import {
BrowserShortUrlClientHttp,
BrowserShortUrlClient,
} from '../../../url_service/short_urls/short_url_client';
import { BrowserUrlService } from '../../../types';
import { LinkContent } from './link_content';

const renderComponent = (props: ComponentProps<typeof LinkContent>) => {
render(
<IntlProvider locale="en">
<LinkContent {...props} />
</IntlProvider>
);
};

describe('LinkContent', () => {
const shareableUrl = 'http://localhost:5601/app/dashboards#/view/123';

const http: BrowserShortUrlClientHttp = {
basePath: {
get: () => '/xyz',
},
fetch: jest.fn(async () => {
return {} as any;
}),
};

let urlService: BrowserUrlService;

// @ts-expect-error there is a type because we override the shortUrls implementation
// eslint-disable-next-line prefer-const
({ service: urlService } = urlServiceTestSetup({
shortUrls: ({ locators }) =>
new BrowserShortUrlClientFactory({
http,
locators,
}),
}));

beforeAll(() => {
Object.defineProperty(document, 'execCommand', {
value: jest.fn(() => true),
});
});

it('uses the delegatedShareUrlHandler to generate the shareable URL when it is provided', async () => {
const user = userEvent.setup();
const objectType = 'dashboard';
const objectId = '123';
const isDirty = false;

const delegatedShareUrlHandler = jest.fn();

renderComponent({
objectType,
objectId,
isDirty,
shareableUrl,
urlService,
allowShortUrl: true,
delegatedShareUrlHandler,
});

await user.click(screen.getByTestId('copyShareUrlButton'));

expect(delegatedShareUrlHandler).toHaveBeenCalled();
});

it('returns the shareable URL when the delegatedShareUrlHandler is not provided and shortURLs are not allowed', async () => {
const user = userEvent.setup();
const objectType = 'dashboard';
const objectId = '123';
const isDirty = false;

renderComponent({
objectType,
objectId,
isDirty,
shareableUrl,
urlService,
allowShortUrl: false,
});

const copyButton = screen.getByTestId('copyShareUrlButton');

await user.click(copyButton);

waitFor(() => {
expect(copyButton.getAttribute('data-share-url')).toBe(shareableUrl);
});
});

it('invokes the createWithLocator method on the shortURL service if a locator is present when the copy button is clicked', async () => {
const user = userEvent.setup();
const objectType = 'dashboard';
const objectId = '123';
const isDirty = false;
const shareableUrlLocatorParams = {
locator: new MockLocatorDefinition('TEST_LOCATOR'),
params: {},
};

const shortURL = 'http://localhost:5601/xyz/r/s/yellow-orange-tomato';

const createWithLocatorSpy = jest.spyOn(BrowserShortUrlClient.prototype, 'createWithLocator');

createWithLocatorSpy.mockResolvedValue({
// @ts-expect-error we only return locator property, as that's all we need for this test
locator: {
getUrl: jest.fn(() => Promise.resolve(shortURL)),
},
});

renderComponent({
objectType,
objectId,
isDirty,
shareableUrl,
urlService,
allowShortUrl: true,
// @ts-ignore this locator is passed mainly to test the code path that invokes createWithLocator
shareableUrlLocatorParams,
});

const copyButton = screen.getByTestId('copyShareUrlButton');

const numberOfClicks = 4;

for (const _click of Array.from({ length: numberOfClicks })) {
await user.click(copyButton);
}

// should only invoke once no matter how many times the button is clicked
expect(createWithLocatorSpy).toHaveBeenCalledTimes(1);
expect(copyButton.getAttribute('data-share-url')).toBe(shortURL);
});

it('invokes the createFromLongUrl method on the shortURL service if a locator is not present when the copy button is clicked', async () => {
const user = userEvent.setup();
const objectType = 'dashboard';
const objectId = '123';
const isDirty = false;

const shortURL = 'http://localhost:5601/xyz/r/s/yellow-orange-tomato';

const createFromLongUrlSpy = jest.spyOn(BrowserShortUrlClient.prototype, 'createFromLongUrl');

// @ts-expect-error we only return url property, as that's all we need for this test
createFromLongUrlSpy.mockResolvedValue({
url: shortURL,
});

renderComponent({
objectType,
objectId,
isDirty,
shareableUrl,
urlService,
allowShortUrl: true,
});

const copyButton = screen.getByTestId('copyShareUrlButton');

const numberOfClicks = 4;

for (const _click of Array.from({ length: numberOfClicks })) {
await user.click(copyButton);
}

// should only invoke once no matter how many times the button is clicked
expect(createFromLongUrlSpy).toHaveBeenCalledTimes(1);
expect(copyButton.getAttribute('data-share-url')).toBe(shortURL);
});
});
Loading

0 comments on commit 7d1b28b

Please sign in to comment.