Files
workavia-mail-front/docs/adr/0081-patrol-web-test-architecture.md
T

175 lines
6.5 KiB
Markdown

# 81. Patrol Web Integration Test — Architecture & Test Authoring
Date: 2026-04-15
## Status
Proposed
## Related ADRs
- [ADR-0053](./0053-patrol-integration-test.md) — Patrol for mobile integration testing (foundation)
- [ADR-0080](./0080-patrol-web-integration-test-setup.md) — Web test setup & execution
- [ADR-0082](./0082-patrol-web-test-migration-guide.md) — Migration guide & implementation example
- [ADR-0083](./0083-patrol-web-test-migration-plan.md) — PR-by-PR migration plan
## Context
[ADR-0053](./0053-patrol-integration-test.md) established a three-layer architecture — **Test → Scenario → Robot** — for mobile. Extending to web with the POC approach revealed three problems:
- **Robot classes mix platform logic** — `ComposerRobot` has both `addContent()` (mobile) and `addContentWeb()` (web), violating SRP.
- **Scenarios are duplicated** — same business flow written twice for each platform, violating DRY.
- **Closed to extension** — adding a new platform requires modifying existing robot classes, violating OCP.
## Decision
Apply **Abstract Factory** + **Interface Segregation** to separate:
- **WHAT** — Business flow (Scenario): shared across all platforms.
- **HOW** — UI interaction (Robot): implemented per platform.
Platform selection uses **Dart conditional exports** — no logic class ever contains a platform `if/else` branch.
### Architecture
```
Test File (one file per scenario)
TestBase.runPatrolTest()
├── auto-applies platform tag (web / mobile)
└── calls createRobotFactory($)
↑ resolved at compile time via conditional export
├── MobileRobotFactory (default)
└── WebRobotFactory (dart.library.html)
│ injects into
BaseTestScenario(PatrolIntegrationTester $, RobotFactory robots)
│ uses
Abstract Robot Interfaces ← Scenarios depend only on these
├── AbstractComposerRobot
└── AbstractThreadRobot
│ implemented by
Concrete Robots
├── mobile/MobileComposerRobot
└── web/WebComposerRobot
```
`TestBase` is the only class that calls `createRobotFactory()`. Scenarios and test files never reference any platform-specific class.
### Folder Structure
```
integration_test/
├── robots/
│ ├── abstract/ # Interfaces — one per feature area
│ ├── mobile/ # Mobile implementations
│ └── web/ # Web implementations (only where UI differs)
├── factories/
│ ├── robot_factory.dart
│ ├── robot_factory_provider.dart # Conditional export — never changes
│ ├── mobile_robot_factory.dart
│ ├── mobile_robot_factory_provider.dart
│ ├── web_robot_factory.dart
│ └── web_robot_factory_provider.dart
├── scenarios/ # All shared — no web_/mobile_ prefix
└── tests/ # One file per scenario — no platform subfolders
```
### Key Design Decisions
**1. Conditional export — the only platform-branching point**
```dart
// factories/robot_factory_provider.dart ← this file never changes
export 'mobile_robot_factory_provider.dart'
if (dart.library.html) 'web_robot_factory_provider.dart';
// Each provider exports one function with the same name
RobotFactory createRobotFactory(PatrolIntegrationTester $) => WebRobotFactory($);
```
Adding a new platform: create a provider file + factory + robot implementations. **No existing file is modified.**
**2. TestBase resolves and injects the factory**
```dart
// base/test_base.dart
import '../factories/robot_factory_provider.dart';
void runPatrolTest({
required String description,
required BaseTestScenario Function(PatrolIntegrationTester $, RobotFactory robots) scenarioBuilder,
List<TestTags> tags = const [],
}) {
final resolvedTags = [
...tags.map((t) => t.name),
];
patrolTest(description, tags: resolvedTags, ($) async {
await setupTest();
await scenarioBuilder($, createRobotFactory($)).execute();
});
}
```
**3. Scenario receives factory via DI — zero platform knowledge**
```dart
abstract class BaseTestScenario extends BaseScenario {
final RobotFactory robots;
const BaseTestScenario(super.$, this.robots);
@override
Future<void> execute() async {
await robots.loginRobot().login(...);
await runTestLogic();
}
}
```
**4. Test file — one file for all platforms**
```dart
void main() {
TestBase().runPatrolTest(
description: 'Send email',
scenarioBuilder: ($, robots) => SendEmailScenario($, robots),
);
}
```
**5. Robot reuse — when behavior is identical, both factories return the same class**
```dart
// WebRobotFactory — reuse mobile robot when no UI difference
@override AbstractThreadRobot threadRobot() => MobileThreadRobot($);
```
### How to Add a Test for a New Feature
1. Add `UiKey`s to all platform widget variants in `lib/features/base/model/ui_keys.dart`.
2. Define an abstract robot interface in `robots/abstract/` if one does not exist for the feature area.
3. Implement the robot in `robots/mobile/` and `robots/web/`. Use a shared base class for methods identical across platforms. Reuse the same class in both factories if behavior is entirely identical.
4. Register the robot in `MobileRobotFactory` and `WebRobotFactory`.
5. Write one scenario in `scenarios/` using abstract robots only.
6. Write one test file in `tests/`: `scenarioBuilder: ($, robots) => MyScenario($, robots)`.
### SOLID Alignment
| Principle | How it applies |
|-----------|---------------|
| **S** | Robot: UI interaction. Scenario: business flow. Factory: object creation. `TestBase`: lifecycle + wiring. |
| **O** | New platform: add provider + factory + robots. No existing class modified. |
| **L** | `WebRobotFactory` and `MobileRobotFactory` are substitutable through `RobotFactory`. |
| **I** | Each robot interface is scoped to its feature area. Split `RobotFactory` if it exceeds ~15 methods. |
| **D** | Scenarios depend on `RobotFactory` (abstraction). `TestBase` imports `createRobotFactory` from provider. |
## Consequences
- Scenarios and test files are written exactly once — no platform variants.
- No logic class contains a platform `if/else` — platform branching is a Dart export declaration.
- Adding a new platform touches zero existing files.
- Platform tags are auto-applied by `TestBase`.
- Migration is a one-time breaking change — see [ADR-0082](./0082-patrol-web-test-migration-guide.md) and [ADR-0083](./0083-patrol-web-test-migration-plan.md).