mirror of
https://github.com/mattermost/mattermost-mobile.git
synced 2026-11-04 09:58:57 +00:00
Closes manual release-test gaps for mobile (Detox + Maestro), automating new test cases and hardening flaky/CI-only failures uncovered along the way. ### New coverage automated - **MM-T416** offline/online post-retry (Android via airplane mode; iOS ultimately marked unsupported — see below) - **MM-T1194 / MM-T1193** custom ToS accept/decline survives relaunch - **MM-T3251** status change via account UI (cross-user visibility) - **MM-T510** channel-wide/keyword mention delivery — later expanded to a full 5-channel mention matrix with exact server-side counts - **MM-T3261 (_1/_2)** Report a Problem external hand-off (browser link mode, email mode) - **MM-T2940** iOS managed-config (MDM) pre-fill/autoconnect - **MM-T3271/3272** opening the app from a push notification (synthetic notification payloads, background/closed states) - Un-skipped several tests that turned out to already pass (stale skips) ### Root-caused failing tests - **Android SQLite corruption**: traced to native Kotlin DB connections not opening in WAL mode while JSI did (post #10109) — journal-mode mismatch corrupted the store on push-triggered writes. Fixed properly with a **WAL patch** (#10117) rather than the original destructive "wipe app database" workaround, which was reverted. - **Fabric crash** (`addViewAt`/reparent) on `SlideUpPanelItem` taps — root-caused to view flattening racing sheet mounts; fixed with `collapsable={false}` (went through a revert/re-revert once CI reproduced the crash without it). - **Search race**: stale in-flight search results could overwrite a cleared search box — fixed with request-id tagging. - **iOS offline detection**: multiple iterations (AAAA sampling → Cloudflare-prefix check → final decision) landed on refusing iOS offline simulation outright, since all E2E servers are Cloudflare-fronted/anycast and the address space can't be enumerated/blocked. - **MM-T3261 config patch** intermittently silently ignored by the server — root-caused to `ReportAProblemType` being environment-managed on PR Spinwicks (config PATCH returns 200 but is silently dropped); now detected and reported as a skip with proven cause, not a failure. - **MM-T416 failed-post timing**: multiple rounds of tightening the wait bound to match the client's actual retry/backoff/timeout ceiling (ended at ~150s, derived from OkHttp/retry-interceptor math, not guessed). - **MM-T5603 upload wait**: raised to match the client's real 180s upload timeout. - **Classification banner suite**: fixed to verify config state directly instead of trusting a PATCH reply that the cloud sometimes drops. ### CI/reporting infra fixes - Dedicated Maestro steps (MM-T3261, MM-T67856_4) weren't feeding into the merged JUnit report and couldn't gate pass/fail — fixed report merging/naming so they now count. - Android Maestro logcat capture added (previously iOS-only diagnostics). - Jest retry logic no longer discards passing results from a shard killed by timeout. - Sharding raised 24→28. - Removed dead Detox Pilot/Playbooks scaffolding (unused, uncovered by CI). ### Cleanup - Comment/docs pass stripping narrative session-archaeology from long explanatory comments, keeping only load-bearing facts and CI references.
605 lines
28 KiB
Markdown
605 lines
28 KiB
Markdown
# Detox E2E — AI Agent Operating Manual
|
||
|
||
This file is the authoritative guide for AI agents working in the `detox/` directory.
|
||
Follow these rules exactly. Do not improvise on conventions that are explicitly defined here.
|
||
|
||
> **Working on Maestro flows?** See `maestro/GUIDELINES.md` for the authoring spec and flow gotchas.
|
||
> (toggle-tap pattern, modal back-nav on iOS 26.3, server preference persistence,
|
||
> `AllowDownloadLogs` path under `SupportSettings`, etc.) and per-flow server-setting
|
||
> requirements. Server provisioning lives in `detox/provision/` (run via `npm run provision` in the detox package) and is
|
||
> shared between Detox and Maestro.
|
||
|
||
---
|
||
|
||
## AGENT SAFETY RULES
|
||
|
||
**Before modifying any file:**
|
||
1. Read the file first — never edit based on assumptions
|
||
2. Verify testIDs exist in the app source before referencing them in page objects
|
||
3. Never hardcode strings in `by.id()` — always use page object testID constants
|
||
4. Never share mutable state between `it()` blocks
|
||
5. Never modify files outside `detox/` without explicit user instruction
|
||
|
||
**Safe operations (proceed without asking):**
|
||
- Adding new test files under `e2e/test/products/`
|
||
- Adding new page objects under `e2e/support/ui/`
|
||
- Adding new server API helpers under `e2e/support/server_api/`
|
||
- Reading any file to understand patterns
|
||
|
||
**Operations that require user confirmation:**
|
||
- Modifying `.detoxrc.json` (affects all test runs)
|
||
- Modifying `e2e/config.js` (affects Jest configuration)
|
||
- Adding new npm dependencies to `detox/package.json`
|
||
- Modifying `create_android_emulator.sh`
|
||
|
||
**Verification steps before calling a task complete:**
|
||
- [ ] TypeScript compiles: `cd detox && npm run tsc`
|
||
- [ ] Lint passes: `cd detox && npm run lint`
|
||
- [ ] New page objects are exported from `index.ts` in their directory
|
||
- [ ] New server API helpers are exported from `e2e/support/server_api/index.ts`
|
||
|
||
---
|
||
|
||
## INVESTIGATION PHASE (start here for any task)
|
||
|
||
Before writing any code, answer these questions by reading the relevant files:
|
||
|
||
1. **Does a page object already exist for the screen?** → check `e2e/support/ui/screen/`
|
||
2. **Does a component object already exist?** → check `e2e/support/ui/component/`
|
||
3. **Does a server API helper already exist?** → check `e2e/support/server_api/`
|
||
4. **Does the testID exist in the app?** → search `app/` with `grep -r "testID=" --include="*.tsx"`
|
||
5. **Is there an existing test in the same area to follow as a pattern?** → check the relevant folder under `e2e/test/products/channels/`
|
||
|
||
---
|
||
|
||
## PACKAGE & COMMANDS
|
||
|
||
`detox/` is a **separate npm package** with its own `node_modules`. Always `cd detox` before running commands.
|
||
|
||
```bash
|
||
# Install dependencies
|
||
cd detox && npm install
|
||
|
||
# Type check
|
||
cd detox && npm run tsc
|
||
|
||
# Lint
|
||
cd detox && npm run lint
|
||
|
||
# Fix lint issues
|
||
cd detox && npm run check # runs both lint and tsc
|
||
|
||
# Build Android APK for tests
|
||
cd detox && npm run e2e:android-build
|
||
|
||
# Run Android tests
|
||
cd detox && npm run e2e:android-test
|
||
|
||
# Run iOS tests (requires pre-built app in ../mobile-artifacts/Mattermost.app)
|
||
cd detox && npm run e2e:ios-test
|
||
|
||
# Run a single test file
|
||
npx detox test -c ios.sim.debug e2e/test/products/channels/search/search_messages.e2e.ts
|
||
npx detox test -c android.emu.debug e2e/test/products/channels/search/search_messages.e2e.ts
|
||
|
||
# Generate report (CI use)
|
||
cd detox && npm run e2e:save-report
|
||
```
|
||
|
||
---
|
||
|
||
## CI PIPELINE INTEGRATION
|
||
|
||
### Trigger Tiers
|
||
|
||
| Tier | Trigger | Platform | Shards | Search Path | Approx Time |
|
||
|------|---------|----------|--------|-------------|-------------|
|
||
| **PR full** | Matterwick + `E2E/Run` label | Detox iOS/Android/iPad + Maestro | 20 Detox (iOS/Android), 1 iPad, 1 Maestro each | `detox/e2e/test` (full) | ~30–45+ min wall-clock |
|
||
| **Main** | Matterwick main push (`run_type=MASTER` today; `MAIN` also accepted → TSIO `mobile-main`) | Same as PR | Same as PR | `detox/e2e/test` | Same as PR |
|
||
| **CMT / Release** | Matterwick on `build-release-*` → CMT | Detox + Maestro across server versions | Full suite on latest server; smoke subset on older | latest: `detox/e2e/test`; older: `…/smoke_test` | Varies by matrix |
|
||
|
||
Status contexts live under the `e2e-test/` namespace, matching the mattermost monorepo. PR/Main: `e2e-test/detox-ios`, `e2e-test/detox-android`, `e2e-test/detox-ipad`, `e2e-test/maestro-ios`, `e2e-test/maestro-android`. CMT: per-shard `e2e-test/<tsio-shard-name>` plus umbrella `e2e-test/compatibility-matrix-testing`. TSIO groups: `mobile-pr-<job>` / `mobile-main-<job>` / `mobile-release-<shard>`.
|
||
|
||
Matterwick provisions five servers for every mobile server-version entry: two Android-only, two iOS-only, and one shared third site. CMT therefore uses `5 × server version count` installations (up to 25 at the five-version cap). The full latest-version suite needs this isolation for its parallel shards; older-version smoke jobs intentionally retain the same topology for consistent URL semantics, even though their single shard uses less of its capacity. iPad shares the iOS pair, and Maestro uses the first server for its platform.
|
||
|
||
### Smoke Tests Location
|
||
|
||
`detox/e2e/test/products/channels/smoke_test/` — quick regression suite used as the **CMT older-server subset**, not as an automatic every-PR-push tier.
|
||
PR E2E runs the full `detox/e2e/test` tree when labeled.
|
||
|
||
### Workflow Files
|
||
|
||
| File | Purpose |
|
||
|------|---------|
|
||
| `.github/workflows/e2e-detox-pr.yml` | Matterwick entry: builds, Detox + Maestro dispatch, TSIO status |
|
||
| `.github/workflows/e2e-detox.yml` / `e2e-maestro-pr.yml` | Platform orchestration (reusable) |
|
||
| `.github/workflows/e2e-ios-template.yml` | Detox iOS shard runner |
|
||
| `.github/workflows/e2e-android-template.yml` | Detox Android shard runner |
|
||
| `.github/workflows/e2e-maestro-template.yml` | Maestro iOS/Android runner |
|
||
| `.github/workflows/compatibility-matrix-testing.yml` | CMT / release multi-server matrix |
|
||
|
||
### Checks for CI PRs
|
||
|
||
Before opening a PR that touches `.github/workflows/`, `.github/actions/`, or `detox/utils/`:
|
||
|
||
```bash
|
||
actionlint -shellcheck= .github/workflows/*.yml
|
||
cd detox && npm run check
|
||
```
|
||
|
||
These failure modes stay invisible in an otherwise-green run, so they are worth
|
||
checking by hand:
|
||
|
||
| Failure mode | Why it hides |
|
||
|--------------|--------------|
|
||
| A `secrets.FOO` not declared in the callee's `on.workflow_call.secrets` | GitHub resolves it to an empty string, so the step passes and the notify/upload it feeds silently never happens. `actionlint` catches it. |
|
||
| `node foo.js \` followed by `RC=$?` | The trailing backslash makes `RC=$?` an *argument*, so the exit code is never captured and the failure gate below always passes. |
|
||
| A job running `tsio-report-status.js` / `tsio-channel-notify-rollup.js` without `permissions.id-token: write` | `mintOidcToken()` warns and exits 0, skipping the commit status or channel rollup. |
|
||
|
||
Reusable-workflow secrets must be **declared in the callee and forwarded by every
|
||
caller** that uses an explicit `secrets:` block. Callers using `secrets: inherit`
|
||
pass everything automatically — which is why a gap can work on one path (Maestro PR)
|
||
and silently no-op on another (Detox PR, CMT).
|
||
|
||
Optional AI review of local changes before pushing, if the CLI is installed and logged in:
|
||
|
||
```bash
|
||
coderabbit --plain --type uncommitted
|
||
```
|
||
|
||
### Detox Configuration (`.detoxrc.json`)
|
||
|
||
| Setting | Value | Notes |
|
||
|---------|-------|-------|
|
||
| iOS device | iPhone 17 Pro / iOS 26.3 | Simulator |
|
||
| Android device | `detox_pixel_8_api_35` AVD | Emulator |
|
||
| `reinstallApp` | `false` | App not reinstalled between tests |
|
||
| `launchApp` | `false` | Tests manually launch app |
|
||
| `shutdownDevice` | `false` | Emulator/simulator stays up after suite |
|
||
| `debugSynchronization` | 20000ms | Detox sync debug timeout |
|
||
| Screenshots | Only on failure | Kept at `detox/artifacts/` |
|
||
| Video | Disabled | |
|
||
|
||
---
|
||
|
||
## FOLDER STRUCTURE
|
||
|
||
```text
|
||
detox/
|
||
├── .detoxrc.json # Detox device/app/config definitions
|
||
├── package.json # Separate npm package (own node_modules)
|
||
├── create_android_emulator.sh # Creates and boots Android AVD for CI
|
||
├── e2e/
|
||
│ ├── config.js # Jest config entry point for Detox
|
||
│ ├── path_builder.js # Artifact paths (screenshots/logs/videos)
|
||
│ ├── support/
|
||
│ │ ├── test_config.ts # URLs + admin credentials from env vars
|
||
│ │ ├── utils/
|
||
│ │ │ ├── index.ts # timeouts, wait(), getRandomId(), isIos(), isAndroid()
|
||
│ │ │ ├── detoxhelpers.ts # waitForElementToBeVisible(), retryWithReload()
|
||
│ │ │ └── email.ts # Email verification helpers
|
||
│ │ ├── server_api/ # REST API client for test data setup/teardown
|
||
│ │ │ ├── client.ts # Axios client with CSRF cookie handling
|
||
│ │ │ ├── setup.ts # apiInit() — creates team + channel + user
|
||
│ │ │ ├── channel.ts # apiCreateChannel, apiAddUserToChannel, apiDeleteChannel
|
||
│ │ │ ├── post.ts # apiCreatePost, apiGetLastPostInChannel
|
||
│ │ │ ├── user.ts # apiCreateUser, apiLogin, generateRandomUser
|
||
│ │ │ ├── team.ts # apiCreateTeam, apiAddUserToTeam
|
||
│ │ │ ├── system.ts # apiGetConfig, apiUpdateConfig
|
||
│ │ │ ├── preference.ts # apiSaveUserPreference
|
||
│ │ │ ├── bot.ts # apiCreateBot
|
||
│ │ │ ├── plugin.ts # apiInstallPlugin
|
||
│ │ │ └── index.ts # Re-exports all: {Channel, Post, Setup, Team, User, ...}
|
||
│ │ └── ui/
|
||
│ │ ├── component/ # Reusable UI component page objects
|
||
│ │ │ ├── autocomplete.ts
|
||
│ │ │ ├── navigation_header.ts
|
||
│ │ │ ├── post_draft.ts
|
||
│ │ │ ├── post_list.ts
|
||
│ │ │ ├── post.ts
|
||
│ │ │ ├── search_bar.ts
|
||
│ │ │ └── index.ts # Re-exports all components
|
||
│ │ └── screen/ # Screen page objects
|
||
│ │ ├── channel_list.ts
|
||
│ │ ├── channel.ts
|
||
│ │ ├── search_messages.ts
|
||
│ │ ├── login.ts
|
||
│ │ ├── server.ts
|
||
│ │ ├── home.ts
|
||
│ │ └── index.ts # Re-exports all screens
|
||
│ └── test/
|
||
│ └── products/
|
||
│ ├── channels/ # ~100 test files
|
||
│ │ ├── autocomplete/ # 8 files
|
||
│ │ ├── search/ # 5 files
|
||
│ │ ├── messaging/ # 24 files
|
||
│ │ ├── channels/ # 16 files
|
||
│ │ ├── account/ # 15 files
|
||
│ │ ├── threads/ # 6 files
|
||
│ │ └── smoke_test/ # quick suite (CMT older-server subset)
|
||
│ └── agents/ # AI agent product tests
|
||
```
|
||
|
||
---
|
||
|
||
## PAGE OBJECT MODEL
|
||
|
||
### Screen Page Object — Required Structure
|
||
|
||
```typescript
|
||
class SomeScreen {
|
||
// 1. testID constants — single source of truth
|
||
testID = {
|
||
someScreen: 'some_screen.screen',
|
||
someButton: 'some_screen.some_button',
|
||
};
|
||
|
||
// 2. Element definitions
|
||
someScreen = element(by.id(this.testID.someScreen));
|
||
someButton = element(by.id(this.testID.someButton));
|
||
|
||
// 3. Dynamic getters for list items
|
||
getItem = (itemId: string) => element(by.id(`some_screen.item.${itemId}`));
|
||
|
||
// 4. toBeVisible() — required, waits for screen to appear
|
||
toBeVisible = async () => {
|
||
await waitFor(this.someScreen).toExist().withTimeout(timeouts.TEN_SEC);
|
||
return this.someScreen;
|
||
};
|
||
|
||
// 5. open() — navigates to this screen
|
||
open = async () => {
|
||
await HomeScreen.someTab.tap();
|
||
return this.toBeVisible();
|
||
};
|
||
}
|
||
|
||
const someScreen = new SomeScreen();
|
||
export default someScreen;
|
||
```
|
||
|
||
### testID Convention
|
||
|
||
Hierarchical dot notation: `scope.feature.element`
|
||
|
||
| Example testID | Meaning |
|
||
|---------------|---------|
|
||
| `search_messages.screen` | Screen container |
|
||
| `search.modifier.in` | "in:" modifier button |
|
||
| `autocomplete.channel_mention.section_list` | Channel mention dropdown |
|
||
| `autocomplete.channel_mention_item.{channelName}` | Individual channel item |
|
||
| `autocomplete.at_mention_item.{userId}` | Individual user mention item |
|
||
| `channel_list.category.channels.channel_item.{channelName}` | Channel in sidebar |
|
||
|
||
**Finding testIDs:** Search the app source with `grep -r 'testID=' app/ --include="*.tsx" | grep "your_keyword"`
|
||
|
||
### Key Screen Page Objects
|
||
|
||
| Export | File | Purpose |
|
||
|--------|------|---------|
|
||
| `SearchMessagesScreen` | `screen/search_messages.ts` | Search screen, modifiers, results |
|
||
| `ChannelScreen` | `screen/channel.ts` | Channel view + post input |
|
||
| `ChannelListScreen` | `screen/channel_list.ts` | Sidebar channel list + navigation |
|
||
| `HomeScreen` | `screen/home.ts` | Tab bar, logout |
|
||
| `LoginScreen` | `screen/login.ts` | Login form |
|
||
| `ServerScreen` | `screen/server.ts` | Server connection screen |
|
||
|
||
### Key Component Page Objects
|
||
|
||
| Export | File | Purpose |
|
||
|--------|------|---------|
|
||
| `Autocomplete` | `component/autocomplete.ts` | All autocomplete dropdown types |
|
||
| `NavigationHeader` | `component/navigation_header.ts` | `searchInput`, `searchClearButton` |
|
||
| `PostDraft` | `component/post_draft.ts` | Post input, send button |
|
||
| `PostList` | `component/post_list.ts` | Generic post list (reused across screens) |
|
||
|
||
### Autocomplete Component Usage
|
||
|
||
```typescript
|
||
await Autocomplete.toBeVisible(); // assert visible
|
||
await Autocomplete.toBeVisible(false); // assert NOT visible
|
||
|
||
// Channel mentions (triggered by "~" or "in:")
|
||
await expect(Autocomplete.sectionChannelMentionList).toExist();
|
||
const {channelMentionItem} = Autocomplete.getChannelMentionItem(channelName);
|
||
|
||
// At-mentions (triggered by "@" or "from:")
|
||
await expect(Autocomplete.sectionAtMentionList).toExist();
|
||
const {atMentionItem} = Autocomplete.getAtMentionItem(userId);
|
||
```
|
||
|
||
### SearchMessagesScreen Modifiers
|
||
|
||
```typescript
|
||
SearchMessagesScreen.searchModifierIn // inserts "in:" → channel mention autocomplete
|
||
SearchMessagesScreen.searchModifierFrom // inserts "from:" → @mention autocomplete
|
||
SearchMessagesScreen.searchModifierBefore // inserts "before:" (date suggestion NOT YET IMPLEMENTED)
|
||
SearchMessagesScreen.searchModifierAfter // inserts "after:" (date suggestion NOT YET IMPLEMENTED)
|
||
SearchMessagesScreen.searchModifierOn // inserts "on:" (date suggestion NOT YET IMPLEMENTED)
|
||
SearchMessagesScreen.searchModifierExclude // inserts "-" prefix
|
||
SearchMessagesScreen.searchModifierPhrases // inserts quotes
|
||
SearchMessagesScreen.searchInput // the text input field
|
||
SearchMessagesScreen.searchClearButton // clears input
|
||
```
|
||
|
||
---
|
||
|
||
## TEST FILE STRUCTURE
|
||
|
||
### Standard Template
|
||
|
||
```typescript
|
||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||
// See LICENSE.txt for license information.
|
||
|
||
// *******************************************************************
|
||
// - [#] indicates a test step (e.g. # Go to a screen)
|
||
// - [*] indicates an assertion (e.g. * Check the title)
|
||
// - Use element testID when selecting an element. Create one if none.
|
||
// *******************************************************************
|
||
|
||
import {Setup} from '@support/server_api';
|
||
import {serverOneUrl, siteOneUrl} from '@support/test_config';
|
||
import {Autocomplete} from '@support/ui/component';
|
||
import {
|
||
ChannelListScreen,
|
||
HomeScreen,
|
||
LoginScreen,
|
||
SearchMessagesScreen,
|
||
ServerScreen,
|
||
} from '@support/ui/screen';
|
||
import {timeouts} from '@support/utils';
|
||
import {expect} from 'detox';
|
||
|
||
describe('Feature Area - Feature Name', () => {
|
||
const serverOneDisplayName = 'Server 1';
|
||
let testChannel: any;
|
||
let testUser: any;
|
||
|
||
beforeAll(async () => {
|
||
const {channel, user} = await Setup.apiInit(siteOneUrl);
|
||
testChannel = channel;
|
||
testUser = user;
|
||
|
||
await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName);
|
||
await LoginScreen.login(testUser);
|
||
});
|
||
|
||
beforeEach(async () => {
|
||
await ChannelListScreen.toBeVisible();
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await HomeScreen.logout();
|
||
});
|
||
|
||
it('MM-TXXXX_1 - should do something', async () => {
|
||
// # Navigate to search
|
||
await SearchMessagesScreen.open();
|
||
|
||
// * Verify search input is visible
|
||
await expect(SearchMessagesScreen.searchInput).toBeVisible();
|
||
|
||
// # Return to channel list for next test
|
||
await ChannelListScreen.open();
|
||
});
|
||
});
|
||
```
|
||
|
||
### Comment Conventions
|
||
|
||
- `// #` — test step / action
|
||
- `// *` — assertion / expected result
|
||
|
||
### Test ID Naming
|
||
|
||
`MM-TXXXX_N` where `XXXX` is the Jira/Zephyr ticket number and `N` is the sub-test index.
|
||
|
||
---
|
||
|
||
## SERVER API
|
||
|
||
### One-liner Init (use for most tests)
|
||
|
||
```typescript
|
||
const {channel, team, user} = await Setup.apiInit(siteOneUrl);
|
||
// Creates: one team, one public channel, one user (added to both)
|
||
```
|
||
|
||
### Custom Init
|
||
|
||
```typescript
|
||
import {Channel, Team, User, Post} from '@support/server_api';
|
||
|
||
const {team} = await Team.apiCreateTeam(siteOneUrl, {prefix: 'my-test'});
|
||
const {channel} = await Channel.apiCreateChannel(siteOneUrl, {
|
||
teamId: team.id,
|
||
type: 'O', // 'O' = public, 'P' = private
|
||
prefix: 'autocomplete',
|
||
});
|
||
const {user} = await User.apiCreateUser(siteOneUrl);
|
||
await Team.apiAddUserToTeam(siteOneUrl, user.id, team.id);
|
||
await Channel.apiAddUserToChannel(siteOneUrl, user.id, channel.id);
|
||
await Post.apiCreatePost(siteOneUrl, {channelId: channel.id, message: 'Hello'});
|
||
```
|
||
|
||
### URL Constants
|
||
|
||
From `@support/test_config`:
|
||
- `siteOneUrl` → `http://localhost:8065` — used for all API calls
|
||
- `serverOneUrl` → device-aware URL — used for `ServerScreen.connectToServer()` (Android uses `10.0.2.2`)
|
||
|
||
---
|
||
|
||
## HELPERS & UTILITIES
|
||
|
||
```typescript
|
||
import {timeouts, wait, getRandomId, isIos, isAndroid} from '@support/utils';
|
||
import {waitForElementToBeVisible} from '@support/utils/detoxhelpers';
|
||
|
||
timeouts.HALF_SEC // 500ms
|
||
timeouts.ONE_SEC // 1000ms
|
||
timeouts.TWO_SEC // 2000ms
|
||
timeouts.TEN_SEC // 10000ms
|
||
timeouts.ONE_MIN // 60000ms
|
||
|
||
await wait(timeouts.ONE_SEC); // simple delay
|
||
const id = getRandomId(); // 6-char alphanumeric unique ID
|
||
if (isIos()) { ... }
|
||
if (isAndroid()) { ... }
|
||
|
||
// Wait without requiring Detox bridge idle (good for animations)
|
||
await waitForElementToBeVisible(element(by.id('...')), timeouts.TEN_SEC);
|
||
```
|
||
|
||
---
|
||
|
||
## TEST COVERAGE MAP
|
||
|
||
### Smoke (`e2e/test/products/channels/smoke_test/`) — CMT older-server subset
|
||
|
||
Quick regression of core flows. Used when CMT runs against non-latest server versions; PR E2E uses the full suite.
|
||
|
||
### Search (`e2e/test/products/channels/search/`)
|
||
|
||
| File | Key Coverage |
|
||
|------|-------------|
|
||
| `search_messages.e2e.ts` | Elements, from:, in:, exclude, phrases, modifiers, recent, cross-team, empty state, edit/reply/delete, save, pin |
|
||
| `cross_team_search.e2e.ts` | Team switching in search results |
|
||
| `pinned_messages.e2e.ts` | View/manage pinned posts |
|
||
| `recent_mentions.e2e.ts` | @mention results in search |
|
||
| `saved_messages.e2e.ts` | Saved/bookmarked posts |
|
||
|
||
### Autocomplete (`e2e/test/products/channels/autocomplete/`)
|
||
|
||
| File | Key Coverage |
|
||
|------|-------------|
|
||
| `at_mention.e2e.ts` | @ suggestions in post draft |
|
||
| `channel_mention.e2e.ts` | ~ suggestions in post draft |
|
||
| `emoji_suggestion.e2e.ts` | : emoji suggestions |
|
||
| `slash_suggestion.e2e.ts` | / command suggestions |
|
||
| `channel_post_draft.e2e.ts` | Combined autocomplete in channel draft |
|
||
| `edit_post.e2e.ts` | Autocomplete in edit mode |
|
||
| `thread_post_draft.e2e.ts` | Autocomplete in thread reply draft |
|
||
|
||
### Other Areas
|
||
|
||
- **messaging/** (24 files) — sending, editing, reactions, attachments, markdown
|
||
- **channels/** (16 files) — create, archive, join, leave, DMs, group messages
|
||
- **account/** (15 files) — profile, notifications, preferences, timezone
|
||
- **threads/** (6 files) — thread replies, following, unread
|
||
|
||
---
|
||
|
||
## ADDING NEW TESTS — CHECKLIST
|
||
|
||
1. **Investigate first**: check existing page objects, helpers, and tests in the same area
|
||
2. **Place file**: `e2e/test/products/channels/<area>/<name>.e2e.ts`
|
||
3. **Use `Setup.apiInit(siteOneUrl)`** for isolated test data (never reuse shared data across tests)
|
||
4. **`beforeEach`**: assert `ChannelListScreen.toBeVisible()` as starting point
|
||
5. **Each `it()`**: navigate back to channel list at the end (clean state for next test)
|
||
6. **Comment style**: `// #` for steps, `// *` for assertions
|
||
7. **testIDs**: use page object constants — never hardcode `by.id('raw_string')`
|
||
8. **New testID needed?**: add to the app component first, then add to page object
|
||
9. **Test isolation**: never share mutable state between `it()` blocks
|
||
10. **Export**: if adding a new page object, export it from the relevant `index.ts`
|
||
11. **Verify**: run `npm run tsc` and `npm run lint` before committing
|
||
|
||
---
|
||
|
||
## KNOWN ISSUES
|
||
|
||
### Date Suggestion Autocomplete (MM-T3393 Step 3)
|
||
|
||
`DateSuggestion` component is commented out in `app/components/autocomplete/autocomplete.tsx` (lines 219–220). The `before:`, `after:`, and `on:` modifiers do **not** show a date picker. Do not write tests asserting date autocomplete behavior until this is implemented.
|
||
|
||
### Channel Mention in Search (MM-T3393 Step 2)
|
||
|
||
Tapping `searchModifierIn` inserts `in:` and **does** trigger channel mention autocomplete (`Autocomplete.sectionChannelMentionList`) without further typing. MM-T5294_3 covers a similar case but also types a channel name — these are distinct scenarios.
|
||
|
||
---
|
||
|
||
## DEBUGGING E2E FAILURES
|
||
|
||
### MANDATORY: read the artifact before proposing a fix
|
||
|
||
This rule overrides any default tendency to "patch first and see." If a detox test is failing, you MUST complete steps 1–4 below BEFORE editing any test, page object, or app code. State each finding in the chat before the fix proposal. No exceptions.
|
||
|
||
1. **Open `detox/artifacts/<latest run>/<test name>/testFnFailure.png`** with the Read tool. State in one sentence what screen the app is on at failure time and which UI element the assertion was looking for.
|
||
2. **Open `device.log` in the same directory.** Find the failing action's timestamp from the run output, then `awk` ±3 seconds around it. Look for:
|
||
- `UIKit:EventDispatch ... Sending UIEvent` → confirms the synthetic touch was dispatched
|
||
- `(UIKitCore) send gesture actions` → confirms a gesture handler received it
|
||
- `RCTAnimatedNodeDTXSpy ... needs update` after the touch → confirms a TouchableOpacity press animation fired
|
||
- `CFNetwork ... Task <id> resuming` then `received response, status N` → confirms (or refutes) that the handler made its HTTP call
|
||
- `DTXJSTimerSyncResource ... timed out after Nms` where N >> the declared duration → JS thread was blocked; correlate against what the handler does after `await`
|
||
3. **Open `detox.log`** and grep for `testFailed`. For each failed expectation, parse its `viewHierarchy` JSON and list the testIDs that ARE and AREN'T present at failure time. Use a `python3` heredoc if needed — `id="..."` attributes are testIDs.
|
||
4. **State the diagnosis explicitly using this template**, then fix accordingly:
|
||
> Tap dispatched: YES/NO (evidence: `<log line>`)
|
||
> Handler ran: YES/NO (evidence: `<log line>`)
|
||
> Side effect occurred (HTTP/DB/nav): YES/NO (evidence: `<log line>`)
|
||
> UI re-rendered: YES/NO (evidence: viewHierarchy testIDs)
|
||
> Therefore root cause is: <pick one>
|
||
> - test (selector/timing/missing wait)
|
||
> - detox/native (tap missed, sync stuck, simulator)
|
||
> - app (handler didn't fire / state didn't update / observer didn't emit)
|
||
|
||
Only after stating those four findings may you propose a fix. If finding 4 says "app bug," the fix is NOT a retry loop or longer timeout in the test — say so and either fix the app or stop and report.
|
||
|
||
### Forbidden patches without evidence
|
||
|
||
Never apply any of these without first completing the artifact-reading checklist above:
|
||
|
||
- **Retry loops around a single tap/action.** Detox `tap` reporting success (`invokeResult: {}`) means the synthetic touch WAS dispatched. Retrying it doesn't help if the app's handler ran but produced no state change. If your first attempt at fixing a flake adds a retry loop, you are guessing.
|
||
- **Increasing a timeout beyond what already works in another test.** If `verifyStatusCleared` previously asserted instantaneously and now needs 10s of polling, the right question is "what changed about the state-update path?", not "how long should I wait?".
|
||
- **Wrapping an action in try/catch to swallow the failure.** Hides the symptom; never fixes the cause.
|
||
- **Adding a second tap because the first "might not have fired."** It fired — check `UIKit:EventDispatch` in `device.log`.
|
||
|
||
You get ONE attempt at increasing a timeout per failure mode. If a second iteration is also "wait longer / try again," stop and run the artifact checklist instead.
|
||
|
||
### Distinguishing tap-not-received from app-didn't-react
|
||
|
||
Symptom: Detox `tap` returns success, then a `toBeVisible` / `toHaveText` / `not.toBeVisible` assertion times out.
|
||
|
||
This is almost always the app, not Detox. Verify by checking:
|
||
|
||
| Question | Where to look | What it means |
|
||
|---|---|---|
|
||
| Was the touch dispatched to a UIWindow? | `device.log` for `Sending UIEvent type: 0` at the action's timestamp | If yes, the tap reached iOS hit-testing |
|
||
| Did a TouchableOpacity animate (press feedback)? | `device.log` for `RCTAnimatedNodeDTXSpy ... needs update` right after the UIEvent | If yes, an onPress handler was invoked |
|
||
| Did the expected side effect fire? | `device.log` for `CFNetwork ... resuming` (HTTP) or batchRecords log (DB) | If yes, the handler ran to completion |
|
||
| Did the React tree update? | `detox.log` `testFailed.viewHierarchy` — search for the testIDs that should have disappeared/appeared | If old testIDs persist, the observer/render didn't propagate — app bug |
|
||
|
||
When the app didn't react, the fix belongs in the app (often: an unawaited fire-and-forget Promise in the handler, a stale observable closure, or a model mutation that doesn't trigger emission). Do not patch this in the test.
|
||
|
||
### Pause-before-edit signals
|
||
|
||
If the user says any of "stop guessing", "look at the logs", "you're in a loop", "read the screenshot" — treat that as a hard stop. Do not propose a code change in the same turn. Reply with findings from the artifact checklist above, nothing else.
|
||
|
||
### Screenshots and Logs
|
||
|
||
Artifacts saved to `detox/artifacts/` after each run:
|
||
- Screenshots: only on test failure (`keepOnlyFailedTestsArtifacts: true`)
|
||
- Logs: always enabled
|
||
|
||
### Common Failure Patterns
|
||
|
||
| Symptom | Likely Cause | Fix |
|
||
|---------|-------------|-----|
|
||
| `element not found` timeout | testID mismatch or screen not loaded | Check actual testID in app source; add `toBeVisible()` wait before interaction |
|
||
| Test passes locally, fails on CI | Race condition or emulator timing | Add `wait(timeouts.ONE_SEC)` before flaky assertion; verify emulator fully booted |
|
||
| All tests fail with login error | Server not reachable | Check the platform-specific server URL; verify server is up |
|
||
| `element is not visible` on tap | Element exists but off-screen | Scroll to element first: `await scrollView.scroll(100, 'down')` |
|
||
| Android emulator hangs | AVD not fully booted | `create_android_emulator.sh` waits for `boot_completed`; check its output in CI logs |
|
||
|
||
### Detox Sync Issues
|
||
|
||
`debugSynchronization: 20000` means Detox logs sync status after 20s of waiting. If you see these logs:
|
||
- App has pending network requests → add `waitFor` instead of `wait`
|
||
- Animations running → use `withTimeout` with a longer period or disable animations in test
|
||
|
||
### CI-Specific Debugging
|
||
|
||
- Test artifacts uploaded as `android-results-{hash}-{runId}` / `ios-results-{hash}-{runId}`
|
||
- Full HTML report linked in PR comments after each E2E run
|
||
- Check GitHub Actions run summary for per-shard pass/fail breakdown
|