Skip to content

Add Maestro test seeding for NativeInputWidget E2E tests#8418

Merged
malmstein merged 23 commits into
developfrom
feature/david/unified_input_e2e_tests
May 22, 2026
Merged

Add Maestro test seeding for NativeInputWidget E2E tests#8418
malmstein merged 23 commits into
developfrom
feature/david/unified_input_e2e_tests

Conversation

@malmstein
Copy link
Copy Markdown
Contributor

@malmstein malmstein commented May 4, 2026

Task/Issue URL: https://app.asana.com/1/137249556945/project/1174433894299346/task/1214486381568975?focus=true
Related proposal: https://app.asana.com/1/137249556945/project/1174433894299346/task/1214952101964616?focus=true

Description

Adds Maestro E2E test seeding and confines the seeder code to internal builds.

Launch-intent seedingLaunchViewModel.start() calls TestScenarioSeeder.seedIfNeeded(...) with intent extras from LaunchBridgeActivity. Maestro flows launch the app with a known state (bookmarks/favorites, omnibar position, native-input toggles) without driving the UI.

Internal-only impl — the real seeder lives in a new :test-seeder-internal module, wired via internalImplementation. Release flavours (play, fdroid) bind a NoOpTestScenarioSeeder in their own source sets. Verified: play/fdroid class outputs contain no RealTestScenarioSeeder, no TestScenario enum, and no RealOmnibarPositionWriter.

Module layout

  • test-seeder/test-seeder-api — interface + EXTRA_* constants + OmnibarPositionWriter seam
  • test-seeder/test-seeder-internalRealTestScenarioSeeder, TestScenario canned datasets, tests
  • app/src/internal/java/.../seeder/RealOmnibarPositionWriter.kt — writes SettingsDataStore.omnibarType; only present in internal flavour
  • app/src/play/java/.../seeder/NoOpTestScenarioSeeder.kt + app/src/fdroid/java/.../seeder/NoOpTestScenarioSeeder.kt — no-op stubs
  • app/build.gradleimplementation project(':test-seeder-api') + internalImplementation project(':test-seeder-internal')

Launch arguments (all optional, all composable, all ignored unless isMaestro="true"):

Argument Effect
isMaestro "true" enables seeding; required (Maestro 2.5.1 does not auto-inject it)
testScenario favorites_3 seeds 3 favorites, bookmarks_2 seeds 2 bookmarks
omnibarPosition top / bottom / splitOmnibarPositionWriter.setFromKey(...)
nativeInputToggle true / falseDuckChat.setNativeInputFieldUserSetting(...)
inputWithAiToggle true / falseDuckChat.setInputScreenUserSetting(...)

Steps to test this PR

Unit tests

  • ./gradlew :test-seeder-internal:testDebugUnitTest — passes
  • ./gradlew :app:testInternalDebugUnitTest :app:testPlayDebugUnitTest :app:testFdroidReleaseUnitTest --tests "com.duckduckgo.app.launch.LaunchViewModelTest" — passes in all three flavours

Confirm release builds carry no seeder code

  • ./gradlew :app:compilePlayDebugSources then find app/build -path '*playDebug*' \( -name 'RealTestScenarioSeeder*' -o -name 'TestScenario.class' -o -name 'RealOmnibarPositionWriter*' \) — empty result
  • Same check for fdroidRelease — empty result; only NoOpTestScenarioSeeder_Factory.class appears in the seeder package

Sample Maestro test (internal build only)

  • Install internal build: ./gradlew :app:installInternalDebug
  • Run: maestro test .maestro/unified_input_screen/unified_input_with_favorites.yaml
  • Verify the new tab page shows Example, DuckDuckGo, and Privacy Test Pages, and the Search / Duck.ai mode pill is visible above the omnibar — without any UI-driven setup

UI changes

Before After
N/A — no UI changes N/A — no UI changes

Note

Medium Risk
Touches app launch flow by adding pre-navigation seeding based on intent extras, and changes a DuckChat feature-flag default; failures could affect startup/navigation timing in internal builds (release flavors bind a no-op).

Overview
Adds a new :test-seeder-api/:test-seeder-internal framework that, when isMaestro="true" is present in launch intent extras, applies registered TestSeederPlugins (validated against TestSeederKey) to seed app state.

Wires seeding into startup by replacing the old suspend determineViewToShow call with LaunchViewModel.start(intent), which runs seeding before referrer waiting and emitting navigation commands; play/fdroid flavors bind a NoOpTestScenarioSeeder while internal builds include real plugins for omnibarPosition, nativeInputToggle, inputWithAiToggle, and seeding favorites/bookmarks.

Adds Maestro flows to exercise unified input scenarios with seeded favorites across omnibar positions/AI on-off, updates unit tests accordingly, and changes DuckChatFeature.nativeInputField default to INTERNAL.

Reviewed by Cursor Bugbot for commit 9de7926. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread app/src/main/java/com/duckduckgo/app/launch/LaunchBridgeActivity.kt Outdated
malmstein and others added 12 commits May 18, 2026 22:06
A seed error (e.g. Room constraint or DataStore I/O failure) must not
prevent the app from routing to BrowserActivity. Wraps seedIfNeeded in
runCatching so determineViewToShow always runs.

Also adds a comment in the Maestro YAML clarifying that isMaestro is
auto-injected by the Maestro runtime, not declared in arguments.
…ggle

Replaces the single testScenario arg that bundled everything with three
independent args that can be freely combined in Maestro YAML. testScenario
handles data seeding (favorites, bookmarks), omnibarPosition accepts
top/bottom/split, nativeInputToggle accepts true/false.

Scenario keys also drop the native_input_ prefix (favorites_3, bookmarks_2).
Move seedIfNeeded into LaunchViewModel.initialiseData(intent) so
LaunchBridgeActivity stays thin and no longer reads intent extras.

Fix toggle wiring: nativeInputToggle now binds to
setNativeInputFieldUserSetting (matching its name), and a new
inputWithAiToggle binds to setInputScreenUserSetting -- the
setting that actually activates the Search/Duck.ai input screen
the Maestro flow asserts against.

Maestro 2.5.1 does not auto-inject isMaestro=true despite the
prior YAML comment, so pass it explicitly. Drop the obsolete
enable_unified_input.yaml shared flow, now covered by the seeder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@malmstein malmstein force-pushed the feature/david/unified_input_e2e_tests branch from 44f1fe4 to 3a4ff76 Compare May 18, 2026 21:57
Comment thread app/src/main/java/com/duckduckgo/app/launch/LaunchViewModel.kt
Split the single unified-input flow into six, one per cell of the
inputWithAiToggle x omnibarPosition matrix, so we catch a regression
in any individual combination instead of relying on one representative
case.

The seeded scenario stays identical across all six (favorites_3,
isMaestro=true, nativeInputToggle=true); only the two varied launch
arguments differ. The original file is renamed to ..._ai_bottom for
naming symmetry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit cfb4484. Configure here.

Comment thread app/src/main/java/com/duckduckgo/app/launch/LaunchBridgeActivity.kt Outdated
David Gonzalez and others added 3 commits May 19, 2026 11:20
TestScenarioSeeder imported DuckChatDataStore directly from
duckchat-impl, tripping the NoImplImportsInAppModule lint rule in the
:app module. Promote setNativeInputFieldUserSetting onto the public
DuckChat interface alongside its sibling setInputScreenUserSetting
and have the seeder depend on DuckChat instead.

As a side benefit, routing through RealDuckChat now invokes
cacheUserSettings() after the toggle, which the direct-to-datastore
path skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two FakeDuckChat test doubles implement DuckChat directly (not via
DuckChatInternal) and weren't updated for the new abstract method
added in the previous commit, breaking duckchat-impl unit test
compilation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
initialiseData() and determineViewToShow() each ran in their own
viewModelScope.launch with no ordering guarantee, so for existing
users (where waitForReferrerCode() resolves instantly) the home
command could fire while the seeder's withContext(io()) writes were
still in flight. Maestro flows that depend on pre-seeded favorites
or settings would then race the new tab page.

Fold both into a single start(intent) that awaits seeding before
waitForReferrerData() and showOnboardingOrHome(). Keep failure
isolation so a seed exception still routes to BrowserActivity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread duckchat/duckchat-api/src/main/java/com/duckduckgo/duckchat/api/DuckChat.kt Outdated
@malmstein malmstein marked this pull request as draft May 19, 2026 15:34
David Gonzalez and others added 4 commits May 19, 2026 22:41
Introduces TestScenarioSeeder and OmnibarPositionWriter interfaces in a
new -api module. Impl will follow in :test-seeder-internal so release
builds ship no code that can mutate user settings or saved sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Splits the interface and impl into separate files within :app and points
LaunchViewModel + tests at the api-module interface. RealTestScenarioSeeder
keeps its current dependencies; the impl move comes in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces RealOmnibarPositionWriter as the boundary between the seeder
and SettingsDataStore so RealTestScenarioSeeder no longer depends on
:app-private types. The writer still lives in :app/src/main here; it
moves into the internal flavour source set together with the seeder.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Creates a new -internal module that contains RealTestScenarioSeeder and
the TestScenario enum, wired into :app via internalImplementation so the
real impl ships only with internal builds. Play and F-Droid flavours get
a NoOpTestScenarioSeeder so LaunchViewModel's seeding call hits a
guaranteed no-op there. RealOmnibarPositionWriter moves to the internal
flavour source set in :app, keeping SettingsDataStore writes out of
release builds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reshape the seeder around the Tech Design landed at
https://app.asana.com/1/137249556945/project/414730916066338/task/1214952101964616:

  - :test-seeder-api gains TestSeederPlugin and TestSeederKey
    (authoritative registry of supported keys), and seedIfNeeded
    now takes a Map<String, String> rather than named string args.
  - :test-seeder-internal becomes a plugin orchestrator. It injects
    DaggerSet<TestSeederPlugin>, validates at construction (undeclared
    key or key collision throws), and dispatches sorted-by-key under
    dispatchers.io(). Unknown runtime key throws — catches Maestro typos.
  - Drop the OmnibarPositionWriter seam, the canned TestScenario enum,
    and the centralised feature deps on the seeder module.
  - Mutations move to plugins next to the state they own:
    OmnibarPositionSeederPlugin in :app/src/internal/java/ (omnibar
    settings are app-private), NativeInputToggleSeederPlugin +
    InputWithAiToggleSeederPlugin in :duckchat-internal,
    AddFavoritesSeederPlugin + AddBookmarksSeederPlugin in
    :saved-sites-impl.
  - LaunchViewModel passes intent.extras.toStringMap() to the seeder.
    Play / fdroid NoOpTestScenarioSeeder updated to the new signature.
  - Six unified-input Maestro flows swap testScenario: "favorites_3"
    for addFavorites: "3" and assert against the generic Favorite N
    titles produced by the plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugin-based seeding moved the only caller outside :duckchat-impl
into NativeInputToggleSeederPlugin in :duckchat-internal, which can
depend on DuckChatInternal directly. Demote the setter back to the
internal interface — matches the other native-input write methods
already declared there. The read side
(observeNativeInputFieldUserSettingEnabled) stays on DuckChat since
ambient UI in other modules still consumes it.

Reverts the API addition from "Route seeder through DuckChat to fix
lint" (9a9c920). RealDuckChat.setNativeInputFieldUserSetting is
unchanged, so the cacheUserSettings() side-effect still fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@malmstein malmstein marked this pull request as ready for review May 20, 2026 21:11
@malmstein malmstein changed the title Add Maestro DB seeding for NativeInputWidget E2E tests Add Maestro test seeding for NativeInputWidget E2E tests May 21, 2026
Per round-2 TD feedback (Cristian, Lukasz) at
https://app.asana.com/1/137249556945/project/414730916066338/task/1214952101964616.

Move AddFavoritesSeederPlugin and AddBookmarksSeederPlugin from
:saved-sites-impl to :saved-sites-internal so plugin classes are off
the play/fdroid classpath, not just inert. Drop the :test-seeder-api
dep from :saved-sites-impl; add it to :saved-sites-internal along
with the test deps it now needs.

Switch addFavorites and addBookmarks from an integer count to a
semicolon-separated list of URLs. Plugins normalise missing schemes
to https:// and derive titles from the host portion. Tests now
control which URLs are seeded, which is required for richer
per-feature values later (e.g. multi-property passwords) and removes
the canned "Favorite N" placeholders.

Update TestSeederKey descriptions and the six unified-input Maestro
flows to match. Add unit tests for both plugins covering single and
multi-URL input, scheme preservation, whitespace trimming, trailing
semicolons, and empty-value rejection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
if (url.startsWith("http://") || url.startsWith("https://")) url else "https://$url"

private fun titleFromUrl(url: String): String =
url.removePrefix("https://").removePrefix("http://").substringBefore("/")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic is duplicated in both the favorites and bookmarks plugins. Could make a SavedSites base plugin to share the logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I”m going that in the following PR

Copy link
Copy Markdown
Contributor

@joshliebe joshliebe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just the one comment, LGTM

@malmstein malmstein merged commit 029dfaa into develop May 22, 2026
18 checks passed
@malmstein malmstein deleted the feature/david/unified_input_e2e_tests branch May 22, 2026 11:11
malmstein added a commit that referenced this pull request May 22, 2026
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1215056189493585

### Description

Lands the full unified-input Maestro suite under
`.maestro/unified_input_screen/`. Replaces the six per-combination
`unified_input_with_favorites_*.yaml` files from the seeders work with a
single consolidated test, and adds six new top-level tests covering the
From-Search / New-Tab scenarios from the Native Input Widget E2E backlog
plus a Duck.ai chat → Search navigation test.

Every top-level test pins `nativeInputToggle: "true"`, uses
`addFavorites: "example.com;duckduckgo.com;privacytest.com"`, and wraps
each iteration in `retry: maxRetries: 3`. The Duck.ai-side tests pin
`inputWithAiToggle: "true"` and sweep `omnibarPosition` ∈ {top, bottom,
split} (three iterations each). `unified_input_with_seeded_favorites`
keeps the AI on/off sweep (six iterations) because the assertion is
AI-independent.

**Layout under `.maestro/unified_input_screen/`:**

```
unified_input_with_seeded_favorites.yaml      # 6 iters (positions × AI)
unified_input_open_chat_move_to_search.yaml   # 3 iters (positions, AI on)
unified_input_search_suggestions.yaml         # 3 iters
unified_input_ask_duckai_suggestion.yaml      # 3 iters
unified_input_duckai_model_reasoning.yaml     # 3 iters
unified_input_duckai_tool_pills.yaml          # 3 iters
unified_input_duckai_web_search_prompt.yaml   # 3 iters
shared/
  launch_and_skip_onboarding.yaml             # local skip helper (see below)
  assert_seeded_favorites_visible.yaml
  open_new_chat.yaml
  flow_search_suggestions.yaml
  flow_ask_duckai_suggestion.yaml
  flow_duckai_model_reasoning.yaml
  flow_duckai_tool_pills.yaml
  flow_duckai_web_search_prompt.yaml
```

**Scenarios covered**

| Test | What it verifies |
|---|---|
| `unified_input_with_seeded_favorites` | Three seeded favorites visible
on the NTP across all omnibar positions and AI states |
| `unified_input_open_chat_move_to_search` | Open Duck.ai chat, submit
query, switch back to Search mode and run a regular search |
| `unified_input_search_suggestions` | Autocomplete suggestions appear
when typing in Search mode; submitting dismisses the unified input and
loads the SERP |
| `unified_input_ask_duckai_suggestion` | "Ask Duck.ai" autocomplete row
opens Duck.ai with the typed query submitted |
| `unified_input_duckai_model_reasoning` | Model picker opens, switching
model updates the chip; reasoning picker opens, picking a non-default
effort persists on re-open |
| `unified_input_duckai_tool_pills` | Selecting Web Search / Create
Image from the options menu places an active pill; tapping the pill
removes the tool |
| `unified_input_duckai_web_search_prompt` | Web Search applies to the
first prompt and clears for the follow-up — the tool is per-prompt, not
sticky |

**Infrastructure notes**

- `shared/launch_and_skip_onboarding.yaml` — robust local replacement
for `../../shared/skip_all_onboarding.yaml`. The upstream skip falls
back to `../../shared/pre_onboarding.yaml` whose asserted welcome copy
has drifted from the new onboarding build, and the Skip Onboarding
button bounds straddle the status bar height so tap-by-id is intercepted
by the system bar. This helper waits for the button, taps by point at
the bottom of its bounds, and waits for the unified input to settle.
- `addFavorites` values are URL-list strings (semicolon-separated hosts)
— matches the landed `AddFavoritesSeederPlugin` semantics from #8418.
- All YAMLs target `appId: com.duckduckgo.mobile.android` (not `.debug`)
to match the binary used in CI / Robin / Maestro Cloud.

**CI wiring**

`.github/workflows/e2e-nightly-non-blockers-suite.yml` adds a `Unified
Input Field` step that runs the suite on Maestro Cloud, scoped via the
`unifiedInputTest` tag, and reports failures to Asana.

### Steps to test this PR

- [ ] Install: `./gradlew installPlayDebug`
- [ ] Run the whole suite: `maestro test .maestro/unified_input_screen
--include-tags unifiedInputTest`
- [ ] All seven top-level tests pass — 24 iterations total (six tests ×
three positions plus the seeded-favorites test's six AI-and-position
combos)

### UI changes
| Before  | After |
| ------ | ----- |
| N/A — test-only change | N/A — test-only change |

---------

Co-authored-by: David Gonzalez <malmstein@Davids-MacBook-Pro.local>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants