Skip to content
Permalink
Browse files

refactor(payments): replace Redux with useAwait hook for API calls

- new useAwait hook that replaces Redux for tracking API call state

- move shared types to lib/types.ts

- fetch common API resources (token, plans, profile, customer,
  subscriptions) in App.tsx

- add common API resources to AppContext for use by subcomponents

- tweaks to tests & stories to reflect the new source for common API
  resources and API calls

- general type annotation improvements - particularly in apiClient

- general test coverage improvements

fixes mozilla#3526
fixes mozilla#3020
  • Loading branch information
lmorchard committed Dec 16, 2019
1 parent 0a6ca46 commit ccbf2c7862816e418c55b1bf76948ce7ef2d9888
Showing with 1,894 additions and 1,689 deletions.
  1. +8 −1 packages/fxa-payments-server/.storybook/components/MockApp.tsx
  2. +144 −6 packages/fxa-payments-server/src/App.test.tsx
  3. +120 −37 packages/fxa-payments-server/src/App.tsx
  4. +36 −0 packages/fxa-payments-server/src/components/ErrorDialogMessage.test.tsx
  5. +7 −8 packages/fxa-payments-server/src/components/{FetchErrorDialogMessage.tsx → ErrorDialogMessage.tsx}
  6. +1 −1 packages/fxa-payments-server/src/components/PaymentForm/index.stories.tsx
  7. +1 −1 packages/fxa-payments-server/src/components/PaymentForm/index.tsx
  8. +0 −8 packages/fxa-payments-server/src/index.tsx
  9. +25 −1 packages/fxa-payments-server/src/lib/AppContext.tsx
  10. +2 −6 packages/fxa-payments-server/src/lib/apiClient.ts
  11. +227 −8 packages/fxa-payments-server/src/lib/hooks.test.tsx
  12. +138 −7 packages/fxa-payments-server/src/lib/hooks.tsx
  13. +67 −0 packages/fxa-payments-server/src/lib/metadataFromPlan.test.ts
  14. +17 −0 packages/fxa-payments-server/src/lib/metadataFromPlan.ts
  15. +27 −38 packages/fxa-payments-server/src/lib/test-utils.tsx
  16. +78 −0 packages/fxa-payments-server/src/lib/types.tsx
  17. +7 −8 packages/fxa-payments-server/src/routes/Product/PlanDetails/index.tsx
  18. +1 −1 packages/fxa-payments-server/src/routes/Product/ProfileBanner.tsx
  19. +1 −1 packages/fxa-payments-server/src/routes/Product/SubscriptionCreate/AccountActivatedBanner.tsx
  20. +234 −0 packages/fxa-payments-server/src/routes/Product/SubscriptionCreate/index.stories.tsx
  21. +41 −26 packages/fxa-payments-server/src/routes/Product/SubscriptionCreate/index.tsx
  22. +2 −2 packages/fxa-payments-server/src/routes/Product/SubscriptionRedirect/index.tsx
  23. +31 −44 packages/fxa-payments-server/src/routes/Product/SubscriptionUpgrade/index.stories.tsx
  24. +38 −51 packages/fxa-payments-server/src/routes/Product/SubscriptionUpgrade/index.test.tsx
  25. +42 −28 packages/fxa-payments-server/src/routes/Product/SubscriptionUpgrade/index.tsx
  26. +1 −1 packages/fxa-payments-server/src/routes/Product/SubscriptionUpgrade/mocks.tsx
  27. +0 −355 packages/fxa-payments-server/src/routes/Product/index.stories.tsx
  28. +130 −165 packages/fxa-payments-server/src/routes/Product/index.test.tsx
  29. +22 −120 packages/fxa-payments-server/src/routes/Product/index.tsx
  30. +14 −10 packages/fxa-payments-server/src/routes/Subscriptions/PaymentUpdateForm.tsx
  31. +2 −2 packages/fxa-payments-server/src/routes/Subscriptions/Reactivate/ConfirmationDialog.tsx
  32. +10 −4 packages/fxa-payments-server/src/routes/Subscriptions/Reactivate/ManagementPanel.tsx
  33. +20 −4 packages/fxa-payments-server/src/routes/Subscriptions/Reactivate/SuccessDialog.tsx
  34. +32 −15 packages/fxa-payments-server/src/routes/Subscriptions/SubscriptionItem.tsx
  35. +111 −221 packages/fxa-payments-server/src/routes/Subscriptions/index.stories.tsx
  36. +186 −358 packages/fxa-payments-server/src/routes/Subscriptions/index.test.tsx
  37. +71 −151 packages/fxa-payments-server/src/routes/Subscriptions/index.tsx
@@ -2,7 +2,11 @@ import React, { useEffect, useMemo, ReactNode } from 'react';
import { action } from '@storybook/addon-actions';
import { StripeProvider } from 'react-stripe-elements';
import { MockLoader } from './MockLoader';
import { AppContext, AppContextType } from '../../src/lib/AppContext';
import {
AppContext,
AppContextType,
defaultAppContext,
} from '../../src/lib/AppContext';
import { config } from '../../src/lib/config';
import ScreenInfo from '../../src/lib/screen-info';

@@ -20,6 +24,7 @@ type MockAppProps = {
};

export const defaultAppContextValue: AppContextType = {
...defaultAppContext,
config: {
...config,
productRedirectURLs: {
@@ -37,6 +42,8 @@ export const defaultAppContextValue: AppContextType = {
getScreenInfo: () => new ScreenInfo(window),
matchMedia: (query: string) => window.matchMedia(query).matches,
locationReload: action('locationReload'),
fetchCustomer: () => Promise.resolve(),
fetchSubscriptions: () => Promise.resolve(),
};

export const defaultStripeStubs = (stripe: stripe.Stripe) => {
@@ -1,15 +1,153 @@
import React from 'react';
import { render } from '@testing-library/react';
import React, { ReactNode } from 'react';
import { render, RenderResult } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import waitForExpect from 'wait-for-expect';

jest.mock('react-stripe-elements', () => ({
...jest.requireActual('react-stripe-elements'),
StripeProvider: ({ children }: { children?: ReactNode }) => (
<div data-testid="stripe-provider">{children}</div>
),
}));
import 'react-stripe-elements';

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
BrowserRouter: ({ children }: { children?: ReactNode }) => (
<div data-testid="browser-router"></div>
),
}));
import 'react-router-dom';

import {
MOCK_PROFILE,
MOCK_CUSTOMER,
MOCK_ACTIVE_SUBSCRIPTIONS,
MOCK_TOKEN,
MOCK_PLANS,
} from './lib/test-utils';

import {
apiFetchPlans,
apiFetchProfile,
apiFetchCustomer,
apiFetchSubscriptions,
apiFetchToken,
} from './lib/apiClient';
jest.mock('./lib/apiClient');

jest.mock('./lib/sentry');

import { defaultAppContextValue } from './lib/test-utils';
import { AppErrorBoundary, AppErrorDialog } from './App';
import { App, AppProps, AppErrorBoundary, AppErrorDialog } from './App';
import { AppContext } from './lib/AppContext';
import { QueryParams } from './lib/types';
import ScreenInfo from './lib/screen-info';
import { Config, defaultConfig } from './lib/config';
import { AuthServerErrno } from './lib/errors';

jest.mock('./lib/sentry');
const mockApiFetchPlans = apiFetchPlans as jest.Mock;
const mockApiFetchProfile = apiFetchProfile as jest.Mock;
const mockApiFetchCustomer = apiFetchCustomer as jest.Mock;
const mockApiFetchSubscriptions = apiFetchSubscriptions as jest.Mock;
const mockApiFetchToken = apiFetchToken as jest.Mock;

describe('App', () => {
const Subject = ({
queryParams = {},
config = defaultConfig(),
matchMedia = () => false,
navigateToUrl = () => {},
locationReload = jest.fn(),
getScreenInfo = () => new ScreenInfo(),
}: {
queryParams?: QueryParams;
config?: Config;
matchMedia?: (query: string) => boolean;
navigateToUrl?: (url: string) => void;
locationReload?: () => void;
getScreenInfo?: () => ScreenInfo;
}) => {
return (
<App
{...{
config,
locationReload,
matchMedia,
navigateToUrl,
queryParams,
getScreenInfo,
}}
/>
);
};

beforeEach(() => {
mockApiFetchProfile.mockResolvedValueOnce(MOCK_PROFILE);
mockApiFetchCustomer.mockResolvedValueOnce(MOCK_CUSTOMER);
mockApiFetchSubscriptions.mockResolvedValueOnce(MOCK_ACTIVE_SUBSCRIPTIONS);
mockApiFetchToken.mockResolvedValueOnce(MOCK_TOKEN);
mockApiFetchPlans.mockResolvedValueOnce(MOCK_PLANS);
});

// TODO: backfill general App component tests
// describe('App', () => {});
afterEach(() => {
jest.clearAllMocks();
});

const awaitLoader = async ({ findByTestId, queryByTestId }: RenderResult) => {
await findByTestId('loading-overlay');
await waitForExpect(() =>
expect(queryByTestId('loading-overlay')).not.toBeInTheDocument()
);
};

const assertErrorDialog = (
mockApi: jest.Mock,
testid: string
) => async () => {
const message = 'oopsie';
mockApi.mockReset();
mockApi.mockRejectedValueOnce({ message });
const renderResult = render(<Subject />);
const { queryByTestId, queryByText } = renderResult;
await awaitLoader(renderResult);
expect(queryByTestId(testid)).toBeInTheDocument();
expect(queryByText(message)).toBeInTheDocument();
};

it(
'displays an error if plans fail to load',
assertErrorDialog(mockApiFetchPlans, 'error-loading-plans')
);

it(
'displays an error if profile fails to load',
assertErrorDialog(mockApiFetchProfile, 'error-loading-profile')
);

it(
'displays an error if subscriptions fail to load',
assertErrorDialog(mockApiFetchSubscriptions, 'error-subscriptions-fetch')
);

it(
'displays an error if customer fail to load',
assertErrorDialog(mockApiFetchCustomer, 'error-loading-customer')
);

it('does not display an error if the customer is unknown', async () => {
const message = 'oopsie';
mockApiFetchCustomer.mockReset();
mockApiFetchCustomer.mockRejectedValueOnce({
message,
errno: AuthServerErrno.UNKNOWN_SUBSCRIPTION_CUSTOMER,
});
const renderResult = render(<Subject />);
const { queryByTestId, queryByText, debug } = renderResult;
await awaitLoader(renderResult);
expect(queryByTestId('error-loading-customer')).not.toBeInTheDocument();
});
});

describe('App/AppErrorBoundary', () => {
beforeEach(() => {

0 comments on commit ccbf2c7

Please sign in to comment.
You can’t perform that action at this time.