mirror of
https://github.com/kuhyx/todo-app.git
synced 2026-11-11 15:54:15 +01:00
Glyph + spoken label replaces every app-written string the guard found: empty states, tile meta, sheet dates/fields, date presets, template/section headings, status footer, snackbars, GitHub and cloud-image results, upload/download badges, Material tooltips. Guard tests (text_free_*_test) walk the painted tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
4.6 KiB
Dart
149 lines
4.6 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:crdt_sync/crdt_sync.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:http/testing.dart';
|
|
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'package:todo/data/app_settings.dart';
|
|
import 'package:todo/sync/sync_settings.dart';
|
|
import 'package:todo/ui/github_mirror_screen.dart';
|
|
import 'package:url_launcher_platform_interface/link.dart';
|
|
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';
|
|
|
|
import 'fake_note_repository.dart';
|
|
import 'fake_secure_storage.dart';
|
|
|
|
import 'github_mirror_harness.dart';
|
|
import 'text_free_guard.dart';
|
|
|
|
/// Stub launcher that records the URL instead of opening it, so `_openPage`
|
|
/// can be exercised without a real platform channel.
|
|
class _FakeUrlLauncher extends UrlLauncherPlatform
|
|
with MockPlatformInterfaceMixin {
|
|
String? launched;
|
|
|
|
@override
|
|
final LinkDelegate? linkDelegate = null;
|
|
|
|
@override
|
|
Future<bool> supportsMode(PreferredLaunchMode mode) async => true;
|
|
|
|
@override
|
|
Future<bool> launchUrl(String url, LaunchOptions options) async {
|
|
launched = url;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('Connect GitHub without a client id shows guidance', (
|
|
tester,
|
|
) async {
|
|
await pumpMirror(tester);
|
|
await tester.tap(find.bySemanticsLabel('Connect GitHub'));
|
|
await tester.pump();
|
|
expect(spoken('Enter the OAuth App client id'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Test connection reports a reachable repo', (tester) async {
|
|
final mock = MockClient((_) async => http.Response('{}', 200));
|
|
await pumpMirror(tester, httpClient: mock);
|
|
|
|
await tester.tap(find.bySemanticsLabel('Test connection'));
|
|
await tester.pump(); // start
|
|
await tester.pump(); // resolve future + rebuild
|
|
|
|
expect(spoken('reachable'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Test connection reports an inaccessible repo', (tester) async {
|
|
final mock = MockClient((_) async => http.Response('', 404));
|
|
await pumpMirror(tester, httpClient: mock);
|
|
|
|
await tester.tap(find.bySemanticsLabel('Test connection'));
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
expect(spoken('Could not access'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('Test connection surfaces a network error', (tester) async {
|
|
final mock = MockClient((_) async => throw Exception('offline'));
|
|
await pumpMirror(tester, httpClient: mock);
|
|
|
|
await tester.tap(find.bySemanticsLabel('Test connection'));
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
expect(spoken('Error:'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('device flow failure to start shows a message', (tester) async {
|
|
final mock = MockClient((_) async => http.Response('nope', 422));
|
|
await pumpMirror(
|
|
tester,
|
|
initial: const SyncSettings(
|
|
owner: 'o',
|
|
repo: 'r',
|
|
token: '',
|
|
clientId: 'cid',
|
|
),
|
|
httpClient: mock,
|
|
);
|
|
|
|
await tester.tap(find.bySemanticsLabel('Connect GitHub'));
|
|
await tester.pump();
|
|
await tester.pump();
|
|
|
|
expect(spoken('Could not start device flow'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('device flow happy path saves the token', (tester) async {
|
|
final mock = MockClient((req) async {
|
|
if (req.url.path.contains('device/code')) {
|
|
return http.Response(
|
|
jsonEncode({
|
|
'device_code': 'dev123',
|
|
'user_code': 'WXYZ-1234',
|
|
'verification_uri': 'https://github.com/login/device',
|
|
'interval': 0,
|
|
'expires_in': 900,
|
|
}),
|
|
200,
|
|
);
|
|
}
|
|
// Token endpoint: authorize immediately.
|
|
return http.Response(jsonEncode({'access_token': 'gho_test'}), 200);
|
|
});
|
|
|
|
await pumpMirror(
|
|
tester,
|
|
initial: const SyncSettings(
|
|
owner: 'o',
|
|
repo: 'r',
|
|
token: '',
|
|
clientId: 'cid',
|
|
),
|
|
httpClient: mock,
|
|
);
|
|
|
|
await tester.tap(find.bySemanticsLabel('Connect GitHub'));
|
|
await tester.pump(); // requestDeviceCode
|
|
await tester.pump(); // dialog builds, shows the user code
|
|
expect(find.text('WXYZ-1234'), findsOneWidget);
|
|
|
|
// Let the dialog poll (interval 0) and resolve the token, then the
|
|
// post-connect sync runs against the mock (list → empty, then PUT).
|
|
await tester.pump(const Duration(milliseconds: 50));
|
|
for (var i = 0; i < 6; i++) {
|
|
await tester.pump();
|
|
}
|
|
|
|
expect(spoken('Connected and synced'), findsOneWidget);
|
|
});
|
|
}
|