blob: a0a6283b98db612a4281fe2af35903fe7b232bcf [file] [log] [blame]
// Copyright (c) 2020, 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 'package:analyzer/file_system/file_system.dart';
import 'package:dartdoc/src/dartdoc_options.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/warnings.dart';
import 'package:test/test.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';
import 'dartdoc_test_base.dart';
import 'src/utils.dart' as utils;
void main() {
defineReflectiveSuite(() {
defineReflectiveTests(DocumentationCommentTest);
});
}
@reflectiveTest
class DocumentationCommentTest extends DartdocTestBase {
@override
String get libraryName => 'my_library';
late Folder projectRoot;
late PackageGraph packageGraph;
late ModelElement libraryModel;
void expectNoWarnings() {
expect(packageGraph.packageWarningCounter.countedWarnings, isEmpty);
expect(packageGraph.packageWarningCounter.hasWarnings, isFalse);
}
Future<void> writePackageWithCommentedLibraries(
List<(String, String)> filesAndComments, {
List<String> additionalArguments = const [],
}) async {
projectRoot = utils.writePackage(
'my_package', resourceProvider, packageConfigProvider);
projectRoot
.getChildAssumingFile('dartdoc_options.yaml')
.writeAsStringSync('''
dartdoc:
warnings:
- missing-code-block-language
''');
for (var (fileName, comment) in filesAndComments) {
projectRoot
.getChildAssumingFolder('lib')
.getChildAssumingFile(fileName)
.writeAsStringSync('$comment\n'
'library;');
}
var optionSet = DartdocOptionRoot.fromOptionGenerators(
'dartdoc', [createDartdocOptions], packageMetaProvider);
optionSet.parseArguments([]);
packageGraph = await utils.bootBasicPackage(
projectRoot.path, packageMetaProvider, packageConfigProvider,
additionalArguments: additionalArguments);
libraryModel = packageGraph.defaultPackage.libraries.first;
}
Future<void> writePackageWithCommentedLibrary(
String comment, {
List<String> additionalArguments = const [],
}) =>
writePackageWithCommentedLibraries([('a.dart', comment)],
additionalArguments: additionalArguments);
void test_removesTripleSlashes() async {
await writePackageWithCommentedLibrary('''
/// Text.
/// More text.
''');
var doc = libraryModel.documentation;
expect(doc, equals('''
Text.
More text.'''));
}
void test_removesSpaceAfterTripleSlashes() async {
await writePackageWithCommentedLibrary('''
/// Text.
/// More text.
''');
var doc = libraryModel.documentation;
// TODO(srawlins): Actually, the three spaces before 'More' is perhaps not
// the best fit. Should it only be two, to match the indent from the first
// line's "Text"?
expect(doc, equals('''
Text.
More text.'''));
}
void test_leavesBlankLines() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// More text.
''');
var doc = libraryModel.documentation;
expect(doc, equals('''
Text.
More text.'''));
}
void test_processesAnimationDirective() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, equals('''
Text.
<div style="position: relative;">
<div id="barHerderAnimation_play_button_"
onclick="var barHerderAnimation = document.getElementById('barHerderAnimation');
if (barHerderAnimation.paused) {
barHerderAnimation.play();
this.style.display = 'none';
} else {
barHerderAnimation.pause();
this.style.display = 'block';
}"
style="position:absolute;
width:100px;
height:200px;
z-index:100000;
background-position: center;
background-repeat: no-repeat;
background-image: url(static-assets/play_button.svg);">
</div>
<video id="barHerderAnimation"
style="width:100px; height:200px;"
onclick="var barHerderAnimation_play_button_ = document.getElementById('barHerderAnimation_play_button_');
if (this.paused) {
this.play();
barHerderAnimation_play_button_.style.display = 'none';
} else {
this.pause();
barHerderAnimation_play_button_.style.display = 'block';
}" loop>
<source src="http://host/path/to/video.mp4" type="video/mp4"/>
</video>
</div>
End text.'''));
}
void test_rendersUnnamedAnimation() async {
await writePackageWithCommentedLibrary('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, contains('<video id="animation_1"'));
}
void test_rendersNamedAnimation() async {
await writePackageWithCommentedLibrary('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=namedAnimation}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, contains('<video id="namedAnimation"'));
}
void test_rendersNamedAnimation_outOfOrder() async {
await writePackageWithCommentedLibrary('''
/// First line.
///
/// {@animation 100 200 id=namedAnimation http://host/path/to/video.mp4}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, contains('<video id="namedAnimation"'));
}
void test_rendersNamedAnimationWithDoubleQuotes() async {
await writePackageWithCommentedLibrary('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id="namedAnimation"}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, contains('<video id="namedAnimation"'));
}
void test_rendersNamedAnimationWithSingleQuotes() async {
await writePackageWithCommentedLibrary('''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id='namedAnimation'}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, contains('<video id="namedAnimation"'));
}
void test_rendersMultipleAnimationsUsingUniqueIds() async {
await writePackageWithCommentedLibraries([
(
'a.dart',
'''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4}
/// {@animation 100 200 http://host/path/to/video2.mp4}
''',
),
// A second element with unnamed animations requires unique IDs as well.
(
'b.dart',
'''
/// First line.
///
/// {@animation 100 200 http://host/path/to/video.mp4}
/// {@animation 100 200 http://host/path/to/video2.mp4}
''',
),
]);
var docA = await libraryModel.processComment();
var docB = packageGraph.defaultPackage.libraries[1].documentation;
expectNoWarnings();
expect(docA, contains('<video id="animation_3"'));
expect(docA, contains('<video id="animation_4"'));
expect(docB, contains('<video id="animation_1"'));
expect(docB, contains('<video id="animation_2"'));
}
void test_docImport() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// @docImport 'dart:async' as async;
///
/// End text.
''');
var doc = libraryModel.documentation;
expect(doc, equals('''
Text.
End text.'''));
}
void test_docImport_onlyLine() async {
await writePackageWithCommentedLibrary('''
/// @docImport 'dart:async' as async;
''');
var doc = libraryModel.documentation;
expect(doc, equals(''));
}
void test_docImport_onlyLine_moreWhitespace() async {
await writePackageWithCommentedLibrary('''
/// @docImport 'dart:async' as async;
''');
var doc = libraryModel.documentation;
expect(doc, equals(''));
}
void test_docImport_consecutiveLines() async {
await writePackageWithCommentedLibrary('''
/// @docImport 'dart:async' as async;
/// @docImport 'dart:isolate' as isolate;
/// @docImport 'dart:math' as math;
''');
var doc = libraryModel.documentation;
expect(doc, equals(''));
}
void test_docImport_multipleLines() async {
await writePackageWithCommentedLibrary('''
/// One.
/// @docImport 'dart:async' as async;
/// Two.
/// @docImport 'dart:isolate' as isolate;
/// Three.
''');
var doc = libraryModel.documentation;
expect(doc, equals('''
One.
Two.
Three.'''));
}
void test_docImport_precedingText() async {
await writePackageWithCommentedLibrary('''
// Copyright such-and-such.
/// @docImport 'dart:async' as async;
''');
var doc = libraryModel.documentation;
expect(doc, equals(''));
}
void test_animationDirectiveHasFewerThanThreeArguments() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');
expect(
libraryModel,
hasInvalidParameterWarning(
'Invalid @animation directive, "{@animation 100 '
'http://host/path/to/video.mp4 id=barHerderAnimation}"\n'
'Animation directives must be of the form "{@animation WIDTH '
'HEIGHT URL [id=ID]}"'),
);
}
void test_animationDirectiveHasMoreThanFourArguments() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100 200 300 400 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');
expect(
libraryModel,
hasInvalidParameterWarning(
'Invalid @animation directive, "{@animation 100 200 300 400 '
'http://host/path/to/video.mp4 id=barHerderAnimation}"\n'
'Animation directives must be of the form "{@animation WIDTH '
'HEIGHT URL [id=ID]}"'),
);
}
void test_animationDirectiveHasNonUniqueIdentifier() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=barHerderAnimation}
/// {@animation 100 200 http://host/path/to/video.mpg id=barHerderAnimation}
///
/// End text.
''');
expect(
libraryModel,
hasInvalidParameterWarning('An animation has a non-unique identifier, '
'"barHerderAnimation". Animation identifiers must be unique.'),
);
}
void test_animationDirectiveHasAnInvalidIdentifier() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 id=not-valid}
///
/// End text.
''');
expect(
libraryModel,
hasInvalidParameterWarning(
'An animation has an invalid identifier, "not-valid". The '
'identifier can only contain letters, numbers and underscores, and '
'must not begin with a number.'),
);
}
void test_animationDirectiveHasMalformedWidth() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100px 200 http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');
expect(
libraryModel,
hasInvalidParameterWarning(
'An animation has an invalid width (barHerderAnimation), '
'"100px". The width must be an integer.'),
);
}
void test_animationDirectiveHasMalformedHeight() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100 200px http://host/path/to/video.mp4 id=barHerderAnimation}
///
/// End text.
''');
expect(
libraryModel,
hasInvalidParameterWarning(
'An animation has an invalid height (barHerderAnimation), '
'"200px". The height must be an integer.'),
);
}
void test_animationDirectiveHasUnknownParameter() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@animation 100 200 http://host/path/to/video.mp4 name=barHerderAnimation}
///
/// End text.
''');
expect(
libraryModel,
hasInvalidParameterWarning(
'The {@animation ...} directive was called with invalid '
'parameters. FormatException: Could not find an option named'),
);
}
void test_processesTemplateDirective() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@template abc}
/// Template text.
/// {@endtemplate}
///
/// End text.
''');
var doc = await libraryModel.processComment();
expectNoWarnings();
expect(doc, equals('''
Text.
{@macro abc}
End text.'''));
}
void test_processesLeadingTemplateDirective() async {
await writePackageWithCommentedLibrary('''
/// {@template abc}
/// Template text.
/// {@endtemplate}
///
/// End text.
''');
var doc = await libraryModel.processComment();
expectNoWarnings();
expect(doc, equals('''
{@macro abc}
End text.'''));
}
void test_processesTrailingTemplateDirective() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@template abc}
/// Template text.
/// {@endtemplate}
''');
var doc = await libraryModel.processComment();
expectNoWarnings();
expect(doc, equals('''
Text.
{@macro abc}'''));
}
void test_processesTemplateDirectiveWithoutBlankLineFollowing() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@template abc}
/// Template text.
/// {@endtemplate}
/// End text.
''');
var doc = await libraryModel.processComment();
expectNoWarnings();
expect(doc, equals('''
Text.
{@macro abc}
End text.'''));
}
void test_allowsWhitespaceAroundTemplateDirectiveName() async {
await writePackageWithCommentedLibrary('''
/// {@template abc }
/// Template text.
/// {@endtemplate}
''');
var doc = await libraryModel.processComment();
expectNoWarnings();
expect(doc, equals('{@macro abc}'));
}
void test_leavesInjectHtmlDirectiveUnprocessedWhenDisabled() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@inject-html}<script></script>{@end-inject-html}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, equals('''
Text.
{@inject-html}<script></script>{@end-inject-html}'''));
}
void test_leavesToolUnprocessedWhenDisabled() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@tool date}{@end-tool}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, equals('''
Text.
{@tool date}{@end-tool}'''));
}
void test_processesInjectHtmlDirectiveWhenEnabled() async {
await writePackageWithCommentedLibrary(
additionalArguments: ['--inject-html'],
'''
/// Text.
///
/// {@inject-html}<script></script>{@end-inject-html}
''',
);
libraryModel = packageGraph.defaultPackage.libraries.first;
var doc = libraryModel.documentation;
expectNoWarnings();
expect(doc, equals('''
Text.
<dartdoc-html>6829def5ec06d211fa90fe69a58213ae901f3ee4</dartdoc-html>
'''));
}
void test_processesYoutubeDirective() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@youtube 100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}
///
/// End text.
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(
doc,
matches(
RegExp(
'^Text.\n\n+'
r'<iframe src="https://www.youtube.com/embed/oHg5SJYRHA0\?rel=0".*</iframe>\s*\n\n+'
r'End text.$',
multiLine: true,
dotAll: true,
),
),
);
}
void test_processesLeadingYoutubeDirective() async {
await writePackageWithCommentedLibrary('''
/// {@youtube 100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}
///
/// End text.
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(
doc,
matches(
RegExp(
r'<iframe src="https://www.youtube.com/embed/oHg5SJYRHA0\?rel=0".*</iframe>\s*\n\n+'
r'End text.$',
multiLine: true,
dotAll: true,
),
),
);
}
void test_processesTrailingYoutubeDirective() async {
await writePackageWithCommentedLibrary('''
/// Text.
///
/// {@youtube 100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}
''');
var doc = libraryModel.documentation;
expectNoWarnings();
expect(
doc,
matches(
RegExp(
'^Text.\n\n+'
r'<iframe src="https://www.youtube.com/embed/oHg5SJYRHA0\?rel=0".*</iframe>\s*$',
multiLine: true,
dotAll: true,
),
),
);
}
void test_youtubeDirectiveHasLessThanThreeArguments() async {
await writePackageWithCommentedLibrary(
'/// {@youtube 100 https://www.youtube.com/watch?v=oHg5SJYRHA0}');
expect(
libraryModel,
hasInvalidParameterWarning('Invalid @youtube directive, '
'"{@youtube 100 https://www.youtube.com/watch?v=oHg5SJYRHA0}"\n'
'YouTube directives must be of the form '
'"{@youtube WIDTH HEIGHT URL}"'),
);
}
void test_youtubeDirectiveHasMoreThanThreeArguments() async {
await writePackageWithCommentedLibrary(
'/// {@youtube 100 200 300 https://www.youtube.com/watch?v=oHg5SJYRHA0}');
expect(
libraryModel,
hasInvalidParameterWarning('Invalid @youtube directive, '
'"{@youtube 100 200 300 https://www.youtube.com/watch?v=oHg5SJYRHA0}"\n'
'YouTube directives must be of the form '
'"{@youtube WIDTH HEIGHT URL}"'),
);
}
void test_youtubeDirectiveHasMalformedWidth() async {
await writePackageWithCommentedLibrary(
'/// {@youtube 100px 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}');
expect(
libraryModel,
hasInvalidParameterWarning(
'A @youtube directive has an invalid width, "100px". '
'The width must be a positive integer.'),
);
}
void test_youtubeDirectiveHasNegativeWidth() async {
await writePackageWithCommentedLibrary(
'/// {@youtube -100 200 https://www.youtube.com/watch?v=oHg5SJYRHA0}');
expect(
libraryModel,
hasInvalidParameterWarning(
'The {@youtube ...} directive was called with invalid '
'parameters. FormatException: Could not find an option with '
'short name "-1".'),
);
}
void test_youtubeDirectiveHasMalformedHeight() async {
await writePackageWithCommentedLibrary(
'/// {@youtube 100 200px https://www.youtube.com/watch?v=oHg5SJYRHA0}');
expect(
libraryModel,
hasInvalidParameterWarning(
'A @youtube directive has an invalid height, "200px". The height '
'must be a positive integer.'),
);
}
void test_youtubeDirectiveHasNegativeHeight() async {
await writePackageWithCommentedLibrary(
'/// {@youtube 100 -200 https://www.youtube.com/watch?v=oHg5SJYRHA0}');
expect(
libraryModel,
hasInvalidParameterWarning(
'The {@youtube ...} directive was called with invalid '
'parameters. FormatException: Could not find an option with '
'short name "-2".'),
);
}
void test_youtubeDirectiveHasInvalidUrl() async {
await writePackageWithCommentedLibrary(
'/// {@youtube 100 200 https://www.not-youtube.com/watch?v=oHg5SJYRHA0}');
expect(
libraryModel,
hasInvalidParameterWarning('A @youtube directive has an invalid URL: '
'"https://www.not-youtube.com/watch?v=oHg5SJYRHA0". Supported '
'YouTube URLs have the following format: '
'https://www.youtube.com/watch?v=oHg5SJYRHA0.'),
);
}
void test_youtubeDirectiveHasUrlWithExtraQueryParameters() async {
await writePackageWithCommentedLibrary(
'/// {@youtube 100 200 https://www.not-youtube.com/watch?v=oHg5SJYRHA0&a=1}');
expect(
libraryModel,
hasInvalidParameterWarning('A @youtube directive has an invalid URL: '
'"https://www.not-youtube.com/watch?v=oHg5SJYRHA0&a=1". '
'Supported YouTube URLs have the following format: '
'https://www.youtube.com/watch?v=oHg5SJYRHA0.'),
);
}
Matcher hasInvalidParameterWarning(String message) =>
_HasWarning(PackageWarning.invalidParameter, message);
// TODO(srawlins): More unit tests: @tool.
}
class _HasWarning extends Matcher {
final PackageWarning kind;
final String message;
_HasWarning(this.kind, this.message);
@override
bool matches(Object? actual, Map<Object?, Object?> matchState) {
if (actual is ModelElement) {
return actual.packageGraph.packageWarningCounter
.hasWarning(actual, kind, message);
} else {
return false;
}
}
@override
Description describe(Description description) =>
description.add('Library to be warned with $kind and message:\n$message');
@override
Description describeMismatch(Object? actual, Description mismatchDescription,
Map<Object?, Object?> matchState, bool verbose) {
if (actual is ModelElement) {
var warnings = actual
.packageGraph.packageWarningCounter.countedWarnings[actual.element];
if (warnings == null) {
return mismatchDescription.add('has no warnings');
}
if (warnings.length == 1) {
var kind = warnings.keys.first;
return mismatchDescription
.add('has one $kind warnings: ${warnings[kind]}');
}
return mismatchDescription.add('has warnings: $warnings');
}
return mismatchDescription.add('is a ${actual.runtimeType}');
}
}