Release 1.0.3: refine tablet layouts, pointer selection and account sync cleanup
This commit is contained in:
@@ -117,4 +117,67 @@ void main() {
|
||||
connectivity.dispose();
|
||||
session.dispose();
|
||||
});
|
||||
for (final fails in [false, true]) {
|
||||
test('account removal awaits native cleanup (failure: $fails)', () async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
'account_migration_v1_done': true,
|
||||
'accounts_list': jsonEncode([
|
||||
const SavedAccount(
|
||||
id: accountId,
|
||||
serverUrl: 'https://server.example.com',
|
||||
username: 'alice',
|
||||
).toJson(),
|
||||
]),
|
||||
'active_account_id': accountId,
|
||||
'acct_${accountId}_ui_synced_folders': ['/Photos'],
|
||||
});
|
||||
final connectivity = ConnectivityController();
|
||||
await pumpUntil(() => connectivity.isOffline);
|
||||
final session = SessionController(connectivity);
|
||||
await pumpUntil(() => session.isLoggedIn);
|
||||
const channel = MethodChannel('dev.ayushya.noo/sync_service');
|
||||
var cleanupCalled = false;
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, (call) async {
|
||||
if (call.method == 'removeAccountData') {
|
||||
expect(call.arguments['accountId'], accountId);
|
||||
expect(session.accounts, isNotEmpty);
|
||||
cleanupCalled = true;
|
||||
if (fails) throw PlatformException(code: 'cleanup_failed');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
try {
|
||||
if (fails) {
|
||||
await expectLater(
|
||||
session.removeAccount(accountId),
|
||||
throwsA(isA<PlatformException>()),
|
||||
);
|
||||
expect(session.accounts, isNotEmpty);
|
||||
expect(
|
||||
(await SharedPreferences.getInstance()).getStringList(
|
||||
'acct_${accountId}_ui_synced_folders',
|
||||
),
|
||||
['/Photos'],
|
||||
);
|
||||
} else {
|
||||
await session.removeAccount(accountId);
|
||||
expect(session.accounts, isEmpty);
|
||||
expect(session.isLoggedIn, false);
|
||||
expect(
|
||||
(await SharedPreferences.getInstance()).containsKey(
|
||||
'acct_${accountId}_ui_synced_folders',
|
||||
),
|
||||
false,
|
||||
);
|
||||
}
|
||||
expect(cleanupCalled, true);
|
||||
} finally {
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.setMockMethodCallHandler(channel, null);
|
||||
session.dispose();
|
||||
connectivity.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:noo/models/saved_account.dart';
|
||||
|
||||
void main() {
|
||||
test('display name survives persistence without changing login identity', () {
|
||||
const account = SavedAccount(
|
||||
id: 'id',
|
||||
serverUrl: 'https://cloud.example',
|
||||
username: 'alice123',
|
||||
displayName: ' Alice Smith ',
|
||||
);
|
||||
final restored = SavedAccount.fromJson(account.toJson());
|
||||
expect(restored.label, 'Alice Smith');
|
||||
expect(restored.username, 'alice123');
|
||||
expect(restored.id, 'id');
|
||||
});
|
||||
test('legacy accounts and blank names fall back to username', () {
|
||||
final legacy = SavedAccount.fromJson({
|
||||
'id': 'id',
|
||||
'serverUrl': 'https://cloud.example',
|
||||
'username': 'alice123',
|
||||
});
|
||||
expect(legacy.label, 'alice123');
|
||||
expect(
|
||||
const SavedAccount(
|
||||
id: 'id',
|
||||
serverUrl: '',
|
||||
username: 'alice123',
|
||||
displayName: ' ',
|
||||
).label,
|
||||
'alice123',
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:noo/widgets/settings/settings_about.dart';
|
||||
import 'package:noo/widgets/noo/core/noo_search_field.dart';
|
||||
import 'package:noo/views/search_view.dart';
|
||||
import 'package:noo/widgets/shell/tablet_account_menu.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
@@ -80,9 +85,15 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
Future<void> pumpSettings(WidgetTester tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(400, 800));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
Future<void> pumpSettings(
|
||||
WidgetTester tester, {
|
||||
Widget home = const AccountView(),
|
||||
Size size = const Size(400, 800),
|
||||
}) async {
|
||||
tester.view.devicePixelRatio = 1;
|
||||
tester.view.physicalSize = size;
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MultiProvider(
|
||||
@@ -102,7 +113,7 @@ void main() {
|
||||
],
|
||||
child: MaterialApp(
|
||||
theme: AppTheme.light(AppTheme.defaultAccent, useDynamicColor: false),
|
||||
home: const AccountView(),
|
||||
home: home,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -138,6 +149,7 @@ void main() {
|
||||
'Tabs',
|
||||
'Action bar',
|
||||
'Swipe on a file',
|
||||
'About',
|
||||
];
|
||||
|
||||
testWidgets(
|
||||
@@ -195,4 +207,95 @@ void main() {
|
||||
expect(find.text('alice'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
testWidgets(
|
||||
'tablet hides phone navigation preferences and keeps viewer glass',
|
||||
(tester) async {
|
||||
await pumpSettings(tester, size: const Size(768, 1024));
|
||||
expect(find.text('Swipe on a file'), findsNothing);
|
||||
await tester.tap(find.text('Appearance'));
|
||||
await settleNav(tester);
|
||||
for (final label in [
|
||||
'Bottom bar style',
|
||||
'Menu style',
|
||||
'Avatar position',
|
||||
'Search in bottom bar',
|
||||
'Upload button',
|
||||
]) {
|
||||
expect(find.text(label), findsNothing);
|
||||
}
|
||||
expect(find.text('Frosted glass'), findsOneWidget);
|
||||
expect(find.text('Theme'), findsOneWidget);
|
||||
await tester.tap(find.text('Tabs'));
|
||||
await settleNav(tester);
|
||||
expect(find.text('Tap tab to scroll to top'), findsNothing);
|
||||
expect(find.text('Default tab'), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('About reads installed version and build', (tester) async {
|
||||
PackageInfo.setMockInitialValues(
|
||||
appName: 'Noo',
|
||||
packageName: 'dev.ayushya.noo',
|
||||
version: '2.3.4',
|
||||
buildNumber: '42',
|
||||
buildSignature: '',
|
||||
);
|
||||
await pumpSettings(
|
||||
tester,
|
||||
home: const Scaffold(body: SettingsAboutSection()),
|
||||
);
|
||||
expect(find.text('2.3.4'), findsOneWidget);
|
||||
expect(find.text('42'), findsOneWidget);
|
||||
expect(find.text('dev.ayushya.noo'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('tablet Search header stays below status bar', (tester) async {
|
||||
tester.view.padding = const FakeViewPadding(top: 48);
|
||||
addTearDown(tester.view.resetPadding);
|
||||
await pumpSettings(
|
||||
tester,
|
||||
home: const SearchView(),
|
||||
size: const Size(768, 1024),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(find.byType(NooSearchField)).dy,
|
||||
greaterThanOrEqualTo(48),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
testWidgets(
|
||||
'portrait tablet Settings selects sections inline and expands accounts',
|
||||
(tester) async {
|
||||
await pumpSettings(tester, size: const Size(768, 1024));
|
||||
await tester.tap(find.text('Security'));
|
||||
await settleNav(tester);
|
||||
expect(
|
||||
find.text("Require this device's PIN or biometric to open Noo"),
|
||||
findsOneWidget,
|
||||
);
|
||||
// One sidebar label and one toolbar title; no repeated card heading.
|
||||
expect(find.text('Security'), findsNWidgets(2));
|
||||
expect(find.byType(NooTopBarBack), findsNothing);
|
||||
final account = find.descendant(
|
||||
of: find.byType(TabletAccountMenu),
|
||||
matching: find.text('alice'),
|
||||
);
|
||||
final menuHeight = tester.getSize(find.byType(TabletAccountMenu)).height;
|
||||
await tester.tap(account);
|
||||
await settleNav(tester);
|
||||
expect(
|
||||
tester.getSize(find.byType(TabletAccountMenu)).height,
|
||||
greaterThan(menuHeight),
|
||||
);
|
||||
expect(find.text('Manage Accounts'), findsOneWidget);
|
||||
await tester.tap(find.text('Manage Accounts'));
|
||||
await settleNav(tester);
|
||||
expect(
|
||||
find.text("Require this device's PIN or biometric to open Noo"),
|
||||
findsNothing,
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:noo/services/nextcloud_service.dart';
|
||||
import 'package:noo/views/file_viewer_screen.dart';
|
||||
import 'package:noo/widgets/noo/core/noo_button.dart';
|
||||
import 'package:noo/widgets/viewer/media_action_bar.dart';
|
||||
import 'package:noo/widgets/viewer/media_pdf_preview.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../widgets/noo/noo_test_utils.dart';
|
||||
@@ -94,6 +95,20 @@ void main() {
|
||||
return service;
|
||||
}
|
||||
|
||||
testWidgets('PDF viewport remains fixed when toolbar hides', (tester) async {
|
||||
await mount(tester, const Size(390, 800), name: 'document.pdf');
|
||||
final before = tester
|
||||
.widget<MediaPdfPreview>(find.byType(MediaPdfPreview))
|
||||
.topInset;
|
||||
await tester.tapAt(const Offset(195, 350));
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
expect(
|
||||
tester.widget<MediaPdfPreview>(find.byType(MediaPdfPreview)).topInset,
|
||||
before,
|
||||
);
|
||||
expect(before, greaterThan(0));
|
||||
});
|
||||
|
||||
for (final width in [390.0, 1200.0]) {
|
||||
testWidgets('text actions use the top bar at width $width', (tester) async {
|
||||
final service = await mount(tester, Size(width, 800));
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:noo/widgets/noo/lists/noo_selection_bar.dart';
|
||||
import 'package:noo/models/selection_action.dart';
|
||||
import 'package:noo/widgets/noo/noo_layout.dart';
|
||||
import 'package:noo/widgets/noo/media/noo_grid_card.dart';
|
||||
import 'package:noo/widgets/noo/files/noo_file_table.dart';
|
||||
import 'package:noo/widgets/noo/files/noo_file_kind.dart';
|
||||
import 'noo_test_utils.dart';
|
||||
import 'package:noo/widgets/noo/media/noo_photo_group.dart';
|
||||
|
||||
void main() {
|
||||
setUpNooTests();
|
||||
testWidgets('photo columns follow the pane rather than the tablet width', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
height: 400,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
NooPhotoGrid(
|
||||
minTileWidth: 150,
|
||||
itemCount: 6,
|
||||
itemBuilder: (_, index) =>
|
||||
SizedBox(key: ValueKey('photo-$index')),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final first = tester.getRect(find.byKey(const ValueKey('photo-0')));
|
||||
final third = tester.getRect(find.byKey(const ValueKey('photo-2')));
|
||||
final fourth = tester.getRect(find.byKey(const ValueKey('photo-3')));
|
||||
expect(first.width, greaterThan(100));
|
||||
expect(third.right, lessThanOrEqualTo(first.left + 400));
|
||||
expect(fourth.top, greaterThan(first.top));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
for (final width in [320.0, 430.0, 700.0]) {
|
||||
testWidgets('selection actions fit portrait tablet pane $width', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: nooTheme(Brightness.light),
|
||||
builder: (_, child) => Material(child: child),
|
||||
home: Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: NooSelectionBar(
|
||||
count: 100,
|
||||
isDesktop: true,
|
||||
onClose: () {},
|
||||
actions: [
|
||||
for (final kind in SelectionActionKind.values)
|
||||
SelectionAction(
|
||||
kind: kind,
|
||||
icon: kind.icon,
|
||||
label: kind.label,
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.text('100 selected'), findsOneWidget);
|
||||
});
|
||||
testWidgets('file cards fit pane width $width', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: nooTheme(Brightness.light),
|
||||
builder: (_, child) => Material(child: child),
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
size: Size(768, 1024),
|
||||
textScaler: TextScaler.linear(1.4),
|
||||
),
|
||||
child: Builder(
|
||||
builder: (context) => Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: 500,
|
||||
child: GridView.builder(
|
||||
gridDelegate: NooLayout.fileGridDelegate(context),
|
||||
itemCount: 6,
|
||||
itemBuilder: (_, index) => const NooGridCard(
|
||||
name: 'Long file name.pdf',
|
||||
meta: '2 MB',
|
||||
thumbnailHeight: 118,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(
|
||||
tester.getSize(find.byType(NooGridCard).first).height,
|
||||
lessThan(200),
|
||||
);
|
||||
});
|
||||
testWidgets('file table preserves name space at $width', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: nooTheme(Brightness.light),
|
||||
builder: (_, child) => Material(child: child),
|
||||
home: Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
child: const NooFileTableRow(
|
||||
kind: NooFileKind.folder,
|
||||
name: 'Important project documents',
|
||||
col2: 'Yesterday',
|
||||
col3: 'Folder',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(
|
||||
tester.getSize(find.text('Important project documents')).width,
|
||||
greaterThan(80),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:noo/widgets/noo/noo_layout.dart';
|
||||
|
||||
void main() {
|
||||
for (final platform in [TargetPlatform.iOS, TargetPlatform.android]) {
|
||||
for (final size in [
|
||||
const Size(600, 960),
|
||||
const Size(768, 1024),
|
||||
const Size(1024, 768),
|
||||
const Size(430, 932),
|
||||
const Size(932, 430),
|
||||
const Size(599, 1024),
|
||||
]) {
|
||||
testWidgets('$platform sidebar at $size', (tester) async {
|
||||
bool? sidebar;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: ThemeData(platform: platform),
|
||||
home: MediaQuery(
|
||||
data: MediaQueryData(size: size),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
sidebar = NooLayout.isDesktop(context);
|
||||
return const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(sidebar, size.shortestSide >= 600);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:noo/widgets/noo/core/noo_pointer_selection.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('mouse reveals checkbox; clicking selects without opening item', (
|
||||
tester,
|
||||
) async {
|
||||
var opened = 0;
|
||||
var toggled = 0;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: NooPointerSelection(
|
||||
child: Scaffold(
|
||||
body: GestureDetector(
|
||||
onTap: () => opened++,
|
||||
child: NooPointerCheckbox(
|
||||
selected: false,
|
||||
onToggle: () => toggled++,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.byType(Checkbox), findsNothing);
|
||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await mouse.addPointer(location: const Offset(10, 10));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(Checkbox), findsOneWidget);
|
||||
await tester.tap(find.byType(Checkbox));
|
||||
expect(toggled, 1);
|
||||
expect(opened, 0);
|
||||
await mouse.removePointer();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(Checkbox), findsNothing);
|
||||
});
|
||||
testWidgets('nonselectable items never show checkboxes', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: NooPointerSelection(child: NooPointerCheckbox(selected: false)),
|
||||
),
|
||||
);
|
||||
final mouse = await tester.createGesture(kind: PointerDeviceKind.mouse);
|
||||
await mouse.addPointer(location: const Offset(10, 10));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(Checkbox), findsNothing);
|
||||
await mouse.removePointer();
|
||||
await tester.pumpAndSettle();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user