devfs_web_test.dart 23.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6 7
import 'dart:io';

8 9 10 11 12
import 'package:dwds/data/build_result.dart';
import 'package:dwds/dwds.dart';
import 'package:dwds/src/loaders/strategy.dart';
import 'package:dwds/src/readers/asset_reader.dart';
import 'package:dwds/src/services/expression_compiler.dart';
13 14
import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart';
15
import 'package:flutter_tools/src/base/platform.dart';
16
import 'package:flutter_tools/src/build_info.dart';
17
import 'package:flutter_tools/src/build_runner/devfs_web.dart';
18
import 'package:flutter_tools/src/compile.dart';
19
import 'package:flutter_tools/src/convert.dart';
20
import 'package:flutter_tools/src/globals.dart' as globals;
21
import 'package:logging/logging.dart';
22
import 'package:mockito/mockito.dart';
23
import 'package:package_config/package_config.dart';
24
import 'package:shelf/shelf.dart';
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

import '../../src/common.dart';
import '../../src/testbed.dart';

const List<int> kTransparentImage = <int>[
  0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
  0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
  0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44,
  0x41, 0x54, 0x78, 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D,
  0x0A, 0x2D, 0xB4, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE,
];

void main() {
  Testbed testbed;
  WebAssetServer webAssetServer;
40
  Platform linux;
41
  PackageConfig packages;
42 43
  Platform windows;
  MockHttpServer mockHttpServer;
44 45

  setUpAll(() async {
46
    packages = await loadPackageConfigUri(Uri.base.resolve('.packages'));
47
  });
48 49

  setUp(() {
50 51 52
    mockHttpServer = MockHttpServer();
    linux = FakePlatform(operatingSystem: 'linux', environment: <String, String>{});
    windows = FakePlatform(operatingSystem: 'windows', environment: <String, String>{});
53
    testbed = Testbed(setup: () {
54 55 56 57 58 59 60
      webAssetServer = WebAssetServer(
        mockHttpServer,
        packages,
        InternetAddress.loopbackIPv4,
        null,
        null,
      );
61 62 63 64
    });
  });

  test('Handles against malformed manifest', () => testbed.run(() async {
65
    final File source = globals.fs.file('source')
66
      ..writeAsStringSync('main() {}');
67
    final File sourcemap = globals.fs.file('sourcemap')
68
      ..writeAsStringSync('{}');
69 70

    // Missing ending offset.
71
    final File manifestMissingOffset = globals.fs.file('manifestA')
72 73 74 75
      ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{
        'code': <int>[0],
        'sourcemap': <int>[0],
      }}));
76
    final File manifestOutOfBounds = globals.fs.file('manifest')
77 78 79 80
      ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{
        'code': <int>[0, 100],
        'sourcemap': <int>[0],
      }}));
81

82 83
    expect(webAssetServer.write(source, manifestMissingOffset, sourcemap), isEmpty);
    expect(webAssetServer.write(source, manifestOutOfBounds, sourcemap), isEmpty);
84 85 86
  }));

  test('serves JavaScript files from in memory cache', () => testbed.run(() async {
87
    final File source = globals.fs.file('source')
88
      ..writeAsStringSync('main() {}');
89
    final File sourcemap = globals.fs.file('sourcemap')
90
      ..writeAsStringSync('{}');
91
    final File manifest = globals.fs.file('manifest')
92 93 94 95 96
      ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{
        'code': <int>[0, source.lengthSync()],
        'sourcemap': <int>[0, 2],
      }}));
    webAssetServer.write(source, manifest, sourcemap);
97

98 99
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
100

101
    expect(response.headers, allOf(<Matcher>[
102 103 104
      containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
      containsPair(HttpHeaders.contentTypeHeader, 'application/javascript'),
      containsPair(HttpHeaders.etagHeader, isNotNull)
105 106
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
107 108 109 110
  }, overrides: <Type, Generator>{
    Platform: () => linux,
  }));

111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
  test('Removes leading slashes for valid requests to avoid requesting outside'
    ' of served directory', () => testbed.run(() async {
    globals.fs.file('foo.png').createSync();
    globals.fs.currentDirectory = globals.fs.directory('project_directory')
      ..createSync();

    final File source = globals.fs.file(globals.fs.path.join('web', 'foo.png'))
      ..createSync(recursive: true)
      ..writeAsBytesSync(kTransparentImage);
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar////foo.png')));

    expect(response.headers, allOf(<Matcher>[
      containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
      containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
      containsPair(HttpHeaders.etagHeader, isNotNull),
      containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
  }));

132
  test('serves JavaScript files from in memory cache not from manifest', () => testbed.run(() async {
133
    webAssetServer.writeFile('foo.js', 'main() {}');
134

135 136
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
137

138
    expect(response.headers, allOf(<Matcher>[
139 140 141 142
      containsPair(HttpHeaders.contentLengthHeader, '9'),
      containsPair(HttpHeaders.contentTypeHeader, 'application/javascript'),
      containsPair(HttpHeaders.etagHeader, isNotNull),
      containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
143 144
    ]));
    expect((await response.read().toList()).first, utf8.encode('main() {}'));
145 146
  }));

147
  test('Returns notModified when the ifNoneMatch header matches the etag', () => testbed.run(() async {
148
    webAssetServer.writeFile('foo.js', 'main() {}');
149 150 151 152 153 154 155 156 157 158 159 160 161 162

    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.js')));
    final String etag = response.headers[HttpHeaders.etagHeader];

    final Response cachedResponse = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.js'), headers: <String, String>{
        HttpHeaders.ifNoneMatchHeader: etag
      }));

    expect(cachedResponse.statusCode, HttpStatus.notModified);
    expect(await cachedResponse.read().toList(), isEmpty);
  }));

163
  test('handles missing JavaScript files from in memory cache', () => testbed.run(() async {
164
    final File source = globals.fs.file('source')
165
      ..writeAsStringSync('main() {}');
166
    final File sourcemap = globals.fs.file('sourcemap')
167
      ..writeAsStringSync('{}');
168
    final File manifest = globals.fs.file('manifest')
169 170 171 172 173
      ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{
        'code': <int>[0, source.lengthSync()],
        'sourcemap': <int>[0, 2],
      }}));
    webAssetServer.write(source, manifest, sourcemap);
174

175 176
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/bar.js')));
177

178
    expect(response.statusCode, HttpStatus.notFound);
179 180
  }));

181 182 183 184 185 186 187 188 189
  test('serves default index.html', () => testbed.run(() async {
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/')));

    expect(response.statusCode, HttpStatus.ok);
    expect((await response.read().toList()).first,
      containsAllInOrder(utf8.encode('<html>')));
  }));

190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
  test('handles web server paths without .lib extension', () => testbed.run(() async {
    final File source = globals.fs.file('source')
      ..writeAsStringSync('main() {}');
    final File sourcemap = globals.fs.file('sourcemap')
      ..writeAsStringSync('{}');
    final File manifest = globals.fs.file('manifest')
      ..writeAsStringSync(json.encode(<String, Object>{'/foo.dart.lib.js': <String, Object>{
        'code': <int>[0, source.lengthSync()],
        'sourcemap': <int>[0, 2],
      }}));
    webAssetServer.write(source, manifest, sourcemap);

    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.dart.js')));

    expect(response.statusCode, HttpStatus.ok);
  }));

208 209 210 211 212 213
  test('serves JavaScript files from in memory cache on Windows', () => testbed.run(() async {
    final File source = globals.fs.file('source')
      ..writeAsStringSync('main() {}');
    final File sourcemap = globals.fs.file('sourcemap')
      ..writeAsStringSync('{}');
    final File manifest = globals.fs.file('manifest')
214
      ..writeAsStringSync(json.encode(<String, Object>{'/foo.js': <String, Object>{
215 216 217 218
        'code': <int>[0, source.lengthSync()],
        'sourcemap': <int>[0, 2],
      }}));
    webAssetServer.write(source, manifest, sourcemap);
219 220 221 222
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://localhost/foo.js')));

    expect(response.headers, allOf(<Matcher>[
223 224 225 226
      containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
      containsPair(HttpHeaders.contentTypeHeader, 'application/javascript'),
      containsPair(HttpHeaders.etagHeader, isNotNull),
      containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
227 228
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
229 230 231 232
  }, overrides: <Type, Generator>{
    Platform: () => windows,
  }));

233 234 235 236 237 238 239 240
   test('serves asset files from in filesystem with url-encoded paths', () => testbed.run(() async {
    final File source = globals.fs.file(globals.fs.path.join('build', 'flutter_assets', Uri.encodeFull('abcd象形字.png')))
      ..createSync(recursive: true)
      ..writeAsBytesSync(kTransparentImage);
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/assets/abcd%25E8%25B1%25A1%25E5%25BD%25A2%25E5%25AD%2597.png')));

    expect(response.headers, allOf(<Matcher>[
241 242 243 244
      containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
      containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
      containsPair(HttpHeaders.etagHeader, isNotNull),
      containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
245 246 247
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
  }));
248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
  test('serves files from web directory', () => testbed.run(() async {
    final File source = globals.fs.file(globals.fs.path.join('web', 'foo.png'))
      ..createSync(recursive: true)
      ..writeAsBytesSync(kTransparentImage);
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.png')));

    expect(response.headers, allOf(<Matcher>[
      containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
      containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
      containsPair(HttpHeaders.etagHeader, isNotNull),
      containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
  }));
263

264 265 266 267
   test('serves asset files from in filesystem with known mime type on Windows', () => testbed.run(() async {
    final File source = globals.fs.file(globals.fs.path.join('build', 'flutter_assets', 'foo.png'))
      ..createSync(recursive: true)
      ..writeAsBytesSync(kTransparentImage);
268 269 270 271
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png')));

    expect(response.headers, allOf(<Matcher>[
272 273 274 275
      containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
      containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
      containsPair(HttpHeaders.etagHeader, isNotNull),
      containsPair(HttpHeaders.cacheControlHeader, 'max-age=0, must-revalidate')
276 277
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
278 279 280 281
  }, overrides: <Type,  Generator>{
    Platform: () => windows,
  }));

282
  test('serves Dart files from in filesystem on Linux/macOS', () => testbed.run(() async {
283
    final File source = globals.fs.file('foo.dart').absolute
284 285 286
      ..createSync(recursive: true)
      ..writeAsStringSync('void main() {}');

287 288
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.dart')));
289

290
    expect(response.headers, containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()));
291
    expect((await response.read().toList()).first, source.readAsBytesSync());
292 293 294 295 296
  }, overrides: <Type,  Generator>{
    Platform: () => linux,
  }));

  test('Handles missing Dart files from filesystem', () => testbed.run(() async {
297 298
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/foo.dart')));
299

300
    expect(response.statusCode, HttpStatus.notFound);
301 302 303
  }));

  test('serves asset files from in filesystem with known mime type', () => testbed.run(() async {
304
    final File source = globals.fs.file(globals.fs.path.join('build', 'flutter_assets', 'foo.png'))
305 306 307
      ..createSync(recursive: true)
      ..writeAsBytesSync(kTransparentImage);

308 309
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo.png')));
310

311
    expect(response.headers, allOf(<Matcher>[
312 313
      containsPair(HttpHeaders.contentLengthHeader, source.lengthSync().toString()),
      containsPair(HttpHeaders.contentTypeHeader, 'image/png'),
314 315
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
316 317 318
  }));

  test('serves asset files files from in filesystem with unknown mime type and length > 12', () => testbed.run(() async {
319
    final File source = globals.fs.file(globals.fs.path.join('build', 'flutter_assets', 'foo'))
320 321 322
      ..createSync(recursive: true)
      ..writeAsBytesSync(List<int>.filled(100, 0));

323 324
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo')));
325

326
    expect(response.headers, allOf(<Matcher>[
327 328
      containsPair(HttpHeaders.contentLengthHeader, '100'),
      containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
329 330
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
331 332 333
  }));

  test('serves asset files files from in filesystem with unknown mime type and length < 12', () => testbed.run(() async {
334
    final File source = globals.fs.file(globals.fs.path.join('build', 'flutter_assets', 'foo'))
335 336 337
      ..createSync(recursive: true)
      ..writeAsBytesSync(<int>[1, 2, 3]);

338 339
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo')));
340

341
    expect(response.headers, allOf(<Matcher>[
342 343
      containsPair(HttpHeaders.contentLengthHeader, '3'),
      containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
344 345
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
346 347
  }));

348 349 350 351 352 353 354 355 356 357 358 359
  test('serves valid etag header for asset files with non-ascii chracters', () => testbed.run(() async {
    globals.fs.file(globals.fs.path.join('build', 'flutter_assets', 'fooπ'))
      ..createSync(recursive: true)
      ..writeAsBytesSync(<int>[1, 2, 3]);

    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/assets/fooπ')));
    final String etag = response.headers[HttpHeaders.etagHeader];

    expect(etag.runes, everyElement(predicate((int char) => char < 255)));
  }));

360
  test('handles serving missing asset file', () => testbed.run(() async {
361 362
    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http://foobar/assets/foo')));
363

364
    expect(response.statusCode, HttpStatus.notFound);
365 366
  }));

367 368 369 370 371 372 373
  test('serves /packages/<package>/<path> files as if they were '
       'package:<package>/<path> uris', () => testbed.run(() async {
    final Uri expectedUri = packages.resolve(
        Uri.parse('package:flutter_tools/foo.dart'));
    final File source = globals.fs.file(globals.fs.path.fromUri(expectedUri))
      ..createSync(recursive: true)
      ..writeAsBytesSync(<int>[1, 2, 3]);
374 375 376 377 378

    final Response response = await webAssetServer
      .handleRequest(Request('GET', Uri.parse('http:///packages/flutter_tools/foo.dart')));

    expect(response.headers, allOf(<Matcher>[
379 380
      containsPair(HttpHeaders.contentLengthHeader, '3'),
      containsPair(HttpHeaders.contentTypeHeader, 'application/octet-stream'),
381 382
    ]));
    expect((await response.read().toList()).first, source.readAsBytesSync());
383 384
  }));

385 386 387 388 389
  test('calling dispose closes the http server', () => testbed.run(() async {
    await webAssetServer.dispose();

    verify(mockHttpServer.close()).called(1);
  }));
390 391

  test('Can start web server with specified assets', () => testbed.run(() async {
392
    globals.fs.file('.packages').writeAsStringSync('\n');
393 394 395 396 397 398 399 400 401 402 403 404
    final File outputFile = globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      ..createSync(recursive: true);
    outputFile.parent.childFile('a.sources').writeAsStringSync('');
    outputFile.parent.childFile('a.json').writeAsStringSync('{}');
    outputFile.parent.childFile('a.map').writeAsStringSync('{}');
    outputFile.parent.childFile('.packages').writeAsStringSync('\n');

    final ResidentCompiler residentCompiler = MockResidentCompiler();
    when(residentCompiler.recompile(
      any,
      any,
      outputPath: anyNamed('outputPath'),
405
      packageConfig: anyNamed('packageConfig'),
406 407 408 409 410 411 412 413 414
    )).thenAnswer((Invocation invocation) async {
      return const CompilerOutput('a', 0, <Uri>[]);
    });

    final WebDevFS webDevFS = WebDevFS(
      hostname: 'localhost',
      port: 0,
      packagesFilePath: '.packages',
      urlTunneller: null,
415
      useSseForDebugProxy: true,
416 417 418 419
      buildMode: BuildMode.debug,
      enableDwds: false,
      entrypoint: Uri.base,
      testMode: true,
420
      expressionCompiler: null,
421
      chromiumLauncher: null,
422 423 424 425
    );
    webDevFS.requireJS.createSync(recursive: true);
    webDevFS.stackTraceMapper.createSync(recursive: true);

426
    final Uri uri = await webDevFS.create();
427 428 429 430 431 432
    webDevFS.webAssetServer.entrypointCacheDirectory = globals.fs.currentDirectory;
    globals.fs.currentDirectory
      .childDirectory('lib')
      .childFile('web_entrypoint.dart')
      ..createSync(recursive: true)
      ..writeAsStringSync('GENERATED');
433 434 435
    webDevFS.webAssetServer.dartSdk
      ..createSync(recursive: true)
      ..writeAsStringSync('HELLO');
436 437 438 439 440 441 442 443 444
    webDevFS.webAssetServer.dartSdkSourcemap
      ..createSync(recursive: true)
      ..writeAsStringSync('THERE');
    webDevFS.webAssetServer.canvasKitDartSdk
      ..createSync(recursive: true)
      ..writeAsStringSync('OL');
    webDevFS.webAssetServer.canvasKitDartSdkSourcemap
      ..createSync(recursive: true)
      ..writeAsStringSync('CHUM');
445 446 447
    webDevFS.webAssetServer.dartSdkSourcemap.createSync(recursive: true);

    await webDevFS.update(
448
      mainUri: globals.fs.file(globals.fs.path.join('lib', 'main.dart')).uri,
449 450 451 452
      generator: residentCompiler,
      trackWidgetCreation: true,
      bundleFirstUpload: true,
      invalidatedFiles: <Uri>[],
453
      packageConfig: PackageConfig.empty,
454 455
    );

456 457 458 459 460 461 462
    expect(webDevFS.webAssetServer.getFile('require.js'), isNotNull);
    expect(webDevFS.webAssetServer.getFile('stack_trace_mapper.js'), isNotNull);
    expect(webDevFS.webAssetServer.getFile('main.dart'), isNotNull);
    expect(webDevFS.webAssetServer.getFile('manifest.json'), isNotNull);
    expect(webDevFS.webAssetServer.getFile('flutter_service_worker.js'), isNotNull);
    expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'HELLO');
    expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'THERE');
463 464 465 466 467

    // Update to the SDK.
    webDevFS.webAssetServer.dartSdk.writeAsStringSync('BELLOW');

    // New SDK should be visible..
468
    expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'BELLOW');
469

470 471
    // Toggle CanvasKit
    webDevFS.webAssetServer.canvasKitRendering = true;
472 473 474 475
    expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js'), 'OL');
    expect(await webDevFS.webAssetServer.dartSourceContents('dart_sdk.js.map'), 'CHUM');

    // Generated entrypoint.
476 477
    expect(await webDevFS.webAssetServer.dartSourceContents('web_entrypoint.dart'),
      contains('GENERATED'));
478

479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
    // served on localhost
    expect(uri, Uri.http('localhost:0', ''));

    await webDevFS.destroy();
  }));

  test('Can start web server with hostname any', () => testbed.run(() async {
    globals.fs.file('.packages').writeAsStringSync('\n');
    final File outputFile = globals.fs.file(globals.fs.path.join('lib', 'main.dart'))
      ..createSync(recursive: true);
    outputFile.parent.childFile('a.sources').writeAsStringSync('');
    outputFile.parent.childFile('a.json').writeAsStringSync('{}');
    outputFile.parent.childFile('a.map').writeAsStringSync('{}');
    outputFile.parent.childFile('.packages').writeAsStringSync('\n');

    final ResidentCompiler residentCompiler = MockResidentCompiler();
    when(residentCompiler.recompile(
      any,
      any,
      outputPath: anyNamed('outputPath'),
499
      packageConfig: anyNamed('packageConfig'),
500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
    )).thenAnswer((Invocation invocation) async {
      return const CompilerOutput('a', 0, <Uri>[]);
    });

    final WebDevFS webDevFS = WebDevFS(
      hostname: 'any',
      port: 0,
      packagesFilePath: '.packages',
      urlTunneller: null,
      useSseForDebugProxy: true,
      buildMode: BuildMode.debug,
      enableDwds: false,
      entrypoint: Uri.base,
      testMode: true,
      expressionCompiler: null,
515
      chromiumLauncher: null,
516 517 518 519 520 521 522
    );
    webDevFS.requireJS.createSync(recursive: true);
    webDevFS.stackTraceMapper.createSync(recursive: true);

    final Uri uri = await webDevFS.create();

    expect(uri, Uri.http('localhost:0', ''));
523 524
    await webDevFS.destroy();
  }));
525 526

  test('Launches DWDS with the correct arguments', () => testbed.run(() async {
527
    globals.fs.file('.packages').writeAsStringSync('\n');
528
    final WebAssetServer server = await WebAssetServer.start(
529
      null,
530
      'any',
531 532
      8123,
      (String url) => null,
533
      true,
534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556
      BuildMode.debug,
      true,
      Uri.file('test.dart'),
      null,
      dwdsLauncher: ({
        AssetReader assetReader,
        Stream<BuildResult> buildResults,
        ConnectionProvider chromeConnection,
        bool enableDebugExtension,
        bool enableDebugging,
        ExpressionCompiler expressionCompiler,
        String hostname,
        LoadStrategy loadStrategy,
        void Function(Level, String) logWriter,
        bool serveDevTools,
        UrlEncoder urlEncoder,
        bool useSseForDebugProxy,
        bool verbose,
      }) async {
        expect(serveDevTools, false);
        expect(verbose, null);
        expect(enableDebugging, true);
        expect(enableDebugExtension, true);
557
        expect(useSseForDebugProxy, true);
558
        expect(hostname, 'any');
559 560 561 562

        return MockDwds();
      });

563
    await server.dispose();
564
  }));
565 566 567
}

class MockHttpServer extends Mock implements HttpServer {}
568
class MockResidentCompiler extends Mock implements ResidentCompiler {}
569
class MockDwds extends Mock implements Dwds {}