diff --git a/packages/peregrine/lib/store/actions/app/actions.js b/packages/peregrine/lib/store/actions/app/actions.js index 7595d60fad..d062e3db6e 100644 --- a/packages/peregrine/lib/store/actions/app/actions.js +++ b/packages/peregrine/lib/store/actions/app/actions.js @@ -7,7 +7,8 @@ const actionTypes = [ 'SET_OFFLINE', 'TOGGLE_SEARCH', 'EXECUTE_SEARCH', - 'MARK_ERROR_HANDLED' + 'MARK_ERROR_HANDLED', + 'SET_PAGE_LOADING' ]; export default createActions(...actionTypes, { prefix }); diff --git a/packages/peregrine/lib/store/reducers/app.js b/packages/peregrine/lib/store/reducers/app.js index 1970bcced4..199b32a703 100644 --- a/packages/peregrine/lib/store/reducers/app.js +++ b/packages/peregrine/lib/store/reducers/app.js @@ -10,7 +10,8 @@ const initialState = { isOnline: navigator.onLine, overlay: false, searchOpen: false, - pending: {} + pending: {}, + isPageLoading: false }; const reducerMap = { @@ -39,6 +40,12 @@ const reducerMap = { isOnline: false, hasBeenOffline: true }; + }, + [actions.setPageLoading]: (state, { payload }) => { + return { + ...state, + isPageLoading: !!payload + }; } }; diff --git a/packages/peregrine/lib/talons/Header/__tests__/useHeader.spec.js b/packages/peregrine/lib/talons/Header/__tests__/useHeader.spec.js new file mode 100644 index 0000000000..d2ba370674 --- /dev/null +++ b/packages/peregrine/lib/talons/Header/__tests__/useHeader.spec.js @@ -0,0 +1,72 @@ +import React, { useEffect } from 'react'; +import { useAppContext } from '@magento/peregrine/lib/context/app'; +import { useHeader } from '../useHeader'; +import { createTestInstance } from '../../../index'; +import { act } from 'react-test-renderer'; + +jest.mock('@magento/peregrine/lib/context/app', () => { + const api = {}; + const state = {}; + return { + useAppContext: jest.fn(() => [state, api]) + }; +}); + +const log = jest.fn(); +const Component = () => { + const talonProps = useHeader(); + + useEffect(() => { + log(talonProps); + }, [talonProps]); + + return
; +}; + +test('useHeader returns correct values from useAppContext', () => { + useAppContext.mockImplementation(() => { + return [ + { + hasBeenOffline: false, + isOnline: true, + searchOpen: false, + isPageLoading: false + }, + { + toggleSearch: jest.fn() + } + ]; + }); + + createTestInstance(); + + expect(log).toHaveBeenCalledWith({ + handleSearchTriggerClick: expect.any(Function), + hasBeenOffline: false, + isOnline: true, + isPageLoading: false, + searchOpen: false + }); +}); + +test('handleSearchTriggerClick calls toggleSearch from useAppContext', () => { + const toggleSearchFn = jest.fn(); + + useAppContext.mockImplementation(() => { + return [ + {}, + { + toggleSearch: toggleSearchFn + } + ]; + }); + + const component = createTestInstance(); + const talonProps = component.root.findByProps({ id: 'header' }).props; + + act(() => { + talonProps.handleSearchTriggerClick(); + }); + + expect(toggleSearchFn).toHaveBeenCalled(); +}); diff --git a/packages/peregrine/lib/talons/Header/useHeader.js b/packages/peregrine/lib/talons/Header/useHeader.js index 50d3c528c2..a640740185 100644 --- a/packages/peregrine/lib/talons/Header/useHeader.js +++ b/packages/peregrine/lib/talons/Header/useHeader.js @@ -3,7 +3,7 @@ import { useAppContext } from '@magento/peregrine/lib/context/app'; export const useHeader = () => { const [ - { hasBeenOffline, isOnline, searchOpen }, + { hasBeenOffline, isOnline, isPageLoading, searchOpen }, { toggleSearch } ] = useAppContext(); @@ -15,6 +15,7 @@ export const useHeader = () => { handleSearchTriggerClick, hasBeenOffline, isOnline, - searchOpen + searchOpen, + isPageLoading }; }; diff --git a/packages/venia-ui/lib/RootComponents/CMS/cms.js b/packages/venia-ui/lib/RootComponents/CMS/cms.js index 6545af0241..e9e377fdfa 100644 --- a/packages/venia-ui/lib/RootComponents/CMS/cms.js +++ b/packages/venia-ui/lib/RootComponents/CMS/cms.js @@ -9,6 +9,7 @@ import { Meta, Title } from '../../components/Head'; import { mergeClasses } from '../../classify'; import defaultClasses from './cms.css'; +import { useAppContext } from '@magento/peregrine/lib/context/app'; const CMSPage = props => { const { id } = props; @@ -17,8 +18,15 @@ const CMSPage = props => { variables: { id: Number(id), onServer: false - } + }, + fetchPolicy: 'cache-and-network' }); + const [ + { isPageLoading }, + { + actions: { setPageLoading } + } + ] = useAppContext(); if (error) { if (process.env.NODE_ENV !== 'production') { @@ -27,12 +35,15 @@ const CMSPage = props => { return
Page Fetch Error
; } - if (loading) { + if (!data) { return fullPageLoadingIndicator; } - if (!data) { - return null; + // Ensure we mark the page as loading while we check the network for updates + if (loading && !isPageLoading) { + setPageLoading(true); + } else if (!loading && isPageLoading) { + setPageLoading(false); } const { content_heading, title } = data.cmsPage; diff --git a/packages/venia-ui/lib/components/Header/__tests__/__snapshots__/header.spec.js.snap b/packages/venia-ui/lib/components/Header/__tests__/__snapshots__/header.spec.js.snap new file mode 100644 index 0000000000..799935f6e1 --- /dev/null +++ b/packages/venia-ui/lib/components/Header/__tests__/__snapshots__/header.spec.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`verify Header can render in default state 1`] = ` +
+
+
+ +
+ +
+ + +
+
+
+`; diff --git a/packages/venia-ui/lib/components/Header/__tests__/header.spec.js b/packages/venia-ui/lib/components/Header/__tests__/header.spec.js new file mode 100644 index 0000000000..a24b65e1b0 --- /dev/null +++ b/packages/venia-ui/lib/components/Header/__tests__/header.spec.js @@ -0,0 +1,54 @@ +import React from 'react'; +import { createTestInstance } from '@magento/peregrine'; +import Header from '../header'; +import { useHeader } from '@magento/peregrine/lib/talons/Header/useHeader'; + +jest.mock('../../../classify'); +jest.mock('../../Logo', () => 'Logo'); +jest.mock('../cartTrigger', () => 'CartTrigger'); +jest.mock('../navTrigger', () => 'NavTrigger'); +jest.mock('../searchTrigger', () => 'SearchTrigger'); +jest.mock('../onlineIndicator', () => 'OnlineIndicator'); +jest.mock('../../PageLoadingIndicator', () => () => ( +
+)); + +jest.mock('@magento/venia-drivers', () => ({ + resourceUrl: jest.fn(url => url), + Link: jest.fn(() => null), + Route: jest.fn(() => null) +})); + +jest.mock('@magento/peregrine/lib/talons/Header/useHeader', () => { + const state = { + handleSearchTriggerClick: jest.fn(), + hasBeenOffline: false, + isOnline: true, + searchOpen: false, + isPageLoading: false + }; + return { + useHeader: jest.fn(() => state) + }; +}); + +test('verify Header can render in default state', () => { + const component = createTestInstance(
); + + expect(component.toJSON()).toMatchSnapshot(); +}); + +test('verify PageLoadingIndicator is displayed when page is loading', () => { + useHeader.mockImplementation(() => { + return { + handleSearchTriggerClick: jest.fn(), + hasBeenOffline: false, + isOnline: true, + searchOpen: false, + isPageLoading: true + }; + }); + + const component = createTestInstance(
); + component.root.findByProps({ id: 'pageLoadingIndicator' }); +}); diff --git a/packages/venia-ui/lib/components/Header/header.css b/packages/venia-ui/lib/components/Header/header.css index 5230924ba6..06e3af5ac1 100755 --- a/packages/venia-ui/lib/components/Header/header.css +++ b/packages/venia-ui/lib/components/Header/header.css @@ -39,6 +39,9 @@ .primaryActions { grid-area: primary; justify-self: start; + display: grid; + grid-auto-flow: column; + align-items: center; } .secondaryActions { diff --git a/packages/venia-ui/lib/components/Header/header.js b/packages/venia-ui/lib/components/Header/header.js index ee49397b4e..d624f227cc 100644 --- a/packages/venia-ui/lib/components/Header/header.js +++ b/packages/venia-ui/lib/components/Header/header.js @@ -12,6 +12,7 @@ import { useHeader } from '@magento/peregrine/lib/talons/Header/useHeader'; import { mergeClasses } from '../../classify'; import defaultClasses from './header.css'; +import PageLoadingIndicator from '../PageLoadingIndicator'; const SearchBar = React.lazy(() => import('../SearchBar')); @@ -20,7 +21,8 @@ const Header = props => { handleSearchTriggerClick, hasBeenOffline, isOnline, - searchOpen + searchOpen, + isPageLoading } = useHeader(); const classes = mergeClasses(defaultClasses, props.classes); @@ -39,12 +41,16 @@ const Header = props => { ) : null; + const pageLoadingIndicator = isPageLoading ? ( + + ) : null; return (
+ {pageLoadingIndicator}
{ + const classes = mergeClasses(defaultClasses, props.classes); + + return ( +
+ +
+ ); +}; + +export default PageLoadingIndicator;