diff --git a/packages/cross_file/CHANGELOG.md b/packages/cross_file/CHANGELOG.md new file mode 100644 index 0000000000..960c870c8a --- /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` 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 0000000000..2c91f14381 --- /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 0000000000..65bd418961 --- /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 0000000000..a3e2873e67 --- /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 0000000000..98c2f8c358 --- /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 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` that completes with the length in bytes. + Future 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 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 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 openRead([int? start, int? end]) { + throw UnimplementedError('openRead() has not been implemented.'); + } + + /// Get the last-modified time for the CrossFile + Future 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 0000000000..ef69af59fa --- /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([bytes]) + : Blob([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 lastModified() async => + Future.value(_lastModified); + + Future get _bytes async { + if (_data != null) { + return Future.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 length() async => _length ?? (await _bytes).length; + + @override + Future readAsString({Encoding encoding = utf8}) async { + return encoding.decode(await _bytes); + } + + @override + Future readAsBytes() async => + Future.value(await _bytes); + + @override + Stream 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 saveTo(String path) async { + // Create a DOM container where we can host the anchor. + _target = ensureInitialized('__x_file_dom_element'); + + // Create an 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 0000000000..91afac5d42 --- /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 0000000000..6d649ced82 --- /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 lastModified() { + if (_lastModified != null) { + return Future.value(_lastModified); + } + // ignore: avoid_slow_async_io + return _file.lastModified(); + } + + @override + Future 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 length() { + if (_length != null) { + return Future.value(_length); + } + return _file.length(); + } + + @override + Future readAsString({Encoding encoding = utf8}) { + if (_bytes != null) { + return Future.value(String.fromCharCodes(_bytes!)); + } + return _file.readAsString(encoding: encoding); + } + + @override + Future readAsBytes() { + if (_bytes != null) { + return Future.value(_bytes); + } + return _file.readAsBytes(); + } + + Stream _getBytes(int? start, int? end) async* { + final Uint8List bytes = _bytes!; + yield bytes.sublist(start ?? 0, end ?? bytes.length); + } + + @override + Stream openRead([int? start, int? end]) { + if (_bytes != null) { + return _getBytes(start, end); + } else { + return _file + .openRead(start ?? 0, end) + .map((List 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 0000000000..9440d8a0e5 --- /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 0000000000..6136bff39f --- /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 0000000000..5195602902 --- /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 0000000000..5dd01c177f --- /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 0000000000..43740f0f19 --- /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([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 0000000000..a8edbe530f --- /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(); + }); + }); +}