diff --git a/android/app/build.gradle b/android/app/build.gradle index 8ac7011..0182e53 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,7 +44,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.resocoder.flutter_testing_tutorial" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/build.gradle b/android/build.gradle index ed45c65..e7ac0d9 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -24,6 +24,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58a..cfe88f6 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/lib/article.dart b/lib/article.dart index 924b582..8791cbd 100644 --- a/lib/article.dart +++ b/lib/article.dart @@ -1,3 +1,4 @@ +/// A simple Article class with a title and content. class Article { final String title; final String content; @@ -7,6 +8,7 @@ class Article { required this.content, }); + /// Creates a copy of this article with the given fields replaced with the new values. Article copyWith({ String? title, String? content, @@ -20,6 +22,7 @@ class Article { @override String toString() => 'Article(title: $title, content: $content)'; + /// Compares two articles based on their titles and contents. @override bool operator ==(Object other) { if (identical(this, other)) return true; diff --git a/lib/article_page.dart b/lib/article_page.dart index a16d4de..a54bc8f 100644 --- a/lib/article_page.dart +++ b/lib/article_page.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_testing_tutorial/article.dart'; +/// A widget that displays an article's title and content. class ArticlePage extends StatelessWidget { + /// The article to be displayed. final Article article; + /// Creates an instance of [ArticlePage]. + /// + /// The [article] parameter is required and must not be null. const ArticlePage({ required this.article, Key? key, @@ -11,8 +16,12 @@ class ArticlePage extends StatelessWidget { @override Widget build(BuildContext context) { + // Get the media query data for responsive design final mq = MediaQuery.of(context); return Scaffold( + appBar: AppBar( + title: Text(article.title), + ), body: SingleChildScrollView( padding: EdgeInsets.only( top: mq.padding.top, @@ -22,11 +31,8 @@ class ArticlePage extends StatelessWidget { ), child: Column( children: [ - Text( - article.title, - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 8), + // Display the article title + // Display the article content Text(article.content), ], ), diff --git a/lib/main.dart b/lib/main.dart index f8bc0ea..a0d5aa5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,14 +6,24 @@ import 'package:flutter_testing_tutorial/news_service.dart'; void main() => runApp(MyApp()); +/// Root widget of the application +/// +/// This stateless widget sets up the basic app structure including: +/// - MaterialApp as the root +/// - Provider for state management +/// - Initial route to NewsPage class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, title: 'News App', + // Initialize NewsChangeNotifier with NewsService dependency home: ChangeNotifierProvider( create: (_) => NewsChangeNotifier(NewsService()), - child: NewsPage(), + child: const NewsPage(), ), ); } diff --git a/lib/news_change_notifier.dart b/lib/news_change_notifier.dart index b390b24..2355732 100644 --- a/lib/news_change_notifier.dart +++ b/lib/news_change_notifier.dart @@ -2,18 +2,27 @@ import 'package:flutter/material.dart'; import 'package:flutter_testing_tutorial/article.dart'; import 'package:flutter_testing_tutorial/news_service.dart'; +/// A class that extends [ChangeNotifier] to manage and notify listeners about news articles. class NewsChangeNotifier extends ChangeNotifier { + /// The service responsible for fetching news articles. final NewsService _newsService; + /// Creates an instance of [NewsChangeNotifier] with the given [NewsService]. NewsChangeNotifier(this._newsService); + /// A list of articles fetched from the news service. List
_articles = []; + /// Returns the list of articles. List
get articles => _articles; + /// Indicates whether articles are currently being loaded. bool _isLoading = false; + + /// Returns true if articles are being loaded, false otherwise. bool get isLoading => _isLoading; + /// Fetches articles from the news service and updates the state. Future getArticles() async { _isLoading = true; notifyListeners(); diff --git a/lib/news_page.dart b/lib/news_page.dart index e0c708a..d0219ac 100644 --- a/lib/news_page.dart +++ b/lib/news_page.dart @@ -3,7 +3,9 @@ import 'package:provider/provider.dart'; import 'package:flutter_testing_tutorial/article_page.dart'; import 'package:flutter_testing_tutorial/news_change_notifier.dart'; +/// A page that displays a list of news articles. class NewsPage extends StatefulWidget { + /// Creates a [NewsPage]. const NewsPage({Key? key}) : super(key: key); @override @@ -11,9 +13,11 @@ class NewsPage extends StatefulWidget { } class _NewsPageState extends State { + @override void initState() { super.initState(); + // Fetch articles when the widget is initialized. Future.microtask( () => context.read().getArticles(), ); @@ -21,9 +25,19 @@ class _NewsPageState extends State { @override Widget build(BuildContext context) { + // Build the UI for the NewsPage. return Scaffold( appBar: AppBar( title: const Text('News'), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () { + // Refresh articles when the refresh button is pressed. + context.read().getArticles(); + }, + ), + ], ), body: Consumer( builder: (context, notifier, child) { @@ -42,6 +56,7 @@ class _NewsPageState extends State { elevation: 2, child: InkWell( onTap: () { + // Navigate to the ArticlePage when an article is tapped. Navigator.of(context).push( MaterialPageRoute( builder: (_) => ArticlePage(article: article), diff --git a/lib/news_service.dart b/lib/news_service.dart index 5757e5b..b0563d7 100644 --- a/lib/news_service.dart +++ b/lib/news_service.dart @@ -3,17 +3,28 @@ import 'package:flutter_testing_tutorial/article.dart'; /// A News service simulating communication with a server. class NewsService { - // Simulating a remote database + /// In-memory list of articles that simulates a remote database. + /// Generates 10 articles with random lorem ipsum text for both + /// title and content. + final _articles = List.generate( 10, (_) => Article( - title: lorem(paragraphs: 1, words: 3), - content: lorem(paragraphs: 10, words: 500), + title: lorem(paragraphs: 1, words: 3), // Generate a 3-word title + content: lorem( + paragraphs: 7, words: 500), // Generate content with 10 paragraphs ), ); + /// Fetches all articles from the simulated database. + /// + /// Adds an artificial delay of 1 second to simulate network latency. + /// + /// Returns: + /// Future>: A list of Article objects after the delay. Future> getArticles() async { await Future.delayed(const Duration(seconds: 1)); return _articles; } + } diff --git a/macos/Podfile b/macos/Podfile index dade8df..049abe2 100644 --- a/macos/Podfile +++ b/macos/Podfile @@ -1,4 +1,4 @@ -platform :osx, '10.11' +platform :osx, '10.14' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index bc1750d..9a0269b 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXAggregateTarget section */ @@ -202,7 +202,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -255,6 +255,7 @@ /* Begin PBXShellScriptBuildPhase section */ 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -386,7 +387,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; @@ -465,7 +466,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; @@ -512,7 +513,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.11; + MACOSX_DEPLOYMENT_TARGET = 10.14; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index ff16f57..f2b5270 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { return true diff --git a/pubspec.lock b/pubspec.lock index f5746b0..d7a6221 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,114 +5,119 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" source: hosted - version: "30.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" source: hosted - version: "2.7.0" - archive: - dependency: transitive - description: - name: archive - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.2" + version: "6.7.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "0bd9a99b6eb96f07af141f0eb53eace8983e8e5aa5de59777aca31684680ef22" + url: "https://pub.dev" source: hosted version: "2.3.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.1" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + url: "https://pub.dev" source: hosted version: "3.0.1" coverage: dependency: transitive description: name: coverage - url: "https://pub.dartlang.org" + sha256: "4b03e11f6d5b8f6e5bb5e9f7889a56fe6c5cbe942da5378ea4d4d7f73ef9dfe5" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.11.0" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + url: "https://pub.dev" source: hosted version: "3.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" flutter: dependency: "direct main" description: flutter @@ -127,14 +132,16 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" source: hosted version: "1.0.4" flutter_lorem: dependency: "direct main" description: name: flutter_lorem - url: "https://pub.dartlang.org" + sha256: "004e1af5b030b632628d8cc1eb2f90451f48b6c0080f43069f69baa4959e9fe5" + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_test: @@ -146,9 +153,10 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -158,21 +166,24 @@ packages: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: bfb651625e251a88804ad6d596af01ea903544757906addcb2dcdf088b5ea185 + url: "https://pub.dev" source: hosted version: "3.0.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + url: "https://pub.dev" source: hosted version: "4.0.0" integration_test: @@ -184,154 +195,208 @@ packages: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "0d4c73c3653ab85bf696d51a9657604c900a370549196a91f33e4c39af760852" + url: "https://pub.dev" source: hosted version: "1.0.3" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" source: hosted - version: "0.6.3" + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" source: hosted version: "1.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "293ae2d49fd79d4c04944c3a26dfd313382d5f52e821ec57119230ae16031ad4" + url: "https://pub.dev" source: hosted version: "1.0.2" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" source: hosted - version: "0.12.10" + version: "0.11.1" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: fd5f81041e6a9fc9b9d7fa2cb8a01123f9f5d5d49136e06cb9dc7d33689529f4 + url: "https://pub.dev" source: hosted version: "1.0.1" mocktail: dependency: "direct dev" description: name: mocktail - url: "https://pub.dartlang.org" + sha256: dd85ca5229cf677079fd9ac740aebfc34d9287cdf294e6b2ba9fae25c39e4dc2 + url: "https://pub.dev" source: hosted version: "0.2.0" nested: dependency: transitive description: name: nested - url: "https://pub.dartlang.org" + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" source: hosted version: "1.0.0" node_preamble: dependency: transitive description: name: node_preamble - url: "https://pub.dartlang.org" + sha256: "8ebdbaa3b96d5285d068f80772390d27c21e1fa10fb2df6627b1b9415043608d" + url: "https://pub.dev" source: hosted version: "2.0.1" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: a4d5ede5ca9c3d88a2fef1147a078570c861714c806485c596b109819135bc12 + url: "https://pub.dev" source: hosted version: "2.0.2" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" source: hosted - version: "1.8.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "1.9.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.5" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "05955e3de2683e1746222efd14b775df7131139e07695dc8e24650f6b4204504" + url: "https://pub.dev" source: hosted version: "1.5.0" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "5.0.2" provider: dependency: "direct main" description: name: provider - url: "https://pub.dartlang.org" + sha256: dc18c7bddb94a1eb3c3154587d16175a657356c80566712e6cd8ca4825eae112 + url: "https://pub.dev" source: hosted version: "6.0.1" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c240984c924796e055e831a0a36db23be8cb04f170b26df572931ab36418421d + url: "https://pub.dev" source: hosted version: "1.2.0" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - url: "https://pub.dartlang.org" + sha256: e0b44ebddec91e70a713e13adf93c1b2100821303b86a18e1ef1d082bd8bd9b8 + url: "https://pub.dev" source: hosted version: "3.0.0" shelf_static: dependency: transitive description: name: shelf_static - url: "https://pub.dartlang.org" + sha256: "4a0d12cd512aa4fc55fed5f6280f02ef183f47ba29b4b0dfd621b1c99b7e6361" + url: "https://pub.dev" source: hosted version: "1.1.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: fd84910bf7d58db109082edf7326b75322b8f186162028482f53dc892f00332d + url: "https://pub.dev" source: hosted version: "1.0.1" sky_engine: @@ -343,135 +408,154 @@ packages: dependency: transitive description: name: source_map_stack_trace - url: "https://pub.dartlang.org" + sha256: "8c463326277f68a628abab20580047b419c2ff66756fd0affd451f73f9508c11" + url: "https://pub.dev" source: hosted version: "2.1.0" source_maps: dependency: transitive description: name: source_maps - url: "https://pub.dartlang.org" + sha256: "52de2200bb098de739794c82d09c41ac27b2e42fd7e23cce7b9c74bf653c7296" + url: "https://pub.dev" source: hosted version: "0.10.10" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" sync_http: dependency: transitive description: name: sync_http - url: "https://pub.dartlang.org" + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test: dependency: transitive description: name: test - url: "https://pub.dartlang.org" + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + url: "https://pub.dev" source: hosted - version: "1.17.10" + version: "1.25.7" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.7.2" test_core: dependency: transitive description: name: test_core - url: "https://pub.dartlang.org" + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.6.4" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - url: "https://pub.dartlang.org" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "14.2.5" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: "0c2ada1b1aeb2ad031ca81872add6be049b8cb479262c6ad3c4b0f9c24eaab2f" + url: "https://pub.dev" source: hosted version: "2.1.0" webdriver: dependency: transitive description: name: webdriver - url: "https://pub.dartlang.org" + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.3" webkit_inspection_protocol: dependency: transitive description: name: webkit_inspection_protocol - url: "https://pub.dartlang.org" + sha256: "5adb6ab8ed14e22bb907aae7338f0c206ea21e7a27004e97664b16c120306f00" + url: "https://pub.dev" source: hosted version: "1.0.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "3cee79b1715110341012d27756d9bae38e650588acd38d3f3c610822e1337ace" + url: "https://pub.dev" source: hosted version: "3.1.0" sdks: - dart: ">=2.14.0 <3.0.0" - flutter: ">=1.16.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/test/news_change_notifier_test.dart b/test/news_change_notifier_test.dart index c868e37..0c5fc20 100644 --- a/test/news_change_notifier_test.dart +++ b/test/news_change_notifier_test.dart @@ -4,57 +4,92 @@ import 'package:flutter_testing_tutorial/news_change_notifier.dart'; import 'package:flutter_testing_tutorial/news_service.dart'; import 'package:mocktail/mocktail.dart'; +/// A mock implementation of the [NewsService] class using mocktail. class MockNewsService extends Mock implements NewsService {} void main() { + /// The system under test (SUT), an instance of [NewsChangeNotifier]. late NewsChangeNotifier sut; + + /// A mock instance of [NewsService] used to simulate backend behavior. late MockNewsService mockNewsService; + /// Sets up the test environment by initializing [mockNewsService] + /// and injecting it into [NewsChangeNotifier]. setUp(() { mockNewsService = MockNewsService(); sut = NewsChangeNotifier(mockNewsService); }); + /// Test to ensure the initial values of [NewsChangeNotifier] are correct. test( "initial values are correct", () { + /// [articles] should be an empty list by default. expect(sut.articles, []); + + /// [isLoading] should be false initially. expect(sut.isLoading, false); }, ); + /// A group of tests related to the [getArticles] method in [NewsChangeNotifier]. group('getArticles', () { + /// Sample articles returned by the mock [NewsService]. final articlesFromService = [ Article(title: 'Test 1', content: 'Test 1 content'), Article(title: 'Test 2', content: 'Test 2 content'), Article(title: 'Test 3', content: 'Test 3 content'), ]; + /// A helper function to configure the mock [NewsService] to return + /// three articles when [getArticles] is called. void arrangeNewsServiceReturns3Articles() { when(() => mockNewsService.getArticles()).thenAnswer( (_) async => articlesFromService, ); } + /// Test to verify that [getArticles] calls the backend service exactly once. test( "gets articles using the NewsService", () async { + // Arrange arrangeNewsServiceReturns3Articles(); + + // Act await sut.getArticles(); + + // Assert verify(() => mockNewsService.getArticles()).called(1); }, ); + /// Test to validate the behavior of [getArticles] in terms of: + /// - Setting [isLoading] to true during data loading. + /// - Updating [articles] with the data returned by the service. + /// - Resetting [isLoading] to false after data is loaded. test( """indicates loading of data, sets articles to the ones from the service, indicates that data is not being loaded anymore""", () async { + // Arrange arrangeNewsServiceReturns3Articles(); + + // Act final future = sut.getArticles(); + + // Assert + /// [isLoading] should be true during the loading process. expect(sut.isLoading, true); + await future; + + /// [articles] should match the data returned by the service. expect(sut.articles, articlesFromService); + + /// [isLoading] should be false after data is loaded. expect(sut.isLoading, false); }, ); diff --git a/test/news_page_test.dart b/test/news_page_test.dart index 8b0b779..e5032f5 100644 --- a/test/news_page_test.dart +++ b/test/news_page_test.dart @@ -1,34 +1,39 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_testing_tutorial/article.dart'; -import 'package:flutter_testing_tutorial/main.dart'; import 'package:flutter_testing_tutorial/news_change_notifier.dart'; import 'package:flutter_testing_tutorial/news_page.dart'; import 'package:flutter_testing_tutorial/news_service.dart'; import 'package:mocktail/mocktail.dart'; import 'package:provider/provider.dart'; +/// A mock implementation of the [NewsService] class using mocktail. class MockNewsService extends Mock implements NewsService {} void main() { + /// Mock instance of [NewsService] used to simulate backend behavior. late MockNewsService mockNewsService; + /// Sets up the test environment by initializing [mockNewsService]. setUp(() { mockNewsService = MockNewsService(); }); + /// A sample list of articles that the mock service will return. final articlesFromService = [ Article(title: 'Test 1', content: 'Test 1 content'), Article(title: 'Test 2', content: 'Test 2 content'), - Article(title: 'Test 3', content: 'Test 3 content'), + Article(title: 'Test 3', content: 'Test 3 content') ]; + /// Configures the mock service to return three articles immediately. void arrangeNewsServiceReturns3Articles() { when(() => mockNewsService.getArticles()).thenAnswer( (_) async => articlesFromService, ); } + /// Configures the mock service to return three articles after a 2-second delay. void arrangeNewsServiceReturns3ArticlesAfter2SecondWait() { when(() => mockNewsService.getArticles()).thenAnswer( (_) async { @@ -38,49 +43,70 @@ void main() { ); } + /// Creates the widget under test, a MaterialApp wrapped with a + /// [ChangeNotifierProvider] to inject the mock [NewsService]. Widget createWidgetUnderTest() { return MaterialApp( title: 'News App', home: ChangeNotifierProvider( create: (_) => NewsChangeNotifier(mockNewsService), - child: NewsPage(), + child: const NewsPage(), ), ); } + /// Test to verify that the app title "News" is displayed correctly. testWidgets( "title is displayed", (WidgetTester tester) async { + // Arrange arrangeNewsServiceReturns3Articles(); + + // Act await tester.pumpWidget(createWidgetUnderTest()); + + // Assert expect(find.text('News'), findsOneWidget); }, ); + /// Test to verify that a loading indicator is displayed while waiting for articles to load. testWidgets( "loading indicator is displayed while waiting for articles", (WidgetTester tester) async { + // Arrange arrangeNewsServiceReturns3ArticlesAfter2SecondWait(); + // Act await tester.pumpWidget(createWidgetUnderTest()); + + // Simulate time passage for 500ms await tester.pump(const Duration(milliseconds: 500)); - expect(find.byKey(Key('progress-indicator')), findsOneWidget); + // Assert + expect(find.byKey(const Key('progress-indicator')), findsOneWidget); + // Wait for all animations to complete await tester.pumpAndSettle(); }, ); + /// Test to verify that articles are displayed correctly after being loaded. testWidgets( "articles are displayed", (WidgetTester tester) async { + // Arrange arrangeNewsServiceReturns3Articles(); + // Act await tester.pumpWidget(createWidgetUnderTest()); + // Allow the widget tree to rebuild await tester.pump(); + // Assert for (final article in articlesFromService) { + // Check if each article's title and content are found expect(find.text(article.title), findsOneWidget); expect(find.text(article.content), findsOneWidget); } diff --git a/web/index.html b/web/index.html index ceeda2d..48346bd 100644 --- a/web/index.html +++ b/web/index.html @@ -34,7 +34,7 @@ application. For more information, see: https://developers.google.com/web/fundamentals/primers/service-workers -->