Add Maestro test seeding for NativeInputWidget E2E tests#8418
Merged
Conversation
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>
44f1fe4 to
3a4ff76
Compare
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>
Contributor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
❌ 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.
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>
joshliebe
reviewed
May 19, 2026
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>
3 tasks
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>
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>
joshliebe
reviewed
May 22, 2026
| if (url.startsWith("http://") || url.startsWith("https://")) url else "https://$url" | ||
|
|
||
| private fun titleFromUrl(url: String): String = | ||
| url.removePrefix("https://").removePrefix("http://").substringBefore("/") |
Contributor
There was a problem hiding this comment.
Logic is duplicated in both the favorites and bookmarks plugins. Could make a SavedSites base plugin to share the logic.
Contributor
Author
There was a problem hiding this comment.
Yes, I”m going that in the following PR
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

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 seeding —
LaunchViewModel.start()callsTestScenarioSeeder.seedIfNeeded(...)with intent extras fromLaunchBridgeActivity. 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-internalmodule, wired viainternalImplementation. Release flavours (play,fdroid) bind aNoOpTestScenarioSeederin their own source sets. Verified:play/fdroidclass outputs contain noRealTestScenarioSeeder, noTestScenarioenum, and noRealOmnibarPositionWriter.Module layout
test-seeder/test-seeder-api— interface +EXTRA_*constants +OmnibarPositionWriterseamtest-seeder/test-seeder-internal—RealTestScenarioSeeder,TestScenariocanned datasets, testsapp/src/internal/java/.../seeder/RealOmnibarPositionWriter.kt— writesSettingsDataStore.omnibarType; only present in internal flavourapp/src/play/java/.../seeder/NoOpTestScenarioSeeder.kt+app/src/fdroid/java/.../seeder/NoOpTestScenarioSeeder.kt— no-op stubsapp/build.gradle—implementation project(':test-seeder-api')+internalImplementation project(':test-seeder-internal')Launch arguments (all optional, all composable, all ignored unless
isMaestro="true"):isMaestro"true"enables seeding; required (Maestro 2.5.1 does not auto-inject it)testScenariofavorites_3seeds 3 favorites,bookmarks_2seeds 2 bookmarksomnibarPositiontop/bottom/split→OmnibarPositionWriter.setFromKey(...)nativeInputToggletrue/false→DuckChat.setNativeInputFieldUserSetting(...)inputWithAiToggletrue/false→DuckChat.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 flavoursConfirm release builds carry no seeder code
./gradlew :app:compilePlayDebugSourcesthenfind app/build -path '*playDebug*' \( -name 'RealTestScenarioSeeder*' -o -name 'TestScenario.class' -o -name 'RealOmnibarPositionWriter*' \)— empty resultfdroidRelease— empty result; onlyNoOpTestScenarioSeeder_Factory.classappears in the seeder packageSample Maestro test (internal build only)
./gradlew :app:installInternalDebugmaestro test .maestro/unified_input_screen/unified_input_with_favorites.yamlUI 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-internalframework that, whenisMaestro="true"is present in launch intent extras, applies registeredTestSeederPlugins (validated againstTestSeederKey) to seed app state.Wires seeding into startup by replacing the old suspend
determineViewToShowcall withLaunchViewModel.start(intent), which runs seeding before referrer waiting and emitting navigation commands;play/fdroidflavors bind aNoOpTestScenarioSeederwhileinternalbuilds include real plugins foromnibarPosition,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.nativeInputFielddefault toINTERNAL.Reviewed by Cursor Bugbot for commit 9de7926. Bugbot is set up for automated code reviews on this repo. Configure here.