kopia lustrzana https://github.com/ge0rg/aprsdroid
				
				
				
			Merge 936f588e4a into 859033e0d7
				
					
				
			
						commit
						49ece3a7f6
					
				|  | @ -0,0 +1,188 @@ | ||||||
|  | name: Android CI | ||||||
|  | 
 | ||||||
|  | on: | ||||||
|  |   push: | ||||||
|  |     branches: [ '**' ] | ||||||
|  |   pull_request: | ||||||
|  |     branches: [ '**' ] | ||||||
|  |   workflow_dispatch: | ||||||
|  | 
 | ||||||
|  | jobs: | ||||||
|  |   compile: | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     name: "Compile all sources" | ||||||
|  | 
 | ||||||
|  |     steps: | ||||||
|  |     - name: Checkout project | ||||||
|  |       uses: actions/checkout@v4 | ||||||
|  |       with: | ||||||
|  |         fetch-depth: 0 | ||||||
|  |         submodules: recursive | ||||||
|  | 
 | ||||||
|  |     - name: Set up JDK 11 | ||||||
|  |       uses: actions/setup-java@v4 | ||||||
|  |       with: | ||||||
|  |         java-version: '11' | ||||||
|  |         distribution: 'adopt' | ||||||
|  |         cache: gradle | ||||||
|  | 
 | ||||||
|  |     - name: Load build outputs | ||||||
|  |       uses: actions/cache@v4 | ||||||
|  |       with: | ||||||
|  |         path: build | ||||||
|  |         key: build-${{ github.sha }} | ||||||
|  | 
 | ||||||
|  |     - name: Create properties file with empty API key | ||||||
|  |       run: echo mapsApiKey="\"${{ secrets.mapsApiKey }}\"" >> local.properties | ||||||
|  | 
 | ||||||
|  |     - name: Build App | ||||||
|  |       run: ./gradlew assemble --stacktrace | ||||||
|  | 
 | ||||||
|  |     - name: Build unit tests | ||||||
|  |       run: ./gradlew assembleDebugUnitTest assembleReleaseUnitTest --stacktrace | ||||||
|  | 
 | ||||||
|  |     - name: Build instrumentation tests | ||||||
|  |       run: ./gradlew assembleAndroidTest --stacktrace | ||||||
|  | 
 | ||||||
|  |   unit-test: | ||||||
|  |     name: "Run all unit tests" | ||||||
|  |     needs: compile | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  | 
 | ||||||
|  |     steps: | ||||||
|  |     - name: Checkout project | ||||||
|  |       uses: actions/checkout@v4 | ||||||
|  |       with: | ||||||
|  |         fetch-depth: 0 | ||||||
|  |         submodules: recursive | ||||||
|  | 
 | ||||||
|  |     - name: Set up JDK 11 | ||||||
|  |       uses: actions/setup-java@v4 | ||||||
|  |       with: | ||||||
|  |         java-version: '11' | ||||||
|  |         distribution: 'adopt' | ||||||
|  |         cache: gradle | ||||||
|  | 
 | ||||||
|  |     - name: Load build outputs | ||||||
|  |       uses: actions/cache@v4 | ||||||
|  |       with: | ||||||
|  |         path: build | ||||||
|  |         key: build-${{ github.sha }} | ||||||
|  | 
 | ||||||
|  |     - name: Run Unit Tests | ||||||
|  |       run: ./gradlew test --stacktrace | ||||||
|  | 
 | ||||||
|  |     - name: Run Linter | ||||||
|  |       run: ./gradlew lint --stacktrace | ||||||
|  |       continue-on-error: true | ||||||
|  | 
 | ||||||
|  |     - name: Upload reports | ||||||
|  |       uses: actions/upload-artifact@v4 | ||||||
|  |       with: | ||||||
|  |         name: Unit Test Reports | ||||||
|  |         path: build/reports | ||||||
|  |       if: failure() | ||||||
|  | 
 | ||||||
|  |   instrumentation: | ||||||
|  |     name: "Testing on API ${{ matrix.api-level }} for ${{ matrix.target }}" | ||||||
|  |     needs: compile | ||||||
|  |     # macOS provided hardware-accelerated emulator | ||||||
|  |     # but now so does Linux | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       matrix: | ||||||
|  |         api-level: [ 15, 21, 24, 31 ] | ||||||
|  |         target: [ default, google_apis, google_apis_playstore ] | ||||||
|  |         exclude: | ||||||
|  |           - api-level: 15 | ||||||
|  |             target: google_apis_playstore | ||||||
|  |           - api-level: 21 | ||||||
|  |             target: google_apis_playstore | ||||||
|  |           - api-level: 24 | ||||||
|  |             target: google_apis | ||||||
|  |           - api-level: 31 | ||||||
|  |             target: google_apis | ||||||
|  | 
 | ||||||
|  |     steps: | ||||||
|  |     - name: Checkout project | ||||||
|  |       uses: actions/checkout@v4 | ||||||
|  |       with: | ||||||
|  |         fetch-depth: 0 | ||||||
|  |         submodules: recursive | ||||||
|  | 
 | ||||||
|  |     - name: Enable KVM | ||||||
|  |       run: | | ||||||
|  |         echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules | ||||||
|  |         sudo udevadm control --reload-rules | ||||||
|  |         sudo udevadm trigger --name-match=kvm | ||||||
|  | 
 | ||||||
|  |     - name: Set up JDK 11 | ||||||
|  |       uses: actions/setup-java@v4 | ||||||
|  |       with: | ||||||
|  |         java-version: '11' | ||||||
|  |         distribution: 'adopt' | ||||||
|  |         cache: gradle | ||||||
|  | 
 | ||||||
|  |     - name: Load build outputs | ||||||
|  |       uses: actions/cache@v4 | ||||||
|  |       with: | ||||||
|  |         path: build | ||||||
|  |         key: build-${{ github.sha }} | ||||||
|  | 
 | ||||||
|  |     - name: AVD cache | ||||||
|  |       uses: actions/cache@v4 | ||||||
|  |       id: avd-cache | ||||||
|  |       with: | ||||||
|  |         path: | | ||||||
|  |           ~/.android/avd/* | ||||||
|  |           ~/.android/adb* | ||||||
|  |         key: avd-${{ matrix.api-level }}-${{ matrix.target }}-sd | ||||||
|  | 
 | ||||||
|  |     - name: Set up JRE 17 | ||||||
|  |       uses: actions/setup-java@v4 | ||||||
|  |       with: | ||||||
|  |         java-version: '17' | ||||||
|  |         distribution: 'temurin' | ||||||
|  |         java-package: 'jre' | ||||||
|  |         #cache: gradle | ||||||
|  | 
 | ||||||
|  |     - name: Create AVD and generate snapshot for caching | ||||||
|  |       if: steps.avd-cache.outputs.cache-hit != 'true' | ||||||
|  |       uses: reactivecircus/android-emulator-runner@v2 | ||||||
|  |       with: | ||||||
|  |         api-level: ${{ matrix.api-level }} | ||||||
|  |         target: ${{ matrix.target }} | ||||||
|  |         arch: ${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }} | ||||||
|  |         force-avd-creation: false | ||||||
|  |         sdcard-path-or-size: '64M' | ||||||
|  |         emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none | ||||||
|  |         disable-animations: false | ||||||
|  |         script: echo "Generated AVD snapshot for caching." | ||||||
|  | 
 | ||||||
|  |     - name: Run Instrumented Tests | ||||||
|  |       uses: reactivecircus/android-emulator-runner@v2 | ||||||
|  |       with: | ||||||
|  |         api-level: ${{ matrix.api-level }} | ||||||
|  |         target: ${{ matrix.target }} | ||||||
|  |         arch: ${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }} | ||||||
|  |         profile: Nexus 6 | ||||||
|  |         force-avd-creation: false | ||||||
|  |         sdcard-path-or-size: '64M' | ||||||
|  |         emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none | ||||||
|  |         disable-animations: true | ||||||
|  |         script: adb logcat -c && adb logcat -f /sdcard/logcat.txt & JAVA_HOME="${JAVA_HOME_11_X64}" ./gradlew connectedCheck --stacktrace || ( adb pull /sdcard/logcat.txt build/reports/; exit 1 ) | ||||||
|  | 
 | ||||||
|  |     - name: Upload reports | ||||||
|  |       uses: actions/upload-artifact@v4 | ||||||
|  |       with: | ||||||
|  |         name: Instrument Test Reports API ${{ matrix.api-level }} ${{ matrix.target }} | ||||||
|  |         path: build/reports | ||||||
|  |       if: failure() | ||||||
|  | 
 | ||||||
|  |     - name: Save successful debug APK | ||||||
|  |       uses: actions/upload-artifact@v4 | ||||||
|  |       with: | ||||||
|  |         name: Debug APK | ||||||
|  |         path: build/outputs/apk/debug/aprsdroid-debug.apk | ||||||
|  |       if: matrix.api-level == 31 && matrix.target == 'google_apis' | ||||||
|  | @ -8,6 +8,8 @@ messages. | ||||||
| 
 | 
 | ||||||
| APRSdroid is Open Source Software written in Scala and licensed under the GPLv2. | APRSdroid is Open Source Software written in Scala and licensed under the GPLv2. | ||||||
| 
 | 
 | ||||||
|  | master: [](../../actions/workflows/android.yml) | ||||||
|  | 
 | ||||||
| Quick links: | Quick links: | ||||||
| 
 | 
 | ||||||
| - [Google Play](https://play.google.com/store/apps/details?id=org.aprsdroid.app) | - [Google Play](https://play.google.com/store/apps/details?id=org.aprsdroid.app) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | package org.aprsdroid.app; | ||||||
|  | 
 | ||||||
|  | import static org.hamcrest.MatcherAssert.assertThat; | ||||||
|  | import static org.hamcrest.Matchers.closeTo; | ||||||
|  | 
 | ||||||
|  | import org.aprsdroid.app.testing.CoordinateMatcher; | ||||||
|  | import org.junit.Test; | ||||||
|  | 
 | ||||||
|  | import scala.Tuple2; | ||||||
|  | 
 | ||||||
|  | public class CoordinateTest { | ||||||
|  |     // Reference data generated from https://www.pgc.umn.edu/apps/convert/
 | ||||||
|  |     private static final String providedNLatitude = "77° 15' 30\" N"; | ||||||
|  |     private static final float expectedNLatitude = 77.258333f; | ||||||
|  |     private static final String providedELongitude = "164° 45' 15\" E"; | ||||||
|  |     private static final float expectedELongitude = 164.754167f; | ||||||
|  |     private static final String providedSLatitude = "45° 30' 45\" S"; | ||||||
|  |     private static final float expectedSLatitude = -45.5125f; | ||||||
|  |     private static final String providedWLongitude = "97° 20' 40\" W"; | ||||||
|  |     private static final float expectedWLongitude = -97.344444f; | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenLocationInNEHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() { | ||||||
|  |         Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedNLatitude, expectedELongitude); | ||||||
|  |         float floatLatitude = CoordinateMatcher.matchLatitude(actual._1); | ||||||
|  |         float floatLongitude = CoordinateMatcher.matchLongitude(actual._2); | ||||||
|  |         assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedNLatitude, 1e-7)); | ||||||
|  |         assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedELongitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenLocationInNWHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() { | ||||||
|  |         Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedNLatitude, expectedWLongitude); | ||||||
|  |         float floatLatitude = CoordinateMatcher.matchLatitude(actual._1); | ||||||
|  |         float floatLongitude = CoordinateMatcher.matchLongitude(actual._2); | ||||||
|  |         assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedNLatitude, 1e-7)); | ||||||
|  |         assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedWLongitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenLocationInSEHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() { | ||||||
|  |         Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedSLatitude, expectedELongitude); | ||||||
|  |         float floatLatitude = CoordinateMatcher.matchLatitude(actual._1); | ||||||
|  |         float floatLongitude = CoordinateMatcher.matchLongitude(actual._2); | ||||||
|  |         assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedSLatitude, 1e-7)); | ||||||
|  |         assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedELongitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenLocationInSWHemisphere_whenFormattedAsDMSString_thenParseBackIntoDecimalValue() { | ||||||
|  |         Tuple2<String, String> actual = AprsPacket$.MODULE$.formatCoordinates(expectedSLatitude, expectedWLongitude); | ||||||
|  |         float floatLatitude = CoordinateMatcher.matchLatitude(actual._1); | ||||||
|  |         float floatLongitude = CoordinateMatcher.matchLongitude(actual._2); | ||||||
|  |         assertThat("Latitude", (double) floatLatitude, closeTo((double) expectedSLatitude, 1e-7)); | ||||||
|  |         assertThat("Longitude", (double) floatLongitude, closeTo((double) expectedWLongitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -2,8 +2,8 @@ package org.aprsdroid.app; | ||||||
| 
 | 
 | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| 
 | 
 | ||||||
| import android.support.test.InstrumentationRegistry; | import androidx.test.platform.app.InstrumentationRegistry; | ||||||
| import android.support.test.runner.AndroidJUnit4; | import androidx.test.ext.junit.runners.AndroidJUnit4; | ||||||
| 
 | 
 | ||||||
| import org.junit.Test; | import org.junit.Test; | ||||||
| import org.junit.runner.RunWith; | import org.junit.runner.RunWith; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,112 @@ | ||||||
|  | package org.aprsdroid.app; | ||||||
|  | 
 | ||||||
|  | import static androidx.test.espresso.Espresso.onView; | ||||||
|  | import static androidx.test.espresso.action.ViewActions.click; | ||||||
|  | import static androidx.test.espresso.action.ViewActions.closeSoftKeyboard; | ||||||
|  | import static androidx.test.espresso.action.ViewActions.typeText; | ||||||
|  | import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; | ||||||
|  | import static androidx.test.espresso.assertion.ViewAssertions.matches; | ||||||
|  | import static androidx.test.espresso.matcher.RootMatchers.isDialog; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.isRoot; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.withHint; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.withId; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.withText; | ||||||
|  | import static org.hamcrest.core.AllOf.allOf; | ||||||
|  | import static org.hamcrest.core.StringContains.containsString; | ||||||
|  | 
 | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | 
 | ||||||
|  | import androidx.test.ext.junit.rules.ActivityScenarioRule; | ||||||
|  | import androidx.test.ext.junit.runners.AndroidJUnit4; | ||||||
|  | 
 | ||||||
|  | import org.aprsdroid.app.testing.SharedPreferencesRule; | ||||||
|  | import org.junit.Assert; | ||||||
|  | import org.junit.Rule; | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.junit.rules.RuleChain; | ||||||
|  | import org.junit.runner.RunWith; | ||||||
|  | 
 | ||||||
|  | @RunWith(AndroidJUnit4.class) | ||||||
|  | public class FirstRunDialog { | ||||||
|  |     private final String pref_callsign = "callsign"; | ||||||
|  |     private final String pref_passcode = "passcode"; | ||||||
|  |     public ActivityScenarioRule activityRule = new ActivityScenarioRule<>(LogActivity.class); | ||||||
|  |     public SharedPreferencesRule prefsRule = new SharedPreferencesRule() { | ||||||
|  |         @Override | ||||||
|  |         protected void modifyPreferences(SharedPreferences preferences) { | ||||||
|  |             preferences.edit().clear().commit(); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     @Rule | ||||||
|  |     public RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule); | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenAFirstTimeRun_whenProvidedABadPasscode_ThenDialogStaysOpen() { | ||||||
|  |         onView(isRoot()) | ||||||
|  |                 .inRoot(isDialog()) | ||||||
|  |                 .check(matches(allOf( | ||||||
|  |                         hasDescendant(withText(containsString("Welcome to APRSdroid"))), | ||||||
|  |                         hasDescendant(withId(R.id.callsign)), | ||||||
|  |                         hasDescendant(withId(R.id.passcode))))); | ||||||
|  |         onView(withId(R.id.callsign)) | ||||||
|  |                 .check(matches(withHint(containsString("Callsign")))) | ||||||
|  |                 .perform(typeText("XA1AAA"), closeSoftKeyboard()); | ||||||
|  |         onView(withId(R.id.passcode)) | ||||||
|  |                 .check(matches(withHint(containsString("Passcode")))) | ||||||
|  |                 .perform(typeText("12345"), closeSoftKeyboard()); | ||||||
|  |         onView(withId(android.R.id.button1)).perform(click());  // OK Button
 | ||||||
|  |         onView(isRoot()) | ||||||
|  |                 .inRoot(isDialog()) | ||||||
|  |                 .check(matches(allOf( | ||||||
|  |                         hasDescendant(withText(containsString("Welcome to APRSdroid"))), | ||||||
|  |                         hasDescendant(withId(R.id.callsign)), | ||||||
|  |                         hasDescendant(withId(R.id.passcode))))); | ||||||
|  |         try { | ||||||
|  |             Thread.sleep(5000); | ||||||
|  |         } catch (InterruptedException ex) { | ||||||
|  |         } | ||||||
|  |         Assert.assertTrue(true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenAFirstTimeRun_whenProvidedAGoodPasscode_ThenDialogCloses() { | ||||||
|  |         onView(isRoot()) | ||||||
|  |                 .inRoot(isDialog()) | ||||||
|  |                 .check(matches(allOf( | ||||||
|  |                         hasDescendant(withText(containsString("Welcome to APRSdroid"))), | ||||||
|  |                         hasDescendant(withId(R.id.callsign)), | ||||||
|  |                         hasDescendant(withId(R.id.passcode))))); | ||||||
|  |         onView(withId(R.id.callsign)) | ||||||
|  |                 .check(matches(withHint(containsString("Callsign")))) | ||||||
|  |                 .perform(typeText("XA1AAA"), closeSoftKeyboard()); | ||||||
|  |         onView(withId(R.id.passcode)) | ||||||
|  |                 .check(matches(withHint(containsString("Passcode")))) | ||||||
|  |                 .perform(typeText("23459"), closeSoftKeyboard()); | ||||||
|  |         onView(withId(android.R.id.button1)).perform(click());  // OK Button
 | ||||||
|  |         onView(allOf( | ||||||
|  |                 isRoot(), | ||||||
|  |                 hasDescendant(withText(containsString("Welcome to APRSdroid"))), | ||||||
|  |                 hasDescendant(withId(R.id.callsign)), | ||||||
|  |                 hasDescendant(withId(R.id.passcode)))) | ||||||
|  |                 .check(doesNotExist()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenAFirstTimeRun_whenProvidedAGoodPasscode_ThenPrefsSaved() { | ||||||
|  |         String expected_callsign = "XA1AAA"; | ||||||
|  |         String expected_passcode = "23459"; | ||||||
|  |         SharedPreferences prefs = prefsRule.getPreferences(); | ||||||
|  |         Assert.assertNull("Callsign", prefs.getString(pref_callsign, null)); | ||||||
|  |         Assert.assertNull("Passcode", prefs.getString(pref_passcode, null)); | ||||||
|  |         onView(withId(R.id.callsign)) | ||||||
|  |                 .check(matches(withHint(containsString("Callsign")))) | ||||||
|  |                 .perform(typeText(expected_callsign), closeSoftKeyboard()); | ||||||
|  |         onView(withId(R.id.passcode)) | ||||||
|  |                 .check(matches(withHint(containsString("Passcode")))) | ||||||
|  |                 .perform(typeText(expected_passcode), closeSoftKeyboard()); | ||||||
|  |         onView(withId(android.R.id.button1)).perform(click());  // OK Button
 | ||||||
|  |         Assert.assertEquals("Callsign", expected_callsign, prefs.getString(pref_callsign, null)); | ||||||
|  |         Assert.assertEquals("Passcode", expected_passcode, prefs.getString(pref_passcode, null)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,464 @@ | ||||||
|  | package org.aprsdroid.app; | ||||||
|  | 
 | ||||||
|  | import static androidx.test.espresso.Espresso.onView; | ||||||
|  | import static androidx.test.espresso.action.ViewActions.click; | ||||||
|  | import static androidx.test.espresso.assertion.ViewAssertions.matches; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.withId; | ||||||
|  | import static androidx.test.espresso.matcher.ViewMatchers.withText; | ||||||
|  | import static org.hamcrest.MatcherAssert.assertThat; | ||||||
|  | import static org.hamcrest.Matchers.closeTo; | ||||||
|  | import static org.hamcrest.Matchers.equalTo; | ||||||
|  | import static org.hamcrest.Matchers.not; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.Intent; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.util.Log; | ||||||
|  | 
 | ||||||
|  | import androidx.test.espresso.action.GeneralLocation; | ||||||
|  | import androidx.test.espresso.action.GeneralSwipeAction; | ||||||
|  | import androidx.test.espresso.action.Press; | ||||||
|  | import androidx.test.espresso.action.Swipe; | ||||||
|  | import androidx.test.ext.junit.rules.ActivityScenarioRule; | ||||||
|  | import androidx.test.ext.junit.runners.AndroidJUnit4; | ||||||
|  | import androidx.test.filters.FlakyTest; | ||||||
|  | import androidx.test.platform.app.InstrumentationRegistry; | ||||||
|  | 
 | ||||||
|  | import com.google.android.gms.common.ConnectionResult; | ||||||
|  | import com.google.android.gms.common.GoogleApiAvailability; | ||||||
|  | 
 | ||||||
|  | import org.aprsdroid.app.testing.DMSLocationAssertion; | ||||||
|  | import org.aprsdroid.app.testing.SharedPreferencesRule; | ||||||
|  | import org.aprsdroid.app.testing.SpecificDMSLocationAssertion; | ||||||
|  | import org.junit.Assume; | ||||||
|  | import org.junit.Rule; | ||||||
|  | import org.junit.Test; | ||||||
|  | import org.junit.experimental.runners.Enclosed; | ||||||
|  | import org.junit.rules.RuleChain; | ||||||
|  | import org.junit.runner.RunWith; | ||||||
|  | 
 | ||||||
|  | @RunWith(Enclosed.class) | ||||||
|  | public class MapModeTest { | ||||||
|  |     private static final String TAG = "APRSdroid-MapModeTest"; | ||||||
|  |     private static final Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); | ||||||
|  | 
 | ||||||
|  |     private static final ActivityScenarioRule<GoogleMapAct> activityRule = | ||||||
|  |             new ActivityScenarioRule<>(new Intent(appContext, GoogleMapAct.class) | ||||||
|  |                     .putExtra("info", R.string.p_source_from_map_save)); | ||||||
|  | 
 | ||||||
|  |     @RunWith(AndroidJUnit4.class) | ||||||
|  |     public static class GivenDefaultHomeLocation { | ||||||
|  |         private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() { | ||||||
|  |             @Override | ||||||
|  |             protected void modifyPreferences(SharedPreferences preferences) { | ||||||
|  |                 preferences.edit().clear().commit(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         @Rule | ||||||
|  |         public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule); | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenFirstLoaded_thenSaveDisabled() { | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(withText(""))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(not(isEnabled()))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenMapIsDragged_thenPositionAndButtonShown() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(not(withText("")))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(isEnabled())); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenMapIsDragged_thenPositionIsValidCoordinates() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(new DMSLocationAssertion()); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @RunWith(AndroidJUnit4.class) | ||||||
|  |     public static class GivenSavedLocationInNEHemisphere { | ||||||
|  |         private static final float expectedLatitude = 37.50123f; | ||||||
|  |         private static final float expectedLongitude = 88.25034f; | ||||||
|  |         private static final float expectedZoom = 4.0f; | ||||||
|  | 
 | ||||||
|  |         private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() { | ||||||
|  |             @Override | ||||||
|  |             protected void modifyPreferences(SharedPreferences preferences) { | ||||||
|  |                 preferences.edit() | ||||||
|  |                         .putFloat("map_lat", expectedLatitude) | ||||||
|  |                         .putFloat("map_lon", expectedLongitude) | ||||||
|  |                         .putFloat("map_zoom", expectedZoom) | ||||||
|  |                         .commit(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         @Rule | ||||||
|  |         public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule); | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenFirstLoaded_thenSaveDisabled() { | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(withText(""))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(not(isEnabled()))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenMapIsDragged_thenPositionAndButtonShown() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(not(withText("")))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(isEnabled())); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             try { | ||||||
|  |                 Thread.sleep(500); | ||||||
|  |             } catch (InterruptedException ex) { | ||||||
|  |                 Log.w(TAG, "Sleep was interrupted: " + ex); | ||||||
|  |             } | ||||||
|  |             prefsRule.getPreferences() | ||||||
|  |                     .edit() | ||||||
|  |                     .remove("map_lat") | ||||||
|  |                     .remove("map_lon") | ||||||
|  |                     .remove("map_zoom") | ||||||
|  |                     .commit(); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .perform(click()); | ||||||
|  |             float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f); | ||||||
|  |             float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f); | ||||||
|  |             float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f); | ||||||
|  |             assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2)); | ||||||
|  |             assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2)); | ||||||
|  |             assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @RunWith(AndroidJUnit4.class) | ||||||
|  |     public static class GivenSavedLocationInSEHemisphere { | ||||||
|  |         private static final float expectedLatitude = -37.50123f; | ||||||
|  |         private static final float expectedLongitude = 88.25034f; | ||||||
|  |         private static final float expectedZoom = 4.5f; | ||||||
|  | 
 | ||||||
|  |         private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() { | ||||||
|  |             @Override | ||||||
|  |             protected void modifyPreferences(SharedPreferences preferences) { | ||||||
|  |                 preferences.edit() | ||||||
|  |                         .putFloat("map_lat", expectedLatitude) | ||||||
|  |                         .putFloat("map_lon", expectedLongitude) | ||||||
|  |                         .putFloat("map_zoom", expectedZoom) | ||||||
|  |                         .commit(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         @Rule | ||||||
|  |         public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule); | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenFirstLoaded_thenSaveDisabled() { | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(withText(""))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(not(isEnabled()))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenMapIsDragged_thenPositionAndButtonShown() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(not(withText("")))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(isEnabled())); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             try { | ||||||
|  |                 Thread.sleep(500); | ||||||
|  |             } catch (InterruptedException ex) { | ||||||
|  |                 Log.w(TAG, "Sleep was interrupted: " + ex); | ||||||
|  |             } | ||||||
|  |             prefsRule.getPreferences() | ||||||
|  |                     .edit() | ||||||
|  |                     .remove("map_lat") | ||||||
|  |                     .remove("map_lon") | ||||||
|  |                     .remove("map_zoom") | ||||||
|  |                     .commit(); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .perform(click()); | ||||||
|  |             float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f); | ||||||
|  |             float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f); | ||||||
|  |             float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f); | ||||||
|  |             assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2)); | ||||||
|  |             assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2)); | ||||||
|  |             assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @RunWith(AndroidJUnit4.class) | ||||||
|  |     public static class GivenSavedLocationInNWHemisphere { | ||||||
|  |         private static final float expectedLatitude = 37.50123f; | ||||||
|  |         private static final float expectedLongitude = -88.25034f; | ||||||
|  |         private static final float expectedZoom = 5.0f; | ||||||
|  | 
 | ||||||
|  |         private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() { | ||||||
|  |             @Override | ||||||
|  |             protected void modifyPreferences(SharedPreferences preferences) { | ||||||
|  |                 preferences.edit() | ||||||
|  |                         .putFloat("map_lat", expectedLatitude) | ||||||
|  |                         .putFloat("map_lon", expectedLongitude) | ||||||
|  |                         .putFloat("map_zoom", expectedZoom) | ||||||
|  |                         .commit(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         @Rule | ||||||
|  |         public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule); | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenFirstLoaded_thenSaveDisabled() { | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(withText(""))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(not(isEnabled()))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenMapIsDragged_thenPositionAndButtonShown() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(not(withText("")))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(isEnabled())); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             try { | ||||||
|  |                 Thread.sleep(500); | ||||||
|  |             } catch (InterruptedException ex) { | ||||||
|  |                 Log.w(TAG, "Sleep was interrupted: " + ex); | ||||||
|  |             } | ||||||
|  |             prefsRule.getPreferences() | ||||||
|  |                     .edit() | ||||||
|  |                     .remove("map_lat") | ||||||
|  |                     .remove("map_lon") | ||||||
|  |                     .remove("map_zoom") | ||||||
|  |                     .commit(); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .perform(click()); | ||||||
|  |             float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f); | ||||||
|  |             float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f); | ||||||
|  |             float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f); | ||||||
|  |             assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2)); | ||||||
|  |             assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2)); | ||||||
|  |             assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @RunWith(AndroidJUnit4.class) | ||||||
|  |     public static class GivenSavedLocationInSWHemisphere { | ||||||
|  |         private static final float expectedLatitude = -37.50123f; | ||||||
|  |         private static final float expectedLongitude = -88.25034f; | ||||||
|  |         private static final float expectedZoom = 5.5f; | ||||||
|  | 
 | ||||||
|  |         private final SharedPreferencesRule prefsRule = new SharedPreferencesRule() { | ||||||
|  |             @Override | ||||||
|  |             protected void modifyPreferences(SharedPreferences preferences) { | ||||||
|  |                 preferences.edit() | ||||||
|  |                         .putFloat("map_lat", expectedLatitude) | ||||||
|  |                         .putFloat("map_lon", expectedLongitude) | ||||||
|  |                         .putFloat("map_zoom", expectedZoom) | ||||||
|  |                         .commit(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         @Rule | ||||||
|  |         public final RuleChain rules = RuleChain.outerRule(prefsRule).around(activityRule); | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenFirstLoaded_thenSaveDisabled() { | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(withText(""))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(not(isEnabled()))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         public void whenMapIsDragged_thenPositionAndButtonShown() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(matches(not(withText("")))); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .check(matches(isEnabled())); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForth_thenPositionIsOriginalCoordinates() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.info)) | ||||||
|  |                     .check(new SpecificDMSLocationAssertion(expectedLatitude, expectedLongitude)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Test | ||||||
|  |         @FlakyTest | ||||||
|  |         public void whenMapIsDraggedBackAndForthAndSaved_thenPositionIsSavedCorrectly() { | ||||||
|  |             Assume.assumeThat("Google Play Services requires", | ||||||
|  |                     GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(appContext), | ||||||
|  |                     equalTo(ConnectionResult.SUCCESS)); | ||||||
|  |             try { | ||||||
|  |                 Thread.sleep(500); | ||||||
|  |             } catch (InterruptedException ex) { | ||||||
|  |                 Log.w(TAG, "Sleep was interrupted: " + ex); | ||||||
|  |             } | ||||||
|  |             prefsRule.getPreferences() | ||||||
|  |                     .edit() | ||||||
|  |                     .remove("map_lat") | ||||||
|  |                     .remove("map_lon") | ||||||
|  |                     .remove("map_zoom") | ||||||
|  |                     .commit(); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER, | ||||||
|  |                             GeneralLocation.CENTER_LEFT, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.mapview)) | ||||||
|  |                     .perform(new GeneralSwipeAction(Swipe.SLOW, GeneralLocation.CENTER_LEFT, | ||||||
|  |                             GeneralLocation.CENTER, Press.THUMB)); | ||||||
|  |             onView(withId(R.id.accept)) | ||||||
|  |                     .perform(click()); | ||||||
|  |             float actualLatitude = prefsRule.getPreferences().getFloat("map_lat", 0.0f); | ||||||
|  |             float actualLongitude = prefsRule.getPreferences().getFloat("map_lon", 0.0f); | ||||||
|  |             float actualZoom = prefsRule.getPreferences().getFloat("map_zoom", 0.0f); | ||||||
|  |             assertThat("Latitude", (double) actualLatitude, closeTo(expectedLatitude, 5e-2)); | ||||||
|  |             assertThat("Longitude", (double) actualLongitude, closeTo(expectedLongitude, 5e-2)); | ||||||
|  |             assertThat("Zoom", (double) actualZoom, closeTo(expectedZoom, 1e-7)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | package org.aprsdroid.app.testing; | ||||||
|  | 
 | ||||||
|  | import static org.hamcrest.Matchers.instanceOf; | ||||||
|  | 
 | ||||||
|  | import android.view.View; | ||||||
|  | import android.widget.TextView; | ||||||
|  | 
 | ||||||
|  | import androidx.test.espresso.NoMatchingViewException; | ||||||
|  | import androidx.test.espresso.ViewAssertion; | ||||||
|  | 
 | ||||||
|  | import org.junit.Assert; | ||||||
|  | 
 | ||||||
|  | public class DMSLocationAssertion implements ViewAssertion { | ||||||
|  |     protected void checkCoordinates(float latitude, float longitude) { | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void check(View view, NoMatchingViewException noViewFoundException) { | ||||||
|  |         if (view == null) | ||||||
|  |             throw noViewFoundException; | ||||||
|  |         Assert.assertThat(view, instanceOf(TextView.class)); | ||||||
|  |         TextView text = (TextView) view; | ||||||
|  |         float latitude = CoordinateMatcher.matchLatitude(text.getText()); | ||||||
|  |         float longitude = CoordinateMatcher.matchLongitude(text.getText()); | ||||||
|  |         checkCoordinates(latitude, longitude); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | package org.aprsdroid.app.testing; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.content.SharedPreferences; | ||||||
|  | import android.preference.PreferenceManager; | ||||||
|  | 
 | ||||||
|  | import androidx.test.platform.app.InstrumentationRegistry; | ||||||
|  | 
 | ||||||
|  | import org.junit.rules.TestRule; | ||||||
|  | import org.junit.runner.Description; | ||||||
|  | import org.junit.runners.model.Statement; | ||||||
|  | 
 | ||||||
|  | public abstract class SharedPreferencesRule implements TestRule { | ||||||
|  |     private SharedPreferences preferences; | ||||||
|  | 
 | ||||||
|  |     protected abstract void modifyPreferences(SharedPreferences preferences); | ||||||
|  | 
 | ||||||
|  |     public SharedPreferences getPreferences() { | ||||||
|  |         return preferences; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public Statement apply(Statement base, Description description) { | ||||||
|  |         Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); | ||||||
|  |         preferences = PreferenceManager.getDefaultSharedPreferences(appContext); | ||||||
|  |         modifyPreferences(preferences); | ||||||
|  |         return base; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | package org.aprsdroid.app.testing; | ||||||
|  | 
 | ||||||
|  | import static org.hamcrest.Matchers.closeTo; | ||||||
|  | 
 | ||||||
|  | import org.junit.Assert; | ||||||
|  | 
 | ||||||
|  | public class SpecificDMSLocationAssertion extends DMSLocationAssertion { | ||||||
|  |     private final float expectedLatitude; | ||||||
|  |     private final float expectedLongitude; | ||||||
|  | 
 | ||||||
|  |     public SpecificDMSLocationAssertion(float myExpectedLatitude, float myExpectedLongitude) { | ||||||
|  |         expectedLatitude = myExpectedLatitude; | ||||||
|  |         expectedLongitude = myExpectedLongitude; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected void checkCoordinates(float latitude, float longitude) { | ||||||
|  |         super.checkCoordinates(latitude, longitude); | ||||||
|  |         Assert.assertThat("Latitude", (double) latitude, closeTo((double) expectedLatitude, 0.05)); | ||||||
|  |         Assert.assertThat("Longitude", (double) longitude, closeTo((double) expectedLongitude, 0.05)); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										19
									
								
								build.gradle
								
								
								
								
							
							
						
						
									
										19
									
								
								build.gradle
								
								
								
								
							|  | @ -6,7 +6,7 @@ buildscript { | ||||||
| 		google() | 		google() | ||||||
| 	} | 	} | ||||||
| 	dependencies { | 	dependencies { | ||||||
| 		classpath 'com.android.tools.build:gradle:3.5.1' | 		classpath 'com.android.tools.build:gradle:3.5.4' | ||||||
| 		// NOTE: Do not place your application dependencies here; they belong | 		// NOTE: Do not place your application dependencies here; they belong | ||||||
| 		// in the individual module build.gradle files | 		// in the individual module build.gradle files | ||||||
| 
 | 
 | ||||||
|  | @ -81,7 +81,7 @@ android { | ||||||
| 
 | 
 | ||||||
| 		resValue "string", "google_maps_key", mapsApiKey() | 		resValue "string", "google_maps_key", mapsApiKey() | ||||||
| 
 | 
 | ||||||
| 		testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | 		testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' | ||||||
| 	} | 	} | ||||||
| 	useLibrary 'org.apache.http.legacy' | 	useLibrary 'org.apache.http.legacy' | ||||||
| 	compileOptions { | 	compileOptions { | ||||||
|  | @ -131,10 +131,10 @@ android { | ||||||
| 			jniLibs.srcDirs = ['libs'] | 			jniLibs.srcDirs = ['libs'] | ||||||
| 		} | 		} | ||||||
| 		androidTest { | 		androidTest { | ||||||
| 			java.srcDirs = ['androidTest/java'] | 			java.srcDirs = ['androidTest/java', 'sharedTest/java'] | ||||||
| 		} | 		} | ||||||
| 		test { | 		test { | ||||||
| 			java.srcDirs = ['test/java'] | 			java.srcDirs = ['test/java', 'sharedTest/java'] | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	lintOptions { | 	lintOptions { | ||||||
|  | @ -158,7 +158,12 @@ dependencies { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	implementation 'com.squareup.okio:okio:2.1.0' | 	implementation 'com.squareup.okio:okio:2.1.0' | ||||||
| 	androidTestImplementation 'com.android.support.test:runner:1.0.2' | 
 | ||||||
| 	androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' | 	testImplementation 'junit:junit:4.13.1' | ||||||
| 	testImplementation 'junit:junit:4.12' | 	testImplementation 'org.hamcrest:hamcrest-core:1.3' | ||||||
|  | 	testImplementation 'org.hamcrest:hamcrest-library:1.3' | ||||||
|  | 	androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' | ||||||
|  | 	androidTestImplementation 'androidx.test:runner:1.4.0' | ||||||
|  | 	androidTestImplementation 'androidx.test:rules:1.4.0' | ||||||
|  | 	androidTestImplementation 'androidx.test.ext:junit:1.1.3' | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | package org.aprsdroid.app.testing; | ||||||
|  | 
 | ||||||
|  | import static org.hamcrest.Matchers.greaterThanOrEqualTo; | ||||||
|  | 
 | ||||||
|  | import org.junit.Assert; | ||||||
|  | 
 | ||||||
|  | import java.util.Objects; | ||||||
|  | import java.util.regex.Matcher; | ||||||
|  | import java.util.regex.Pattern; | ||||||
|  | 
 | ||||||
|  | public class CoordinateMatcher { | ||||||
|  |     private static final String NUMBER = "(-?\\d+(?:\\.\\d*)?)"; | ||||||
|  |     private static final String dms_latitude_pattern = NUMBER + "°\\s*" + NUMBER + "'\\s*" + NUMBER + "\"\\s*([NS])"; | ||||||
|  |     private static final String dms_longitude_pattern = NUMBER + "°\\s*" + NUMBER + "'\\s*" + NUMBER + "\"\\s*([EW])"; | ||||||
|  |     private static final Pattern dms_latitude_regex = Pattern.compile(dms_latitude_pattern, Pattern.CASE_INSENSITIVE); | ||||||
|  |     private static final Pattern dms_longitude_regex = Pattern.compile(dms_longitude_pattern, Pattern.CASE_INSENSITIVE); | ||||||
|  | 
 | ||||||
|  |     private static float convertField(CharSequence string, Pattern regex, String name) { | ||||||
|  |         Matcher matcher = regex.matcher(string); | ||||||
|  |         Assert.assertTrue(name + " not found", matcher.find()); | ||||||
|  |         float value = 0; | ||||||
|  |         try { | ||||||
|  |             int degrees = Integer.parseInt(Objects.requireNonNull(matcher.group(1))); | ||||||
|  |             Assert.assertThat(name + " degrees", degrees, greaterThanOrEqualTo(0)); | ||||||
|  |             value = (float) degrees; | ||||||
|  |         } catch (NumberFormatException ex) { | ||||||
|  |             Assert.fail(name + " degree field not an integer"); | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|  |             int minutes = Integer.parseInt(Objects.requireNonNull(matcher.group(2))); | ||||||
|  |             Assert.assertThat(name + " minutes", minutes, greaterThanOrEqualTo(0)); | ||||||
|  |             value += (float) minutes / 60.0f; | ||||||
|  |         } catch (NumberFormatException ex) { | ||||||
|  |             Assert.fail(name + " minute field not an integer"); | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|  |             float seconds = Float.parseFloat(Objects.requireNonNull(matcher.group(3))); | ||||||
|  |             Assert.assertThat(name + " seconds", seconds, greaterThanOrEqualTo(0.0f)); | ||||||
|  |             value += seconds / 3600.0f; | ||||||
|  |         } catch (NumberFormatException ex) { | ||||||
|  |             Assert.fail(name + " seconds field not an number"); | ||||||
|  |         } | ||||||
|  |         String direction = Objects.requireNonNull(matcher.group(4)); | ||||||
|  |         if (direction.equalsIgnoreCase("S") || direction.equalsIgnoreCase("W")) { | ||||||
|  |             value *= -1.0f; | ||||||
|  |         } | ||||||
|  |         return value; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static float matchLatitude(CharSequence string) { | ||||||
|  |         return convertField(string, dms_latitude_regex, "Latitude"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static float matchLongitude(CharSequence string) { | ||||||
|  |         return convertField(string, dms_longitude_regex, "Longitude"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -0,0 +1,43 @@ | ||||||
|  | package org.aprsdroid.app; | ||||||
|  | 
 | ||||||
|  | import static org.hamcrest.MatcherAssert.assertThat; | ||||||
|  | import static org.hamcrest.Matchers.closeTo; | ||||||
|  | 
 | ||||||
|  | import org.aprsdroid.app.testing.CoordinateMatcher; | ||||||
|  | import org.junit.Test; | ||||||
|  | 
 | ||||||
|  | public class CoordinateTest { | ||||||
|  |     // Reference data generated from https://www.pgc.umn.edu/apps/convert/
 | ||||||
|  |     private static final String providedNLatitude = "77° 15' 30\" N"; | ||||||
|  |     private static final float expectedNLatitude = 77.258333f; | ||||||
|  |     private static final String providedELongitude = "164° 45' 15\" E"; | ||||||
|  |     private static final float expectedELongitude = 164.754167f; | ||||||
|  |     private static final String providedSLatitude = "45° 30' 45\" S"; | ||||||
|  |     private static final float expectedSLatitude = -45.5125f; | ||||||
|  |     private static final String providedWLongitude = "97° 20' 40\" W"; | ||||||
|  |     private static final float expectedWLongitude = -97.344444f; | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenDMSLatitudeInN_whenParsingString_ThenShouldMatchDecimal() { | ||||||
|  |         float value = CoordinateMatcher.matchLatitude(providedNLatitude); | ||||||
|  |         assertThat("Latitude", (double) value, closeTo((double) expectedNLatitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenDMSLongitudeInE_whenParsingString_ThenShouldMatchDecimal() { | ||||||
|  |         float value = CoordinateMatcher.matchLongitude(providedELongitude); | ||||||
|  |         assertThat("Longitude", (double) value, closeTo((double) expectedELongitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenDMSLatitudeInS_whenParsingString_ThenShouldMatchDecimal() { | ||||||
|  |         float value = CoordinateMatcher.matchLatitude(providedSLatitude); | ||||||
|  |         assertThat("Latitude", (double) value, closeTo((double) expectedSLatitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Test | ||||||
|  |     public void givenDMSLongitudeInW_whenParsingString_ThenShouldMatchDecimal() { | ||||||
|  |         float value = CoordinateMatcher.matchLongitude(providedWLongitude); | ||||||
|  |         assertThat("Longitude", (double) value, closeTo((double) expectedWLongitude, 1e-7)); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Ładowanie…
	
		Reference in New Issue
	
	 Loren M. Lang
						Loren M. Lang