diff --git a/.detoxrc.js b/.detoxrc.js deleted file mode 100644 index 90662043..00000000 --- a/.detoxrc.js +++ /dev/null @@ -1,86 +0,0 @@ -/** @type {Detox.DetoxConfig} */ -module.exports = { - testRunner: { - args: { - $0: 'jest', - config: '__e2e__/jest.config.js', - }, - jest: { - setupTimeout: 120000, - }, - }, - apps: { - 'ios.debug': { - type: 'ios.app', - binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/bluesky.app', - build: - 'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', - }, - 'ios.release': { - type: 'ios.app', - binaryPath: - 'ios/build/Build/Products/Release-iphonesimulator/bluesky.app', - build: - 'xcodebuild -workspace ios/Bluesky.xcworkspace -scheme Bluesky -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', - }, - 'android.debug': { - type: 'android.apk', - binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', - build: - 'cd android ; ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug ; cd -', - reversePorts: [8081], - }, - 'android.release': { - type: 'android.apk', - binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', - build: - 'cd android ; ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release ; cd -', - }, - }, - devices: { - simulator: { - type: 'ios.simulator', - device: { - type: 'iPhone 15 Pro', - }, - }, - attached: { - type: 'android.attached', - device: { - adbName: '.*', - }, - }, - emulator: { - type: 'android.emulator', - device: { - avdName: 'Pixel_3a_API_30_x86', - }, - }, - }, - configurations: { - 'ios.sim.debug': { - device: 'simulator', - app: 'ios.debug', - }, - 'ios.sim.release': { - device: 'simulator', - app: 'ios.release', - }, - 'android.att.debug': { - device: 'attached', - app: 'android.debug', - }, - 'android.att.release': { - device: 'attached', - app: 'android.release', - }, - 'android.emu.debug': { - device: 'emulator', - app: 'android.debug', - }, - 'android.emu.release': { - device: 'emulator', - app: 'android.release', - }, - }, -} diff --git a/.eslintrc.js b/.eslintrc.js index 29136d5d..eb7ad04b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,7 +9,6 @@ module.exports = { parser: '@typescript-eslint/parser', plugins: [ '@typescript-eslint', - 'detox', 'react', 'lingui', 'simple-import-sort', diff --git a/__e2e__/config.yml b/__e2e__/config.yml new file mode 100644 index 00000000..b36b0ef6 --- /dev/null +++ b/__e2e__/config.yml @@ -0,0 +1,2 @@ +flows: + - "flows/*" \ No newline at end of file diff --git a/__e2e__/flows/composer-self-label.yml b/__e2e__/flows/composer-self-label.yml new file mode 100644 index 00000000..cc38b1d9 --- /dev/null +++ b/__e2e__/flows/composer-self-label.yml @@ -0,0 +1,30 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: ?users +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + +# Post an image with the porn label +- tapOn: + id: "composeFAB" +- inputText: "Post with an image" +- tapOn: + id: "openGalleryBtn" +- tapOn: + id: "labelsBtn" +- tapOn: + label: "Tap on porn" + point: 78%,67% +- tapOn: + label: "Tap on confirm" + point: 51%,92% +- tapOn: + id: "composerPublishBtn" +- tapOn: + id: "e2eRefreshHome" +- assertVisible: "Adult Content" \ No newline at end of file diff --git a/__e2e__/flows/composer.yml b/__e2e__/flows/composer.yml new file mode 100644 index 00000000..f6d760ea --- /dev/null +++ b/__e2e__/flows/composer.yml @@ -0,0 +1,87 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: ?users +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" +- tapOn: + id: "composeFAB" +- inputText: "Post text only" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "composeFAB" +- inputText: "Post with an image" +- tapOn: + id: "openGalleryBtn" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "composeFAB" +- inputText: "Post with a https://example.com link card" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "e2eRefreshHome" +- tapOn: + id: "replyBtn" +- inputText: "Reply text only" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "replyBtn" +- inputText: "Reply with an image" +- tapOn: + id: "openGalleryBtn" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "replyBtn" +- inputText: "Reply with a https://example.com link card" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "repostBtn" +- tapOn: + id: "quoteBtn" +- inputText: "QP text only" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "repostBtn" +- tapOn: + id: "quoteBtn" +- inputText: "QP with an image" +- tapOn: + id: "openGalleryBtn" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" +- tapOn: + id: "repostBtn" +- tapOn: + id: "quoteBtn" +- inputText: "QP with a https://example.com link card" +- tapOn: + id: "composerPublishBtn" +- assertVisible: + id: "composeFAB" diff --git a/__e2e__/flows/create-account.yml b/__e2e__/flows/create-account.yml new file mode 100644 index 00000000..99ac1371 --- /dev/null +++ b/__e2e__/flows/create-account.yml @@ -0,0 +1,37 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eOpenLoggedOutView" +- tapOn: + id: "createAccountButton" +- tapOn: + id: "selectServiceButton" +- tapOn: + id: "customSelectBtn" +- tapOn: + id: "customServerTextInput" +- inputText: "http://localhost:3000" +- pressKey: Enter +- tapOn: + id: "doneBtn" +- tapOn: + id: "emailInput" +- inputText: "example@test.com" +- tapOn: + id: "passwordInput" +- inputText: "hunter2" +- pressKey: Enter +- tapOn: + id: "nextBtn" +- tapOn: + id: "handleInput" +- inputText: "e2e-test" +- tapOn: + id: "nextBtn" + diff --git a/__e2e__/flows/curate-lists.yml b/__e2e__/flows/curate-lists.yml new file mode 100644 index 00000000..35f4f800 --- /dev/null +++ b/__e2e__/flows/curate-lists.yml @@ -0,0 +1,208 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&follows&posts" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + +- tapOn: + label: "Create a curate list" + id: "e2eGotoLists" +- tapOn: + id: "newUserListBtn" +- assertVisible: + id: "createOrEditListModal" +- tapOn: + id: "editNameInput" +- inputText: "Good Ppl" +- tapOn: + id: "editDescriptionInput" +- inputText: "They good" +- tapOn: "Save" +- tapOn: "Save" +- assertNotVisible: + id: "createOrEditListModal" +- tapOn: "About" +- assertVisible: "Good Ppl" +- assertVisible: "They good" + +- tapOn: + label: "Edit display name and description via the edit curatelist modal" + point: "90%,9%" +- tapOn: "Edit list details" +- assertVisible: + id: "createOrEditListModal" +- tapOn: + id: "editNameInput" +- eraseText +- inputText: "Bad Ppl" +- hideKeyboard +- tapOn: + id: "editDescriptionInput" +- eraseText +- inputText: "They bad" +- tapOn: "Save" +- tapOn: "Save" +- assertNotVisible: + id: "createOrEditListModal" +- assertVisible: Bad Ppl +- assertVisible: They bad + +- tapOn: + label: "Remove description via the edit curatelist modal" + point: "90%,9%" +- tapOn: "Edit list details" +- assertVisible: + id: "createOrEditListModal" +- tapOn: + id: "editDescriptionInput" +- eraseText +- tapOn: "Save" +- tapOn: "Save" +- assertNotVisible: + id: "createOrEditListModal" +- assertNotVisible: + id: "listDescription" + +- tapOn: + label: "Delete the curatelist" + point: "90%,9%" +- tapOn: "Delete List" +- tapOn: + id: "confirmBtn" +- assertVisible: + id: "listsEmpty" + +- tapOn: + label: "Create a new curatelist" + id: "e2eGotoLists" +- tapOn: + id: "newUserListBtn" +- assertVisible: + id: "createOrEditListModal" +- tapOn: + id: "editNameInput" +- inputText: "Good Ppl" +- tapOn: + id: "editDescriptionInput" +- inputText: "They good" +- tapOn: "Save" +- tapOn: "Save" +- assertNotVisible: + id: "createOrEditListModal" +- tapOn: "About" +- assertVisible: "Good Ppl" +- assertVisible: "They good" +- tapOn: "About" + +- tapOn: + label: "Adds users on curatelists from the list" + id: "addUserBtn" +- assertVisible: + id: "listAddUserModal" +- tapOn: + id: "searchInput" +- inputText: "b" +- pressKey: Enter +- tapOn: + id: "user-bob.test-addBtn" +- tapOn: + id: "doneBtn" +- assertNotVisible: + id: "listAddUserModal" +- assertVisible: + id: "user-bob.test" + +- tapOn: "Posts" +- assertVisible: + label: "Shows posts by the users in the list" + id: "feedItem-by-bob.test" + +- tapOn: + label: "Pins the list" + id: "pinBtn" +- tapOn: + id: "e2eGotoHome" +- tapOn: "Good Ppl" +- assertVisible: + id: "feedItem-by-bob.test" +- tapOn: + id: "bottomBarFeedsBtn" +- tapOn: + id: "saved-feed-Good Ppl" +- assertVisible: + id: "feedItem-by-bob.test" +- tapOn: + id: "unpinBtn" +- tapOn: + id: "bottomBarHomeBtn" +- assertNotVisible: + id: "homeScreenFeedTabs-Good Ppl" +- tapOn: + id: "e2eGotoLists" +- tapOn: + id: "list-Good Ppl" + +- tapOn: "About" +- assertVisible: + label: "Removes users on curatelists from the list" + id: "user-bob.test" +- tapOn: + point: "90%,43%" +- assertVisible: + id: "userAddRemoveListsModal" +- tapOn: + id: "user-bob.test-addBtn" +- tapOn: + id: "doneBtn" +- assertNotVisible: + id: "userAddRemoveListsModal" + +- tapOn: + label: "Shows the curatelist on my profile" + id: "bottomBarProfileBtn" +- swipe: + from: + id: "profilePager-selector" + direction: LEFT +- tapOn: + id: "profilePager-selector-5" +- tapOn: + id: "list-Good Ppl" + +- tapOn: + label: "Adds and removes users on curatelists from the profile" + id: "bottomBarSearchBtn" +- tapOn: + id: "searchTextInput" +- inputText: "bob" +- tapOn: + id: "searchAutoCompleteResult-bob.test" +- assertVisible: + id: "profileView" +- tapOn: + id: "profileHeaderDropdownBtn" +- tapOn: "Add to Lists" +- assertVisible: + id: "userAddRemoveListsModal" +- tapOn: + id: "user-bob.test-addBtn" +- tapOn: + id: "doneBtn" +- assertNotVisible: + id: "userAddRemoveListsModal" +- tapOn: + id: "profileHeaderDropdownBtn" +- tapOn: "Add to Lists" +- assertVisible: + id: "userAddRemoveListsModal" +- tapOn: + id: "user-bob.test-addBtn" +- tapOn: + id: "doneBtn" +- assertNotVisible: + id: "userAddRemoveListsModal" diff --git a/__e2e__/flows/home-screen.yml b/__e2e__/flows/home-screen.yml new file mode 100644 index 00000000..69a1fe37 --- /dev/null +++ b/__e2e__/flows/home-screen.yml @@ -0,0 +1,63 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: ?users&follows&posts&feeds +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + +- tapOn: + label: "Can go to feeds page using feeds button in tab bar" + text: "Feeds ✨" +- assertVisible: "Discover New Feeds" + +- tapOn: + label: "Feeds button disappears after pinning a feed" + id: "bottomBarProfileBtn" +- swipe: + from: + id: "profilePager-selector" + direction: LEFT +- tapOn: + id: "profilePager-selector-4" +- tapOn: + id: "feed-alice-favs" +- tapOn: "Pin to Home" +- tapOn: + id: "bottomBarHomeBtn" +- assertNotVisible: "Feeds ✨" + +- tapOn: + label: "Can like posts" + id: "likeBtn" +- assertVisible: + id: "likeCount" + text: "1" +- tapOn: + id: "likeBtn" +- assertNotVisible: + id: "likeCount" + +- tapOn: + label: "Can repost posts" + id: "repostBtn" +- tapOn: "Repost" +- assertVisible: + id: "repostCount" + text: "1" +- tapOn: + id: "repostBtn" +- tapOn: "Undo repost" +- assertNotVisible: + id: "repostCount" + +- tapOn: + label: "Can delete posts" + id: "postDropdownBtn" + childOf: + id: "feedItem-by-alice.test" +- tapOn: "Delete post" +- tapOn: "Delete" diff --git a/__e2e__/flows/login.yml b/__e2e__/flows/login.yml new file mode 100644 index 00000000..f1001f78 --- /dev/null +++ b/__e2e__/flows/login.yml @@ -0,0 +1,26 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eOpenLoggedOutView" +- tapOn: "Sign in" +- tapOn: + id: "selectServiceButton" +- tapOn: "Custom" +- tapOn: + id: "customServerTextInput" +- inputText: "http://localhost:3000" +- tapOn: "Done" +- tapOn: + id: "loginUsernameInput" +- inputText: "Alice" +- tapOn: + id: "loginPasswordInput" +- inputText: "hunter2" +- pressKey: Enter +- assertVisible: "Following" \ No newline at end of file diff --git a/__e2e__/flows/mod-lists.yml b/__e2e__/flows/mod-lists.yml new file mode 100644 index 00000000..75ee100a --- /dev/null +++ b/__e2e__/flows/mod-lists.yml @@ -0,0 +1,45 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&follows&labels" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + +# create a modlist +- tapOn: + id: "e2eGotoModeration" +- tapOn: + id: "moderationlistsBtn" +- tapOn: "New" +- tapOn: + id: "editNameInput" +- inputText: "Muted Users" +- tapOn: + id: "editDescriptionInput" +- inputText: "Shhh" +- tapOn: "Save" +- tapOn: "Save" + +# view modlist +- assertVisible: "Muted Users" +- assertVisible: "Shhh" + +# toggle mute subscription +- tapOn: + point: "70%,9%" +- tapOn: "Mute accounts" +- tapOn: "Mute list" +- tapOn: "Unmute" + +# toggle block subscription +- tapOn: + point: "70%,9%" +- tapOn: "Block accounts" +- tapOn: "Block list" +- tapOn: "Unblock" + + # the rest of the behaviors are tested in curate-lists.yml \ No newline at end of file diff --git a/__e2e__/flows/profile-screen-edit.yml b/__e2e__/flows/profile-screen-edit.yml new file mode 100644 index 00000000..602cc668 --- /dev/null +++ b/__e2e__/flows/profile-screen-edit.yml @@ -0,0 +1,119 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&posts&feeds" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + + +# Navigate to my profile +- tapOn: + id: "bottomBarProfileBtn" + +# Can see feeds +- swipe: + from: + id: "profilePager-selector" + direction: LEFT +- tapOn: + id: "profilePager-selector-4" +- assertVisible: + id: "feed-alice-favs" +- swipe: + from: + id: "profilePager-selector" + direction: RIGHT +- tapOn: + id: "profilePager-selector-0" + +# Open and close edit profile modal +- tapOn: + id: "profileHeaderEditProfileButton" +- assertVisible: + id: "editProfileModal" +- tapOn: + id: "editProfileCancelBtn" +- assertNotVisible: + id: "editProfileModal" + +# Edit display name and description via the edit profile modal +- tapOn: + id: "profileHeaderEditProfileButton" +- assertVisible: + id: "editProfileModal" +- tapOn: + id: "editProfileDisplayNameInput" +- eraseText +- inputText: "Alicia" +- tapOn: + id: "editProfileDescriptionInput" +- eraseText +- inputText: "One cool hacker" +- tapOn: "Description" +- tapOn: + id: "editProfileSaveBtn" +- assertNotVisible: + id: "editProfileModal" +- assertVisible: "Alicia" +- assertVisible: "One cool hacker" + +# Remove display name and description via the edit profile modal +- tapOn: + id: "profileHeaderEditProfileButton" +- assertVisible: + id: "editProfileModal" +- tapOn: + id: "editProfileDisplayNameInput" +- eraseText +- tapOn: + id: "editProfileDescriptionInput" +- eraseText +- tapOn: "Description" +- tapOn: + id: "editProfileSaveBtn" +- assertNotVisible: + id: "editProfileModal" +- assertVisible: "alice.test" +- assertNotVisible: "One cool hacker" + +# Set avi and banner via the edit profile modal +- assertVisible: + id: "userBannerFallback" +- tapOn: + id: "profileHeaderEditProfileButton" +- assertVisible: + id: "editProfileModal" +- tapOn: + id: "changeBannerBtn" +- tapOn: "Upload from Library" +- tapOn: + id: "changeAvatarBtn" +- tapOn: "Upload from Library" +- tapOn: + id: "editProfileSaveBtn" +- assertNotVisible: + id: "editProfileModal" +- assertVisible: + id: "userBannerImage" + +# # Remove avi and banner via the edit profile modal +- tapOn: + id: "profileHeaderEditProfileButton" +- assertVisible: + id: "editProfileModal" +- tapOn: + id: "changeBannerBtn" +- tapOn: "Remove Banner" +- tapOn: + id: "changeAvatarBtn" +- tapOn: "Remove Avatar" +- tapOn: + id: "editProfileSaveBtn" +- assertNotVisible: + id: "editProfileModal" +- assertVisible: + id: "userBannerFallback" diff --git a/__e2e__/flows/profile-screen.yml b/__e2e__/flows/profile-screen.yml new file mode 100644 index 00000000..7d2d43de --- /dev/null +++ b/__e2e__/flows/profile-screen.yml @@ -0,0 +1,37 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&posts&feeds" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + +# Navigate to another user profile +- tapOn: + id: "bottomBarSearchBtn" +- tapOn: + id: "searchTextInput" +- inputText: "b" +- tapOn: + id: "searchAutoCompleteResult-bob.test" +- assertVisible: + id: "profileView" + +# Can follow/unfollow another user +- tapOn: + id: "followBtn" +- tapOn: + id: "unfollowBtn" + +# Can mute/unmute another user +- tapOn: + id: "profileHeaderDropdownBtn" +- tapOn: "Mute Account" +- assertVisible: "Account Muted" +- tapOn: + id: "profileHeaderDropdownBtn" +- tapOn: "Unmute Account" +- assertNotVisible: "Account Muted" \ No newline at end of file diff --git a/__e2e__/flows/search-screen.yml b/__e2e__/flows/search-screen.yml new file mode 100644 index 00000000..0d31d03f --- /dev/null +++ b/__e2e__/flows/search-screen.yml @@ -0,0 +1,22 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + +# Navigate to another user profile via autocomplete +- tapOn: + id: "bottomBarSearchBtn" +- tapOn: + id: "searchTextInput" +- inputText: "b" +- tapOn: + id: "searchAutoCompleteResult-bob.test" +- assertVisible: + id: "profileView" + diff --git a/__e2e__/flows/thread-muting.yml b/__e2e__/flows/thread-muting.yml new file mode 100644 index 00000000..316389a7 --- /dev/null +++ b/__e2e__/flows/thread-muting.yml @@ -0,0 +1,82 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&follows" +- runFlow: + file: ../setupApp.yml + + +# Login, create a thread, and log out +- tapOn: + id: "e2eSignInAlice" +- tapOn: + id: "composeFAB" +- inputText: "Test thread" +- tapOn: + id: "composerPublishBtn" + +# Login, reply to the thread, and log out +- tapOn: + id: "e2eSignInBob" +- tapOn: + id: "replyBtn" +- inputText: "Reply 1" +- tapOn: + id: "composerPublishBtn" + +# Login, confirm notification exists, mute thread, and log out +- tapOn: + id: "e2eSignInAlice" +- tapOn: + id: "bottomBarNotificationsBtn" +- assertVisible: + id: "feedItem-by-bob.test" +- tapOn: + id: "feedItem-by-bob.test" +- tapOn: + id: "postDropdownBtn" + childOf: + id: "postThreadItem-by-bob.test" +- tapOn: "Mute thread" + +# Login, reply to the thread twice, and log out +- tapOn: + id: "e2eSignInBob" +- tapOn: + id: "bottomBarProfileBtn" +- tapOn: + id: "profilePager-selector-1" +- tapOn: + id: "replyBtn" +- inputText: "Reply 2" +- tapOn: + id: "composerPublishBtn" +- tapOn: + id: "replyBtn" +- inputText: "Reply 3" +- tapOn: + id: "composerPublishBtn" + + +# Login, confirm notifications dont exist, unmute the thread, confirm notifications exist +- tapOn: + id: "e2eSignInAlice" +- tapOn: + id: "bottomBarNotificationsBtn" +- assertNotVisible: + id: "feedItem-by-bob.test" +- tapOn: + id: "bottomBarHomeBtn" +- tapOn: + id: "postDropdownBtn" +- tapOn: "Unmute thread" +- tapOn: + id: "bottomBarNotificationsBtn" +- swipe: + from: + id: "notifsFeed" + direction: DOWN +- assertVisible: + id: "feedItem-by-bob.test" diff --git a/__e2e__/flows/thread-screen.yml b/__e2e__/flows/thread-screen.yml new file mode 100644 index 00000000..22f71345 --- /dev/null +++ b/__e2e__/flows/thread-screen.yml @@ -0,0 +1,84 @@ +appId: xyz.blueskyweb.app +--- +- runScript: + file: ../setupServer.js + env: + SERVER_PATH: "?users&follows&thread" +- runFlow: + file: ../setupApp.yml +- tapOn: + id: "e2eSignInAlice" + + +# Navigate to thread +- tapOn: "Thread root" +- assertVisible: "Thread reply" + +# Can like the root post +- tapOn: + id: "likeBtn" + childOf: + id: "postThreadItem-by-bob.test" +- assertVisible: + id: "likeCount-expanded" +- tapOn: + id: "likeBtn" + childOf: + id: "postThreadItem-by-bob.test" +- assertNotVisible: + id: "likeCount-expanded" + +# Can like a reply post +- tapOn: + id: "likeBtn" + childOf: + id: "postThreadItem-by-carla.test" +- assertVisible: + id: "likeCount" + childOf: + id: "postThreadItem-by-carla.test" +- tapOn: + id: "likeBtn" + childOf: + id: "postThreadItem-by-carla.test" +- assertNotVisible: + id: "likeCount" + childOf: + id: "postThreadItem-by-carla.test" + +# Can repost the root post +- tapOn: + id: "repostBtn" + childOf: + id: "postThreadItem-by-bob.test" +- tapOn: "Repost" +- assertVisible: + id: "repostCount-expanded" +- tapOn: + id: "repostBtn" + childOf: + id: "postThreadItem-by-bob.test" +- tapOn: "Undo repost" +- assertNotVisible: + id: "repostCount-expanded" + + +# Can repost a reply post +- tapOn: + id: "repostBtn" + childOf: + id: "postThreadItem-by-carla.test" +- tapOn: "Repost" +- assertVisible: + id: "repostCount" + childOf: + id: "postThreadItem-by-carla.test" +- tapOn: + id: "repostBtn" + childOf: + id: "postThreadItem-by-carla.test" +- tapOn: "Undo repost" +- assertNotVisible: + id: "repostCount" + childOf: + id: "postThreadItem-by-carla.test" diff --git a/__e2e__/jest.config.js b/__e2e__/jest.config.js deleted file mode 100644 index 80c2ad5b..00000000 --- a/__e2e__/jest.config.js +++ /dev/null @@ -1,12 +0,0 @@ -/** @type {import('@jest/types').Config.InitialOptions} */ -module.exports = { - rootDir: '..', - testMatch: ['/__e2e__/**/*.test.ts'], - testTimeout: 120000, - maxWorkers: 1, - globalSetup: 'detox/runners/jest/globalSetup', - globalTeardown: 'detox/runners/jest/globalTeardown', - reporters: ['detox/runners/jest/reporter'], - testEnvironment: 'detox/runners/jest/testEnvironment', - verbose: true, -} diff --git a/__e2e__/maestro/scroll.yaml b/__e2e__/perf-test.yml similarity index 100% rename from __e2e__/maestro/scroll.yaml rename to __e2e__/perf-test.yml index 2d32793e..7a7b7a18 100644 --- a/__e2e__/maestro/scroll.yaml +++ b/__e2e__/perf-test.yml @@ -1,3 +1,4 @@ + # flow.yaml appId: xyz.blueskyweb.app @@ -74,4 +75,3 @@ appId: xyz.blueskyweb.app - "scroll" - "scroll" - "scroll" - diff --git a/__e2e__/setupApp.yml b/__e2e__/setupApp.yml new file mode 100644 index 00000000..8c3ffd2d --- /dev/null +++ b/__e2e__/setupApp.yml @@ -0,0 +1,11 @@ +appId: xyz.blueskyweb.app +--- +- launchApp: + appId: "xyz.blueskyweb.app" + clearState: true +- waitForAnimationToEnd +- tapOn: "http://localhost:8081" +- waitForAnimationToEnd +- swipe: + from: "Bluesky" + direction: DOWN diff --git a/__e2e__/setupServer.js b/__e2e__/setupServer.js new file mode 100644 index 00000000..7b1fb957 --- /dev/null +++ b/__e2e__/setupServer.js @@ -0,0 +1,5 @@ +// eslint-disable-next-line +http.post('http://localhost:1986/' + SERVER_PATH, { + headers: {'Content-Type': 'text/plain'}, + body: '', +}) diff --git a/__e2e__/tests/composer.test.ts b/__e2e__/tests/composer.test.ts deleted file mode 100644 index 06781410..00000000 --- a/__e2e__/tests/composer.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* eslint-env detox/detox */ - -import {beforeAll, describe, it} from '@jest/globals' -import {expect} from 'detox' - -import {createServer, loginAsAlice, openApp, sleep} from '../util' - -describe('Composer', () => { - beforeAll(async () => { - await createServer('?users') - await openApp({ - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, - }) - }) - - it('Login', async () => { - await loginAsAlice() - await element(by.id('homeScreenFeedTabs-Following')).tap() - }) - - it('Post text only', async () => { - await element(by.id('composeFAB')).tap() - await device.takeScreenshot('1- opened composer') - await element(by.id('composerTextInput')).typeText('Post text only') - await device.takeScreenshot('2- entered text') - await element(by.id('composerPublishBtn')).tap() - await device.takeScreenshot('3- opened general section') - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('Post with an image', async () => { - await element(by.id('composeFAB')).tap() - await element(by.id('composerTextInput')).typeText('Post with an image') - await element(by.id('openGalleryBtn')).tap() - await sleep(1e3) - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('Post with a link card', async () => { - await element(by.id('composeFAB')).tap() - await element(by.id('composerTextInput')).typeText( - 'Post with a https://example.com link card', - ) - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('Reply text only', async () => { - await element(by.id('e2eRefreshHome')).tap() - - const post = by.id('feedItem-by-alice.test') - await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('composerTextInput')).typeText('Reply text only') - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('Reply with an image', async () => { - const post = by.id('feedItem-by-alice.test') - await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('composerTextInput')).typeText('Reply with an image') - await element(by.id('openGalleryBtn')).tap() - await sleep(1e3) - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('Reply with a link card', async () => { - const post = by.id('feedItem-by-alice.test') - await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('composerTextInput')).typeText( - 'Reply with a https://example.com link card', - ) - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('QP text only', async () => { - const post = by.id('feedItem-by-alice.test') - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() - await element(by.id('composerTextInput')).typeText('QP text only') - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('QP with an image', async () => { - const post = by.id('feedItem-by-alice.test') - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() - await element(by.id('composerTextInput')).typeText('QP with an image') - await element(by.id('openGalleryBtn')).tap() - await sleep(1e3) - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('QP with a link card', async () => { - const post = by.id('feedItem-by-alice.test') - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() - await element(by.id('quoteBtn').withAncestor(by.id('repostModal'))).tap() - await element(by.id('composerTextInput')).typeText( - 'QP with a https://example.com link card', - ) - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) -}) diff --git a/__e2e__/tests/create-account.test.ts b/__e2e__/tests/create-account.test.ts deleted file mode 100644 index 9c56c914..00000000 --- a/__e2e__/tests/create-account.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-env detox/detox */ - -import {describe, beforeAll, it} from '@jest/globals' -import {expect} from 'detox' -import {openApp, createServer} from '../util' - -describe('Create account', () => { - let service: string - beforeAll(async () => { - service = await createServer('') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('I can create a new account', async () => { - await element(by.id('e2eOpenLoggedOutView')).tap() - - await element(by.id('createAccountButton')).tap() - await device.takeScreenshot('1- opened create account screen') - await element(by.id('selectServiceButton')).tap() - await device.takeScreenshot('2- selected other server') - await element(by.id('customSelectBtn')).tap() - await element(by.id('customServerTextInput')).typeText(service) - await element(by.id('customServerTextInput')).tapReturnKey() - await element(by.id('doneBtn')).tap() - await device.takeScreenshot('3- input test server URL') - await element(by.id('emailInput')).typeText('example@test.com') - await element(by.id('passwordInput')).typeText('hunter2') - await device.takeScreenshot('4- entered account details') - - await element(by.id('nextBtn')).tap() - - await element(by.id('handleInput')).typeText('e2e-test') - await device.takeScreenshot('5- entered handle') - - await element(by.id('nextBtn')).tap() - - await expect(element(by.id('onboardingInterests'))).toBeVisible() - }) -}) diff --git a/__e2e__/tests/curate-lists.test.ts b/__e2e__/tests/curate-lists.test.ts deleted file mode 100644 index 635357b8..00000000 --- a/__e2e__/tests/curate-lists.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -/* eslint-env detox/detox */ - -import {beforeAll, describe, it} from '@jest/globals' -import {expect} from 'detox' - -import {createServer, loginAsAlice, loginAsBob, openApp, sleep} from '../util' - -describe('Curate lists', () => { - beforeAll(async () => { - await createServer('?users&follows&posts') - await openApp({ - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, - }) - }) - - it('Login and create a curatelists', async () => { - await loginAsAlice() - await element(by.id('e2eGotoLists')).tap() - await element(by.id('newUserListBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('editNameInput')).typeText('Good Ppl') - await element(by.id('editDescriptionInput')).typeText('They good') - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await element(by.text('About')).tap() - await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl') - await expect(element(by.id('listDescription'))).toHaveText('They good') - }) - - it('Edit display name and description via the edit curatelist modal', async () => { - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Edit list details')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('editNameInput')).clearText() - await element(by.id('editNameInput')).typeText('Bad Ppl') - await element(by.id('editDescriptionInput')).clearText() - await element(by.id('editDescriptionInput')).typeText('They bad') - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') - await expect(element(by.id('listDescription'))).toHaveText('They bad') - // have to wait for the toast to clear - await waitFor(element(by.id('headerDropdownBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Remove description via the edit curatelist modal', async () => { - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Edit list details')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('editDescriptionInput')).clearText() - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await expect(element(by.id('listDescription'))).not.toBeVisible() - // have to wait for the toast to clear - await waitFor(element(by.id('headerDropdownBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Set avi via the edit curatelist modal', async () => { - await expect(element(by.id('userAvatarFallback'))).toExist() - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Edit list details')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Upload from Library')).tap() - await sleep(3e3) - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await expect(element(by.id('userAvatarImage'))).toExist() - // have to wait for the toast to clear - await waitFor(element(by.id('headerDropdownBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Remove avi via the edit curatelist modal', async () => { - await expect(element(by.id('userAvatarImage'))).toExist() - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Edit list details')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Remove Avatar')).tap() - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await expect(element(by.id('userAvatarFallback'))).toExist() - // have to wait for the toast to clear - await waitFor(element(by.id('headerDropdownBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Delete the curatelist', async () => { - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Delete List')).tap() - await element(by.id('confirmBtn')).tap() - await expect(element(by.id('listsEmpty'))).toBeVisible() - }) - - it('Create a new curatelist', async () => { - await element(by.id('e2eGotoLists')).tap() - await element(by.id('newUserListBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('editNameInput')).typeText('Good Ppl') - await element(by.id('editDescriptionInput')).typeText('They good') - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await element(by.text('About')).tap() - await expect(element(by.id('headerTitle'))).toHaveText('Good Ppl') - await expect(element(by.id('listDescription'))).toHaveText('They good') - }) - - it('Adds users on curatelists from the list', async () => { - await element(by.text('About')).tap() - await element(by.id('addUserBtn')).tap() - await expect(element(by.id('listAddUserModal'))).toBeVisible() - await element(by.id('searchInput')).typeText('b') - await waitFor(element(by.id('user-bob.test-addBtn'))) - .toBeVisible() - .withTimeout(5000) - await element(by.id('user-bob.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('listAddUserModal'))).not.toBeVisible() - await expect(element(by.id('user-bob.test'))).toBeVisible() - }) - - it('Shows posts by the users in the list', async () => { - await element(by.text('Posts')).tap() - await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() - }) - - it('Pins the list', async () => { - await expect(element(by.id('pinBtn'))).toBeVisible() - await element(by.id('pinBtn')).tap() - await element(by.id('e2eGotoHome')).tap() - await element(by.id('homeScreenFeedTabs-Good Ppl')).tap() - await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() - - await element(by.id('bottomBarFeedsBtn')).tap() - await element(by.id('saved-feed-Good Ppl')).tap() - await expect(element(by.id('feedItem-by-bob.test'))).toBeVisible() - - await element(by.id('unpinBtn')).tap() - await element(by.id('bottomBarHomeBtn')).tap() - await expect( - element(by.id('homeScreenFeedTabs-Good Ppl')), - ).not.toBeVisible() - - await element(by.id('e2eGotoLists')).tap() - await element(by.id('list-Good Ppl')).tap() - }) - - it('Removes users on curatelists from the list', async () => { - await element(by.text('About')).tap() - await expect(element(by.id('user-bob.test'))).toBeVisible() - await element(by.id('user-bob.test-editBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() - await element(by.id('user-bob.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() - }) - - it('Shows the curatelist on my profile', async () => { - await element(by.id('bottomBarProfileBtn')).tap() - await element(by.id('profilePager-selector')).swipe('left') - await element(by.id('profilePager-selector-5')).tap() - await element(by.id('list-Good Ppl')).tap() - }) - - it('Adds and removes users on curatelists from the profile', async () => { - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('bob') - await element(by.id('searchAutoCompleteResult-bob.test')).tap() - await expect(element(by.id('profileView'))).toBeVisible() - - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Add to Lists')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() - await element(by.id('user-bob.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() - - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Add to Lists')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() - await element(by.id('user-bob.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() - }) - - it('Can report a user list', async () => { - await element(by.id('e2eGotoSettings')).tap() - await element(by.id('signOutBtn')).tap() - await loginAsBob() - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('alice') - await element(by.id('searchAutoCompleteResult-alice.test')).tap() - await element(by.id('profilePager-selector')).swipe('left') - await element(by.id('profilePager-selector-3')).tap() - await element(by.id('list-Good Ppl')).tap() - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Report List')).tap() - await expect(element(by.id('reportModal'))).toBeVisible() - await expect(element(by.text('Report List'))).toBeVisible() - await element( - by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'), - ).tap() - await element(by.id('sendReportBtn')).tap() - await expect(element(by.id('reportModal'))).not.toBeVisible() - }) -}) diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts deleted file mode 100644 index b594c469..00000000 --- a/__e2e__/tests/home-screen.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* eslint-env detox/detox */ - -import {beforeAll, describe, it} from '@jest/globals' -import {expect} from 'detox' - -import {createServer, loginAsAlice, openApp} from '../util' - -describe('Home screen', () => { - beforeAll(async () => { - await createServer('?users&follows&posts&feeds') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('Login', async () => { - await loginAsAlice() - await element(by.id('homeScreenFeedTabs-Following')).tap() - }) - - it('Can go to feeds page using feeds button in tab bar', async () => { - await element(by.id('homeScreenFeedTabs-Feeds ✨')).tap() - await expect(element(by.text('Discover New Feeds'))).toBeVisible() - }) - - it('Feeds button disappears after pinning a feed', async () => { - await element(by.id('bottomBarProfileBtn')).tap() - await element(by.id('profilePager-selector')).swipe('left') - await element(by.id('profilePager-selector-4')).tap() - await element(by.id('feed-alice-favs')).tap() - await element(by.id('pinBtn')).tap() - await element(by.id('bottomBarHomeBtn')).tap() - await expect( - element(by.id('homeScreenFeedTabs-Feeds ✨')), - ).not.toBeVisible() - }) - - it('Can like posts', async () => { - const carlaPosts = by.id('feedItem-by-carla.test') - await expect( - element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), - ).not.toExist() - await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() - await expect( - element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), - ).toHaveText('1') - await element(by.id('likeBtn').withAncestor(carlaPosts)).atIndex(0).tap() - await expect( - element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), - ).not.toExist() - }) - - it('Can repost posts', async () => { - const carlaPosts = by.id('feedItem-by-carla.test') - await expect( - element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), - ).not.toExist() - await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), - ).toHaveText('1') - await element(by.id('repostBtn').withAncestor(carlaPosts)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), - ).not.toExist() - }) - - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf - // it('Can report posts', async () => { - // const carlaPosts = by.id('feedItem-by-carla.test') - // await element(by.id('postDropdownBtn').withAncestor(carlaPosts)) - // .atIndex(0) - // .tap() - // await element(by.text('Report post')).tap() - // await element(by.id('com.atproto.moderation.defs#reasonSpam')).tap() - // await element(by.id('sendReportBtn')).tap() - // }) - - it('Can swipe between feeds', async () => { - await element(by.id('homeScreen')).swipe('left', 'fast', 0.75) - await expect(element(by.id('customFeedPage'))).toBeVisible() - await element(by.id('homeScreen')).swipe('right', 'fast', 0.75) - await expect(element(by.id('followingFeedPage'))).toBeVisible() - }) - - it('Can tap between feeds', async () => { - await element(by.id('homeScreenFeedTabs-alice-favs')).tap() - await expect(element(by.id('customFeedPage'))).toBeVisible() - await element(by.id('homeScreenFeedTabs-Following')).tap() - await expect(element(by.id('followingFeedPage'))).toBeVisible() - }) - - it('Can delete posts', async () => { - const alicePosts = by.id('feedItem-by-alice.test') - await expect(element(alicePosts.withDescendant(by.text('Post')))).toExist() - await element(by.id('postDropdownBtn').withAncestor(alicePosts)) - .atIndex(0) - .tap() - await element(by.text('Delete post')).tap() - await expect(element(by.id('confirmModal'))).toBeVisible() - await element(by.id('confirmBtn')).tap() - await expect( - element(alicePosts.withDescendant(by.text('Post'))), - ).not.toExist() - }) -}) diff --git a/__e2e__/tests/invite-codes.test.skip.ts b/__e2e__/tests/invite-codes.test.skip.ts deleted file mode 100644 index 9f00f052..00000000 --- a/__e2e__/tests/invite-codes.test.skip.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* eslint-env detox/detox */ - -import {beforeAll, describe, it} from '@jest/globals' -import {expect} from 'detox' - -import {createServer, loginAsAlice, openApp} from '../util' - -describe('invite-codes', () => { - let service: string - let inviteCode = '' - beforeAll(async () => { - service = await createServer('?users&invite') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('I can fetch invite codes', async () => { - await loginAsAlice() - await element(by.id('e2eOpenInviteCodesModal')).tap() - await expect(element(by.id('inviteCodesModal'))).toBeVisible() - const attrs = await element(by.id('inviteCode-0-code')).getAttributes() - inviteCode = attrs.text - await element(by.id('closeBtn')).tap() - await element(by.id('e2eSignOut')).tap() - }) - - it('I can create a new account with the invite code', async () => { - await element(by.id('e2eOpenLoggedOutView')).tap() - await element(by.id('createAccountButton')).tap() - await device.takeScreenshot('1- opened create account screen') - await element(by.id('selectServiceButton')).tap() - await device.takeScreenshot('2- selected other server') - await element(by.id('customSelectBtn')).tap() - await element(by.id('customServerTextInput')).typeText(service) - await element(by.id('customServerTextInput')).tapReturnKey() - await element(by.id('doneBtn')).tap() - await device.takeScreenshot('3- input test server URL') - await element(by.id('inviteCodeInput')).typeText(inviteCode) - await element(by.id('emailInput')).typeText('example@test.com') - await element(by.id('passwordInput')).typeText('hunter2') - await device.takeScreenshot('4- entered account details') - await element(by.id('nextBtn')).tap() - await element(by.id('handleInput')).typeText('e2e-test') - await device.takeScreenshot('4- entered handle') - await element(by.id('nextBtn')).tap() - await expect(element(by.id('onboardingInterests'))).toBeVisible() - }) -}) diff --git a/__e2e__/tests/login.test.ts b/__e2e__/tests/login.test.ts deleted file mode 100644 index b4cedef6..00000000 --- a/__e2e__/tests/login.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-env detox/detox */ - -import {describe, beforeAll, it} from '@jest/globals' -import {expect} from 'detox' -import {openApp, login, createServer} from '../util' - -describe('Login', () => { - let service: string - beforeAll(async () => { - service = await createServer('?users') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('As Alice, I can login', async () => { - await element(by.id('e2eOpenLoggedOutView')).tap() - - await expect(element(by.id('signInButton'))).toBeVisible() - await login(service, 'alice', 'hunter2', { - takeScreenshots: true, - }) - await device.takeScreenshot('5- opened home screen') - }) -}) diff --git a/__e2e__/tests/merge-feed.test.skip.ts b/__e2e__/tests/merge-feed.test.skip.ts deleted file mode 100644 index 4a8b3cbc..00000000 --- a/__e2e__/tests/merge-feed.test.skip.ts +++ /dev/null @@ -1,163 +0,0 @@ -/* eslint-env detox/detox */ - -import {describe, beforeAll, it} from '@jest/globals' -import {expect} from 'detox' -import {openApp, loginAsAlice, createServer} from '../util' - -describe('Mergefeed', () => { - beforeAll(async () => { - await createServer('?mergefeed') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('Login', async () => { - await element(by.id('e2eOpenLoggedOutView')).tap() - await loginAsAlice() - await element(by.id('e2eToggleMergefeed')).tap() - await element(by.id('bottomBarFeedsBtn')).tap() - await element(by.id('feed-alice-favs-toggleSave')).tap() - await element(by.id('e2eGotoHome')).tap() - }) - - it('Sees the expected mix of posts with default filters', async () => { - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'down', - 'slow', - 1, - 0.5, - 0.5, - ) - // followed users - await expect( - element( - by.id('postText').withAncestor(by.id('feedItem-by-carla.test')), - ).atIndex(0), - ).toHaveText('Post 9') - await expect( - element( - by.id('postText').withAncestor(by.id('feedItem-by-bob.test')), - ).atIndex(0), - ).toHaveText('Post 9') - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'up', - 'fast', - 1, - 0.5, - 0.5, - ) - // feed users - await expect( - element( - by.id('postText').withAncestor(by.id('feedItem-by-dan.test')), - ).atIndex(0), - ).toHaveText('Post 0') - }) - - it('Sees the expected mix of posts with replies disabled', async () => { - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'down', - 'fast', - 1, - 0.5, - 0.5, - ) - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'down', - 'fast', - 1, - 0.5, - 0.5, - ) - await element(by.id('viewHeaderHomeFeedPrefsBtn')).tap() - await element(by.id('toggleRepliesBtn')).tap() - await element(by.id('confirmBtn')).tap() - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'down', - 'slow', - 1, - 0.5, - 0.5, - ) - - // followed users - await expect( - element( - by.id('postText').withAncestor(by.id('feedItem-by-carla.test')), - ).atIndex(0), - ).toHaveText('Post 9') - await expect( - element( - by.id('postText').withAncestor(by.id('feedItem-by-bob.test')), - ).atIndex(0), - ).toHaveText('Post 9') - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'up', - 'fast', - 1, - 0.5, - 0.5, - ) - - // feed users - await expect( - element( - by.id('postText').withAncestor(by.id('feedItem-by-dan.test')), - ).atIndex(0), - ).toHaveText('Post 0') - }) - - it('Sees the expected mix of posts with no follows', async () => { - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'down', - 'fast', - 1, - 0.5, - 0.5, - ) - - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('bob') - await element(by.id('searchAutoCompleteResult-bob.test')).tap() - await expect(element(by.id('profileView'))).toBeVisible() - await element(by.id('unfollowBtn')).tap() - await element(by.id('profileHeaderBackBtn')).tap() - - // have to wait for the toast to clear - await waitFor(element(by.id('searchTextInputClearBtn'))) - .toBeVisible() - .withTimeout(5000) - await element(by.id('searchTextInputClearBtn')).tap() - await element(by.id('searchTextInput')).typeText('carla') - await element(by.id('searchAutoCompleteResult-carla.test')).tap() - await expect(element(by.id('profileView'))).toBeVisible() - await element(by.id('unfollowBtn')).tap() - await element(by.id('profileHeaderBackBtn')).tap() - - await element(by.id('bottomBarHomeBtn')).tap() - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'down', - 'slow', - 1, - 0.5, - 0.5, - ) - await element(by.id('followingFeedPage-feed-flatlist')).swipe( - 'down', - 'slow', - 1, - 0.5, - 0.5, - ) - - // followed users NOT present - await expect(element(by.id('feedItem-by-carla.test'))).not.toExist() - await expect(element(by.id('feedItem-by-bob.test'))).not.toExist() - - // feed users - await expect( - element( - by.id('postText').withAncestor(by.id('feedItem-by-dan.test')), - ).atIndex(0), - ).toHaveText('Post 0') - }) -}) diff --git a/__e2e__/tests/mod-lists.test.ts b/__e2e__/tests/mod-lists.test.ts deleted file mode 100644 index c3d4149e..00000000 --- a/__e2e__/tests/mod-lists.test.ts +++ /dev/null @@ -1,189 +0,0 @@ -/* eslint-env detox/detox */ - -import {describe, beforeAll, it} from '@jest/globals' -import {expect} from 'detox' -import {openApp, loginAsAlice, loginAsBob, createServer} from '../util' - -describe('Mod lists', () => { - beforeAll(async () => { - await createServer('?users&follows&labels') - await openApp({ - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, - }) - }) - - it('Login and view my modlists', async () => { - await loginAsAlice() - await element(by.id('e2eGotoModeration')).tap() - await element(by.id('moderationlistsBtn')).tap() - await expect(element(by.id('list-Muted Users'))).toBeVisible() - await element(by.id('list-Muted Users')).tap() - await expect( - element(by.id('user-muted-by-list-account.test')), - ).toBeVisible() - }) - - it('Toggle mute subscription', async () => { - await element(by.id('unmuteBtn')).tap() - await element(by.id('subscribeBtn')).tap() - await element(by.text('Mute accounts')).tap() - await element(by.id('confirmBtn')).tap() - }) - - it('Edit display name and description via the edit modlist modal', async () => { - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Edit list details')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('editNameInput')).clearText() - await element(by.id('editNameInput')).typeText('Bad Ppl') - await element(by.id('editDescriptionInput')).clearText() - await element(by.id('editDescriptionInput')).typeText('They bad') - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') - await expect(element(by.id('listDescription'))).toHaveText('They bad') - // have to wait for the toast to clear - await waitFor(element(by.id('headerDropdownBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Remove description via the edit modlist modal', async () => { - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Edit list details')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('editDescriptionInput')).clearText() - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await expect(element(by.id('listDescription'))).not.toBeVisible() - // have to wait for the toast to clear - await waitFor(element(by.id('headerDropdownBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - // DISABLED e2e environment is real finicky about avatar uploads -prf - // it('Set avi via the edit modlist modal', async () => { - // await expect(element(by.id('userAvatarFallback'))).toExist() - // await element(by.id('headerDropdownBtn')).tap() - // await element(by.text('Edit list details')).tap() - // await expect(element(by.id('createOrEditListModal'))).toBeVisible() - // await element(by.id('changeAvatarBtn')).tap() - // await element(by.text('Library')).tap() - // await sleep(3e3) - // await element(by.id('saveBtn')).tap() - // await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - // await expect(element(by.id('userAvatarImage'))).toExist() - // // have to wait for the toast to clear - // await waitFor(element(by.id('headerDropdownBtn'))) - // .toBeVisible() - // .withTimeout(5000) - // }) - - // it('Remove avi via the edit modlist modal', async () => { - // await expect(element(by.id('userAvatarImage'))).toExist() - // await element(by.id('headerDropdownBtn')).tap() - // await element(by.text('Edit list details')).tap() - // await expect(element(by.id('createOrEditListModal'))).toBeVisible() - // await element(by.id('changeAvatarBtn')).tap() - // await element(by.text('Remove')).tap() - // await element(by.id('saveBtn')).tap() - // await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - // await expect(element(by.id('userAvatarFallback'))).toExist() - // // have to wait for the toast to clear - // await waitFor(element(by.id('headerDropdownBtn'))) - // .toBeVisible() - // .withTimeout(5000) - // }) - - it('Delete the modlist', async () => { - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Delete List')).tap() - await element(by.id('confirmBtn')).tap() - await expect(element(by.id('listsEmpty'))).toBeVisible() - }) - - it('Create a new modlist', async () => { - await element(by.id('newModListBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).toBeVisible() - await element(by.id('editNameInput')).typeText('Bad Ppl') - await element(by.id('editDescriptionInput')).typeText('They bad') - await element(by.id('saveBtn')).tap() - await expect(element(by.id('createOrEditListModal'))).not.toBeVisible() - await expect(element(by.id('headerTitle'))).toHaveText('Bad Ppl') - await expect(element(by.id('listDescription'))).toHaveText('They bad') - }) - - it('Adds and removes users on modlists from the list', async () => { - await element(by.id('addUserBtn')).tap() - await expect(element(by.id('listAddUserModal'))).toBeVisible() - await waitFor(element(by.id('user-warn-posts.test-addBtn'))) - .toBeVisible() - .withTimeout(5000) - await element(by.id('user-warn-posts.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('listAddUserModal'))).not.toBeVisible() - await element(by.id('listItems-flatlist')).swipe( - 'down', - 'slow', - 1, - 0.5, - 0.5, - ) - await expect(element(by.id('user-warn-posts.test'))).toBeVisible() - await element(by.id('user-warn-posts.test-editBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() - await element(by.id('user-warn-posts.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() - }) - - it('Shows the modlist on my profile', async () => { - await element(by.id('bottomBarProfileBtn')).tap() - await element(by.id('profilePager-selector')).swipe('left') - await element(by.id('profilePager-selector-5')).tap() - await element(by.id('list-Bad Ppl')).tap() - }) - - it('Adds and removes users on modlists from the profile', async () => { - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('bob') - await element(by.id('searchAutoCompleteResult-bob.test')).tap() - await expect(element(by.id('profileView'))).toBeVisible() - - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Add to Lists')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() - await element(by.id('user-bob.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() - - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Add to Lists')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() - await element(by.id('user-bob.test-addBtn')).tap() - await element(by.id('doneBtn')).tap() - await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() - }) - - it('Can report a mute list', async () => { - await element(by.id('e2eGotoSettings')).tap() - await element(by.id('signOutBtn')).tap() - await loginAsBob() - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('alice') - await element(by.id('searchAutoCompleteResult-alice.test')).tap() - await element(by.id('profilePager-selector')).swipe('left') - await element(by.id('profilePager-selector-3')).tap() - await element(by.id('list-Bad Ppl')).tap() - await element(by.id('headerDropdownBtn')).tap() - await element(by.text('Report List')).tap() - await expect(element(by.id('reportModal'))).toBeVisible() - await expect(element(by.text('Report List'))).toBeVisible() - await element( - by.id('reportReasonRadios-com.atproto.moderation.defs#reasonRude'), - ).tap() - await element(by.id('sendReportBtn')).tap() - await expect(element(by.id('reportModal'))).not.toBeVisible() - }) -}) diff --git a/__e2e__/tests/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts deleted file mode 100644 index 7c3207ec..00000000 --- a/__e2e__/tests/profile-screen.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -/* eslint-env detox/detox */ - -import {beforeAll, describe, it} from '@jest/globals' -import {expect} from 'detox' - -import {createServer, loginAsAlice, openApp, sleep} from '../util' - -describe('Profile screen', () => { - beforeAll(async () => { - await createServer('?users&posts&feeds') - await openApp({ - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, - }) - }) - - it('Login and navigate to my profile', async () => { - await loginAsAlice() - await element(by.id('bottomBarProfileBtn')).tap() - }) - - it('Can see feeds', async () => { - await element(by.id('profilePager-selector')).swipe('left') - await element(by.id('profilePager-selector-4')).tap() - await expect(element(by.id('feed-alice-favs'))).toBeVisible() - await element(by.id('profilePager-selector')).swipe('right') - await element(by.id('profilePager-selector-0')).tap() - }) - - it('Open and close edit profile modal', async () => { - await element(by.id('profileHeaderEditProfileButton')).tap() - await expect(element(by.id('editProfileModal'))).toBeVisible() - await element(by.id('editProfileCancelBtn')).tap() - await expect(element(by.id('editProfileModal'))).not.toBeVisible() - }) - - it('Edit display name and description via the edit profile modal', async () => { - await element(by.id('profileHeaderEditProfileButton')).tap() - await expect(element(by.id('editProfileModal'))).toBeVisible() - await element(by.id('editProfileDisplayNameInput')).clearText() - await element(by.id('editProfileDisplayNameInput')).typeText('Alicia') - await element(by.id('editProfileDescriptionInput')).clearText() - await element(by.id('editProfileDescriptionInput')).typeText( - 'One cool hacker', - ) - await element(by.id('editProfileSaveBtn')).tap() - await expect(element(by.id('editProfileModal'))).not.toBeVisible() - await expect(element(by.id('profileHeaderDisplayName'))).toHaveText( - 'Alicia', - ) - await expect(element(by.id('profileHeaderDescription'))).toHaveText( - 'One cool hacker', - ) - }) - - it('Remove display name and description via the edit profile modal', async () => { - await element(by.id('profileHeaderEditProfileButton')).tap() - await expect(element(by.id('editProfileModal'))).toBeVisible() - await element(by.id('editProfileDisplayNameInput')).clearText() - await element(by.id('editProfileDescriptionInput')).clearText() - await element(by.id('editProfileSaveBtn')).tap() - await expect(element(by.id('editProfileModal'))).not.toBeVisible() - await expect(element(by.id('profileHeaderDisplayName'))).toHaveText( - 'alice.test', - ) - await expect(element(by.id('profileHeaderDescription'))).not.toExist() - }) - - it('Set avi and banner via the edit profile modal', async () => { - await expect(element(by.id('userBannerFallback'))).toExist() - await expect(element(by.id('userAvatarFallback'))).toExist() - await element(by.id('profileHeaderEditProfileButton')).tap() - await expect(element(by.id('editProfileModal'))).toBeVisible() - await element(by.id('changeBannerBtn')).tap() - await element(by.text('Upload from Library')).tap() - await sleep(3e3) - await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Upload from Library')).tap() - await sleep(3e3) - await element(by.id('editProfileSaveBtn')).tap() - await expect(element(by.id('editProfileModal'))).not.toBeVisible() - await expect(element(by.id('userBannerImage'))).toExist() - await expect(element(by.id('userAvatarImage'))).toExist() - }) - - it('Remove avi and banner via the edit profile modal', async () => { - await expect(element(by.id('userBannerImage'))).toExist() - await expect(element(by.id('userAvatarImage'))).toExist() - await element(by.id('profileHeaderEditProfileButton')).tap() - await expect(element(by.id('editProfileModal'))).toBeVisible() - await element(by.id('changeBannerBtn')).tap() - await element(by.text('Remove Banner')).tap() - await element(by.id('changeAvatarBtn')).tap() - await element(by.text('Remove Avatar')).tap() - await element(by.id('editProfileSaveBtn')).tap() - await expect(element(by.id('editProfileModal'))).not.toBeVisible() - await expect(element(by.id('userBannerFallback'))).toExist() - await expect(element(by.id('userAvatarFallback'))).toExist() - }) - - it('Navigate to another user profile', async () => { - await element(by.id('bottomBarSearchBtn')).tap() - // have to wait for the toast to clear - await waitFor(element(by.id('searchTextInput'))) - .toBeVisible() - .withTimeout(5000) - await element(by.id('searchTextInput')).typeText('bob') - await element(by.id('searchAutoCompleteResult-bob.test')).tap() - await expect(element(by.id('profileView'))).toBeVisible() - }) - - it('Can follow/unfollow another user', async () => { - await element(by.id('followBtn')).tap() - await expect(element(by.id('unfollowBtn'))).toBeVisible() - await element(by.id('unfollowBtn')).tap() - await expect(element(by.id('followBtn'))).toBeVisible() - }) - - it('Can mute/unmute another user', async () => { - await expect(element(by.id('profileHeaderAlert'))).not.toExist() - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Mute Account')).tap() - await expect(element(by.id('profileHeaderAlert'))).toBeVisible() - await element(by.id('profileHeaderDropdownBtn')).tap() - await element(by.text('Unmute Account')).tap() - await expect(element(by.id('profileHeaderAlert'))).not.toExist() - }) - - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf - // it('Can report another user', async () => { - // await element(by.id('profileHeaderDropdownBtn')).tap() - // await element(by.text('Report Account')).tap() - // await expect(element(by.id('reportModal'))).toBeVisible() - // await element( - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), - // ).tap() - // await element(by.id('sendReportBtn')).tap() - // await expect(element(by.id('reportModal'))).not.toBeVisible() - // }) - - it('Can like posts', async () => { - await element(by.id('postsFeed-flatlist')).swipe( - 'down', - 'slow', - 1, - 0.5, - 0.5, - ) - - const posts = by.id('feedItem-by-bob.test') - await expect( - element(by.id('likeCount').withAncestor(posts)).atIndex(0), - ).not.toExist() - await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() - await expect( - element(by.id('likeCount').withAncestor(posts)).atIndex(0), - ).toHaveText('1') - await element(by.id('likeBtn').withAncestor(posts)).atIndex(0).tap() - await expect( - element(by.id('likeCount').withAncestor(posts)).atIndex(0), - ).not.toExist() - }) - - it('Can repost posts', async () => { - const posts = by.id('feedItem-by-bob.test') - await expect( - element(by.id('repostCount').withAncestor(posts)).atIndex(0), - ).not.toExist() - await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount').withAncestor(posts)).atIndex(0), - ).toHaveText('1') - await element(by.id('repostBtn').withAncestor(posts)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount').withAncestor(posts)).atIndex(0), - ).not.toExist() - }) - - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf - // it('Can report posts', async () => { - // const posts = by.id('feedItem-by-bob.test') - // await element(by.id('postDropdownBtn').withAncestor(posts)).atIndex(0).tap() - // await element(by.text('Report post')).tap() - // await expect(element(by.id('reportModal'))).toBeVisible() - // await element( - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), - // ).tap() - // await element(by.id('sendReportBtn')).tap() - // await expect(element(by.id('reportModal'))).not.toBeVisible() - // }) -}) diff --git a/__e2e__/tests/search-screen.test.ts b/__e2e__/tests/search-screen.test.ts deleted file mode 100644 index 1dbb3cbf..00000000 --- a/__e2e__/tests/search-screen.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* eslint-env detox/detox */ - -import {describe, beforeAll, it} from '@jest/globals' -import {expect} from 'detox' -import {openApp, loginAsAlice, createServer} from '../util' - -describe('Search screen', () => { - beforeAll(async () => { - await createServer('?users') - await openApp({ - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, - }) - }) - - it('Login', async () => { - await loginAsAlice() - }) - - it('Navigate to another user profile via autocomplete', async () => { - await element(by.id('bottomBarSearchBtn')).tap() - await element(by.id('searchTextInput')).typeText('bob') - await element(by.id('searchAutoCompleteResult-bob.test')).tap() - await expect(element(by.id('profileView'))).toBeVisible() - }) -}) diff --git a/__e2e__/tests/self-labeling.test.ts b/__e2e__/tests/self-labeling.test.ts deleted file mode 100644 index bba8ed48..00000000 --- a/__e2e__/tests/self-labeling.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* eslint-env detox/detox */ - -import {describe, beforeAll, it} from '@jest/globals' -import {expect} from 'detox' -import {openApp, loginAsAlice, createServer, sleep} from '../util' - -describe('Self-labeling', () => { - beforeAll(async () => { - await createServer('?users') - await openApp({ - permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, - }) - }) - - it('Login', async () => { - await loginAsAlice() - await element(by.id('homeScreenFeedTabs-Following')).tap() - }) - - it('Post an image with the porn label', async () => { - await element(by.id('composeFAB')).tap() - await element(by.id('composerTextInput')).typeText('Post with an image') - await element(by.id('openGalleryBtn')).tap() - await sleep(3e3) - await element(by.id('labelsBtn')).tap() - await element(by.id('pornLabelBtn')).tap() - await element(by.id('confirmBtn')).tap() - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - const posts = by.id('feedItem-by-alice.test') - await element(by.id('e2eRefreshHome')).tap() - await expect( - element(by.id('contentHider-embed').withAncestor(posts)).atIndex(0), - ).toExist() - }) -}) diff --git a/__e2e__/tests/shell.test.skip.ts b/__e2e__/tests/shell.test.skip.ts deleted file mode 100644 index 69619dd8..00000000 --- a/__e2e__/tests/shell.test.skip.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* eslint-env detox/detox */ - -import {openApp, loginAsAlice, createServer} from '../util' - -describe('Shell', () => { - beforeAll(async () => { - await createServer('?users') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('Login', async () => { - await loginAsAlice() - await element(by.id('homeScreenFeedTabs-Following')).tap() - }) - - it('Can swipe the shelf open', async () => { - await element(by.id('homeScreen')).swipe('right', 'fast', 0.75) - await expect(element(by.id('drawer'))).toBeVisible() - await element(by.id('drawer')).swipe('left', 'fast', 0.75) - await expect(element(by.id('drawer'))).not.toBeVisible() - }) - - it('Can open the shelf by pressing the header avi', async () => { - await element(by.id('viewHeaderDrawerBtn')).tap() - await expect(element(by.id('drawer'))).toBeVisible() - }) - - it('Can navigate using the shelf', async () => { - await element(by.id('menuItemButton-Notifications')).tap() - await expect(element(by.id('drawer'))).not.toBeVisible() - await expect(element(by.id('notificationsScreen'))).toBeVisible() - }) -}) diff --git a/__e2e__/tests/thread-muting.test.ts b/__e2e__/tests/thread-muting.test.ts deleted file mode 100644 index ae62f93d..00000000 --- a/__e2e__/tests/thread-muting.test.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* eslint-env detox/detox */ - -import {describe, beforeAll, it} from '@jest/globals' -import {expect} from 'detox' -import {openApp, loginAsAlice, loginAsBob, createServer} from '../util' - -describe('Thread muting', () => { - beforeAll(async () => { - await createServer('?users&follows') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('Login, create a thread, and log out', async () => { - await loginAsAlice() - await element(by.id('homeScreenFeedTabs-Following')).tap() - await element(by.id('composeFAB')).tap() - await element(by.id('composerTextInput')).typeText('Test thread') - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('Login, reply to the thread, and log out', async () => { - await loginAsBob() - await element(by.id('homeScreenFeedTabs-Following')).tap() - const alicePosts = by.id('feedItem-by-alice.test') - await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() - await element(by.id('composerTextInput')).typeText('Reply 1') - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - }) - - it('Login, confirm notification exists, mute thread, and log out', async () => { - await loginAsAlice() - await element(by.id('bottomBarNotificationsBtn')).tap() - const bobNotifs = by.id('feedItem-by-bob.test') - await expect( - element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), - ).toHaveText('Reply 1') - await element(by.id('postDropdownBtn').withAncestor(bobNotifs)) - .atIndex(0) - .tap() - await element(by.text('Mute thread')).tap() - // have to wait for the toast to clear - await waitFor(element(by.id('viewHeaderDrawerBtn'))) - .toBeVisible() - .withTimeout(5000) - }) - - it('Login, reply to the thread twice, and log out', async () => { - await loginAsBob() - - await element(by.id('bottomBarProfileBtn')).tap() - await element(by.id('profilePager-selector-1')).tap() - const bobPosts = by.id('feedItem-by-bob.test') - await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap() - await element(by.id('composerTextInput')).typeText('Reply 2') - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - - const alicePosts = by.id('feedItem-by-alice.test') - await element(by.id('replyBtn').withAncestor(alicePosts)).atIndex(0).tap() - await element(by.id('composerTextInput')).typeText('Reply 3') - await element(by.id('composerPublishBtn')).tap() - await expect(element(by.id('composeFAB'))).toBeVisible() - - await element(by.id('bottomBarHomeBtn')).tap() - }) - - it('Login, confirm notifications dont exist, unmute the thread, confirm notifications exist', async () => { - await loginAsAlice() - - await element(by.id('bottomBarNotificationsBtn')).tap() - const bobNotifs = by.id('feedItem-by-bob.test') - await expect( - element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), - ).not.toExist() - - await element(by.id('bottomBarHomeBtn')).tap() - const alicePosts = by.id('feedItem-by-alice.test') - await element(by.id('postDropdownBtn').withAncestor(alicePosts)) - .atIndex(0) - .tap() - await element(by.text('Unmute thread')).tap() - - // TODO - // the swipe down to trigger PTR isnt working and I dont want to block on this - // -prf - // await element(by.id('bottomBarNotificationsBtn')).tap() - // await element(by.id('notifsFeed')).swipe('down', 'fast') - // await waitFor(element(by.id('postText').withAncestor(bobNotifs))) - // .toBeVisible() - // .withTimeout(5000) - // await expect( - // element(by.id('postText').withAncestor(bobNotifs)).atIndex(0), - // ).toHaveText('Reply 2') - // await expect( - // element(by.id('postText').withAncestor(bobNotifs)).atIndex(1), - // ).toHaveText('Reply 3') - // await expect( - // element(by.id('postText').withAncestor(bobNotifs)).atIndex(2), - // ).toHaveText('Reply 1') - }) -}) diff --git a/__e2e__/tests/thread-screen.test.ts b/__e2e__/tests/thread-screen.test.ts deleted file mode 100644 index b99da11a..00000000 --- a/__e2e__/tests/thread-screen.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* eslint-env detox/detox */ - -import {beforeAll, describe, it} from '@jest/globals' -import {expect} from 'detox' - -import {createServer, loginAsAlice, openApp} from '../util' - -describe('Thread screen', () => { - beforeAll(async () => { - await createServer('?users&follows&thread') - await openApp({permissions: {notifications: 'YES'}}) - }) - - it('Login & navigate to thread', async () => { - await loginAsAlice() - await element(by.id('homeScreenFeedTabs-Following')).tap() - await element(by.id('feedItem-by-bob.test')).atIndex(0).tap() - await expect( - element( - by - .id('postThreadItem-by-bob.test') - .withDescendant(by.text('Thread root')), - ), - ).toBeVisible() - await expect( - element( - by - .id('postThreadItem-by-carla.test') - .withDescendant(by.text('Thread reply')), - ), - ).toBeVisible() - }) - - it('Can like the root post', async () => { - const post = by.id('postThreadItem-by-bob.test') - await expect( - element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), - ).not.toExist() - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() - await expect( - element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), - ).toHaveText('1 like') - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() - await expect( - element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0), - ).not.toExist() - }) - - it('Can like a reply post', async () => { - const post = by.id('postThreadItem-by-carla.test') - await expect( - element(by.id('likeCount').withAncestor(post)).atIndex(0), - ).not.toExist() - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() - await expect( - element(by.id('likeCount').withAncestor(post)).atIndex(0), - ).toHaveText('1') - await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() - await expect( - element(by.id('likeCount').withAncestor(post)).atIndex(0), - ).not.toExist() - }) - - it('Can repost the root post', async () => { - const post = by.id('postThreadItem-by-bob.test') - await expect( - element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), - ).not.toExist() - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), - ).toHaveText('1 repost') - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0), - ).not.toExist() - }) - - it('Can repost a reply post', async () => { - const post = by.id('postThreadItem-by-carla.test') - await expect( - element(by.id('repostCount').withAncestor(post)).atIndex(0), - ).not.toExist() - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount').withAncestor(post)).atIndex(0), - ).toHaveText('1') - await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() - await expect(element(by.id('repostModal'))).toBeVisible() - await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() - await expect(element(by.id('repostModal'))).not.toBeVisible() - await expect( - element(by.id('repostCount').withAncestor(post)).atIndex(0), - ).not.toExist() - }) - - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf - // it('Can report the root post', async () => { - // const post = by.id('postThreadItem-by-bob.test') - // await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() - // await element(by.text('Report post')).tap() - // await expect(element(by.id('reportModal'))).toBeVisible() - // await element( - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), - // ).tap() - // await element(by.id('sendReportBtn')).tap() - // await expect(element(by.id('reportModal'))).not.toBeVisible() - // }) - - // TODO skipping because the test env PDS isnt setup correctly to handle the report -prf - // it('Can report a reply post', async () => { - // const post = by.id('postThreadItem-by-carla.test') - // await element(by.id('postDropdownBtn').withAncestor(post)).atIndex(0).tap() - // await element(by.text('Report post')).tap() - // await expect(element(by.id('reportModal'))).toBeVisible() - // await element( - // by.id('reportReasonRadios-com.atproto.moderation.defs#reasonSpam'), - // ).tap() - // await element(by.id('sendReportBtn')).tap() - // await expect(element(by.id('reportModal'))).not.toBeVisible() - // }) -}) diff --git a/__e2e__/util.ts b/__e2e__/util.ts deleted file mode 100644 index 70fbdb60..00000000 --- a/__e2e__/util.ts +++ /dev/null @@ -1,141 +0,0 @@ -import {execSync} from 'child_process' -import {resolveConfig} from 'detox/internals' -import http from 'http' - -const platform = device.getPlatform() - -export async function openApp(opts: any) { - opts = opts || {} - const config = await resolveConfig() - - if (device.getPlatform() === 'ios') { - // disable password autofill - execSync( - `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`, - ) - execSync( - `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/EffectiveUserSettings.plist`, - ) - execSync( - `plutil -replace restrictedBool.allowPasswordAutoFill.value -bool NO ~/Library/Developer/CoreSimulator/Devices/${device.id}/data/Library/UserConfigurationProfiles/PublicInfo/PublicEffectiveUserSettings.plist`, - ) - } - if (config.configurationName.split('.').includes('debug')) { - return await openAppForDebugBuild(platform, opts) - } else { - return await device.launchApp({ - ...opts, - newInstance: true, - }) - } -} - -export async function isVisible(id: string) { - try { - await expect(element(by.id(id))).toBeVisible() - return true - } catch (e) { - return false - } -} - -export async function login( - service: string, - username: string, - password: string, - {takeScreenshots} = {takeScreenshots: false}, -) { - await element(by.id('signInButton')).tap() - if (takeScreenshots) { - await device.takeScreenshot('1- opened sign-in screen') - } - if (await isVisible('chooseAccountForm')) { - await element(by.id('chooseNewAccountBtn')).tap() - } - await element(by.id('selectServiceButton')).tap() - if (takeScreenshots) { - await device.takeScreenshot('2- opened service selector') - } - await element(by.id('customSelectBtn')).tap() - await element(by.id('customServerTextInput')).typeText(service) - await element(by.id('customServerTextInput')).tapReturnKey() - await element(by.id('doneBtn')).tap() - if (takeScreenshots) { - await device.takeScreenshot('3- input custom service') - } - await element(by.id('loginUsernameInput')).typeText(username) - await element(by.id('loginPasswordInput')).typeText(password) - if (takeScreenshots) { - await device.takeScreenshot('4- entered username and password') - } - await element(by.id('loginNextButton')).tap() -} - -export async function loginAsAlice() { - await element(by.id('e2eSignInAlice')).tap() -} - -export async function loginAsBob() { - await element(by.id('e2eSignInBob')).tap() -} - -async function openAppForDebugBuild(platform: string, opts: any) { - const deepLinkUrl = // Local testing with packager - /*process.env.EXPO_USE_UPDATES - ? // Testing latest published EAS update for the test_debug channel - getDeepLinkUrl(getLatestUpdateUrl()) - : */ getDeepLinkUrl(getDevLauncherPackagerUrl(platform)) - - if (platform === 'ios') { - await device.launchApp({ - ...opts, - newInstance: true, - }) - sleep(3000) - await device.openURL({ - url: deepLinkUrl, - }) - } else { - await device.launchApp({ - ...opts, - newInstance: true, - url: deepLinkUrl, - }) - } - - await sleep(3000) -} - -export async function createServer(path = ''): Promise { - return new Promise(function (resolve, reject) { - var req = http.request( - { - method: 'POST', - host: 'localhost', - port: 1986, - path: `/${path}`, - }, - function (res) { - const body: Buffer[] = [] - res.on('data', chunk => body.push(chunk)) - res.on('end', function () { - try { - resolve(Buffer.concat(body).toString()) - } catch (e) { - reject(e) - } - }) - }, - ) - req.on('error', reject) - req.end() - }) -} - -const getDeepLinkUrl = (url: string) => - `expo+bluesky://expo-development-client/?url=${encodeURIComponent(url)}` - -const getDevLauncherPackagerUrl = (platform: string) => - `http://localhost:8081/index.bundle?platform=${platform}&dev=true&minify=false&disableOnboarding=1` - -export const sleep = (t: number) => new Promise(res => setTimeout(res, t)) diff --git a/docs/build.md b/docs/build.md index deab91a5..88733d3b 100644 --- a/docs/build.md +++ b/docs/build.md @@ -16,10 +16,6 @@ - Add `eval "$(rbenv init - zsh)"` to your `~/.zshrc` - From inside the project directory: - `bundler install` (this will install Cocoapods) -- Setup your environment [for e2e testing using detox](https://wix.github.io/Detox/docs/introduction/getting-started): - - `yarn global add detox-cli` - - `brew tap wix/brew` - - `brew install applesimutils` - After initial setup: - Copy `google-services.json.example` to `google-services.json` or provide your own `google-services.json`. (A real firebase project is NOT required) - `npx expo prebuild` -> you will also need to run this anytime `app.json` or native `package.json` deps change @@ -120,10 +116,7 @@ To open the [Developer Menu](https://docs.expo.dev/debugging/tools/#developer-me ### Running E2E Tests -- Make sure you've set your environment following the above -- Make sure Metro and the dev server are running -- Run `yarn e2e` -- Find the artifacts in the `artifact` folder +See [testing.md](./testing.md). ### Polyfills diff --git a/docs/testing.md b/docs/testing.md index e9b9445e..ae0a424f 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -3,13 +3,19 @@ Make sure you've copied `.env.example` to `.env.test` and provided any required values. -### Using Maestro E2E tests +## Using Maestro + 1. Install Maestro by following [these instructions](https://maestro.mobile.dev/getting-started/installing-maestro). This will help us run the E2E tests. -2. You can write Maestro tests in `__e2e__/maestro` directory by creating a new `.yaml` file or by modifying an existing one. -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. +2. You can write Maestro tests in `/.maestro/flows/` directory by creating a new `.yml` file or by modifying an existing one. +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. +### Running Maestro tests -### Using Flashlight for Performance Testing +- In one tab, run `yarn e2e:mock-server` +- In a second tab, run `yarn e2e:metro` +- In a third tab, run `yarn e2e:run` + +## Using Flashlight for Performance Testing 1. Make sure Maestro is installed (optional: only for automated testing) by following the instructions above 2. Install Flashlight by following [these instructions](https://docs.flashlight.dev/) 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) diff --git a/jest/test-pds.ts b/jest/test-pds.ts index 1c52d944..2fe623ca 100644 --- a/jest/test-pds.ts +++ b/jest/test-pds.ts @@ -114,8 +114,7 @@ export async function createServer( pdsUrl, mocker: new Mocker(testNet, pdsUrl, pic), async close() { - await testNet.pds.server.destroy() - await testNet.plc.server.destroy() + await testNet.close() }, } } diff --git a/package.json b/package.json index 4ed2b933..13ffc35c 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,10 @@ "lint": "eslint --cache --ext .js,.jsx,.ts,.tsx src", "typecheck": "tsc --project ./tsconfig.check.json", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node --project tsconfig.e2e.json __e2e__/mock-server.ts", - "e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", - "e2e:build": "NODE_ENV=test detox build -c ios.sim.debug", - "e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all", + "e2e:metro": "EXPO_PUBLIC_ENV=e2e NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", + "e2e:run": "maestro test __e2e__", "perf:test": "NODE_ENV=test maestro test", - "perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml", + "perf:test:run": "NODE_ENV=test maestro test __e2e__/perf-test.yml", "perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand \"yarn perf:test\" --duration 150000 --resultsFilePath .perf/results.json", "perf:test:results": "NODE_ENV=test flashlight report .perf/results.json", "perf:measure": "NODE_ENV=test flashlight measure", @@ -239,10 +238,8 @@ "babel-plugin-module-resolver": "^5.0.0", "babel-plugin-react-native-web": "^0.18.12", "babel-preset-expo": "^10.0.0", - "detox": "^20.14.8", "eslint": "^8.19.0", "eslint-plugin-bsky-internal": "link:./eslint", - "eslint-plugin-detox": "^1.0.0", "eslint-plugin-ft-flow": "^2.0.3", "eslint-plugin-lingui": "^0.2.0", "eslint-plugin-react": "^7.33.2", diff --git a/src/view/com/testing/TestCtrls.e2e.tsx b/src/view/com/testing/TestCtrls.e2e.tsx index 1eb99c4f..1c82a712 100644 --- a/src/view/com/testing/TestCtrls.e2e.tsx +++ b/src/view/com/testing/TestCtrls.e2e.tsx @@ -1,11 +1,14 @@ import React from 'react' -import {Pressable, View} from 'react-native' -import {navigate} from '../../../Navigation' -import {useModalControls} from '#/state/modals' +import {LogBox, Pressable, View} from 'react-native' import {useQueryClient} from '@tanstack/react-query' -import {useSessionApi} from '#/state/session' + +import {useModalControls} from '#/state/modals' import {useSetFeedViewPreferencesMutation} from '#/state/queries/preferences' +import {useSessionApi} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' +import {navigate} from '../../../Navigation' + +LogBox.ignoreAllLogs() /** * This utility component is only included in the test simulator diff --git a/src/view/com/util/Toast.e2e.tsx b/src/view/com/util/Toast.e2e.tsx new file mode 100644 index 00000000..c5582ff0 --- /dev/null +++ b/src/view/com/util/Toast.e2e.tsx @@ -0,0 +1 @@ +export function show() {} diff --git a/yarn.lock b/yarn.lock index 1e53b306..f29994bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10719,47 +10719,6 @@ detect-port-alt@^1.1.6: address "^1.0.1" debug "^2.6.0" -detox@^20.14.8: - version "20.14.8" - resolved "https://registry.yarnpkg.com/detox/-/detox-20.14.8.tgz#0a550cf677fc98a68d56d162e1c5caad317de9ca" - integrity sha512-3E/0/7Cb7x+wcBsZpCxD8FykZUsFnfVT00d6PWH940boc0Mo1Kzabh+I151X/On4qZMqVdUzgwmap/z8g/kmaw== - dependencies: - ajv "^8.6.3" - bunyan "^1.8.12" - bunyan-debug-stream "^3.1.0" - caf "^15.0.1" - chalk "^4.0.0" - child-process-promise "^2.2.0" - execa "^5.1.1" - find-up "^5.0.0" - fs-extra "^11.0.0" - funpermaproxy "^1.1.0" - glob "^8.0.3" - ini "^1.3.4" - jest-environment-emit "^1.0.5" - json-cycle "^1.3.0" - lodash "^4.17.11" - multi-sort-stream "^1.0.3" - multipipe "^4.0.0" - node-ipc "9.2.1" - proper-lockfile "^3.0.2" - resolve-from "^5.0.0" - sanitize-filename "^1.6.1" - semver "^7.0.0" - serialize-error "^8.0.1" - shell-quote "^1.7.2" - signal-exit "^3.0.3" - stream-json "^1.7.4" - strip-ansi "^6.0.1" - telnet-client "1.2.8" - tempfile "^2.0.0" - trace-event-lib "^1.3.1" - which "^1.3.1" - ws "^7.0.0" - yargs "^17.0.0" - yargs-parser "^21.0.0" - yargs-unparser "^2.0.0" - didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -11393,13 +11352,6 @@ eslint-module-utils@^2.8.0: version "0.0.0" uid "" -eslint-plugin-detox@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-detox/-/eslint-plugin-detox-1.0.0.tgz#2d9c0130e8ebc4ced56efb6eeaf0d0f5c163398d" - integrity sha512-Dd+Cwyap5IO9DBKXOKrQTE1RQk9hvSSi+qsS1cMVPZY37mojz2PvriEOfGhKj5XN1G14lJ8TArf+6Y+Np2ZsoQ== - dependencies: - requireindex "~1.1.0" - eslint-plugin-eslint-comments@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa"