An ATproto social media client -- with an independent Appview.

Replace e2e tests with Maestro (#3983)

* Setup maestro tests and convert some initial tests

* Remove detox

* Replace all tests with maestro

authored by Paul Frazee and committed by GitHub d49b93dc 5cd4ac3a

-86
.detoxrc.js
··· 1 - /** @type {Detox.DetoxConfig} */ 2 - module.exports = { 3 - testRunner: { 4 - args: { 5 - $0: 'jest', 6 - config: '__e2e__/jest.config.js', 7 - }, 8 - jest: { 9 - setupTimeout: 120000, 10 - }, 11 - }, 12 - apps: { 13 - 'ios.debug': { 14 - type: 'ios.app', 15 - binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app', 16 - build: 17 - 'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', 18 - }, 19 - 'ios.release': { 20 - type: 'ios.app', 21 - binaryPath: 22 - 'ios/build/Build/Products/Release-iphonesimulator/bluesky.app', 23 - build: 24 - 'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', 25 - }, 26 - 'android.debug': { 27 - type: 'android.apk', 28 - binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', 29 - build: 30 - 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', 31 - reversePorts: [8081], 32 - }, 33 - 'android.release': { 34 - type: 'android.apk', 35 - binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', 36 - build: 37 - 'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -', 38 - }, 39 - }, 40 - devices: { 41 - simulator: { 42 - type: 'ios.simulator', 43 - device: { 44 - type: 'iPhone 15 Pro', 45 - }, 46 - }, 47 - attached: { 48 - type: 'android.attached', 49 - device: { 50 - adbName: '.*', 51 - }, 52 - }, 53 - emulator: { 54 - type: 'android.emulator', 55 - device: { 56 - avdName: 'Pixel_3a_API_30_x86', 57 - }, 58 - }, 59 - }, 60 - configurations: { 61 - 'ios.sim.debug': { 62 - device: 'simulator', 63 - app: 'ios.debug', 64 - }, 65 - 'ios.sim.release': { 66 - device: 'simulator', 67 - app: 'ios.release', 68 - }, 69 - 'android.att.debug': { 70 - device: 'attached', 71 - app: 'android.debug', 72 - }, 73 - 'android.att.release': { 74 - device: 'attached', 75 - app: 'android.release', 76 - }, 77 - 'android.emu.debug': { 78 - device: 'emulator', 79 - app: 'android.debug', 80 - }, 81 - 'android.emu.release': { 82 - device: 'emulator', 83 - app: 'android.release', 84 - }, 85 - }, 86 - }
-1
.eslintrc.js
··· 9 9 parser: '@typescript-eslint/parser', 10 10 plugins: [ 11 11 '@typescript-eslint', 12 - 'detox', 13 12 'react', 14 13 'lingui', 15 14 'simple-import-sort',
+2
__e2e__/config.yml
··· 1 + flows: 2 + - "flows/*"
+30
__e2e__/flows/composer-self-label.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: ?users 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + # Post an image with the porn label 13 + - tapOn: 14 + id: "composeFAB" 15 + - inputText: "Post with an image" 16 + - tapOn: 17 + id: "openGalleryBtn" 18 + - tapOn: 19 + id: "labelsBtn" 20 + - tapOn: 21 + label: "Tap on porn" 22 + point: 78%,67% 23 + - tapOn: 24 + label: "Tap on confirm" 25 + point: 51%,92% 26 + - tapOn: 27 + id: "composerPublishBtn" 28 + - tapOn: 29 + id: "e2eRefreshHome" 30 + - assertVisible: "Adult Content"
+87
__e2e__/flows/composer.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: ?users 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + - tapOn: 12 + id: "composeFAB" 13 + - inputText: "Post text only" 14 + - tapOn: 15 + id: "composerPublishBtn" 16 + - assertVisible: 17 + id: "composeFAB" 18 + - tapOn: 19 + id: "composeFAB" 20 + - inputText: "Post with an image" 21 + - tapOn: 22 + id: "openGalleryBtn" 23 + - tapOn: 24 + id: "composerPublishBtn" 25 + - assertVisible: 26 + id: "composeFAB" 27 + - tapOn: 28 + id: "composeFAB" 29 + - inputText: "Post with a https://example.com link card" 30 + - tapOn: 31 + id: "composerPublishBtn" 32 + - assertVisible: 33 + id: "composeFAB" 34 + - tapOn: 35 + id: "e2eRefreshHome" 36 + - tapOn: 37 + id: "replyBtn" 38 + - inputText: "Reply text only" 39 + - tapOn: 40 + id: "composerPublishBtn" 41 + - assertVisible: 42 + id: "composeFAB" 43 + - tapOn: 44 + id: "replyBtn" 45 + - inputText: "Reply with an image" 46 + - tapOn: 47 + id: "openGalleryBtn" 48 + - tapOn: 49 + id: "composerPublishBtn" 50 + - assertVisible: 51 + id: "composeFAB" 52 + - tapOn: 53 + id: "replyBtn" 54 + - inputText: "Reply with a https://example.com link card" 55 + - tapOn: 56 + id: "composerPublishBtn" 57 + - assertVisible: 58 + id: "composeFAB" 59 + - tapOn: 60 + id: "repostBtn" 61 + - tapOn: 62 + id: "quoteBtn" 63 + - inputText: "QP text only" 64 + - tapOn: 65 + id: "composerPublishBtn" 66 + - assertVisible: 67 + id: "composeFAB" 68 + - tapOn: 69 + id: "repostBtn" 70 + - tapOn: 71 + id: "quoteBtn" 72 + - inputText: "QP with an image" 73 + - tapOn: 74 + id: "openGalleryBtn" 75 + - tapOn: 76 + id: "composerPublishBtn" 77 + - assertVisible: 78 + id: "composeFAB" 79 + - tapOn: 80 + id: "repostBtn" 81 + - tapOn: 82 + id: "quoteBtn" 83 + - inputText: "QP with a https://example.com link card" 84 + - tapOn: 85 + id: "composerPublishBtn" 86 + - assertVisible: 87 + id: "composeFAB"
+37
__e2e__/flows/create-account.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eOpenLoggedOutView" 11 + - tapOn: 12 + id: "createAccountButton" 13 + - tapOn: 14 + id: "selectServiceButton" 15 + - tapOn: 16 + id: "customSelectBtn" 17 + - tapOn: 18 + id: "customServerTextInput" 19 + - inputText: "http://localhost:3000" 20 + - pressKey: Enter 21 + - tapOn: 22 + id: "doneBtn" 23 + - tapOn: 24 + id: "emailInput" 25 + - inputText: "example@test.com" 26 + - tapOn: 27 + id: "passwordInput" 28 + - inputText: "hunter2" 29 + - pressKey: Enter 30 + - tapOn: 31 + id: "nextBtn" 32 + - tapOn: 33 + id: "handleInput" 34 + - inputText: "e2e-test" 35 + - tapOn: 36 + id: "nextBtn" 37 +
+208
__e2e__/flows/curate-lists.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users&follows&posts" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + - tapOn: 13 + label: "Create a curate list" 14 + id: "e2eGotoLists" 15 + - tapOn: 16 + id: "newUserListBtn" 17 + - assertVisible: 18 + id: "createOrEditListModal" 19 + - tapOn: 20 + id: "editNameInput" 21 + - inputText: "Good Ppl" 22 + - tapOn: 23 + id: "editDescriptionInput" 24 + - inputText: "They good" 25 + - tapOn: "Save" 26 + - tapOn: "Save" 27 + - assertNotVisible: 28 + id: "createOrEditListModal" 29 + - tapOn: "About" 30 + - assertVisible: "Good Ppl" 31 + - assertVisible: "They good" 32 + 33 + - tapOn: 34 + label: "Edit display name and description via the edit curatelist modal" 35 + point: "90%,9%" 36 + - tapOn: "Edit list details" 37 + - assertVisible: 38 + id: "createOrEditListModal" 39 + - tapOn: 40 + id: "editNameInput" 41 + - eraseText 42 + - inputText: "Bad Ppl" 43 + - hideKeyboard 44 + - tapOn: 45 + id: "editDescriptionInput" 46 + - eraseText 47 + - inputText: "They bad" 48 + - tapOn: "Save" 49 + - tapOn: "Save" 50 + - assertNotVisible: 51 + id: "createOrEditListModal" 52 + - assertVisible: Bad Ppl 53 + - assertVisible: They bad 54 + 55 + - tapOn: 56 + label: "Remove description via the edit curatelist modal" 57 + point: "90%,9%" 58 + - tapOn: "Edit list details" 59 + - assertVisible: 60 + id: "createOrEditListModal" 61 + - tapOn: 62 + id: "editDescriptionInput" 63 + - eraseText 64 + - tapOn: "Save" 65 + - tapOn: "Save" 66 + - assertNotVisible: 67 + id: "createOrEditListModal" 68 + - assertNotVisible: 69 + id: "listDescription" 70 + 71 + - tapOn: 72 + label: "Delete the curatelist" 73 + point: "90%,9%" 74 + - tapOn: "Delete List" 75 + - tapOn: 76 + id: "confirmBtn" 77 + - assertVisible: 78 + id: "listsEmpty" 79 + 80 + - tapOn: 81 + label: "Create a new curatelist" 82 + id: "e2eGotoLists" 83 + - tapOn: 84 + id: "newUserListBtn" 85 + - assertVisible: 86 + id: "createOrEditListModal" 87 + - tapOn: 88 + id: "editNameInput" 89 + - inputText: "Good Ppl" 90 + - tapOn: 91 + id: "editDescriptionInput" 92 + - inputText: "They good" 93 + - tapOn: "Save" 94 + - tapOn: "Save" 95 + - assertNotVisible: 96 + id: "createOrEditListModal" 97 + - tapOn: "About" 98 + - assertVisible: "Good Ppl" 99 + - assertVisible: "They good" 100 + - tapOn: "About" 101 + 102 + - tapOn: 103 + label: "Adds users on curatelists from the list" 104 + id: "addUserBtn" 105 + - assertVisible: 106 + id: "listAddUserModal" 107 + - tapOn: 108 + id: "searchInput" 109 + - inputText: "b" 110 + - pressKey: Enter 111 + - tapOn: 112 + id: "user-bob.test-addBtn" 113 + - tapOn: 114 + id: "doneBtn" 115 + - assertNotVisible: 116 + id: "listAddUserModal" 117 + - assertVisible: 118 + id: "user-bob.test" 119 + 120 + - tapOn: "Posts" 121 + - assertVisible: 122 + label: "Shows posts by the users in the list" 123 + id: "feedItem-by-bob.test" 124 + 125 + - tapOn: 126 + label: "Pins the list" 127 + id: "pinBtn" 128 + - tapOn: 129 + id: "e2eGotoHome" 130 + - tapOn: "Good Ppl" 131 + - assertVisible: 132 + id: "feedItem-by-bob.test" 133 + - tapOn: 134 + id: "bottomBarFeedsBtn" 135 + - tapOn: 136 + id: "saved-feed-Good Ppl" 137 + - assertVisible: 138 + id: "feedItem-by-bob.test" 139 + - tapOn: 140 + id: "unpinBtn" 141 + - tapOn: 142 + id: "bottomBarHomeBtn" 143 + - assertNotVisible: 144 + id: "homeScreenFeedTabs-Good Ppl" 145 + - tapOn: 146 + id: "e2eGotoLists" 147 + - tapOn: 148 + id: "list-Good Ppl" 149 + 150 + - tapOn: "About" 151 + - assertVisible: 152 + label: "Removes users on curatelists from the list" 153 + id: "user-bob.test" 154 + - tapOn: 155 + point: "90%,43%" 156 + - assertVisible: 157 + id: "userAddRemoveListsModal" 158 + - tapOn: 159 + id: "user-bob.test-addBtn" 160 + - tapOn: 161 + id: "doneBtn" 162 + - assertNotVisible: 163 + id: "userAddRemoveListsModal" 164 + 165 + - tapOn: 166 + label: "Shows the curatelist on my profile" 167 + id: "bottomBarProfileBtn" 168 + - swipe: 169 + from: 170 + id: "profilePager-selector" 171 + direction: LEFT 172 + - tapOn: 173 + id: "profilePager-selector-5" 174 + - tapOn: 175 + id: "list-Good Ppl" 176 + 177 + - tapOn: 178 + label: "Adds and removes users on curatelists from the profile" 179 + id: "bottomBarSearchBtn" 180 + - tapOn: 181 + id: "searchTextInput" 182 + - inputText: "bob" 183 + - tapOn: 184 + id: "searchAutoCompleteResult-bob.test" 185 + - assertVisible: 186 + id: "profileView" 187 + - tapOn: 188 + id: "profileHeaderDropdownBtn" 189 + - tapOn: "Add to Lists" 190 + - assertVisible: 191 + id: "userAddRemoveListsModal" 192 + - tapOn: 193 + id: "user-bob.test-addBtn" 194 + - tapOn: 195 + id: "doneBtn" 196 + - assertNotVisible: 197 + id: "userAddRemoveListsModal" 198 + - tapOn: 199 + id: "profileHeaderDropdownBtn" 200 + - tapOn: "Add to Lists" 201 + - assertVisible: 202 + id: "userAddRemoveListsModal" 203 + - tapOn: 204 + id: "user-bob.test-addBtn" 205 + - tapOn: 206 + id: "doneBtn" 207 + - assertNotVisible: 208 + id: "userAddRemoveListsModal"
+63
__e2e__/flows/home-screen.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: ?users&follows&posts&feeds 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + - tapOn: 13 + label: "Can go to feeds page using feeds button in tab bar" 14 + text: "Feeds ✨" 15 + - assertVisible: "Discover New Feeds" 16 + 17 + - tapOn: 18 + label: "Feeds button disappears after pinning a feed" 19 + id: "bottomBarProfileBtn" 20 + - swipe: 21 + from: 22 + id: "profilePager-selector" 23 + direction: LEFT 24 + - tapOn: 25 + id: "profilePager-selector-4" 26 + - tapOn: 27 + id: "feed-alice-favs" 28 + - tapOn: "Pin to Home" 29 + - tapOn: 30 + id: "bottomBarHomeBtn" 31 + - assertNotVisible: "Feeds ✨" 32 + 33 + - tapOn: 34 + label: "Can like posts" 35 + id: "likeBtn" 36 + - assertVisible: 37 + id: "likeCount" 38 + text: "1" 39 + - tapOn: 40 + id: "likeBtn" 41 + - assertNotVisible: 42 + id: "likeCount" 43 + 44 + - tapOn: 45 + label: "Can repost posts" 46 + id: "repostBtn" 47 + - tapOn: "Repost" 48 + - assertVisible: 49 + id: "repostCount" 50 + text: "1" 51 + - tapOn: 52 + id: "repostBtn" 53 + - tapOn: "Undo repost" 54 + - assertNotVisible: 55 + id: "repostCount" 56 + 57 + - tapOn: 58 + label: "Can delete posts" 59 + id: "postDropdownBtn" 60 + childOf: 61 + id: "feedItem-by-alice.test" 62 + - tapOn: "Delete post" 63 + - tapOn: "Delete"
+26
__e2e__/flows/login.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eOpenLoggedOutView" 11 + - tapOn: "Sign in" 12 + - tapOn: 13 + id: "selectServiceButton" 14 + - tapOn: "Custom" 15 + - tapOn: 16 + id: "customServerTextInput" 17 + - inputText: "http://localhost:3000" 18 + - tapOn: "Done" 19 + - tapOn: 20 + id: "loginUsernameInput" 21 + - inputText: "Alice" 22 + - tapOn: 23 + id: "loginPasswordInput" 24 + - inputText: "hunter2" 25 + - pressKey: Enter 26 + - assertVisible: "Following"
+45
__e2e__/flows/mod-lists.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users&follows&labels" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + # create a modlist 13 + - tapOn: 14 + id: "e2eGotoModeration" 15 + - tapOn: 16 + id: "moderationlistsBtn" 17 + - tapOn: "New" 18 + - tapOn: 19 + id: "editNameInput" 20 + - inputText: "Muted Users" 21 + - tapOn: 22 + id: "editDescriptionInput" 23 + - inputText: "Shhh" 24 + - tapOn: "Save" 25 + - tapOn: "Save" 26 + 27 + # view modlist 28 + - assertVisible: "Muted Users" 29 + - assertVisible: "Shhh" 30 + 31 + # toggle mute subscription 32 + - tapOn: 33 + point: "70%,9%" 34 + - tapOn: "Mute accounts" 35 + - tapOn: "Mute list" 36 + - tapOn: "Unmute" 37 + 38 + # toggle block subscription 39 + - tapOn: 40 + point: "70%,9%" 41 + - tapOn: "Block accounts" 42 + - tapOn: "Block list" 43 + - tapOn: "Unblock" 44 + 45 + # the rest of the behaviors are tested in curate-lists.yml
+119
__e2e__/flows/profile-screen-edit.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users&posts&feeds" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + 13 + # Navigate to my profile 14 + - tapOn: 15 + id: "bottomBarProfileBtn" 16 + 17 + # Can see feeds 18 + - swipe: 19 + from: 20 + id: "profilePager-selector" 21 + direction: LEFT 22 + - tapOn: 23 + id: "profilePager-selector-4" 24 + - assertVisible: 25 + id: "feed-alice-favs" 26 + - swipe: 27 + from: 28 + id: "profilePager-selector" 29 + direction: RIGHT 30 + - tapOn: 31 + id: "profilePager-selector-0" 32 + 33 + # Open and close edit profile modal 34 + - tapOn: 35 + id: "profileHeaderEditProfileButton" 36 + - assertVisible: 37 + id: "editProfileModal" 38 + - tapOn: 39 + id: "editProfileCancelBtn" 40 + - assertNotVisible: 41 + id: "editProfileModal" 42 + 43 + # Edit display name and description via the edit profile modal 44 + - tapOn: 45 + id: "profileHeaderEditProfileButton" 46 + - assertVisible: 47 + id: "editProfileModal" 48 + - tapOn: 49 + id: "editProfileDisplayNameInput" 50 + - eraseText 51 + - inputText: "Alicia" 52 + - tapOn: 53 + id: "editProfileDescriptionInput" 54 + - eraseText 55 + - inputText: "One cool hacker" 56 + - tapOn: "Description" 57 + - tapOn: 58 + id: "editProfileSaveBtn" 59 + - assertNotVisible: 60 + id: "editProfileModal" 61 + - assertVisible: "Alicia" 62 + - assertVisible: "One cool hacker" 63 + 64 + # Remove display name and description via the edit profile modal 65 + - tapOn: 66 + id: "profileHeaderEditProfileButton" 67 + - assertVisible: 68 + id: "editProfileModal" 69 + - tapOn: 70 + id: "editProfileDisplayNameInput" 71 + - eraseText 72 + - tapOn: 73 + id: "editProfileDescriptionInput" 74 + - eraseText 75 + - tapOn: "Description" 76 + - tapOn: 77 + id: "editProfileSaveBtn" 78 + - assertNotVisible: 79 + id: "editProfileModal" 80 + - assertVisible: "alice.test" 81 + - assertNotVisible: "One cool hacker" 82 + 83 + # Set avi and banner via the edit profile modal 84 + - assertVisible: 85 + id: "userBannerFallback" 86 + - tapOn: 87 + id: "profileHeaderEditProfileButton" 88 + - assertVisible: 89 + id: "editProfileModal" 90 + - tapOn: 91 + id: "changeBannerBtn" 92 + - tapOn: "Upload from Library" 93 + - tapOn: 94 + id: "changeAvatarBtn" 95 + - tapOn: "Upload from Library" 96 + - tapOn: 97 + id: "editProfileSaveBtn" 98 + - assertNotVisible: 99 + id: "editProfileModal" 100 + - assertVisible: 101 + id: "userBannerImage" 102 + 103 + # # Remove avi and banner via the edit profile modal 104 + - tapOn: 105 + id: "profileHeaderEditProfileButton" 106 + - assertVisible: 107 + id: "editProfileModal" 108 + - tapOn: 109 + id: "changeBannerBtn" 110 + - tapOn: "Remove Banner" 111 + - tapOn: 112 + id: "changeAvatarBtn" 113 + - tapOn: "Remove Avatar" 114 + - tapOn: 115 + id: "editProfileSaveBtn" 116 + - assertNotVisible: 117 + id: "editProfileModal" 118 + - assertVisible: 119 + id: "userBannerFallback"
+37
__e2e__/flows/profile-screen.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users&posts&feeds" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + # Navigate to another user profile 13 + - tapOn: 14 + id: "bottomBarSearchBtn" 15 + - tapOn: 16 + id: "searchTextInput" 17 + - inputText: "b" 18 + - tapOn: 19 + id: "searchAutoCompleteResult-bob.test" 20 + - assertVisible: 21 + id: "profileView" 22 + 23 + # Can follow/unfollow another user 24 + - tapOn: 25 + id: "followBtn" 26 + - tapOn: 27 + id: "unfollowBtn" 28 + 29 + # Can mute/unmute another user 30 + - tapOn: 31 + id: "profileHeaderDropdownBtn" 32 + - tapOn: "Mute Account" 33 + - assertVisible: "Account Muted" 34 + - tapOn: 35 + id: "profileHeaderDropdownBtn" 36 + - tapOn: "Unmute Account" 37 + - assertNotVisible: "Account Muted"
+22
__e2e__/flows/search-screen.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + # Navigate to another user profile via autocomplete 13 + - tapOn: 14 + id: "bottomBarSearchBtn" 15 + - tapOn: 16 + id: "searchTextInput" 17 + - inputText: "b" 18 + - tapOn: 19 + id: "searchAutoCompleteResult-bob.test" 20 + - assertVisible: 21 + id: "profileView" 22 +
+82
__e2e__/flows/thread-muting.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users&follows" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + 10 + 11 + # Login, create a thread, and log out 12 + - tapOn: 13 + id: "e2eSignInAlice" 14 + - tapOn: 15 + id: "composeFAB" 16 + - inputText: "Test thread" 17 + - tapOn: 18 + id: "composerPublishBtn" 19 + 20 + # Login, reply to the thread, and log out 21 + - tapOn: 22 + id: "e2eSignInBob" 23 + - tapOn: 24 + id: "replyBtn" 25 + - inputText: "Reply 1" 26 + - tapOn: 27 + id: "composerPublishBtn" 28 + 29 + # Login, confirm notification exists, mute thread, and log out 30 + - tapOn: 31 + id: "e2eSignInAlice" 32 + - tapOn: 33 + id: "bottomBarNotificationsBtn" 34 + - assertVisible: 35 + id: "feedItem-by-bob.test" 36 + - tapOn: 37 + id: "feedItem-by-bob.test" 38 + - tapOn: 39 + id: "postDropdownBtn" 40 + childOf: 41 + id: "postThreadItem-by-bob.test" 42 + - tapOn: "Mute thread" 43 + 44 + # Login, reply to the thread twice, and log out 45 + - tapOn: 46 + id: "e2eSignInBob" 47 + - tapOn: 48 + id: "bottomBarProfileBtn" 49 + - tapOn: 50 + id: "profilePager-selector-1" 51 + - tapOn: 52 + id: "replyBtn" 53 + - inputText: "Reply 2" 54 + - tapOn: 55 + id: "composerPublishBtn" 56 + - tapOn: 57 + id: "replyBtn" 58 + - inputText: "Reply 3" 59 + - tapOn: 60 + id: "composerPublishBtn" 61 + 62 + 63 + # Login, confirm notifications dont exist, unmute the thread, confirm notifications exist 64 + - tapOn: 65 + id: "e2eSignInAlice" 66 + - tapOn: 67 + id: "bottomBarNotificationsBtn" 68 + - assertNotVisible: 69 + id: "feedItem-by-bob.test" 70 + - tapOn: 71 + id: "bottomBarHomeBtn" 72 + - tapOn: 73 + id: "postDropdownBtn" 74 + - tapOn: "Unmute thread" 75 + - tapOn: 76 + id: "bottomBarNotificationsBtn" 77 + - swipe: 78 + from: 79 + id: "notifsFeed" 80 + direction: DOWN 81 + - assertVisible: 82 + id: "feedItem-by-bob.test"
+84
__e2e__/flows/thread-screen.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - runScript: 4 + file: ../setupServer.js 5 + env: 6 + SERVER_PATH: "?users&follows&thread" 7 + - runFlow: 8 + file: ../setupApp.yml 9 + - tapOn: 10 + id: "e2eSignInAlice" 11 + 12 + 13 + # Navigate to thread 14 + - tapOn: "Thread root" 15 + - assertVisible: "Thread reply" 16 + 17 + # Can like the root post 18 + - tapOn: 19 + id: "likeBtn" 20 + childOf: 21 + id: "postThreadItem-by-bob.test" 22 + - assertVisible: 23 + id: "likeCount-expanded" 24 + - tapOn: 25 + id: "likeBtn" 26 + childOf: 27 + id: "postThreadItem-by-bob.test" 28 + - assertNotVisible: 29 + id: "likeCount-expanded" 30 + 31 + # Can like a reply post 32 + - tapOn: 33 + id: "likeBtn" 34 + childOf: 35 + id: "postThreadItem-by-carla.test" 36 + - assertVisible: 37 + id: "likeCount" 38 + childOf: 39 + id: "postThreadItem-by-carla.test" 40 + - tapOn: 41 + id: "likeBtn" 42 + childOf: 43 + id: "postThreadItem-by-carla.test" 44 + - assertNotVisible: 45 + id: "likeCount" 46 + childOf: 47 + id: "postThreadItem-by-carla.test" 48 + 49 + # Can repost the root post 50 + - tapOn: 51 + id: "repostBtn" 52 + childOf: 53 + id: "postThreadItem-by-bob.test" 54 + - tapOn: "Repost" 55 + - assertVisible: 56 + id: "repostCount-expanded" 57 + - tapOn: 58 + id: "repostBtn" 59 + childOf: 60 + id: "postThreadItem-by-bob.test" 61 + - tapOn: "Undo repost" 62 + - assertNotVisible: 63 + id: "repostCount-expanded" 64 + 65 + 66 + # Can repost a reply post 67 + - tapOn: 68 + id: "repostBtn" 69 + childOf: 70 + id: "postThreadItem-by-carla.test" 71 + - tapOn: "Repost" 72 + - assertVisible: 73 + id: "repostCount" 74 + childOf: 75 + id: "postThreadItem-by-carla.test" 76 + - tapOn: 77 + id: "repostBtn" 78 + childOf: 79 + id: "postThreadItem-by-carla.test" 80 + - tapOn: "Undo repost" 81 + - assertNotVisible: 82 + id: "repostCount" 83 + childOf: 84 + id: "postThreadItem-by-carla.test"
-12
__e2e__/jest.config.js
··· 1 - /** @type {import('@jest/types').Config.InitialOptions} */ 2 - module.exports = { 3 - rootDir: '..', 4 - testMatch: ['<rootDir>/__e2e__/**/*.test.ts'], 5 - testTimeout: 120000, 6 - maxWorkers: 1, 7 - globalSetup: 'detox/runners/jest/globalSetup', 8 - globalTeardown: 'detox/runners/jest/globalTeardown', 9 - reporters: ['detox/runners/jest/reporter'], 10 - testEnvironment: 'detox/runners/jest/testEnvironment', 11 - verbose: true, 12 - }
+1 -1
__e2e__/maestro/scroll.yaml __e2e__/perf-test.yml
··· 1 + 1 2 # flow.yaml 2 3 3 4 appId: xyz.blueskyweb.app ··· 74 75 - "scroll" 75 76 - "scroll" 76 77 - "scroll" 77 -
+11
__e2e__/setupApp.yml
··· 1 + appId: xyz.blueskyweb.app 2 + --- 3 + - launchApp: 4 + appId: "xyz.blueskyweb.app" 5 + clearState: true 6 + - waitForAnimationToEnd 7 + - tapOn: "http://localhost:8081" 8 + - waitForAnimationToEnd 9 + - swipe: 10 + from: "Bluesky" 11 + direction: DOWN
+5
__e2e__/setupServer.js
··· 1 + // eslint-disable-next-line 2 + http.post('http://localhost:1986/' + SERVER_PATH, { 3 + headers: {'Content-Type': 'text/plain'}, 4 + body: '', 5 + })
-109
__e2e__/tests/composer.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {beforeAll, describe, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - 6 - import {createServer, loginAsAlice, openApp, sleep} from '../util' 7 - 8 - describe('Composer', () => { 9 - beforeAll(async () => { 10 - await createServer('?users') 11 - await openApp({ 12 - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, 13 - }) 14 - }) 15 - 16 - it('Login', async () => { 17 - await loginAsAlice() 18 - await element(by.id('homeScreenFeedTabs-Following')).tap() 19 - }) 20 - 21 - it('Post text only', async () => { 22 - await element(by.id('composeFAB')).tap() 23 - await device.takeScreenshot('1- opened composer') 24 - await element(by.id('composerTextInput')).typeText('Post text only') 25 - await device.takeScreenshot('2- entered text') 26 - await element(by.id('composerPublishBtn')).tap() 27 - await device.takeScreenshot('3- opened general section') 28 - await expect(element(by.id('composeFAB'))).toBeVisible() 29 - }) 30 - 31 - it('Post with an image', async () => { 32 - await element(by.id('composeFAB')).tap() 33 - await element(by.id('composerTextInput')).typeText('Post with an image') 34 - await element(by.id('openGalleryBtn')).tap() 35 - await sleep(1e3) 36 - await element(by.id('composerPublishBtn')).tap() 37 - await expect(element(by.id('composeFAB'))).toBeVisible() 38 - }) 39 - 40 - it('Post with a link card', async () => { 41 - await element(by.id('composeFAB')).tap() 42 - await element(by.id('composerTextInput')).typeText( 43 - 'Post with a https://example.com link card', 44 - ) 45 - await element(by.id('composerPublishBtn')).tap() 46 - await expect(element(by.id('composeFAB'))).toBeVisible() 47 - }) 48 - 49 - it('Reply text only', async () => { 50 - await element(by.id('e2eRefreshHome')).tap() 51 - 52 - const post = by.id('feedItem-by-alice.test') 53 - await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() 54 - await element(by.id('composerTextInput')).typeText('Reply text only') 55 - await element(by.id('composerPublishBtn')).tap() 56 - await expect(element(by.id('composeFAB'))).toBeVisible() 57 - }) 58 - 59 - it('Reply with an image', async () => { 60 - const post = by.id('feedItem-by-alice.test') 61 - await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() 62 - await element(by.id('composerTextInput')).typeText('Reply with an image') 63 - await element(by.id('openGalleryBtn')).tap() 64 - await sleep(1e3) 65 - await element(by.id('composerPublishBtn')).tap() 66 - await expect(element(by.id('composeFAB'))).toBeVisible() 67 - }) 68 - 69 - it('Reply with a link card', async () => { 70 - const post = by.id('feedItem-by-alice.test') 71 - await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() 72 - await element(by.id('composerTextInput')).typeText( 73 - 'Reply with a https://example.com link card', 74 - ) 75 - await element(by.id('composerPublishBtn')).tap() 76 - await expect(element(by.id('composeFAB'))).toBeVisible() 77 - }) 78 - 79 - it('QP text only', async () => { 80 - const post = by.id('feedItem-by-alice.test') 81 - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() 82 - await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() 83 - await element(by.id('composerTextInput')).typeText('QP text only') 84 - await element(by.id('composerPublishBtn')).tap() 85 - await expect(element(by.id('composeFAB'))).toBeVisible() 86 - }) 87 - 88 - it('QP with an image', async () => { 89 - const post = by.id('feedItem-by-alice.test') 90 - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() 91 - await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() 92 - await element(by.id('composerTextInput')).typeText('QP with an image') 93 - await element(by.id('openGalleryBtn')).tap() 94 - await sleep(1e3) 95 - await element(by.id('composerPublishBtn')).tap() 96 - await expect(element(by.id('composeFAB'))).toBeVisible() 97 - }) 98 - 99 - it('QP with a link card', async () => { 100 - const post = by.id('feedItem-by-alice.test') 101 - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() 102 - await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() 103 - await element(by.id('composerTextInput')).typeText( 104 - 'QP with a https://example.com link card', 105 - ) 106 - await element(by.id('composerPublishBtn')).tap() 107 - await expect(element(by.id('composeFAB'))).toBeVisible() 108 - }) 109 - })
-39
__e2e__/tests/create-account.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {describe, beforeAll, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - import {openApp, createServer} from '../util' 6 - 7 - describe('Create account', () => { 8 - let service: string 9 - beforeAll(async () => { 10 - service = await createServer('') 11 - await openApp({permissions: {notifications: 'YES'}}) 12 - }) 13 - 14 - it('I can create a new account', async () => { 15 - await element(by.id('e2eOpenLoggedOutView')).tap() 16 - 17 - await element(by.id('createAccountButton')).tap() 18 - await device.takeScreenshot('1- opened create account screen') 19 - await element(by.id('selectServiceButton')).tap() 20 - await device.takeScreenshot('2- selected other server') 21 - await element(by.id('customSelectBtn')).tap() 22 - await element(by.id('customServerTextInput')).typeText(service) 23 - await element(by.id('customServerTextInput')).tapReturnKey() 24 - await element(by.id('doneBtn')).tap() 25 - await device.takeScreenshot('3- input test server URL') 26 - await element(by.id('emailInput')).typeText('example@test.com') 27 - await element(by.id('passwordInput')).typeText('hunter2') 28 - await device.takeScreenshot('4- entered account details') 29 - 30 - await element(by.id('nextBtn')).tap() 31 - 32 - await element(by.id('handleInput')).typeText('e2e-test') 33 - await device.takeScreenshot('5- entered handle') 34 - 35 - await element(by.id('nextBtn')).tap() 36 - 37 - await expect(element(by.id('onboardingInterests'))).toBeVisible() 38 - }) 39 - })
-213
__e2e__/tests/curate-lists.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {beforeAll, describe, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - 6 - import {createServer, loginAsAlice, loginAsBob, openApp, sleep} from '../util' 7 - 8 - describe('Curate lists', () => { 9 - beforeAll(async () => { 10 - await createServer('?users&follows&posts') 11 - await openApp({ 12 - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, 13 - }) 14 - }) 15 - 16 - it('Login and create a curatelists', async () => { 17 - await loginAsAlice() 18 - await element(by.id('e2eGotoLists')).tap() 19 - await element(by.id('newUserListBtn')).tap() 20 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 21 - await element(by.id('editNameInput')).typeText('Good Ppl') 22 - await element(by.id('editDescriptionInput')).typeText('They good') 23 - await element(by.id('saveBtn')).tap() 24 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 25 - await element(by.text('About')).tap() 26 - await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl') 27 - await expect(element(by.id('listDescription'))).toHaveText('They good') 28 - }) 29 - 30 - it('Edit display name and description via the edit curatelist modal', async () => { 31 - await element(by.id('headerDropdownBtn')).tap() 32 - await element(by.text('Edit list details')).tap() 33 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 34 - await element(by.id('editNameInput')).clearText() 35 - await element(by.id('editNameInput')).typeText('Bad Ppl') 36 - await element(by.id('editDescriptionInput')).clearText() 37 - await element(by.id('editDescriptionInput')).typeText('They bad') 38 - await element(by.id('saveBtn')).tap() 39 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 40 - await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') 41 - await expect(element(by.id('listDescription'))).toHaveText('They bad') 42 - // have to wait for the toast to clear 43 - await waitFor(element(by.id('headerDropdownBtn'))) 44 - .toBeVisible() 45 - .withTimeout(5000) 46 - }) 47 - 48 - it('Remove description via the edit curatelist modal', async () => { 49 - await element(by.id('headerDropdownBtn')).tap() 50 - await element(by.text('Edit list details')).tap() 51 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 52 - await element(by.id('editDescriptionInput')).clearText() 53 - await element(by.id('saveBtn')).tap() 54 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 55 - await expect(element(by.id('listDescription'))).not.toBeVisible() 56 - // have to wait for the toast to clear 57 - await waitFor(element(by.id('headerDropdownBtn'))) 58 - .toBeVisible() 59 - .withTimeout(5000) 60 - }) 61 - 62 - it('Set avi via the edit curatelist modal', async () => { 63 - await expect(element(by.id('userAvatarFallback'))).toExist() 64 - await element(by.id('headerDropdownBtn')).tap() 65 - await element(by.text('Edit list details')).tap() 66 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 67 - await element(by.id('changeAvatarBtn')).tap() 68 - await element(by.text('Upload from Library')).tap() 69 - await sleep(3e3) 70 - await element(by.id('saveBtn')).tap() 71 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 72 - await expect(element(by.id('userAvatarImage'))).toExist() 73 - // have to wait for the toast to clear 74 - await waitFor(element(by.id('headerDropdownBtn'))) 75 - .toBeVisible() 76 - .withTimeout(5000) 77 - }) 78 - 79 - it('Remove avi via the edit curatelist modal', async () => { 80 - await expect(element(by.id('userAvatarImage'))).toExist() 81 - await element(by.id('headerDropdownBtn')).tap() 82 - await element(by.text('Edit list details')).tap() 83 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 84 - await element(by.id('changeAvatarBtn')).tap() 85 - await element(by.text('Remove Avatar')).tap() 86 - await element(by.id('saveBtn')).tap() 87 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 88 - await expect(element(by.id('userAvatarFallback'))).toExist() 89 - // have to wait for the toast to clear 90 - await waitFor(element(by.id('headerDropdownBtn'))) 91 - .toBeVisible() 92 - .withTimeout(5000) 93 - }) 94 - 95 - it('Delete the curatelist', async () => { 96 - await element(by.id('headerDropdownBtn')).tap() 97 - await element(by.text('Delete List')).tap() 98 - await element(by.id('confirmBtn')).tap() 99 - await expect(element(by.id('listsEmpty'))).toBeVisible() 100 - }) 101 - 102 - it('Create a new curatelist', async () => { 103 - await element(by.id('e2eGotoLists')).tap() 104 - await element(by.id('newUserListBtn')).tap() 105 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 106 - await element(by.id('editNameInput')).typeText('Good Ppl') 107 - await element(by.id('editDescriptionInput')).typeText('They good') 108 - await element(by.id('saveBtn')).tap() 109 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 110 - await element(by.text('About')).tap() 111 - await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl') 112 - await expect(element(by.id('listDescription'))).toHaveText('They good') 113 - }) 114 - 115 - it('Adds users on curatelists from the list', async () => { 116 - await element(by.text('About')).tap() 117 - await element(by.id('addUserBtn')).tap() 118 - await expect(element(by.id('listAddUserModal'))).toBeVisible() 119 - await element(by.id('searchInput')).typeText('b') 120 - await waitFor(element(by.id('user-bob.test-addBtn'))) 121 - .toBeVisible() 122 - .withTimeout(5000) 123 - await element(by.id('user-bob.test-addBtn')).tap() 124 - await element(by.id('doneBtn')).tap() 125 - await expect(element(by.id('listAddUserModal'))).not.toBeVisible() 126 - await expect(element(by.id('user-bob.test'))).toBeVisible() 127 - }) 128 - 129 - it('Shows posts by the users in the list', async () => { 130 - await element(by.text('Posts')).tap() 131 - await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() 132 - }) 133 - 134 - it('Pins the list', async () => { 135 - await expect(element(by.id('pinBtn'))).toBeVisible() 136 - await element(by.id('pinBtn')).tap() 137 - await element(by.id('e2eGotoHome')).tap() 138 - await element(by.id('homeScreenFeedTabs-Good Ppl')).tap() 139 - await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() 140 - 141 - await element(by.id('bottomBarFeedsBtn')).tap() 142 - await element(by.id('saved-feed-Good Ppl')).tap() 143 - await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() 144 - 145 - await element(by.id('unpinBtn')).tap() 146 - await element(by.id('bottomBarHomeBtn')).tap() 147 - await expect( 148 - element(by.id('homeScreenFeedTabs-Good Ppl')), 149 - ).not.toBeVisible() 150 - 151 - await element(by.id('e2eGotoLists')).tap() 152 - await element(by.id('list-Good Ppl')).tap() 153 - }) 154 - 155 - it('Removes users on curatelists from the list', async () => { 156 - await element(by.text('About')).tap() 157 - await expect(element(by.id('user-bob.test'))).toBeVisible() 158 - await element(by.id('user-bob.test-editBtn')).tap() 159 - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() 160 - await element(by.id('user-bob.test-addBtn')).tap() 161 - await element(by.id('doneBtn')).tap() 162 - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() 163 - }) 164 - 165 - it('Shows the curatelist on my profile', async () => { 166 - await element(by.id('bottomBarProfileBtn')).tap() 167 - await element(by.id('profilePager-selector')).swipe('left') 168 - await element(by.id('profilePager-selector-5')).tap() 169 - await element(by.id('list-Good Ppl')).tap() 170 - }) 171 - 172 - it('Adds and removes users on curatelists from the profile', async () => { 173 - await element(by.id('bottomBarSearchBtn')).tap() 174 - await element(by.id('searchTextInput')).typeText('bob') 175 - await element(by.id('searchAutoCompleteResult-bob.test')).tap() 176 - await expect(element(by.id('profileView'))).toBeVisible() 177 - 178 - await element(by.id('profileHeaderDropdownBtn')).tap() 179 - await element(by.text('Add to Lists')).tap() 180 - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() 181 - await element(by.id('user-bob.test-addBtn')).tap() 182 - await element(by.id('doneBtn')).tap() 183 - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() 184 - 185 - await element(by.id('profileHeaderDropdownBtn')).tap() 186 - await element(by.text('Add to Lists')).tap() 187 - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() 188 - await element(by.id('user-bob.test-addBtn')).tap() 189 - await element(by.id('doneBtn')).tap() 190 - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() 191 - }) 192 - 193 - it('Can report a user list', async () => { 194 - await element(by.id('e2eGotoSettings')).tap() 195 - await element(by.id('signOutBtn')).tap() 196 - await loginAsBob() 197 - await element(by.id('bottomBarSearchBtn')).tap() 198 - await element(by.id('searchTextInput')).typeText('alice') 199 - await element(by.id('searchAutoCompleteResult-alice.test')).tap() 200 - await element(by.id('profilePager-selector')).swipe('left') 201 - await element(by.id('profilePager-selector-3')).tap() 202 - await element(by.id('list-Good Ppl')).tap() 203 - await element(by.id('headerDropdownBtn')).tap() 204 - await element(by.text('Report List')).tap() 205 - await expect(element(by.id('reportModal'))).toBeVisible() 206 - await expect(element(by.text('Report List'))).toBeVisible() 207 - await element( 208 - by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'), 209 - ).tap() 210 - await element(by.id('sendReportBtn')).tap() 211 - await expect(element(by.id('reportModal'))).not.toBeVisible() 212 - }) 213 - })
-110
__e2e__/tests/home-screen.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {beforeAll, describe, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - 6 - import {createServer, loginAsAlice, openApp} from '../util' 7 - 8 - describe('Home screen', () => { 9 - beforeAll(async () => { 10 - await createServer('?users&follows&posts&feeds') 11 - await openApp({permissions: {notifications: 'YES'}}) 12 - }) 13 - 14 - it('Login', async () => { 15 - await loginAsAlice() 16 - await element(by.id('homeScreenFeedTabs-Following')).tap() 17 - }) 18 - 19 - it('Can go to feeds page using feeds button in tab bar', async () => { 20 - await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap() 21 - await expect(element(by.text('Discover New Feeds'))).toBeVisible() 22 - }) 23 - 24 - it('Feeds button disappears after pinning a feed', async () => { 25 - await element(by.id('bottomBarProfileBtn')).tap() 26 - await element(by.id('profilePager-selector')).swipe('left') 27 - await element(by.id('profilePager-selector-4')).tap() 28 - await element(by.id('feed-alice-favs')).tap() 29 - await element(by.id('pinBtn')).tap() 30 - await element(by.id('bottomBarHomeBtn')).tap() 31 - await expect( 32 - element(by.id('homeScreenFeedTabs-Feeds ✨')), 33 - ).not.toBeVisible() 34 - }) 35 - 36 - it('Can like posts', async () => { 37 - const carlaPosts = by.id('feedItem-by-carla.test') 38 - await expect( 39 - element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), 40 - ).not.toExist() 41 - await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() 42 - await expect( 43 - element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), 44 - ).toHaveText('1') 45 - await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() 46 - await expect( 47 - element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), 48 - ).not.toExist() 49 - }) 50 - 51 - it('Can repost posts', async () => { 52 - const carlaPosts = by.id('feedItem-by-carla.test') 53 - await expect( 54 - element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), 55 - ).not.toExist() 56 - await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap() 57 - await expect(element(by.id('repostModal'))).toBeVisible() 58 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 59 - await expect(element(by.id('repostModal'))).not.toBeVisible() 60 - await expect( 61 - element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), 62 - ).toHaveText('1') 63 - await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap() 64 - await expect(element(by.id('repostModal'))).toBeVisible() 65 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 66 - await expect(element(by.id('repostModal'))).not.toBeVisible() 67 - await expect( 68 - element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), 69 - ).not.toExist() 70 - }) 71 - 72 - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf 73 - // it('Can report posts', async () => { 74 - // const carlaPosts = by.id('feedItem-by-carla.test') 75 - // await element(by.id('postDropdownBtn').withAncestor(carlaPosts)) 76 - // .atIndex(0) 77 - // .tap() 78 - // await element(by.text('Report post')).tap() 79 - // await element(by.id('com.atproto.moderation.defs#reasonSpam')).tap() 80 - // await element(by.id('sendReportBtn')).tap() 81 - // }) 82 - 83 - it('Can swipe between feeds', async () => { 84 - await element(by.id('homeScreen')).swipe('left', 'fast', 0.75) 85 - await expect(element(by.id('customFeedPage'))).toBeVisible() 86 - await element(by.id('homeScreen')).swipe('right', 'fast', 0.75) 87 - await expect(element(by.id('followingFeedPage'))).toBeVisible() 88 - }) 89 - 90 - it('Can tap between feeds', async () => { 91 - await element(by.id('homeScreenFeedTabs-alice-favs')).tap() 92 - await expect(element(by.id('customFeedPage'))).toBeVisible() 93 - await element(by.id('homeScreenFeedTabs-Following')).tap() 94 - await expect(element(by.id('followingFeedPage'))).toBeVisible() 95 - }) 96 - 97 - it('Can delete posts', async () => { 98 - const alicePosts = by.id('feedItem-by-alice.test') 99 - await expect(element(alicePosts.withDescendant(by.text('Post')))).toExist() 100 - await element(by.id('postDropdownBtn').withAncestor(alicePosts)) 101 - .atIndex(0) 102 - .tap() 103 - await element(by.text('Delete post')).tap() 104 - await expect(element(by.id('confirmModal'))).toBeVisible() 105 - await element(by.id('confirmBtn')).tap() 106 - await expect( 107 - element(alicePosts.withDescendant(by.text('Post'))), 108 - ).not.toExist() 109 - }) 110 - })
-47
__e2e__/tests/invite-codes.test.skip.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {beforeAll, describe, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - 6 - import {createServer, loginAsAlice, openApp} from '../util' 7 - 8 - describe('invite-codes', () => { 9 - let service: string 10 - let inviteCode = '' 11 - beforeAll(async () => { 12 - service = await createServer('?users&invite') 13 - await openApp({permissions: {notifications: 'YES'}}) 14 - }) 15 - 16 - it('I can fetch invite codes', async () => { 17 - await loginAsAlice() 18 - await element(by.id('e2eOpenInviteCodesModal')).tap() 19 - await expect(element(by.id('inviteCodesModal'))).toBeVisible() 20 - const attrs = await element(by.id('inviteCode-0-code')).getAttributes() 21 - inviteCode = attrs.text 22 - await element(by.id('closeBtn')).tap() 23 - await element(by.id('e2eSignOut')).tap() 24 - }) 25 - 26 - it('I can create a new account with the invite code', async () => { 27 - await element(by.id('e2eOpenLoggedOutView')).tap() 28 - await element(by.id('createAccountButton')).tap() 29 - await device.takeScreenshot('1- opened create account screen') 30 - await element(by.id('selectServiceButton')).tap() 31 - await device.takeScreenshot('2- selected other server') 32 - await element(by.id('customSelectBtn')).tap() 33 - await element(by.id('customServerTextInput')).typeText(service) 34 - await element(by.id('customServerTextInput')).tapReturnKey() 35 - await element(by.id('doneBtn')).tap() 36 - await device.takeScreenshot('3- input test server URL') 37 - await element(by.id('inviteCodeInput')).typeText(inviteCode) 38 - await element(by.id('emailInput')).typeText('example@test.com') 39 - await element(by.id('passwordInput')).typeText('hunter2') 40 - await device.takeScreenshot('4- entered account details') 41 - await element(by.id('nextBtn')).tap() 42 - await element(by.id('handleInput')).typeText('e2e-test') 43 - await device.takeScreenshot('4- entered handle') 44 - await element(by.id('nextBtn')).tap() 45 - await expect(element(by.id('onboardingInterests'))).toBeVisible() 46 - }) 47 - })
-23
__e2e__/tests/login.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {describe, beforeAll, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - import {openApp, login, createServer} from '../util' 6 - 7 - describe('Login', () => { 8 - let service: string 9 - beforeAll(async () => { 10 - service = await createServer('?users') 11 - await openApp({permissions: {notifications: 'YES'}}) 12 - }) 13 - 14 - it('As Alice, I can login', async () => { 15 - await element(by.id('e2eOpenLoggedOutView')).tap() 16 - 17 - await expect(element(by.id('signInButton'))).toBeVisible() 18 - await login(service, 'alice', 'hunter2', { 19 - takeScreenshots: true, 20 - }) 21 - await device.takeScreenshot('5- opened home screen') 22 - }) 23 - })
-163
__e2e__/tests/merge-feed.test.skip.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {describe, beforeAll, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - import {openApp, loginAsAlice, createServer} from '../util' 6 - 7 - describe('Mergefeed', () => { 8 - beforeAll(async () => { 9 - await createServer('?mergefeed') 10 - await openApp({permissions: {notifications: 'YES'}}) 11 - }) 12 - 13 - it('Login', async () => { 14 - await element(by.id('e2eOpenLoggedOutView')).tap() 15 - await loginAsAlice() 16 - await element(by.id('e2eToggleMergefeed')).tap() 17 - await element(by.id('bottomBarFeedsBtn')).tap() 18 - await element(by.id('feed-alice-favs-toggleSave')).tap() 19 - await element(by.id('e2eGotoHome')).tap() 20 - }) 21 - 22 - it('Sees the expected mix of posts with default filters', async () => { 23 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 24 - 'down', 25 - 'slow', 26 - 1, 27 - 0.5, 28 - 0.5, 29 - ) 30 - // followed users 31 - await expect( 32 - element( 33 - by.id('postText').withAncestor(by.id('feedItem-by-carla.test')), 34 - ).atIndex(0), 35 - ).toHaveText('Post 9') 36 - await expect( 37 - element( 38 - by.id('postText').withAncestor(by.id('feedItem-by-bob.test')), 39 - ).atIndex(0), 40 - ).toHaveText('Post 9') 41 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 42 - 'up', 43 - 'fast', 44 - 1, 45 - 0.5, 46 - 0.5, 47 - ) 48 - // feed users 49 - await expect( 50 - element( 51 - by.id('postText').withAncestor(by.id('feedItem-by-dan.test')), 52 - ).atIndex(0), 53 - ).toHaveText('Post 0') 54 - }) 55 - 56 - it('Sees the expected mix of posts with replies disabled', async () => { 57 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 58 - 'down', 59 - 'fast', 60 - 1, 61 - 0.5, 62 - 0.5, 63 - ) 64 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 65 - 'down', 66 - 'fast', 67 - 1, 68 - 0.5, 69 - 0.5, 70 - ) 71 - await element(by.id('viewHeaderHomeFeedPrefsBtn')).tap() 72 - await element(by.id('toggleRepliesBtn')).tap() 73 - await element(by.id('confirmBtn')).tap() 74 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 75 - 'down', 76 - 'slow', 77 - 1, 78 - 0.5, 79 - 0.5, 80 - ) 81 - 82 - // followed users 83 - await expect( 84 - element( 85 - by.id('postText').withAncestor(by.id('feedItem-by-carla.test')), 86 - ).atIndex(0), 87 - ).toHaveText('Post 9') 88 - await expect( 89 - element( 90 - by.id('postText').withAncestor(by.id('feedItem-by-bob.test')), 91 - ).atIndex(0), 92 - ).toHaveText('Post 9') 93 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 94 - 'up', 95 - 'fast', 96 - 1, 97 - 0.5, 98 - 0.5, 99 - ) 100 - 101 - // feed users 102 - await expect( 103 - element( 104 - by.id('postText').withAncestor(by.id('feedItem-by-dan.test')), 105 - ).atIndex(0), 106 - ).toHaveText('Post 0') 107 - }) 108 - 109 - it('Sees the expected mix of posts with no follows', async () => { 110 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 111 - 'down', 112 - 'fast', 113 - 1, 114 - 0.5, 115 - 0.5, 116 - ) 117 - 118 - await element(by.id('bottomBarSearchBtn')).tap() 119 - await element(by.id('searchTextInput')).typeText('bob') 120 - await element(by.id('searchAutoCompleteResult-bob.test')).tap() 121 - await expect(element(by.id('profileView'))).toBeVisible() 122 - await element(by.id('unfollowBtn')).tap() 123 - await element(by.id('profileHeaderBackBtn')).tap() 124 - 125 - // have to wait for the toast to clear 126 - await waitFor(element(by.id('searchTextInputClearBtn'))) 127 - .toBeVisible() 128 - .withTimeout(5000) 129 - await element(by.id('searchTextInputClearBtn')).tap() 130 - await element(by.id('searchTextInput')).typeText('carla') 131 - await element(by.id('searchAutoCompleteResult-carla.test')).tap() 132 - await expect(element(by.id('profileView'))).toBeVisible() 133 - await element(by.id('unfollowBtn')).tap() 134 - await element(by.id('profileHeaderBackBtn')).tap() 135 - 136 - await element(by.id('bottomBarHomeBtn')).tap() 137 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 138 - 'down', 139 - 'slow', 140 - 1, 141 - 0.5, 142 - 0.5, 143 - ) 144 - await element(by.id('followingFeedPage-feed-flatlist')).swipe( 145 - 'down', 146 - 'slow', 147 - 1, 148 - 0.5, 149 - 0.5, 150 - ) 151 - 152 - // followed users NOT present 153 - await expect(element(by.id('feedItem-by-carla.test'))).not.toExist() 154 - await expect(element(by.id('feedItem-by-bob.test'))).not.toExist() 155 - 156 - // feed users 157 - await expect( 158 - element( 159 - by.id('postText').withAncestor(by.id('feedItem-by-dan.test')), 160 - ).atIndex(0), 161 - ).toHaveText('Post 0') 162 - }) 163 - })
-189
__e2e__/tests/mod-lists.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {describe, beforeAll, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - import {openApp, loginAsAlice, loginAsBob, createServer} from '../util' 6 - 7 - describe('Mod lists', () => { 8 - beforeAll(async () => { 9 - await createServer('?users&follows&labels') 10 - await openApp({ 11 - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, 12 - }) 13 - }) 14 - 15 - it('Login and view my modlists', async () => { 16 - await loginAsAlice() 17 - await element(by.id('e2eGotoModeration')).tap() 18 - await element(by.id('moderationlistsBtn')).tap() 19 - await expect(element(by.id('list-Muted Users'))).toBeVisible() 20 - await element(by.id('list-Muted Users')).tap() 21 - await expect( 22 - element(by.id('user-muted-by-list-account.test')), 23 - ).toBeVisible() 24 - }) 25 - 26 - it('Toggle mute subscription', async () => { 27 - await element(by.id('unmuteBtn')).tap() 28 - await element(by.id('subscribeBtn')).tap() 29 - await element(by.text('Mute accounts')).tap() 30 - await element(by.id('confirmBtn')).tap() 31 - }) 32 - 33 - it('Edit display name and description via the edit modlist modal', async () => { 34 - await element(by.id('headerDropdownBtn')).tap() 35 - await element(by.text('Edit list details')).tap() 36 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 37 - await element(by.id('editNameInput')).clearText() 38 - await element(by.id('editNameInput')).typeText('Bad Ppl') 39 - await element(by.id('editDescriptionInput')).clearText() 40 - await element(by.id('editDescriptionInput')).typeText('They bad') 41 - await element(by.id('saveBtn')).tap() 42 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 43 - await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') 44 - await expect(element(by.id('listDescription'))).toHaveText('They bad') 45 - // have to wait for the toast to clear 46 - await waitFor(element(by.id('headerDropdownBtn'))) 47 - .toBeVisible() 48 - .withTimeout(5000) 49 - }) 50 - 51 - it('Remove description via the edit modlist modal', async () => { 52 - await element(by.id('headerDropdownBtn')).tap() 53 - await element(by.text('Edit list details')).tap() 54 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 55 - await element(by.id('editDescriptionInput')).clearText() 56 - await element(by.id('saveBtn')).tap() 57 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 58 - await expect(element(by.id('listDescription'))).not.toBeVisible() 59 - // have to wait for the toast to clear 60 - await waitFor(element(by.id('headerDropdownBtn'))) 61 - .toBeVisible() 62 - .withTimeout(5000) 63 - }) 64 - 65 - // DISABLED e2e environment is real finicky about avatar uploads -prf 66 - // it('Set avi via the edit modlist modal', async () => { 67 - // await expect(element(by.id('userAvatarFallback'))).toExist() 68 - // await element(by.id('headerDropdownBtn')).tap() 69 - // await element(by.text('Edit list details')).tap() 70 - // await expect(element(by.id('createOrEditListModal'))).toBeVisible() 71 - // await element(by.id('changeAvatarBtn')).tap() 72 - // await element(by.text('Library')).tap() 73 - // await sleep(3e3) 74 - // await element(by.id('saveBtn')).tap() 75 - // await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 76 - // await expect(element(by.id('userAvatarImage'))).toExist() 77 - // // have to wait for the toast to clear 78 - // await waitFor(element(by.id('headerDropdownBtn'))) 79 - // .toBeVisible() 80 - // .withTimeout(5000) 81 - // }) 82 - 83 - // it('Remove avi via the edit modlist modal', async () => { 84 - // await expect(element(by.id('userAvatarImage'))).toExist() 85 - // await element(by.id('headerDropdownBtn')).tap() 86 - // await element(by.text('Edit list details')).tap() 87 - // await expect(element(by.id('createOrEditListModal'))).toBeVisible() 88 - // await element(by.id('changeAvatarBtn')).tap() 89 - // await element(by.text('Remove')).tap() 90 - // await element(by.id('saveBtn')).tap() 91 - // await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 92 - // await expect(element(by.id('userAvatarFallback'))).toExist() 93 - // // have to wait for the toast to clear 94 - // await waitFor(element(by.id('headerDropdownBtn'))) 95 - // .toBeVisible() 96 - // .withTimeout(5000) 97 - // }) 98 - 99 - it('Delete the modlist', async () => { 100 - await element(by.id('headerDropdownBtn')).tap() 101 - await element(by.text('Delete List')).tap() 102 - await element(by.id('confirmBtn')).tap() 103 - await expect(element(by.id('listsEmpty'))).toBeVisible() 104 - }) 105 - 106 - it('Create a new modlist', async () => { 107 - await element(by.id('newModListBtn')).tap() 108 - await expect(element(by.id('createOrEditListModal'))).toBeVisible() 109 - await element(by.id('editNameInput')).typeText('Bad Ppl') 110 - await element(by.id('editDescriptionInput')).typeText('They bad') 111 - await element(by.id('saveBtn')).tap() 112 - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() 113 - await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') 114 - await expect(element(by.id('listDescription'))).toHaveText('They bad') 115 - }) 116 - 117 - it('Adds and removes users on modlists from the list', async () => { 118 - await element(by.id('addUserBtn')).tap() 119 - await expect(element(by.id('listAddUserModal'))).toBeVisible() 120 - await waitFor(element(by.id('user-warn-posts.test-addBtn'))) 121 - .toBeVisible() 122 - .withTimeout(5000) 123 - await element(by.id('user-warn-posts.test-addBtn')).tap() 124 - await element(by.id('doneBtn')).tap() 125 - await expect(element(by.id('listAddUserModal'))).not.toBeVisible() 126 - await element(by.id('listItems-flatlist')).swipe( 127 - 'down', 128 - 'slow', 129 - 1, 130 - 0.5, 131 - 0.5, 132 - ) 133 - await expect(element(by.id('user-warn-posts.test'))).toBeVisible() 134 - await element(by.id('user-warn-posts.test-editBtn')).tap() 135 - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() 136 - await element(by.id('user-warn-posts.test-addBtn')).tap() 137 - await element(by.id('doneBtn')).tap() 138 - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() 139 - }) 140 - 141 - it('Shows the modlist on my profile', async () => { 142 - await element(by.id('bottomBarProfileBtn')).tap() 143 - await element(by.id('profilePager-selector')).swipe('left') 144 - await element(by.id('profilePager-selector-5')).tap() 145 - await element(by.id('list-Bad Ppl')).tap() 146 - }) 147 - 148 - it('Adds and removes users on modlists from the profile', async () => { 149 - await element(by.id('bottomBarSearchBtn')).tap() 150 - await element(by.id('searchTextInput')).typeText('bob') 151 - await element(by.id('searchAutoCompleteResult-bob.test')).tap() 152 - await expect(element(by.id('profileView'))).toBeVisible() 153 - 154 - await element(by.id('profileHeaderDropdownBtn')).tap() 155 - await element(by.text('Add to Lists')).tap() 156 - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() 157 - await element(by.id('user-bob.test-addBtn')).tap() 158 - await element(by.id('doneBtn')).tap() 159 - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() 160 - 161 - await element(by.id('profileHeaderDropdownBtn')).tap() 162 - await element(by.text('Add to Lists')).tap() 163 - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() 164 - await element(by.id('user-bob.test-addBtn')).tap() 165 - await element(by.id('doneBtn')).tap() 166 - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() 167 - }) 168 - 169 - it('Can report a mute list', async () => { 170 - await element(by.id('e2eGotoSettings')).tap() 171 - await element(by.id('signOutBtn')).tap() 172 - await loginAsBob() 173 - await element(by.id('bottomBarSearchBtn')).tap() 174 - await element(by.id('searchTextInput')).typeText('alice') 175 - await element(by.id('searchAutoCompleteResult-alice.test')).tap() 176 - await element(by.id('profilePager-selector')).swipe('left') 177 - await element(by.id('profilePager-selector-3')).tap() 178 - await element(by.id('list-Bad Ppl')).tap() 179 - await element(by.id('headerDropdownBtn')).tap() 180 - await element(by.text('Report List')).tap() 181 - await expect(element(by.id('reportModal'))).toBeVisible() 182 - await expect(element(by.text('Report List'))).toBeVisible() 183 - await element( 184 - by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'), 185 - ).tap() 186 - await element(by.id('sendReportBtn')).tap() 187 - await expect(element(by.id('reportModal'))).not.toBeVisible() 188 - }) 189 - })
-196
__e2e__/tests/profile-screen.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {beforeAll, describe, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - 6 - import {createServer, loginAsAlice, openApp, sleep} from '../util' 7 - 8 - describe('Profile screen', () => { 9 - beforeAll(async () => { 10 - await createServer('?users&posts&feeds') 11 - await openApp({ 12 - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, 13 - }) 14 - }) 15 - 16 - it('Login and navigate to my profile', async () => { 17 - await loginAsAlice() 18 - await element(by.id('bottomBarProfileBtn')).tap() 19 - }) 20 - 21 - it('Can see feeds', async () => { 22 - await element(by.id('profilePager-selector')).swipe('left') 23 - await element(by.id('profilePager-selector-4')).tap() 24 - await expect(element(by.id('feed-alice-favs'))).toBeVisible() 25 - await element(by.id('profilePager-selector')).swipe('right') 26 - await element(by.id('profilePager-selector-0')).tap() 27 - }) 28 - 29 - it('Open and close edit profile modal', async () => { 30 - await element(by.id('profileHeaderEditProfileButton')).tap() 31 - await expect(element(by.id('editProfileModal'))).toBeVisible() 32 - await element(by.id('editProfileCancelBtn')).tap() 33 - await expect(element(by.id('editProfileModal'))).not.toBeVisible() 34 - }) 35 - 36 - it('Edit display name and description via the edit profile modal', async () => { 37 - await element(by.id('profileHeaderEditProfileButton')).tap() 38 - await expect(element(by.id('editProfileModal'))).toBeVisible() 39 - await element(by.id('editProfileDisplayNameInput')).clearText() 40 - await element(by.id('editProfileDisplayNameInput')).typeText('Alicia') 41 - await element(by.id('editProfileDescriptionInput')).clearText() 42 - await element(by.id('editProfileDescriptionInput')).typeText( 43 - 'One cool hacker', 44 - ) 45 - await element(by.id('editProfileSaveBtn')).tap() 46 - await expect(element(by.id('editProfileModal'))).not.toBeVisible() 47 - await expect(element(by.id('profileHeaderDisplayName'))).toHaveText( 48 - 'Alicia', 49 - ) 50 - await expect(element(by.id('profileHeaderDescription'))).toHaveText( 51 - 'One cool hacker', 52 - ) 53 - }) 54 - 55 - it('Remove display name and description via the edit profile modal', async () => { 56 - await element(by.id('profileHeaderEditProfileButton')).tap() 57 - await expect(element(by.id('editProfileModal'))).toBeVisible() 58 - await element(by.id('editProfileDisplayNameInput')).clearText() 59 - await element(by.id('editProfileDescriptionInput')).clearText() 60 - await element(by.id('editProfileSaveBtn')).tap() 61 - await expect(element(by.id('editProfileModal'))).not.toBeVisible() 62 - await expect(element(by.id('profileHeaderDisplayName'))).toHaveText( 63 - 'alice.test', 64 - ) 65 - await expect(element(by.id('profileHeaderDescription'))).not.toExist() 66 - }) 67 - 68 - it('Set avi and banner via the edit profile modal', async () => { 69 - await expect(element(by.id('userBannerFallback'))).toExist() 70 - await expect(element(by.id('userAvatarFallback'))).toExist() 71 - await element(by.id('profileHeaderEditProfileButton')).tap() 72 - await expect(element(by.id('editProfileModal'))).toBeVisible() 73 - await element(by.id('changeBannerBtn')).tap() 74 - await element(by.text('Upload from Library')).tap() 75 - await sleep(3e3) 76 - await element(by.id('changeAvatarBtn')).tap() 77 - await element(by.text('Upload from Library')).tap() 78 - await sleep(3e3) 79 - await element(by.id('editProfileSaveBtn')).tap() 80 - await expect(element(by.id('editProfileModal'))).not.toBeVisible() 81 - await expect(element(by.id('userBannerImage'))).toExist() 82 - await expect(element(by.id('userAvatarImage'))).toExist() 83 - }) 84 - 85 - it('Remove avi and banner via the edit profile modal', async () => { 86 - await expect(element(by.id('userBannerImage'))).toExist() 87 - await expect(element(by.id('userAvatarImage'))).toExist() 88 - await element(by.id('profileHeaderEditProfileButton')).tap() 89 - await expect(element(by.id('editProfileModal'))).toBeVisible() 90 - await element(by.id('changeBannerBtn')).tap() 91 - await element(by.text('Remove Banner')).tap() 92 - await element(by.id('changeAvatarBtn')).tap() 93 - await element(by.text('Remove Avatar')).tap() 94 - await element(by.id('editProfileSaveBtn')).tap() 95 - await expect(element(by.id('editProfileModal'))).not.toBeVisible() 96 - await expect(element(by.id('userBannerFallback'))).toExist() 97 - await expect(element(by.id('userAvatarFallback'))).toExist() 98 - }) 99 - 100 - it('Navigate to another user profile', async () => { 101 - await element(by.id('bottomBarSearchBtn')).tap() 102 - // have to wait for the toast to clear 103 - await waitFor(element(by.id('searchTextInput'))) 104 - .toBeVisible() 105 - .withTimeout(5000) 106 - await element(by.id('searchTextInput')).typeText('bob') 107 - await element(by.id('searchAutoCompleteResult-bob.test')).tap() 108 - await expect(element(by.id('profileView'))).toBeVisible() 109 - }) 110 - 111 - it('Can follow/unfollow another user', async () => { 112 - await element(by.id('followBtn')).tap() 113 - await expect(element(by.id('unfollowBtn'))).toBeVisible() 114 - await element(by.id('unfollowBtn')).tap() 115 - await expect(element(by.id('followBtn'))).toBeVisible() 116 - }) 117 - 118 - it('Can mute/unmute another user', async () => { 119 - await expect(element(by.id('profileHeaderAlert'))).not.toExist() 120 - await element(by.id('profileHeaderDropdownBtn')).tap() 121 - await element(by.text('Mute Account')).tap() 122 - await expect(element(by.id('profileHeaderAlert'))).toBeVisible() 123 - await element(by.id('profileHeaderDropdownBtn')).tap() 124 - await element(by.text('Unmute Account')).tap() 125 - await expect(element(by.id('profileHeaderAlert'))).not.toExist() 126 - }) 127 - 128 - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf 129 - // it('Can report another user', async () => { 130 - // await element(by.id('profileHeaderDropdownBtn')).tap() 131 - // await element(by.text('Report Account')).tap() 132 - // await expect(element(by.id('reportModal'))).toBeVisible() 133 - // await element( 134 - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), 135 - // ).tap() 136 - // await element(by.id('sendReportBtn')).tap() 137 - // await expect(element(by.id('reportModal'))).not.toBeVisible() 138 - // }) 139 - 140 - it('Can like posts', async () => { 141 - await element(by.id('postsFeed-flatlist')).swipe( 142 - 'down', 143 - 'slow', 144 - 1, 145 - 0.5, 146 - 0.5, 147 - ) 148 - 149 - const posts = by.id('feedItem-by-bob.test') 150 - await expect( 151 - element(by.id('likeCount').withAncestor(posts)).atIndex(0), 152 - ).not.toExist() 153 - await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() 154 - await expect( 155 - element(by.id('likeCount').withAncestor(posts)).atIndex(0), 156 - ).toHaveText('1') 157 - await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() 158 - await expect( 159 - element(by.id('likeCount').withAncestor(posts)).atIndex(0), 160 - ).not.toExist() 161 - }) 162 - 163 - it('Can repost posts', async () => { 164 - const posts = by.id('feedItem-by-bob.test') 165 - await expect( 166 - element(by.id('repostCount').withAncestor(posts)).atIndex(0), 167 - ).not.toExist() 168 - await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap() 169 - await expect(element(by.id('repostModal'))).toBeVisible() 170 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 171 - await expect(element(by.id('repostModal'))).not.toBeVisible() 172 - await expect( 173 - element(by.id('repostCount').withAncestor(posts)).atIndex(0), 174 - ).toHaveText('1') 175 - await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap() 176 - await expect(element(by.id('repostModal'))).toBeVisible() 177 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 178 - await expect(element(by.id('repostModal'))).not.toBeVisible() 179 - await expect( 180 - element(by.id('repostCount').withAncestor(posts)).atIndex(0), 181 - ).not.toExist() 182 - }) 183 - 184 - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf 185 - // it('Can report posts', async () => { 186 - // const posts = by.id('feedItem-by-bob.test') 187 - // await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() 188 - // await element(by.text('Report post')).tap() 189 - // await expect(element(by.id('reportModal'))).toBeVisible() 190 - // await element( 191 - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), 192 - // ).tap() 193 - // await element(by.id('sendReportBtn')).tap() 194 - // await expect(element(by.id('reportModal'))).not.toBeVisible() 195 - // }) 196 - })
-25
__e2e__/tests/search-screen.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {describe, beforeAll, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - import {openApp, loginAsAlice, createServer} from '../util' 6 - 7 - describe('Search screen', () => { 8 - beforeAll(async () => { 9 - await createServer('?users') 10 - await openApp({ 11 - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, 12 - }) 13 - }) 14 - 15 - it('Login', async () => { 16 - await loginAsAlice() 17 - }) 18 - 19 - it('Navigate to another user profile via autocomplete', async () => { 20 - await element(by.id('bottomBarSearchBtn')).tap() 21 - await element(by.id('searchTextInput')).typeText('bob') 22 - await element(by.id('searchAutoCompleteResult-bob.test')).tap() 23 - await expect(element(by.id('profileView'))).toBeVisible() 24 - }) 25 - })
-36
__e2e__/tests/self-labeling.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {describe, beforeAll, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - import {openApp, loginAsAlice, createServer, sleep} from '../util' 6 - 7 - describe('Self-labeling', () => { 8 - beforeAll(async () => { 9 - await createServer('?users') 10 - await openApp({ 11 - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, 12 - }) 13 - }) 14 - 15 - it('Login', async () => { 16 - await loginAsAlice() 17 - await element(by.id('homeScreenFeedTabs-Following')).tap() 18 - }) 19 - 20 - it('Post an image with the porn label', async () => { 21 - await element(by.id('composeFAB')).tap() 22 - await element(by.id('composerTextInput')).typeText('Post with an image') 23 - await element(by.id('openGalleryBtn')).tap() 24 - await sleep(3e3) 25 - await element(by.id('labelsBtn')).tap() 26 - await element(by.id('pornLabelBtn')).tap() 27 - await element(by.id('confirmBtn')).tap() 28 - await element(by.id('composerPublishBtn')).tap() 29 - await expect(element(by.id('composeFAB'))).toBeVisible() 30 - const posts = by.id('feedItem-by-alice.test') 31 - await element(by.id('e2eRefreshHome')).tap() 32 - await expect( 33 - element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0), 34 - ).toExist() 35 - }) 36 - })
-33
__e2e__/tests/shell.test.skip.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {openApp, loginAsAlice, createServer} from '../util' 4 - 5 - describe('Shell', () => { 6 - beforeAll(async () => { 7 - await createServer('?users') 8 - await openApp({permissions: {notifications: 'YES'}}) 9 - }) 10 - 11 - it('Login', async () => { 12 - await loginAsAlice() 13 - await element(by.id('homeScreenFeedTabs-Following')).tap() 14 - }) 15 - 16 - it('Can swipe the shelf open', async () => { 17 - await element(by.id('homeScreen')).swipe('right', 'fast', 0.75) 18 - await expect(element(by.id('drawer'))).toBeVisible() 19 - await element(by.id('drawer')).swipe('left', 'fast', 0.75) 20 - await expect(element(by.id('drawer'))).not.toBeVisible() 21 - }) 22 - 23 - it('Can open the shelf by pressing the header avi', async () => { 24 - await element(by.id('viewHeaderDrawerBtn')).tap() 25 - await expect(element(by.id('drawer'))).toBeVisible() 26 - }) 27 - 28 - it('Can navigate using the shelf', async () => { 29 - await element(by.id('menuItemButton-Notifications')).tap() 30 - await expect(element(by.id('drawer'))).not.toBeVisible() 31 - await expect(element(by.id('notificationsScreen'))).toBeVisible() 32 - }) 33 - })
-103
__e2e__/tests/thread-muting.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {describe, beforeAll, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - import {openApp, loginAsAlice, loginAsBob, createServer} from '../util' 6 - 7 - describe('Thread muting', () => { 8 - beforeAll(async () => { 9 - await createServer('?users&follows') 10 - await openApp({permissions: {notifications: 'YES'}}) 11 - }) 12 - 13 - it('Login, create a thread, and log out', async () => { 14 - await loginAsAlice() 15 - await element(by.id('homeScreenFeedTabs-Following')).tap() 16 - await element(by.id('composeFAB')).tap() 17 - await element(by.id('composerTextInput')).typeText('Test thread') 18 - await element(by.id('composerPublishBtn')).tap() 19 - await expect(element(by.id('composeFAB'))).toBeVisible() 20 - }) 21 - 22 - it('Login, reply to the thread, and log out', async () => { 23 - await loginAsBob() 24 - await element(by.id('homeScreenFeedTabs-Following')).tap() 25 - const alicePosts = by.id('feedItem-by-alice.test') 26 - await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() 27 - await element(by.id('composerTextInput')).typeText('Reply 1') 28 - await element(by.id('composerPublishBtn')).tap() 29 - await expect(element(by.id('composeFAB'))).toBeVisible() 30 - }) 31 - 32 - it('Login, confirm notification exists, mute thread, and log out', async () => { 33 - await loginAsAlice() 34 - await element(by.id('bottomBarNotificationsBtn')).tap() 35 - const bobNotifs = by.id('feedItem-by-bob.test') 36 - await expect( 37 - element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), 38 - ).toHaveText('Reply 1') 39 - await element(by.id('postDropdownBtn').withAncestor(bobNotifs)) 40 - .atIndex(0) 41 - .tap() 42 - await element(by.text('Mute thread')).tap() 43 - // have to wait for the toast to clear 44 - await waitFor(element(by.id('viewHeaderDrawerBtn'))) 45 - .toBeVisible() 46 - .withTimeout(5000) 47 - }) 48 - 49 - it('Login, reply to the thread twice, and log out', async () => { 50 - await loginAsBob() 51 - 52 - await element(by.id('bottomBarProfileBtn')).tap() 53 - await element(by.id('profilePager-selector-1')).tap() 54 - const bobPosts = by.id('feedItem-by-bob.test') 55 - await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap() 56 - await element(by.id('composerTextInput')).typeText('Reply 2') 57 - await element(by.id('composerPublishBtn')).tap() 58 - await expect(element(by.id('composeFAB'))).toBeVisible() 59 - 60 - const alicePosts = by.id('feedItem-by-alice.test') 61 - await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() 62 - await element(by.id('composerTextInput')).typeText('Reply 3') 63 - await element(by.id('composerPublishBtn')).tap() 64 - await expect(element(by.id('composeFAB'))).toBeVisible() 65 - 66 - await element(by.id('bottomBarHomeBtn')).tap() 67 - }) 68 - 69 - it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => { 70 - await loginAsAlice() 71 - 72 - await element(by.id('bottomBarNotificationsBtn')).tap() 73 - const bobNotifs = by.id('feedItem-by-bob.test') 74 - await expect( 75 - element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), 76 - ).not.toExist() 77 - 78 - await element(by.id('bottomBarHomeBtn')).tap() 79 - const alicePosts = by.id('feedItem-by-alice.test') 80 - await element(by.id('postDropdownBtn').withAncestor(alicePosts)) 81 - .atIndex(0) 82 - .tap() 83 - await element(by.text('Unmute thread')).tap() 84 - 85 - // TODO 86 - // the swipe down to trigger PTR isnt working and I dont want to block on this 87 - // -prf 88 - // await element(by.id('bottomBarNotificationsBtn')).tap() 89 - // await element(by.id('notifsFeed')).swipe('down', 'fast') 90 - // await waitFor(element(by.id('postText').withAncestor(bobNotifs))) 91 - // .toBeVisible() 92 - // .withTimeout(5000) 93 - // await expect( 94 - // element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), 95 - // ).toHaveText('Reply 2') 96 - // await expect( 97 - // element(by.id('postText').withAncestor(bobNotifs)).atIndex(1), 98 - // ).toHaveText('Reply 3') 99 - // await expect( 100 - // element(by.id('postText').withAncestor(bobNotifs)).atIndex(2), 101 - // ).toHaveText('Reply 1') 102 - }) 103 - })
-131
__e2e__/tests/thread-screen.test.ts
··· 1 - /* eslint-env detox/detox */ 2 - 3 - import {beforeAll, describe, it} from '@jest/globals' 4 - import {expect} from 'detox' 5 - 6 - import {createServer, loginAsAlice, openApp} from '../util' 7 - 8 - describe('Thread screen', () => { 9 - beforeAll(async () => { 10 - await createServer('?users&follows&thread') 11 - await openApp({permissions: {notifications: 'YES'}}) 12 - }) 13 - 14 - it('Login & navigate to thread', async () => { 15 - await loginAsAlice() 16 - await element(by.id('homeScreenFeedTabs-Following')).tap() 17 - await element(by.id('feedItem-by-bob.test')).atIndex(0).tap() 18 - await expect( 19 - element( 20 - by 21 - .id('postThreadItem-by-bob.test') 22 - .withDescendant(by.text('Thread root')), 23 - ), 24 - ).toBeVisible() 25 - await expect( 26 - element( 27 - by 28 - .id('postThreadItem-by-carla.test') 29 - .withDescendant(by.text('Thread reply')), 30 - ), 31 - ).toBeVisible() 32 - }) 33 - 34 - it('Can like the root post', async () => { 35 - const post = by.id('postThreadItem-by-bob.test') 36 - await expect( 37 - element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), 38 - ).not.toExist() 39 - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() 40 - await expect( 41 - element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), 42 - ).toHaveText('1 like') 43 - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() 44 - await expect( 45 - element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), 46 - ).not.toExist() 47 - }) 48 - 49 - it('Can like a reply post', async () => { 50 - const post = by.id('postThreadItem-by-carla.test') 51 - await expect( 52 - element(by.id('likeCount').withAncestor(post)).atIndex(0), 53 - ).not.toExist() 54 - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() 55 - await expect( 56 - element(by.id('likeCount').withAncestor(post)).atIndex(0), 57 - ).toHaveText('1') 58 - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() 59 - await expect( 60 - element(by.id('likeCount').withAncestor(post)).atIndex(0), 61 - ).not.toExist() 62 - }) 63 - 64 - it('Can repost the root post', async () => { 65 - const post = by.id('postThreadItem-by-bob.test') 66 - await expect( 67 - element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), 68 - ).not.toExist() 69 - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() 70 - await expect(element(by.id('repostModal'))).toBeVisible() 71 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 72 - await expect(element(by.id('repostModal'))).not.toBeVisible() 73 - await expect( 74 - element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), 75 - ).toHaveText('1 repost') 76 - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() 77 - await expect(element(by.id('repostModal'))).toBeVisible() 78 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 79 - await expect(element(by.id('repostModal'))).not.toBeVisible() 80 - await expect( 81 - element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), 82 - ).not.toExist() 83 - }) 84 - 85 - it('Can repost a reply post', async () => { 86 - const post = by.id('postThreadItem-by-carla.test') 87 - await expect( 88 - element(by.id('repostCount').withAncestor(post)).atIndex(0), 89 - ).not.toExist() 90 - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() 91 - await expect(element(by.id('repostModal'))).toBeVisible() 92 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 93 - await expect(element(by.id('repostModal'))).not.toBeVisible() 94 - await expect( 95 - element(by.id('repostCount').withAncestor(post)).atIndex(0), 96 - ).toHaveText('1') 97 - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() 98 - await expect(element(by.id('repostModal'))).toBeVisible() 99 - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() 100 - await expect(element(by.id('repostModal'))).not.toBeVisible() 101 - await expect( 102 - element(by.id('repostCount').withAncestor(post)).atIndex(0), 103 - ).not.toExist() 104 - }) 105 - 106 - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf 107 - // it('Can report the root post', async () => { 108 - // const post = by.id('postThreadItem-by-bob.test') 109 - // await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() 110 - // await element(by.text('Report post')).tap() 111 - // await expect(element(by.id('reportModal'))).toBeVisible() 112 - // await element( 113 - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), 114 - // ).tap() 115 - // await element(by.id('sendReportBtn')).tap() 116 - // await expect(element(by.id('reportModal'))).not.toBeVisible() 117 - // }) 118 - 119 - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf 120 - // it('Can report a reply post', async () => { 121 - // const post = by.id('postThreadItem-by-carla.test') 122 - // await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() 123 - // await element(by.text('Report post')).tap() 124 - // await expect(element(by.id('reportModal'))).toBeVisible() 125 - // await element( 126 - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), 127 - // ).tap() 128 - // await element(by.id('sendReportBtn')).tap() 129 - // await expect(element(by.id('reportModal'))).not.toBeVisible() 130 - // }) 131 - })
-141
__e2e__/util.ts
··· 1 - import {execSync} from 'child_process' 2 - import {resolveConfig} from 'detox/internals' 3 - import http from 'http' 4 - 5 - const platform = device.getPlatform() 6 - 7 - export async function openApp(opts: any) { 8 - opts = opts || {} 9 - const config = await resolveConfig() 10 - 11 - if (device.getPlatform() === 'ios') { 12 - // disable password autofill 13 - execSync( 14 - `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles/Library/ConfigurationProfiles/UserSettings.plist`, 15 - ) 16 - execSync( 17 - `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`, 18 - ) 19 - execSync( 20 - `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`, 21 - ) 22 - } 23 - if (config.configurationName.split('.').includes('debug')) { 24 - return await openAppForDebugBuild(platform, opts) 25 - } else { 26 - return await device.launchApp({ 27 - ...opts, 28 - newInstance: true, 29 - }) 30 - } 31 - } 32 - 33 - export async function isVisible(id: string) { 34 - try { 35 - await expect(element(by.id(id))).toBeVisible() 36 - return true 37 - } catch (e) { 38 - return false 39 - } 40 - } 41 - 42 - export async function login( 43 - service: string, 44 - username: string, 45 - password: string, 46 - {takeScreenshots} = {takeScreenshots: false}, 47 - ) { 48 - await element(by.id('signInButton')).tap() 49 - if (takeScreenshots) { 50 - await device.takeScreenshot('1- opened sign-in screen') 51 - } 52 - if (await isVisible('chooseAccountForm')) { 53 - await element(by.id('chooseNewAccountBtn')).tap() 54 - } 55 - await element(by.id('selectServiceButton')).tap() 56 - if (takeScreenshots) { 57 - await device.takeScreenshot('2- opened service selector') 58 - } 59 - await element(by.id('customSelectBtn')).tap() 60 - await element(by.id('customServerTextInput')).typeText(service) 61 - await element(by.id('customServerTextInput')).tapReturnKey() 62 - await element(by.id('doneBtn')).tap() 63 - if (takeScreenshots) { 64 - await device.takeScreenshot('3- input custom service') 65 - } 66 - await element(by.id('loginUsernameInput')).typeText(username) 67 - await element(by.id('loginPasswordInput')).typeText(password) 68 - if (takeScreenshots) { 69 - await device.takeScreenshot('4- entered username and password') 70 - } 71 - await element(by.id('loginNextButton')).tap() 72 - } 73 - 74 - export async function loginAsAlice() { 75 - await element(by.id('e2eSignInAlice')).tap() 76 - } 77 - 78 - export async function loginAsBob() { 79 - await element(by.id('e2eSignInBob')).tap() 80 - } 81 - 82 - async function openAppForDebugBuild(platform: string, opts: any) { 83 - const deepLinkUrl = // Local testing with packager 84 - /*process.env.EXPO_USE_UPDATES 85 - ? // Testing latest published EAS update for the test_debug channel 86 - getDeepLinkUrl(getLatestUpdateUrl()) 87 - : */ getDeepLinkUrl(getDevLauncherPackagerUrl(platform)) 88 - 89 - if (platform === 'ios') { 90 - await device.launchApp({ 91 - ...opts, 92 - newInstance: true, 93 - }) 94 - sleep(3000) 95 - await device.openURL({ 96 - url: deepLinkUrl, 97 - }) 98 - } else { 99 - await device.launchApp({ 100 - ...opts, 101 - newInstance: true, 102 - url: deepLinkUrl, 103 - }) 104 - } 105 - 106 - await sleep(3000) 107 - } 108 - 109 - export async function createServer(path = ''): Promise<string> { 110 - return new Promise(function (resolve, reject) { 111 - var req = http.request( 112 - { 113 - method: 'POST', 114 - host: 'localhost', 115 - port: 1986, 116 - path: `/${path}`, 117 - }, 118 - function (res) { 119 - const body: Buffer[] = [] 120 - res.on('data', chunk => body.push(chunk)) 121 - res.on('end', function () { 122 - try { 123 - resolve(Buffer.concat(body).toString()) 124 - } catch (e) { 125 - reject(e) 126 - } 127 - }) 128 - }, 129 - ) 130 - req.on('error', reject) 131 - req.end() 132 - }) 133 - } 134 - 135 - const getDeepLinkUrl = (url: string) => 136 - `expo+bluesky://expo-development-client/?url=${encodeURIComponent(url)}` 137 - 138 - const getDevLauncherPackagerUrl = (platform: string) => 139 - `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1` 140 - 141 - export const sleep = (t: number) => new Promise(res => setTimeout(res, t))
+1 -8
docs/build.md
··· 16 16 - Add `eval "$(rbenv init - zsh)"` to your `~/.zshrc` 17 17 - From inside the project directory: 18 18 - `bundler install` (this will install Cocoapods) 19 - - Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started): 20 - - `yarn global add detox-cli` 21 - - `brew tap wix/brew` 22 - - `brew install applesimutils` 23 19 - After initial setup: 24 20 - Copy `google-services.json.example` to `google-services.json` or provide your own `google-services.json`. (A real firebase project is NOT required) 25 21 - `npx expo prebuild` -> you will also need to run this anytime `app.json` or native `package.json` deps change ··· 120 116 121 117 ### Running E2E Tests 122 118 123 - - Make sure you've set your environment following the above 124 - - Make sure Metro and the dev server are running 125 - - Run `yarn e2e` 126 - - Find the artifacts in the `artifact` folder 119 + See [testing.md](./testing.md). 127 120 128 121 ### Polyfills 129 122
+10 -4
docs/testing.md
··· 3 3 Make sure you've copied `.env.example` to `.env.test` and provided any required 4 4 values. 5 5 6 - ### Using Maestro E2E tests 6 + ## Using Maestro 7 + 7 8 1. Install Maestro by following [these instructions](https://maestro.mobile.dev/getting-started/installing-maestro). This will help us run the E2E tests. 8 - 2. You can write Maestro tests in `__e2e__/maestro` directory by creating a new `.yaml` file or by modifying an existing one. 9 - 3. You can also use [Maestro Studio](https://maestro.mobile.dev/getting-started/maestro-studio) which automatically generates commands by recording your actions on the app. Therefore, you can create realistic tests without having to manually write any code. Use the `maestro studio` command to start recording your actions. 9 + 2. You can write Maestro tests in `/.maestro/flows/` directory by creating a new `.yml` file or by modifying an existing one. 10 + 3. You can also use [Maestro Studio](https://maestro.mobile.dev/getting-started/maestro-studio) which automatically generates commands by recording your actions on the app. Therefore, you can create realistic tests without having to manually write any code. Use the `maestro studio` command to start recording your actions. 10 11 12 + ### Running Maestro tests 11 13 12 - ### Using Flashlight for Performance Testing 14 + - In one tab, run `yarn e2e:mock-server` 15 + - In a second tab, run `yarn e2e:metro` 16 + - In a third tab, run `yarn e2e:run` 17 + 18 + ## Using Flashlight for Performance Testing 13 19 1. Make sure Maestro is installed (optional: only for automated testing) by following the instructions above 14 20 2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/) 15 21 3. The simplest way to get started is by running `yarn perf:measure` which will run a live preview of the performance test results. You can [see a demo here](https://github.com/bamlab/flashlight/assets/4534323/4038a342-f145-4c3b-8cde-17949bf52612)
+1 -2
jest/test-pds.ts
··· 114 114 pdsUrl, 115 115 mocker: new Mocker(testNet, pdsUrl, pic), 116 116 async close() { 117 - await testNet.pds.server.destroy() 118 - await testNet.plc.server.destroy() 117 + await testNet.close() 119 118 }, 120 119 } 121 120 }
+3 -6
package.json
··· 31 31 "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src", 32 32 "typecheck": "tsc --project ./tsconfig.check.json", 33 33 "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", 34 - "e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", 35 - "e2e:build": "NODE_ENV=test detox build -c ios.sim.debug", 36 - "e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all", 34 + "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", 35 + "e2e:run": "maestro test __e2e__", 37 36 "perf:test": "NODE_ENV=test maestro test", 38 - "perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml", 37 + "perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml", 39 38 "perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json", 40 39 "perf:test:results": "NODE_ENV=test flashlight report .perf/results.json", 41 40 "perf:measure": "NODE_ENV=test flashlight measure", ··· 239 238 "babel-plugin-module-resolver": "^5.0.0", 240 239 "babel-plugin-react-native-web": "^0.18.12", 241 240 "babel-preset-expo": "^10.0.0", 242 - "detox": "^20.14.8", 243 241 "eslint": "^8.19.0", 244 242 "eslint-plugin-bsky-internal": "link:./eslint", 245 - "eslint-plugin-detox": "^1.0.0", 246 243 "eslint-plugin-ft-flow": "^2.0.3", 247 244 "eslint-plugin-lingui": "^0.2.0", 248 245 "eslint-plugin-react": "^7.33.2",
+7 -4
src/view/com/testing/TestCtrls.e2e.tsx
··· 1 1 import React from 'react' 2 - import {Pressable, View} from 'react-native' 3 - import {navigate} from '../../../Navigation' 2 + import {LogBox, Pressable, View} from 'react-native' 3 + import {useQueryClient} from '@tanstack/react-query' 4 + 4 5 import {useModalControls} from '#/state/modals' 5 - import {useQueryClient} from '@tanstack/react-query' 6 + import {useSetFeedViewPreferencesMutation} from '#/state/queries/preferences' 6 7 import {useSessionApi} from '#/state/session' 7 - import {useSetFeedViewPreferencesMutation} from '#/state/queries/preferences' 8 8 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 9 + import {navigate} from '../../../Navigation' 10 + 11 + LogBox.ignoreAllLogs() 9 12 10 13 /** 11 14 * This utility component is only included in the test simulator
+1
src/view/com/util/Toast.e2e.tsx
··· 1 + export function show() {}
-48
yarn.lock
··· 10719 10719 address "^1.0.1" 10720 10720 debug "^2.6.0" 10721 10721 10722 - detox@^20.14.8: 10723 - version "20.14.8" 10724 - resolved "https://registry.yarnpkg.com/detox/-/detox-20.14.8.tgz#0a550cf677fc98a68d56d162e1c5caad317de9ca" 10725 - integrity sha512-3E/0/7Cb7x+wcBsZpCxD8FykZUsFnfVT00d6PWH940boc0Mo1Kzabh+I151X/On4qZMqVdUzgwmap/z8g/kmaw== 10726 - dependencies: 10727 - ajv "^8.6.3" 10728 - bunyan "^1.8.12" 10729 - bunyan-debug-stream "^3.1.0" 10730 - caf "^15.0.1" 10731 - chalk "^4.0.0" 10732 - child-process-promise "^2.2.0" 10733 - execa "^5.1.1" 10734 - find-up "^5.0.0" 10735 - fs-extra "^11.0.0" 10736 - funpermaproxy "^1.1.0" 10737 - glob "^8.0.3" 10738 - ini "^1.3.4" 10739 - jest-environment-emit "^1.0.5" 10740 - json-cycle "^1.3.0" 10741 - lodash "^4.17.11" 10742 - multi-sort-stream "^1.0.3" 10743 - multipipe "^4.0.0" 10744 - node-ipc "9.2.1" 10745 - proper-lockfile "^3.0.2" 10746 - resolve-from "^5.0.0" 10747 - sanitize-filename "^1.6.1" 10748 - semver "^7.0.0" 10749 - serialize-error "^8.0.1" 10750 - shell-quote "^1.7.2" 10751 - signal-exit "^3.0.3" 10752 - stream-json "^1.7.4" 10753 - strip-ansi "^6.0.1" 10754 - telnet-client "1.2.8" 10755 - tempfile "^2.0.0" 10756 - trace-event-lib "^1.3.1" 10757 - which "^1.3.1" 10758 - ws "^7.0.0" 10759 - yargs "^17.0.0" 10760 - yargs-parser "^21.0.0" 10761 - yargs-unparser "^2.0.0" 10762 - 10763 10722 didyoumean@^1.2.2: 10764 10723 version "1.2.2" 10765 10724 resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" ··· 11392 11351 "eslint-plugin-bsky-internal@link:./eslint": 11393 11352 version "0.0.0" 11394 11353 uid "" 11395 - 11396 - eslint-plugin-detox@^1.0.0: 11397 - version "1.0.0" 11398 - resolved "https://registry.yarnpkg.com/eslint-plugin-detox/-/eslint-plugin-detox-1.0.0.tgz#2d9c0130e8ebc4ced56efb6eeaf0d0f5c163398d" 11399 - integrity sha512-Dd+Cwyap5IO9DBKXOKrQTE1RQk9hvSSi+qsS1cMVPZY37mojz2PvriEOfGhKj5XN1G14lJ8TArf+6Y+Np2ZsoQ== 11400 - dependencies: 11401 - requireindex "~1.1.0" 11402 11354 11403 11355 eslint-plugin-eslint-comments@^3.2.0: 11404 11356 version "3.2.0"