Skip to content

Commit

Permalink
feat: e2e tests (#240)
Browse files Browse the repository at this point in the history
## 📜 Description

Added e2e-test 🤓 

## 💡 Motivation and Context

E2E tests are needed to assure quality of the package.

I decided to use detox for that (mainly because of the speed and
familiarity with it).

Also I added checks in CI. Everything is pretty standard for iOS. For
Android I added API 28 + AOSP image (because test-butler works only with
AOSP). I think later I'll try to integrate `google_apis` image, and
maybe matrix strategy to test across different API versions.

It was hard to test functionality of this library (because we don't have
`toHaveStyle` matchers, but even if we had them it also would be
difficult to test because of different keyboard sizes etc.). So I've
decided to go with UI snapshot approach - I take a screenshot in certain
cycle of test and compare it with original screenshot (thus I assure
there is no visual regressions).

Last but not least - all tests are written in TS 🤓 

## 📢 Changelog

### E2E
- added detox for e2e testing;
- added helpers;
- added tests for
`KeyboardAnimation`/`AwareScrollView`/`EnabledDisabled`

### CI
- added e2e tests run on iOS;
- added e2e tests run on Android;

### Example app

#### JS
- added `testID` for various elements;

#### Android
- updated `build.gradle`; 

## 🤔 How Has This Been Tested?

Tested locally and on CI.

## 📸 Screenshots (if appropriate):

|Android|iOS|
|--------|----|
|<video
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/9a35a72b-2b6e-40c2-8661-9c796714df70">|<video
src="https://github.com/kirillzyusko/react-native-keyboard-controller/assets/22820318/b45830ad-6ba1-49b7-82a7-9b86a61a0b85">|

## 📝 Checklist

- [x] CI successfully passed
  • Loading branch information
kirillzyusko committed Dec 1, 2023
1 parent d8a312d commit 35d0ada
Show file tree
Hide file tree
Showing 59 changed files with 3,638 additions and 15 deletions.
82 changes: 82 additions & 0 deletions .github/workflows/android-e2e-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: 🛠 Android e2e tests
on:
pull_request:
paths:
- '.github/workflows/android-e2e-test.yml'
- 'package.json'
- 'android/**'
- 'example/**'
- 'e2e/**'
- 'src/**'
push:
branches:
- main

jobs:
test:
name: ⚙️ Automated test cases
runs-on: macos-12
timeout-minutes: 60
env:
WORKING_DIRECTORY: example
concurrency:
group: android-e2e-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'yarn'
- name: Setup JDK 11
uses: actions/setup-java@v3
with:
distribution: 'microsoft'
java-version: '11'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Restore node_modules from cache
uses: actions/cache@v3
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install root dependencies
run: yarn install
- name: Install node_modules for example/
run: yarn install --frozen-lockfile --cwd example
- name: Install e2e dependencies
run: yarn install --cwd e2e
- name: Restore Gradle cache
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-e2e-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-e2e-gradle-
- name: Build app
working-directory: e2e
run: yarn build-example:android
- name: Run emulator and tests
uses: reactivecircus/android-emulator-runner@v2.28.0
with:
working-directory: e2e
api-level: 28
target: default
profile: pixel_2
ram-size: '4096M'
disk-size: '10G'
disable-animations: false
avd-name: e2e_emulator
arch: x86_64
script: yarn test-example:android
- uses: actions/upload-artifact@v3
if: ${{ failure() }}
with:
name: android-fail-screen-shoots
path: e2e/artifacts
1 change: 1 addition & 0 deletions .github/workflows/compress-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
# The `GITHUB_TOKEN` is automatically generated by GitHub and scoped only to the repository that is currently running the action. By default, the action can’t update Pull Requests initiated from forked repositories.
# See https://docs.github.com/en/actions/reference/authentication-in-a-workflow and https://help.github.com/en/articles/virtual-environments-for-github-actions#token-permissions
githubToken: ${{ secrets.GITHUB_TOKEN }}
ignorePaths: 'e2e/**'
jpegQuality: '80'
jpegProgressive: false
pngQuality: '80'
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/ios-e2e-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: 🛠 iOS e2e tests
on:
pull_request:
paths:
- '.github/workflows/ios-e2e-test.yml'
- 'react-native-keyboard-controller.podspec'
- 'package.json'
- 'ios/**'
- 'example/**'
- 'e2e/**'
- 'src/**'
push:
branches:
- main

jobs:
test:
name: ⚙️ Automated test cases
runs-on: macos-12
timeout-minutes: 60
env:
WORKING_DIRECTORY: example
concurrency:
group: ios-e2e-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: 'yarn'
- name: Get Xcode version
run: xcodebuild -version
- name: Install AppleSimulatorUtils
run: brew tap wix/brew && brew install applesimutils
- name: Install root dependencies
run: yarn install
- name: Install example project dependencies
working-directory: ${{ env.WORKING_DIRECTORY }}
run: yarn
- name: Install e2e dependencies
run: yarn install --cwd e2e
- name: Restore buildcache
uses: mikehardy/buildcache-action@v2
continue-on-error: true
with:
cache_key: react-native-keyboard-controller-e2e
- name: Restore Pods cache
uses: actions/cache@v3
with:
path: |
example/ios/Pods
~/Library/Caches/CocoaPods
~/.cocoapods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: |
${{ runner.os }}-pods-
- name: Install pods
working-directory: ${{ env.WORKING_DIRECTORY }}/ios
run: pod install
- name: Build app
working-directory: e2e
run: yarn build-example:ios
- name: Use software keyboard
run: defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool NO
- name: Test app
working-directory: e2e
run: yarn test-example:ios
- uses: actions/upload-artifact@v3
if: ${{ failure() }}
with:
name: ios-fail-screen-shoots
path: e2e/artifacts
1 change: 1 addition & 0 deletions FabricExample/src/components/KeyboardAnimation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export default function KeyboardAnimation() {
</View>
<View>
<TextInput
testID="keyboard_animation_text_input"
style={{
width: 200,
marginTop: 50,
Expand Down
1 change: 1 addition & 0 deletions FabricExample/src/components/TextInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const TextInput = (props: TextInputProps) => {
style={styles.container}
multiline
numberOfLines={10}
testID={props.placeholder}
{...props}
placeholder={`${props.placeholder} (${props.keyboardType === 'default' ? 'text' : 'numeric'})`}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default function AwareScrollView() {
useResizeMode();

return (
<KeyboardAwareScrollView bottomOffset={50} style={styles.container} contentContainerStyle={styles.content}>
<KeyboardAwareScrollView testID='aware_scroll_view_container' bottomOffset={50} style={styles.container} contentContainerStyle={styles.content}>
{new Array(10).fill(0).map((_, i) => (
<TextInput
key={i}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default function EnabledDisabled() {
<Button
title={enabled ? 'Enabled' : 'Disabled'}
onPress={() => setEnabled(!enabled)}
testID='toggle_button'
/>
<KeyboardAnimationTemplate />
</View>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@ type Props = {
} & Example;

const ExampleLink: FC<Props> = (props) => {
const { onPress, title, info, icons, index } = props;
const { onPress, title, testID, info, icons, index } = props;

const onCardPress = useCallback(() => onPress(info), [onPress]);

return (
<TouchableOpacity onPress={onCardPress} style={styles.container}>
<TouchableOpacity
testID={testID}
onPress={onCardPress}
style={styles.container}
>
<View style={styles.row}>
<Text>
{index}. {title}
Expand Down
27 changes: 24 additions & 3 deletions FabricExample/src/screens/Examples/Main/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,80 @@ import type { Example } from './types';
export const examples: Example[] = [
{
title: 'Animated transition',
testID: 'animated_transition',
info: ScreenNames.ANIMATED_EXAMPLE,
icons: '😍⌨️',
},
{ title: 'Chat', info: ScreenNames.REANIMATED_CHAT, icons: '💬' },
{ title: 'Events', info: ScreenNames.EVENTS, icons: '🎃 📅' },
{
title: 'Chat',
testID: 'reanimated_chat',
info: ScreenNames.REANIMATED_CHAT,
icons: '💬',
},
{
title: 'Events',
testID: 'events',
info: ScreenNames.EVENTS,
icons: '🎃 📅',
},
{
title: 'Aware scroll view',
testID: 'aware_scroll_view',
info: ScreenNames.AWARE_SCROLL_VIEW,
icons: '🤓',
},
{
title: 'Aware scroll view sticky footer',
testID: 'aware_scroll_view_sticky_footer',
info: ScreenNames.AWARE_SCROLL_VIEW_STICKY_FOOTER,
icons: '🤓🩹',
},
{
title: 'Status Bar',
testID: 'status_bar',
info: ScreenNames.STATUS_BAR,
icons: '🔋',
},
{
title: 'Lottie',
testID: 'lottie',
info: ScreenNames.LOTTIE,
icons: '⚠️ 🎞',
},
{
title: 'Non UI Props',
testID: 'non_ui_props',
info: ScreenNames.NON_UI_PROPS,
icons: '🚀',
},
{
title: 'Interactive keyboard 🤖',
testID: 'interactive_keyboard_android',
info: ScreenNames.INTERACTIVE_KEYBOARD,
icons: '👆📱',
},
{
title: 'Interactive keyboard 🍏',
testID: 'interactive_keyboard_ios',
info: ScreenNames.INTERACTIVE_KEYBOARD_IOS,
icons: '👆📱',
},
{
title: 'Native stack',
testID: 'native_stack',
info: ScreenNames.NATIVE_STACK,
icons: '⚛️',
},
{
title: 'KeyboardAvoidingView',
testID: 'keyboard_avoiding_view',
info: ScreenNames.KEYBOARD_AVOIDING_VIEW,
icons: '😶',
},
{
title: 'Enabled/disabled',
testID: 'enabled_disabled',
info: ScreenNames.ENABLED_DISABLED,
icons: '💡',
},
];
];
2 changes: 1 addition & 1 deletion FabricExample/src/screens/Examples/Main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const ExampleMain = ({ navigation }: Props) => {
);

return (
<ScrollView contentContainerStyle={styles.scrollViewContainer}>
<ScrollView testID='main_scroll_view' contentContainerStyle={styles.scrollViewContainer}>
{examples.map((example, index) => (
<ExampleLink
key={example.title}
Expand Down
1 change: 1 addition & 0 deletions FabricExample/src/screens/Examples/Main/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ScreenNames } from '../../../constants/screenNames';

export type Example = {
testID: string;
title: string;
info: ScreenNames;
icons: string;
Expand Down
Loading

0 comments on commit 35d0ada

Please sign in to comment.