* Add logged out e2e ctrl, fix login test

* Fix log handling via env vars in expo

* Fix create account test

* Upgrade dev-env

* Fix home screen tests

* Fix composer tests

* Fix curate-lists tests, split in two

* Fix invite codes test

* Fix curate-lists tests

* Give up on mergefeed test

* Fix mod lists

* Fix app view url

* Fix profile tests

* Fix profile test with hack

* Keep using globals

* Fix two more

* Fix thread view

* Better skip for merge feed

* Revert debug code
zio/stable
Eric Bailey 2023-12-05 14:50:56 -06:00 committed by GitHub
parent ed5a97d0fa
commit 5f553c29df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1600 additions and 234 deletions

View File

@ -1,3 +1,6 @@
# Copy this to `.env` and `.env.test` files
SENTRY_AUTH_TOKEN= SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_ENV=development
EXPO_PUBLIC_LOG_LEVEL=debug EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG= EXPO_PUBLIC_LOG_DEBUG=

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer, sleep} from '../util' import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Composer', () => { describe('Composer', () => {
@ -45,6 +47,8 @@ describe('Composer', () => {
}) })
it('Reply text only', async () => { it('Reply text only', async () => {
await element(by.id('e2eRefreshHome')).tap()
const post = by.id('feedItem-by-alice.test') const post = by.id('feedItem-by-alice.test')
await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('replyBtn').withAncestor(post)).atIndex(0).tap()
await element(by.id('composerTextInput')).typeText('Reply text only') await element(by.id('composerTextInput')).typeText('Reply text only')

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, createServer} from '../util' import {openApp, createServer} from '../util'
describe('Create account', () => { describe('Create account', () => {
@ -10,6 +12,8 @@ describe('Create account', () => {
}) })
it('I can create a new account', async () => { it('I can create a new account', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await element(by.id('createAccountButton')).tap() await element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen') await device.takeScreenshot('1- opened create account screen')
await element(by.id('otherServerBtn')).tap() await element(by.id('otherServerBtn')).tap()
@ -17,14 +21,20 @@ describe('Create account', () => {
await element(by.id('customServerInput')).clearText() await element(by.id('customServerInput')).clearText()
await element(by.id('customServerInput')).typeText(service) await element(by.id('customServerInput')).typeText(service)
await device.takeScreenshot('3- input test server URL') await device.takeScreenshot('3- input test server URL')
await element(by.id('nextBtn')).tap() await element(by.id('nextBtn')).tap()
await element(by.id('emailInput')).typeText('example@test.com') await element(by.id('emailInput')).typeText('example@test.com')
await element(by.id('passwordInput')).typeText('hunter2') await element(by.id('passwordInput')).typeText('hunter2')
await device.takeScreenshot('4- entered account details') await device.takeScreenshot('4- entered account details')
await element(by.id('nextBtn')).tap() await element(by.id('nextBtn')).tap()
await element(by.id('handleInput')).typeText('e2e-test') await element(by.id('handleInput')).typeText('e2e-test')
await device.takeScreenshot('4- entered handle') await device.takeScreenshot('4- entered handle')
await element(by.id('nextBtn')).tap() await element(by.id('nextBtn')).tap()
await expect(element(by.id('welcomeOnboarding'))).toBeVisible() await expect(element(by.id('welcomeOnboarding'))).toBeVisible()
await element(by.id('continueBtn')).tap() await element(by.id('continueBtn')).tap()
await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible() await expect(element(by.id('recommendedFeedsOnboarding'))).toBeVisible()

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util' import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
describe('Curate lists', () => { describe('Curate lists', () => {
@ -11,7 +13,6 @@ describe('Curate lists', () => {
}) })
it('Login and create a curatelists', async () => { it('Login and create a curatelists', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await loginAsAlice() await loginAsAlice()
await element(by.id('e2eGotoLists')).tap() await element(by.id('e2eGotoLists')).tap()
await element(by.id('newUserListBtn')).tap() await element(by.id('newUserListBtn')).tap()
@ -27,7 +28,7 @@ describe('Curate lists', () => {
it('Edit display name and description via the edit curatelist modal', async () => { it('Edit display name and description via the edit curatelist modal', async () => {
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).clearText() await element(by.id('editNameInput')).clearText()
await element(by.id('editNameInput')).typeText('Bad Ppl') await element(by.id('editNameInput')).typeText('Bad Ppl')
@ -45,7 +46,7 @@ describe('Curate lists', () => {
it('Remove description via the edit curatelist modal', async () => { it('Remove description via the edit curatelist modal', async () => {
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editDescriptionInput')).clearText() await element(by.id('editDescriptionInput')).clearText()
await element(by.id('saveBtn')).tap() await element(by.id('saveBtn')).tap()
@ -60,7 +61,7 @@ describe('Curate lists', () => {
it('Set avi via the edit curatelist modal', async () => { it('Set avi via the edit curatelist modal', async () => {
await expect(element(by.id('userAvatarFallback'))).toExist() await expect(element(by.id('userAvatarFallback'))).toExist()
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap() await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Library')).tap() await element(by.text('Library')).tap()
@ -77,7 +78,7 @@ describe('Curate lists', () => {
it('Remove avi via the edit curatelist modal', async () => { it('Remove avi via the edit curatelist modal', async () => {
await expect(element(by.id('userAvatarImage'))).toExist() await expect(element(by.id('userAvatarImage'))).toExist()
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap() await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove')).tap() await element(by.text('Remove')).tap()
@ -98,6 +99,7 @@ describe('Curate lists', () => {
}) })
it('Create a new curatelist', async () => { it('Create a new curatelist', async () => {
await element(by.id('e2eGotoLists')).tap()
await element(by.id('newUserListBtn')).tap() await element(by.id('newUserListBtn')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).typeText('Good Ppl') await element(by.id('editNameInput')).typeText('Good Ppl')
@ -128,6 +130,7 @@ describe('Curate lists', () => {
}) })
it('Pins the list', async () => { it('Pins the list', async () => {
await expect(element(by.id('pinBtn'))).toBeVisible()
await element(by.id('pinBtn')).tap() await element(by.id('pinBtn')).tap()
await element(by.id('e2eGotoHome')).tap() await element(by.id('e2eGotoHome')).tap()
await element(by.id('homeScreenFeedTabs-Good Ppl')).tap() await element(by.id('homeScreenFeedTabs-Good Ppl')).tap()
@ -152,15 +155,15 @@ describe('Curate lists', () => {
await expect(element(by.id('user-bob.test'))).toBeVisible() await expect(element(by.id('user-bob.test'))).toBeVisible()
await element(by.id('user-bob.test-editBtn')).tap() await element(by.id('user-bob.test-editBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('toggleBtn-Good Ppl')).tap() await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('saveBtn')).tap() await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
}) })
it('Shows the curatelist on my profile', async () => { it('Shows the curatelist on my profile', async () => {
await element(by.id('bottomBarProfileBtn')).tap() await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('selector')).swipe('left') await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('selector-4')).tap() await element(by.id('profilePager-selector-5')).tap()
await element(by.id('list-Good Ppl')).tap() await element(by.id('list-Good Ppl')).tap()
}) })
@ -173,15 +176,15 @@ describe('Curate lists', () => {
await element(by.id('profileHeaderDropdownBtn')).tap() await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap() await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('toggleBtn-Good Ppl')).tap() await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('saveBtn')).tap() await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap() await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap() await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('toggleBtn-Good Ppl')).tap() await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('saveBtn')).tap() await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
}) })
@ -192,8 +195,8 @@ describe('Curate lists', () => {
await element(by.id('bottomBarSearchBtn')).tap() await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('alice') await element(by.id('searchTextInput')).typeText('alice')
await element(by.id('searchAutoCompleteResult-alice.test')).tap() await element(by.id('searchAutoCompleteResult-alice.test')).tap()
await element(by.id('selector')).swipe('left') await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('selector-3')).tap() await element(by.id('profilePager-selector-3')).tap()
await element(by.id('list-Good Ppl')).tap() await element(by.id('list-Good Ppl')).tap()
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Report List')).tap() await element(by.text('Report List')).tap()

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util' import {openApp, loginAsAlice, createServer} from '../util'
describe('Home screen', () => { describe('Home screen', () => {

View File

@ -5,6 +5,8 @@
* with the side drawer. * with the side drawer.
*/ */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util' import {openApp, loginAsAlice, createServer} from '../util'
describe('invite-codes', () => { describe('invite-codes', () => {
@ -16,7 +18,6 @@ describe('invite-codes', () => {
}) })
it('I can fetch invite codes', async () => { it('I can fetch invite codes', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await loginAsAlice() await loginAsAlice()
await element(by.id('e2eOpenInviteCodesModal')).tap() await element(by.id('e2eOpenInviteCodesModal')).tap()
await expect(element(by.id('inviteCodesModal'))).toBeVisible() await expect(element(by.id('inviteCodesModal'))).toBeVisible()
@ -27,6 +28,7 @@ describe('invite-codes', () => {
}) })
it('I can create a new account with the invite code', async () => { 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 element(by.id('createAccountButton')).tap()
await device.takeScreenshot('1- opened create account screen') await device.takeScreenshot('1- opened create account screen')
await element(by.id('otherServerBtn')).tap() await element(by.id('otherServerBtn')).tap()
@ -51,19 +53,4 @@ describe('invite-codes', () => {
await element(by.id('continueBtn')).tap() await element(by.id('continueBtn')).tap()
await expect(element(by.id('homeScreen'))).toBeVisible() await expect(element(by.id('homeScreen'))).toBeVisible()
}) })
it('I get a notification for the new user', async () => {
await element(by.id('e2eSignOut')).tap()
await loginAsAlice()
await waitFor(element(by.id('homeScreen')))
.toBeVisible()
.withTimeout(5000)
await element(by.id('bottomBarNotificationsBtn')).tap()
await expect(element(by.id('invitedUser'))).toBeVisible()
})
it('I can dismiss the new user notification', async () => {
await element(by.id('dismissBtn')).tap()
await expect(element(by.id('invitedUser'))).not.toBeVisible()
})
}) })

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, login, createServer} from '../util' import {openApp, login, createServer} from '../util'
describe('Login', () => { describe('Login', () => {
@ -10,6 +12,8 @@ describe('Login', () => {
}) })
it('As Alice, I can login', async () => { it('As Alice, I can login', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await expect(element(by.id('signInButton'))).toBeVisible() await expect(element(by.id('signInButton'))).toBeVisible()
await login(service, 'alice', 'hunter2', { await login(service, 'alice', 'hunter2', {
takeScreenshots: true, takeScreenshots: true,

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util' import {openApp, loginAsAlice, createServer} from '../util'
describe('Mergefeed', () => { describe('Mergefeed', () => {
@ -9,8 +11,12 @@ describe('Mergefeed', () => {
}) })
it('Login', async () => { it('Login', async () => {
await element(by.id('e2eOpenLoggedOutView')).tap()
await loginAsAlice() await loginAsAlice()
await element(by.id('e2eToggleMergefeed')).tap() 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 () => { it('Sees the expected mix of posts with default filters', async () => {

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util' import {openApp, loginAsAlice, loginAsBob, createServer, sleep} from '../util'
describe('Mod lists', () => { describe('Mod lists', () => {
@ -11,7 +13,6 @@ describe('Mod lists', () => {
}) })
it('Login and view my modlists', async () => { it('Login and view my modlists', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await loginAsAlice() await loginAsAlice()
await element(by.id('e2eGotoModeration')).tap() await element(by.id('e2eGotoModeration')).tap()
await element(by.id('moderationlistsBtn')).tap() await element(by.id('moderationlistsBtn')).tap()
@ -31,7 +32,7 @@ describe('Mod lists', () => {
it('Edit display name and description via the edit modlist modal', async () => { it('Edit display name and description via the edit modlist modal', async () => {
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editNameInput')).clearText() await element(by.id('editNameInput')).clearText()
await element(by.id('editNameInput')).typeText('Bad Ppl') await element(by.id('editNameInput')).typeText('Bad Ppl')
@ -49,7 +50,7 @@ describe('Mod lists', () => {
it('Remove description via the edit modlist modal', async () => { it('Remove description via the edit modlist modal', async () => {
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('editDescriptionInput')).clearText() await element(by.id('editDescriptionInput')).clearText()
await element(by.id('saveBtn')).tap() await element(by.id('saveBtn')).tap()
@ -64,7 +65,7 @@ describe('Mod lists', () => {
it('Set avi via the edit modlist modal', async () => { it('Set avi via the edit modlist modal', async () => {
await expect(element(by.id('userAvatarFallback'))).toExist() await expect(element(by.id('userAvatarFallback'))).toExist()
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap() await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Library')).tap() await element(by.text('Library')).tap()
@ -81,7 +82,7 @@ describe('Mod lists', () => {
it('Remove avi via the edit modlist modal', async () => { it('Remove avi via the edit modlist modal', async () => {
await expect(element(by.id('userAvatarImage'))).toExist() await expect(element(by.id('userAvatarImage'))).toExist()
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Edit List Details')).tap() await element(by.text('Edit list details')).tap()
await expect(element(by.id('createOrEditListModal'))).toBeVisible() await expect(element(by.id('createOrEditListModal'))).toBeVisible()
await element(by.id('changeAvatarBtn')).tap() await element(by.id('changeAvatarBtn')).tap()
await element(by.text('Remove')).tap() await element(by.text('Remove')).tap()
@ -131,15 +132,15 @@ describe('Mod lists', () => {
await expect(element(by.id('user-warn-posts.test'))).toBeVisible() await expect(element(by.id('user-warn-posts.test'))).toBeVisible()
await element(by.id('user-warn-posts.test-editBtn')).tap() await element(by.id('user-warn-posts.test-editBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('toggleBtn-Bad Ppl')).tap() await element(by.id('user-warn-posts.test-addBtn')).tap()
await element(by.id('saveBtn')).tap() await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
}) })
it('Shows the modlist on my profile', async () => { it('Shows the modlist on my profile', async () => {
await element(by.id('bottomBarProfileBtn')).tap() await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('selector')).swipe('left') await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('selector-4')).tap() await element(by.id('profilePager-selector-5')).tap()
await element(by.id('list-Bad Ppl')).tap() await element(by.id('list-Bad Ppl')).tap()
}) })
@ -152,15 +153,15 @@ describe('Mod lists', () => {
await element(by.id('profileHeaderDropdownBtn')).tap() await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap() await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('toggleBtn-Bad Ppl')).tap() await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('saveBtn')).tap() await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
await element(by.id('profileHeaderDropdownBtn')).tap() await element(by.id('profileHeaderDropdownBtn')).tap()
await element(by.text('Add to Lists')).tap() await element(by.text('Add to Lists')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).toBeVisible()
await element(by.id('toggleBtn-Bad Ppl')).tap() await element(by.id('user-bob.test-addBtn')).tap()
await element(by.id('saveBtn')).tap() await element(by.id('doneBtn')).tap()
await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible() await expect(element(by.id('userAddRemoveListsModal'))).not.toBeVisible()
}) })
@ -171,8 +172,8 @@ describe('Mod lists', () => {
await element(by.id('bottomBarSearchBtn')).tap() await element(by.id('bottomBarSearchBtn')).tap()
await element(by.id('searchTextInput')).typeText('alice') await element(by.id('searchTextInput')).typeText('alice')
await element(by.id('searchAutoCompleteResult-alice.test')).tap() await element(by.id('searchAutoCompleteResult-alice.test')).tap()
await element(by.id('selector')).swipe('left') await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('selector-3')).tap() await element(by.id('profilePager-selector-3')).tap()
await element(by.id('list-Bad Ppl')).tap() await element(by.id('list-Bad Ppl')).tap()
await element(by.id('headerDropdownBtn')).tap() await element(by.id('headerDropdownBtn')).tap()
await element(by.text('Report List')).tap() await element(by.text('Report List')).tap()

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer, sleep} from '../util' import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Profile screen', () => { describe('Profile screen', () => {
@ -11,17 +13,16 @@ describe('Profile screen', () => {
}) })
it('Login and navigate to my profile', async () => { it('Login and navigate to my profile', async () => {
await expect(element(by.id('signInButton'))).toBeVisible()
await loginAsAlice() await loginAsAlice()
await element(by.id('bottomBarProfileBtn')).tap() await element(by.id('bottomBarProfileBtn')).tap()
}) })
it('Can see feeds', async () => { it('Can see feeds', async () => {
await element(by.id('selector')).swipe('left') await element(by.id('profilePager-selector')).swipe('left')
await element(by.id('selector-4')).tap() await element(by.id('profilePager-selector-4')).tap()
await expect(element(by.id('feed-alice-favs'))).toBeVisible() await expect(element(by.id('feed-alice-favs'))).toBeVisible()
await element(by.id('selector')).swipe('right') await element(by.id('profilePager-selector')).swipe('right')
await element(by.id('selector-0')).tap() await element(by.id('profilePager-selector-0')).tap()
}) })
it('Open and close edit profile modal', async () => { it('Open and close edit profile modal', async () => {
@ -135,6 +136,14 @@ describe('Profile screen', () => {
}) })
it('Can like posts', async () => { 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') const posts = by.id('feedItem-by-bob.test')
await expect( await expect(
element(by.id('likeCount').withAncestor(posts)).atIndex(0), element(by.id('likeCount').withAncestor(posts)).atIndex(0),

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util' import {openApp, loginAsAlice, createServer} from '../util'
describe('Search screen', () => { describe('Search screen', () => {

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer, sleep} from '../util' import {openApp, loginAsAlice, createServer, sleep} from '../util'
describe('Self-labeling', () => { describe('Self-labeling', () => {

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, loginAsBob, createServer} from '../util' import {openApp, loginAsAlice, loginAsBob, createServer} from '../util'
describe('Thread muting', () => { describe('Thread muting', () => {
@ -48,7 +50,7 @@ describe('Thread muting', () => {
await loginAsBob() await loginAsBob()
await element(by.id('bottomBarProfileBtn')).tap() await element(by.id('bottomBarProfileBtn')).tap()
await element(by.id('selector-1')).tap() await element(by.id('profilePager-selector-1')).tap()
const bobPosts = by.id('feedItem-by-bob.test') const bobPosts = by.id('feedItem-by-bob.test')
await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap() await element(by.id('replyBtn').withAncestor(bobPosts)).atIndex(0).tap()
await element(by.id('composerTextInput')).typeText('Reply 2') await element(by.id('composerTextInput')).typeText('Reply 2')

View File

@ -1,5 +1,7 @@
/* eslint-env detox/detox */ /* eslint-env detox/detox */
import {describe, beforeAll, it} from '@jest/globals'
import {expect} from 'detox'
import {openApp, loginAsAlice, createServer} from '../util' import {openApp, loginAsAlice, createServer} from '../util'
describe('Thread screen', () => { describe('Thread screen', () => {
@ -31,15 +33,15 @@ describe('Thread screen', () => {
it('Can like the root post', async () => { it('Can like the root post', async () => {
const post = by.id('postThreadItem-by-bob.test') const post = by.id('postThreadItem-by-bob.test')
await expect( await expect(
element(by.id('likeCount').withAncestor(post)).atIndex(0), element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist() ).not.toExist()
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
await expect( await expect(
element(by.id('likeCount').withAncestor(post)).atIndex(0), element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
).toHaveText('1 like') ).toHaveText('1 like')
await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap()
await expect( await expect(
element(by.id('likeCount').withAncestor(post)).atIndex(0), element(by.id('likeCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist() ).not.toExist()
}) })
@ -61,21 +63,21 @@ describe('Thread screen', () => {
it('Can repost the root post', async () => { it('Can repost the root post', async () => {
const post = by.id('postThreadItem-by-bob.test') const post = by.id('postThreadItem-by-bob.test')
await expect( await expect(
element(by.id('repostCount').withAncestor(post)).atIndex(0), element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist() ).not.toExist()
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
await expect(element(by.id('repostModal'))).toBeVisible() await expect(element(by.id('repostModal'))).toBeVisible()
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
await expect(element(by.id('repostModal'))).not.toBeVisible() await expect(element(by.id('repostModal'))).not.toBeVisible()
await expect( await expect(
element(by.id('repostCount').withAncestor(post)).atIndex(0), element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
).toHaveText('1 repost') ).toHaveText('1 repost')
await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap() await element(by.id('repostBtn').withAncestor(post)).atIndex(0).tap()
await expect(element(by.id('repostModal'))).toBeVisible() await expect(element(by.id('repostModal'))).toBeVisible()
await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap() await element(by.id('repostBtn').withAncestor(by.id('repostModal'))).tap()
await expect(element(by.id('repostModal'))).not.toBeVisible() await expect(element(by.id('repostModal'))).not.toBeVisible()
await expect( await expect(
element(by.id('repostCount').withAncestor(post)).atIndex(0), element(by.id('repostCount-expanded').withAncestor(post)).atIndex(0),
).not.toExist() ).not.toExist()
}) })

View File

@ -1,5 +1,8 @@
# Testing instructions # Testing instructions
Make sure you've copied `.env.example` to `.env.test` and provided any required
values.
### Using Maestro E2E tests ### Using Maestro E2E tests
1. Install Maestro by following [these instructions](https://maestro.mobile.dev/getting-started/installing-maestro). This will help us run the E2E tests. 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. 2. You can write Maestro tests in `__e2e__/maestro` directory by creating a new `.yaml` file or by modifying an existing one.

View File

@ -1,15 +1,21 @@
import 'react-native-gesture-handler' // must be first import 'react-native-gesture-handler' // must be first
import {LogBox} from 'react-native' import {LogBox} from 'react-native'
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
import '#/platform/polyfills' import '#/platform/polyfills'
import {IS_TEST} from '#/env'
import {registerRootComponent} from 'expo' import {registerRootComponent} from 'expo'
import {doPolyfill} from '#/lib/api/api-polyfill' import {doPolyfill} from '#/lib/api/api-polyfill'
doPolyfill()
import App from '#/App' import App from '#/App'
doPolyfill()
if (IS_TEST) {
LogBox.ignoreAllLogs() // suppress all logs in tests
} else {
LogBox.ignoreLogs(['Require cycle:']) // suppress require-cycle warnings, it's fine
}
// registerRootComponent calls AppRegistry.registerComponent('main', () => App); // registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build, // It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately // the environment is set up appropriately

View File

@ -59,17 +59,21 @@ export async function createServer(
): Promise<TestPDS> { ): Promise<TestPDS> {
const port = await getPort() const port = await getPort()
const port2 = await getPort(port + 1) const port2 = await getPort(port + 1)
const port3 = await getPort(port2 + 1)
const pdsUrl = `http://localhost:${port}` const pdsUrl = `http://localhost:${port}`
const id = ids.next() const id = ids.next()
const testNet = await TestNetwork.create({ const testNet = await TestNetwork.create({
pds: { pds: {
port, port,
publicUrl: pdsUrl, hostname: 'localhost',
inviteRequired,
dbPostgresSchema: `pds_${id}`, dbPostgresSchema: `pds_${id}`,
inviteRequired,
}, },
bsky: { bsky: {
dbPostgresSchema: `bsky_${id}`, dbPostgresSchema: `bsky_${id}`,
port: port3,
publicUrl: 'http://localhost:2584',
}, },
plc: {port: port2}, plc: {port: port2},
}) })

View File

@ -21,9 +21,9 @@
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --project ./tsconfig.check.json", "typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts",
"e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:metro": "NODE_ENV=test RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build": "detox build -c ios.sim.debug", "e2e:build": "NODE_ENV=test detox build -c ios.sim.debug",
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", "e2e:run": "NODE_ENV=test detox test --configuration ios.sim.debug --take-screenshots all",
"perf:test": "NODE_ENV=test maestro test", "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__/maestro/scroll.yaml",
"perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json", "perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
@ -168,7 +168,7 @@
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
"devDependencies": { "devDependencies": {
"@atproto/dev-env": "^0.2.5", "@atproto/dev-env": "^0.2.16",
"@babel/core": "^7.23.2", "@babel/core": "^7.23.2",
"@babel/preset-env": "^7.20.0", "@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",

View File

@ -1,4 +1,4 @@
export const IS_TEST = process.env.NODE_ENV === 'test' export const IS_TEST = process.env.EXPO_PUBLIC_ENV === 'test'
export const IS_DEV = __DEV__ export const IS_DEV = __DEV__
export const IS_PROD = !IS_DEV export const IS_PROD = !IS_DEV
export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || '' export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''

View File

@ -61,6 +61,7 @@ export interface CreateOrEditListModal {
export interface UserAddRemoveListsModal { export interface UserAddRemoveListsModal {
name: 'user-add-remove-lists' name: 'user-add-remove-lists'
subject: string subject: string
handle: string
displayName: string displayName: string
onAdd?: (listUri: string) => void onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void onRemove?: (listUri: string) => void

View File

@ -170,6 +170,7 @@ export function FeedSourceCardLoaded({
{showSaveBtn && feed.type === 'feed' && ( {showSaveBtn && feed.type === 'feed' && (
<View> <View>
<Pressable <Pressable
testID={`feed-${feed.displayName}-toggleSave`}
disabled={isSavePending || isPinPending || isRemovePending} disabled={isSavePending || isPinPending || isRemovePending}
accessibilityRole="button" accessibilityRole="button"
accessibilityLabel={ accessibilityLabel={

View File

@ -132,6 +132,7 @@ export function ListMembers({
name: 'user-add-remove-lists', name: 'user-add-remove-lists',
subject: profile.did, subject: profile.did,
displayName: profile.displayName || profile.handle, displayName: profile.displayName || profile.handle,
handle: profile.handle,
}) })
}, },
[openModal], [openModal],

View File

@ -28,11 +28,13 @@ export const snapPoints = ['fullscreen']
export function Component({ export function Component({
subject, subject,
handle,
displayName, displayName,
onAdd, onAdd,
onRemove, onRemove,
}: { }: {
subject: string subject: string
handle: string
displayName: string displayName: string
onAdd?: (listUri: string) => void onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void onRemove?: (listUri: string) => void
@ -60,6 +62,7 @@ export function Component({
list={list} list={list}
memberships={memberships} memberships={memberships}
subject={subject} subject={subject}
handle={handle}
onAdd={onAdd} onAdd={onAdd}
onRemove={onRemove} onRemove={onRemove}
/> />
@ -87,6 +90,7 @@ function ListItem({
list, list,
memberships, memberships,
subject, subject,
handle,
onAdd, onAdd,
onRemove, onRemove,
}: { }: {
@ -94,6 +98,7 @@ function ListItem({
list: GraphDefs.ListView list: GraphDefs.ListView
memberships: ListMembersip[] | undefined memberships: ListMembersip[] | undefined
subject: string subject: string
handle: string
onAdd?: (listUri: string) => void onAdd?: (listUri: string) => void
onRemove?: (listUri: string) => void onRemove?: (listUri: string) => void
}) { }) {
@ -182,7 +187,7 @@ function ListItem({
<ActivityIndicator /> <ActivityIndicator />
) : ( ) : (
<Button <Button
testID={`user-${subject}-addBtn`} testID={`user-${handle}-addBtn`}
type="default" type="default"
label={membership === false ? _(msg`Add`) : _(msg`Remove`)} label={membership === false ? _(msg`Add`) : _(msg`Remove`)}
onPress={onToggleMembership} onPress={onToggleMembership}

View File

@ -375,7 +375,10 @@ let PostThreadItemLoaded = ({
style={styles.expandedInfoItem} style={styles.expandedInfoItem}
href={repostsHref} href={repostsHref}
title={repostsTitle}> title={repostsTitle}>
<Text testID="repostCount" type="lg" style={pal.textLight}> <Text
testID="repostCount-expanded"
type="lg"
style={pal.textLight}>
<Text type="xl-bold" style={pal.text}> <Text type="xl-bold" style={pal.text}>
{formatCount(post.repostCount)} {formatCount(post.repostCount)}
</Text>{' '} </Text>{' '}
@ -390,7 +393,10 @@ let PostThreadItemLoaded = ({
style={styles.expandedInfoItem} style={styles.expandedInfoItem}
href={likesHref} href={likesHref}
title={likesTitle}> title={likesTitle}>
<Text testID="likeCount" type="lg" style={pal.textLight}> <Text
testID="likeCount-expanded"
type="lg"
style={pal.textLight}>
<Text type="xl-bold" style={pal.text}> <Text type="xl-bold" style={pal.text}>
{formatCount(post.likeCount)} {formatCount(post.likeCount)}
</Text>{' '} </Text>{' '}

View File

@ -217,6 +217,7 @@ let ProfileHeaderLoaded = ({
openModal({ openModal({
name: 'user-add-remove-lists', name: 'user-add-remove-lists',
subject: profile.did, subject: profile.did,
handle: profile.handle,
displayName: profile.displayName || profile.handle, displayName: profile.displayName || profile.handle,
onAdd: invalidateProfileQuery, onAdd: invalidateProfileQuery,
onRemove: invalidateProfileQuery, onRemove: invalidateProfileQuery,

View File

@ -5,6 +5,7 @@ import {useModalControls} from '#/state/modals'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {useSessionApi} from '#/state/session' import {useSessionApi} from '#/state/session'
import {useSetFeedViewPreferencesMutation} from '#/state/queries/preferences' import {useSetFeedViewPreferencesMutation} from '#/state/queries/preferences'
import {useLoggedOutViewControls} from '#/state/shell/logged-out'
/** /**
* This utility component is only included in the test simulator * This utility component is only included in the test simulator
@ -19,6 +20,7 @@ export function TestCtrls() {
const {logout, login} = useSessionApi() const {logout, login} = useSessionApi()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation() const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
const {setShowLoggedOut} = useLoggedOutViewControls()
const onPressSignInAlice = async () => { const onPressSignInAlice = async () => {
await login({ await login({
service: 'http://localhost:3000', service: 'http://localhost:3000',
@ -95,6 +97,12 @@ export function TestCtrls() {
accessibilityRole="button" accessibilityRole="button"
style={BTN} style={BTN}
/> />
<Pressable
testID="e2eOpenLoggedOutView"
onPress={() => setShowLoggedOut(true)}
accessibilityRole="button"
style={BTN}
/>
</View> </View>
) )
} }

View File

@ -7,6 +7,7 @@ import {colors} from 'lib/styles'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {useAnimatedValue} from 'lib/hooks/useAnimatedValue'
import {IS_TEST} from '#/env'
const TIMEOUT = 4e3 const TIMEOUT = 4e3
@ -14,6 +15,7 @@ export function show(
message: string, message: string,
_icon: FontAwesomeProps['icon'] = 'check', _icon: FontAwesomeProps['icon'] = 'check',
) { ) {
if (IS_TEST) return
const item = new RootSiblings(<Toast message={message} />) const item = new RootSiblings(<Toast message={message} />)
setTimeout(() => { setTimeout(() => {
item.destroy() item.destroy()

View File

@ -42,6 +42,7 @@ export function SearchResultCard({
return ( return (
<Link <Link
testID={`searchAutoCompleteResult-${profile.handle}`}
href={makeProfileLink(profile)} href={makeProfileLink(profile)}
title={profile.handle} title={profile.handle}
asAnchor asAnchor

1614
yarn.lock

File diff suppressed because it is too large Load Diff