From 1fa92752a2bb8b46256becd01f7135df66419619 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Sat, 24 Oct 2020 19:13:35 -0400 Subject: [PATCH 1/5] export mock context utils --- .../components/CostGrowth/CostGrowth.test.tsx | 19 +- .../CostInsightsNavigation.test.tsx | 18 +- .../CostInsightsTabs.test.tsx | 32 +-- .../PeriodSelect/PeriodSelect.test.tsx | 3 +- .../ProductInsightsCard.test.tsx | 86 ++---- .../ProjectGrowthAlertCard.test.tsx | 18 +- .../ProjectSelect/ProjectSelect.test.tsx | 9 +- .../ResourceGrowthBarChartLegend.test.tsx | 26 +- plugins/cost-insights/src/hooks/useGroups.tsx | 2 +- plugins/cost-insights/src/index.ts | 1 + plugins/cost-insights/src/utils/mockData.ts | 6 +- plugins/cost-insights/src/utils/tests.tsx | 257 +++++++++++++----- 12 files changed, 242 insertions(+), 235 deletions(-) diff --git a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx index 6302d9929f..026b16c74a 100644 --- a/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx +++ b/plugins/cost-insights/src/components/CostGrowth/CostGrowth.test.tsx @@ -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; -}) => ( - - - {children} - +}>) => ( + + {children} ); diff --git a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx index 7ca4958c8e..ac8561aa91 100644 --- a/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsNavigation/CostInsightsNavigation.test.tsx @@ -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( - + {children} , ); diff --git a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx index 0859ad18ce..b75e0ab0d9 100644 --- a/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx +++ b/plugins/cost-insights/src/components/CostInsightsTabs/CostInsightsTabs.test.tsx @@ -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('', () => { const renderWrapped = (children: React.ReactNode) => renderInTestApp( - - - - {children} - - - , + + {children} + , ); it('Does NOT display the tabs bar if owner belongs to less than two GROUPS', async () => { diff --git a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx index b5381cdd84..3c55285908 100644 --- a/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx +++ b/plugins/cost-insights/src/components/PeriodSelect/PeriodSelect.test.tsx @@ -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[]); diff --git a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx index 1ca1b48c6c..bae4eef591 100644 --- a/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx +++ b/plugins/cost-insights/src/components/ProductInsightsCard/ProductInsightsCard.test.tsx @@ -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 = { - 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, }); -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( - - - - - + + + + + ({ + ...p, + duration: duration, + }))} > - + - - + + - , + , ); describe('', () => { diff --git a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx index dd70433003..97775258c9 100644 --- a/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx +++ b/plugins/cost-insights/src/components/ProjectGrowthAlertCard/ProjectGrowthAlertCard.test.tsx @@ -46,15 +46,9 @@ describe('', () => { ); const title = new RegExp(`Project growth for ${MockProject}`); const rendered = await renderInTestApp( - + - + , @@ -71,13 +65,7 @@ describe('', () => { const subheader = new RegExp('1 product'); const title = new RegExp(`Project growth for ${MockProject}`); const rendered = await renderInTestApp( - + ', () => { let Component: React.ReactNode; beforeEach(() => { Component = () => ( - + ); diff --git a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx index ecc601a0b7..e3c2506296 100644 --- a/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx +++ b/plugins/cost-insights/src/components/ResourceGrowthBarChartLegend/ResourceGrowthBarChartLegend.test.tsx @@ -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; -}) => ( - - - {children} - +const MockContext = ({ children }: PropsWithChildren<{}>) => ( + + {children} ); @@ -52,7 +38,7 @@ describe('', () => { ({ ratio, amount, costText, engineerTest }) => { it(`Should display the correct cost and engineer text for ${ratio} percent change`, async () => { const rendered = await renderInTestApp( - + = ({ dispatch({ [DefaultLoadingAction.UserGroups]: isLoading }), }); -type GroupsContextProps = { +export type GroupsContextProps = { groups: Group[]; }; diff --git a/plugins/cost-insights/src/index.ts b/plugins/cost-insights/src/index.ts index ba912d2316..71cc66dba6 100644 --- a/plugins/cost-insights/src/index.ts +++ b/plugins/cost-insights/src/index.ts @@ -19,3 +19,4 @@ export * from './api'; export * from './components'; export { useCurrency } from './hooks'; export * from './types'; +export * from './utils/tests'; diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index a25f0dad20..11c70eee25 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -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, diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/utils/tests.tsx index fbf665c131..4d2cad8999 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/utils/tests.tsx @@ -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 publically 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 Context provides minimal defaults which can be overridden, allowing users to define + context props only when necessary. +*/ + +type PartialPropsWithChildren = PropsWithChildren>; export const MockGroups: Group[] = [{ id: 'tech' }, { id: 'mock-group' }]; -type MockFilterProviderProps = { - setPageFilters: Dispatch>>; - setProductFilters: Dispatch>>; - duration?: Duration; -}; +export type MockFilterProviderProps = PartialPropsWithChildren< + FilterContextProps +>; export const MockFilterProvider = ({ - setPageFilters, - setProductFilters, children, - duration = Duration.P1M, -}: PropsWithChildren) => { - 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 ( - ({ - ...period, - duration: duration, - })), - setPageFilters: setPageFilters, - setProductFilters: setProductFilters, - }} - > + {children} ); }; -export const MockConfigProvider = ({ - metrics, - products, - icons, - engineerCost, - currencies, +export type MockLoadingProviderProps = PartialPropsWithChildren< + LoadingContextProps +>; + +export const MockLoadingProvider = ({ children, -}: PropsWithChildren) => ( - - {children} - -); + ...context +}: MockLoadingProviderProps) => { + const defaultContext: LoadingContextProps = { + state: {}, + actions: [], + dispatch: jest.fn(), + }; + return ( + + {children} + + ); +}; + +export type MockConfigProviderProps = PartialPropsWithChildren< + ConfigContextProps +>; + +export const MockConfigProvider = ({ + children, + ...context +}: MockConfigProviderProps) => { + const defaultContext: ConfigContextProps = { + metrics: [], + products: [], + icons: [], + engineerCost: 0, + currencies: [], + }; + return ( + + {children} + + ); +}; + +export type MockCurrencyProviderProps = PartialPropsWithChildren< + CurrencyContextProps +>; export const MockCurrencyProvider = ({ - currency, - setCurrency, children, -}: PropsWithChildren) => ( - - {children} - -); + ...context +}: MockCurrencyProviderProps) => { + const defaultContext: CurrencyContextProps = { + currency: { + kind: null, + label: 'Engineers 🛠', + unit: 'engineer', + }, + setCurrency: jest.fn(), + }; + return ( + + {children} + + ); +}; + +export type MockBillingDateProviderProps = PartialPropsWithChildren< + BillingDateContextProps +>; export const MockBillingDateProvider = ({ - lastCompleteBillingDate, children, -}: PropsWithChildren) => ( - - {children} - -); + ...context +}: MockBillingDateProviderProps) => { + const defaultContext: BillingDateContextProps = { + lastCompleteBillingDate: '2020-10-01', + }; + return ( + + {children} + + ); +}; -export const MockScrollProvider = ({ children }: PropsWithChildren<{}>) => ( - - {children} - -); +export type MockScrollProviderProps = PropsWithChildren<{}>; + +export const MockScrollProvider = ({ children }: MockScrollProviderProps) => { + const defaultContext: ScrollContextProps = { + scrollTo: null, + setScrollTo: jest.fn(), + }; + return ( + + {children} + + ); +}; + +export type MockGroupsProviderProps = PartialPropsWithChildren< + GroupsContextProps +>; + +export const MockGroupsProvider = ({ + children, + ...context +}: MockGroupsProviderProps) => { + const defaultContext: GroupsContextProps = { + groups: [], + }; + return ( + + {children} + + ); +}; + +export type MockCostInsightsApiProviderProps = PartialPropsWithChildren<{ + identityApi: Partial; + costInsightsApi: Partial; +}>; + +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 {children}; +}; From 7491ee2170c30a4d26f169d941295e353a262631 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Sat, 24 Oct 2020 19:15:07 -0400 Subject: [PATCH 2/5] export cost growth --- plugins/cost-insights/src/components/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/cost-insights/src/components/index.ts b/plugins/cost-insights/src/components/index.ts index 6ba17f22fc..ba04830218 100644 --- a/plugins/cost-insights/src/components/index.ts +++ b/plugins/cost-insights/src/components/index.ts @@ -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'; From 8d1360aa9ff716b45b769e48e5c46f2319eadb94 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Sat, 24 Oct 2020 19:34:09 -0400 Subject: [PATCH 3/5] changeset --- .changeset/clean-phones-vanish.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/clean-phones-vanish.md diff --git a/.changeset/clean-phones-vanish.md b/.changeset/clean-phones-vanish.md new file mode 100644 index 0000000000..4c80b82f24 --- /dev/null +++ b/.changeset/clean-phones-vanish.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +export test utilities for mocking context From 3b4cac510dbaad8255ec2d36ef1dc4d9b0c5c662 Mon Sep 17 00:00:00 2001 From: Ryan Vazquez Date: Mon, 26 Oct 2020 09:30:13 -0400 Subject: [PATCH 4/5] fix typo and verbiage --- plugins/cost-insights/src/utils/tests.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cost-insights/src/utils/tests.tsx b/plugins/cost-insights/src/utils/tests.tsx index 4d2cad8999..db47bd72bd 100644 --- a/plugins/cost-insights/src/utils/tests.tsx +++ b/plugins/cost-insights/src/utils/tests.tsx @@ -35,14 +35,14 @@ import { ScrollContext, ScrollContextProps } from '../hooks/useScroll'; import { Duration, Group } from '../types'; /* - Mock Providers and types are exposed publically to allow users to test custom implementations + 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 Context provides minimal defaults which can be overridden, allowing users to define + Each Mock Provider provides minimal defaults which can be overridden, allowing users to define context props only when necessary. */ From d89401df3c1f98a07a9896ab661d83ded1d054d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Mon, 26 Oct 2020 18:28:49 +0100 Subject: [PATCH 5/5] fix(catalog): unbreak the catalog client (#3106) --- plugins/catalog/src/api/CatalogClient.test.ts | 6 ++---- plugins/catalog/src/api/CatalogClient.ts | 12 ++++-------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/plugins/catalog/src/api/CatalogClient.test.ts b/plugins/catalog/src/api/CatalogClient.test.ts index 7c0123317d..927e8dd52c 100644 --- a/plugins/catalog/src/api/CatalogClient.test.ts +++ b/plugins/catalog/src/api/CatalogClient.test.ts @@ -34,7 +34,7 @@ describe('CatalogClient', () => { client = new CatalogClient({ discoveryApi }); }); - describe('getEntiies', () => { + describe('getEntities', () => { const defaultResponse: Entity[] = [ { apiVersion: '1', @@ -71,9 +71,7 @@ describe('CatalogClient', () => { expect.assertions(2); server.use( rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { - expect(req.url.searchParams.toString()).toBe( - 'a=1&b=2&b=3&%C3%B6=%3D', - ); + expect(req.url.search).toBe('?filter=a=1,b=2,b=3,%C3%B6=%3D'); return res(ctx.json([])); }), ); diff --git a/plugins/catalog/src/api/CatalogClient.ts b/plugins/catalog/src/api/CatalogClient.ts index 6b10541107..642183f916 100644 --- a/plugins/catalog/src/api/CatalogClient.ts +++ b/plugins/catalog/src/api/CatalogClient.ts @@ -68,17 +68,13 @@ export class CatalogClient implements CatalogApi { ): Promise { let path = `/entities`; if (filter) { - const params = new URLSearchParams(); + const parts: string[] = []; for (const [key, value] of Object.entries(filter)) { - if (Array.isArray(value)) { - for (const v of value) { - params.append(key, v); - } - } else { - params.append(key, value); + for (const v of [value].flat()) { + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`); } } - path += `?${params.toString()}`; + path += `?filter=${parts.join(',')}`; } return await this.getRequired(path);