Flutter巡逻查找插件patrol_finders的使用

发布于 1周前 作者 vueper 来自 Flutter

Flutter巡逻查找插件patrol_finders的使用

简介

patrol_finders 是基于 flutter_test 的高级API,旨在简化Flutter小部件测试。它引入了一套新的自定义查找器系统,使测试更加简洁易懂,编写测试代码也变得更快、更有趣。

patrol_finders on pub.dev codestyle

更多文档请参阅:patrol.leancode.co

安装

在命令行中执行以下命令来安装 patrol_finders

$ dart pub add patrol_finders --dev

使用示例

示例应用

首先,我们创建一个简单的Flutter应用程序作为测试对象。这个应用程序包含了一个计数器和一些按钮,用于触发不同的操作。

import 'package:flutter/material.dart';

void main() {
  runApp(const ExampleApp());
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      darkTheme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.dark,
      ),
      theme: ThemeData(
        primarySwatch: Colors.blue,
        brightness: Brightness.light,
      ),
      home: const ExampleHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class ExampleHomePage extends StatefulWidget {
  const ExampleHomePage({super.key, required this.title});

  final String title;

  @override
  State<ExampleHomePage> createState() => _ExampleHomePageState();
}

class _ExampleHomePageState extends State<ExampleHomePage> {
  var _counter = 0;

  void _incrementCounter([int value = 1]) {
    setState(() {
      _counter += value;
    });
  }

  void _decrementCounter([int value = 1]) {
    setState(() {
      _counter -= value;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      key: const Key('scaffold'),
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView(
        padding: EdgeInsets.all(8),
        key: const Key('listViewKey'),
        children: [
          const Text(
            'You have pushed the button this many times:',
          ),
          Text(
            '$_counter',
            key: const Key('counterText'),
            style: Theme.of(context).textTheme.headlineMedium,
          ),
          const TextField(
            key: Key('textField'),
            decoration: InputDecoration(
              border: OutlineInputBorder(),
              hintText: 'You have entered this text',
            ),
          ),
          SizedBox(height: 8),
          Container(
            key: const Key('box1'),
            color: Theme.of(context).colorScheme.surface,
            padding: const EdgeInsets.all(8),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('box 1'),
                ListTile(
                  onTap: () => _incrementCounter(10),
                  key: const Key('tile1'),
                  title: const Text('Add'),
                  trailing: IconButton(
                    icon: const Icon(Icons.add, key: Key('icon1')),
                    onPressed: _incrementCounter,
                  ),
                ),
                ListTile(
                  onTap: () => _decrementCounter(10),
                  key: const Key('tile2'),
                  title: const Text('Subtract'),
                  trailing: IconButton(
                    icon: const Icon(Icons.remove, key: Key('icon2')),
                    onPressed: _decrementCounter,
                  ),
                ),
              ],
            ),
          ),
          const SizedBox(height: 16),
          Container(
            key: const Key('box2'),
            color: Theme.of(context).colorScheme.surface,
            padding: const EdgeInsets.all(8),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text('box 2'),
                ListTile(
                  onTap: () => _incrementCounter(10),
                  key: const Key('tile1'),
                  title: const Text('Add'),
                  trailing: IconButton(
                    icon: const Icon(Icons.add, key: Key('icon1')),
                    onPressed: _incrementCounter,
                  ),
                ),
                ListTile(
                  onTap: () => _decrementCounter(10),
                  key: const Key('tile2'),
                  title: const Text('Subtract'),
                  trailing: IconButton(
                    icon: const Icon(Icons.remove, key: Key('icon2')),
                    onPressed: _decrementCounter,
                  ),
                ),
              ],
            ),
          ),
          TextButton(
            onPressed: () async => Navigator.of(context).push(
              MaterialPageRoute<void>(
                builder: (_) => const LoadingScreen(),
              ),
            ),
            child: const Text('Open loading screen'),
          ),
          TextButton(
            onPressed: () async => Navigator.of(context).push(
              MaterialPageRoute<void>(
                builder: (_) => const OverlayScreen(),
              ),
            ),
            child: const Text('Open overlay screen'),
          ),
          TextButton(
            onPressed: () async => Navigator.of(context).push(
              MaterialPageRoute<void>(
                builder: (_) => const ScrollingScreen(),
              ),
            ),
            child: const Text('Open scrolling screen'),
          ),
          Text('EXAMPLE_KEY: ${const String.fromEnvironment('EXAMPLE_KEY')}'),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

测试代码

接下来,我们将使用 patrol_finders 来编写测试代码。通过这些测试代码,我们可以验证应用程序的行为是否符合预期。

import 'package:example/main.dart';
import 'package:flutter/material.dart';
import 'package:patrol_finders/patrol_finders.dart';

void main() {
  patrolWidgetTest(
    'logs in successfully',
    ($) async {
      await $.pumpWidgetAndSettle(const ExampleApp());

      /// Finds widget with Key('emailInput') and enters text into it
      ///
      await $(#emailInput).enterText('user@leancode.co');

      /// Finds widget with Key('passwordInput') and enters text into it
      await $(#passwordInput).enterText('ny4ncat');

      // Finds all widgets with text 'Log in' which are descendants of widgets
      // with Key('box1'), which are descendants of a Scaffold widget, and taps
      // on the first 'Log in' text.
      await $(Scaffold).$(#box1).$('Log in').tap();

      // Finds all Scrollables which have Text descendant, and taps on the first
      // Scrollable
      await $(Scrollable).containing(Text).tap();

      // Finds all Scrollables which have ElevatedButton descendant and Text
      // descendant, and taps on the first Scrollable
      await $(Scrollable).containing(ElevatedButton).containing(Text).tap();

      // Finds all Scrollables which have TextButton descendant which has Text
      // descendant, and taps on the first Scrollable
      await $(Scrollable).containing($(TextButton).$(Text)).tap();
    },
  );

  patrolWidgetTest(
    'increments counter',
    ($) async {
      await $.pumpWidgetAndSettle(const ExampleApp());

      // Taps on the floating action button to increment the counter
      await $(FloatingActionButton).tap();

      // Verifies that the counter has been incremented
      expect(await $(Text).withText('1').exists, true);
    },
  );

  patrolWidgetTest(
    'enters text into TextField',
    ($) async {
      await $.pumpWidgetAndSettle(const ExampleApp());

      // Enters text into the TextField
      await $(TextField).enterText('Hello, World!');

      // Verifies that the text has been entered
      expect(await $(TextField).hasText('Hello, World!'), true);
    },
  );
}

自定义查找器

patrol_finders 提供了丰富的自定义查找器功能,可以方便地定位和操作UI元素。例如:

  • $(#key):通过键查找小部件。
  • $(Text):通过文本查找小部件。
  • $(Scaffold):通过父级小部件查找子级小部件。
  • $(Scrollable).containing(...):查找包含特定子级的小部件。

通过这些查找器,你可以轻松地编写复杂的测试用例,确保应用程序的各个部分都能正常工作。

总结

patrol_finders 是一个非常强大的工具,可以帮助你更高效地编写和维护Flutter应用程序的测试代码。通过使用它的自定义查找器系统,你可以快速定位和操作UI元素,从而提高测试的准确性和可靠性。

如果你对 patrol_finders 感兴趣,建议阅读官方文档以了解更多详细信息和高级用法:patrol.leancode.co


更多关于Flutter巡逻查找插件patrol_finders的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter巡逻查找插件patrol_finders的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中使用patrol_finders插件的示例代码。请注意,由于patrol_finders并非一个广为人知的Flutter插件,假设它的功能类似于搜索或定位设备上的某些项目(如文件、设备、用户等),以下代码将基于这种假设进行编写。

首先,确保在pubspec.yaml文件中添加patrol_finders依赖(假设它存在于pub.dev或你的私有包仓库中):

dependencies:
  flutter:
    sdk: flutter
  patrol_finders: ^x.y.z  # 替换为实际版本号

然后运行flutter pub get来安装依赖。

接下来,在你的Flutter项目中,你可以按照以下方式使用patrol_finders插件:

import 'package:flutter/material.dart';
import 'package:patrol_finders/patrol_finders.dart';  // 假设插件提供了这个导入路径

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Patrol Finders Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PatrolFindersScreen(),
    );
  }
}

class PatrolFindersScreen extends StatefulWidget {
  @override
  _PatrolFindersScreenState createState() => _PatrolFindersScreenState();
}

class _PatrolFindersScreenState extends State<PatrolFindersScreen> {
  final PatrolFindersClient _patrolFindersClient = PatrolFindersClient();
  List<FinderResult> _results = [];
  String _query = "";

  void _search() async {
    setState(() {
      _results = [];  // 清空之前的结果
    });
    try {
      FinderResultList resultList = await _patrolFindersClient.search(_query);
      setState(() {
        _results = resultList.results;
      });
    } catch (e) {
      print("Search error: $e");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Patrol Finders Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          children: [
            TextField(
              decoration: InputDecoration(
                labelText: 'Search',
                suffixIcon: IconButton(
                  icon: Icon(Icons.search),
                  onPressed: _search,
                ),
              ),
              onChanged: (value) {
                setState(() {
                  _query = value;
                });
              },
            ),
            SizedBox(height: 16),
            Expanded(
              child: _results.isEmpty
                  ? Center(child: Text('No results found'))
                  : ListView.builder(
                      itemCount: _results.length,
                      itemBuilder: (context, index) {
                        FinderResult result = _results[index];
                        return ListTile(
                          title: Text(result.name),
                          subtitle: Text(result.description),
                          trailing: Icon(Icons.arrow_forward),
                          onTap: () {
                            // 处理点击事件,比如导航到详细页面
                          },
                        );
                      },
                    ),
            ),
          ],
        ),
      ),
    );
  }
}

// 假设PatrolFindersClient和FinderResult是从插件中获取的类
class PatrolFindersClient {
  Future<FinderResultList> search(String query) async {
    // 这里应该是调用插件的API进行搜索
    // 假设FinderResultList是一个包含FinderResult列表的类
    // 这里仅模拟返回一些数据
    return Future.value(FinderResultList(
      results: [
        FinderResult(name: "Item 1", description: "Description of item 1"),
        FinderResult(name: "Item 2", description: "Description of item 2"),
        // ... 更多模拟数据
      ],
    ));
  }
}

class FinderResultList {
  final List<FinderResult> results;

  FinderResultList({required this.results});
}

class FinderResult {
  final String name;
  final String description;

  FinderResult({required this.name, required this.description});
}

请注意,上述代码中的PatrolFindersClientFinderResultListFinderResult类是基于假设创建的,因为实际的patrol_finders插件可能具有不同的API和类结构。你需要根据插件的实际文档来调整这些类的定义和用法。

另外,如果patrol_finders插件有特定的初始化步骤或配置要求,请务必在代码中相应位置添加这些步骤。

回到顶部