diff --git a/packages/marketplace/src/components/pages/__tests__/__snapshots__/client.tsx.snap b/packages/marketplace/src/components/pages/__tests__/__snapshots__/client.tsx.snap deleted file mode 100644 index bbfb4f1c4d..0000000000 --- a/packages/marketplace/src/components/pages/__tests__/__snapshots__/client.tsx.snap +++ /dev/null @@ -1,268 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Client should match a snapshot when LOADING false 1`] = ` - -
- -
- -
-
- -
-`; - -exports[`Client should match a snapshot when LOADING true 1`] = ` - -
- - -
- -
-`; - -exports[`Client should match a snapshot when featured apps is empty [] 1`] = ` - -
- -
- -
-
- -
-`; - -exports[`Client should match a snapshot when featured apps is undefined 1`] = ` - -
- -
- -
-
- -
-`; diff --git a/packages/marketplace/src/components/pages/__tests__/client.tsx b/packages/marketplace/src/components/pages/__tests__/client.tsx deleted file mode 100644 index e71f18348f..0000000000 --- a/packages/marketplace/src/components/pages/__tests__/client.tsx +++ /dev/null @@ -1,261 +0,0 @@ -import * as React from 'react' -import { Provider } from 'react-redux' -import { shallow, mount } from 'enzyme' -import { appsDataStub, featuredAppsDataStub } from '@/sagas/__stubs__/apps' -import { ReduxState } from '@/types/core' -import { ClientAppSummary } from '@/reducers/client/app-summary' -import { - Client, - ClientProps, - mapStateToProps, - mapDispatchToProps, - handleAfterClose, - handleOnChange, - handleOnCardClick, - onCardClickParams, -} from '../client' -import { addQuery } from '@/utils/client-url-params' -import { AppSummaryModel } from '@reapit/foundations-ts-definitions' -import { RouteComponentProps, StaticContext } from 'react-router' -import { appDetailDataStub } from '@/sagas/__stubs__/app-detail' -import ClientWelcomeMessageModal from '@/components/ui/client-welcome-message' -import store from '@/core/store' -import { BrowserRouter as Router } from 'react-router-dom' - -const routerProps = { - match: { - params: { - page: '2', - }, - }, - location: { - search: 'page=1', - }, -} as RouteComponentProps - -const props = (loading: boolean): ClientProps => ({ - appSummaryState: { - isAppSummaryLoading: loading, - data: { - featuredApps: featuredAppsDataStub.data, - apps: appsDataStub, - } as ClientAppSummary, - }, - appDetail: { - appDetailData: appDetailDataStub, - loading: false, - error: false, - authentication: { - loading: false, - code: '', - }, - isStale: false, - }, - clientId: '1', - setStateViewBrowse: jest.fn(), - installationsFormState: 'PENDING', - installationsSetFormState: jest.fn(), - fetchAppDetail: jest.fn(), - ...routerProps, -}) - -describe('Client', () => { - it('should match a snapshot when LOADING false', () => { - expect(shallow()).toMatchSnapshot() - }) - - it('should match a snapshot when LOADING true', () => { - expect(shallow()).toMatchSnapshot() - }) - - it('should match a snapshot when featured apps is empty []', () => { - const routerProps = { - match: { - params: { - page: '2', - }, - }, - location: { - search: 'page=1', - }, - } as RouteComponentProps - - const props: ClientProps = { - appSummaryState: { - isAppSummaryLoading: false, - data: { - featuredApps: [] as AppSummaryModel[], - apps: appsDataStub, - } as ClientAppSummary, - }, - appDetail: { - appDetailData: appDetailDataStub, - loading: false, - error: false, - authentication: { - loading: false, - code: '', - }, - isStale: false, - }, - clientId: '1', - installationsFormState: 'PENDING', - setStateViewBrowse: jest.fn(), - installationsSetFormState: jest.fn(), - fetchAppDetail: jest.fn(), - ...routerProps, - } - - expect(shallow()).toMatchSnapshot() - }) - - it('should match a snapshot when featured apps is undefined', () => { - const props: ClientProps = { - appSummaryState: { - isAppSummaryLoading: false, - data: { - featuredApps: undefined, - apps: appsDataStub, - } as ClientAppSummary, - }, - appDetail: { - appDetailData: appDetailDataStub, - loading: false, - error: false, - authentication: { - loading: false, - code: '', - }, - isStale: false, - }, - clientId: '1', - installationsFormState: 'PENDING', - setStateViewBrowse: jest.fn(), - installationsSetFormState: jest.fn(), - fetchAppDetail: jest.fn(), - ...routerProps, - } - - expect(shallow()).toMatchSnapshot() - }) - - describe('mapStateToProps', () => { - it('should return correctly', () => { - const mockState = { - client: { - appSummary: { - data: { - apps: appsDataStub.data, - featuredApps: featuredAppsDataStub.data, - }, - isAppSummaryLoading: false, - error: 'error', - }, - appDetail: { - data: appDetailDataStub.data, - isAppDetailLoading: false, - error: '', - }, - }, - appDetail: { - appDetailData: appDetailDataStub, - error: false, - loading: false, - }, - auth: { - loginSession: { - loginIdentity: { - clientId: 'ABC', - }, - }, - }, - installations: { - formState: 'PENDING', - }, - } as ReduxState - const output = { - appSummaryState: mockState.client.appSummary, - appDetail: mockState.appDetail, - clientId: 'ABC', - installationsFormState: 'PENDING', - } - const result = mapStateToProps(mockState) - expect(result).toEqual(output) - }) - }) - - describe('mapDispatchToProps', () => { - it('should call fetchAppDetail correctly', () => { - const mockDispatch = jest.fn() - const { fetchAppDetail } = mapDispatchToProps(mockDispatch) - fetchAppDetail('123', 'ABC') - expect(mockDispatch).toBeCalled() - }) - it('should call installationsSetFormState correctly', () => { - const mockDispatch = jest.fn() - const { installationsSetFormState } = mapDispatchToProps(mockDispatch) - installationsSetFormState('SUCCESS') - expect(mockDispatch).toBeCalled() - }) - }) - - describe('handleOnChange', () => { - it('should call push correctly', () => { - const mockHistory = { - push: jest.fn(), - } - const fn = handleOnChange(mockHistory) - fn(1) - expect(mockHistory.push).toBeCalledWith(addQuery({ page: 1 })) - }) - }) - - describe('handleAfterClose', () => { - const setVisible = jest.fn() - const fn = handleAfterClose({ setVisible }) - fn() - expect(setVisible).toBeCalledWith(false) - }) - - describe('handleOnCardClick', () => { - it('should run correctly', () => { - const mockProps: onCardClickParams = { - setVisible: jest.fn(), - appDetail: { - authentication: { - loading: false, - code: '200', - }, - error: false, - loading: false, - appDetailData: { - data: appsDataStub.data.data![0], - }, - isStale: true, - }, - setStateViewBrowse: jest.fn(), - fetchAppDetail: jest.fn(), - clientId: 'ABC', - } - const fn = handleOnCardClick(mockProps) - fn({ id: '1' }) - expect(mockProps.setVisible).toBeCalled() - expect(mockProps.fetchAppDetail).toBeCalled() - }) - }) - - describe('show welcome modal when firstLogin', () => { - it('should run correctly', () => { - const wrapper = mount( - - - - - , - ) - setTimeout(() => { - expect(wrapper.find()).toEqual(1) - }, 200) - }) - }) -}) diff --git a/packages/marketplace/src/components/pages/client/__tests__/__snapshots__/client.test.tsx.snap b/packages/marketplace/src/components/pages/client/__tests__/__snapshots__/client.test.tsx.snap new file mode 100644 index 0000000000..31c81e6491 --- /dev/null +++ b/packages/marketplace/src/components/pages/client/__tests__/__snapshots__/client.test.tsx.snap @@ -0,0 +1,2245 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Client should match a snapshot when LOADING false 1`] = ` + + + + + + +
+ + + +
+ +
+
+ +

+ Browse Apps +

+
+ +
+ Search +
+
+ + +
+ + + + + } + type="text" + > + +
+
+ + + + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ Filter +
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+ + + + + + + + + + + +`; + +exports[`Client should match a snapshot when LOADING false 2`] = ` + + + + + + +
+ + + +
+ +
+
+ +

+ Browse Apps +

+
+ +
+ Search +
+
+ + +
+ + + + + } + type="text" + > + +
+
+ + + + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ Filter +
+
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+ + + + + + + + + + + +`; + +exports[`Client should match a snapshot when featured apps is empty [] 1`] = ` + + + + + + +
+ + + +
+ +
+
+ +

+ Browse Apps +

+
+ +
+ Search +
+
+ + +
+ + + + + } + type="text" + > + +
+
+ + + + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ Filter +
+
+ + + +
+
+
+
+
+
+
+
+ + +
+ +
+ We are sorry, there are no apps listed compatible with your account +
+
+ +
+ +
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+
+`; + +exports[`Client should match a snapshot when featured apps is undefined 1`] = ` + + + + + + +
+ + + +
+ +
+
+ +

+ Browse Apps +

+
+ +
+ Search +
+
+ + +
+ + + + + } + type="text" + > + +
+
+ + + + +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+ +
+ Filter +
+
+ + + +
+
+
+
+
+
+
+
+ + +
+ +
+ We are sorry, there are no apps listed compatible with your account +
+
+ +
+ +
+
+
+
+
+
+
+ + + + + +
+
+
+
+
+
+`; diff --git a/packages/marketplace/src/components/pages/client/__tests__/client.test.tsx b/packages/marketplace/src/components/pages/client/__tests__/client.test.tsx new file mode 100644 index 0000000000..5d5786cc6f --- /dev/null +++ b/packages/marketplace/src/components/pages/client/__tests__/client.test.tsx @@ -0,0 +1,214 @@ +import * as React from 'react' +import { Provider } from 'react-redux' +import { mount } from 'enzyme' +import { appsDataStub } from '@/sagas/__stubs__/apps' +import { ClientAppSummary } from '@/reducers/client/app-summary' +import { + Client, + handleSetStateViewBrowse, + handleFetchAppDetail, + handleInstallationsSetFormState, + handleAfterClose, + handleOnChange, + handleOnCardClick, + onCardClickParams, +} from '../client' +import { addQuery } from '@/utils/client-url-params' +import { AppSummaryModel } from '@reapit/foundations-ts-definitions' +import ClientWelcomeMessageModal from '@/components/ui/client-welcome-message' +import { BrowserRouter as Router } from 'react-router-dom' +import configureStore from 'redux-mock-store' +import * as ReactRedux from 'react-redux' +import { MemoryRouter } from 'react-router' +import Routes from '@/constants/routes' +import appState from '@/reducers/__stubs__/app-state' +import { setAppDetailModalStateBrowse } from '@/actions/app-detail-modal' +import { appDetailRequestData } from '@/actions/app-detail' +import { appInstallationsSetFormState } from '@/actions/app-installations' + +const createState = appSummaryState => { + return { + ...appState, + client: { + ...appState.client, + appSummary: { ...appState.client, ...appSummaryState }, + }, + } +} + +describe('Client', () => { + let store + + beforeEach(() => { + /* mocking store */ + // spyDispatch = jest.spyOn(ReactRedux, 'useDispatch').mockImplementation(() => store.dispatch) + }) + it('should match a snapshot when LOADING false', () => { + const appSummaryState = { + isAppSummaryLoading: false, + } + const mockStore = configureStore() + store = mockStore(createState(appSummaryState)) + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) + + it('should match a snapshot when LOADING false', () => { + const appSummaryState = { + isAppSummaryLoading: true, + } + const mockStore = configureStore() + store = mockStore(createState(appSummaryState)) + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) + + it('should match a snapshot when featured apps is empty []', () => { + const appSummaryState = { + isAppSummaryLoading: false, + data: { + featuredApps: [] as AppSummaryModel[], + apps: appsDataStub, + } as ClientAppSummary, + } + const mockStore = configureStore() + store = mockStore(createState(appSummaryState)) + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) + + it('should match a snapshot when featured apps is undefined', () => { + const appSummaryState = { + isAppSummaryLoading: false, + data: { + featuredApps: undefined, + apps: appsDataStub, + } as ClientAppSummary, + } + const mockStore = configureStore() + store = mockStore(createState(appSummaryState)) + expect( + mount( + + + + + , + ), + ).toMatchSnapshot() + }) + + describe('handleOnChange', () => { + it('should call push correctly', () => { + const mockHistory = { + push: jest.fn(), + } + const fn = handleOnChange(mockHistory) + fn(1) + expect(mockHistory.push).toBeCalledWith(addQuery({ page: 1 })) + }) + }) + + describe('handleAfterClose', () => { + const setVisible = jest.fn() + const fn = handleAfterClose({ setVisible }) + fn() + expect(setVisible).toBeCalledWith(false) + }) + + describe('handleOnCardClick', () => { + it('should run correctly', () => { + const mockProps: onCardClickParams = { + setVisible: jest.fn(), + appDetail: { + authentication: { + loading: false, + code: '200', + }, + error: false, + loading: false, + appDetailData: { + data: appsDataStub.data.data![0], + }, + isStale: true, + }, + setStateViewBrowse: jest.fn(), + fetchAppDetail: jest.fn(), + clientId: 'ABC', + } + const fn = handleOnCardClick(mockProps) + fn({ id: '1' }) + expect(mockProps.setVisible).toBeCalled() + expect(mockProps.fetchAppDetail).toBeCalled() + }) + }) + + describe('show welcome modal when firstLogin', () => { + it('should run correctly', () => { + const appSummaryState = { + isAppSummaryLoading: false, + data: { + featuredApps: [] as AppSummaryModel[], + apps: appsDataStub, + } as ClientAppSummary, + } + const mockStore = configureStore() + store = mockStore(createState(appSummaryState)) + const wrapper = mount( + + + + + , + ) + setTimeout(() => { + expect(wrapper.find()).toEqual(1) + }, 200) + }) + }) + + it('handleSetStateViewBrowse should run correctly', () => { + const dispatch = jest.fn() + const fn = handleSetStateViewBrowse(dispatch) + fn() + expect(dispatch).toBeCalledWith(setAppDetailModalStateBrowse()) + }) + + it('handleFetchAppDetail should run correctly', () => { + const dispatch = jest.fn() + const id = 'id' + const clientId = 'clientId' + const fn = handleFetchAppDetail(dispatch) + fn(id, clientId) + expect(dispatch).toBeCalledWith(appDetailRequestData({ id, clientId })) + }) + + it('handleInstallationsSetFormState should run correctly', () => { + const dispatch = jest.fn() + const formState = 'DONE' + const fn = handleInstallationsSetFormState(dispatch) + fn(formState) + expect(dispatch).toBeCalledWith(appInstallationsSetFormState(formState)) + }) +}) diff --git a/packages/marketplace/src/components/pages/client.tsx b/packages/marketplace/src/components/pages/client/client.tsx similarity index 66% rename from packages/marketplace/src/components/pages/client.tsx rename to packages/marketplace/src/components/pages/client/client.tsx index 2814bd2cc6..c1dc1e1dd3 100644 --- a/packages/marketplace/src/components/pages/client.tsx +++ b/packages/marketplace/src/components/pages/client/client.tsx @@ -1,10 +1,9 @@ import * as React from 'react' -import { connect } from 'react-redux' -import { ReduxState, FormState } from '@/types/core' -import { ClientAppSummaryState } from '@/reducers/client/app-summary' +import { useSelector, useDispatch } from 'react-redux' +import { FormState } from '@/types/core' import { Loader } from '@reapit/elements' import ErrorBoundary from '@/components/hocs/error-boundary' -import { withRouter, RouteComponentProps } from 'react-router' +import { useHistory, useLocation } from 'react-router' import AppList from '@/components/ui/app-list' import AppSidebar from '@/components/ui/app-sidebar' import { appDetailRequestData } from '@/actions/app-detail' @@ -14,21 +13,11 @@ import { selectClientId, selectAppSummary } from '@/selector/client' import { AppSummaryModel } from '@reapit/foundations-ts-definitions' import styles from '@/styles/pages/client.scss?mod' import { appInstallationsSetFormState } from '@/actions/app-installations' -import { addQuery, getParamValueFromPath, hasFilterParams } from '@/utils/client-url-params' +import { addQuery, hasFilterParams } from '@/utils/client-url-params' import { setAppDetailModalStateBrowse } from '@/actions/app-detail-modal' - -export interface ClientMappedActions { - setStateViewBrowse: () => void - fetchAppDetail: (id: string, clientId: string) => void - installationsSetFormState: (formState: FormState) => void -} - -export interface ClientMappedProps { - appSummaryState: ClientAppSummaryState - appDetail: AppDetailState - clientId: string - installationsFormState: FormState -} +import { selectAppDetail } from '@/selector/client' +import { selectInstallationFormState } from '@/selector/installations' +import { Dispatch } from 'redux' export const handleAfterClose = ({ setVisible }) => () => setVisible(false) export const handleOnChange = history => (page: number) => { @@ -74,24 +63,35 @@ export const handleInstallationDone = ({ } } -export type ClientProps = ClientMappedActions & ClientMappedProps & RouteComponentProps<{ page?: any }> +export const handleSetStateViewBrowse = (dispatch: Dispatch) => () => { + dispatch(setAppDetailModalStateBrowse()) +} + +export const handleFetchAppDetail = (dispatch: Dispatch) => (id, clientId) => { + dispatch(appDetailRequestData({ id, clientId })) +} + +export const handleInstallationsSetFormState = (dispatch: Dispatch) => (formState: FormState) => { + dispatch(appInstallationsSetFormState(formState)) +} + +export const Client: React.FunctionComponent = () => { + const dispatch = useDispatch() + const history = useHistory() + const location = useLocation() + + const appSummaryState = useSelector(selectAppSummary) + const appDetail = useSelector(selectAppDetail) + const clientId = useSelector(selectClientId) + const installationsFormState = useSelector(selectInstallationFormState) + + const setStateViewBrowse = handleSetStateViewBrowse(dispatch) + const fetchAppDetail = handleFetchAppDetail(dispatch) + const installationsSetFormState = handleInstallationsSetFormState(dispatch) + + const urlParams = new URLSearchParams(location.search) + const page = Number(urlParams.get('page')) || 1 -export const Client: React.FunctionComponent = ({ - appSummaryState, - history, - location, - fetchAppDetail, - appDetail, - clientId, - setStateViewBrowse, - installationsFormState, - installationsSetFormState, -}) => { - const pageNumber = - !isNaN(Number(getParamValueFromPath(location.search, 'page'))) && - Number(getParamValueFromPath(location.search, 'page')) > 0 - ? Number(getParamValueFromPath(location.search, 'page')) - : 1 const hasParams = hasFilterParams(location.search) const unfetched = !appSummaryState.data const loading = appSummaryState.isAppSummaryLoading @@ -137,11 +137,11 @@ export const Client: React.FunctionComponent = ({ list={apps} loading={loading} onCardClick={handleOnCardClick({ setVisible, setStateViewBrowse, appDetail, fetchAppDetail, clientId })} - infoType={pageNumber > 1 || hasParams ? '' : 'CLIENT_APPS_EMPTY'} + infoType={page > 1 || hasParams ? '' : 'CLIENT_APPS_EMPTY'} pagination={{ totalCount, pageSize, - pageNumber, + pageNumber: page, onChange: handleOnChange(history), }} numOfColumn={3} @@ -154,17 +154,4 @@ export const Client: React.FunctionComponent = ({ ) } -export const mapStateToProps = (state: ReduxState): ClientMappedProps => ({ - appSummaryState: selectAppSummary(state), - appDetail: state.appDetail, - clientId: selectClientId(state), - installationsFormState: state.installations.formState, -}) - -export const mapDispatchToProps = (dispatch: any): ClientMappedActions => ({ - setStateViewBrowse: () => dispatch(setAppDetailModalStateBrowse()), - fetchAppDetail: (id: string, clientId: string) => dispatch(appDetailRequestData({ id, clientId })), - installationsSetFormState: (formState: FormState) => dispatch(appInstallationsSetFormState(formState)), -}) - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(Client)) +export default Client diff --git a/packages/marketplace/src/components/pages/client/index.ts b/packages/marketplace/src/components/pages/client/index.ts new file mode 100644 index 0000000000..f7f6b59060 --- /dev/null +++ b/packages/marketplace/src/components/pages/client/index.ts @@ -0,0 +1,2 @@ +import Client from './client' +export default Client diff --git a/packages/marketplace/src/selector/__tests__/client.ts b/packages/marketplace/src/selector/__tests__/client.ts index 5d3a7c3353..a04941d0f3 100644 --- a/packages/marketplace/src/selector/__tests__/client.ts +++ b/packages/marketplace/src/selector/__tests__/client.ts @@ -1,5 +1,5 @@ import { ReduxState } from '@/types/core' -import { selectClientId, selectLoggedUserEmail, selectFeaturedApps } from '../client' +import { selectClientId, selectLoggedUserEmail, selectFeaturedApps, selectAppDetail } from '../client' import { featuredAppsDataStub } from '@/sagas/__stubs__/apps' describe('selectClientId', () => { @@ -74,4 +74,12 @@ describe('selectFeaturedApps', () => { const result = selectFeaturedApps(input) expect(result).toEqual([]) }) + + it('should run correctly and return {}', () => { + const input = { + appDetail: {}, + } as ReduxState + const result = selectAppDetail(input) + expect(result).toEqual(input.appDetail) + }) }) diff --git a/packages/marketplace/src/selector/client.ts b/packages/marketplace/src/selector/client.ts index 90d70e11e7..b8f36d2824 100644 --- a/packages/marketplace/src/selector/client.ts +++ b/packages/marketplace/src/selector/client.ts @@ -15,3 +15,7 @@ export const selectAppSummary = (state: ReduxState) => { export const selectFeaturedApps = (state: ReduxState) => { return state?.client.appSummary.data?.featuredApps || [] } + +export const selectAppDetail = (state: ReduxState) => { + return state.appDetail +} diff --git a/packages/marketplace/src/tests/badges/badge-branches.svg b/packages/marketplace/src/tests/badges/badge-branches.svg index e661eac8b5..92faf57963 100644 --- a/packages/marketplace/src/tests/badges/badge-branches.svg +++ b/packages/marketplace/src/tests/badges/badge-branches.svg @@ -1 +1 @@ -Coverage:branchesCoverage:branches71.48%71.48% \ No newline at end of file +Coverage:branchesCoverage:branches71.48%71.48% diff --git a/packages/marketplace/src/tests/badges/badge-functions.svg b/packages/marketplace/src/tests/badges/badge-functions.svg index adb67294e2..740b37d533 100644 --- a/packages/marketplace/src/tests/badges/badge-functions.svg +++ b/packages/marketplace/src/tests/badges/badge-functions.svg @@ -1 +1 @@ -Coverage:functionsCoverage:functions78.55%78.55% \ No newline at end of file +Coverage:functionsCoverage:functions78.55%78.55% diff --git a/packages/marketplace/src/tests/badges/badge-lines.svg b/packages/marketplace/src/tests/badges/badge-lines.svg index a80382fa7b..8797d72721 100644 --- a/packages/marketplace/src/tests/badges/badge-lines.svg +++ b/packages/marketplace/src/tests/badges/badge-lines.svg @@ -1 +1 @@ -Coverage:linesCoverage:lines90.37%90.37% \ No newline at end of file +Coverage:linesCoverage:lines90.37%90.37% diff --git a/packages/marketplace/src/tests/badges/badge-statements.svg b/packages/marketplace/src/tests/badges/badge-statements.svg index 63bb81b25f..f0fc5fe7cd 100644 --- a/packages/marketplace/src/tests/badges/badge-statements.svg +++ b/packages/marketplace/src/tests/badges/badge-statements.svg @@ -1 +1 @@ -Coverage:statementsCoverage:statements89.58%89.58% \ No newline at end of file +Coverage:statementsCoverage:statements89.58%89.58%