From 8613c0c90a9e006a4187e92b568d86b7f142e078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Urb=C3=A1nek?= Date: Thu, 18 Jun 2026 14:59:22 +0200 Subject: [PATCH 1/6] docs: note that make watch is human-only, agents use make gen Co-Authored-By: Claude Sonnet 4.6 --- AGENTS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index cd2c4fe..aa08b20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,7 +27,7 @@ Use this file as the entrypoint for automated work in this repository. ## Codegen And Commands - Run `make gen` after changing `@riverpod`, `@freezed`, `@RoutePage`, localization, or generated asset inputs. -- Use `make watch` for continuous code generation when doing annotation-heavy work. +- Use `make watch` for continuous code generation when doing annotation-heavy work. This is a human-only command — agents must never run it (it spawns a persistent background watcher). Agents always use one-shot `make gen`. - Do not hand-edit generated files such as `*.g.dart`, `*.freezed.dart`, `*.gr.dart`, or files under `lib/assets/`. - Preferred validation commands: - `fvm flutter analyze` @@ -69,6 +69,7 @@ Existing skills: - `implement` — implement one generated Flutter task using the repo architecture and verification rules - `implement-tasks-sequence` — execute generated task files in dependency order before final verification - `techspec` — translate a PRD into an implementation-ready Flutter tech spec at `.claude/tasks//techspec.md`, grounded in the Riverpod / Freezed / AutoRoute / Dio / Firebase stack +- `spec-feature` — run the full specification pipeline (`prd` → `techspec` → `tasks`) in one shot, from feature idea to implementation-ready tasks ### How AI tools find these skills - **Spec-compliant agents** scan `.agents/skills/` directly — it is the From 7187dd794655fd89bf84bc559a5859fb364f88e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Urb=C3=A1nek?= Date: Thu, 18 Jun 2026 14:59:25 +0200 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20add=20spec-feature=20skill=20to=20c?= =?UTF-8?q?hain=20prd=20=E2=86=92=20techspec=20=E2=86=92=20tasks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .agents/skills/spec-feature/SKILL.md | 107 +++++++++++++++++++++++++++ .claude/commands/spec-feature.md | 8 ++ .claude/skills/spec-feature | 1 + 3 files changed, 116 insertions(+) create mode 100644 .agents/skills/spec-feature/SKILL.md create mode 100644 .claude/commands/spec-feature.md create mode 120000 .claude/skills/spec-feature diff --git a/.agents/skills/spec-feature/SKILL.md b/.agents/skills/spec-feature/SKILL.md new file mode 100644 index 0000000..8b7cd0a --- /dev/null +++ b/.agents/skills/spec-feature/SKILL.md @@ -0,0 +1,107 @@ +--- +name: spec-feature +description: > + Top-level orchestrator for Flutter feature specification. Runs the full uninterrupted pipeline: + prd → techspec → tasks in one shot. Use when the user says "spec a feature", "write a spec for", + "start feature spec", "plan a feature", or wants to go from idea to implementation-ready tasks + without typing three separate commands. +allowed-tools: Bash Read Grep Glob Edit Write Agent Skill +user-invocable: true +model: claude-opus-4-7 +--- + +# Flutter Template Spec Feature + +Single entry point that runs the full feature specification pipeline from idea to +implementation-ready tasks. It is designed to be invoked once; each sub-skill may pause for +user input during its mandatory clarification steps — that is intentional and correct. After +each sub-skill finishes, this orchestrator automatically invokes the next phase without requiring +the user to type the next command. + +Use this when you want to go from a feature idea directly to a full task breakdown, without +manually running `/prd`, `/techspec`, and `/tasks` in sequence. + +## Prerequisites + +- No existing `prd.md` or `techspec.md` for this feature (or the user has confirmed it is safe + to overwrite). +- The user can answer clarification questions — both `prd` and `techspec` have mandatory + interactive clarification steps that will pause the pipeline for user input. + +If a PRD and tech spec already exist and are complete, use `/start-job [feature-name]` instead. + +## Arguments + +Accepts an optional feature name. If provided, pass it to each sub-skill so they can pre-fill +the folder name and skip asking. If omitted, the `prd` sub-skill will determine the feature name +during its workflow. + +## Pipeline + +Run each phase in order. Between phases, do NOT ask for user approval — the sub-skills themselves +handle all necessary user interaction. If a phase fails hard (the sub-skill reports an +unrecoverable error), stop and surface it to the user. + +> **Critical orchestration rule:** when a sub-skill returns control, do not treat its output as +> the end of your turn. You are still inside this `/spec-feature` orchestrator. Immediately +> invoke the next phase's skill in the same response. The only valid stops are: +> (a) a sub-skill's mandatory clarification pause (which is correct — wait for the user, then +> resume), (b) a hard failure, or (c) the Final Report after Phase 3. + +### Phase 1 — Product Requirements Document + +Invoke the `prd` skill (pass the feature name argument if provided). + +The `prd` skill has two mandatory interactive steps: it will ask clarifying questions (Step 1) +and present a plan for confirmation (Step 2). These pauses are correct — wait for user responses. +After the user confirms the plan and `prd` saves `prd.md`, it will return control. + +**After `prd` returns (i.e., after `prd.md` has been saved): do not stop, do not wait for +additional input, do not summarize. Immediately invoke `techspec` (Phase 2) in the same +response.** + +### Phase 2 — Technical Specification + +Invoke the `techspec` skill (pass the feature name argument if provided). + +The `techspec` skill has one mandatory interactive step: it will ask technical clarification +questions (Step 3). This pause is correct — wait for user responses. After the user answers and +`techspec` saves `techspec.md`, it will return control. + +**After `techspec` returns (i.e., after `techspec.md` has been saved): do not stop, do not wait +for additional input, do not summarize. Immediately invoke `tasks` (Phase 3) in the same +response.** + +### Phase 3 — Task Breakdown + +Invoke the `tasks` skill (pass the feature name argument if provided). + +The `tasks` skill runs without approval prompts and generates `tasks.md` plus individual task +files. When invoked from this orchestrator (not directly by the user), `tasks` must skip its +next-step prompt and return control here. + +**After `tasks` returns: produce the Final Report. This is the only place the pipeline stops on +success.** + +## Final Report + +Output a single consolidated summary: +- Feature name and the folder created under `.claude/tasks/` +- What the PRD captured (problem statement, target platforms, scope boundaries) +- Key technical decisions from the tech spec (state model, data sources, navigation shape, any + new packages needed) +- Task count, phases, and dependency shape +- **Reminder:** the feature is now ready for implementation. Run `/start-job [feature-name]` to + execute the implementation pipeline, or step through manually with `/implement-tasks-sequence`. + +## Rules + +- **Never commit.** This pipeline only produces spec and task files. +- **Never skip sub-skill clarification steps.** The mandatory interactive steps in `prd` and + `techspec` exist to capture requirements accurately. Do not bypass or pre-answer them. +- **Do not edit generated specs.** After `prd` or `techspec` writes a file, do not rewrite it + as the orchestrator. Only the sub-skill owns its output. +- **Stop on hard failure.** If a phase cannot proceed (missing context, the sub-skill reports an + unrecoverable error), stop and surface the issue to the user — do not continue on a broken base. +- **Pass the feature name through.** If an argument was provided, pass it consistently to all + three sub-skills so the folder name stays stable across phases. diff --git a/.claude/commands/spec-feature.md b/.claude/commands/spec-feature.md new file mode 100644 index 0000000..2a0191d --- /dev/null +++ b/.claude/commands/spec-feature.md @@ -0,0 +1,8 @@ +--- +description: Run the full feature specification pipeline (prd → techspec → tasks) end to end. +argument-hint: [feature name] +--- + +Use the `spec-feature` skill to go from a feature idea to implementation-ready tasks in one shot. + +$ARGUMENTS diff --git a/.claude/skills/spec-feature b/.claude/skills/spec-feature new file mode 120000 index 0000000..5db238d --- /dev/null +++ b/.claude/skills/spec-feature @@ -0,0 +1 @@ +../../.agents/skills/spec-feature \ No newline at end of file From 88f1736a3015aa21bd41323cd9b84aa6aeb1e044 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Urb=C3=A1nek?= Date: Thu, 18 Jun 2026 14:59:29 +0200 Subject: [PATCH 3/6] feat: add context.md handoff across prd, techspec, and tasks skills Co-Authored-By: Claude Sonnet 4.6 --- .agents/skills/prd/SKILL.md | 30 ++++++++++++++++++++++++++++++ .agents/skills/tasks/SKILL.md | 24 ++++++++++++++++++++++++ .agents/skills/techspec/SKILL.md | 28 ++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+) diff --git a/.agents/skills/prd/SKILL.md b/.agents/skills/prd/SKILL.md index 7ab7633..e667226 100644 --- a/.agents/skills/prd/SKILL.md +++ b/.agents/skills/prd/SKILL.md @@ -91,6 +91,36 @@ Save the PRD to `.claude/tasks/[feature-name]/prd.md`. Choose a stable kebab-case feature folder name, for example `profile-detail`, `event-check-in`, or `push-notification-settings`. If a matching task folder already exists, ask before overwriting an existing PRD. +### Step 4b: Write context.md + +After saving `prd.md`, write `.claude/tasks/[feature-name]/context.md` with the following +structure (use the actual values from the PRD — do not leave placeholder text): + +```markdown +# [Feature Name] — Spec Context + +## Feature Name +[kebab-case folder name] + +## Problem Statement +[1-2 sentences capturing the core problem and who it affects] + +## Target Platforms +[Android, iOS, web, macOS, Windows, Linux — list only those in scope] + +## Key Product Decisions +- [bullet list of the significant product choices captured in the PRD] + +## Out of Scope +- [bullet list of items explicitly excluded] + +## Open Questions +- [any questions that remain unresolved after the PRD; empty list if none] +``` + +This file accumulates context across the spec pipeline. Later skills (`techspec`, `tasks`) will +read it and append their own sections — do not remove or reformat existing sections. + ### Step 5: Report Results - Confirm the file path diff --git a/.agents/skills/tasks/SKILL.md b/.agents/skills/tasks/SKILL.md index 95df835..2e08895 100644 --- a/.agents/skills/tasks/SKILL.md +++ b/.agents/skills/tasks/SKILL.md @@ -32,6 +32,13 @@ The user does not want to review the task list before implementation. Generate t total count reasonable (aim for cohesive tasks, not micro-tasks), and stop after reporting. The caller (`/start-job` or the user directly) decides what runs next. +### Step 0: Load Shared Context + +Before reading the PRD and tech spec, check whether `.claude/tasks/[feature-name]/context.md` +exists. If it does, read it. Use its contents to skip re-deriving already-captured decisions — +the problem statement, target platforms, architectural choices, and files to create are already +settled. Do not re-ask the user about items already recorded there. + ### Step 1: Analyze PRD and Tech Spec Read both documents and identify: @@ -87,6 +94,23 @@ Each task file should include: `*.freezed.dart`, `*.gr.dart`), reuse from `lib/common/` before adding new primitives - Quality gates — `make gen` clean, `fvm flutter analyze` passes, `fvm flutter test` passes +### Step 4b: Append to context.md + +Append the following section to `.claude/tasks/[feature-name]/context.md` (create the file if +it does not exist). Use actual values — do not leave placeholder text: + +```markdown + +## Tasks (from tasks breakdown) + +### Task Count and Phases +[e.g., "8 tasks across 3 phases: Foundation (3), Core (3), Integration (2)"] + +### Dependency Shape +[one sentence describing the dependency structure, e.g., "All Foundation tasks must complete +before Core tasks begin; Integration tasks depend on all Core tasks."] +``` + ### Step 5: Brief Summary Output a one-paragraph summary (what was generated, how many tasks, dependency shape). Keep it diff --git a/.agents/skills/techspec/SKILL.md b/.agents/skills/techspec/SKILL.md index 85da948..b2fe24b 100644 --- a/.agents/skills/techspec/SKILL.md +++ b/.agents/skills/techspec/SKILL.md @@ -29,6 +29,13 @@ services for backend, and FVM-pinned Flutter SDK. ## Workflow +### Step 0: Load Shared Context + +Before reading the PRD, check whether `.claude/tasks/[feature-name]/context.md` exists. If it +does, read it. Use its contents to skip re-deriving already-captured decisions (problem +statement, target platforms, key product decisions, out-of-scope items) — do not ask the user +about things already settled there. + ### Step 1: Analyze PRD Read the PRD at `.claude/tasks/[feature-name]/prd.md` and extract: @@ -97,6 +104,27 @@ Save to `.claude/tasks/[feature-name]/techspec.md`. - Summarize key architectural decisions (state model, data sources, navigation shape, any new packages needed in `pubspec.yaml`) +### Step 5b: Append to context.md + +Append the following section to `.claude/tasks/[feature-name]/context.md` (create the file if +it does not exist). Use actual values from the tech spec — do not leave placeholder text: + +```markdown + +## Technical Decisions (from techspec) + +### Key Architectural Decisions +- State model: [e.g., AsyncNotifier with UserProfileState freezed union] +- Data sources: [e.g., dio HTTP via UserProfileUseCase, no local cache] +- Navigation: [e.g., new @RoutePage full-screen route, no deep-link] + +### Files to Create +- [brief list of new files, one per line, relative to lib/] + +### New Packages Needed +- [package name and reason, or "none"] +``` + ### Step 6: Final next-step prompt After saving the techspec, end with this guidance: From 17e54b0fc2df57b8c9397af3369081980d29b1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Urb=C3=A1nek?= Date: Thu, 18 Jun 2026 14:59:32 +0200 Subject: [PATCH 4/6] test: add widget tests for landing, authentication, and home features Co-Authored-By: Claude Sonnet 4.6 --- .../authentication_page_content_test.dart | 78 +++++++++++++++++++ .../features/home/home_page_content_test.dart | 41 ++++++++++ .../force_update_page_content_test.dart | 33 ++++++++ 3 files changed, 152 insertions(+) create mode 100644 test/features/authentication/authentication_page_content_test.dart create mode 100644 test/features/home/home_page_content_test.dart create mode 100644 test/features/landing/force_update_page_content_test.dart diff --git a/test/features/authentication/authentication_page_content_test.dart b/test/features/authentication/authentication_page_content_test.dart new file mode 100644 index 0000000..e783e62 --- /dev/null +++ b/test/features/authentication/authentication_page_content_test.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_app/app/configuration/configuration.dart'; +import 'package:flutter_app/app/setup/flavor.dart'; +import 'package:flutter_app/app/theme/app_theme.dart'; +import 'package:flutter_app/assets/app_localizations.gen.dart'; +import 'package:flutter_app/features/authentication/authentication_page_content.dart'; +import 'package:flutter_app/features/authentication/authentication_state.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; + +Widget _wrap(Widget child) { + return MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + theme: AppTheme.getThemeData(brightness: Brightness.light), + home: Scaffold(body: child), + ); +} + +class _StubAuthNotifier extends AuthenticationStateNotifier { + _StubAuthNotifier(this._stub); + + final AuthenticationState _stub; + + @override + FutureOr build() async => _stub; +} + +void main() { + Configuration.setup(flavor: Flavor.develop); + + patrolWidgetTest( + 'AuthenticationPageContent - shows all sign-in buttons', + ($) async { + await $.pumpWidgetAndSettle( + ProviderScope( + overrides: [ + authenticationStateProvider.overrideWith( + () => _StubAuthNotifier( + const AuthenticationState(isGoogleSigningIn: false, isAppleSigningIn: false), + ), + ), + ], + child: _wrap(const AuthenticationPageContent()), + ), + ); + + expect(find.text('Mock Sign In'), findsOneWidget); + expect(find.text('Sign in Anonymously'), findsOneWidget); + expect(find.text('Sign in with Google'), findsOneWidget); + expect(find.text('Sign in with Apple'), findsOneWidget); + }, + ); + + patrolWidgetTest( + 'AuthenticationPageContent - Apple button visible while Google is loading', + ($) async { + await $.tester.pumpWidget( + ProviderScope( + overrides: [ + authenticationStateProvider.overrideWith( + () => _StubAuthNotifier( + const AuthenticationState(isGoogleSigningIn: true, isAppleSigningIn: false), + ), + ), + ], + child: _wrap(const AuthenticationPageContent()), + ), + ); + await $.tester.pump(); + + expect(find.text('Sign in with Apple'), findsOneWidget); + }, + ); +} diff --git a/test/features/home/home_page_content_test.dart b/test/features/home/home_page_content_test.dart new file mode 100644 index 0000000..6a740f1 --- /dev/null +++ b/test/features/home/home_page_content_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_app/app/configuration/configuration.dart'; +import 'package:flutter_app/app/setup/flavor.dart'; +import 'package:flutter_app/app/theme/app_theme.dart'; +import 'package:flutter_app/assets/app_localizations.gen.dart'; +import 'package:flutter_app/features/home/home_page_content.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; + +Widget buildSubject() { + return MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + theme: AppTheme.getThemeData(brightness: Brightness.light), + home: const Scaffold(body: HomePageContent()), + ); +} + +void main() { + Configuration.setup(flavor: Flavor.develop); + + patrolWidgetTest( + 'HomePageContent - shows Home title', + ($) async { + await $.pumpWidgetAndSettle(buildSubject()); + + expect(find.byKey(const Key('home_title')), findsOneWidget); + expect(find.text('Home'), findsOneWidget); + }, + ); + + patrolWidgetTest( + 'HomePageContent - shows debug tools button', + ($) async { + await $.pumpWidgetAndSettle(buildSubject()); + + // Tap not tested here — it requires a router context + expect(find.text('Open debug tools'), findsOneWidget); + }, + ); +} diff --git a/test/features/landing/force_update_page_content_test.dart b/test/features/landing/force_update_page_content_test.dart new file mode 100644 index 0000000..ee3d66c --- /dev/null +++ b/test/features/landing/force_update_page_content_test.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_app/app/configuration/configuration.dart'; +import 'package:flutter_app/app/setup/flavor.dart'; +import 'package:flutter_app/app/theme/app_theme.dart'; +import 'package:flutter_app/assets/app_localizations.gen.dart'; +import 'package:flutter_app/features/landing/force_update_page_content.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:patrol/patrol.dart'; + +Widget buildSubject() { + return ProviderScope( + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + theme: AppTheme.getThemeData(brightness: Brightness.light), + home: const Scaffold(body: ForceUpdatePageContent()), + ), + ); +} + +void main() { + Configuration.setup(flavor: Flavor.develop); + + patrolWidgetTest( + 'ForceUpdatePageContent - shows title and description', + ($) async { + await $.pumpWidgetAndSettle(buildSubject()); + + expect(find.textContaining('update', findRichText: true, skipOffstage: false), findsWidgets); + }, + ); +} From 061c53fe92a9cdbae8240d9635c243d996af40c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Urb=C3=A1nek?= Date: Thu, 18 Jun 2026 15:00:38 +0200 Subject: [PATCH 5/6] fix: address pr-review nits in skills and widget tests Co-Authored-By: Claude Sonnet 4.6 --- .agents/skills/tasks/SKILL.md | 6 +++--- .../authentication/authentication_page_content_test.dart | 2 ++ test/features/landing/force_update_page_content_test.dart | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.agents/skills/tasks/SKILL.md b/.agents/skills/tasks/SKILL.md index 2e08895..f6764ee 100644 --- a/.agents/skills/tasks/SKILL.md +++ b/.agents/skills/tasks/SKILL.md @@ -130,9 +130,9 @@ asking how they want to proceed: Wait for the user's choice. Do NOT auto-run anything. -When invoked by `/start-job`: skip the prompt above and return control to the `/start-job` -orchestrator. The orchestrator must then invoke `implement-tasks-sequence` as Phase 2 without -pausing for user input. +When invoked by `/start-job` or `/spec-feature`: skip the prompt above and return control to the +calling orchestrator. `/start-job` will then invoke `implement-tasks-sequence` as Phase 2; +`/spec-feature` will produce its Final Report. ## Project context diff --git a/test/features/authentication/authentication_page_content_test.dart b/test/features/authentication/authentication_page_content_test.dart index e783e62..3ec83f7 100644 --- a/test/features/authentication/authentication_page_content_test.dart +++ b/test/features/authentication/authentication_page_content_test.dart @@ -58,6 +58,8 @@ void main() { patrolWidgetTest( 'AuthenticationPageContent - Apple button visible while Google is loading', ($) async { + // pumpWidgetAndSettle would time out because isGoogleSigningIn=true keeps + // the loading spinner animating indefinitely — use a single pump instead. await $.tester.pumpWidget( ProviderScope( overrides: [ diff --git a/test/features/landing/force_update_page_content_test.dart b/test/features/landing/force_update_page_content_test.dart index ee3d66c..236f968 100644 --- a/test/features/landing/force_update_page_content_test.dart +++ b/test/features/landing/force_update_page_content_test.dart @@ -27,7 +27,8 @@ void main() { ($) async { await $.pumpWidgetAndSettle(buildSubject()); - expect(find.textContaining('update', findRichText: true, skipOffstage: false), findsWidgets); + expect(find.text('We are better than ever!'), findsOneWidget); + expect(find.textContaining('important update'), findsOneWidget); }, ); } From 6575828eed94709b30d106d54ab191364bdb9dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Urb=C3=A1nek?= Date: Thu, 18 Jun 2026 15:18:50 +0200 Subject: [PATCH 6/6] fix: resolve localized strings from widget tree instead of hardcoding English strings in tests --- test/features/home/home_page_content_test.dart | 3 ++- test/features/landing/force_update_page_content_test.dart | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/test/features/home/home_page_content_test.dart b/test/features/home/home_page_content_test.dart index 6a740f1..66740b8 100644 --- a/test/features/home/home_page_content_test.dart +++ b/test/features/home/home_page_content_test.dart @@ -35,7 +35,8 @@ void main() { await $.pumpWidgetAndSettle(buildSubject()); // Tap not tested here — it requires a router context - expect(find.text('Open debug tools'), findsOneWidget); + final l10n = AppLocalizations.of($.tester.element(find.byType(HomePageContent)))!; + expect(find.text(l10n.featureHomepageOpenDebugTools), findsOneWidget); }, ); } diff --git a/test/features/landing/force_update_page_content_test.dart b/test/features/landing/force_update_page_content_test.dart index 236f968..6a84381 100644 --- a/test/features/landing/force_update_page_content_test.dart +++ b/test/features/landing/force_update_page_content_test.dart @@ -27,8 +27,9 @@ void main() { ($) async { await $.pumpWidgetAndSettle(buildSubject()); - expect(find.text('We are better than ever!'), findsOneWidget); - expect(find.textContaining('important update'), findsOneWidget); + final l10n = AppLocalizations.of($.tester.element(find.byType(ForceUpdatePageContent)))!; + expect(find.text(l10n.forceUpdateTitle), findsOneWidget); + expect(find.text(l10n.forceUpdateDescription), findsOneWidget); }, ); }