Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e/AndroidUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const utils = {
'pm revoke com.reactnativenavigation.playground android.permission.READ_PHONE_STATE'
),
executeShellCommand: (command) => {
// TODO Change to use Detox's ADB (see keyboard driver)
exec.execSync(`adb -s ${device.id} shell ${command}`);
},
setDemoMode: () => {
Expand Down
35 changes: 31 additions & 4 deletions e2e/Keyboard.test.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
import { default as TestIDs, default as testIDs } from '../playground/src/testIDs';
import Android from './AndroidUtils';
import { device } from 'detox';
import Utils from './Utils';
import kbdDriver from './drivers/androidKeyboard';

const { elementByLabel, elementById } = Utils;
const { elementByLabel, elementById, sleep } = Utils;

const KBD_OBSCURED_TEXT = 'Keyboard Demo';

describe.e2e('Keyboard', () => {
beforeAll(async () => {
await kbdDriver.init();
await kbdDriver.enableOnScreenKeyboard();

if (device.getPlatform() === 'android') {
// 1st-time Android keyboard appearance is laggy (Android's lazy init?)
await device.launchApp({ newInstance: true });
await elementById(TestIDs.KEYBOARD_SCREEN_BTN).tap();
await elementById(TestIDs.TEXT_INPUT1).tap();
await sleep(2000);
}
});

afterAll(async () => {
await kbdDriver.restoreOnScreenKeyboard();
});

beforeEach(async () => {
await device.launchApp({ newInstance: true });
await elementById(TestIDs.KEYBOARD_SCREEN_BTN).tap();
});

it('Push - should close keyboard when Back clicked', async () => {
await expect(elementByLabel(KBD_OBSCURED_TEXT)).toBeVisible();
await elementById(TestIDs.TEXT_INPUT1).tap();
await expect(elementByLabel('Keyboard Demo')).not.toBeVisible();
await expect(elementByLabel(KBD_OBSCURED_TEXT)).not.toBeVisible();
await elementById(TestIDs.BACK_BUTTON).tap();
await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible();
});

it('Modal - should close keyboard when close clicked', async () => {
await elementById(TestIDs.MODAL_BTN).tap();
await elementById(TestIDs.TEXT_INPUT1).tap();
await expect(elementByLabel('Keyboard Demo')).not.toBeVisible();
await expect(elementByLabel(KBD_OBSCURED_TEXT)).not.toBeVisible();
await elementById(TestIDs.DISMISS_MODAL_TOPBAR_BTN).tap();
await expect(elementById(testIDs.MAIN_BOTTOM_TABS)).toBeVisible();
});
Expand All @@ -46,4 +67,10 @@ describe.e2e('Keyboard', () => {
await elementById(TestIDs.MODAL_BTN).tap();
await expect(elementById(TestIDs.TEXT_INPUT1)).not.toBeFocused();
});

it(':android: should respect UI with keyboard awareness', async () => {
await elementById(TestIDs.PUSH_KEYBOARD_SCREEN_STICKY_FOOTER).tap();
await elementById(TestIDs.TEXT_INPUT2).tap();
await expect(elementByLabel(KBD_OBSCURED_TEXT)).toBeVisible();
});
});
29 changes: 26 additions & 3 deletions e2e/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ function convertToSSIMFormat(image) {
data: new Uint8ClampedArray(image.data),
width: image.width,
height: image.height
}
;
};
}

function loadImage(path) {
Expand All @@ -34,6 +33,29 @@ function bitmapDiff(imagePath, expectedImagePath, ssimThreshold = SSIM_SCORE_THR
}
}

const sleep = (ms) =>
new Promise((res) => setTimeout(res, ms));

/**
* @param tries Total tries to attempt (retries + 1)
* @param delay Delay between retries, in milliseconds
* @param {Function<Promise<Boolean>>} func
* @returns {Promise<void>}
* @throws {Error} if the function fails after all retries
*/
async function retry({ tries = 3, delay = 1000 }, func) {
for (let i = 0; i < tries; i++) {
const result = await func();
if (result) {
return;
}

await sleep(delay);
}

throw new Error(`Failed even after ${tries} attempts`);
}

const utils = {
elementByLabel: (label) => {
// uncomment for running tests with rn's new arch
Expand All @@ -58,7 +80,8 @@ const utils = {
return element(by.type('_UIModernBarButton').and(by.label('Back'))).tap();
}
},
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
sleep,
retry,
expectImagesToBeEqual: (imagePath, expectedImagePath) => {
bitmapDiff(imagePath, expectedImagePath);

Expand Down
66 changes: 66 additions & 0 deletions e2e/drivers/androidKeyboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { device } from 'detox';
import Utils from '../Utils';

const { retry, sleep } = Utils;

const androidKbdDriver = {
async init() {
if (device.getPlatform() !== 'android') {
return;
}

await this._initADB();
await this._initKbdState();
},

async enableOnScreenKeyboard() {
if (!this.adb) {
// Not initialized (iOS?)
return;
}
await this._setOnscreenKeyboard(true);
},

async restoreOnScreenKeyboard() {
if (!this.adb) {
// Not initialized (iOS?)
return;
}
await this._setOnscreenKeyboard(this.kbdEnabled);
},

_initADB() {
const { id: adbName } = device;
const { adb } = device.deviceDriver;

if (!adb || !adbName) {
throw new Error(`Keyboard driver init failed (id=${adbName}, hasADB=${!!adb})`);
}

this.adb = adb;
this.adbName = adbName;
},

async _initKbdState() {
this.kbdEnabled = await this.adb.shell(this.adbName, 'settings get Secure show_ime_with_hard_keyboard');

if (!(this.kbdEnabled === '0' || this.kbdEnabled === '1')) {
console.warn('[KbdDriver] Unable to get on-screen KBD setting, defaulting to false');
this.kbdEnabled = '0';
}
},

async _setOnscreenKeyboard(_value) {
const value = (!!Number(_value) ? '1' : '0');

await retry( { tries: 10 }, async () => {
await this.adb.shell(this.adbName, `settings put Secure show_ime_with_hard_keyboard ${value}`);
await sleep(1000);

const result = await this.adb.shell(this.adbName, 'settings get Secure show_ime_with_hard_keyboard');
return result === value;
});
}
}

module.exports = androidKbdDriver;
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,12 @@ public Animator getPopAnimation(Options appearingOptions, Options disappearingOp
@Override
protected WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) {
Insets sysInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars());
view.setPaddingRelative(0, 0, 0, sysInsets.bottom);
return WindowInsetsCompat.CONSUMED;
}
Insets imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime());

int bottomInset = (imeInsets.bottom > 0) ? 0 : sysInsets.bottom;
view.setPaddingRelative(0, 0, 0, bottomInset);
return insets;
}

@RestrictTo(RestrictTo.Scope.TESTS)
public BottomTabs getBottomTabs() {
Expand Down
62 changes: 45 additions & 17 deletions playground/src/screens/KeyboardScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import React from 'react';
import { View, ScrollView, Dimensions, StyleSheet, Image, TextInput, Text } from 'react-native';
import {
SafeAreaView,
View,
ScrollView,
Dimensions,
StyleSheet,
Image,
TextInput,
Text,
KeyboardAvoidingView,
} from 'react-native';
import {
NavigationProps,
NavigationComponent,
Expand All @@ -15,6 +25,7 @@ const KEYBOARD_LABEL = 'Keyboard Demo';
interface Props extends NavigationProps {
title?: string;
autoFocus?: boolean;
stickyFooter?: boolean;
}

export default class KeyboardScreen extends NavigationComponent<Props> {
Expand All @@ -34,6 +45,7 @@ export default class KeyboardScreen extends NavigationComponent<Props> {
},
};
}

constructor(props: Props) {
super(props);
Navigation.events().bindComponent(this);
Expand All @@ -50,30 +62,41 @@ export default class KeyboardScreen extends NavigationComponent<Props> {
}

render() {
const FooterRoot = this.props.stickyFooter === true ? KeyboardAvoidingView : View;
return (
<View style={styles.root}>
<SafeAreaView style={styles.root}>
<ScrollView>
<Image style={styles.image} source={require('../../img/2048.jpeg')} />
<View style={{ alignItems: 'center' }}>
<Button
style={styles.button}
label={'Modal Keyboard Screen'}
label={'Modal screen'}
testID={testIDs.MODAL_BTN}
onPress={async () => {
await this.openModalKeyboard(undefined);
}}
/>
<View style={styles.row}>
<Button
style={styles.button}
label={'Push screen w/ focus'}
testID={testIDs.PUSH_FOCUSED_KEYBOARD_SCREEN}
onPress={async () => {
await this.openPushedKeyboard(undefined, true);
}}
/>
<Button
style={styles.button}
label={'w/ sticky-footer'}
testID={testIDs.PUSH_KEYBOARD_SCREEN_STICKY_FOOTER}
onPress={async () => {
await this.openPushedKeyboard(undefined, undefined, true);
}}
/>
</View>
<Button
style={styles.button}
label={'Push Focused Keyboard Screen'}
testID={testIDs.PUSH_FOCUSED_KEYBOARD_SCREEN}
onPress={async () => {
await this.openPushedKeyboard(undefined, true);
}}
/>
<Button
style={styles.button}
label={'Show Focused Keyboard Screen Modal'}
label={'Modal screen w/ focus'}
testID={testIDs.MODAL_FOCUSED_KEYBOARD_SCREEN}
onPress={async () => {
await this.openModalKeyboard(undefined, true);
Expand Down Expand Up @@ -104,20 +127,21 @@ export default class KeyboardScreen extends NavigationComponent<Props> {
/>
</View>
</ScrollView>
<View style={styles.footer}>
<Text style={styles.input}> {KEYBOARD_LABEL}</Text>
</View>
</View>
<FooterRoot behavior="height" style={styles.footer}>
<Text style={styles.input}>{KEYBOARD_LABEL}</Text>
</FooterRoot>
</SafeAreaView>
);
}

openPushedKeyboard = async (text?: string, autoFocus?: boolean) => {
openPushedKeyboard = async (text?: string, autoFocus?: boolean, stickyFooter?: boolean) => {
await Navigation.push(this.props.componentId, {
component: {
name: Screens.KeyboardScreen,
passProps: {
title: text,
autoFocus,
stickyFooter,
},
},
});
Expand Down Expand Up @@ -169,4 +193,8 @@ const styles = StyleSheet.create({
width: screenWidth,
resizeMode: 'cover',
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
});
1 change: 1 addition & 0 deletions playground/src/testIDs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const testIDs = {
MODAL_DISABLED_BACK_BTN: 'SHOW_MODAL_DISABLED_BACK_BTN',
PUSH_FOCUSED_KEYBOARD_SCREEN: 'PUSH_FOCUSED_KEYBOARD_SCREEN',
MODAL_FOCUSED_KEYBOARD_SCREEN: 'MODAL_FOCUSED_KEYBOARD_SCREEN',
PUSH_KEYBOARD_SCREEN_STICKY_FOOTER: 'PUSH_KEYBOARD_SCREEN_STICKY_FOOTER',
PAGE_SHEET_MODAL_BTN: 'SHOW_PAGE_SHEET_MODAL_BUTTON',
DISMISS_MODAL_BTN: 'DISMISS_MODAL_BUTTON',
DISMISS_REACT_MODAL_BTN: 'DISMISS_REACT_MODAL_BUTTON',
Expand Down