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:convert';
6
7import 'package:file/file.dart';
8import 'package:file/memory.dart';
9import 'package:path/path.dart' as path;
10import 'package:platform/platform.dart';
11import 'package:pub_semver/pub_semver.dart';
12import 'package:snippets/snippets.dart';
13import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
14
15import '../bin/snippets.dart' as snippets_main;
16import 'fake_process_manager.dart';
17
18class FakeFlutterInformation extends FlutterInformation {
19 FakeFlutterInformation(this.flutterRoot);
20
21 final Directory flutterRoot;
22
23 @override
24 Directory getFlutterRoot() {
25 return flutterRoot;
26 }
27
28 @override
29 Map<String, dynamic> getFlutterInformation() {
30 return <String, dynamic>{
31 'flutterRoot': flutterRoot,
32 'frameworkVersion': Version(2, 10, 0),
33 'dartSdkVersion': Version(2, 12, 1),
34 };
35 }
36}
37
38void main() {
39 group('Generator', () {
40 late MemoryFileSystem memoryFileSystem = MemoryFileSystem();
41 late FlutterRepoSnippetConfiguration configuration;
42 late SnippetGenerator generator;
43 late Directory tmpDir;
44
45 void writeSkeleton(String type) {
46 switch (type) {
47 case 'dartpad':
48 configuration.getHtmlSkeletonFile('dartpad').writeAsStringSync('''
49<div>HTML Bits (DartPad-style)</div>
50<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?split=60&run=true&sample_id={{id}}&sample_channel={{channel}}">
51<div>More HTML Bits</div>
52''');
53 case 'sample':
54 case 'snippet':
55 configuration.getHtmlSkeletonFile(type).writeAsStringSync('''
56<div>HTML Bits</div>
57{{description}}
58<pre>{{code}}</pre>
59<pre>{{app}}</pre>
60<div>More HTML Bits</div>
61''');
62 }
63 }
64
65 setUp(() {
66 // Create a new filesystem.
67 memoryFileSystem = MemoryFileSystem();
68 tmpDir = memoryFileSystem.systemTempDirectory.createTempSync('flutter_snippets_test.');
69 configuration = FlutterRepoSnippetConfiguration(
70 flutterRoot: memoryFileSystem.directory(path.join(tmpDir.absolute.path, 'flutter')),
71 filesystem: memoryFileSystem,
72 );
73 configuration.skeletonsDirectory.createSync(recursive: true);
74 <String>['dartpad', 'sample', 'snippet'].forEach(writeSkeleton);
75 FlutterInformation.instance = FakeFlutterInformation(configuration.flutterRoot);
76 generator = SnippetGenerator(
77 configuration: configuration,
78 filesystem: memoryFileSystem,
79 flutterRoot: configuration.skeletonsDirectory.parent,
80 );
81 });
82
83 test('generates samples', () async {
84 final File inputFile =
85 memoryFileSystem.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
86 ..createSync(recursive: true)
87 ..writeAsStringSync(r'''
88A description of the sample.
89
90On several lines.
91
92** See code in examples/api/widgets/foo/foo_example.0.dart **
93''');
94 final String examplePath = path.join(
95 configuration.flutterRoot.path,
96 'examples/api/widgets/foo/foo_example.0.dart',
97 );
98 memoryFileSystem.file(examplePath)
99 ..create(recursive: true)
100 ..writeAsStringSync('''
101// Copyright
102
103// Flutter code sample for [MyElement].
104
105void main() {
106 runApp(MaterialApp(title: 'foo'));
107}\n''');
108 final File outputFile = memoryFileSystem.file(
109 path.join(tmpDir.absolute.path, 'snippet_out.txt'),
110 );
111 final SnippetDartdocParser sampleParser = SnippetDartdocParser(memoryFileSystem);
112 const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
113 const int sourceLine = 222;
114 final SourceElement element = sampleParser.parseFromDartdocToolFile(
115 inputFile,
116 element: 'MyElement',
117 startLine: sourceLine,
118 sourceFile: memoryFileSystem.file(sourcePath),
119 type: 'sample',
120 );
121
122 expect(element.samples, isNotEmpty);
123 element.samples.first.metadata.addAll(<String, Object?>{'channel': 'stable'});
124 final String code = generator.generateCode(element.samples.first, output: outputFile);
125 expect(code, contains("runApp(MaterialApp(title: 'foo'));"));
126 final String html = generator.generateHtml(element.samples.first);
127 expect(html, contains('<div>HTML Bits</div>'));
128 expect(html, contains('<div>More HTML Bits</div>'));
129 expect(html, contains(r'''runApp(MaterialApp(title: &#39;foo&#39;));'''));
130 expect(html, isNot(contains('sample_channel=stable')));
131 expect(
132 html,
133 contains(
134 'A description of the sample.\n'
135 '\n'
136 'On several lines.{@inject-html}</div>',
137 ),
138 );
139 expect(html, contains('void main() {'));
140
141 final String outputContents = outputFile.readAsStringSync();
142 expect(outputContents, contains('void main() {'));
143 });
144
145 test('generates snippets', () async {
146 final File inputFile =
147 memoryFileSystem.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
148 ..createSync(recursive: true)
149 ..writeAsStringSync(r'''
150A description of the snippet.
151
152On several lines.
153
154```code
155void main() {
156 print('The actual $name.');
157}
158```
159''');
160
161 final SnippetDartdocParser sampleParser = SnippetDartdocParser(memoryFileSystem);
162 const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
163 const int sourceLine = 222;
164 final SourceElement element = sampleParser.parseFromDartdocToolFile(
165 inputFile,
166 element: 'MyElement',
167 startLine: sourceLine,
168 sourceFile: memoryFileSystem.file(sourcePath),
169 type: 'snippet',
170 );
171 expect(element.samples, isNotEmpty);
172 element.samples.first.metadata.addAll(<String, Object>{'channel': 'stable'});
173 final String code = generator.generateCode(element.samples.first);
174 expect(code, contains('// A description of the snippet.'));
175 final String html = generator.generateHtml(element.samples.first);
176 expect(html, contains('<div>HTML Bits</div>'));
177 expect(html, contains('<div>More HTML Bits</div>'));
178 expect(html, contains(r' print(&#39;The actual $name.&#39;);'));
179 expect(
180 html,
181 contains(
182 '<div class="snippet-description">{@end-inject-html}A description of the snippet.\n\n'
183 'On several lines.{@inject-html}</div>\n',
184 ),
185 );
186 expect(html, contains('main() {'));
187 });
188
189 test('generates dartpad samples', () async {
190 final File inputFile =
191 memoryFileSystem.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
192 ..createSync(recursive: true)
193 ..writeAsStringSync(r'''
194A description of the snippet.
195
196On several lines.
197
198** See code in examples/api/widgets/foo/foo_example.0.dart **
199''');
200 final String examplePath = path.join(
201 configuration.flutterRoot.path,
202 'examples/api/widgets/foo/foo_example.0.dart',
203 );
204 memoryFileSystem.file(examplePath)
205 ..create(recursive: true)
206 ..writeAsStringSync('''
207// Copyright
208
209// Flutter code sample for [MyElement].
210
211void main() {
212 runApp(MaterialApp(title: 'foo'));
213}\n''');
214
215 final SnippetDartdocParser sampleParser = SnippetDartdocParser(memoryFileSystem);
216 const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
217 const int sourceLine = 222;
218 final SourceElement element = sampleParser.parseFromDartdocToolFile(
219 inputFile,
220 element: 'MyElement',
221 startLine: sourceLine,
222 sourceFile: memoryFileSystem.file(sourcePath),
223 type: 'dartpad',
224 );
225 expect(element.samples, isNotEmpty);
226 element.samples.first.metadata.addAll(<String, Object>{'channel': 'stable'});
227 final String code = generator.generateCode(element.samples.first);
228 expect(code, contains("runApp(MaterialApp(title: 'foo'));"));
229 final String html = generator.generateHtml(element.samples.first);
230 expect(html, contains('<div>HTML Bits (DartPad-style)</div>'));
231 expect(html, contains('<div>More HTML Bits</div>'));
232 expect(
233 html,
234 contains(
235 '<iframe class="snippet-dartpad" src="https://dartpad.dev/embed-flutter.html?split=60&run=true&sample_id=MyElement.0&sample_channel=stable">\n',
236 ),
237 );
238 });
239
240 test('generates sample metadata', () async {
241 final File inputFile =
242 memoryFileSystem.file(path.join(tmpDir.absolute.path, 'snippet_in.txt'))
243 ..createSync(recursive: true)
244 ..writeAsStringSync(r'''
245A description of the snippet.
246
247On several lines.
248
249```dart
250void main() {
251 print('The actual $name.');
252}
253```
254''');
255
256 final File outputFile = memoryFileSystem.file(
257 path.join(tmpDir.absolute.path, 'snippet_out.dart'),
258 );
259 final File expectedMetadataFile = memoryFileSystem.file(
260 path.join(tmpDir.absolute.path, 'snippet_out.json'),
261 );
262
263 final SnippetDartdocParser sampleParser = SnippetDartdocParser(memoryFileSystem);
264 const String sourcePath = 'packages/flutter/lib/src/widgets/foo.dart';
265 const int sourceLine = 222;
266 final SourceElement element = sampleParser.parseFromDartdocToolFile(
267 inputFile,
268 element: 'MyElement',
269 startLine: sourceLine,
270 sourceFile: memoryFileSystem.file(sourcePath),
271 type: 'sample',
272 );
273 expect(element.samples, isNotEmpty);
274 element.samples.first.metadata.addAll(<String, Object>{'channel': 'stable'});
275 generator.generateCode(element.samples.first, output: outputFile);
276 expect(expectedMetadataFile.existsSync(), isTrue);
277 final Map<String, dynamic> json =
278 jsonDecode(expectedMetadataFile.readAsStringSync()) as Map<String, dynamic>;
279 expect(json['id'], equals('MyElement.0'));
280 expect(json['channel'], equals('stable'));
281 expect(json['file'], equals('snippet_out.dart'));
282 expect(json['description'], equals('A description of the snippet.\n\nOn several lines.'));
283 expect(json['sourcePath'], equals('packages/flutter/lib/src/widgets/foo.dart'));
284 });
285 });
286
287 group('snippets command line argument test', () {
288 late MemoryFileSystem memoryFileSystem = MemoryFileSystem();
289 late Directory tmpDir;
290 late Directory flutterRoot;
291 late FakeProcessManager fakeProcessManager;
292
293 setUp(() {
294 fakeProcessManager = FakeProcessManager();
295 memoryFileSystem = MemoryFileSystem();
296 tmpDir = memoryFileSystem.systemTempDirectory.createTempSync('flutter_snippets_test.');
297 flutterRoot = memoryFileSystem.directory(path.join(tmpDir.absolute.path, 'flutter'))
298 ..createSync(recursive: true);
299 });
300
301 test('command line arguments are parsed and passed to generator', () {
302 final FakePlatform platform = FakePlatform(
303 environment: <String, String>{
304 'PACKAGE_NAME': 'dart:ui',
305 'LIBRARY_NAME': 'library',
306 'ELEMENT_NAME': 'element',
307 'FLUTTER_ROOT': flutterRoot.absolute.path,
308 // The details here don't really matter other than the flutter root.
309 'FLUTTER_VERSION':
310 '''
311 {
312 "frameworkVersion": "2.5.0-6.0.pre.55",
313 "channel": "use_snippets_pkg",
314 "repositoryUrl": "git@github.com:flutter/flutter.git",
315 "frameworkRevision": "fec4641e1c88923ecd6c969e2ff8a0dd12dc0875",
316 "frameworkCommitDate": "2021-08-11 15:19:48 -0700",
317 "engineRevision": "d8bbebed60a77b3d4fe9c840dc94dfbce159d951",
318 "dartSdkVersion": "2.14.0 (build 2.14.0-393.0.dev)",
319 "flutterRoot": "${flutterRoot.absolute.path}"
320 }''',
321 },
322 );
323 final FlutterInformation flutterInformation = FlutterInformation(
324 filesystem: memoryFileSystem,
325 processManager: fakeProcessManager,
326 platform: platform,
327 );
328 FlutterInformation.instance = flutterInformation;
329 MockSnippetGenerator mockSnippetGenerator = MockSnippetGenerator();
330 snippets_main.snippetGenerator = mockSnippetGenerator;
331 String errorMessage = '';
332 errorExit = (String message) {
333 errorMessage = message;
334 };
335
336 snippets_main.platform = platform;
337 snippets_main.filesystem = memoryFileSystem;
338 snippets_main.processManager = fakeProcessManager;
339 final File input = memoryFileSystem.file(tmpDir.childFile('input.snippet'))
340 ..writeAsString('/// Test file');
341 snippets_main.main(<String>['--input=${input.absolute.path}']);
342
343 final Map<String, dynamic> metadata = mockSnippetGenerator.sample.metadata;
344 // Ignore the channel, because channel is really just the branch, and will be
345 // different on development workstations.
346 metadata.remove('channel');
347 expect(
348 metadata,
349 equals(<String, dynamic>{
350 'id': 'dart_ui.library.element',
351 'element': 'element',
352 'sourcePath': 'unknown.dart',
353 'sourceLine': 1,
354 'serial': '',
355 'package': 'dart:ui',
356 'library': 'library',
357 }),
358 );
359
360 snippets_main.main(<String>[]);
361 expect(
362 errorMessage,
363 equals(
364 'The --input option must be specified, either on the command line, or in the INPUT environment variable.',
365 ),
366 );
367 errorMessage = '';
368
369 snippets_main.main(<String>['--input=${input.absolute.path}', '--type=snippet']);
370 expect(errorMessage, equals(''));
371 errorMessage = '';
372
373 mockSnippetGenerator = MockSnippetGenerator();
374 snippets_main.snippetGenerator = mockSnippetGenerator;
375 snippets_main.main(<String>[
376 '--input=${input.absolute.path}',
377 '--type=snippet',
378 '--no-format-output',
379 ]);
380 expect(mockSnippetGenerator.formatOutput, equals(false));
381 errorMessage = '';
382
383 input.deleteSync();
384 snippets_main.main(<String>['--input=${input.absolute.path}']);
385 expect(errorMessage, equals('The input file ${input.absolute.path} does not exist.'));
386 errorMessage = '';
387 });
388 });
389}
390
391class MockSnippetGenerator extends SnippetGenerator {
392 late CodeSample sample;
393 File? output;
394 String? copyright;
395 String? description;
396 late bool formatOutput;
397 late bool addSectionMarkers;
398 late bool includeAssumptions;
399
400 @override
401 String generateCode(
402 CodeSample sample, {
403 File? output,
404 String? copyright,
405 String? description,
406 bool formatOutput = true,
407 bool addSectionMarkers = false,
408 bool includeAssumptions = false,
409 }) {
410 this.sample = sample;
411 this.output = output;
412 this.copyright = copyright;
413 this.description = description;
414 this.formatOutput = formatOutput;
415 this.addSectionMarkers = addSectionMarkers;
416 this.includeAssumptions = includeAssumptions;
417
418 return '';
419 }
420
421 @override
422 String generateHtml(CodeSample sample) {
423 return '';
424 }
425}
426