Add example test, update example READMEs (#91130)

This commit is contained in:
Greg Spencer 2021-10-04 13:53:02 -07:00 committed by GitHub
parent 2bab6514b0
commit 983cbb273b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 133 additions and 273 deletions

View file

@ -674,6 +674,7 @@ Future<void> _runFrameworkTests() async {
workingDirectory: path.join(flutterRoot, 'examples', 'api'),
);
}
await _runFlutterTest(path.join(flutterRoot, 'examples', 'api'), options: soundNullSafetyOptions);
await _runFlutterTest(path.join(flutterRoot, 'examples', 'hello_world'), options: soundNullSafetyOptions);
await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers'), options: soundNullSafetyOptions);
}

View file

@ -1,238 +0,0 @@
# Dartdoc Generation
The Flutter API documentation contains code blocks that help provide context or
a good starting point when learning to use any of Flutter's APIs.
To generate these code blocks, Flutter uses dartdoc tools to turn documentation
in the source code into API documentation, as seen on https://api.flutter.dev/.
## Table of Contents
- [Types of code blocks](#types-of-code-blocks)
- [Snippet tool](#snippet-tool)
- [Sample tool](#sample-tool)
- [Skeletons](#skeletons)
- [Test Doc Generation Workflow](#test-doc-generation-workflow)
## Types of code blocks
There are three kinds of code blocks.
* A `snippet`, which is a more or less context-free code snippet that we
magically determine how to analyze.
* A `dartpad` sample, which gets placed into a full-fledged application, and can
be executed inline in the documentation on the web page using
DartPad.
* A `sample`, which gets placed into a full-fledged application, but isn't
placed into DartPad in the documentation because it doesn't make sense to do
so.
Ideally, every sample is a DartPad sample, but some samples don't have any visual
representation and some just don't make sense that way (for example, sample
code for setting the system UI's notification area color on Android won't do
anything on the web).
### Snippet Tool
![Code snippet image](assets/code_snippet.png)
The code `snippet` tool generates a block containing a description and example
code. Here is an example of the code `snippet` tool in use:
```dart
/// {@tool snippet}
///
/// If the avatar is to have an image, the image should be specified in the
/// [backgroundImage] property:
///
/// ```dart
/// CircleAvatar(
/// backgroundImage: NetworkImage(userAvatarUrl),
/// )
/// ```
/// {@end-tool}
```
This will generate sample code that can be copied to the clipboard and added to
existing applications.
This uses the skeleton for [snippet](config/skeletons/snippet.html) snippets
when generating the HTML to put into the Dart docs.
#### Analysis
The `../bots/analyze_sample_code.dart` script finds code inside the `@tool
snippet` sections and uses the Dart analyzer to check them.
There are several kinds of sample code you can specify:
* Constructor calls, typically showing what might exist in a build method. These
will be inserted into an assignment expression assigning to a variable of type
"dynamic" and followed by a semicolon, for analysis.
* Class definitions. These start with "class", and are analyzed verbatim.
* Other code. It gets included verbatim, though any line that says `// ...` is
considered to separate the block into multiple blocks to be processed
individually.
The above means that it's tricky to include verbatim imperative code (e.g. a
call to a method) since it won't be valid to have such code at the top level.
Instead, wrap it in a function or even a whole class, or make it a valid
variable declaration.
You can declare code that should be included in the analysis but not shown in
the API docs by adding a comment "// Examples can assume:" to the file (usually
at the top of the file, after the imports), following by one or more
commented-out lines of code. That code is included verbatim in the analysis. For
example:
```dart
// Examples can assume:
// final BuildContext context;
// final String userAvatarUrl;
```
You can assume that the entire Flutter framework and most common
`dart:*` packages are imported and in scope; `dart:math` as `math` and
`dart:ui` as `ui`.
### Sample Tool
![Code sample image](assets/code_sample.png)
The code `sample` and `dartpad` tools can expand sample code into full Flutter
applications. These sample applications can be directly copied and used to
demonstrate the API's functionality in a sample application, or used with the
`flutter create` command to create a local project with the sample code. The
`dartpad` samples are embedded into the API docs web page and are live
applications in the API documentation.
```dart
/// {@tool sample --template=stateless_widget_material}
/// This example shows how to make a simple [FloatingActionButton] in a
/// [Scaffold], with a pink [backgroundColor] and a thumbs up [Icon].
///
/// ```dart
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: Text('Floating Action Button Sample'),
/// ),
/// body: Center(
/// child: Text('Press the button below!')
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () {
/// // Add your onPressed code here!
/// },
/// child: Icon(Icons.thumb_up),
/// backgroundColor: Colors.pink,
/// ),
/// );
/// }
/// ```
/// {@end-tool}
```
This uses the skeleton for [application](config/skeletons/sample.html)
snippets.
The `sample` and `dartpad` tools also allow for quick Flutter app generation
using the following command:
```bash
flutter create --sample=[directory.File.sampleNumber] [name_of_project_directory]
```
This command is displayed as part of the sample in the API docs.
#### Templates
To support showing an entire app when you click on the right tab of the
code sample UI, we have to be able to insert the `sample` or `dartpad` block
into the template and instantiate the right parts.
To do this, there is a [config/templates](config/templates) directory that
contains a list of templates. These templates represent an entire app that the
`sample` or `dartpad` can be placed into, basically a replacement for
`lib/main.dart` in a flutter app package.
For more information about how to create, use, or update templates, see
[config/templates/README.md](config/templates/README.md).
#### Analysis
The `../bots/analyze_sample_code.dart` script finds code inside the `@tool
sample` sections and uses the Dart analyzer to check them after applying the
specified template.
## Skeletons
A skeleton (concerning this tool) is an HTML template into which the Dart
code blocks and descriptions are interpolated.
There is currently one skeleton for
[application](config/skeletons/sample.html) samples, one for
[dartpad](config/skeletons/dartpad-sample.html), and one for
[snippet](config/skeletons/snippet.html) code samples, but there could be more.
Skeletons use mustache notation (e.g. `{{code}}`) to mark where components will
be interpolated into the template. It doesn't use the mustache
package since these are simple string substitutions, but it uses the same
syntax.
The code block generation tools that process the source input and emit HTML for
output, which dartdoc places back into the documentation. Any options given to
the `{@tool ...}` directive are passed on verbatim to the tool.
The `snippets` tool renders these examples through a combination of markdown
and HTML using the `{@inject-html}` dartdoc directive.
## Test Doc Generation Workflow
If you are making changes to an existing code block or are creating a new code
block, follow these steps to generate a local copy of the API docs and verify
that your code blocks are showing up correctly:
1. Make an update to a code block or create a new code block.
2. From the root directory, run `./dev/bots/docs.sh`. This should start
generating a local copy of the API documentation.
3. Once complete, check `./dev/docs/doc` to check your API documentation. The
search bar will not work locally, so open `./dev/docs/doc/index.html` to
navigate through the documentation, or search `./dev/docs/doc/flutter` for
your page of interest.
Note that generating the sample output will not allow you to run your code in
DartPad, because DartPad pulls the code it runs from the appropriate docs server
(master or stable).
Copy the generated code and paste it into a regular DartPad instance to test if
it runs in DartPad. To get the code that will be produced by your documentation
changes, run sample analysis locally (see the next section) and paste the output
into a DartPad at https://dartpad.dartlang.org.
## Running sample analysis locally
If all you want to do is analyze the sample code you have written locally, then
generating the entire docs output takes a long time.
Instead, you can run the analysis locally with this command from the Flutter root:
```
TMPDIR=/tmp bin/cache/dart-sdk/bin/dart dev/bots/analyze_sample_code.dart --temp=samples
```
This will analyze the samples, and leave the generated files in `/tmp/samples`
You can find the sample you are working on in `/tmp/samples`. It is named using the
path to the file it is in, and the line of the file that the `{@tool ...}` directive
is on.
For example, the file `sample.src.widgets.animated_list.52.dart` points to the sample
in `packages/flutter/src/widgets/animated_list.dart` at line 52. You can then take the
contents of that file, and paste it into [Dartpad](https://dartpad.dev) and see if it
works. If the sample relies on new features that have just landed, it may not work
until the features make it into the `dev` branch.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View file

@ -1,67 +1,135 @@
# API Example Code
This directory contains the API sample code that is referenced from the
API documentation in each class.
This directory contains the API sample code that is referenced from the API
documentation in the framework.
They can be run individually by just specifying the path to the example on the
command line (or in the run configuration of an IDE).
The examples can be run individually by just specifying the path to the example
on the command line (or in the run configuration of an IDE).
For example, to run, in Chrome, the first example from the `Curve2D` class, you
would run it like so, from this [api](.) directory:
For example (no pun intended!), to run, the first example from the `Curve2D`
class in Chrome, you would run it like so from the [api](.) directory:
```
% flutter run -d chrome lib/animation/curves/curve2_d.0.dart
```
These same examples are available on the API docs site. For instance, the
example above is available on [this
page](https://api.flutter.dev/flutter/animation/Curve2D-class.html#animation.Curve2D.1).
Most of them are available as interactive examples in Dartpad, but some just
don't make sense on the web, and so are available as standalone examples that
can be run here.
All of these same examples are available on the API docs site. For instance, the
example above is available on [this page](
https://api.flutter.dev/flutter/animation/Curve2D-class.html#animation.Curve2D.1).
Most of the samples are available as interactive examples in
[Dartpad](https://dartpad.dev), but some (the ones marked with `{@tool sample}`
in the framework source code), just don't make sense on the web, and so are
available as standalone examples that can be run here. For instance, setting the
system overlay style doesn't make sense on the web (it only changes the
notification area background color on Android), so you can run the example for
that on an Android device like so:
```
% flutter run -d MyAndroidDevice lib/services/system_chrome/system_chrome.set_system_u_i_overlay_style.1.dart
```
## Naming
The naming scheme for the files is similar to the hierarchy under
[packages/flutter/lib/src](../../packages/flutter/lib/src), except that the
files are represented as directories, and each sample in each file as a separate
file in that directory. So, for the example above, the examples are from the
files are represented as directories (without the `.dart` suffix), and each
sample in the file is a separate file in that directory. So, for the example
above, where the examples are from the
[packages/flutter/lib/src/animation/curves.dart](../../packages/flutter/lib/src/animation/curves.dart)
file, the `Curve2D` class, and the first sample (hence the index "0") for that
symbol resides in the
[lib/animation/curves/curve2_d.0.dart](lib/animation/curves/curve2_d.0.dart)
file.
file, the `Curve2D` class, the first sample (hence the index "0") for that
symbol resides in the file named
[lib/animation/curves/curve2_d.0.dart](lib/animation/curves/curve2_d.0.dart).
Symbol names are converted from "CamelCase" to "snake_case". Dots are left
between symbol names, so the first example for symbol
`InputDecoration.prefixIconConstraints` would be converted to
`input_decoration.prefix_icon_constraints.0.dart`.
If the same example is linked to from multiple symbols, the source will be in
the canonical location for one of the symbols, and the link in the API docs
block for the other symbols will point to the first symbol's example location.
## Authoring
When authoring these examples, place a block like so in the Dartdoc
documentation for the symbol you would like to attach it to. Here's what it
might look like if you wanted to add a new example to the `Curve2D` class. First
add the stub to the symbol documentation:
> For more detailed information about authoring examples, see
> [the snippets package](https://pub.dev/packages/snippets).
When authoring examples, first place a block in the Dartdoc documentation for
the symbol you would like to attach it to. Here's what it might look like if you
wanted to add a new example to the `Curve2D` class:
```dart
/// {@tool dartpad --template=material_scaffold}
/// {@tool dartpad}
/// Write a description of the example here. This description will appear in the
/// API web documentation to introduce the example.
///
/// ** See code in examples/api/lib/animation/curves/curve2_d.0.dart **
/// {@end-tool}
```
The "See code in" line needs to be formatted exactly as above, with no wrapping
or newlines, one space after the "`**`" at the beginning, and one space before
the "`**`" at the end, and the words "See code in" at the beginning of the line.
This is what the snippets tool and the IDE use when finding the example source
code that you are creating.
Use `{@tool dartpad}` for Dartpad examples, and use `{@tool sample}` for
examples that shouldn't be run/shown in Dartpad.
Once that comment block is inserted in the source code, create a new file at the
appropriate path under [`examples/api`](.). You should also add tests for your
sample code under [`examples/api/test`](./test).
The entire example should be in a single file, so that Dartpad can load it.
Only packages that can be loaded by Dartpad may be imported. If you use one that
hasn't been used in an example before, you may have to add it to the
[pubspec.yaml](pubspec.yaml) in the api directory.
## Snippets
There is another type of example that can also be authored, using `{@tool
snippet}`. Snippet examples are just written inline in the source, like so:
```dart
/// {@tool dartpad}
/// Write a description of the example here. This description will appear in the
/// API web documentation to introduce the example.
///
/// ```dart
/// // These are the sections you want to fill out in the template.
/// // They will be transferred to the example file when you extract it.
/// // Sample code goes here, e.g.:
/// const Widget emptyBox = SizedBox();
/// ```
/// {@end-tool}
```
Then install the `extract_sample` command with:
The source for these snippets isn't stored under the [`examples/api`](.)
directory, or available in Dartpad in the API docs, since they're not intended
to be runnable, they just show some incomplete snippet of example code. It must
compile (in the context of the sample analyzer), but doesn't need to do
anything. See [the snippets documentation](
https://pub.dev/packages/snippets#snippet-tool) for more information about the
context that the analyzer uses.
```
% pub global activate snippets
```
## Writing Tests
And run the `extract_sample` command from the Flutter repo dir:
Examples are required to have tests. There is already a "smoke test" that runs
all the API examples, just to make sure that they start up without crashing. In
addition, we also require writing tests of functionality in the examples, and
generally just do what we normally do for writing tests. The one thing that
makes it more challenging for the examples is that they can't really be written
for testability in any obvious way, since that would probably complicate the
example and make it harder to explain.
```
$ pub global run extract_sample packages/flutter/lib/src/animation/curves.dart
```
As an example, in regular framework code, you might include a parameter for a
`Platform` object that can be overridden by a test to supply a dummy platform,
but in the example, this would be unnecessary complexity for the example. In all
other ways, these are just normal tests.
This will create a new file in the `examples/api` directory, in this case it
would create `examples/api/lib/animation/curves/curve2_d.1.dart`
Tests go into a directory under [test](./test) that matches their location under
[lib](./lib). They are named the same as the example they are testing, with
`_test.dart` at the end, like other tests. For instance, a `LayoutBuilder`
example that resides in [`lib/widgets/layout_builder/layout_builder.0.dart`](
./lib/widgets/layout_builder/layout_builder.0.dart) would have its tests in a
file named [`test/animation/curves/curve2_d.0_test.dart`](
./test/widgets/layout_builder/layout_builder.0_test.dart)

View file

@ -0,0 +1,29 @@
// Copyright 2014 The Flutter Authors. 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:flutter/widgets.dart';
import 'package:flutter_api_samples/widgets/layout_builder/layout_builder.0.dart' as example;
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('has two containers when wide', (WidgetTester tester) async {
await tester.pumpWidget(
const example.MyApp(),
);
final Finder containerFinder = find.byType(Container);
expect(containerFinder, findsNWidgets(2));
});
testWidgets('has one container when narrow', (WidgetTester tester) async {
await tester.pumpWidget(
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: const example.MyApp(),
),
);
final Finder containerFinder = find.byType(Container);
expect(containerFinder, findsNWidgets(2));
});
}