Skip to content

Commit

Permalink
added tests for new components
Browse files Browse the repository at this point in the history
Signed-off-by: Giuseppe Macri <[email protected]>
  • Loading branch information
Giuseppe Macri committed Nov 15, 2023
1 parent 433be5b commit ca475f8
Show file tree
Hide file tree
Showing 34 changed files with 832 additions and 622 deletions.
6 changes: 3 additions & 3 deletions contributing/DEVELOPERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ cd kepler.gl
# Add the main kepler.gl repository as an upstream remote to your repository:
git remote add upstream "[email protected]:keplergl/kepler.gl.git"

# Install JavaScript dependencies:
yarn bootstrap

# Install Puppeteer
yarn global add puppeteer

# Install JavaScript dependencies:
yarn bootstrap

# Setup mapbox access token locally
export MapboxAccessToken=<insert_your_token>

Expand Down
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ const config = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
verbose: true,
testMatch: ['<rootDir>/src/**/*.spec.js', '<rootDir>/test/**/*.spec.js'],
testPathIgnorePatterns: [
// ignore all dist computed directories
"<rootDir>/.*(/|\\\\)dist(/|\\\\).*"
],
testMatch: ['<rootDir>/src/**/*.spec.(ts|tsx)', '<rootDir>/src/**/*.spec.js', '<rootDir>/test/**/*.spec.js'],
// Per https://jestjs.io/docs/configuration#transformignorepatterns-arraystring, transformIgnorePatterns ignores
// node_modules and pnp folders by default so that they are not transpiled
// Some libraries (even if transitive) are transitioning to ESM and need additional transpilation. Relevant issues:
Expand Down
25 changes: 21 additions & 4 deletions src/components/src/common/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,22 +143,39 @@ type ModalFooterProps = {
confirmButton?: ModalButtonProps;
};

/**
* this method removes the `disabled` property from button props when disabled is set to false
* to avoid issue with the disabled tag
*
* @param props
*/
const processDisabledProperty = (props: ModalButtonProps): ModalButtonProps => {
if (!props.disabled) {
const {disabled, ...newProps} = props;
return newProps;
}
return props;
};

export const ModalFooter: React.FC<ModalFooterProps> = ({
cancel,
confirm,
cancelButton,
confirmButton
}) => {
const cancelButtonProps = {...defaultCancelButton, ...cancelButton};
const confirmButtonProps = {...defaultConfirmButton, ...confirmButton};
const cancelButtonProps = processDisabledProperty({
...defaultCancelButton,
...cancelButton
});
const confirmButtonProps = processDisabledProperty({...defaultConfirmButton, ...confirmButton});
return (
<StyledModalFooter className="modal--footer">
<FooterActionWrapper>
<Button className="modal--footer--cancel-button" {...cancelButtonProps} onClick={cancel}>
<FormattedMessage id={cancelButtonProps.children} />
<FormattedMessage id={cancelButtonProps.children ?? ''} />
</Button>
<Button className="modal--footer--confirm-button" {...confirmButtonProps} onClick={confirm}>
<FormattedMessage id={confirmButtonProps.children} />
<FormattedMessage id={confirmButtonProps.children ?? ''} />
</Button>
</FooterActionWrapper>
</StyledModalFooter>
Expand Down
3 changes: 2 additions & 1 deletion src/components/src/common/styled-components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ export interface ButtonProps {
inactive?: boolean;
}

export const Button = styled.div.attrs(props => ({
// this needs to be an actual button to be able to set disabled attribute correctly
export const Button = styled.button.attrs(props => ({
className: classnames('button', props.className)
}))<ButtonProps>`
align-items: center;
Expand Down
4 changes: 4 additions & 0 deletions src/components/src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export const FeatureFlagsContextProvider = (
</FeatureFlagsContext.Provider>
);

/**
* This provides keeps track of the ist cloud providers
* and the current selected one
*/
export const CloudProviderContext = createContext<CloudProviderContextType>({
provider: null,
setProvider: () => {},
Expand Down
3 changes: 3 additions & 0 deletions src/components/src/hooks/use-cloud-list-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,7 @@ export const CloudListProvider: React.FC<CloudListProviderProps> = ({children, p
return <CloudProviderContext.Provider value={value}>{children}</CloudProviderContext.Provider>;
};

/**
* this hook provides access the CloudList provider context
*/
export const useCloudListProvider = () => useContext(CloudProviderContext);
18 changes: 15 additions & 3 deletions src/components/src/kepler-gl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ export function getVisibleDatasets(datasets) {
return filterObjectByPredicate(datasets, (key, value) => key !== GEOCODER_DATASET_NAME);
}

export const sidePanelSelector = (props: KeplerGLProps, filteredDatasets) => ({
export const sidePanelSelector = (props: KeplerGLProps, availableProviders, filteredDatasets) => ({
appName: props.appName ? props.appName : DEFAULT_KEPLER_GL_PROPS.appName,
version: props.version ? props.version : DEFAULT_KEPLER_GL_PROPS.version,
appWebsite: props.appWebsite,
Expand All @@ -209,6 +209,7 @@ export const sidePanelSelector = (props: KeplerGLProps, filteredDatasets) => ({
overlayBlending: props.visState.overlayBlending,

width: props.sidePanelWidth ? props.sidePanelWidth : DEFAULT_KEPLER_GL_PROPS.width,
availableProviders,
mapSaved: props.providerState.mapSaved
});

Expand Down Expand Up @@ -420,6 +421,17 @@ function KeplerGlFactory(
datasetsSelector = props => props.visState.datasets;
filteredDatasetsSelector = createSelector(this.datasetsSelector, getVisibleDatasets);

availableProviders = createSelector(
(props: KeplerGLProps) => props.cloudProviders,
providers =>
Array.isArray(providers) && providers.length
? {
hasStorage: providers.some(p => p.hasPrivateStorage()),
hasShare: providers.some(p => p.hasSharingUrl())
}
: {}
);

localeMessagesSelector = createSelector(
(props: KeplerGLProps) => props.localeMessages,
customMessages => (customMessages ? mergeMessages(messages, customMessages) : messages)
Expand Down Expand Up @@ -481,10 +493,10 @@ function KeplerGlFactory(
const theme = this.availableThemeSelector(this.props);
const localeMessages = this.localeMessagesSelector(this.props);
const isExportingImage = uiState.exportImage.exporting;
const availableProviders = this.availableProviders(this.props);

const filteredDatasets = this.filteredDatasetsSelector(this.props);
const sideFields = sidePanelSelector(this.props, filteredDatasets);

const sideFields = sidePanelSelector(this.props, availableProviders, filteredDatasets);
const plotContainerFields = plotContainerSelector(this.props);
const bottomWidgetFields = bottomWidgetSelector(this.props, theme);
const modalContainerFields = modalContainerSelector(this.props, this.root.current);
Expand Down
4 changes: 0 additions & 4 deletions src/components/src/modal-container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,6 @@ export default function ModalContainerFactory(
};

_onSaveMap = (provider, overwrite = false) => {
// const {currentProvider} = this.props.providerState;
// @ts-ignore
// const provider = this.props.cloudProviders.find(p => p.name === currentProvider);
this._exportFileToCloud({
provider,
isPublic: false,
Expand Down Expand Up @@ -499,7 +496,6 @@ export default function ModalContainerFactory(
template = (
<ShareMapModal
{...providerState}
isReady={!uiState.exportImage.processing}
onExport={this._onShareMapUrl}
cleanupExportImage={uiStateActions.cleanupExportImage}
onUpdateImageSetting={uiStateActions.setExportImageSetting}
Expand Down
13 changes: 10 additions & 3 deletions src/components/src/modals/cloud-components/cloud-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {Button} from '../../common/styled-components';
import {ArrowLeft} from '../../common/icons';
import {FormattedMessage} from '@kepler.gl/localization';
import styled from 'styled-components';
import {dataTestIds} from '@kepler.gl/constants';
import {Provider} from '@kepler.gl/cloud-providers';

const StyledStorageHeader = styled.div`
display: flex;
Expand Down Expand Up @@ -57,18 +59,23 @@ const Title = styled.span`
}
`;

export const CloudHeader = ({provider, onBack}) => {
type CloudHeaderProps = {
provider: Provider;
onBack: () => void;
};

export const CloudHeader: React.FC<CloudHeaderProps> = ({provider, onBack}) => {
const managementUrl = useMemo(() => provider.getManagementUrl(), [provider]);
return (
<div>
<div data-testid={dataTestIds.cloudHeader}>
<StyledStorageHeader>
<StyledBackBtn>
<Button link onClick={onBack}>
<ArrowLeft height="14px" />
<FormattedMessage id={'modal.loadStorageMap.back'} />
</Button>
</StyledBackBtn>
{provider.getManagementUrl && (
{managementUrl && (
<a
key={1}
href={managementUrl}
Expand Down
59 changes: 59 additions & 0 deletions src/components/src/modals/cloud-components/cloud-item.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// @ts-nocheck

//colocating test next the file
import React from 'react';
import {fireEvent} from '@testing-library/react';
import {renderWithTheme} from '../../../../../test/helpers/component-jest-utils';

import {CloudItem} from './cloud-item';
import moment from 'moment';

describe('CloudItem', () => {
const mockVis = {
title: 'Test Title',
description: 'Test Description',
lastModification: new Date().toISOString(),
thumbnail: 'test-thumbnail.jpg',
privateMap: true
};

it('renders without crashing', () => {
const {getByText} = renderWithTheme(<CloudItem vis={mockVis} onClick={() => {}} />);
expect(getByText('Test Title')).toBeInTheDocument();
});

it('renders PrivacyBadge for private maps', () => {
const {getByText} = renderWithTheme(<CloudItem vis={{...mockVis, privateMap: true}} onClick={() => {}} />);
expect(getByText('Private')).toBeInTheDocument();
});

it('does not render PrivacyBadge for public maps', () => {
const {queryByText} = renderWithTheme(<CloudItem vis={{...mockVis, privateMap: false}} onClick={() => {}} />);
expect(queryByText('Private')).toBeNull();
});

it('displays correct thumbnail image', () => {
const {getByRole} = renderWithTheme(<CloudItem vis={mockVis} onClick={() => {}} />);
expect(getByRole('thumbnail-wrapper').style.backgroundImage).toContain('test-thumbnail.jpg');
});

it('displays MapIcon when no thumbnail is provided', () => {
const {getByRole} = renderWithTheme(<CloudItem vis={{...mockVis, thumbnail: null}} onClick={() => {}} />);
expect(getByRole('map-icon')).toBeInTheDocument();
});

it('displays title, description, and last modification date', () => {
const {getByText} = renderWithTheme(<CloudItem vis={mockVis} onClick={() => {}} />);
expect(getByText('Test Title')).toBeInTheDocument();
expect(getByText('Test Description')).toBeInTheDocument();
expect(getByText(`Last modified ${moment.utc(mockVis.lastModification).fromNow()}`)).toBeInTheDocument();
});

it('calls onClick when component is clicked', () => {
const onClickMock = jest.fn();
const {getByText} = renderWithTheme(<CloudItem vis={mockVis} onClick={onClickMock} />);
fireEvent.click(getByText('Test Title'));
expect(onClickMock).toHaveBeenCalled();
});
});

7 changes: 4 additions & 3 deletions src/components/src/modals/cloud-components/cloud-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,19 +124,20 @@ const StyledVisualizationItem = styled.div`
`;

export const CloudItem = ({vis, onClick}) => {
const thumbnailStyle = {backgroundImage: `url(${vis.thumbnail})`};
return (
<StyledVisualizationItem onClick={onClick}>
{vis.thumbnail ? (
<div className="vis_item-thumb" style={{backgroundImage: `url(${vis.thumbnail})`}}>
<div role="thumbnail-wrapper" className="vis_item-thumb" style={thumbnailStyle}>
{vis.hasOwnProperty('privateMap') ? <PrivacyBadge privateMap={vis.privateMap} /> : null}
</div>
) : (
<MapIcon className="vis_item-icon">
<MapIcon role="map-icon" className="vis_item-icon">
{vis.hasOwnProperty('privateMap') ? <PrivacyBadge privateMap={vis.privateMap} /> : null}
</MapIcon>
)}
<span className="vis_item-title">{vis.title}</span>
{vis.description && vis.description.length && (
{vis.description?.length && (
<span className="vis_item-description">{vis.description}</span>
)}
<span className="vis_item-modification-date">
Expand Down
46 changes: 46 additions & 0 deletions src/components/src/modals/cloud-components/cloud-maps.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// @ts-nocheck
import React from 'react';
import {fireEvent} from '@testing-library/react';
import {renderWithTheme} from '../../../../../test/helpers/component-jest-utils';
import {CloudMaps} from './cloud-maps';

describe('CloudMaps Component', () => {
it('renderWithThemes without crashing', () => {
const {getByText} = renderWithTheme(<CloudMaps isLoading={false} maps={[]} error={null} />);
expect(getByText(/noSavedMaps/i)).toBeInTheDocument();
});

it('displays error message when there is an error', () => {
const errorMessage = 'Test Error';
const {getByText} = renderWithTheme(<CloudMaps isLoading={false} maps={[]} error={{ message: errorMessage }} />);
expect(getByText(`Error while fetching maps: ${errorMessage}`)).toBeInTheDocument();
});

it('displays loading spinner when isLoading is true', () => {
const {getByText} = renderWithTheme(<CloudMaps isLoading={true} maps={[]} error={null} />);
expect(getByText('modal.loadingDialog.loading')).toBeInTheDocument(); // Ensure your spinner has 'data-testid="loading-spinner"'
});

it('renderWithThemes correct number of CloudItems based on maps prop', () => {
const mockMaps = [{ id: 1, title: 'map' }, { id: 2, title: 'map' }, { id: 3, title: 'map' }];
const {getAllByText} = renderWithTheme(<CloudMaps isLoading={false} maps={mockMaps} error={null} />);
expect(getAllByText('map')).toHaveLength(mockMaps.length); // Ensure your CloudItem has 'data-testid="cloud-item"'
});

it('displays message when there are no maps', () => {
const {getByText} = renderWithTheme(<CloudMaps isLoading={false} maps={[]} error={null} />);
expect(getByText(/noSavedMaps/i)).toBeInTheDocument();
});

it('calls onSelectMap when a CloudItem is clicked', () => {
const mockMaps = [{ id: 1, title: 'map' }, { id: 2, title: 'map' }, { id: 3, title: 'map' }];
const onSelectMap = jest.fn();
const provider = 'testProvider';
const {getAllByText} = renderWithTheme(<CloudMaps provider={provider} onSelectMap={onSelectMap} isLoading={false} maps={mockMaps} error={null} />);

const firstItem = getAllByText('map')[0];
fireEvent.click(firstItem);
expect(onSelectMap).toHaveBeenCalledWith(provider, mockMaps[0]);
});
});

2 changes: 1 addition & 1 deletion src/components/src/modals/cloud-components/cloud-maps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const StyledSpinner = styled.div`

export const CloudMaps = ({provider, onSelectMap, isLoading, maps, error}) => {
if (error) {
return <div>Error while fetching maps</div>;
return <div>Error while fetching maps: {error.message}</div>;
}

if (isLoading) {
Expand Down
24 changes: 14 additions & 10 deletions src/components/src/modals/cloud-components/provider-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import CloudTile from '../cloud-tile';
import React from 'react';
import styled from 'styled-components';
import {Provider} from '@kepler.gl/cloud-providers';
import {dataTestIds} from '@kepler.gl/constants';

const StyledProviderSection = styled.div.attrs({
className: 'provider-selection'
Expand All @@ -34,13 +35,16 @@ type ProviderSelectProps = {
cloudProviders: Provider[];
};

export const ProviderSelect: React.FC<ProviderSelectProps> = ({cloudProviders = []}) =>
cloudProviders.length ? (
<StyledProviderSection>
{cloudProviders.map(provider => (
<CloudTile key={provider.name} provider={provider} />
))}
</StyledProviderSection>
) : (
<p>No storage provider available</p>
);
export const ProviderSelect: React.FC<ProviderSelectProps> = ({cloudProviders = []}) => (
<div data-testid={dataTestIds.providerSelect}>
{cloudProviders.length ? (
<StyledProviderSection>
{cloudProviders.map(provider => (
<CloudTile key={provider.name} provider={provider} />
))}
</StyledProviderSection>
) : (
<p>No storage provider available</p>
)}
</div>
);
Loading

0 comments on commit ca475f8

Please sign in to comment.