-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(in-app-messaging): add unit testing for component util hooks (#…
- Loading branch information
1 parent
591c40a
commit 0a86073
Showing
22 changed files
with
1,579 additions
and
105 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,13 @@ | ||
module.exports = { | ||
preset: 'react-native', | ||
modulePathIgnorePatterns: ['<rootDir>/dist/'], | ||
collectCoverageFrom: ['<rootDir>/src/**/*.{js,jsx,ts,tsx}', '!<rootDir>/src/**/*{c,C}onstants.ts'], | ||
coverageThreshold: { | ||
global: { | ||
branches: 16, | ||
functions: 9, | ||
lines: 16, | ||
statements: 16, | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
...eact-native/src/InAppMessaging/components/hooks/useMessage/__tests__/handleAction.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* | ||
* Copyright 2017-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with | ||
* the License. A copy of the License is located at | ||
* | ||
* http://aws.amazon.com/apache2.0/ | ||
* | ||
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR | ||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions | ||
* and limitations under the License. | ||
*/ | ||
|
||
import { Linking } from 'react-native'; | ||
import { ConsoleLogger as Logger } from '@aws-amplify/core'; | ||
import { InAppMessageAction } from '@aws-amplify/notifications'; | ||
|
||
import handleAction from '../handleAction'; | ||
|
||
jest.mock('react-native', () => ({ | ||
Linking: { | ||
canOpenURL: jest.fn(), | ||
openURL: jest.fn(), | ||
}, | ||
})); | ||
|
||
const logger = new Logger('TEST_LOGGER'); | ||
|
||
const deepLink = 'DEEP_LINK'; | ||
const link = 'LINK'; | ||
const url = 'https://docs.amplify.aws/'; | ||
|
||
const error = 'ERROR'; | ||
|
||
describe('handleAction', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it.each([deepLink, link])('handles a %s action as expected in the happy path', async (action) => { | ||
(Linking.canOpenURL as jest.Mock).mockResolvedValueOnce(true); | ||
|
||
await handleAction(action as InAppMessageAction, url); | ||
|
||
expect(logger.info).toHaveBeenCalledWith(`Handle action: ${action}`); | ||
expect(Linking.canOpenURL).toHaveBeenCalledWith(url); | ||
expect(logger.info).toHaveBeenCalledWith(`Opening url: ${url}`); | ||
expect(Linking.openURL).toHaveBeenCalledWith(url); | ||
expect(logger.info).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it.each([deepLink, link])( | ||
'logs a warning and early returns when a %s action is provided with a null url value', | ||
async (action) => { | ||
const invalidUrl = null; | ||
|
||
await handleAction(action as InAppMessageAction, invalidUrl); | ||
|
||
expect(logger.info).toHaveBeenCalledWith(`Handle action: ${action}`); | ||
expect(logger.warn).toHaveBeenCalledWith(`url must be of type string: ${invalidUrl}`); | ||
expect(logger.info).toHaveBeenCalledTimes(1); | ||
expect(logger.warn).toHaveBeenCalledTimes(1); | ||
expect(Linking.canOpenURL).not.toHaveBeenCalled(); | ||
} | ||
); | ||
|
||
it.each([deepLink, link])( | ||
'logs a warning and early returns when a %s action is provided with an undefined url value', | ||
async (action) => { | ||
const invalidUrl = undefined; | ||
|
||
await handleAction(action as InAppMessageAction, invalidUrl); | ||
|
||
expect(logger.info).toHaveBeenCalledWith(`Handle action: ${action}`); | ||
expect(logger.warn).toHaveBeenCalledWith(`url must be of type string: ${invalidUrl}`); | ||
expect(logger.info).toHaveBeenCalledTimes(1); | ||
expect(logger.warn).toHaveBeenCalledTimes(1); | ||
expect(Linking.canOpenURL).not.toHaveBeenCalled(); | ||
} | ||
); | ||
|
||
it('logs a warning when Linking.canOpenUrl returns false', async () => { | ||
(Linking.canOpenURL as jest.Mock).mockResolvedValueOnce(false); | ||
|
||
await handleAction(link, url); | ||
|
||
expect(logger.info).toHaveBeenCalledWith(`Handle action: ${link}`); | ||
expect(Linking.canOpenURL).toHaveBeenCalledTimes(1); | ||
expect(logger.warn).toHaveBeenCalledWith(`Unsupported url provided: ${url}`); | ||
expect(Linking.openURL).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('logs an error when Linking.canOpenUrl fails', async () => { | ||
(Linking.canOpenURL as jest.Mock).mockRejectedValueOnce(error); | ||
|
||
await handleAction(link, url); | ||
|
||
expect(logger.info).toHaveBeenCalledWith(`Handle action: ${link}`); | ||
expect(logger.error).toHaveBeenCalledWith(`Call to Linking.canOpenURL failed: ${error}`); | ||
}); | ||
|
||
it('logs an error when Linking.openUrl fails', async () => { | ||
(Linking.canOpenURL as jest.Mock).mockResolvedValueOnce(true); | ||
(Linking.openURL as jest.Mock).mockRejectedValue(error); | ||
|
||
await handleAction(link, url); | ||
|
||
expect(logger.info).toHaveBeenCalledWith(`Handle action: ${link}`); | ||
expect(logger.error).toHaveBeenCalledWith(`Call to Linking.openURL failed: ${error}`); | ||
}); | ||
}); |
218 changes: 218 additions & 0 deletions
218
...-react-native/src/InAppMessaging/components/hooks/useMessage/__tests__/useMessage.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
/* | ||
* Copyright 2017-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with | ||
* the License. A copy of the License is located at | ||
* | ||
* http://aws.amazon.com/apache2.0/ | ||
* | ||
* or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR | ||
* CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions | ||
* and limitations under the License. | ||
*/ | ||
|
||
import { InAppMessage, InAppMessageInteractionEvent, Notifications } from '@aws-amplify/notifications'; | ||
import { ConsoleLogger as Logger } from '@aws-amplify/core'; | ||
|
||
import useInAppMessaging from '../../../../hooks/useInAppMessaging'; | ||
import BannerMessage from '../../../BannerMessage'; | ||
import CarouselMessage from '../../../CarouselMessage'; | ||
import FullScreenMessage from '../../../FullScreenMessage'; | ||
import ModalMessage from '../../../ModalMessage'; | ||
import { InAppMessageComponentBaseProps } from '../../../types'; | ||
|
||
import useMessage from '../useMessage'; | ||
|
||
jest.mock('@aws-amplify/notifications', () => ({ | ||
...jest.requireActual('@aws-amplify/notifications'), | ||
Notifications: { InAppMessaging: { notifyMessageInteraction: jest.fn() } }, | ||
})); | ||
|
||
jest.mock('../../../../hooks/useInAppMessaging', () => ({ | ||
__esModule: true, | ||
default: jest.fn(), | ||
})); | ||
|
||
jest.useFakeTimers(); | ||
|
||
const logger = new Logger('TEST_LOGGER'); | ||
|
||
const mockUseInAppMessaging = useInAppMessaging as jest.Mock; | ||
const mockClearInAppMessage = jest.fn(); | ||
|
||
const header = { content: 'header one' }; | ||
const baseInAppMessage: Partial<InAppMessage> = { id: 'test', content: [{ header }] }; | ||
const carouselInAppMessage: Partial<InAppMessage> = { | ||
id: 'carousel', | ||
content: [{ header }, { header: { content: 'header two' } }], | ||
layout: 'CAROUSEL', | ||
}; | ||
|
||
function CustomBannerMessage() { | ||
return null; | ||
} | ||
function CustomCarouselMessage() { | ||
return null; | ||
} | ||
function CustomFullScreenMessage() { | ||
return null; | ||
} | ||
function CustomModalMessage() { | ||
return null; | ||
} | ||
|
||
describe('useMessage', () => { | ||
beforeEach(() => { | ||
(logger.info as jest.Mock).mockClear(); | ||
}); | ||
|
||
// happy path test for banner and full screen layouts | ||
it.each([ | ||
['BOTTOM_BANNER', BannerMessage, { position: 'bottom' }], | ||
['FULL_SCREEN', FullScreenMessage, null], | ||
['MIDDLE_BANNER', BannerMessage, { position: 'middle' }], | ||
['TOP_BANNER', BannerMessage, { position: 'top' }], | ||
['MODAL', ModalMessage, null], | ||
])('returns the expected values of Component and props for a %s layout', (layout, layoutComponent, layoutProps) => { | ||
mockUseInAppMessaging.mockReturnValueOnce({ components: {}, inAppMessage: { ...baseInAppMessage, layout } }); | ||
const { Component, props } = useMessage(); | ||
|
||
expect(Component).toBe(layoutComponent); | ||
expect(props).toEqual( | ||
expect.objectContaining({ | ||
...layoutProps, | ||
header, | ||
layout, | ||
onClose: expect.any(Function) as InAppMessageComponentBaseProps['onClose'], | ||
onDisplay: expect.any(Function) as InAppMessageComponentBaseProps['onDisplay'], | ||
style: undefined, | ||
}) | ||
); | ||
}); | ||
|
||
it('returns the expected values of Component and props for a CAROUSEL layout', () => { | ||
mockUseInAppMessaging.mockReturnValueOnce({ components: {}, inAppMessage: carouselInAppMessage }); | ||
|
||
const { Component, props } = useMessage(); | ||
|
||
expect(Component).toBe(CarouselMessage); | ||
expect(props).toEqual( | ||
expect.objectContaining({ | ||
data: [{ header }, { header: { content: 'header two' } }], | ||
layout: 'CAROUSEL', | ||
onClose: expect.any(Function) as InAppMessageComponentBaseProps['onClose'], | ||
onDisplay: expect.any(Function) as InAppMessageComponentBaseProps['onDisplay'], | ||
style: undefined, | ||
}) | ||
); | ||
}); | ||
|
||
it.each([ | ||
['BannerMessage', 'BOTTOM_BANNER', CustomBannerMessage], | ||
['BannerMessage', 'MIDDLE_BANNER', CustomBannerMessage], | ||
['BannerMessage', 'TOP_BANNER', CustomBannerMessage], | ||
['CarouselMessage', 'CAROUSEL', CustomCarouselMessage], | ||
['FullScreenMessage', 'FULL_SCREEN', CustomFullScreenMessage], | ||
['ModalMessage', 'MODAL', CustomModalMessage], | ||
])( | ||
'returns a custom %s component for a %s layout in place of the default component when provided', | ||
(componentKey, layout, CustomComponent) => { | ||
mockUseInAppMessaging.mockReturnValueOnce({ | ||
components: { [componentKey]: CustomComponent }, | ||
inAppMessage: { layout }, | ||
}); | ||
|
||
const { Component } = useMessage(); | ||
|
||
expect(Component).toBe(CustomComponent); | ||
} | ||
); | ||
|
||
it('returns null values for Component and props when inAppMessage is null', () => { | ||
mockUseInAppMessaging.mockReturnValueOnce({ components: {}, inAppMessage: null }); | ||
|
||
const { Component, props } = useMessage(); | ||
|
||
expect(Component).toBeNull(); | ||
expect(props).toBeNull(); | ||
}); | ||
|
||
it('returns null values for Component and props when inAppMessage.layout is not supported', () => { | ||
const layout = 'NOT_A_SUPPORTED_LAYOUT'; | ||
mockUseInAppMessaging.mockReturnValueOnce({ | ||
components: {}, | ||
inAppMessage: { layout }, | ||
}); | ||
|
||
const { Component, props } = useMessage(); | ||
|
||
expect(logger.info).toHaveBeenCalledWith(`Received unknown InAppMessage layout: ${layout}`); | ||
expect(logger.info).toHaveBeenCalledTimes(1); | ||
expect(Component).toBeNull(); | ||
expect(props).toBeNull(); | ||
}); | ||
|
||
describe('event handling', () => { | ||
const inAppMessage = { | ||
content: [{ primaryButton: { action: 'CLOSE', title: 'primary' } }], | ||
layout: 'TOP_BANNER', | ||
}; | ||
|
||
beforeEach(() => { | ||
mockUseInAppMessaging.mockReturnValueOnce({ | ||
clearInAppMessage: mockClearInAppMessage, | ||
components: {}, | ||
inAppMessage, | ||
}); | ||
|
||
mockClearInAppMessage.mockClear(); | ||
(Notifications.InAppMessaging.notifyMessageInteraction as jest.Mock).mockClear(); | ||
}); | ||
|
||
describe('onClose', () => { | ||
it('calls the expected methods', () => { | ||
const { props } = useMessage(); | ||
|
||
props.onClose(); | ||
|
||
expect(Notifications.InAppMessaging.notifyMessageInteraction).toHaveBeenCalledTimes(1); | ||
expect(Notifications.InAppMessaging.notifyMessageInteraction).toHaveBeenCalledWith( | ||
inAppMessage, | ||
InAppMessageInteractionEvent.MESSAGE_DISMISSED | ||
); | ||
expect(mockClearInAppMessage).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
|
||
describe('onDisplay', () => { | ||
it('calls the expected methods', () => { | ||
const { props } = useMessage(); | ||
|
||
props.onDisplay(); | ||
|
||
expect(Notifications.InAppMessaging.notifyMessageInteraction).toHaveBeenCalledTimes(1); | ||
expect(Notifications.InAppMessaging.notifyMessageInteraction).toHaveBeenCalledWith( | ||
inAppMessage, | ||
InAppMessageInteractionEvent.MESSAGE_DISPLAYED | ||
); | ||
}); | ||
}); | ||
|
||
describe('onActionCallback', () => { | ||
it('calls the expected methods via the onPress function of the primary button', () => { | ||
const { props } = useMessage(); | ||
|
||
props.primaryButton.onPress(); | ||
|
||
jest.runAllTimers(); | ||
|
||
expect(Notifications.InAppMessaging.notifyMessageInteraction).toHaveBeenCalledTimes(1); | ||
expect(Notifications.InAppMessaging.notifyMessageInteraction).toHaveBeenCalledWith( | ||
inAppMessage, | ||
InAppMessageInteractionEvent.MESSAGE_ACTION_TAKEN | ||
); | ||
expect(mockClearInAppMessage).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.