클라이언트 강좌 4편에서 화면이 도착했다. ui://desk 를 읽으면 546바이트가 왔고, 클라이언트는 그걸 그냥 문자열로 받아서 길이만 세고 끝냈다.
이번 트랙은 그 문자열이 그림이 되는 쪽이다.
네 줄
{ "type": "page", "title": "Step 1",
"content": { "type": "center",
"child": { "type": "text", "text": "The whole screen is this file",
"style": { "fontSize": 34, "fontWeight": "bold", "color": "#111827" } } } }
233바이트. 이게 화면 전부다. 노드가 셋 있다.
page— 한 화면. 제목을 가진다.center— 자식 하나를 가운데에 둔다.text— 글자.
트리다. page 안에 content 가 하나, center 안에 child 가 하나. 여러 개를 담는 노드는 2편에서 나온다.
이걸 그리는 쪽
final rt = MCPUIRuntime(enableDebugMode: false);
// screen = jsonDecode(파일 내용)
await tester.runAsync(() => rt.initialize(screen));
await tester.pumpWidget(RepaintBoundary(
key: key,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Builder(
builder: (c) => rt.buildUI(
context: c,
onToolCall: (tool, params) async => fired.add(tool),
),
),
),
));
initialize 에 맵을 넘기고, buildUI 가 위젯을 돌려준다. 그 사이에 화면에 대한 판단은 없다.
이 호스트가 테스트 안에서 도는 건 캡처를 뜨기 위해서다. 앱이라면 pumpWidget 자리에 runApp 이 온다 — 위아래 두 줄은 그대로다.
onToolCall 은 4편에서 쓴다. 지금은 이름을 받아 적기만 한다.
무엇이 없는지가 요점이다
이 트랙의 호스트는 host/test/capture_test.dart 파일 하나다. 다섯 편이 끝날 때까지 이 파일은 거의 안 바뀐다 — 다섯 화면 전부 같은 호스트가 그린다.
그래서 이 강좌에서 가장 중요한 검사는 "있는지"가 아니라 "없는지" 다.
# 화면에 보이는 글자가 프로그램 안에 있으면 실패한다.
grep -rq 'The whole screen is this file' $SRC && \
die "step1: the words on the screen are inside the host"
# 위젯을 손으로 짓고 있으면 실패한다.
grep -rqE '\bText\(|\bColumn\(|\bRow\(|\bElevatedButton\(' $SRC && \
die "step1: the host builds widgets by hand"
$SRC 는 host/test host/pubspec.yaml 이다. host/build 는 뺐다 — 컴파일 산출물에 문자열이 그대로 박혀 있어서, 거기까지 훑으면 이 검사가 영원히 통과한다. 실제로 4편 검사를 처음 붙였을 때 그 이유로 잘못 걸렸다.