Account switcher (#85)
* Update the account-create and signin views to use the design system. Also: - Add borderDark to the theme - Start to an account selector in the signin flow * Dark mode fixes in signin ui * Track multiple active accounts and provide account-switching UI * Add test tooling for an in-memory pds * Add complete integration tests for login and the account switcher
This commit is contained in:
parent
439305b57e
commit
9027882fb4
23 changed files with 2406 additions and 658 deletions
241
__tests__/accounts.test.tsx
Normal file
241
__tests__/accounts.test.tsx
Normal file
|
@ -0,0 +1,241 @@
|
|||
import React from 'react'
|
||||
import {MobileShell} from '../src/view/shell/mobile'
|
||||
import {cleanup, fireEvent, render, waitFor} from '../jest/test-utils'
|
||||
import {createServer, TestPDS} from '../jest/test-pds'
|
||||
import {RootStoreModel, setupState} from '../src/state'
|
||||
|
||||
const WAIT_OPTS = {timeout: 5e3}
|
||||
|
||||
describe('Account flows', () => {
|
||||
let pds: TestPDS | undefined
|
||||
let rootStore: RootStoreModel | undefined
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
pds = await createServer()
|
||||
rootStore = await setupState(pds.pdsUrl)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
await pds?.close()
|
||||
})
|
||||
|
||||
it('renders initial screen', () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
const signUpScreen = getByTestId('signinOrCreateAccount')
|
||||
|
||||
expect(signUpScreen).toBeTruthy()
|
||||
})
|
||||
|
||||
it('completes signin to the server', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('loginForm')).toBeTruthy()
|
||||
|
||||
// input the target server
|
||||
expect(getByTestId('loginSelectServiceButton')).toBeTruthy()
|
||||
fireEvent.press(getByTestId('loginSelectServiceButton'))
|
||||
expect(getByTestId('serverInputModal')).toBeTruthy()
|
||||
fireEvent.changeText(
|
||||
getByTestId('customServerTextInput'),
|
||||
pds?.pdsUrl || '',
|
||||
)
|
||||
fireEvent.press(getByTestId('customServerSelectBtn'))
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('loginUsernameInput')).toBeTruthy()
|
||||
}, WAIT_OPTS)
|
||||
|
||||
// enter username & pass
|
||||
fireEvent.changeText(getByTestId('loginUsernameInput'), 'alice')
|
||||
fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2')
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('loginNextButton')).toBeTruthy()
|
||||
}, WAIT_OPTS)
|
||||
fireEvent.press(getByTestId('loginNextButton'))
|
||||
|
||||
// signed in
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('homeFeed')).toBeTruthy()
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
})
|
||||
|
||||
it('opens the login screen when "add account" is pressed', async () => {
|
||||
const {getByTestId, getAllByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// press '+ new account' in switcher
|
||||
fireEvent.press(getByTestId('switchToNewAccountBtn'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the "choose account" form when a previous session has been created', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('logs directly into the account due to still possessing session tokens', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
|
||||
// select the previous account
|
||||
fireEvent.press(getByTestId('chooseAccountBtn-alice.test'))
|
||||
|
||||
// signs in immediately
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('homeFeed')).toBeTruthy()
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(1)
|
||||
})
|
||||
|
||||
it('logs into a second account via the switcher', async () => {
|
||||
const {getByTestId, getAllByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(() => expect(getByTestId('homeFeed')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// open side menu
|
||||
fireEvent.press(getAllByTestId('viewHeaderBackOrMenuBtn')[0])
|
||||
await waitFor(() => expect(getByTestId('menuView')).toBeTruthy(), WAIT_OPTS)
|
||||
|
||||
// nav to settings
|
||||
fireEvent.press(getByTestId('menuItemButton-Settings'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// press '+ new account' in switcher
|
||||
fireEvent.press(getByTestId('switchToNewAccountBtn'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
|
||||
// select a new account
|
||||
fireEvent.press(getByTestId('chooseNewAccountBtn'))
|
||||
expect(getByTestId('loginForm')).toBeTruthy()
|
||||
|
||||
// input the target server
|
||||
expect(getByTestId('loginSelectServiceButton')).toBeTruthy()
|
||||
fireEvent.press(getByTestId('loginSelectServiceButton'))
|
||||
expect(getByTestId('serverInputModal')).toBeTruthy()
|
||||
fireEvent.changeText(
|
||||
getByTestId('customServerTextInput'),
|
||||
pds?.pdsUrl || '',
|
||||
)
|
||||
fireEvent.press(getByTestId('customServerSelectBtn'))
|
||||
await waitFor(
|
||||
() => expect(getByTestId('loginUsernameInput')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// enter username & pass
|
||||
fireEvent.changeText(getByTestId('loginUsernameInput'), 'bob')
|
||||
fireEvent.changeText(getByTestId('loginPasswordInput'), 'hunter2')
|
||||
await waitFor(
|
||||
() => expect(getByTestId('loginNextButton')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
fireEvent.press(getByTestId('loginNextButton'))
|
||||
|
||||
// signed in
|
||||
await waitFor(() => {
|
||||
expect(getByTestId('settingsScreen')).toBeTruthy() // we go back to settings in this situation
|
||||
expect(rootStore?.me?.displayName).toBe('Bob')
|
||||
expect(rootStore?.me?.handle).toBe('bob.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Bob')
|
||||
expect(rootStore?.me?.handle).toBe('bob.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
})
|
||||
|
||||
it('can instantly switch between accounts', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// select the alice account
|
||||
fireEvent.press(getByTestId('switchToAccountBtn-alice.test'))
|
||||
|
||||
// swapped account
|
||||
await waitFor(() => {
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
}, WAIT_OPTS)
|
||||
expect(rootStore?.me?.displayName).toBe('Alice')
|
||||
expect(rootStore?.me?.handle).toBe('alice.test')
|
||||
expect(rootStore?.session.accounts.length).toBe(2)
|
||||
})
|
||||
|
||||
it('will prompt for a password if you sign out', async () => {
|
||||
const {getByTestId} = render(<MobileShell />, rootStore)
|
||||
await waitFor(
|
||||
() => expect(getByTestId('settingsScreen')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// press the sign out button
|
||||
fireEvent.press(getByTestId('signOutBtn'))
|
||||
|
||||
// in the logged out state
|
||||
await waitFor(
|
||||
() => expect(getByTestId('signinOrCreateAccount')).toBeTruthy(),
|
||||
WAIT_OPTS,
|
||||
)
|
||||
|
||||
// move to signin view
|
||||
fireEvent.press(getByTestId('signInButton'))
|
||||
expect(getByTestId('signIn')).toBeTruthy()
|
||||
expect(getByTestId('chooseAccountForm')).toBeTruthy()
|
||||
|
||||
// select an existing account
|
||||
fireEvent.press(getByTestId('chooseAccountBtn-alice.test'))
|
||||
|
||||
// goes to login screen instead of straight back to settings
|
||||
expect(getByTestId('loginForm')).toBeTruthy()
|
||||
})
|
||||
})
|
|
@ -1,126 +0,0 @@
|
|||
import React from 'react'
|
||||
import {Signin} from '../../../../src/view/com/login/Signin'
|
||||
import {cleanup, fireEvent, render} from '../../../../jest/test-utils'
|
||||
import {SessionServiceClient, sessionClient as AtpApi} from '@atproto/api'
|
||||
import {
|
||||
mockedSessionStore,
|
||||
mockedShellStore,
|
||||
} from '../../../../__mocks__/state-mock'
|
||||
import {Keyboard} from 'react-native'
|
||||
|
||||
describe('Signin', () => {
|
||||
const requestPasswordResetMock = jest.fn()
|
||||
const resetPasswordMock = jest.fn()
|
||||
jest.spyOn(AtpApi, 'service').mockReturnValue({
|
||||
com: {
|
||||
atproto: {
|
||||
account: {
|
||||
requestPasswordReset: requestPasswordResetMock,
|
||||
resetPassword: resetPasswordMock,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as SessionServiceClient)
|
||||
const mockedProps = {
|
||||
onPressBack: jest.fn(),
|
||||
}
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks()
|
||||
cleanup()
|
||||
})
|
||||
|
||||
it('renders logs in form', async () => {
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const loginFormView = await findByTestId('loginFormView')
|
||||
expect(loginFormView).toBeTruthy()
|
||||
|
||||
const loginUsernameInput = await findByTestId('loginUsernameInput')
|
||||
expect(loginUsernameInput).toBeTruthy()
|
||||
|
||||
fireEvent.changeText(loginUsernameInput, 'testusername')
|
||||
|
||||
const loginPasswordInput = await findByTestId('loginPasswordInput')
|
||||
expect(loginPasswordInput).toBeTruthy()
|
||||
|
||||
fireEvent.changeText(loginPasswordInput, 'test pass')
|
||||
|
||||
const loginNextButton = await findByTestId('loginNextButton')
|
||||
expect(loginNextButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(loginNextButton)
|
||||
|
||||
expect(mockedSessionStore.login).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders selects service from login form', async () => {
|
||||
const keyboardSpy = jest.spyOn(Keyboard, 'dismiss')
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const loginSelectServiceButton = await findByTestId(
|
||||
'loginSelectServiceButton',
|
||||
)
|
||||
expect(loginSelectServiceButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(loginSelectServiceButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
expect(keyboardSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders new password form', async () => {
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const forgotPasswordButton = await findByTestId('forgotPasswordButton')
|
||||
expect(forgotPasswordButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(forgotPasswordButton)
|
||||
const forgotPasswordView = await findByTestId('forgotPasswordView')
|
||||
expect(forgotPasswordView).toBeTruthy()
|
||||
|
||||
const forgotPasswordEmail = await findByTestId('forgotPasswordEmail')
|
||||
expect(forgotPasswordEmail).toBeTruthy()
|
||||
fireEvent.changeText(forgotPasswordEmail, 'test@email.com')
|
||||
|
||||
const newPasswordButton = await findByTestId('newPasswordButton')
|
||||
expect(newPasswordButton).toBeTruthy()
|
||||
fireEvent.press(newPasswordButton)
|
||||
|
||||
expect(requestPasswordResetMock).toHaveBeenCalled()
|
||||
|
||||
const newPasswordView = await findByTestId('newPasswordView')
|
||||
expect(newPasswordView).toBeTruthy()
|
||||
|
||||
const newPasswordInput = await findByTestId('newPasswordInput')
|
||||
expect(newPasswordInput).toBeTruthy()
|
||||
const resetCodeInput = await findByTestId('resetCodeInput')
|
||||
expect(resetCodeInput).toBeTruthy()
|
||||
|
||||
fireEvent.changeText(newPasswordInput, 'test pass')
|
||||
fireEvent.changeText(resetCodeInput, 'test reset code')
|
||||
|
||||
const setNewPasswordButton = await findByTestId('setNewPasswordButton')
|
||||
expect(setNewPasswordButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(setNewPasswordButton)
|
||||
|
||||
expect(resetPasswordMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('renders forgot password form', async () => {
|
||||
const {findByTestId} = render(<Signin {...mockedProps} />)
|
||||
|
||||
const forgotPasswordButton = await findByTestId('forgotPasswordButton')
|
||||
expect(forgotPasswordButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(forgotPasswordButton)
|
||||
const forgotPasswordSelectServiceButton = await findByTestId(
|
||||
'forgotPasswordSelectServiceButton',
|
||||
)
|
||||
expect(forgotPasswordSelectServiceButton).toBeTruthy()
|
||||
|
||||
fireEvent.press(forgotPasswordSelectServiceButton)
|
||||
|
||||
expect(mockedShellStore.openModal).toHaveBeenCalled()
|
||||
})
|
||||
})
|
|
@ -46,10 +46,10 @@ describe('Menu', () => {
|
|||
})
|
||||
|
||||
it("presses notifications menu item' button", () => {
|
||||
const {getAllByTestId} = render(<Menu {...mockedProps} />)
|
||||
const {getByTestId} = render(<Menu {...mockedProps} />)
|
||||
|
||||
const menuItemButton = getAllByTestId('menuItemButton')
|
||||
fireEvent.press(menuItemButton[1])
|
||||
const menuItemButton = getByTestId('menuItemButton-Notifications')
|
||||
fireEvent.press(menuItemButton)
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalled()
|
||||
expect(mockedNavigationStore.switchTo).toHaveBeenCalledWith(1, true)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue