Skip to content

Commit

Permalink
[PWA-245] Shipping Information (Authenticated) (#2380)
Browse files Browse the repository at this point in the history
* Automatically set default shipping address if available

* Automatically set default shipping address if available

* Handle use case for customer entering address for first time

* Stub out functionality that will toggle between two views on checkout page

* - Complete UI for Address Book
- Wire up mutation for address book selection

* Complete changes to address edit to support editing customer address

* Complete the rest of the new address flow and cleanup

* Run linter

* Big decomp of customer logic into dedicated components

* - Select the current address on inital load of address book
- Animate the address card when new address is selected

* Address feedback from UX

* Complete rest of PR feedback in prep for UX review

* Fix the GraphQL validation errors

* - Remove Customer data from cache during login/create actions
- Address some more UX feedback

* Different approach to list position and disabled checkbox

* [PWA-562] Checkout (auth): Stored Address Follow Up (#2403)

* - Refactor Region component to support other value keys
- Change Customer form to use region id instead of code

* Remove fragment from update response so we dont get flash of null data

* Add some comments clarifying temporary work arounds

* Address PR feedback and start working on getting existing tests to pass.

* Switched modifier class for checkout page

* Cast our regionId to string so isRequired validation works

* Prettier and fix existing failing test

* Cover ui components with tests

* - Re-factor useShippingInformation to use skipped getQuery calls
- Cover talon in tests

* Cover the rest of the newly added talons with tests.

* Address QA/PR feedback

* Update tests and snaps with new click handler

* Remove box-shadow transition from add new address card also

* Update snaps

* Refactor to use skip instead of lazy query to fix bug with effect order on signin.

Co-authored-by: Devagouda <[email protected]>
  • Loading branch information
tjwiebell and dpatil-magento authored May 28, 2020
1 parent 841e669 commit 74ed0ec
Show file tree
Hide file tree
Showing 67 changed files with 4,717 additions and 229 deletions.
14 changes: 14 additions & 0 deletions packages/peregrine/lib/Apollo/clearCustomerDataFromCache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { deleteCacheEntry } from './deleteCacheEntry';

/**
* Deletes all references to Customer from the apollo cache including entries
* that start with "$" which were automatically created by Apollo InMemoryCache.
* By coincidence this rule additionally clears CustomerAddress entries, but
* we'll need to keep this in mind by adding additional patterns as MyAccount
* features are completed.
*
* @param {ApolloClient} client
*/
export const clearCustomerDataFromCache = async client => {
await deleteCacheEntry(client, key => key.match(/^\$?Customer/));
};
2 changes: 2 additions & 0 deletions packages/peregrine/lib/talons/AuthModal/useAuthModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useApolloClient, useMutation } from '@apollo/react-hooks';

import { useUserContext } from '../../context/user';
import { clearCartDataFromCache } from '../../Apollo/clearCartDataFromCache';
import { clearCustomerDataFromCache } from '../../Apollo/clearCustomerDataFromCache';

const UNAUTHED_ONLY = ['CREATE_ACCOUNT', 'FORGOT_PASSWORD', 'SIGN_IN'];

Expand Down Expand Up @@ -76,6 +77,7 @@ export const useAuthModal = props => {
// Delete cart/user data from the redux store.
await signOut({ revokeToken });
await clearCartDataFromCache(apolloClient);
await clearCustomerDataFromCache(apolloClient);

// Refresh the page as a way to say "re-initialize". An alternative
// would be to call apolloClient.resetStore() but that would require
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`callbacks update and return state handleApplyAddress 1`] = `
Object {
"variables": Object {
"addressId": 2,
"cartId": "cart123",
},
}
`;

exports[`returns the correct shape 1`] = `
Object {
"activeAddress": undefined,
"customerAddresses": Array [
Object {
"firstname": "Philip",
"id": 1,
"lastname": "Fry",
"street": Array [
"3000 57th Street",
],
},
Object {
"firstname": "Bender",
"id": 2,
"lastname": "Rodríguez",
"street": Array [
"3000 57th Street",
],
},
Object {
"firstname": "John",
"id": 3,
"lastname": "Zoidberg",
"street": Array [
"1 Dumpster Alley",
],
},
],
"handleAddAddress": [Function],
"handleApplyAddress": [Function],
"handleCancel": [Function],
"handleEditAddress": [Function],
"handleSelectAddress": [Function],
"isLoading": true,
"selectedAddress": 2,
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`event handlers fire callbacks handleEditAddress 1`] = `
Object {
"country": Object {
"code": "US",
},
"email": "[email protected]",
"firstname": "Philip",
"id": 66,
"region": Object {
"id": 22,
},
}
`;

exports[`returns correct shape 1`] = `
Object {
"handleClick": [Function],
"handleEditAddress": [Function],
"handleKeyPress": [Function],
"hasUpdate": false,
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React from 'react';
import { act } from 'react-test-renderer';

import { useAddressBook } from '../useAddressBook';
import createTestInstance from '../../../../util/createTestInstance';
import { useAppContext } from '../../../../context/app';

const mockGetCustomerAddresses = jest.fn().mockReturnValue({
data: {
customer: {
addresses: [
{
firstname: 'Philip',
id: 1,
lastname: 'Fry',
street: ['3000 57th Street']
},
{
firstname: 'Bender',
id: 2,
lastname: 'Rodríguez',
street: ['3000 57th Street']
},
{
firstname: 'John',
id: 3,
lastname: 'Zoidberg',
street: ['1 Dumpster Alley']
}
]
}
},
error: false,
loading: false
});

const mockGetCustomerCartAddress = jest.fn().mockReturnValue({
data: {
customerCart: {
shipping_addresses: [
{
firstname: 'Bender',
lastname: 'Rodríguez',
street: ['3000 57th Street']
}
]
}
},
error: false,
loading: false
});

const mockSetCustomerAddressOnCart = jest.fn();

jest.mock('@apollo/react-hooks', () => ({
useQuery: jest.fn().mockImplementation(query => {
if (query === 'getCustomerAddressesQuery')
return mockGetCustomerAddresses();

if (query === 'getCustomerCartAddressQuery')
return mockGetCustomerCartAddress();

return;
}),
useMutation: jest.fn(() => [
mockSetCustomerAddressOnCart,
{ loading: true }
])
}));

jest.mock('../../../../context/app', () => {
const state = {};
const api = {
toggleDrawer: jest.fn()
};
const useAppContext = jest.fn(() => [state, api]);

return { useAppContext };
});

jest.mock('../../../../context/cart', () => {
const state = {
cartId: 'cart123'
};
const api = {};
const useCartContext = jest.fn(() => [state, api]);

return { useCartContext };
});

const Component = props => {
const talonProps = useAddressBook(props);
return <i talonProps={talonProps} />;
};

const toggleActiveContent = jest.fn();
const mockProps = {
mutations: {},
queries: {
getCustomerAddressesQuery: 'getCustomerAddressesQuery',
getCustomerCartAddressQuery: 'getCustomerCartAddressQuery'
},
toggleActiveContent
};

test('returns the correct shape', () => {
const tree = createTestInstance(<Component {...mockProps} />);
const { root } = tree;
const { talonProps } = root.findByType('i').props;

expect(talonProps).toMatchSnapshot();
});

test('auto selects new address', () => {
mockGetCustomerAddresses.mockReturnValueOnce({
data: {
customer: {
addresses: [
{
firstname: 'Flexo',
id: 44,
lastname: 'Rodríguez',
street: ['3000 57th Street']
}
]
}
},
error: false,
loading: false
});

const tree = createTestInstance(<Component {...mockProps} />);

act(() => {
tree.update(<Component {...mockProps} />);
});

const { root } = tree;
const { talonProps } = root.findByType('i').props;
expect(talonProps.selectedAddress).toBe(3);
});

describe('callbacks update and return state', () => {
const tree = createTestInstance(<Component {...mockProps} />);
const { root } = tree;
const { talonProps } = root.findByType('i').props;

test('handleEditAddress', () => {
const [, { toggleDrawer }] = useAppContext();
const { handleEditAddress } = talonProps;

act(() => {
handleEditAddress('activeAddress');
});

const { talonProps: newTalonProps } = root.findByType('i').props;

expect(toggleDrawer).toHaveBeenCalled();
expect(newTalonProps.activeAddress).toBe('activeAddress');
});

test('handleAddAddress', () => {
const [, { toggleDrawer }] = useAppContext();
const { handleAddAddress } = talonProps;

act(() => {
handleAddAddress();
});

const { talonProps: newTalonProps } = root.findByType('i').props;

expect(toggleDrawer).toHaveBeenCalled();
expect(newTalonProps.activeAddress).toBeUndefined();
});

test('handleSelectAddress', () => {
const { handleSelectAddress } = talonProps;

act(() => {
handleSelectAddress(318);
});

const { talonProps: newTalonProps } = root.findByType('i').props;
expect(newTalonProps.selectedAddress).toBe(318);
});

test('handleApplyAddress', async () => {
const { handleApplyAddress } = talonProps;

await act(() => {
handleApplyAddress();
});

expect(mockSetCustomerAddressOnCart).toHaveBeenCalled();
expect(mockSetCustomerAddressOnCart.mock.calls[0][0]).toMatchSnapshot();
expect(toggleActiveContent).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React from 'react';
import { act } from 'react-test-renderer';

import createTestInstance from '../../../../util/createTestInstance';
import { useAddressCard } from '../useAddressCard';

const address = {
country_code: 'US',
email: '[email protected]',
firstname: 'Philip',
id: 66,
region: {
region_id: 22
}
};
const onEdit = jest.fn();
const onSelection = jest.fn();

const mockProps = {
address,
onEdit,
onSelection
};

const Component = props => {
const talonProps = useAddressCard(props);
return <i talonProps={talonProps} />;
};

test('returns correct shape', () => {
const tree = createTestInstance(<Component {...mockProps} />);
const { root } = tree;
const { talonProps } = root.findByType('i').props;

expect(talonProps).toMatchSnapshot();
});

test('returns correct value for update animation', () => {
const tree = createTestInstance(<Component {...mockProps} />);
const { root } = tree;
const { talonProps } = root.findByType('i').props;

expect(talonProps.hasUpdate).toBe(false);

act(() => {
tree.update(
<Component
{...mockProps}
address={{ ...address, firstname: 'Bender' }}
/>
);
});

const { talonProps: newTalonProps } = root.findByType('i').props;

expect(newTalonProps.hasUpdate).toBe(true);
});

describe('event handlers fire callbacks', () => {
const tree = createTestInstance(<Component {...mockProps} />);
const { root } = tree;
const { talonProps } = root.findByType('i').props;

test('handleClick', () => {
const { handleClick } = talonProps;
handleClick();
expect(onSelection).toHaveBeenCalledWith(66);
});

test('handleKeyPress', () => {
const { handleKeyPress } = talonProps;

handleKeyPress({ key: 'Tab' });
expect(onSelection).not.toBeCalled();

handleKeyPress({ key: 'Enter' });
expect(onSelection).toHaveBeenCalledWith(66);
});

test('handleEditAddress', () => {
const { handleEditAddress } = talonProps;
handleEditAddress();
expect(onEdit).toHaveBeenCalled();
expect(onEdit.mock.calls[0][0]).toMatchSnapshot();
});
});
Loading

0 comments on commit 74ed0ec

Please sign in to comment.