# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project overview

Mobile UI test automation for Sony Music's iOS and Android apps (Orchard,
AWAL, SME brands), written in Python with Behave (Gherkin/BDD) driving
Appium. Tests run either against a local simulator/emulator or against real
devices on BrowserStack.

## Commands

### Setup

```
virtualenv venv && source ./venv/bin/activate && pip install -r requirements.txt
cp .env.shadow .env   # fill in BRAND, OS, EXTENSION, PREFIX
mkdir apps             # put the target .app (iOS) or .apk (Android) here
```
No credentials go in `.env`. Test user credentials come from AWS Secrets
Manager at runtime via `helpers/secrets_manager.py`, which needs active AWS
credentials in the process running the tests (e.g. via `awsume <profile>`) —
see the `local-device-setup` skill for the terminal-vs-IDE details.
iOS additionally needs Xcode + `appium driver install xcuitest`, `brew install carthage libimobiledevice ffmpeg`.
Android additionally needs Android Studio SDK tools + `appium driver install uiautomator2`, `ANDROID_HOME`/`JAVA_HOME` set.

### Running tests locally

```
make local_ios_tests            # boots Appium, runs ios_basic_flow.feature
make local_android_tests        # boots emulator + Appium, runs android_basic_flow.feature
behave -i <feature_file> --tags=~@wip   # run one feature directly (fastest inner loop)
behave -i <feature_file> --tags=~@wip -n "<scenario name>"   # run a single scenario
```
`run_android_tests`/`run_ios_tests` (invoked by the targets above) always exclude `@wip`-tagged scenarios.

### Running on BrowserStack

```
make browserstack_docker              # full dockerized run (requires .env + awsume -o for AWS creds)
make local_browserstack_android       # via run.py, no docker, subset of android_*.feature files
make local_browserstack_ios           # via run.py, no docker, subset of ios_*.feature files
```
Requires `BROWSERSTACK_USERNAME`/`BROWSERSTACK_ACCESS_KEY` in `.env` and `USE_BROWSERSTACK=true`.

### Lint

```
make lint          # flake8 over page_objects, features/environment.py, helpers, run.py
                    # + flake8 features/steps --ignore F811 (step defs intentionally reuse function names)
make docker_lint    # same, via docker compose
```
There is no formatter/autofix — flake8 only checks style, it doesn't rewrite code.

## Architecture

### Test flow: feature → step → page object → BaseScreen

- `features/*.feature` — Gherkin scenarios, one file per screen/flow, split by
  platform (`android_*.feature` / `ios_*.feature`). The same behavior is
  normally expressed twice, once per platform file — keep them in sync when
  adding a scenario to one.
- `features/steps/*.py` — step definitions. They should only translate
  Gherkin text into a single call on a page object already attached to
  `context` (e.g. `context.home_screen.reset_filters()`); business logic and
  assertions belong in the page object, not the step.
- `page_objects/ios_pom/` and `page_objects/android_pom/` — one class per
  screen, mirrored across both platforms with matching public method names
  (e.g. `HomeScreenIos`/`HomeScreenAndroid` both expose `reset_filters()`).
  Every screen class extends `page_objects/base_screen.py`'s `BaseScreen`.
  `page_objects/mixins/` holds logic shared across both platforms
  (`playlists_mixin.py`, `header_info_mixin.py`) plus retry decorators
  (`mixins/utils.py`: `retry_on_stale_element`, `retry_on_assertion`).
- `page_objects/base_screen.py` — the only place that should talk to the
  Appium driver directly. Locators are `(By, value)` tuples; use `waiter`,
  `click_with_retry`, `scroll_and_assert`, `scroll_page`, `swipe_*` rather
  than calling `self.driver.find_element(...)` from a screen class.

Watch for existing asymmetry, not the intended pattern: `ios_pom/track_screen.py`
has no Android counterpart, and `android_pom/playlist_screen.py` /
`charts_digest_screen.py` have no iOS counterpart.

### Driver lifecycle (`features/environment.py`)

One Appium driver is created per **feature**, not per scenario — all
scenarios in a feature file share the same app session. Hooks (there is no
`before_step`):
- `before_all` — sets up a per-run logger (a UUID-based name, since the
  BrowserStack SDK restarts Behave internally and env vars can't be used to
  separate runs).
- `before_feature` — resolves brand/user from tags (`@orchard-app`/
  `@awal-app`/`@sme-app`, `employee`), starts the driver (local via
  `helpers/driver_factory.create_driver`, or BrowserStack via
  `helpers/browserstack_helper.BrowserstackHelper`), and wires page objects
  onto `context` (`declare_page_objects`).
- `before_scenario` — skip logic (`@skip`, `@skip_if_sme`, or a previously
  failed scenario in the same feature skipping the rest via
  `feature.skip_remaining`).
- `after_step` — on failure, marks `feature.skip_remaining = True` (so the
  rest of the feature's scenarios get skipped) and, on BrowserStack only,
  saves a screenshot into `ScreenshotManager`. Local runs do **not** get an
  automatic failure screenshot.
- `after_scenario` — on a failed `language`-tagged scenario, resets the
  app's language back to default so it doesn't affect the next scenario.
- `after_feature` — quits the driver and reports pass/fail to BrowserStack.

### Helpers (`helpers/`) — flat, not namespaced by concern

Everything lives directly under `helpers/` (there's no `helpers/appium/` or
`helpers/integrations/` subpackage):
- `driver_factory.create_driver(remote_url, caps, mobile_os)` — builds the
  `XCUITestOptions`/`UiAutomator2Options` driver; used identically by both
  the local path and `browserstack_helper`.
- `gestures.py` — platform-agnostic W3C Actions tap/swipe/press-hold.
- `ios_helper.py` / `android_helper.py` — per-platform scrolling, keyboard
  handling, and (Android) `resource_id_xpath`/`content_desc_xpath` locator
  builders. `keyboard_utils.py` dispatches to whichever of the two matches
  the current platform.
- `driver_utils.py` — `get_device_name`/`get_platform_name` from capabilities.
- `coordinates/` — `resolver.get_coordinate` maps a device name to a
  hardcoded coordinate; `android_coordinates.py`/`ios_coordinates.py` are
  currently empty placeholders, not wired up to anything yet.
- `browserstack_helper.BrowserstackHelper` — BrowserStack session lifecycle
  (build id, session id, video, logs, marking pass/fail). `get_screenshot_url`
  extracts the URL by string-splitting raw log text rather than parsing
  structured JSON — treat its output as best-effort, not authoritative.
- `secrets_manager.SecretsManagerClient` — the only sanctioned way to read a
  secret; see below.
- `users.py` — test user credentials (via `SecretsManagerClient`),
  `test_data.py` / `localization.py` — static fixture data, `models.py` —
  the `Coordinate` dataclass, `exceptions.py` — `ElementShouldNotExistError`.
- `cucumber_json.py` / `screenshot_manager.py` / `log_analyzer.py` —
  reporting: a custom Behave JSON formatter, a singleton step→screenshot
  path map it reads from, and CI build-pass/fail evaluation from log output.
- `datadog_helper.py` — uploads JUnit results to Datadog.

### Secrets convention

Every secret lookup goes through `SecretsManagerClient.get_secret`/
`get_secret_json` (`helpers/secrets_manager.py`), with the secret path
always prefixed `f"{os.getenv('CONFIG_ENV', 'qa')}/e2e-test-secrets/..."`
(see `helpers/users.py` for the canonical example). Don't introduce a second
way of reading secrets.

### Tags

`@wip` (excluded from all standard runs), `@skip`, `@skip_if_sme` (skip on
the SME brand app), brand tags `@orchard-app`/`@awal-app`/`@sme-app`,
`employee` (use the employee test user instead of client), `language`
(triggers the language-reset recovery in `after_scenario`), `@smoke`.

## Code review checklist

- New/changed page objects exist for **both** `ios_pom` and `android_pom`
  with matching public method names.
- Page-object methods use `BaseScreen` primitives (`waiter`, `click_with_retry`,
  `scroll_and_assert`, etc.) — no direct `self.driver.find_element` calls.
- Step definitions delegate to exactly one page-object call; no bare `assert`
  or business logic living in `features/steps/*.py`.
- The same action isn't registered under both `@when` and `@then`.
- Secrets go through `SecretsManagerClient`, never hardcoded or logged.
- A new scenario added to one platform's feature file has a matching scenario
  in the other platform's feature file, unless the behavior is genuinely
  platform-specific.
