blob: 1d97bd90195424e3536423e2e6fff709fbe73038 [file] [log] [blame]
// Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'package:build/build.dart';
import 'package:glob/glob.dart';
import 'package:html/dom.dart';
import 'package:html/parser.dart';
import 'package:path/path.dart' as p;
const _inputExtension = '_test.dart';
const _outputExtension = '_test.debug.html';
/// Returns the (optional) user-provided HTML file to use as an input.
///
/// For example, for `test/foo_test.dart`, we look for `test/foo_test.html`.
AssetId _customHtmlId(AssetId test) => test.changeExtension('.html');
/// Returns the builder-generated HTML file for browsers to navigate to.
AssetId _debugHtmlId(AssetId test) => test.changeExtension('.debug.html');
/// Returns the JS script path for the browser for [dartTest].
String _jsScriptPath(AssetId dartTest) => '${p.url.basename(dartTest.path)}.js';
/// Generates a `*.debug.html` for every file in `test/**/*_test.dart`.
///
/// This is normally used in order to use Chrome (or another browser's)
/// debugging tools (such as setting breakpoints) while running a test suite.
class DebugTestBuilder implements Builder {
/// Generates a `.debug.html` for the provided `._test.dart` file.
static Future<void> _generateDebugHtml(
BuildStep buildStep,
AssetId dartTest,
) async {
final customHtmlId = _customHtmlId(dartTest);
final jsScriptPath = _jsScriptPath(buildStep.inputId);
String debugHtml;
if (await buildStep.canRead(customHtmlId)) {
debugHtml = _replaceCustomHtml(
await buildStep.readAsString(customHtmlId),
jsScriptPath,
);
} else {
debugHtml = _createDebugHtml(jsScriptPath);
}
return buildStep.writeAsString(_debugHtmlId(dartTest), debugHtml);
}
/// Returns the content of [customHtml] modified to work with this package.
static String _replaceCustomHtml(String customHtml, String jsScriptPath) {
final document = parse(customHtml);
// Replace <link rel="x-dart-test"> with <script src="{jsScriptPath}">.
final linkTag = document.querySelector('link[rel="x-dart-test"]');
final scriptTag = Element.tag('script');
scriptTag.attributes['src'] = jsScriptPath;
linkTag.replaceWith(scriptTag);
// Remove the <script src="packages/test/dart.js"></script> if present.
document.querySelector('script[src="packages/test/dart.js"]')?.remove();
return document.outerHtml;
}
static String _createDebugHtml(String jsScriptPath) => ''
'<html>\n'
' <head>\n'
' <script src="$jsScriptPath"></script>\n'
' </head>\n'
'</html>\n';
const DebugTestBuilder();
@override
final buildExtensions = const {
_inputExtension: [_outputExtension],
};
@override
Future<void> build(BuildStep buildStep) {
return _generateDebugHtml(buildStep, buildStep.inputId);
}
}
/// Generates `text/index.html`, useful for navigating and running tests.
class DebugIndexBuilder implements Builder {
static final _allTests = Glob(p.url.join('test', '**$_inputExtension'));
static AssetId _outputFor(BuildStep buildStep) {
return AssetId(buildStep.inputId.package, p.url.join('test', 'index.html'));
}
static String _generateHtml(Iterable<AssetId> tests) {
if (tests.isEmpty) {
return '<strong>No tests found!</strong>';
}
final buffer = StringBuffer(' <ul>');
for (final test in tests) {
final path =
p.url.joinAll(p.url.split(_debugHtmlId(test).path)..removeAt(0));
buffer.writeln(' <li><a href="/$path">${test.path}</a></li>');
}
buffer.writeln(' </ul>');
return buffer.toString();
}
const DebugIndexBuilder();
@override
final buildExtensions = const {
r'$test$': ['index.html'],
};
@override
Future<void> build(BuildStep buildStep) async {
final files = await buildStep.findAssets(_allTests).toList();
final output = _outputFor(buildStep);
return buildStep.writeAsString(
output,
'<html>\n'
' <body>\n'
' ${_generateHtml(files)}\n'
' </body>\n'
'</html>');
}
}