1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:io' as io;
7
8import 'package:flutter_devicelab/framework/browser.dart';
9import 'package:shelf/shelf.dart';
10import 'package:shelf/shelf_io.dart' as shelf_io;
11import 'package:shelf_static/shelf_static.dart';
12
13/// Runs Chrome, opens the given `appUrl`, and returns the result reported by the
14/// app.
15///
16/// The app is served from the `appDirectory`. Typically, the app is built
17/// using `flutter build web` and served from `build/web`.
18///
19/// The launched app is expected to report the result by sending an HTTP POST
20/// request to "/test-result" containing result data as plain text body of the
21/// request. This function has no opinion about what that string contains.
22Future<String> evalTestAppInChrome({
23 required String appUrl,
24 required String appDirectory,
25 int serverPort = 8080,
26 int browserDebugPort = 8081,
27}) async {
28 io.HttpServer? server;
29 Chrome? chrome;
30 try {
31 final Completer<String> resultCompleter = Completer<String>();
32 server = await io.HttpServer.bind('localhost', serverPort);
33 final Cascade cascade = Cascade()
34 .add((Request request) async {
35 if (request.requestedUri.path.endsWith('/test-result')) {
36 resultCompleter.complete(await request.readAsString());
37 return Response.ok('Test results received');
38 }
39 return Response.notFound('');
40 })
41 .add(createStaticHandler(appDirectory));
42 shelf_io.serveRequests(server, cascade.handler);
43 final io.Directory userDataDirectory = io.Directory.systemTemp.createTempSync(
44 'flutter_chrome_user_data.',
45 );
46 chrome = await Chrome.launch(
47 ChromeOptions(
48 headless: true,
49 debugPort: browserDebugPort,
50 url: appUrl,
51 userDataDirectory: userDataDirectory.path,
52 windowHeight: 500,
53 windowWidth: 500,
54 ),
55 onError: resultCompleter.completeError,
56 );
57 return await resultCompleter.future;
58 } finally {
59 chrome?.stop();
60 await server?.close();
61 }
62}
63
64typedef ServerRequestListener = void Function(Request);
65
66class AppServer {
67 AppServer._(this._server, this.chrome, this.onChromeError);
68
69 static Future<AppServer> start({
70 required String appUrl,
71 required String appDirectory,
72 required String cacheControl,
73 int serverPort = 8080,
74 int browserDebugPort = 8081,
75 bool headless = true,
76 List<Handler>? additionalRequestHandlers,
77 }) async {
78 io.HttpServer server;
79 Chrome chrome;
80 server = await io.HttpServer.bind('localhost', serverPort);
81 final Handler staticHandler = createStaticHandler(appDirectory, defaultDocument: 'index.html');
82 Cascade cascade = Cascade();
83 if (additionalRequestHandlers != null) {
84 for (final Handler handler in additionalRequestHandlers) {
85 cascade = cascade.add(handler);
86 }
87 }
88 cascade = cascade.add((Request request) async {
89 final Response response = await staticHandler(request);
90 return response.change(headers: <String, Object>{'cache-control': cacheControl});
91 });
92 shelf_io.serveRequests(server, cascade.handler);
93 final io.Directory userDataDirectory = io.Directory.systemTemp.createTempSync(
94 'flutter_chrome_user_data.',
95 );
96 final Completer<String> chromeErrorCompleter = Completer<String>();
97 chrome = await Chrome.launch(
98 ChromeOptions(
99 headless: headless,
100 debugPort: browserDebugPort,
101 url: appUrl,
102 userDataDirectory: userDataDirectory.path,
103 ),
104 onError: chromeErrorCompleter.complete,
105 );
106 return AppServer._(server, chrome, chromeErrorCompleter.future);
107 }
108
109 final Future<String> onChromeError;
110 final io.HttpServer _server;
111 final Chrome chrome;
112
113 Future<void> stop() async {
114 chrome.stop();
115 await _server.close();
116 }
117}
118