blob: c3f412414044a8a60e9a4dc85f7a7c3ec3b7defe [file] [log] [blame]
// Copyright (c) 2022, 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/memory_file_system.dart';
import 'package:dartdoc/options.dart';
import 'package:dartdoc/src/dartdoc.dart';
import 'package:dartdoc/src/failure.dart';
import 'package:dartdoc/src/model/model.dart';
import 'package:dartdoc/src/package_meta.dart';
import 'package:path/path.dart' as p;
import 'package:test/test.dart';
import 'src/test_descriptor_utils.dart' as d;
import 'src/utils.dart' as utils;
void main() async {
const packageName = 'test_package';
late String packagePath;
late MemoryResourceProvider resourceProvider;
late PackageMetaProvider packageMetaProvider;
late DartdocGeneratorOptionContext context;
setUp(() {
packageMetaProvider = utils.testPackageMetaProvider;
resourceProvider =
packageMetaProvider.resourceProvider as MemoryResourceProvider;
});
Future<PubPackageBuilder> createPackageBuilder({
List<String> additionalOptions = const [],
bool skipUnreachableSdkLibraries = true,
}) async {
context = await utils.generatorContextFromArgv([
'--input',
packagePath,
'--output',
p.join(packagePath, 'doc'),
'--sdk-dir',
packageMetaProvider.defaultSdkDir.path,
'--allow-tools',
'--no-link-to-remote',
...additionalOptions,
], packageMetaProvider);
var packageConfigProvider = utils
.getTestPackageConfigProvider(packageMetaProvider.defaultSdkDir.path);
packageConfigProvider.addPackageToConfigFor(
packagePath, packageName, Uri.file('$packagePath/'));
return PubPackageBuilder(
context,
packageMetaProvider,
packageConfigProvider,
skipUnreachableSdkLibraries: skipUnreachableSdkLibraries,
);
}
Future<Dartdoc> buildDartdoc({
List<String> additionalOptions = const [],
bool skipUnreachableSdkLibraries = true,
}) async {
final packageBuilder = await createPackageBuilder(
additionalOptions: additionalOptions,
skipUnreachableSdkLibraries: skipUnreachableSdkLibraries,
);
return await Dartdoc.fromContext(
context,
packageBuilder,
);
}
test('favicon option copies a favicon file', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
favicon: anicon.png
''',
libFiles: [d.file('lib.dart', '')],
files: [d.file('anicon.png', 'Just plain text')],
resourceProvider: resourceProvider,
);
await utils.writeDartdocResources(resourceProvider);
await (await buildDartdoc()).generateDocs();
final faviconContent = resourceProvider
.getFile(
p.joinAll([packagePath, 'doc', 'static-assets', 'favicon.png']))
.readAsStringSync();
expect(faviconContent, contains('Just plain text'));
});
test('header option adds content to index.html', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
header: ['extras/header.html']
''',
libFiles: [d.file('lib.dart', '')],
files: [
d.dir('extras', [d.file('header.html', '<em>Header</em> things.')])
],
resourceProvider: resourceProvider,
);
await utils.writeDartdocResources(resourceProvider);
await (await buildDartdoc()).generateDocs();
final indexContent = resourceProvider
.getFile(p.joinAll([packagePath, 'doc', 'index.html']))
.readAsStringSync();
expect(indexContent, contains('<em>Header</em> things.'));
});
test('footer option adds content to index.html', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
footer: ['extras/footer.html']
''',
libFiles: [d.file('lib.dart', '')],
files: [
d.dir('extras', [d.file('footer.html', '<em>Footer</em> things.')])
],
resourceProvider: resourceProvider,
);
await utils.writeDartdocResources(resourceProvider);
await (await buildDartdoc()).generateDocs();
final indexContent = resourceProvider
.getFile(p.joinAll([packagePath, 'doc', 'index.html']))
.readAsStringSync();
expect(indexContent, contains('<em>Footer</em> things.'));
});
test('footerText option adds text to index.html', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
footerText: ['extras/footer.txt']
''',
libFiles: [d.file('lib.dart', '')],
files: [
d.dir('extras', [d.file('footer.txt', 'Just footer text')])
],
resourceProvider: resourceProvider,
);
await utils.writeDartdocResources(resourceProvider);
await (await buildDartdoc()).generateDocs();
final indexContent = resourceProvider
.getFile(p.joinAll([packagePath, 'doc', 'index.html']))
.readAsStringSync();
expect(indexContent, contains('Just footer text'));
});
test('excludeFooterVersion option does not display version', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
excludeFooterVersion: true
''',
libFiles: [d.file('lib.dart', '')],
files: [
d.dir('extras', [d.file('footer.txt', 'Just footer text')])
],
resourceProvider: resourceProvider,
);
await utils.writeDartdocResources(resourceProvider);
await (await buildDartdoc()).generateDocs();
final indexContent = resourceProvider
.getFile(p.joinAll([packagePath, 'doc', 'index.html']))
.readAsStringSync();
final footerRegex =
RegExp(r'<footer>(.*\s*?\n?)+?</footer>', multiLine: true);
// Get footer, and check for version number.
final match = footerRegex.firstMatch(indexContent);
if (match == null) {
fail('Could not find footer tag in "$indexContent"');
}
final version = RegExp(r'(\d+\.)?(\d+\.)?(\*|\d+)');
expect(version.hasMatch(match.group(0)!), false, reason: indexContent);
});
test('examplePathPrefix option finds examples in a custom path', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
examplePathPrefix: 'package_examples'
''',
libFiles: [
d.file('lib.dart', '''
/// An example in a non-default location:
///
/// {@example foo.dart}
class Foo {}
'''),
],
files: [
d.dir('package_examples', [
d.file('foo.dart.md', '''
```
An example in an unusual dir.
```
'''),
]),
],
resourceProvider: resourceProvider,
);
final packageGraph =
await (await createPackageBuilder()).buildPackageGraph();
final classFoo = packageGraph.allCanonicalModelElements
.whereType<Class>()
.firstWhere((c) => c.name == 'Foo');
expect(classFoo.documentationAsHtml,
contains('<code class="language-dart">An example in an unusual dir.'));
});
test("'format=md' option generates markdown files", () async {
packagePath = await d.createPackage(
packageName,
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
],
resourceProvider: resourceProvider,
);
await (await buildDartdoc(additionalOptions: ['--format', 'md']))
.generateDocsBase();
final indexContent = resourceProvider
.getFile(p.joinAll([packagePath, 'doc', 'library_1', 'Foo-class.md']))
.readAsStringSync();
expect(indexContent, contains('# Foo class'));
});
test("'format=bad' option results in DartdocFailure", () async {
packagePath = await d.createPackage(
packageName,
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
],
resourceProvider: resourceProvider,
);
expect(
() => buildDartdoc(additionalOptions: ['--format', 'bad']),
throwsA(const TypeMatcher<DartdocFailure>().having((f) => f.message,
'message', startsWith('Unsupported output format'))));
});
test("'include' option can be specified in options file", () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
include: ["library_1", "library_2"]
''',
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
d.file('library_2.dart', '''
library library_2;
class Bar {}
'''),
d.file('library_3.dart', '''
library library_3;
class Baz {}
'''),
],
resourceProvider: resourceProvider,
);
final packageGraph =
await (await createPackageBuilder()).buildPackageGraph();
expect(packageGraph.localPublicLibraries.map((l) => l.name),
orderedEquals(['library_1', 'library_2']));
});
test("'include' command line option overrides options file option", () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
include: ["library_1", "library_2"]
''',
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
d.file('library_2.dart', '''
library library_2;
class Bar {}
'''),
d.file('library_3.dart', '''
library library_3;
class Baz {}
'''),
],
resourceProvider: resourceProvider,
);
final packageGraph = await (await createPackageBuilder(
additionalOptions: ['--include', 'library_3'],
))
.buildPackageGraph();
expect(packageGraph.localPublicLibraries.map((l) => l.name),
orderedEquals(['library_3']));
});
test("'exclude' command line option overrides options file option", () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
include: ["library_1", "library_2"]
''',
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
d.file('library_2.dart', '''
library library_2;
class Bar {}
'''),
],
resourceProvider: resourceProvider,
);
final packageGraph = await (await createPackageBuilder(
additionalOptions: ['--exclude', 'library_1'],
))
.buildPackageGraph();
expect(packageGraph.localPublicLibraries.map((l) => l.name),
orderedEquals(['library_2']));
});
test('showUndocumentedCategories option shows undocumented categories',
() async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
showUndocumentedCategories: true
''',
libFiles: [
d.file('lib.dart', '''
/// {@category SomethingUndocumented}
class Foo {}
'''),
],
resourceProvider: resourceProvider,
);
final packageGraph =
await (await createPackageBuilder()).buildPackageGraph();
final classFoo = packageGraph.allCanonicalModelElements
.whereType<Class>()
.firstWhere((c) => c.name == 'Foo');
expect(classFoo.displayedCategories, isNotEmpty);
});
test('categoryOrder orders categories', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
categories:
One:
markdown: one.md
Two:
markdown: two.md
Three:
markdown: three.md
categoryOrder: ["Three", "One", "Two"]
''',
libFiles: [
d.file('lib.dart', '''
library 'lib1';
/// {@category One}
class C1 {}
/// {@category Two}
class C2 {}
/// {@category Three}
class C3 {}
'''),
],
files: [
d.file('one.md', ''),
d.file('two.md', ''),
d.file('three.md', ''),
],
resourceProvider: resourceProvider,
);
final packageGraph =
await (await createPackageBuilder()).buildPackageGraph();
final package = packageGraph.packages
.firstWhere((element) => element.name == packageName);
expect(package.documentedCategoriesSorted.map((c) => c.name),
equals(['Three', 'One', 'Two']));
});
test('categories not included in categoryOrder are ordered at the end',
() async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
categories:
Three:
markdown: three.md
One:
markdown: one.md
Two:
markdown: two.md
Four:
markdown: four.md
categoryOrder: ["Two", "One"]
''',
libFiles: [
d.file('lib.dart', '''
library 'lib1';
/// {@category Three}
class C3 {}
/// {@category One}
class C1 {}
/// {@category Two}
class C2 {}
/// {@category Four}
class C4 {}
'''),
],
files: [
d.file('one.md', ''),
d.file('two.md', ''),
d.file('three.md', ''),
d.file('four.md', ''),
],
resourceProvider: resourceProvider,
);
final packageGraph =
await (await createPackageBuilder()).buildPackageGraph();
final package = packageGraph.packages
.firstWhere((element) => element.name == packageName);
expect(package.documentedCategoriesSorted.map((c) => c.name),
equals(['Two', 'One', 'Four', 'Three']));
});
test('categories are only tracked when used', () async {
packagePath = await d.createPackage(
packageName,
dartdocOptions: '''
dartdoc:
categories:
One:
markdown: one.md
Two:
markdown: two.md
''',
libFiles: [
d.file('lib.dart', '''
library 'lib1';
/// {@category One}
class C1 {}
'''),
],
files: [
d.file('one.md', ''),
d.file('two.md', ''),
],
resourceProvider: resourceProvider,
);
final packageGraph =
await (await createPackageBuilder()).buildPackageGraph();
final package = packageGraph.packages
.firstWhere((element) => element.name == packageName);
expect(
package.documentedCategoriesSorted.map((c) => c.name), equals(['One']));
});
test(
"'templates-dir' option referencing a non-existent directory results in "
'DartdocFailure', () async {
packagePath = await d.createPackage(
packageName,
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
],
resourceProvider: resourceProvider,
);
expect(
() => buildDartdoc(additionalOptions: ['--templates-dir', 'bad']),
throwsA(const TypeMatcher<DartdocFailure>().having(
(f) => f.message,
'message',
startsWith(
'Argument --templates-dir, set to bad, resolves to missing path'),
)));
});
test("'templates-dir' option specifies the templates to use", () async {
packagePath = await d.createPackage(
packageName,
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
],
files: [
d.dir('templates', [
d.file('_sidebar_for_container.html', 'EMPTY'),
d.file('_sidebar_for_library.html', 'EMPTY'),
d.file('404error.html', 'EMPTY'),
d.file('category.html', 'EMPTY'),
d.file('class.html', 'CLASS FILE'),
d.file('constructor.html', 'EMPTY'),
d.file('enum.html', 'EMPTY'),
d.file('extension.html', 'EMPTY'),
d.file('function.html', 'EMPTY'),
d.file('index.html', 'EMPTY'),
d.file('library.html', 'EMPTY'),
d.file('method.html', 'EMPTY'),
d.file('mixin.html', 'EMPTY'),
d.file('property.html', 'EMPTY'),
d.file('top_level_property.html', 'EMPTY'),
d.file('typedef.html', 'EMPTY'),
d.file('search.html', 'EMPTY'),
]),
],
resourceProvider: resourceProvider,
);
var customTemplatesDir = p.join(packagePath, 'templates');
await utils.writeDartdocResources(resourceProvider);
var dartdoc = await buildDartdoc(
additionalOptions: ['--templates-dir', customTemplatesDir]);
await dartdoc.generateDocsBase();
final indexContent = resourceProvider
.getFile(p.joinAll([packagePath, 'doc', 'library_1', 'Foo-class.html']))
.readAsStringSync();
expect(indexContent, contains('CLASS FILE'));
});
test(
"'templates-dir' option referencing an empty directory results in "
'DartdocFailure', () async {
packagePath = await d.createPackage(
packageName,
libFiles: [
d.file('library_1.dart', '''
library library_1;
class Foo {}
'''),
],
resourceProvider: resourceProvider,
);
var customTemplatesDir = resourceProvider
.newFolder(resourceProvider.pathContext
.canonicalize(resourceProvider.convertPath('/custom_templates')))
.path;
expect(
() => buildDartdoc(
additionalOptions: ['--templates-dir', customTemplatesDir]),
throwsA(const TypeMatcher<DartdocFailure>().having((f) => f.message,
'message', startsWith('Missing required template file'))));
});
}