diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-03-31 13:17:26 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-03-31 13:17:26 -0500 |
commit | a3334a01a221877d3e06e02f960fda441f3460bd (patch) | |
tree | 64cdbb1232d1a3c00750c346b6e3ae529b51d1b0 /__e2e__ | |
parent | 19f3a2fa92a61ddb785fc4e42d73792c1d0e772c (diff) | |
download | voidsky-a3334a01a221877d3e06e02f960fda441f3460bd.tar.zst |
Lex refactor (#362)
* Remove the hackcheck for upgrades * Rename the PostEmbeds folder to match the codebase style * Updates to latest lex refactor * Update to use new bsky agent * Update to use api package's richtext library * Switch to upsertProfile * Add TextEncoder/TextDecoder polyfill * Add Intl.Segmenter polyfill * Update composer to calculate lengths by grapheme * Fix detox * Fix login in e2e * Create account e2e passing * Implement an e2e mocking framework * Don't use private methods on mobx models as mobx can't track them * Add tooling for e2e-specific builds and add e2e media-picker mock * Add some tests and fix some bugs around profile editing * Add shell tests * Add home screen tests * Add thread screen tests * Add tests for other user profile screens * Add search screen tests * Implement profile imagery change tools and tests * Update to new embed behaviors * Add post tests * Fix to profile-screen test * Fix session resumption * Update web composer to new api * 1.11.0 * Fix pagination cursor parameters * Add quote posts to notifications * Fix embed layouts * Remove youtube inline player and improve tap handling on link cards * Reset minimal shell mode on all screen loads and feed swipes (close #299) * Update podfile.lock * Improve post notfound UI (close #366) * Bump atproto packages
Diffstat (limited to '__e2e__')
-rw-r--r-- | __e2e__/jest.config.js | 12 | ||||
-rw-r--r-- | __e2e__/mock-server.ts | 75 | ||||
-rw-r--r-- | __e2e__/tests/composer.test.ts | 108 | ||||
-rw-r--r-- | __e2e__/tests/create-account.test.ts | 31 | ||||
-rw-r--r-- | __e2e__/tests/home-screen.test.ts | 92 | ||||
-rw-r--r-- | __e2e__/tests/login.test.ts | 19 | ||||
-rw-r--r-- | __e2e__/tests/profile-screen.test.ts | 173 | ||||
-rw-r--r-- | __e2e__/tests/search-screen.test.ts | 24 | ||||
-rw-r--r-- | __e2e__/tests/shell.test.ts | 34 | ||||
-rw-r--r-- | __e2e__/tests/thread-screen.test.ts | 123 | ||||
-rw-r--r-- | __e2e__/util.ts | 96 |
11 files changed, 787 insertions, 0 deletions
diff --git a/__e2e__/jest.config.js b/__e2e__/jest.config.js new file mode 100644 index 000000000..80c2ad5b3 --- /dev/null +++ b/__e2e__/jest.config.js @@ -0,0 +1,12 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + rootDir: '..', + testMatch: ['<rootDir>/__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__/mock-server.ts b/__e2e__/mock-server.ts new file mode 100644 index 000000000..7a2be6060 --- /dev/null +++ b/__e2e__/mock-server.ts @@ -0,0 +1,75 @@ +import {createServer as createHTTPServer} from 'node:http' +import {parse} from 'node:url' +import {createServer, TestPDS} from '../jest/test-pds' + +async function main() { + let server: TestPDS + createHTTPServer(async (req, res) => { + const url = parse(req.url || '/', true) + if (req.method !== 'POST') { + return res.writeHead(200).end() + } + try { + console.log('Closing old server') + await server?.close() + console.log('Starting new server') + server = await createServer() + console.log('Listening at', server.pdsUrl) + if (url?.query) { + if ('users' in url.query) { + console.log('Generating mock users') + await server.mocker.createUser('alice') + await server.mocker.createUser('bob') + await server.mocker.createUser('carla') + await server.mocker.users.alice.agent.upsertProfile(() => ({ + displayName: 'Alice', + description: 'Test user 1', + })) + await server.mocker.users.bob.agent.upsertProfile(() => ({ + displayName: 'Bob', + description: 'Test user 2', + })) + await server.mocker.users.carla.agent.upsertProfile(() => ({ + displayName: 'Carla', + description: 'Test user 3', + })) + } + if ('follows' in url.query) { + console.log('Generating mock follows') + await server.mocker.follow('alice', 'bob') + await server.mocker.follow('alice', 'carla') + await server.mocker.follow('bob', 'alice') + await server.mocker.follow('bob', 'carla') + await server.mocker.follow('carla', 'alice') + await server.mocker.follow('carla', 'bob') + } + if ('posts' in url.query) { + console.log('Generating mock posts') + for (let user in server.mocker.users) { + await server.mocker.users[user].agent.post({text: 'Post'}) + } + } + if ('thread' in url.query) { + console.log('Generating mock posts') + const res = await server.mocker.users.bob.agent.post({ + text: 'Thread root', + }) + await server.mocker.users.carla.agent.post({ + text: 'Thread reply', + reply: { + parent: {cid: res.cid, uri: res.uri}, + root: {cid: res.cid, uri: res.uri}, + }, + }) + } + } + console.log('Ready') + return res.writeHead(200).end(server.pdsUrl) + } catch (e) { + console.error('Error!', e) + return res.writeHead(500).end() + } + }).listen(1986) + console.log('Mock server manager listening on 1986') +} +main() diff --git a/__e2e__/tests/composer.test.ts b/__e2e__/tests/composer.test.ts new file mode 100644 index 000000000..afc23cc13 --- /dev/null +++ b/__e2e__/tests/composer.test.ts @@ -0,0 +1,108 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer, sleep} from '../util' + +describe('Composer', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + 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('addLinkCardBtn')).tap() + await element(by.id('composerPublishBtn')).tap() + await expect(element(by.id('composeFAB'))).toBeVisible() + }) + + it('Reply text only', 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 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('addLinkCardBtn')).tap() + 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('addLinkCardBtn')).tap() + 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 new file mode 100644 index 000000000..7b2e00fb5 --- /dev/null +++ b/__e2e__/tests/create-account.test.ts @@ -0,0 +1,31 @@ +/* eslint-env detox/detox */ + +import {openApp, createServer} from '../util' + +describe('Create account', () => { + let service: string + beforeAll(async () => { + service = await createServer('mock0') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('I can create a new account', async () => { + await element(by.id('createAccountButton')).tap() + await device.takeScreenshot('1- opened create account screen') + await element(by.id('otherServerBtn')).tap() + await device.takeScreenshot('2- selected other server') + await element(by.id('customServerInput')).clearText() + await element(by.id('customServerInput')).typeText(service) + await device.takeScreenshot('3- input test server URL') + await element(by.id('nextBtn')).tap() + await element(by.id('emailInput')).typeText('example@test.com') + await element(by.id('passwordInput')).typeText('hunter2') + await element(by.id('is13Input')).tap() + 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('homeScreen'))).toBeVisible() + }) +}) diff --git a/__e2e__/tests/home-screen.test.ts b/__e2e__/tests/home-screen.test.ts new file mode 100644 index 000000000..1ec1774f3 --- /dev/null +++ b/__e2e__/tests/home-screen.test.ts @@ -0,0 +1,92 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Home screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&follows&posts') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + await element(by.id('homeScreenFeedTabs-Following')).tap() + }) + + it('Can like posts', async () => { + const carlaPosts = by.id('feedItem-by-carla.test') + await expect( + element(by.id('likeCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('0') + 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), + ).toHaveText('0') + }) + + it('Can repost posts', async () => { + const carlaPosts = by.id('feedItem-by-carla.test') + await expect( + element(by.id('repostCount').withAncestor(carlaPosts)).atIndex(0), + ).toHaveText('0') + 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), + ).toHaveText('0') + }) + + 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.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) + + it('Can swipe between feeds', async () => { + await element(by.id('homeScreen')).swipe('left', 'fast', 0.75) + await expect(element(by.id('whatshotFeedPage'))).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-What's hot")).tap() + await expect(element(by.id('whatshotFeedPage'))).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.id('postDropdownDeleteBtn')).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/login.test.ts b/__e2e__/tests/login.test.ts new file mode 100644 index 000000000..788016db6 --- /dev/null +++ b/__e2e__/tests/login.test.ts @@ -0,0 +1,19 @@ +/* eslint-env detox/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 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/profile-screen.test.ts b/__e2e__/tests/profile-screen.test.ts new file mode 100644 index 000000000..e1b6dcaf4 --- /dev/null +++ b/__e2e__/tests/profile-screen.test.ts @@ -0,0 +1,173 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer, sleep} from '../util' + +describe('Profile screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&posts') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login and navigate to my profile', async () => { + await expect(element(by.id('signInButton'))).toBeVisible() + await login(service, 'alice', 'hunter2') + await element(by.id('bottomBarProfileBtn')).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'))).toHaveText('') + }) + + 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.id('changeBannerLibraryBtn')).tap() + await sleep(3e3) + await element(by.id('changeAvatarBtn')).tap() + await element(by.id('changeAvatarLibraryBtn')).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.id('changeBannerRemoveBtn')).tap() + await element(by.id('changeAvatarBtn')).tap() + await element(by.id('changeAvatarRemoveBtn')).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(2000) + 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('profileHeaderMutedNotice'))).not.toExist() + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownMuteBtn')).tap() + await expect(element(by.id('profileHeaderMutedNotice'))).toBeVisible() + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownMuteBtn')).tap() + await expect(element(by.id('profileHeaderMutedNotice'))).not.toExist() + }) + + it('Can report another user', async () => { + await element(by.id('profileHeaderDropdownBtn')).tap() + await element(by.id('profileHeaderDropdownReportBtn')).tap() + await expect(element(by.id('reportAccountModal'))).toBeVisible() + await element(by.id('reportAccountRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportAccountModal'))).not.toBeVisible() + }) + + it('Can like posts', async () => { + const posts = by.id('feedItem-by-bob.test') + await expect( + element(by.id('likeCount').withAncestor(posts)).atIndex(0), + ).toHaveText('0') + 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), + ).toHaveText('0') + }) + + it('Can repost posts', async () => { + const posts = by.id('feedItem-by-bob.test') + await expect( + element(by.id('repostCount').withAncestor(posts)).atIndex(0), + ).toHaveText('0') + 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), + ).toHaveText('0') + }) + + 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.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) +}) diff --git a/__e2e__/tests/search-screen.test.ts b/__e2e__/tests/search-screen.test.ts new file mode 100644 index 000000000..093d97c89 --- /dev/null +++ b/__e2e__/tests/search-screen.test.ts @@ -0,0 +1,24 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Search screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users') + await openApp({ + permissions: {notifications: 'YES', medialibrary: 'YES', photos: 'YES'}, + }) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + }) + + 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/shell.test.ts b/__e2e__/tests/shell.test.ts new file mode 100644 index 000000000..5cfd4277f --- /dev/null +++ b/__e2e__/tests/shell.test.ts @@ -0,0 +1,34 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Shell', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login', async () => { + await login(service, 'alice', 'hunter2') + 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-screen.test.ts b/__e2e__/tests/thread-screen.test.ts new file mode 100644 index 000000000..f84c339ce --- /dev/null +++ b/__e2e__/tests/thread-screen.test.ts @@ -0,0 +1,123 @@ +/* eslint-env detox/detox */ + +import {openApp, login, createServer} from '../util' + +describe('Thread screen', () => { + let service: string + beforeAll(async () => { + service = await createServer('?users&follows&thread') + await openApp({permissions: {notifications: 'YES'}}) + }) + + it('Login & navigate to thread', async () => { + await login(service, 'alice', 'hunter2') + 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').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 like') + await element(by.id('likeBtn').withAncestor(post)).atIndex(0).tap() + await expect( + element(by.id('likeCount').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), + ).toHaveText('0') + 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), + ).toHaveText('0') + }) + + it('Can repost the root post', async () => { + const post = by.id('postThreadItem-by-bob.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 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').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), + ).toHaveText('0') + 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), + ).toHaveText('0') + }) + + 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.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) + + 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.id('postDropdownReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).toBeVisible() + await element(by.id('reportPostRadios-spam')).tap() + await element(by.id('sendReportBtn')).tap() + await expect(element(by.id('reportPostModal'))).not.toBeVisible() + }) +}) diff --git a/__e2e__/util.ts b/__e2e__/util.ts new file mode 100644 index 000000000..78d9f9f5d --- /dev/null +++ b/__e2e__/util.ts @@ -0,0 +1,96 @@ +import {resolveConfig} from 'detox/internals' + +const platform = device.getPlatform() + +export async function openApp(opts: any) { + opts = opts || {} + const config = await resolveConfig() + 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('loginSelectServiceButton')).tap() + if (takeScreenshots) { + await device.takeScreenshot('2- opened service selector') + } + await element(by.id('customServerTextInput')).typeText(service) + await element(by.id('customServerSelectBtn')).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() +} + +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 = '') { + const res = await fetch(`http://localhost:1986/${path}`, {method: 'POST'}) + const resBody = await res.text() + return resBody +} + +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)) |