Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions lib/widgets/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,15 @@ class ChooseAccountPage extends StatelessWidget {
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
for (final (:accountId, :account) in globalStore.accountEntries)
_buildAccountItem(context,
accountId: accountId,
title: Text(account.realmUrl.toString()),
subtitle: Text(account.email)),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Flexible(child: SingleChildScrollView(
child: Column(mainAxisSize: MainAxisSize.min, children: [
for (final (:accountId, :account) in globalStore.accountEntries)
_buildAccountItem(context,
accountId: accountId,
title: Text(account.realmUrl.toString()),
subtitle: Text(account.email)),
]))),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => Navigator.push(context,
Expand Down
7 changes: 7 additions & 0 deletions test/flutter_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import 'package:checks/checks.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

extension RectChecks on Subject<Rect> {
Subject<double> get top => has((d) => d.top, 'top');
Subject<double> get bottom => has((d) => d.bottom, 'bottom');

// TODO others
}

extension AnimationChecks<T> on Subject<Animation<T>> {
Subject<AnimationStatus> get status => has((d) => d.status, 'status');
Subject<T> get value => has((d) => d.value, 'value');
Expand Down
110 changes: 109 additions & 1 deletion test/widgets/app_test.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import 'package:checks/checks.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/zulip_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zulip/model/database.dart';
import 'package:zulip/widgets/app.dart';
import 'package:zulip/widgets/inbox.dart';
import 'package:zulip/widgets/page.dart';
import 'package:zulip/widgets/store.dart';

import '../example_data.dart' as eg;
import '../flutter_checks.dart';
import '../model/binding.dart';
import '../test_navigation.dart';
import 'page_checks.dart';
Expand Down Expand Up @@ -51,4 +55,108 @@ void main() {
]);
});
});

group('ChooseAccountPage', () {
Future<void> setupChooseAccountPage(WidgetTester tester, {
required List<Account> accounts,
}) async {
addTearDown(testBinding.reset);

for (final account in accounts) {
await testBinding.globalStore
.insertAccount(account.toCompanion(false));
}

await tester.pumpWidget(
const MaterialApp(
localizationsDelegates: ZulipLocalizations.localizationsDelegates,
supportedLocales: ZulipLocalizations.supportedLocales,
home: GlobalStoreWidget(
child: ChooseAccountPage())));

// global store gets loaded
await tester.pumpAndSettle();
}

List<Account> generateAccounts(int count) {
return List.generate(count, (i) => eg.account(
id: i,
user: eg.user(fullName: 'User $i', email: 'user$i@example'),
apiKey: 'user${i}apikey',
));
}

Finder findAccount(Account account) => find.text(account.email).hitTestable();

Finder findButton<T extends ButtonStyleButton>({required String withText}) {
return find
.descendant(of: find.bySubtype<T>(), matching: find.text(withText))
.hitTestable();
}

void checkAccountShown(Account account, {required bool expected}) {
check(findAccount(account).evaluate()).length.equals(expected ? 1 : 0);
}

void checkButtonShown<T extends ButtonStyleButton>({
required String withText,
required bool expected,
}) {
check(findButton<T>(withText: withText).evaluate())
.length.equals(expected ? 1 : 0);
}

testWidgets('accounts list is scrollable when more than a screenful', (tester) async {
final accounts = generateAccounts(15);
await setupChooseAccountPage(tester, accounts: accounts);

// Accounts list is more than a screenful
// * First account is shown
// * Last account is out of view
checkAccountShown(accounts.first, expected: true);
checkAccountShown(accounts.last, expected: false);

// Button to add an account is visible
// and not moved offscreen by the long list of accounts
checkButtonShown(withText: 'Add an account', expected: true);

// Accounts list is scrollable to the bottom
await tester.scrollUntilVisible(findAccount(accounts.last), 50);
checkAccountShown(accounts.last, expected: true);
});

testWidgets('with just one account, the layout is centered', (tester) async {
final account = eg.selfAccount;
await setupChooseAccountPage(tester, accounts: [account]);

const buttonText = 'Add an account';
checkAccountShown(account, expected: true);
checkButtonShown(withText: buttonText, expected: true);

final screenHeight =
(tester.view.physicalSize / tester.view.devicePixelRatio).height;

check(tester.getRect(findAccount(account)))
..top.isGreaterThan(1 / 3 * screenHeight)
..bottom.isLessThan(2 / 3 * screenHeight);

check(tester.getRect(findButton(withText: buttonText)))
..top.isGreaterThan(1 / 3 * screenHeight)
..bottom.isLessThan(2 / 3 * screenHeight);
});

testWidgets('with no accounts, the Add an Account button is centered', (tester) async {
await setupChooseAccountPage(tester, accounts: []);

const buttonText = 'Add an account';
checkButtonShown(withText: buttonText, expected: true);

final screenHeight =
(tester.view.physicalSize / tester.view.devicePixelRatio).height;

check(tester.getRect(findButton(withText: buttonText)))
..top.isGreaterThan(1 / 3 * screenHeight)
..bottom.isLessThan(2 / 3 * screenHeight);
});
});
}