Merge pull request #3083 from spotify/ryanv/export-mock-context-utils

Ryanv/export mock context utils
This commit is contained in:
Ryan Vazquez
2020-10-26 10:58:25 -04:00
committed by GitHub
14 changed files with 248 additions and 235 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-cost-insights': patch
---
export test utilities for mocking context
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import React, { ReactNode } from 'react';
import React, { PropsWithChildren } from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import CostGrowth from './CostGrowth';
import {
@@ -37,21 +37,12 @@ const MockContext = ({
children,
currency,
engineerCost,
}: {
children: ReactNode;
}: PropsWithChildren<{
currency: Currency;
engineerCost: number;
}) => (
<MockConfigProvider
engineerCost={engineerCost}
currencies={defaultCurrencies}
metrics={[]}
products={[]}
icons={[]}
>
<MockCurrencyProvider currency={currency} setCurrency={jest.fn()}>
{children}
</MockCurrencyProvider>
}>) => (
<MockConfigProvider engineerCost={engineerCost}>
<MockCurrencyProvider currency={currency}>{children}</MockCurrencyProvider>
</MockConfigProvider>
);
@@ -18,7 +18,7 @@ import React from 'react';
import { default as HappyFace } from '@material-ui/icons/SentimentSatisfiedAlt';
import { renderInTestApp } from '@backstage/test-utils';
import CostInsightsNavigation from './CostInsightsNavigation';
import { defaultCurrencies, Metric, Product, Icon } from '../../types';
import { Product, Icon } from '../../types';
import { MockConfigProvider, MockScrollProvider } from '../../utils/tests';
import { getDefaultNavigationItems } from '../../utils/navigation';
@@ -36,23 +36,9 @@ const mockProducts: Product[] = [
},
];
const mockMetrics: Metric[] = [
{
kind: 'some-metric',
name: 'Some Metric',
default: false,
},
];
const renderWrapped = (children: React.ReactNode) =>
renderInTestApp(
<MockConfigProvider
metrics={mockMetrics}
products={mockProducts}
icons={mockIcons}
engineerCost={20_000}
currencies={defaultCurrencies}
>
<MockConfigProvider products={mockProducts} icons={mockIcons}>
<MockScrollProvider>{children}</MockScrollProvider>
</MockConfigProvider>,
);
@@ -17,14 +17,11 @@
import React from 'react';
import CostInsightsTabs from './CostInsightsTabs';
import UserEvent from '@testing-library/user-event';
import { Group, defaultCurrencies } from '../../types';
import { MockFilterProvider, MockConfigProvider } from '../../utils/tests';
import { LoadingContext } from '../../hooks/useLoading';
import { Group } from '../../types';
import { MockFilterProvider, MockLoadingProvider } from '../../utils/tests';
import { renderInTestApp } from '@backstage/test-utils';
import { mockDefaultState } from '../../utils/mockData';
const mockSetPageFilters = jest.fn();
const mockLoadingDispatch = jest.fn();
const mockGroups: Group[] = [
{
@@ -41,28 +38,9 @@ const mockGroups: Group[] = [
describe('<CostInsightsTabs />', () => {
const renderWrapped = (children: React.ReactNode) =>
renderInTestApp(
<MockConfigProvider
products={[]}
metrics={[]}
icons={[]}
engineerCost={200_000}
currencies={defaultCurrencies}
>
<MockFilterProvider
setPageFilters={mockSetPageFilters}
setProductFilters={jest.fn()}
>
<LoadingContext.Provider
value={{
state: mockDefaultState,
dispatch: mockLoadingDispatch,
actions: [],
}}
>
{children}
</LoadingContext.Provider>
</MockFilterProvider>
</MockConfigProvider>,
<MockFilterProvider setPageFilters={mockSetPageFilters}>
<MockLoadingProvider>{children}</MockLoadingProvider>
</MockFilterProvider>,
);
it('Does NOT display the tabs bar if owner belongs to less than two GROUPS', async () => {
@@ -16,11 +16,10 @@
import React from 'react';
import { getByRole, waitFor } from '@testing-library/react';
import { renderInTestApp } from '@backstage/test-utils';
import UserEvent from '@testing-library/user-event';
import PeriodSelect, { getDefaultOptions } from './PeriodSelect';
import { Duration, getDefaultPageFilters, Group } from '../../types';
import { renderInTestApp } from '@backstage/test-utils';
import { MockBillingDateProvider } from '../../utils/tests';
const DefaultPageFilters = getDefaultPageFilters([{ id: 'tools' }] as Group[]);
@@ -15,50 +15,27 @@
*/
import React from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import ProductInsightsCard from './ProductInsightsCard';
import { CostInsightsApi } from '../../api';
import {
createMockEntity,
createMockProductCost,
mockDefaultLoadingState,
MockComputeEngine,
mockDefaultState,
MockProductFilters,
} from '../../utils/mockData';
import {
ApiProvider,
ApiRegistry,
IdentityApi,
identityApiRef,
} from '@backstage/core';
import { CostInsightsApi, costInsightsApiRef } from '../../api';
import { renderInTestApp } from '@backstage/test-utils';
import { GroupsContext } from '../../hooks/useGroups';
import { LoadingContext } from '../../hooks/useLoading';
import {
defaultCurrencies,
Duration,
findAlways,
Product,
ProductCost,
} from '../../types';
import {
MockCostInsightsApiProvider,
MockBillingDateProvider,
MockConfigProvider,
MockCurrencyProvider,
MockFilterProvider,
MockGroupsProvider,
MockScrollProvider,
MockLoadingProvider,
} from '../../utils/tests';
const mockLoadingDispatch = jest.fn();
const mockSetPageFilters = jest.fn();
const mockSetProductFilters = jest.fn();
const mockSetCurrency = jest.fn();
const engineers = findAlways(defaultCurrencies, c => c.kind === null);
const identityApi: Partial<IdentityApi> = {
getProfile: () => ({
email: 'test-email@example.com',
displayName: 'User 1',
}),
};
import { Duration, Product, ProductCost, ProductPeriod } from '../../types';
const costInsightsApi = (
productCost: ProductCost,
@@ -67,13 +44,6 @@ const costInsightsApi = (
Promise.resolve(productCost) as Promise<ProductCost>,
});
const getApis = (productCost: ProductCost) => {
return ApiRegistry.from([
[identityApiRef, identityApi],
[costInsightsApiRef, costInsightsApi(productCost)],
]);
};
const mockProductCost = createMockProductCost(() => ({
entities: [],
aggregation: [3000, 4000],
@@ -89,42 +59,28 @@ const renderProductInsightsCardInTestApp = async (
duration: Duration,
) =>
await renderInTestApp(
<ApiProvider apis={getApis(productCost)}>
<MockConfigProvider
metrics={[]}
products={[]}
icons={[]}
engineerCost={0}
currencies={[]}
>
<LoadingContext.Provider
value={{
state: mockDefaultState,
actions: [],
dispatch: mockLoadingDispatch,
}}
>
<GroupsContext.Provider value={{ groups: [{ id: 'test-group' }] }}>
<MockBillingDateProvider lastCompleteBillingDate="2020-10-01">
<MockCostInsightsApiProvider costInsightsApi={costInsightsApi(productCost)}>
<MockConfigProvider>
<MockLoadingProvider state={mockDefaultLoadingState}>
<MockGroupsProvider>
<MockBillingDateProvider>
<MockFilterProvider
setPageFilters={mockSetPageFilters}
setProductFilters={mockSetProductFilters}
duration={duration}
productFilters={MockProductFilters.map((p: ProductPeriod) => ({
...p,
duration: duration,
}))}
>
<MockScrollProvider>
<MockCurrencyProvider
currency={engineers}
setCurrency={mockSetCurrency}
>
<MockCurrencyProvider>
<ProductInsightsCard product={product} />
</MockCurrencyProvider>
</MockScrollProvider>
</MockFilterProvider>
</MockBillingDateProvider>
</GroupsContext.Provider>
</LoadingContext.Provider>
</MockGroupsProvider>
</MockLoadingProvider>
</MockConfigProvider>
</ApiProvider>,
</MockCostInsightsApiProvider>,
);
describe('<ProductInsightsCard/>', () => {
@@ -46,15 +46,9 @@ describe('<ProjectGrowthAlertCard />', () => {
);
const title = new RegExp(`Project growth for ${MockProject}`);
const rendered = await renderInTestApp(
<MockConfigProvider
metrics={[]}
products={[]}
icons={[]}
engineerCost={200_000}
currencies={[]}
>
<MockConfigProvider engineerCost={200_000}>
<MockBillingDateProvider lastCompleteBillingDate="2020-10-01">
<MockCurrencyProvider currency={engineers} setCurrency={jest.fn()}>
<MockCurrencyProvider currency={engineers}>
<ProjectGrowthAlertCard alert={MockProjectGrowthAlert} />,
</MockCurrencyProvider>
</MockBillingDateProvider>
@@ -71,13 +65,7 @@ describe('<ProjectGrowthAlertCard />', () => {
const subheader = new RegExp('1 product');
const title = new RegExp(`Project growth for ${MockProject}`);
const rendered = await renderInTestApp(
<MockConfigProvider
metrics={[]}
products={[]}
icons={[]}
engineerCost={200_000}
currencies={[]}
>
<MockConfigProvider engineerCost={200_000}>
<MockBillingDateProvider lastCompleteBillingDate="2020-10-01">
<MockCurrencyProvider currency={engineers} setCurrency={jest.fn()}>
<ProjectGrowthAlertCard
@@ -27,20 +27,15 @@ const mockProjects = [
{ id: 'project3' },
];
const mockSetPageFilters = jest.fn();
describe('<ProjectSelect />', () => {
let Component: React.ReactNode;
beforeEach(() => {
Component = () => (
<MockFilterProvider
setPageFilters={mockSetPageFilters}
setProductFilters={jest.fn()}
>
<MockFilterProvider>
<ProjectSelect
project="all"
projects={mockProjects}
onSelect={mockSetPageFilters}
onSelect={jest.fn()}
/>
</MockFilterProvider>
);
@@ -14,31 +14,17 @@
* limitations under the License.
*/
import React, { ReactNode } from 'react';
import React, { PropsWithChildren } from 'react';
import { renderInTestApp } from '@backstage/test-utils';
import ResourceGrowthBarChartLegend from './ResourceGrowthBarChartLegend';
import { Currency, defaultCurrencies, Duration, findAlways } from '../../types';
import { defaultCurrencies, Duration, findAlways } from '../../types';
import { MockConfigProvider, MockCurrencyProvider } from '../../utils/tests';
const engineers = findAlways(defaultCurrencies, c => c.kind === null);
const MockContext = ({
children,
currency,
}: {
children: ReactNode;
currency: Currency;
}) => (
<MockConfigProvider
engineerCost={200_000}
currencies={[]}
metrics={[]}
products={[]}
icons={[]}
>
<MockCurrencyProvider currency={currency} setCurrency={jest.fn()}>
{children}
</MockCurrencyProvider>
const MockContext = ({ children }: PropsWithChildren<{}>) => (
<MockConfigProvider engineerCost={200_000}>
<MockCurrencyProvider currency={engineers}>{children}</MockCurrencyProvider>
</MockConfigProvider>
);
@@ -52,7 +38,7 @@ describe('<ResourceGrowthBarChartLegend />', () => {
({ ratio, amount, costText, engineerTest }) => {
it(`Should display the correct cost and engineer text for ${ratio} percent change`, async () => {
const rendered = await renderInTestApp(
<MockContext currency={engineers}>
<MockContext>
<ResourceGrowthBarChartLegend
duration={Duration.P3M}
change={{ ratio, amount }}
@@ -15,5 +15,6 @@
*/
export { default as BarChart } from './BarChart';
export { default as CostGrowth } from './CostGrowth';
export { default as LegendItem } from './LegendItem';
export * from './Tooltip';
@@ -37,7 +37,7 @@ const mapLoadingToProps: MapLoadingToProps<GroupsProviderLoadingProps> = ({
dispatch({ [DefaultLoadingAction.UserGroups]: isLoading }),
});
type GroupsContextProps = {
export type GroupsContextProps = {
groups: Group[];
};
+1
View File
@@ -19,3 +19,4 @@ export * from './api';
export * from './components';
export { useCurrency } from './hooks';
export * from './types';
export * from './utils/tests';
+4 -2
View File
@@ -19,7 +19,7 @@ import {
Duration,
Entity,
findAlways,
getDefaultState,
getDefaultState as getDefaultLoadingState,
Product,
ProductCost,
ProductFilters,
@@ -154,7 +154,9 @@ export const MockLoadingActions = ([
DefaultLoadingAction.CostInsightsPage,
] as string[]).concat(MockProducts.map(product => product.kind));
export const mockDefaultState = getDefaultState(MockLoadingActions);
export const mockDefaultLoadingState = getDefaultLoadingState(
MockLoadingActions,
);
export const MockComputeEngine = findAlways(
MockProducts,
+191 -66
View File
@@ -13,96 +13,221 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { Dispatch, PropsWithChildren, SetStateAction } from 'react';
import React, { PropsWithChildren } from 'react';
import { costInsightsApiRef, CostInsightsApi } from '../api';
import {
Duration,
getDefaultPageFilters,
Group,
Maybe,
PageFilters,
ProductFilters,
ProductPeriod,
} from '../types';
import { FilterContext } from '../hooks/useFilters';
ApiProvider,
ApiRegistry,
IdentityApi,
identityApiRef,
} from '@backstage/core';
import { LoadingContext, LoadingContextProps } from '../hooks/useLoading';
import { GroupsContext, GroupsContextProps } from '../hooks/useGroups';
import { FilterContext, FilterContextProps } from '../hooks/useFilters';
import { ConfigContext, ConfigContextProps } from '../hooks/useConfig';
import { CurrencyContext, CurrencyContextProps } from '../hooks/useCurrency';
import { ScrollContext } from '../hooks/useScroll';
import {
BillingDateContext,
BillingDateContextProps,
} from '../hooks/useLastCompleteBillingDate';
import { MockProductFilters } from './mockData';
import { ScrollContext, ScrollContextProps } from '../hooks/useScroll';
import { Duration, Group } from '../types';
/*
Mock Providers and types are exposed publicly to allow users to test custom implementations
such as alerts, which may require context.
Utility functions such as getDefaultPageFilters, etc. are intentionally
omitted as we do not want to expose explictly or implicitly internal implementations
that may be subject to change.
Each Mock Provider provides minimal defaults which can be overridden, allowing users to define
context props only when necessary.
*/
type PartialPropsWithChildren<T> = PropsWithChildren<Partial<T>>;
export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }];
type MockFilterProviderProps = {
setPageFilters: Dispatch<SetStateAction<Maybe<PageFilters>>>;
setProductFilters: Dispatch<SetStateAction<Maybe<ProductFilters>>>;
duration?: Duration;
};
export type MockFilterProviderProps = PartialPropsWithChildren<
FilterContextProps
>;
export const MockFilterProvider = ({
setPageFilters,
setProductFilters,
children,
duration = Duration.P1M,
}: PropsWithChildren<MockFilterProviderProps>) => {
const pageFilters = getDefaultPageFilters(MockGroups);
...context
}: MockFilterProviderProps) => {
const defaultContext: FilterContextProps = {
pageFilters: {
group: 'tech',
project: null,
duration: Duration.P90D,
metric: null,
},
productFilters: [],
setPageFilters: jest.fn(),
setProductFilters: jest.fn(),
};
return (
<FilterContext.Provider
value={{
pageFilters: pageFilters,
productFilters: MockProductFilters.map((period: ProductPeriod) => ({
...period,
duration: duration,
})),
setPageFilters: setPageFilters,
setProductFilters: setProductFilters,
}}
>
<FilterContext.Provider value={{ ...defaultContext, ...context }}>
{children}
</FilterContext.Provider>
);
};
export const MockConfigProvider = ({
metrics,
products,
icons,
engineerCost,
currencies,
export type MockLoadingProviderProps = PartialPropsWithChildren<
LoadingContextProps
>;
export const MockLoadingProvider = ({
children,
}: PropsWithChildren<ConfigContextProps>) => (
<ConfigContext.Provider
value={{ metrics, products, icons, engineerCost, currencies }}
>
{children}
</ConfigContext.Provider>
);
...context
}: MockLoadingProviderProps) => {
const defaultContext: LoadingContextProps = {
state: {},
actions: [],
dispatch: jest.fn(),
};
return (
<LoadingContext.Provider value={{ ...defaultContext, ...context }}>
{children}
</LoadingContext.Provider>
);
};
export type MockConfigProviderProps = PartialPropsWithChildren<
ConfigContextProps
>;
export const MockConfigProvider = ({
children,
...context
}: MockConfigProviderProps) => {
const defaultContext: ConfigContextProps = {
metrics: [],
products: [],
icons: [],
engineerCost: 0,
currencies: [],
};
return (
<ConfigContext.Provider value={{ ...defaultContext, ...context }}>
{children}
</ConfigContext.Provider>
);
};
export type MockCurrencyProviderProps = PartialPropsWithChildren<
CurrencyContextProps
>;
export const MockCurrencyProvider = ({
currency,
setCurrency,
children,
}: PropsWithChildren<CurrencyContextProps>) => (
<CurrencyContext.Provider value={{ currency, setCurrency }}>
{children}
</CurrencyContext.Provider>
);
...context
}: MockCurrencyProviderProps) => {
const defaultContext: CurrencyContextProps = {
currency: {
kind: null,
label: 'Engineers 🛠',
unit: 'engineer',
},
setCurrency: jest.fn(),
};
return (
<CurrencyContext.Provider value={{ ...defaultContext, ...context }}>
{children}
</CurrencyContext.Provider>
);
};
export type MockBillingDateProviderProps = PartialPropsWithChildren<
BillingDateContextProps
>;
export const MockBillingDateProvider = ({
lastCompleteBillingDate,
children,
}: PropsWithChildren<BillingDateContextProps>) => (
<BillingDateContext.Provider
value={{ lastCompleteBillingDate: lastCompleteBillingDate }}
>
{children}
</BillingDateContext.Provider>
);
...context
}: MockBillingDateProviderProps) => {
const defaultContext: BillingDateContextProps = {
lastCompleteBillingDate: '2020-10-01',
};
return (
<BillingDateContext.Provider value={{ ...defaultContext, ...context }}>
{children}
</BillingDateContext.Provider>
);
};
export const MockScrollProvider = ({ children }: PropsWithChildren<{}>) => (
<ScrollContext.Provider value={{ scrollTo: null, setScrollTo: jest.fn() }}>
{children}
</ScrollContext.Provider>
);
export type MockScrollProviderProps = PropsWithChildren<{}>;
export const MockScrollProvider = ({ children }: MockScrollProviderProps) => {
const defaultContext: ScrollContextProps = {
scrollTo: null,
setScrollTo: jest.fn(),
};
return (
<ScrollContext.Provider value={defaultContext}>
{children}
</ScrollContext.Provider>
);
};
export type MockGroupsProviderProps = PartialPropsWithChildren<
GroupsContextProps
>;
export const MockGroupsProvider = ({
children,
...context
}: MockGroupsProviderProps) => {
const defaultContext: GroupsContextProps = {
groups: [],
};
return (
<GroupsContext.Provider value={{ ...defaultContext, ...context }}>
{children}
</GroupsContext.Provider>
);
};
export type MockCostInsightsApiProviderProps = PartialPropsWithChildren<{
identityApi: Partial<IdentityApi>;
costInsightsApi: Partial<CostInsightsApi>;
}>;
export const MockCostInsightsApiProvider = ({
children,
...context
}: MockCostInsightsApiProviderProps) => {
const defaultIdentityApi: IdentityApi = {
getProfile: jest.fn(),
getIdToken: jest.fn(),
getUserId: jest.fn(),
signOut: jest.fn(),
};
const defaultCostInsightsApi: CostInsightsApi = {
getAlerts: jest.fn(),
getDailyMetricData: jest.fn(),
getGroupDailyCost: jest.fn(),
getGroupProjects: jest.fn(),
getLastCompleteBillingDate: jest.fn(),
getProductInsights: jest.fn(),
getProjectDailyCost: jest.fn(),
getUserGroups: jest.fn(),
};
// TODO: defaultConfigApiRef: ConfigApiRef
const defaultContext = ApiRegistry.from([
[identityApiRef, { ...defaultIdentityApi, ...context.identityApi }],
[
costInsightsApiRef,
{ ...defaultCostInsightsApi, ...context.costInsightsApi },
],
// [configApiRef, { ...defaultConfigApiRef, ...context.configApiRef }]
]);
return <ApiProvider apis={defaultContext}>{children}</ApiProvider>;
};