whatsapp-rust 0.5.0

Rust client for WhatsApp Web
Documentation
# E2E Testing Best Practices

E2E tests live in `tests/e2e/` and run against a mock WhatsApp server. They test real connection flows, encryption, and event delivery.

## Test Infrastructure

- **`tests/e2e/src/lib.rs`**: `TestClient` helper — connects to mock server, waits for pairing + sync, provides event-based assertions.
- Each test creates isolated in-memory SQLite DBs (UUID-based), so tests have no shared state.
- Tests across files run **in parallel**; tests within a file run **sequentially**.

## File Organization for Parallelism

Split test files by domain so they run concurrently. Cargo runs each test binary (file) in parallel, but tests within a binary run sequentially. A single large file becomes the bottleneck.

```
tests/e2e/tests/
├── chat_actions.rs      # Pin, mute, archive, star
├── connection.rs        # Connect, reconnect
├── groups.rs            # Group CRUD, admin, settings
├── media.rs             # Upload, download, send media
├── messaging.rs         # Send/receive text messages
├── chatstate_ttl.rs     # Chatstate TTL expiry (35s sleep — own file for parallelism)
├── offline_groups.rs    # Offline group notifications
├── offline_messages.rs  # Offline message queuing + delivery
├── offline_receipts.rs  # Offline receipt + presence delivery
├── presence.rs          # Typing indicators, availability
├── profile.rs           # Push name, status text
├── profile_picture.rs   # Profile picture CRUD
└── receipts.rs          # Online receipt routing
```

When adding new tests, place them in the file matching their domain. If a file grows beyond ~10-15 tests, consider splitting further.

## Event-Driven Waiting (Preferred)

Use `wait_for_event()` with predicates instead of arbitrary sleeps. This is both faster and more reliable:

```rust
// GOOD: event-driven — returns as soon as the event arrives
let event = client_b
    .wait_for_event(15, |e| matches!(e, Event::Message(msg, _) if msg.conversation.as_deref() == Some("hello")))
    .await?;

// BAD: arbitrary sleep — wastes time or causes flaky failures
tokio::time::sleep(Duration::from_secs(2)).await;
```

Reference: `groups.rs` uses zero sleeps and runs at ~2.2s/test. Follow this pattern for new tests.

## Offline Testing Pattern

When testing offline event delivery, use short sleeps (100ms) after `reconnect()` or `disconnect()` to let the server detect the TCP close. Localhost connections close nearly instantly:

```rust
// Client goes offline (triggers auto-reconnect in background)
client_b.client.reconnect().await;
tokio::time::sleep(Duration::from_millis(100)).await;

// Now send while client is offline — server queues it
client_a.client.send_message(jid_b.clone(), message).await?;

// Client reconnects automatically and receives from offline queue
let event = client_b.wait_for_event(30, |e| matches!(e, Event::Message(..))).await?;
```

For full disconnects (no auto-reconnect):
```rust
client_b.disconnect().await;
tokio::time::sleep(Duration::from_millis(100)).await;
```

## `reconnect_and_wait()` Helper

Use `TestClient::reconnect_and_wait()` when you need the client back online (not testing offline behavior):

```rust
// Reconnects and waits for Connected event — no arbitrary sleep needed
client_b.reconnect_and_wait().await?;
```

Do NOT use this for offline tests — it waits for the client to be back online, defeating the purpose.

## Timeout Guidelines

- **Event waits in online flows**: 10-15s (events arrive in <1s normally)
- **Event waits after offline reconnect**: 30s (reconnect + offline queue drain)
- **Negative assertions** (event should NOT arrive): 3-5s
- **Post-disconnect sleeps**: 100ms (TCP close detection)
- **Sequential processing delays**: 50ms (ensure server ordering)

## Writing New E2E Tests

1. Use `TestClient::connect("unique_prefix")` with a unique prefix per client per test.
2. Use `wait_for_event()` for all assertions — avoid polling or sleeping.
3. Always call `disconnect()` on all clients at the end (cleanup).
4. Return `anyhow::Result<()>` for clean error propagation.
5. Use `env_logger` for debug output: `let _ = env_logger::builder().is_test(true).try_init();`