| name | flutter-driver-e2e |
| description | Use when writing Flutter integration/E2E tests or driving running simulators โ Playwright-style patterns built on flutter_driver + mcp__dart tools. |
Flutter Driver E2E (Playwright-style for simulators)
์๋ฎฌ๋ ์ดํฐ/๋๋ฐ์ด์ค์์ ๋๋ Flutter ์ฑ์ ์ธ๋ถ์์ ์ ์ดํ๋ E2E ์๋ํ ๋ ์ด์ด.
web Playwright์ idiom์ Flutter์ ์ด์ํ๋, ํต์ฌ ์์ง์ flutter_driver + mcp__dart ํด.
When to use
| ์ํฉ | ๋๊ตฌ |
|---|
| ๋จ์ผ ํจ์ / pure logic | test + mocktail (unit) |
| ๋จ์ผ ์์ ฏ / ์์ ํธ๋ฆฌ | testWidgets + pumpWidget (widget) |
| ์ค์ ์ฑ ๋ฐํ์, ๋ฉํฐ ํ๋ฉด ํ๋ก์ฐ, OS ๋ ๋ฒจ ์ ์ค์ฒ, ๋คํธ์ํฌ ํฌํจ ์๋๋ฆฌ์ค | flutter_driver + mcp__dart (this skill) |
| CI ๋ณ๋ ฌ ร ์คํฌ๋ฆฐ์ท ํ๊ท | integration_test/ ํจํค์ง (flutter_driver ์์ ํธํ) |
Rule of thumb: "์ฑ์ ์ผ ๋ค ์ฌ๋์ฒ๋ผ ํ๋ฆ์ ๋ฐ๋ผ๊ฐ์ผ" ํ๋ฉด ์ด ์คํฌ. ๊ทธ ์ธ์ widget test๊ฐ ๋ ๋น ๋ฅด๊ณ ์์ ์ .
์ ํ์ ๋งคํ (Playwright โ flutter_driver)
| Playwright | Flutter Finder | ์ธ์ |
|---|
page.locator('#login-btn') | find.byKey(Key('login-btn')) | 1์์. ๋ชจ๋ interactive ์์ ฏ์ Key ๋ถ์ฌ |
page.getByText('๋ก๊ทธ์ธ') | find.text('๋ก๊ทธ์ธ') | ๋ค๊ตญ์ด ํ
์คํธ ์ฃผ์ โ Key ์ฐ์ |
page.getByRole('button') | find.byType(ElevatedButton) | ํ์
๋ชจํธํ ๋ ๋ถ๋ชจ Key๋ก scope |
page.getByTestId('foo') | find.byValueKey('foo') (driver) | driver extension์ ValueKey ๋งค์นญ |
page.locator('form >> #email') | find.descendant(of: ..., matching: ...) | ancestor/descendant ์กฐํฉ |
์์น: Playwright์์ data-testid ์ฐ๋ฏ Flutter์์ Key('semantic-id')๋ฅผ ์์ ฏ์ ๋ฐ์๋ผ. find.text๋ ๋ก์ผ์ผยทcopy ๋ณ๊ฒฝ์ ์ทจ์ฝ.
Wait / Settle idiom
Playwright๋ ๋๋ถ๋ถ auto-wait. Flutter๋ ๋ช
์์ ์ผ๋ก pumpํด์ผ ํจ.
| Playwright | Flutter ๋ฑ๊ฐ | ์ฃผ์ |
|---|
| auto-wait on action | tester.pumpAndSettle() (widget test) / driver.waitUntilNoTransientCallbacks() (driver) | ์ ๋๋ฉ์ด์
/Future๊ฐ ๋ชจ๋ ๋๋ ๋๊น์ง |
page.waitForSelector | driver.waitFor(finder, timeout: ...) | ์กฐ๊ฑด ๋ง์กฑ๊น์ง ํด๋ง |
expect(locator).toBeVisible() | driver.waitFor(finder).then(...) ๋๋ expectLater(find.X, findsOneWidget) | ํ๋ ์ ๋จ์ vs wallclock ๊ตฌ๋ถ |
page.waitForLoadState('networkidle') | Flutter์ ์์ โ ์ฑ์์ ๋ก๋ฉ ๋ ์ํ๋ฅผ Key๋ก ๋
ธ์ถ (e.g. Key('home-loaded')) ํ waitFor | ๋คํธ์ํฌ idle์ ์ง์ ์ ํธํ |
page.waitForTimeout(ms) | Future.delayed | ์ํฐํจํด. ์กฐ๊ฑด ๋๊ธฐ๋ก ๋ฐ๊ฟ๋ผ |
pumpAndSettle ํจ์ : ๋ฌดํ ์ ๋๋ฉ์ด์
(shimmer, pulse)์ด ์์ผ๋ฉด hang. ์กฐ๊ฑด waitFor๋ก ๊ฐ์ํ๊ฑฐ๋ ์ ๋๋ฉ์ด์
disable ํ๋๊ทธ.
mcp__dart ํด ์ค์ผ์คํธ๋ ์ด์
ํ๋ค์ค๊ฐ ์ ๊ณตํ๋ mcp__dart MCP๊ฐ ์ค์ ์๋ฎฌ๋ ์ดํฐ ์ ์ด๊ถ์ ์ฅ๊ณ ์์. ์ ํ์ ์ฒด์ธ:
1. list_devices โ ์คํ ๊ฐ๋ฅํ ์๋ฎฌ๋ ์ดํฐ ํ์ธ
2. launch_app(path, device) โ ์ฑ ์คํ + VM service attach
3. set_widget_selection_mode โ DevTools ์
๋ ์
ํ์ฑํ (๋๋ฒ๊ทธ์ฉ)
4. get_widget_tree โ ํ์ฌ ํ๋ฉด ์์ ฏ ๋คํ (selector ๊ฒ์ฆ)
5. flutter_driver({action}) โ ์ค์ tap/enter/scroll
6. get_runtime_errors โ ์ํธ์์ฉ ํ ์๋ฌ ์ฆ์ ์์ง
7. get_app_logs(since=...) โ print/logger ์ถ๋ ฅ ์์ง
8. hot_reload โ ๋น ๋ฅธ ๋ฐ๋ณต (์ฝ๋ ์์ ํ)
9. stop_app โ ์ ๋ฆฌ
์์น: ์คํฌ๋ฆฝํธ ํ ์ค๋ก flutter test ๋๋ฆฌ์ง ๋ง๊ณ , ๋จ๊ณ๋ณ MCP ํด ํธ์ถ๋ก ๊ด์ฐฐ + ํ์ + ๋ฆฌ์ปค๋ฒ๋ฆฌ๋ฅผ ์ธ์
์์์ ํด๋ผ. ์๋ฌ๊ฐ ๋๋ฉด ์ฆ์ get_runtime_errors๋ก ์์ธ ํ์ธ โ selector ๊ต์ฒด ๋๋ waitFor ์ถ๊ฐ.
Canonical E2E flow (๋ก๊ทธ์ธ โ ํ โ ์์ธ)
// integration_test/login_flow_e2e.dart
import 'package:integration_test/integration_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
import 'package:myapp/main.dart' as app;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('login โ home โ detail ํ๋ก์ฐ', (tester) async {
// Arrange: ์ฑ ๋ถํ
app.main();
await tester.pumpAndSettle();
// Act 1: ๋ก๊ทธ์ธ
await tester.enterText(find.byKey(const Key('login-email')), 'test@x.com');
await tester.enterText(find.byKey(const Key('login-password')), 'pw1234');
await tester.tap(find.byKey(const Key('login-submit')));
await tester.pumpAndSettle(const Duration(seconds: 3));
// Assert 1: ํ ๋๋ฌ
expect(find.byKey(const Key('home-loaded')), findsOneWidget);
// Act 2: ์ฒซ ์์ดํ
ํญ
await tester.tap(find.byKey(const Key('item-0')));
await tester.pumpAndSettle();
// Assert 2: ์์ธ ๋๋ฌ
expect(find.byKey(const Key('detail-screen')), findsOneWidget);
});
}
์คํ:
flutter test integration_test/login_flow_e2e.dart -d <device-id>
๋๋ MCP ์ค์ผ์คํธ๋ ์ด์
(๋ํํ ๋๋ฒ๊น
):
mcp__dart__list_devices
mcp__dart__launch_app(path="client", device="<id>")
mcp__dart__get_widget_tree # ํ์ฌ ํ๋ฉด ๊ตฌ์กฐ ํ์ธ
mcp__dart__flutter_driver(...) # ๋จ๊ณ๋ณ ์ํธ์์ฉ
mcp__dart__get_runtime_errors # ๋จ๊ณ๋ง๋ค ์๋ฌ ๊ฒ์ฆ
Key ๊ท์น (์ฑ๊ณต์ 90%)
์ ๊ท ์์ ฏ ์์ฑ ์ interactive + state-bearing ์์ ฏ์ ๋ฌด์กฐ๊ฑด Key:
// 1. Form ํ๋
TextField(key: Key('login-email'), ...)
// 2. ๋ฒํผ
ElevatedButton(key: Key('login-submit'), onPressed: ..., ...)
// 3. ๋ฆฌ์คํธ ์์ดํ
(์ธ๋ฑ์ค ๊ธฐ๋ฐ)
ListView.builder(
itemBuilder: (_, i) => ItemCard(key: Key('item-$i'), ...),
)
// 4. ๋ก๋ฉ/์๋ฌ/์ฑ๊ณต ์ํ ๋ถ๊ธฐ ํ๋ฉด
if (loaded) Column(key: const Key('home-loaded'), ...)
else if (error) Column(key: const Key('home-error'), ...)
else Column(key: const Key('home-loading'), ...)
Key ๋ค์ด๋ฐ ์ปจ๋ฒค์
: {screen}-{role} (login-submit, home-loaded, item-$index).
ํ๋ฉด ๋จ์ ์ ๋์ด ์๋ submit, button ๊ฐ์ ์ด๋ฆ ๊ธ์ง โ ๋ค๋ฅธ ํ๋ฉด์์ ์ถฉ๋.
๋๋ฒ๊น
ํ๋ ์ด๋ถ
| ์ฆ์ | ์ฒซ ์ก์
|
|---|
Finder found zero widgets | mcp__dart__get_widget_tree๋ก ์ค์ ํธ๋ฆฌ ๋คํ โ Key ์กด์ฌ ํ์ธ |
pumpAndSettle timed out after 10s | ๋ฌดํ ์ ๋๋ฉ์ด์
์์ฌ โ ๊ตฌ์ฒด์ waitFor(finder) ๋ก ๊ต์ฒด |
| Tap ํ ํ๋ฉด ์ ํ ์ ๋จ | get_runtime_errors + get_app_logs ๋์์ ์์ง โ ์์ธ๊ฐ silently ๋จนํ๋ ์ค์ผ ์ ์์ |
| CI์์ ์คํจ, ๋ก์ปฌ์ ์ฑ๊ณต | ํ์ด๋ฐ ์ด์ 99%. pumpAndSettle(Duration(seconds: 5)) ๋์ ์ํ Key ๊ธฐ๋ฐ waitFor |
| ํ
์คํธ ๋งค์นญ ์คํจ | ๋ก์ผ์ผยท๊ณต๋ฐฑยทinvisible char. find.byKey๋ก ์ด์ |
Anti-patterns
Future.delayed + pump ์กฐํฉ์ผ๋ก ํ์ด๋ฐ ๋ง์ถ๊ธฐ โ CI์์ ๊นจ์ง
find.text('๋ฒํผ')์ผ๋ก๋ง ์กฐํ โ ๋ค๊ตญ์ด/์นดํผ ๋ณ๊ฒฝ์ ์ทจ์ฝ
- ํ ํ
์คํธ์์ 5๊ฐ ์ด์ ํ๋ฉด ์ด๋ โ ์คํจ ์ ์์ธ ๊ท๋ช
๋ถ๊ฐ, ์ชผ๊ฐ๋ผ
tester.tap ํ pump (ํ ํ๋ ์๋ง) โ ๋ค๋น๊ฒ์ด์
์ pumpAndSettle ํ์
- ํ
์คํธ์์ ์ค์ API ํธ์ถ โ mock / fake repository๋ก ๊ฒฉ๋ฆฌ
CI ํตํฉ
- name: E2E (iOS simulator)
run: |
xcrun simctl boot "iPhone 15"
flutter test integration_test/ -d "iPhone 15"
- ์คํฌ๋ฆฐ์ท ํ๊ท:
integration_test์ binding.takeScreenshot(name) ์ฌ์ฉ
- ์คํจ ์ํฐํฉํธ: ๋ก๊ทธ + ์คํฌ๋ฆฐ์ท + ์์ ฏ ํธ๋ฆฌ ๋คํ ์
๋ก๋ (์์ธ ๊ท๋ช
์ ์ ๋ถ)
์ฐ๊ด
- ์ E2E ํ
์คํธ ์์ฑ โ tdd-workflow ์คํฌ ๋จผ์ ์ ์ฉ (Red-Green-Refactor)
- ์ ๋๋ฉ์ด์
heavy ํ๋ฉด E2E โ mobile-touch ์ฐธ์กฐ (๋ฌดํ ์ ๋ disable ํ๋๊ทธ)
- CI/๋ฆด๋ฆฌ์ฆ ์ฐ๋ โ deployment-patterns ์ฐธ์กฐ