Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions shared/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import tseslint from 'typescript-eslint'

const ignores = [
'**/*.d.ts',
'android/app/build/**',
'babel.config.js',
'common-adapters/icon.constants-gen.desktop.tsx',
'common-adapters/icon.constants-gen.native.tsx',
Expand Down
3 changes: 2 additions & 1 deletion shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@
"test:e2e:ios:ipad": "bash tests/e2e/run-ios-appium.sh iPadTest",
"test:e2e:ios:iphone-old": "bash tests/e2e/run-ios-appium.sh iPhoneTestOld",
"test:e2e:ios:ipad-old": "bash tests/e2e/run-ios-appium.sh iPadTestOld",
"test:e2e:ios:lifecycle": "bash tests/e2e/run-ios-lifecycle.sh iPhoneTest",
"test:e2e:ios:report": "node tests/e2e/generate-appium-report.mts && open tests/results/ios-appium-report.html",
"test:e2e:android": "bash tests/e2e/run-android-appium.sh",
"test:e2e:android:report": "node tests/e2e/generate-appium-report.mts android && open tests/results/android-appium-report.html",
"test:unit": "napi-postinstall unrs-resolver 1.11.1 check; jest --runInBand",
"test:unit:ios": "xcodebuild test -project ./ios/Keybase.xcodeproj -scheme 'Keybase For Test' -destination 'platform=iOS Simulator,name=iPhone 6s,OS=9.3'",
"tsc": "./node_modules/typescript-native/bin/tsc --project ./tsconfig.desktop.json && ./node_modules/typescript-native/bin/tsc --project ./tsconfig.native.json"
"tsc": "./node_modules/typescript-native/bin/tsc --project ./tsconfig.desktop.json && ./node_modules/typescript-native/bin/tsc --project ./tsconfig.native.json && ./node_modules/typescript-native/bin/tsc --project ./tests/e2e/ios-appium/tsconfig.json"
},
"keywords": [],
"author": "",
Expand Down
184 changes: 184 additions & 0 deletions shared/tests/e2e/ios-appium/flows/lifecycle-app-state.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import {expect} from '@wdio/globals'
import {requireSmokeUser} from '../helpers/app'
import {byText, tab} from '../helpers/elements'
import {
activateApp,
appPid,
appSnapshot,
backgroundApp,
closeNotificationCenter,
crashReportsSince,
findLines,
goAppStateUpdates,
goLogMark,
goLogSince,
launchApp,
metroClientLogSince,
metroLogMark,
openNotificationCenter,
openSelfConversation,
startSenderDevice,
terminateApp,
waitFor,
waitForAppState,
waitForAvatar200,
waitForLinesInOrder,
} from '../helpers/lifecycle'

// Log lines these flows rely on:
// - Go (ios.log): "lifecycle: <uiActive|uiInactive|uiBackground|…>: …" per native UI report,
// "MobileAppState.Update: useful update: <STATE>" per Go app state change,
// "Srv: start: addr: <address>" when the image server starts on a new address,
// "kbhttp.Srv: server starting on: <address>" on every start of a Go http server.
// - Metro (JS): "app focus changed: <state>" when the shell store's app state changes.
// The cold launch test requires a match of each server line, so an empty result in the
// Notification Center test means no restart, not a pattern that no longer matches Go's log.
const httpSrvStarted = /Srv: start: addr: /
const httpSrvStartedAny = /kbhttp\.Srv: server starting on: /
describe('app lifecycle: app state', () => {
it('cold launch reaches active under scenes and serves images', async () => {
const user = requireSmokeUser()
const since = Date.now()
await terminateApp()
const goMark = goLogMark()
await launchApp()

// The app restores its last screen, which may hide the tab bar, so wait on state.
const snap = await waitForAppState('active', undefined, 90000)
const goLines = await waitForLinesInOrder('Go to report the launch', () => goLogSince(goMark), [
/lifecycle: uiInactive: /,
/MobileAppState\.Update: useful update: FOREGROUND/,
/lifecycle: uiActive: /,
])
expect(goLines).toHaveLength(3)
// JS must hold the address of the server Go started, not a stale one.
const started = findLines(goLogSince(goMark), httpSrvStarted).at(-1) ?? ''
expect(started).toContain(`addr: ${snap.httpSrv.address} `)
expect(findLines(goLogSince(goMark), httpSrvStartedAny).length).toBeGreaterThanOrEqual(1)

const avatar = await waitForAvatar200(user)
expect(avatar.status).toBe(200)

// The UIScene SIGTRAP crashed within a second of launch; give it several.
const pid = appPid()
await browser.pause(5000)
expect(appPid()).toBe(pid)
expect(crashReportsSince(since)).toEqual([])
})

it('background, then foreground: images load and chat receives a new message', async () => {
const user = requireSmokeUser()
const since = Date.now()
const convID = await openSelfConversation(user)
const sender = await startSenderDevice(user)
try {
const pid = appPid()
const goMark = goLogMark()
await backgroundApp()
await waitForLinesInOrder('Go to go to the background', () => goLogSince(goMark), [
/lifecycle: uiInactive: /,
/lifecycle: uiBackground: /,
])

const text = `e2e-lifecycle-recv-${Date.now()}`
await sender.send(convID, text)
await browser.pause(10000)
await activateApp()

const snap = await waitForAppState('active')
expect(snap.screen?.params?.['conversationIDKey']).toBe(convID)
await waitForLinesInOrder('Go to return to the foreground', () => goLogSince(goMark), [
/lifecycle: uiBackground: /,
/lifecycle: uiInactive: /,
/MobileAppState\.Update: useful update: FOREGROUND/,
/lifecycle: uiActive: /,
])
await waitForAvatar200(user)

// The message sent from the other device while this one was in the background.
await byText(text).waitForExist({interval: 250, timeout: 60000, timeoutMsg: `"${text}" never arrived`})
expect(appPid()).toBe(pid)
expect(crashReportsSince(since)).toEqual([])
} finally {
// A cleanup failure must not hide the test's own failure.
await sender.stop().catch((e: unknown) => {
// eslint-disable-next-line no-console
console.warn(`sender device cleanup failed: ${e instanceof Error ? e.message : String(e)}`)
})
}
})

it('five quick background/foreground cycles leave the app healthy', async () => {
const user = requireSmokeUser()
const since = Date.now()
await waitForAppState('active')
const pid = appPid()
const goMark = goLogMark()
const metroMark = metroLogMark()

const cycles = 5
for (let i = 0; i < cycles; i++) {
await backgroundApp()
await browser.pause(700)
await activateApp()
await browser.pause(700)
}

await waitForAppState('active')
expect(appPid()).toBe(pid)
// Every cycle reaches Go, and the last word is foreground.
await waitFor(
'Go to see every cycle',
() => {
const lines = goLogSince(goMark)
const backgrounds = findLines(lines, /lifecycle: uiBackground: /).length
const actives = findLines(lines, /lifecycle: uiActive: /).length
return backgrounds >= cycles && actives >= cycles && goAppStateUpdates(goMark).at(-1) === 'FOREGROUND'
? true
: undefined
},
{interval: 500, timeout: 20000}
)
// JS saw the app go away and come back, ending active.
const focus = findLines(metroClientLogSince(metroMark), /app focus changed: /)
expect(focus.some(l => l.endsWith('app focus changed: background'))).toBe(true)
expect(focus.at(-1)).toMatch(/app focus changed: active$/)

const avatar = await waitForAvatar200(user)
expect(avatar.status).toBe(200)
await expect(tab('People')).toExist()
expect(crashReportsSince(since)).toEqual([])
})

it('Notification Center makes the app inactive and keeps images served', async () => {
const user = requireSmokeUser()
const before = await waitForAppState('active')
const goMark = goLogMark()
const metroMark = metroLogMark()

await openNotificationCenter()
const inactive = await waitForAppState('inactive', undefined, 15000)
await waitForLinesInOrder('Go to go inactive', () => goLogSince(goMark), [
/MobileAppState\.Update: useful update: INACTIVE/,
/lifecycle: uiInactive: /,
])
// INACTIVE is not background: the image server keeps serving at the same address.
expect(inactive.httpSrv.address).toBe(before.httpSrv.address)
await waitForAvatar200(user)
expect(findLines(goLogSince(goMark), /lifecycle: uiBackground: /)).toEqual([])

await closeNotificationCenter()
await waitForAppState('active', undefined, 15000)
await waitForLinesInOrder('Go to become active again', () => goLogSince(goMark), [
/MobileAppState\.Update: useful update: FOREGROUND/,
/lifecycle: uiActive: /,
])
const focus = findLines(metroClientLogSince(metroMark), /app focus changed: /)
expect(focus).toEqual([
expect.stringMatching(/app focus changed: inactive$/),
expect.stringMatching(/app focus changed: active$/),
])
expect(findLines(goLogSince(goMark), httpSrvStartedAny)).toEqual([])
expect((await appSnapshot()).httpSrv.address).toBe(before.httpSrv.address)
})
})
Loading