Skip to content

Commit

Permalink
feat(android): support setDatePickerDate (wix#3790)
Browse files Browse the repository at this point in the history
  • Loading branch information
mauricedoepke authored Feb 6, 2023
1 parent c2ccb1e commit d34546a
Show file tree
Hide file tree
Showing 20 changed files with 324 additions and 49 deletions.
4 changes: 4 additions & 0 deletions detox/android/detox/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ dependencies {
api('androidx.test.espresso:espresso-web:3.4.0') {
because 'Web-View testing'
}
api('androidx.test.espresso:espresso-contrib:3.4.0') {
because 'Android datepicker support'
exclude group: "org.checkerframework", module: "checker"
}
api('androidx.test:rules:1.4.0') {
because 'of ActivityTestRule. Needed by users *and* internally used by Detox.'
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.wix.detox.espresso;

import android.view.View;
import android.os.Build;

import com.wix.detox.common.DetoxErrors.DetoxRuntimeException;
import com.wix.detox.common.DetoxErrors.StaleActionException;
Expand All @@ -19,17 +20,24 @@
import com.wix.detox.espresso.scroll.SwipeHelper;

import org.hamcrest.Matcher;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;

import androidx.test.espresso.UiController;
import androidx.test.espresso.ViewAction;
import androidx.test.espresso.action.CoordinatesProvider;
import androidx.test.espresso.action.GeneralClickAction;
import androidx.test.espresso.action.GeneralLocation;
import androidx.test.espresso.action.Press;
import androidx.test.espresso.contrib.PickerActions;

import static androidx.test.espresso.action.ViewActions.actionWithAssertions;
import static androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;

import static org.hamcrest.Matchers.allOf;


Expand All @@ -39,6 +47,8 @@

public class DetoxAction {
private static final String LOG_TAG = "detox";
private static final String ISO8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
private static final String ISO8601_FORMAT_NO_TZ = "yyyy-MM-dd'T'HH:mm:ss";

private DetoxAction() {
// static class
Expand Down Expand Up @@ -149,6 +159,19 @@ public static ViewAction scrollToIndex(int index) {
return new ScrollToIndexAction(index);
}

public static ViewAction setDatePickerDate(String dateString, String formatString) throws ParseException {
Date date;
if (formatString.equals("ISO8601")) {
date = parseDateISO8601(dateString);
} else {
date = new SimpleDateFormat(formatString).parse(dateString);
}

Calendar cal = Calendar.getInstance();
cal.setTime(date);
return PickerActions.setDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1, cal.get(Calendar.DAY_OF_MONTH));
}

public static ViewAction adjustSliderToPosition(final double newPosition) {
return new AdjustSliderToPositionAction(newPosition);
}
Expand Down Expand Up @@ -179,4 +202,12 @@ public String getResult() {
}
};
}

private static Date parseDateISO8601(String dateString) throws ParseException {
try {
return new SimpleDateFormat(ISO8601_FORMAT).parse(dateString);
} catch (ParseException e) {
return new SimpleDateFormat(ISO8601_FORMAT_NO_TZ).parse(dateString);
}
}
}
13 changes: 8 additions & 5 deletions detox/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1365,12 +1365,15 @@ declare global {
setColumnToValue(column: number, value: string): Promise<void>;

/**
* Sets the date of a date picker to a date generated from the provided string and date format. (iOS only)
* @param dateString string representing a date in the supplied `dateFormat`
* @param dateFormat format for the `dateString` supplied
* Sets the date of a date-picker according to the specified date-string and format.
* @param dateString Textual representation of a date (e.g. '2023/01/01'). Should be in coherence with the format specified by `dateFormat`.
* @param dateFormat Format of `dateString`: Generally either 'ISO8601' or an explicitly specified format (e.g. 'yyyy/MM/dd'); It should
* follow the rules of NSDateFormatter for iOS and DateTimeFormatter for Android.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString
* @example
* await expect(element(by.id('datePicker'))).toBeVisible();
* await element(by.id('datePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00', "yyyy-MM-dd'T'HH:mm:ssZZZZZ");
* await element(by.id('datePicker')).setDatePickerDate('2023-01-01T00:00:00Z', 'ISO8601');
* await element(by.id('datePicker')).setDatePickerDate(new Date().toISOString(), 'ISO8601');
* await element(by.id('datePicker')).setDatePickerDate('2023/01/01', 'yyyy/MM/dd');
*/
setDatePickerDate(dateString: string, dateFormat: string): Promise<void>;

Expand Down
11 changes: 11 additions & 0 deletions detox/src/android/AndroidExpect.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,17 @@ describe('AndroidExpect', () => {
await expectToThrow(() => e.element(e.by.id('ScrollView161')).scrollTo('noDirection'));
});

it('should setDatePickerDate', async () => {
await e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00', 'ISO8601');
await e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019/02/06', 'YYYY/MM/DD');
});

it('should not setDatePickerDate given bad args', async () => {
await expectToThrow(() => e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019-02-06', 'yyyy-mm-dd'));
await expectToThrow(() => e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate('2019-02-06T05:10:00-08:00'));
await expectToThrow(() => e.element(e.by.type('android.widget.DatePicker')).setDatePickerDate(2019, 'ISO8601'));
});

it('should swipe', async () => {
await e.element(e.by.id('ScrollView799')).swipe('down');
await e.element(e.by.id('ScrollView799')).swipe('down', 'fast');
Expand Down
8 changes: 8 additions & 0 deletions detox/src/android/actions/native.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ class ScrollToIndex extends Action {
}
}

class SetDatePickerDateAction extends Action {
constructor(dateString, formatString) {
super();
this._call = invoke.callDirectly(DetoxActionApi.setDatePickerDate(dateString, formatString));
}
}

class AdjustSliderToPosition extends Action {
constructor(newPosition) {
super();
Expand Down Expand Up @@ -155,5 +162,6 @@ module.exports = {
SwipeAction,
TakeElementScreenshot,
ScrollToIndex,
SetDatePickerDateAction,
AdjustSliderToPosition,
};
11 changes: 11 additions & 0 deletions detox/src/android/core/NativeElement.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const tempfile = require('tempfile');

const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
const invoke = require('../../invoke');
const { removeMilliseconds } = require('../../utils/dateUtils');
const { actionDescription } = require('../../utils/invocationTraceDescriptions');
const actions = require('../actions/native');
const DetoxMatcherApi = require('../espressoapi/DetoxMatcher');
Expand Down Expand Up @@ -112,6 +113,16 @@ class NativeElement {
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
}

async setDatePickerDate(rawDateString, formatString) {
const dateString = formatString === 'ISO8601'
? removeMilliseconds(rawDateString)
: rawDateString;

const action = new actions.SetDatePickerDateAction(dateString, formatString);
const traceDescription = actionDescription.setDatePickerDate(dateString, formatString);
return await new ActionInteraction(this._invocationManager, this, action, traceDescription).execute();
}

/**
* @param {'up' | 'right' | 'down' | 'left'} direction
* @param {'slow' | 'fast'} [speed]
Expand Down
25 changes: 25 additions & 0 deletions detox/src/android/espressoapi/DetoxAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,19 @@ class DetoxAction {
};
}

static setDatePickerDate(dateString, formatString) {
if (typeof dateString !== "string") throw new Error("dateString should be a string, but got " + (dateString + (" (" + (typeof dateString + ")"))));
if (typeof formatString !== "string") throw new Error("formatString should be a string, but got " + (formatString + (" (" + (typeof formatString + ")"))));
return {
target: {
type: "Class",
value: "com.wix.detox.espresso.DetoxAction"
},
method: "setDatePickerDate",
args: [dateString, formatString]
};
}

static adjustSliderToPosition(newPosition) {
if (typeof newPosition !== "number") throw new Error("newPosition should be a number, but got " + (newPosition + (" (" + (typeof newPosition + ")"))));
return {
Expand All @@ -220,6 +233,18 @@ class DetoxAction {
};
}

static parseDateISO8601(dateString) {
if (typeof dateString !== "string") throw new Error("dateString should be a string, but got " + (dateString + (" (" + (typeof dateString + ")"))));
return {
target: {
type: "Class",
value: "com.wix.detox.espresso.DetoxAction"
},
method: "parseDateISO8601",
args: [dateString]
};
}

}

module.exports = DetoxAction;
4 changes: 4 additions & 0 deletions detox/src/ios/expectTwo.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const _ = require('lodash');
const tempfile = require('tempfile');

const { assertEnum, assertNormalized } = require('../utils/assertArgument');
const { removeMilliseconds } = require('../utils/dateUtils');
const { actionDescription, expectDescription } = require('../utils/invocationTraceDescriptions');
const log = require('../utils/logger').child({ cat: 'ws-client, ws' });
const traceInvocationCall = require('../utils/traceInvocationCall').bind(null, log);
Expand Down Expand Up @@ -275,6 +276,9 @@ class Element {
setDatePickerDate(dateString, dateFormat) {
if (typeof dateString !== 'string') throw new Error('dateString should be a string, but got ' + (dateString + (' (' + (typeof dateString + ')'))));
if (typeof dateFormat !== 'string') throw new Error('dateFormat should be a string, but got ' + (dateFormat + (' (' + (typeof dateFormat + ')'))));
if (dateFormat === 'ISO8601') {
dateString = removeMilliseconds(dateString);
}

const traceDescription = actionDescription.setDatePickerDate(dateString, dateFormat);
return this.withAction('setDatePickerDate', traceDescription, dateString, dateFormat);
Expand Down
28 changes: 28 additions & 0 deletions detox/src/ios/expectTwo.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,34 @@ describe('expectTwo', () => {
expect(testCall).toDeepEqual(jsonOutput);
});

it(`should trim milliseconds for setDatePickerDate with ISO8601 format`, async () => {
const testCall = await e.element(e.by.id('datePicker')).setDatePickerDate('2019-01-01T00:00:00.000Z', 'ISO8601');
const jsonOutput = {
'invocation': {
'type': 'action',
'action': 'setDatePickerDate',
'params': ['2019-01-01T00:00:00Z', 'ISO8601'],
'predicate': { 'type': 'id', 'value': 'datePicker' }
}
};

expect(testCall).toDeepEqual(jsonOutput);
});

it(`should not trim milliseconds for setDatePickerDate with a custom format`, async () => {
const testCall = await e.element(e.by.id('datePicker')).setDatePickerDate('2019-01-01T00:00:00.000Z', 'YYYY-MM-DDTHH:mm:sss.fT');
const jsonOutput = {
'invocation': {
'type': 'action',
'action': 'setDatePickerDate',
'params': ['2019-01-01T00:00:00.000Z', 'YYYY-MM-DDTHH:mm:sss.fT'],
'predicate': { 'type': 'id', 'value': 'datePicker' }
}
};

expect(testCall).toDeepEqual(jsonOutput);
});

describe(`waitFor`, () => {
it(`should produce correct JSON for toBeNotVisible expectation`, async () => {
const testCall = await e.waitFor(e.element(e.by.text('Text5'))).toBeNotVisible().whileElement(e.by.id('ScrollView630')).scroll(50, 'down');
Expand Down
5 changes: 5 additions & 0 deletions detox/src/utils/dateUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ function shortFormat(date) {
return `${HH}:${MM}:${ss}.${milli}`;
}

function removeMilliseconds(isoDate) {
return isoDate.replace(/(T\d\d:\d\d:\d\d)(\.\d\d\d)/, '$1');
}

module.exports = {
shortFormat,
removeMilliseconds,
};
17 changes: 17 additions & 0 deletions detox/src/utils/dateUtils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,21 @@ describe('Date/time utils', () => {
expect(dateUtils.shortFormat(date)).toEqual('00:00:00.000');
});
});

describe('removeMilliseconds', () => {
it('should remove milliseconds for zero timezone', () => {
const isoDate = '2019-02-06T14:10:05.000Z';
expect(dateUtils.removeMilliseconds(isoDate)).toBe('2019-02-06T14:10:05Z');
});

it('should remove milliseconds for specific timezone', () => {
const isoDate = '2019-02-06T21:23:45.000-10:00';
expect(dateUtils.removeMilliseconds(isoDate)).toBe('2019-02-06T21:23:45-10:00');
});

it('should not affect a correct string', () => {
const isoDate = '2019-02-06T14:12:34+00:00';
expect(dateUtils.removeMilliseconds(isoDate)).toBe(isoDate);
});
});
});
1 change: 1 addition & 0 deletions detox/test/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ dependencies {
implementation project(':react-native-webview')
implementation project(':react-native-community-checkbox')
implementation project(':react-native-community-geolocation')
implementation project(':react-native-datetimepicker')

androidTestImplementation(project(path: ':detox'))
androidTestImplementation 'com.linkedin.testbutler:test-butler-library:2.2.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.reactnativecommunity.geolocation.GeolocationPackage;
import com.reactnativecommunity.slider.ReactSliderPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.reactcommunity.rndatetimepicker.RNDateTimePickerPackage;

import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -39,7 +40,8 @@ protected List<ReactPackage> getPackages() {
new RNCWebViewPackage(),
new NativeModulePackage(),
new AsyncStoragePackage(),
new ReactCheckBoxPackage()
new ReactCheckBoxPackage(),
new RNDateTimePickerPackage()
);
}
}
3 changes: 3 additions & 0 deletions detox/test/android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ project(':react-native-community-geolocation').projectDir = new File(rootProject

include ':@react-native-community_slider'
project(':@react-native-community_slider').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/slider/android')

include ':react-native-datetimepicker'
project(':react-native-datetimepicker').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/datetimepicker/android')
Loading

0 comments on commit d34546a

Please sign in to comment.