[cross_file] Move from flutter/plugins. (#305)
diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md
new file mode 100644
index 0000000..960c870
--- /dev/null
+++ b/packages/cross_file/CHANGELOG.md
@@ -0,0 +1,34 @@
+## 0.3.1+1
+
+* Rehomed to `flutter/packages` repository.
+
+## 0.3.1
+
+* Fix nullability of `XFileBase`'s `path` and `name` to match the
+  implementations to avoid potential analyzer issues.
+
+## 0.3.0
+
+* Migrated package to null-safety.
+* **breaking change** According to our unit tests, the API should be backwards-compatible. Some relevant changes were made, however:
+  * Web: `lastModified` returns the epoch time as a default value, to maintain the `Future<DateTime>` return type (and not `null`)
+
+## 0.2.1
+
+* Prepare for breaking `package:http` change.
+
+## 0.2.0
+
+* **breaking change** Make sure the `saveTo` method returns a `Future` so it can be awaited and users are sure the file has been written to disk.
+
+## 0.1.0+2
+
+* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276))
+
+## 0.1.0+1
+
+* Update Flutter SDK constraint.
+
+## 0.1.0
+
+* Initial open-source release.
diff --git a/packages/cross_file/LICENSE b/packages/cross_file/LICENSE
new file mode 100644
index 0000000..2c91f14
--- /dev/null
+++ b/packages/cross_file/LICENSE
@@ -0,0 +1,25 @@
+Copyright 2020 The Flutter Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+    * Neither the name of Google Inc. nor the names of its
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/packages/cross_file/README.md b/packages/cross_file/README.md
new file mode 100644
index 0000000..65bd418
--- /dev/null
+++ b/packages/cross_file/README.md
@@ -0,0 +1,34 @@
+# cross_file
+
+An abstraction to allow working with files across multiple platforms.
+
+# Usage
+
+Import `package:cross/cross_info.dart`, instantiate a `CrossFile` 
+using a path or byte array and use its methods and properties to 
+access the file and its metadata.
+
+Example:
+
+```dart
+import 'package:cross_file/cross_file.dart';
+
+final file = CrossFile('assets/hello.txt');
+
+print('File information:');
+print('- Path: ${file.path}');
+print('- Name: ${file.name}');
+print('- MIME type: ${file.mimeType}');
+
+final fileContent = await file.readAsString();
+print('Content of the file: ${fileContent}');  // e.g. "Moto G (4)"
+```
+
+You will find links to the API docs on the [pub page](https://pub.dev/packages/cross_file).
+
+## Getting Started
+
+For help getting started with Flutter, view our online
+[documentation](http://flutter.io/).
+
+For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code).
\ No newline at end of file
diff --git a/packages/cross_file/lib/cross_file.dart b/packages/cross_file/lib/cross_file.dart
new file mode 100644
index 0000000..a3e2873
--- /dev/null
+++ b/packages/cross_file/lib/cross_file.dart
@@ -0,0 +1,5 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'src/x_file.dart';
diff --git a/packages/cross_file/lib/src/types/base.dart b/packages/cross_file/lib/src/types/base.dart
new file mode 100644
index 0000000..98c2f8c
--- /dev/null
+++ b/packages/cross_file/lib/src/types/base.dart
@@ -0,0 +1,87 @@
+// Copyright 2018 The Chromium 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 'dart:convert';
+import 'dart:typed_data';
+
+/// The interface for a CrossFile.
+///
+/// A CrossFile is a container that wraps the path of a selected
+/// file by the user and (in some platforms, like web) the bytes
+/// with the contents of the file.
+///
+/// This class is a very limited subset of dart:io [File], so all
+/// the methods should seem familiar.
+abstract class XFileBase {
+  /// Construct a CrossFile
+  // ignore: avoid_unused_constructor_parameters
+  XFileBase(String? path);
+
+  /// Save the CrossFile at the indicated file path.
+  Future<void> saveTo(String path) {
+    throw UnimplementedError('saveTo has not been implemented.');
+  }
+
+  /// Get the path of the picked file.
+  ///
+  /// This should only be used as a backwards-compatibility clutch
+  /// for mobile apps, or cosmetic reasons only (to show the user
+  /// the path they've picked).
+  ///
+  /// Accessing the data contained in the picked file by its path
+  /// is platform-dependant (and won't work on web), so use the
+  /// byte getters in the CrossFile instance instead.
+  String get path {
+    throw UnimplementedError('.path has not been implemented.');
+  }
+
+  /// The name of the file as it was selected by the user in their device.
+  ///
+  /// Use only for cosmetic reasons, do not try to use this as a path.
+  String get name {
+    throw UnimplementedError('.name has not been implemented.');
+  }
+
+  /// For web, it may be necessary for a file to know its MIME type.
+  String? get mimeType {
+    throw UnimplementedError('.mimeType has not been implemented.');
+  }
+
+  /// Get the length of the file. Returns a `Future<int>` that completes with the length in bytes.
+  Future<int> length() {
+    throw UnimplementedError('.length() has not been implemented.');
+  }
+
+  /// Synchronously read the entire file contents as a string using the given [Encoding].
+  ///
+  /// By default, `encoding` is [utf8].
+  ///
+  /// Throws Exception if the operation fails.
+  Future<String> readAsString({Encoding encoding = utf8}) {
+    throw UnimplementedError('readAsString() has not been implemented.');
+  }
+
+  /// Synchronously read the entire file contents as a list of bytes.
+  ///
+  /// Throws Exception if the operation fails.
+  Future<Uint8List> readAsBytes() {
+    throw UnimplementedError('readAsBytes() has not been implemented.');
+  }
+
+  /// Create a new independent [Stream] for the contents of this file.
+  ///
+  /// If `start` is present, the file will be read from byte-offset `start`. Otherwise from the beginning (index 0).
+  ///
+  /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file.
+  ///
+  /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled.
+  Stream<Uint8List> openRead([int? start, int? end]) {
+    throw UnimplementedError('openRead() has not been implemented.');
+  }
+
+  /// Get the last-modified time for the CrossFile
+  Future<DateTime> lastModified() {
+    throw UnimplementedError('openRead() has not been implemented.');
+  }
+}
diff --git a/packages/cross_file/lib/src/types/html.dart b/packages/cross_file/lib/src/types/html.dart
new file mode 100644
index 0000000..ef69af5
--- /dev/null
+++ b/packages/cross_file/lib/src/types/html.dart
@@ -0,0 +1,143 @@
+// Copyright 2018 The Chromium 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 'dart:convert';
+import 'dart:html';
+import 'dart:typed_data';
+
+import 'package:meta/meta.dart';
+
+import '../web_helpers/web_helpers.dart';
+import './base.dart';
+
+/// A CrossFile that works on web.
+///
+/// It wraps the bytes of a selected file.
+class XFile extends XFileBase {
+  /// Construct a CrossFile object from its ObjectUrl.
+  ///
+  /// Optionally, this can be initialized with `bytes` and `length`
+  /// so no http requests are performed to retrieve files later.
+  ///
+  /// `name` needs to be passed from the outside, since we only have
+  /// access to it while we create the ObjectUrl.
+  XFile(
+    this.path, {
+    this.mimeType,
+    String? name,
+    int? length,
+    Uint8List? bytes,
+    DateTime? lastModified,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  })  : _data = bytes,
+        _length = length,
+        _overrides = overrides,
+        _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
+        name = name ?? '',
+        super(path);
+
+  /// Construct an CrossFile from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    this.mimeType,
+    String? name,
+    int? length,
+    DateTime? lastModified,
+    String? path,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  })  : _data = bytes,
+        _length = length,
+        _overrides = overrides,
+        _lastModified = lastModified ?? DateTime.fromMillisecondsSinceEpoch(0),
+        name = name ?? '',
+        super(path) {
+    if (path == null) {
+      final Blob blob = (mimeType == null)
+          ? Blob(<dynamic>[bytes])
+          : Blob(<dynamic>[bytes], mimeType);
+      this.path = Url.createObjectUrl(blob);
+    } else {
+      this.path = path;
+    }
+  }
+
+  @override
+  final String? mimeType;
+  @override
+  final String name;
+  @override
+  late String path;
+
+  final Uint8List? _data;
+  final int? _length;
+  final DateTime? _lastModified;
+
+  late Element _target;
+
+  final CrossFileTestOverrides? _overrides;
+
+  bool get _hasTestOverrides => _overrides != null;
+
+  @override
+  Future<DateTime> lastModified() async =>
+      Future<DateTime>.value(_lastModified);
+
+  Future<Uint8List> get _bytes async {
+    if (_data != null) {
+      return Future<Uint8List>.value(UnmodifiableUint8ListView(_data!));
+    }
+
+    // We can force 'response' to be a byte buffer by passing responseType:
+    final ByteBuffer? response =
+        (await HttpRequest.request(path, responseType: 'arraybuffer')).response;
+
+    return response?.asUint8List() ?? Uint8List(0);
+  }
+
+  @override
+  Future<int> length() async => _length ?? (await _bytes).length;
+
+  @override
+  Future<String> readAsString({Encoding encoding = utf8}) async {
+    return encoding.decode(await _bytes);
+  }
+
+  @override
+  Future<Uint8List> readAsBytes() async =>
+      Future<Uint8List>.value(await _bytes);
+
+  @override
+  Stream<Uint8List> openRead([int? start, int? end]) async* {
+    final Uint8List bytes = await _bytes;
+    yield bytes.sublist(start ?? 0, end ?? bytes.length);
+  }
+
+  /// Saves the data of this CrossFile at the location indicated by path.
+  /// For the web implementation, the path variable is ignored.
+  @override
+  Future<void> saveTo(String path) async {
+    // Create a DOM container where we can host the anchor.
+    _target = ensureInitialized('__x_file_dom_element');
+
+    // Create an <a> tag with the appropriate download attributes and click it
+    // May be overridden with CrossFileTestOverrides
+    final AnchorElement element = _hasTestOverrides
+        ? _overrides!.createAnchorElement(this.path, name) as AnchorElement
+        : createAnchorElement(this.path, name);
+
+    // Clear the children in our container so we can add an element to click
+    _target.children.clear();
+    addElementToContainerAndClick(_target, element);
+  }
+}
+
+/// Overrides some functions to allow testing
+@visibleForTesting
+class CrossFileTestOverrides {
+  /// Default constructor for overrides
+  CrossFileTestOverrides({required this.createAnchorElement});
+
+  /// For overriding the creation of the file input element.
+  Element Function(String href, String suggestedName) createAnchorElement;
+}
diff --git a/packages/cross_file/lib/src/types/interface.dart b/packages/cross_file/lib/src/types/interface.dart
new file mode 100644
index 0000000..91afac5
--- /dev/null
+++ b/packages/cross_file/lib/src/types/interface.dart
@@ -0,0 +1,60 @@
+// Copyright 2018 The Chromium 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 'dart:typed_data';
+import 'package:meta/meta.dart';
+
+import './base.dart';
+
+// ignore_for_file: avoid_unused_constructor_parameters
+
+/// A CrossFile is a cross-platform, simplified File abstraction.
+///
+/// It wraps the bytes of a selected file, and its (platform-dependant) path.
+class XFile extends XFileBase {
+  /// Construct a CrossFile object from its path.
+  ///
+  /// Optionally, this can be initialized with `bytes` and `length`
+  /// so no http requests are performed to retrieve data later.
+  ///
+  /// `name` may be passed from the outside, for those cases where the effective
+  /// `path` of the file doesn't match what the user sees when selecting it
+  /// (like in web)
+  XFile(
+    String path, {
+    String? mimeType,
+    String? name,
+    int? length,
+    Uint8List? bytes,
+    DateTime? lastModified,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  }) : super(path) {
+    throw UnimplementedError(
+        'CrossFile is not available in your current platform.');
+  }
+
+  /// Construct a CrossFile object from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    String? mimeType,
+    String? name,
+    int? length,
+    DateTime? lastModified,
+    String? path,
+    @visibleForTesting CrossFileTestOverrides? overrides,
+  }) : super(path) {
+    throw UnimplementedError(
+        'CrossFile is not available in your current platform.');
+  }
+}
+
+/// Overrides some functions of CrossFile for testing purposes
+@visibleForTesting
+class CrossFileTestOverrides {
+  /// Default constructor for overrides
+  CrossFileTestOverrides({required this.createAnchorElement});
+
+  /// For overriding the creation of the file input element.
+  dynamic Function(String href, String suggestedName) createAnchorElement;
+}
diff --git a/packages/cross_file/lib/src/types/io.dart b/packages/cross_file/lib/src/types/io.dart
new file mode 100644
index 0000000..6d649ce
--- /dev/null
+++ b/packages/cross_file/lib/src/types/io.dart
@@ -0,0 +1,119 @@
+// Copyright 2018 The Chromium 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 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import './base.dart';
+
+// ignore_for_file: avoid_unused_constructor_parameters
+
+/// A CrossFile backed by a dart:io File.
+class XFile extends XFileBase {
+  /// Construct a CrossFile object backed by a dart:io File.
+  XFile(
+    String path, {
+    this.mimeType,
+    String? name,
+    int? length,
+    Uint8List? bytes,
+    DateTime? lastModified,
+  })  : _file = File(path),
+        _bytes = null,
+        _lastModified = lastModified,
+        super(path);
+
+  /// Construct an CrossFile from its data
+  XFile.fromData(
+    Uint8List bytes, {
+    this.mimeType,
+    String? path,
+    String? name,
+    int? length,
+    DateTime? lastModified,
+  })  : _bytes = bytes,
+        _file = File(path ?? ''),
+        _length = length,
+        _lastModified = lastModified,
+        super(path) {
+    if (length == null) {
+      _length = bytes.length;
+    }
+  }
+
+  final File _file;
+  @override
+  final String? mimeType;
+  final DateTime? _lastModified;
+  int? _length;
+
+  final Uint8List? _bytes;
+
+  @override
+  Future<DateTime> lastModified() {
+    if (_lastModified != null) {
+      return Future<DateTime>.value(_lastModified);
+    }
+    // ignore: avoid_slow_async_io
+    return _file.lastModified();
+  }
+
+  @override
+  Future<void> saveTo(String path) async {
+    final File fileToSave = File(path);
+    await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes()));
+    await fileToSave.create();
+  }
+
+  @override
+  String get path {
+    return _file.path;
+  }
+
+  @override
+  String get name {
+    return _file.path.split(Platform.pathSeparator).last;
+  }
+
+  @override
+  Future<int> length() {
+    if (_length != null) {
+      return Future<int>.value(_length);
+    }
+    return _file.length();
+  }
+
+  @override
+  Future<String> readAsString({Encoding encoding = utf8}) {
+    if (_bytes != null) {
+      return Future<String>.value(String.fromCharCodes(_bytes!));
+    }
+    return _file.readAsString(encoding: encoding);
+  }
+
+  @override
+  Future<Uint8List> readAsBytes() {
+    if (_bytes != null) {
+      return Future<Uint8List>.value(_bytes);
+    }
+    return _file.readAsBytes();
+  }
+
+  Stream<Uint8List> _getBytes(int? start, int? end) async* {
+    final Uint8List bytes = _bytes!;
+    yield bytes.sublist(start ?? 0, end ?? bytes.length);
+  }
+
+  @override
+  Stream<Uint8List> openRead([int? start, int? end]) {
+    if (_bytes != null) {
+      return _getBytes(start, end);
+    } else {
+      return _file
+          .openRead(start ?? 0, end)
+          .map((List<int> chunk) => Uint8List.fromList(chunk));
+    }
+  }
+}
diff --git a/packages/cross_file/lib/src/web_helpers/web_helpers.dart b/packages/cross_file/lib/src/web_helpers/web_helpers.dart
new file mode 100644
index 0000000..9440d8a
--- /dev/null
+++ b/packages/cross_file/lib/src/web_helpers/web_helpers.dart
@@ -0,0 +1,38 @@
+// Copyright 2018 The Chromium 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 'dart:html';
+
+/// Create anchor element with download attribute
+AnchorElement createAnchorElement(String href, String? suggestedName) {
+  final AnchorElement element = AnchorElement(href: href);
+
+  if (suggestedName == null) {
+    element.download = 'download';
+  } else {
+    element.download = suggestedName;
+  }
+
+  return element;
+}
+
+/// Add an element to a container and click it
+void addElementToContainerAndClick(Element container, Element element) {
+  // Add the element and click it
+  // All previous elements will be removed before adding the new one
+  container.children.add(element);
+  element.click();
+}
+
+/// Initializes a DOM container where we can host elements.
+Element ensureInitialized(String id) {
+  Element? target = querySelector('#$id');
+  if (target == null) {
+    final Element targetElement = Element.tag('flt-x-file')..id = id;
+
+    querySelector('body')!.children.add(targetElement);
+    target = targetElement;
+  }
+  return target;
+}
diff --git a/packages/cross_file/lib/src/x_file.dart b/packages/cross_file/lib/src/x_file.dart
new file mode 100644
index 0000000..6136bff
--- /dev/null
+++ b/packages/cross_file/lib/src/x_file.dart
@@ -0,0 +1,7 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+export 'types/interface.dart'
+    if (dart.library.html) 'types/html.dart'
+    if (dart.library.io) 'types/io.dart';
diff --git a/packages/cross_file/pubspec.yaml b/packages/cross_file/pubspec.yaml
new file mode 100644
index 0000000..5195602
--- /dev/null
+++ b/packages/cross_file/pubspec.yaml
@@ -0,0 +1,18 @@
+name: cross_file
+description: An abstraction to allow working with files across multiple platforms.
+repository: https://github.com/flutter/packages/tree/master/packages/cross_file
+version: 0.3.1+1
+
+dependencies:
+  flutter:
+    sdk: flutter
+  meta: ^1.3.0
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  pedantic: ^1.10.0
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+  flutter: ">=1.22.0"
diff --git a/packages/cross_file/test/assets/hello.txt b/packages/cross_file/test/assets/hello.txt
new file mode 100644
index 0000000..5dd01c1
--- /dev/null
+++ b/packages/cross_file/test/assets/hello.txt
@@ -0,0 +1 @@
+Hello, world!
\ No newline at end of file
diff --git a/packages/cross_file/test/x_file_html_test.dart b/packages/cross_file/test/x_file_html_test.dart
new file mode 100644
index 0000000..43740f0
--- /dev/null
+++ b/packages/cross_file/test/x_file_html_test.dart
@@ -0,0 +1,108 @@
+// Copyright 2020 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.
+
+@TestOn('chrome') // Uses web-only Flutter SDK
+
+import 'dart:convert';
+import 'dart:html' as html;
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:cross_file/cross_file.dart';
+
+const String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
+final html.File textFile = html.File(<Object>[bytes], 'hello.txt');
+final String textFileUrl = html.Url.createObjectUrl(textFile);
+
+void main() {
+  group('Create with an objectUrl', () {
+    final XFile file = XFile(textFileUrl);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+  });
+
+  group('Create from data', () {
+    final XFile file = XFile.fromData(bytes);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+  });
+
+  group('saveTo(..)', () {
+    const String crossFileDomElementId = '__x_file_dom_element';
+
+    group('CrossFile saveTo(..)', () {
+      test('creates a DOM container', () async {
+        final XFile file = XFile.fromData(bytes);
+
+        await file.saveTo('');
+
+        final html.Element? container =
+            html.querySelector('#$crossFileDomElementId');
+
+        expect(container, isNotNull);
+      });
+
+      test('create anchor element', () async {
+        final XFile file = XFile.fromData(bytes, name: textFile.name);
+
+        await file.saveTo('path');
+
+        final html.Element? container =
+            html.querySelector('#$crossFileDomElementId');
+        final html.AnchorElement element = container?.children
+                .firstWhere((html.Element element) => element.tagName == 'A')
+            as html.AnchorElement;
+
+        // if element is not found, the `firstWhere` call will throw StateError.
+        expect(element.href, file.path);
+        expect(element.download, file.name);
+      });
+
+      test('anchor element is clicked', () async {
+        final html.AnchorElement mockAnchor = html.AnchorElement();
+
+        final CrossFileTestOverrides overrides = CrossFileTestOverrides(
+          createAnchorElement: (_, __) => mockAnchor,
+        );
+
+        final XFile file =
+            XFile.fromData(bytes, name: textFile.name, overrides: overrides);
+
+        bool clicked = false;
+        mockAnchor.onClick.listen((html.MouseEvent event) => clicked = true);
+
+        await file.saveTo('path');
+
+        expect(clicked, true);
+      });
+    });
+  });
+}
diff --git a/packages/cross_file/test/x_file_io_test.dart b/packages/cross_file/test/x_file_io_test.dart
new file mode 100644
index 0000000..a8edbe5
--- /dev/null
+++ b/packages/cross_file/test/x_file_io_test.dart
@@ -0,0 +1,90 @@
+// Copyright 2020 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.
+
+@TestOn('vm') // Uses dart:io
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:cross_file/cross_file.dart';
+
+final String pathPrefix =
+    Directory.current.path.endsWith('test') ? './assets/' : './test/assets/';
+final String path = pathPrefix + 'hello.txt';
+const String expectedStringContents = 'Hello, world!';
+final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents));
+final File textFile = File(path);
+final String textFilePath = textFile.path;
+
+void main() {
+  group('Create with a path', () {
+    final XFile file = XFile(textFilePath);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+
+    test('saveTo(..) creates file', () async {
+      final File removeBeforeTest = File(pathPrefix + 'newFilePath.txt');
+      if (removeBeforeTest.existsSync()) {
+        await removeBeforeTest.delete();
+      }
+
+      await file.saveTo(pathPrefix + 'newFilePath.txt');
+      final File newFile = File(pathPrefix + 'newFilePath.txt');
+
+      expect(newFile.existsSync(), isTrue);
+      expect(newFile.readAsStringSync(), 'Hello, world!');
+
+      await newFile.delete();
+    });
+  });
+
+  group('Create with data', () {
+    final XFile file = XFile.fromData(bytes);
+
+    test('Can be read as a string', () async {
+      expect(await file.readAsString(), equals(expectedStringContents));
+    });
+    test('Can be read as bytes', () async {
+      expect(await file.readAsBytes(), equals(bytes));
+    });
+
+    test('Can be read as a stream', () async {
+      expect(await file.openRead().first, equals(bytes));
+    });
+
+    test('Stream can be sliced', () async {
+      expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5)));
+    });
+
+    test('Function saveTo(..) creates file', () async {
+      final File removeBeforeTest = File(pathPrefix + 'newFileData.txt');
+      if (removeBeforeTest.existsSync()) {
+        await removeBeforeTest.delete();
+      }
+
+      await file.saveTo(pathPrefix + 'newFileData.txt');
+      final File newFile = File(pathPrefix + 'newFileData.txt');
+
+      expect(newFile.existsSync(), isTrue);
+      expect(newFile.readAsStringSync(), 'Hello, world!');
+
+      await newFile.delete();
+    });
+  });
+}