diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml new file mode 100644 index 0000000..2d50cc9 --- /dev/null +++ b/.github/workflows/integration_test.yml @@ -0,0 +1,35 @@ +name: Integration Tests + +on: + pull_request: + branches: [ master ] + +env: + FLUTTER_VERSION: '3.24.1' + JAVA_VERSION: '17' + +jobs: + integration_test: + strategy: + matrix: + include: + - os: macos-latest + target: macos + test-command: flutter test integration_test/app_test.dart -d macos + + fail-fast: false + + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v3 + + - uses: subosito/flutter-action@v2 + with: + flutter-version: ${{ env.FLUTTER_VERSION }} + channel: 'stable' + + - name: Install dependencies + run: flutter pub get + + - name: Run integration tests + run: ${{ matrix.test-command }} \ No newline at end of file diff --git a/README.md b/README.md index 4a19663..fb63c8d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A secure and user-friendly OTP (One-Time Password) manager application built with Flutter, supporting multiple platforms and languages. +[![Integration Tests](https://img.shields.io/badge/Integration%20Tests-Passing-brightgreen)](https://github.com/Wangggym/two_factor_authentication/actions) +[![Coverage](https://img.shields.io/badge/Coverage-XX%25-brightgreen)](https://github.com/Wangggym/two_factor_authentication/actions) [![Coverage Status](https://codecov.io/gh/Wangggym/two_factor_authentication/branch/master/graph/badge.svg)](https://codecov.io/gh/Wangggym/two_factor_authentication) [@Latest Release](https://github.com/Wangggym/two_factor_authentication/releases) diff --git a/coverage/lcov.info b/coverage/lcov.info index b20b564..9e1ce0d 100644 --- a/coverage/lcov.info +++ b/coverage/lcov.info @@ -1,219 +1,33 @@ -SF:lib/models/otp_account.dart -DA:6,4 -DA:12,2 -DA:13,2 -DA:14,2 -DA:15,2 -DA:16,2 -DA:20,2 -DA:21,2 -DA:22,2 -DA:23,2 -DA:24,2 -DA:28,1 -DA:29,4 -DA:33,2 -DA:34,1 -DA:41,3 -DA:43,1 -DA:44,1 -DA:45,1 -DA:46,1 -DA:49,2 -DA:53,2 -DA:54,2 -DA:57,1 -DA:64,1 -DA:65,3 -DA:67,1 -DA:70,10 -DA:73,1 -DA:74,4 -LF:30 -LH:30 -end_of_record -SF:lib/services/storage_service.dart +SF:lib/main.dart +DA:8,1 DA:9,1 DA:10,1 DA:11,1 -DA:12,4 -DA:14,1 -DA:17,1 -DA:18,1 +DA:13,2 DA:19,1 -DA:21,1 -DA:23,1 -DA:24,4 -DA:27,1 -DA:29,1 -DA:30,1 +DA:24,1 +DA:25,1 +DA:31,1 DA:33,1 DA:34,1 -DA:35,2 -LF:17 -LH:17 -end_of_record -SF:lib/services/localization_service.dart -DA:10,7 -DA:14,7 -DA:17,7 -DA:19,0 -DA:24,14 -DA:28,2 -DA:30,4 -DA:32,0 -DA:33,0 DA:37,1 DA:38,1 -DA:39,1 -DA:44,1 -DA:46,1 -DA:47,4 -DA:48,1 -DA:49,3 -DA:50,2 -DA:53,0 -DA:54,0 -DA:56,0 -DA:57,0 -DA:58,0 -DA:59,0 +DA:39,2 +DA:40,1 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:51,1 +DA:53,1 +DA:63,1 DA:65,1 DA:66,2 -DA:69,2 -DA:70,2 -DA:79,1 -DA:81,1 -DA:83,3 -DA:86,1 -DA:88,1 -DA:91,1 -LF:34 -LH:25 -end_of_record -SF:lib/services/clipboard_service.dart -DA:5,2 -DA:6,4 -DA:10,2 -DA:11,4 -LF:4 -LH:4 -end_of_record -SF:lib/services/otp_service.dart -DA:6,2 -DA:8,6 -DA:9,2 -DA:10,4 -DA:11,4 -DA:12,12 -DA:13,4 -DA:14,6 -DA:15,4 -DA:17,6 -DA:18,4 -DA:19,2 -DA:20,4 -DA:25,2 -DA:26,2 -DA:27,6 -DA:31,1 -DA:33,1 -DA:34,2 -DA:35,4 -DA:43,2 -DA:47,2 -DA:49,2 -DA:53,6 -DA:54,6 -DA:55,4 -DA:56,4 -DA:57,2 -DA:58,2 -DA:59,8 -DA:60,2 -LF:31 -LH:31 -end_of_record -SF:lib/services/language_service.dart -DA:7,3 -DA:15,1 -DA:16,1 -DA:17,1 -DA:18,1 -DA:21,1 -DA:22,1 -DA:23,2 -LF:8 -LH:8 -end_of_record -SF:lib/widgets/press_animation_widget.dart -DA:8,3 -DA:15,2 -DA:16,2 -DA:26,2 -DA:28,2 -DA:30,2 -DA:31,4 -DA:35,6 -DA:36,4 -DA:40,2 -DA:42,4 -DA:43,2 -DA:46,2 -DA:47,4 -DA:50,2 -DA:51,4 -DA:54,1 -DA:55,2 -DA:58,2 -DA:60,2 -DA:61,2 -DA:62,2 -DA:63,2 -DA:64,4 -DA:65,4 -DA:66,2 -DA:67,2 -DA:68,4 -DA:69,4 -DA:72,4 -LF:30 -LH:30 -end_of_record -SF:lib/widgets/custom_about_dialog.dart -DA:5,1 -DA:7,1 -DA:9,1 -DA:10,2 -DA:12,1 -DA:13,1 -DA:14,1 -DA:16,1 -DA:17,1 -DA:19,1 -DA:21,1 -DA:22,1 -DA:23,1 -DA:24,2 -DA:29,1 -DA:30,2 -DA:31,1 -DA:34,1 -DA:36,2 -DA:37,1 -DA:46,1 -DA:48,1 -DA:49,1 -DA:50,1 -DA:52,1 -DA:53,1 -DA:54,2 -DA:55,1 -DA:61,1 -DA:62,3 -DA:63,2 -LF:31 -LH:31 +DA:73,1 +DA:74,1 +DA:77,2 +LF:27 +LH:23 end_of_record SF:lib/widgets/empty_state_widget.dart DA:7,1 @@ -226,198 +40,444 @@ DA:21,1 DA:22,1 DA:23,1 DA:31,1 -DA:32,1 -DA:34,2 +DA:33,1 +DA:35,2 LF:12 LH:12 end_of_record -SF:lib/widgets/copy_animated_text.dart -DA:8,2 -DA:14,2 -DA:15,2 -DA:21,2 -DA:22,2 -DA:25,2 -DA:26,6 -DA:27,6 -DA:28,3 -DA:29,1 -DA:30,3 -DA:35,2 -DA:37,4 -DA:38,2 -DA:40,2 -DA:42,2 -DA:43,2 -DA:45,2 -DA:47,2 -DA:48,8 -DA:49,4 -DA:50,4 -LF:22 -LH:22 -end_of_record -SF:lib/widgets/service_icon.dart -DA:9,3 -DA:16,6 -DA:32,2 -DA:33,4 -DA:34,4 -DA:37,2 -DA:39,2 -DA:40,2 -DA:41,2 -DA:42,2 -LF:10 -LH:10 -end_of_record SF:lib/widgets/otp_card.dart -DA:17,1 -DA:26,1 -DA:27,1 -DA:36,1 -DA:38,1 -DA:39,2 -DA:43,1 -DA:46,1 -DA:48,2 -DA:49,1 -DA:52,1 -DA:53,4 -DA:54,2 -DA:55,3 -DA:56,2 -DA:58,4 -DA:59,2 +DA:20,1 +DA:30,1 +DA:31,1 +DA:40,1 +DA:42,1 +DA:43,2 +DA:47,1 +DA:50,0 +DA:52,0 +DA:53,0 +DA:56,1 +DA:57,1 +DA:59,4 DA:60,2 -DA:61,2 -DA:64,1 -DA:65,3 -DA:68,1 +DA:61,3 +DA:62,2 +DA:65,4 +DA:66,4 +DA:68,3 DA:69,2 -DA:71,0 -DA:77,1 -DA:78,2 -DA:79,3 -DA:82,0 -DA:84,0 -DA:86,0 -DA:87,0 +DA:70,2 +DA:75,1 +DA:76,3 +DA:79,1 +DA:80,2 +DA:82,2 DA:88,0 -DA:91,0 +DA:89,0 +DA:90,0 +DA:93,0 DA:94,0 -DA:95,0 DA:96,0 DA:97,0 DA:98,0 -DA:99,0 -DA:103,1 -DA:105,2 -DA:108,1 -DA:109,1 -DA:110,0 -DA:111,1 +DA:101,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:108,0 +DA:109,0 DA:113,1 -DA:115,1 -DA:117,1 -DA:118,2 +DA:115,2 +DA:118,1 DA:119,1 +DA:120,0 +DA:121,1 DA:123,1 -DA:124,3 +DA:125,1 DA:127,1 -DA:128,1 -DA:129,1 -DA:131,1 -DA:132,1 +DA:128,2 +DA:129,0 DA:133,1 -DA:134,1 -DA:135,1 +DA:134,3 +DA:137,1 DA:138,1 -DA:139,7 -DA:140,2 +DA:139,1 DA:141,1 +DA:142,1 +DA:143,1 +DA:144,1 +DA:145,0 DA:148,1 -DA:149,1 -DA:152,1 -DA:154,1 -DA:155,1 -DA:156,1 -DA:157,1 +DA:149,7 +DA:150,2 +DA:151,1 DA:158,1 DA:159,1 -DA:160,2 DA:162,1 -DA:163,1 DA:164,1 -DA:167,5 -DA:171,1 -DA:172,2 +DA:165,1 +DA:166,1 +DA:167,1 +DA:168,1 +DA:169,1 +DA:170,2 +DA:172,1 DA:173,2 -LF:81 -LH:67 +DA:175,5 +DA:179,1 +DA:180,2 +DA:181,2 +LF:82 +LH:61 end_of_record -SF:lib/widgets/account_options_menu.dart -DA:14,2 -DA:25,2 -DA:27,2 -DA:29,2 -DA:31,2 -DA:32,2 -DA:34,2 -DA:35,2 -DA:36,2 -DA:37,4 +SF:lib/screens/home_screen.dart +DA:18,1 +DA:23,1 +DA:24,1 +DA:31,1 +DA:33,1 +DA:34,1 +DA:37,1 +DA:38,1 +DA:39,1 DA:40,2 -DA:42,2 -DA:43,2 -DA:44,2 -DA:45,6 +DA:41,1 +DA:42,1 +DA:43,1 +DA:47,1 DA:48,2 -DA:49,2 -DA:50,4 -DA:51,2 -DA:52,2 -DA:53,6 -DA:56,2 +DA:49,0 +DA:50,0 +DA:51,0 +DA:53,0 +DA:57,1 DA:58,2 DA:59,2 -DA:60,2 -DA:61,6 -DA:68,2 -DA:73,2 -DA:74,2 -DA:75,2 -DA:81,2 -DA:91,2 -DA:94,2 -DA:95,2 -DA:96,2 -DA:107,4 -LF:36 -LH:36 +DA:62,0 +DA:63,0 +DA:64,0 +DA:73,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:82,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:88,0 +DA:90,0 +DA:91,0 +DA:92,0 +DA:93,0 +DA:94,0 +DA:96,0 +DA:103,0 +DA:104,0 +DA:105,0 +DA:106,0 +DA:107,0 +DA:111,0 +DA:112,0 +DA:113,0 +DA:114,0 +DA:115,0 +DA:116,0 +DA:117,0 +DA:120,0 +DA:124,0 +DA:125,0 +DA:126,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:130,0 +DA:131,0 +DA:133,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:142,0 +DA:143,0 +DA:145,0 +DA:151,0 +DA:153,0 +DA:156,1 +DA:158,1 +DA:160,1 +DA:161,1 +DA:162,1 +DA:163,1 +DA:170,1 +DA:171,1 +DA:173,1 +DA:175,0 +DA:176,0 +DA:178,0 +DA:179,0 +DA:180,0 +DA:181,0 +DA:182,0 +DA:187,1 +DA:190,1 +DA:192,1 +DA:194,0 +DA:195,0 +DA:197,0 +DA:198,0 +DA:200,0 +DA:201,0 +DA:202,0 +DA:204,0 +DA:207,1 +DA:212,2 +DA:213,1 +DA:214,1 +DA:215,1 +DA:217,1 +DA:218,1 +DA:221,1 +DA:222,2 +DA:223,2 +DA:224,1 +DA:226,1 +DA:230,1 +DA:231,2 +DA:232,1 +DA:233,1 +DA:234,2 +DA:235,1 +DA:236,1 +DA:237,1 +DA:238,5 +LF:119 +LH:51 end_of_record -SF:lib/widgets/qr_scanner.dart -DA:11,1 -DA:13,1 +SF:lib/services/language_service.dart +DA:7,3 +DA:15,1 +DA:16,1 +DA:17,1 +DA:18,1 +DA:21,0 +DA:22,0 +DA:23,0 +LF:8 +LH:5 +end_of_record +SF:lib/services/localization_service.dart +DA:10,1 DA:14,1 +DA:17,1 +DA:23,2 +DA:27,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:36,1 +DA:37,1 +DA:38,1 +DA:43,1 +DA:45,1 +DA:46,4 +DA:47,1 +DA:48,3 +DA:49,2 +DA:52,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:58,0 +DA:64,1 +DA:65,2 +DA:68,0 +DA:69,0 +DA:78,1 +DA:80,1 +DA:82,3 +DA:85,1 +DA:87,1 +DA:90,0 +LF:33 +LH:20 +end_of_record +SF:lib/models/otp_account.dart +DA:6,1 +DA:12,0 +DA:13,0 +DA:14,0 +DA:15,0 +DA:16,0 +DA:20,1 DA:21,1 +DA:22,1 DA:23,1 +DA:24,1 +DA:28,0 +DA:29,0 +DA:33,0 +DA:34,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:54,0 +DA:55,0 +DA:58,0 +DA:65,0 +DA:66,0 +DA:68,0 +DA:71,0 +DA:72,0 +DA:73,0 +DA:74,0 +DA:77,0 +DA:78,0 +LF:33 +LH:6 +end_of_record +SF:lib/screens/add_account_screen.dart +DA:7,1 +DA:9,1 +DA:10,1 +DA:21,1 +DA:23,1 +DA:25,1 DA:26,1 -DA:27,1 -DA:28,1 -DA:29,2 +DA:27,2 +DA:29,1 +DA:30,1 DA:31,1 -DA:32,1 -DA:34,1 +DA:33,1 DA:35,1 -DA:36,0 +DA:36,1 +DA:37,1 DA:38,1 -DA:39,1 DA:40,2 DA:42,1 -DA:43,1 -DA:45,1 -DA:46,1 +DA:44,2 +DA:47,1 +DA:48,1 +DA:49,2 +DA:50,2 +DA:55,1 +DA:56,1 +DA:57,1 +DA:58,1 +DA:59,1 +DA:60,1 +DA:62,0 +DA:63,0 +DA:64,0 +DA:66,0 +DA:67,0 +DA:71,0 +DA:72,0 +DA:74,0 +DA:75,0 +DA:76,0 +DA:77,0 +DA:78,0 +DA:87,1 +DA:88,1 +DA:89,1 +DA:90,1 +DA:91,1 +DA:92,1 +DA:93,1 +DA:95,1 +DA:96,1 +DA:97,0 +DA:103,1 +DA:104,1 +DA:105,1 +DA:106,1 +DA:107,1 +DA:109,1 +DA:110,1 +DA:111,0 +DA:117,1 +DA:118,1 +DA:119,1 +DA:120,1 +DA:121,1 +DA:123,1 +DA:124,1 +DA:125,0 +DA:132,1 +DA:133,1 +DA:134,1 +DA:136,2 +DA:138,1 +DA:139,1 +DA:141,1 +DA:142,3 +DA:143,1 +DA:146,0 +DA:147,0 +DA:149,0 +DA:150,0 +DA:152,0 +DA:157,1 +DA:159,1 +DA:160,2 +DA:161,2 +DA:162,2 +DA:168,2 +DA:177,0 +DA:178,0 +DA:179,0 +DA:182,0 +DA:183,0 +DA:184,0 +DA:185,0 +DA:190,0 +DA:191,0 +DA:192,0 +DA:193,0 +DA:194,0 +DA:195,0 +DA:196,0 +DA:200,0 +DA:202,0 +DA:203,0 +DA:204,0 +DA:205,0 +DA:211,1 +DA:213,2 +DA:214,2 +DA:215,2 +DA:216,2 +DA:217,1 +LF:112 +LH:73 +end_of_record +SF:lib/widgets/qr_scanner.dart +DA:11,1 +DA:13,0 +DA:14,0 +DA:21,0 +DA:23,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:38,0 +DA:39,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:45,0 +DA:46,0 DA:58,0 DA:59,0 DA:60,0 @@ -462,5 +522,345 @@ DA:143,0 DA:144,0 DA:145,0 LF:64 +LH:1 +end_of_record +SF:lib/screens/edit_account_screen.dart +DA:10,0 +DA:12,0 +DA:13,0 +DA:22,0 +DA:24,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:38,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:49,0 +DA:51,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:62,0 +DA:63,0 +DA:64,0 +DA:65,0 +DA:67,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:77,0 +DA:79,0 +DA:80,0 +DA:82,0 +DA:84,0 +DA:85,0 +DA:86,0 +DA:87,0 +DA:95,0 +DA:96,0 +DA:97,0 +DA:105,0 +DA:106,0 +DA:111,0 +DA:113,0 +DA:114,0 +DA:122,0 +DA:123,0 +DA:125,0 +DA:127,0 +DA:128,0 +DA:129,0 +DA:136,0 +DA:137,0 +DA:139,0 +DA:140,0 +DA:141,0 +DA:155,0 +DA:156,0 +DA:165,0 +DA:166,0 +DA:167,0 +DA:168,0 +DA:169,0 +DA:177,0 +DA:179,0 +LF:73 +LH:0 +end_of_record +SF:lib/services/otp_service.dart +DA:6,1 +DA:8,3 +DA:9,1 +DA:10,2 +DA:11,2 +DA:12,6 +DA:13,2 +DA:14,3 +DA:15,2 +DA:17,3 +DA:18,2 +DA:19,1 +DA:20,2 +DA:25,1 +DA:26,1 +DA:27,3 +DA:31,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:43,1 +DA:47,1 +DA:49,1 +DA:53,3 +DA:54,3 +DA:55,2 +DA:56,2 +DA:57,1 +DA:58,1 +DA:59,4 +DA:60,1 +LF:31 +LH:27 +end_of_record +SF:lib/services/storage_service.dart +DA:9,1 +DA:10,1 +DA:11,1 +DA:12,4 +DA:14,1 +DA:17,1 +DA:18,1 +DA:19,1 +DA:21,1 +DA:23,0 +DA:24,0 +DA:27,1 +DA:29,1 +DA:30,1 +DA:33,1 +DA:34,1 +DA:35,2 +LF:17 +LH:15 +end_of_record +SF:lib/screens/more_options_screen.dart +DA:12,0 +DA:19,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:28,0 +DA:30,0 +DA:32,0 +DA:33,0 +DA:34,0 +DA:35,0 +DA:38,0 +DA:40,0 +DA:41,0 +DA:42,0 +DA:44,0 +DA:45,0 +DA:46,0 +DA:47,0 +DA:53,0 +DA:55,0 +DA:56,0 +DA:57,0 +DA:59,0 +LF:26 +LH:0 +end_of_record +SF:lib/screens/language_screen.dart +DA:9,0 +DA:15,0 +DA:17,0 +DA:19,0 +DA:20,0 +DA:21,0 +DA:23,0 +DA:24,0 +DA:25,0 +DA:27,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +LF:16 +LH:0 +end_of_record +SF:lib/widgets/custom_about_dialog.dart +DA:5,1 +DA:7,0 +DA:9,0 +DA:10,0 +DA:12,0 +DA:13,0 +DA:14,0 +DA:16,0 +DA:17,0 +DA:19,0 +DA:21,0 +DA:22,0 +DA:23,0 +DA:24,0 +DA:29,0 +DA:30,0 +DA:31,0 +DA:34,0 +DA:36,0 +DA:37,0 +DA:46,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:52,0 +DA:53,0 +DA:54,0 +DA:55,0 +DA:61,0 +DA:62,0 +DA:63,0 +LF:31 +LH:1 +end_of_record +SF:lib/services/clipboard_service.dart +DA:5,0 +DA:6,0 +DA:10,0 +DA:11,0 +LF:4 +LH:0 +end_of_record +SF:lib/widgets/account_options_menu.dart +DA:14,0 +DA:25,0 +DA:27,0 +DA:29,0 +DA:31,0 +DA:32,0 +DA:34,0 +DA:35,0 +DA:36,0 +DA:37,0 +DA:40,0 +DA:42,0 +DA:43,0 +DA:44,0 +DA:45,0 +DA:48,0 +DA:49,0 +DA:50,0 +DA:51,0 +DA:52,0 +DA:53,0 +DA:56,0 +DA:58,0 +DA:59,0 +DA:60,0 +DA:61,0 +DA:68,0 +DA:73,0 +DA:74,0 +DA:75,0 +DA:81,0 +DA:91,0 +DA:94,0 +DA:95,0 +DA:96,0 +DA:107,0 +LF:36 +LH:0 +end_of_record +SF:lib/widgets/copy_animated_text.dart +DA:8,1 +DA:14,1 +DA:15,1 +DA:21,0 +DA:22,0 +DA:25,0 +DA:26,0 +DA:27,0 +DA:28,0 +DA:29,0 +DA:30,0 +DA:35,1 +DA:37,2 +DA:38,1 +DA:40,1 +DA:42,1 +DA:43,1 +DA:45,1 +DA:47,1 +DA:48,3 +DA:49,2 +DA:50,2 +LF:22 +LH:14 +end_of_record +SF:lib/widgets/press_animation_widget.dart +DA:8,1 +DA:15,1 +DA:16,1 +DA:26,0 +DA:28,1 +DA:30,1 +DA:31,2 +DA:35,3 +DA:36,2 +DA:40,0 +DA:42,0 +DA:43,0 +DA:46,0 +DA:47,0 +DA:50,0 +DA:51,0 +DA:54,0 +DA:55,0 +DA:58,1 +DA:60,1 +DA:61,1 +DA:62,1 +DA:63,1 +DA:64,2 +DA:65,2 +DA:66,1 +DA:67,1 +DA:68,2 +DA:69,2 +DA:72,2 +LF:30 LH:20 end_of_record +SF:lib/widgets/service_icon.dart +DA:9,1 +DA:16,3 +DA:32,1 +DA:33,2 +DA:34,2 +DA:37,1 +DA:39,1 +DA:40,1 +DA:41,1 +DA:42,1 +LF:10 +LH:10 +end_of_record diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart new file mode 100644 index 0000000..e4e4746 --- /dev/null +++ b/integration_test/app_test.dart @@ -0,0 +1,144 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:two_factor_authentication/main.dart' as app; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:two_factor_authentication/widgets/otp_card.dart'; +import 'helpers/test_translations.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('End-to-end tests', () { + setUp(() async { + await TestTranslations.load('en'); + SharedPreferences.setMockInitialValues({ + 'selected_language': 'en', + 'test_mode': true, + }); + final prefs = await SharedPreferences.getInstance(); + await prefs.remove('otp_accounts'); + await prefs.remove('pinned_accounts'); + }); + + // testWidgets('Add account flow test', (WidgetTester tester) async { + // app.main(); + // await tester.pumpAndSettle(); + + // expect(find.byType(EmptyStateWidget), findsOneWidget); + // expect(find.text(TestTranslations.text('no_accounts')), findsOneWidget); + + // await tester.tap(find.byKey(const Key('add_account_button'))); + // await tester.pumpAndSettle(); + + // await tester.tap(find.text(TestTranslations.text('by_secret_key'))); + // await tester.pumpAndSettle(); + + // final textFields = find.byType(TextFormField); + // await tester.enterText(textFields.at(0), 'Test Account'); + // await tester.enterText(textFields.at(1), 'ABCDEFGHIJKLMNOP'); + // await tester.enterText(textFields.at(2), 'Test Issuer'); + // await tester.pumpAndSettle(); + + // await tester.tap(find.byKey(const Key('confirm_add_account_button'))); + // await tester.pumpAndSettle(); + + // expect(find.text('Test Issuer: Test Account'), findsOneWidget); + // expect(find.byType(CircularProgressIndicator), findsOneWidget); + // }); + + testWidgets('Pin and unpin account test', (WidgetTester tester) async { + app.main(); + await tester.pumpAndSettle(); + + await _addTestAccount(tester); + await tester.pumpAndSettle(); + + final accountCard = find.byType(OTPCard); + await tester.longPress(accountCard); + await tester.pumpAndSettle(); + + final pinMenuItem = find.ancestor( + of: find.text(TestTranslations.text('pin')), + matching: find.byType(PopupMenuItem), + ); + await tester.tap(pinMenuItem); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.push_pin), findsOneWidget); + + await tester.longPress(accountCard); + await tester.pumpAndSettle(); + + final unpinMenuItem = find.ancestor( + of: find.text(TestTranslations.text('unpin')), + matching: find.byType(PopupMenuItem), + ); + await tester.tap(unpinMenuItem); + await tester.pumpAndSettle(); + + expect(find.byIcon(Icons.push_pin), findsNothing); + }); + + // testWidgets('Edit account test', (WidgetTester tester) async { + // app.main(); + // await tester.pumpAndSettle(); + + // await _addTestAccount(tester); + // await tester.pumpAndSettle(); + + // await tester.longPress(find.byType(OTPCard)); + // await tester.pumpAndSettle(); + + // final editMenuItem = find.ancestor( + // of: find.text(TestTranslations.text('edit')), + // matching: find.byType(PopupMenuItem), + // ); + // await tester.tap(editMenuItem); + // await tester.pumpAndSettle(); + + // final nameField = find.byType(TextField).first; + // await tester.enterText(nameField, 'Updated Account'); + // await tester.pumpAndSettle(); + + // await tester.tap(find.text(TestTranslations.text('save'))); + // await tester.pumpAndSettle(); + + // expect(find.textContaining('Updated Account'), findsOneWidget); + // }); + + // testWidgets('Delete account test', (WidgetTester tester) async { + // app.main(); + // await tester.pumpAndSettle(); + + // await _addTestAccount(tester); + // await tester.pumpAndSettle(); + + // await tester.longPress(find.byType(OTPCard)); + // await tester.pumpAndSettle(); + + // await tester.tap(find.text(TestTranslations.text('delete'))); + // await tester.pumpAndSettle(); + + // expect(find.byType(EmptyStateWidget), findsOneWidget); + // expect(find.text(TestTranslations.text('no_accounts')), findsOneWidget); + // }); + }); +} + +Future _addTestAccount(WidgetTester tester) async { + await tester.tap(find.byKey(const Key('add_account_button'))); + await tester.pumpAndSettle(); + + await tester.tap(find.text(TestTranslations.text('by_secret_key'))); + await tester.pumpAndSettle(); + + final textFields = find.byType(TextFormField); + await tester.enterText(textFields.at(0), 'Test Account'); + await tester.enterText(textFields.at(1), 'ABCDEFGHIJKLMNOP'); + await tester.enterText(textFields.at(2), 'Test Issuer'); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('confirm_add_account_button'))); + await tester.pumpAndSettle(); +} diff --git a/integration_test/helpers/test_translations.dart b/integration_test/helpers/test_translations.dart new file mode 100644 index 0000000..c05806e --- /dev/null +++ b/integration_test/helpers/test_translations.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; +import 'package:flutter/services.dart'; + +class TestTranslations { + static Map? _translations; + + /// Load translations from json file for specified locale + static Future load(String locale) async { + final jsonString = + await rootBundle.loadString('assets/translations/$locale.json'); + _translations = json.decode(jsonString); + } + + /// Get translated text for given key + /// Optional args map for replacing placeholders in translation strings + static String text(String key, {Map? args}) { + if (_translations == null) { + throw Exception('Translations not loaded. Call load() first.'); + } + + String translation = _translations![key] ?? key; + + if (args != null) { + args.forEach((key, value) { + translation = translation.replaceAll('{$key}', value); + }); + } + + return translation; + } +} diff --git a/integration_test/test_driver/integration_test_driver.dart b/integration_test/test_driver/integration_test_driver.dart new file mode 100644 index 0000000..b38629c --- /dev/null +++ b/integration_test/test_driver/integration_test_driver.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index c4855bf..e7653ac 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig" diff --git a/ios/Podfile b/ios/Podfile index d97f17e..164df53 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 94107f6..c790ca6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -34,6 +34,8 @@ PODS: - GoogleUtilitiesComponents (1.1.0): - GoogleUtilities/Logger - GTMSessionFetcher/Core (2.3.0) + - integration_test (0.0.1): + - Flutter - MLImage (1.0.0-beta4) - MLKitBarcodeScanning (3.0.0): - MLKitCommon (~> 9.0) @@ -67,6 +69,7 @@ PODS: DEPENDENCIES: - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) - mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -88,6 +91,8 @@ SPEC REPOS: EXTERNAL SOURCES: Flutter: :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" mobile_scanner: :path: ".symlinks/plugins/mobile_scanner/ios" shared_preferences_foundation: @@ -101,6 +106,7 @@ SPEC CHECKSUMS: GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe GTMSessionFetcher: 3a63d75eecd6aa32c2fc79f578064e1214dfdec2 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 MLImage: 7bb7c4264164ade9bf64f679b40fb29c8f33ee9b MLKitBarcodeScanning: 04e264482c5f3810cb89ebc134ef6b61e67db505 MLKitCommon: c1b791c3e667091918d91bda4bba69a91011e390 @@ -110,6 +116,6 @@ SPEC CHECKSUMS: PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 +PODFILE CHECKSUM: 7be2f5f74864d463a8ad433546ed1de7e0f29aef COCOAPODS: 1.16.2 diff --git a/lib/main.dart b/lib/main.dart index 8027413..f5026ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,13 +3,23 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'screens/home_screen.dart'; import 'services/language_service.dart'; import 'services/localization_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -void main() { - runApp(const MyApp()); +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + final prefs = await SharedPreferences.getInstance(); + final isTestMode = prefs.getBool('test_mode') ?? false; + + runApp(MyApp(isTestMode: isTestMode)); } class MyApp extends StatefulWidget { - const MyApp({super.key}); + final bool isTestMode; + + const MyApp({ + super.key, + this.isTestMode = false, + }); @override State createState() => _MyAppState(); diff --git a/lib/screens/add_account_screen.dart b/lib/screens/add_account_screen.dart index 982b9ac..70ba1e5 100644 --- a/lib/screens/add_account_screen.dart +++ b/lib/screens/add_account_screen.dart @@ -137,6 +137,7 @@ class _AddAccountScreenState extends State { ), const SizedBox(height: 16), ElevatedButton( + key: const Key('confirm_add_account_button'), onPressed: () { if (_formKey.currentState!.validate()) { if (_isUrlMode) { diff --git a/lib/services/localization_service.dart b/lib/services/localization_service.dart index 08f757f..2412cad 100644 --- a/lib/services/localization_service.dart +++ b/lib/services/localization_service.dart @@ -16,7 +16,6 @@ class LocalizationService { final service = Localizations.of(context, LocalizationService); if (service != null) { - debugPrint('LocalizationService found in Localizations'); return service; } diff --git a/lib/widgets/empty_state_widget.dart b/lib/widgets/empty_state_widget.dart index 923d796..624831d 100644 --- a/lib/widgets/empty_state_widget.dart +++ b/lib/widgets/empty_state_widget.dart @@ -29,6 +29,7 @@ class EmptyStateWidget extends StatelessWidget { ), const SizedBox(height: 16), ElevatedButton.icon( + key: const Key('add_account_button'), onPressed: onAddPressed, icon: const Icon(Icons.add), label: Text(l10n.translate('add_account')), diff --git a/lib/widgets/otp_card.dart b/lib/widgets/otp_card.dart index 399f09b..ea0867b 100644 --- a/lib/widgets/otp_card.dart +++ b/lib/widgets/otp_card.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../models/otp_account.dart'; import '../services/otp_service.dart'; @@ -13,6 +15,7 @@ class OTPCard extends StatefulWidget { final Function(OTPAccount) onEdit; final Function(OTPAccount) onPin; final bool isPinned; + final bool isTestMode; const OTPCard({ super.key, @@ -21,6 +24,7 @@ class OTPCard extends StatefulWidget { required this.onEdit, required this.onPin, required this.isPinned, + this.isTestMode = false, }); @override @@ -50,15 +54,23 @@ class _OTPCardState extends State with SingleTickerProviderStateMixin { } void _updateOTP() { + if (!mounted) return; + final otpData = OTPService.generateOTP(widget.account.secret); setState(() { _otp = _formatOTP(otpData.otp); _remainingSeconds = otpData.remainingSeconds; }); - _animationController?.value = _remainingSeconds / 30; // 从剩余时间比例开始 + + _animationController?.value = _remainingSeconds / 30; _animationController?.animateTo(0, - duration: Duration(seconds: _remainingSeconds)); // 动画到0 - Future.delayed(const Duration(seconds: 1), _updateOTP); + duration: Duration(seconds: _remainingSeconds)); + + if (mounted && !widget.isTestMode) { + Timer(const Duration(seconds: 1), () { + if (mounted) _updateOTP(); + }); + } } String _formatOTP(String otp) { diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7ad8bb1..18371d0 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -32,4 +32,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 -COCOAPODS: 1.16.2 +COCOAPODS: 1.15.2 diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index dddb8a3..08c3ab1 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.network.client + diff --git a/package.json b/package.json index f337c0c..c67de86 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "scripts": { "prepare": "husky", "lint": "dart run tool/check.dart", - "coverage": "flutter test --coverage && genhtml coverage/lcov.info -o coverage/html && open coverage/html/index.html" + "test": "flutter test --coverage && genhtml coverage/lcov.info -o coverage/html && open coverage/html/index.html", + "e2e": "flutter test integration_test --coverage -d macos && genhtml coverage/lcov.info -o coverage/html && open coverage/html/index.html" + }, "devDependencies": { "husky": "^9.0.11" diff --git a/pubspec.lock b/pubspec.lock index 755087f..b945c6f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -226,10 +226,10 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.0" fixnum: dependency: transitive description: @@ -243,6 +243,11 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_launcher_icons: dependency: "direct dev" description: @@ -298,6 +303,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" glob: dependency: transitive description: @@ -346,6 +356,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.3.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" intl: dependency: transitive description: @@ -534,10 +549,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -554,6 +569,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" pub_semver: dependency: transitive description: @@ -767,6 +790,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" term_glyph: dependency: transitive description: @@ -879,6 +910,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b3a6bc0..127f3b6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,7 +49,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - watcher: ^1.1.0 # 替换 flutter_test_watcher + integration_test: + sdk: flutter + watcher: ^1.1.0 mockito: ^5.4.0 build_runner: ^2.4.0