feat: spotlight effect on character selection and next button (#179)

* feat: footer for character selection

* chore: size

* chore: external feature for footer

* chore: draft

* chore: sizes

* chore: remove padding

* chore: simplify sizing

* chore: color

* chore: simplify

* chore: adjust

* chore: testing

* test: coverage

* chore: import

* chore: todo

* chore: refactor

* chore: image

* chore: entry point

* chore: text color

* chore: wip

* chore: feedback

* chore: feedback

* chore: dispose

* chore: wip

* chore: revert

* chore: backgroud color

* chore: remove size from component

* chore: rollback

* chore: adjust size

* chore: wip

* chore: colors

* chore: oval

* chore: colors

* chore: background

* feat: colors

* chore: layout

* chore: backgrounds

* chore: arrow

* chore: colors

* chore: blur effect

* chore: maths

* chore: size

* feat: goldens

* chore: test

* feat: tests

* chore: delete

* chore: landing

* chore: landing

* chore: import

* chore: background

* chore: refactor

* feat: next button

* chore: remove nullability from backgrounds

* Update lib/app/app.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* chore: improve background

* chore: refactor body

* chore: refactor

* chore: revert

* chore: refactor

* chore: golden adjustments

* chore: background

* chore: refactor

* chore: testing gradients

* chore: shadows

* chore: goldens shadow

* Update lib/character_selection/widgets/character_spotlight.dart

Co-authored-by: Alejandro Santiago <dev@alestiago.com>

* chore: body

* chore: goldens

* chore: disable shadow

* chore: shadow

* chore: ci

* chore: ci

* chore: flags

* chore: test

* chore: test

* chore: optimization

* chore: flutter test

* chore: enable shadows

* chore: skip goldens in CI

Co-authored-by: Alejandro Santiago <dev@alestiago.com>
This commit is contained in:
Oscar
2022-11-11 13:17:47 +01:00
committed by GitHub
parent 33011beaeb
commit 4d3d1e33ed
29 changed files with 535 additions and 279 deletions

View File

@ -8,7 +8,34 @@ concurrency:
jobs:
build:
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1
with:
flutter_version: 3.3.1
coverage_excludes: "**/*.g.dart"
runs-on: ubuntu-latest
steps:
- name: 📚 Git Checkout
uses: actions/checkout@v3
- name: 🐦 Setup Flutter
uses: subosito/flutter-action@v2
- name: 📦 Install Dependencies
run: |
flutter pub global activate very_good_cli
very_good --analytics false
very_good packages get --recursive
- name: ⚙️ Run Setup
if: "${{inputs.setup != ''}}"
run: ${{inputs.setup}}
- name: ✨ Check Formatting
run: flutter format --set-exit-if-changed lib test
- name: 🕵️ Analyze
run: flutter analyze lib test
- name: 🧪 Run Tests
run: flutter test --no-pub --coverage --test-randomize-ordering-seed random
- name: 📊 Check Code Coverage
uses: VeryGoodOpenSource/very_good_coverage@v2
with:
exclude: "**/*.g.dart"

6
.vscode/tasks.json vendored
View File

@ -33,6 +33,12 @@
"command": "cd ${input:dartFolder} && dart test --coverage=coverage --platform='chrome,vm' && dart pub global run coverage:format_coverage --lcov --in=coverage --out=coverage/lcov.info --packages=.dart_tool/package_config.json --report-on='lib' && genhtml ./coverage/lcov.info -o coverage && open ./coverage/index.html",
"problemMatcher": []
},
{
"label": "flutter: Regenerate goldens",
"type": "shell",
"command": "very_good test --tags golden --update-goldens",
"problemMatcher": []
},
],
"inputs": [
{

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@ -31,10 +31,6 @@ class $AssetsBackgroundsGen {
AssetGenImage get blueCircle =>
const AssetGenImage('assets/backgrounds/blue_circle.png');
/// File path: assets/backgrounds/character_selection_background.png
AssetGenImage get characterSelectionBackground => const AssetGenImage(
'assets/backgrounds/character_selection_background.png');
/// File path: assets/backgrounds/landing_background.png
AssetGenImage get landingBackground =>
const AssetGenImage('assets/backgrounds/landing_background.png');
@ -78,6 +74,10 @@ class $AssetsIconsGen {
AssetGenImage get flutterIcon =>
const AssetGenImage('assets/icons/flutter_icon.png');
/// File path: assets/icons/go_next_button_icon.png
AssetGenImage get goNextButtonIcon =>
const AssetGenImage('assets/icons/go_next_button_icon.png');
/// File path: assets/icons/retake_button_icon.png
AssetGenImage get retakeButtonIcon =>
const AssetGenImage('assets/icons/retake_button_icon.png');

View File

@ -24,10 +24,16 @@ class CharacterSelectionView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const AppPageView(
background: CharacterSelectionBackground(),
body: CharacterSelectionBody(),
footer: SimplifiedFooter(),
return AppPageView(
background: const CharacterSelectionBackground(),
body: const CharacterSelectionBody(),
footer: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: const [
NextButton(),
SimplifiedFooter(),
],
),
);
}
}

View File

@ -1,3 +1,4 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
@ -6,17 +7,49 @@ class CharacterSelectionBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
return const DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
PhotoboothColors.purple,
PhotoboothColors.blue,
return CustomPaint(
painter: Gradients(
gradient: UnmodifiableListView(
[
const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
PhotoboothColors.purple,
PhotoboothColors.blue,
],
),
LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.5),
Colors.black.withOpacity(0),
],
)
],
),
),
);
}
}
@visibleForTesting
class Gradients extends CustomPainter {
Gradients({required this.gradient});
final UnmodifiableListView<Gradient> gradient;
@override
void paint(Canvas canvas, Size size) {
final rect = Offset.zero & size;
final paint = Paint();
for (final gradient in gradient) {
paint.shader = gradient.createShader(rect);
canvas.drawRect(rect, paint);
}
}
@override
bool shouldRepaint(covariant Gradients oldDelegate) => false;
}

View File

@ -1,51 +1,55 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import 'package:io_photobooth/character_selection/widgets/character_selection_header.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
class CharacterSelectionBody extends StatelessWidget {
const CharacterSelectionBody({super.key});
// Minimum height calculated to avoid overlap in the stack
static const _minBodyHeight = 600.0;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = context.l10n;
final size = MediaQuery.of(context).size;
double bodyHeight;
double viewportFraction;
if (size.width <= PhotoboothBreakpoints.small) {
bodyHeight = size.height * 0.6;
viewportFraction = 0.55;
} else if (size.width <= PhotoboothBreakpoints.medium) {
bodyHeight = size.height * 0.6;
viewportFraction = 0.35;
} else {
bodyHeight = size.height * 0.75;
viewportFraction = 0.2;
}
bodyHeight = math.max(_minBodyHeight, bodyHeight);
return Column(
children: [
const SizedBox(height: 104),
SelectableText(
l10n.chooseYourCharacterTitleText,
style: theme.textTheme.headline1,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
SelectableText(
l10n.youCanChangeThemLaterSubheading,
style: theme.textTheme.headline3,
textAlign: TextAlign.center,
),
const SizedBox(height: 53),
LayoutBuilder(
builder: (_, constraints) {
if (constraints.maxWidth <= PhotoboothBreakpoints.small) {
return const CharacterSelector.small();
} else if (constraints.maxWidth <= PhotoboothBreakpoints.medium) {
return const CharacterSelector.medium();
} else if (constraints.maxWidth <= PhotoboothBreakpoints.large) {
return const CharacterSelector.large();
} else {
return const CharacterSelector.xLarge();
}
},
),
const SizedBox(height: 42),
FloatingActionButton(
onPressed: () {
Navigator.of(context).push(PhotoBoothPage.route());
},
child: const Icon(Icons.arrow_right),
SizedBox(
height: bodyHeight,
child: Stack(
children: [
const Align(
alignment: Alignment.topCenter,
child: CharacterSelectionHeader(),
),
Align(
alignment: Alignment.topCenter,
child: CharacterSpotlight(
bodyHeight: bodyHeight,
viewPortFraction: viewportFraction,
),
),
Align(
alignment: Alignment.bottomCenter,
child: CharacterSelector(viewportFraction: viewportFraction),
),
],
),
),
],
);

View File

@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
class CharacterSelectionHeader extends StatelessWidget {
const CharacterSelectionHeader({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final l10n = context.l10n;
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 24),
SelectableText(
l10n.chooseYourCharacterTitleText,
style: theme.textTheme.headline1
?.copyWith(color: PhotoboothColors.white),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
SelectableText(
l10n.youCanChangeThemLaterSubheading,
style: theme.textTheme.headline3
?.copyWith(color: PhotoboothColors.white),
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@ -2,31 +2,7 @@ import 'package:flutter/material.dart';
import 'package:io_photobooth/assets/assets.dart';
class CharacterSelector extends StatefulWidget {
@visibleForTesting
const CharacterSelector({super.key, required this.viewportFraction});
const CharacterSelector.small({Key? key})
: this(
viewportFraction: 0.55,
key: key,
);
const CharacterSelector.medium({Key? key})
: this(
viewportFraction: 0.3,
key: key,
);
const CharacterSelector.large({Key? key})
: this(
viewportFraction: 0.2,
key: key,
);
const CharacterSelector.xLarge({Key? key})
: this(
viewportFraction: 0.2,
key: key,
);
final double viewportFraction;
@ -88,25 +64,22 @@ class CharacterSelectorState extends State<CharacterSelector> {
@override
Widget build(BuildContext context) {
return SizedBox(
height: 600,
child: PageView.builder(
itemCount: 2,
controller: pageController,
onPageChanged: (value) {
setState(() {
_activePage = value;
});
},
itemBuilder: (context, index) {
final isActive = index == _activePage;
return InkWell(
onTap: () => _onTapCharacter(index),
key: characterKeys[index],
child: _Character(isActive: isActive, image: _characters[index]),
);
},
),
return PageView.builder(
itemCount: 2,
controller: pageController,
onPageChanged: (value) {
setState(() {
_activePage = value;
});
},
itemBuilder: (context, index) {
final isActive = index == _activePage;
return InkWell(
onTap: () => _onTapCharacter(index),
key: characterKeys[index],
child: _Character(isActive: isActive, image: _characters[index]),
);
},
);
}
}
@ -123,9 +96,12 @@ class _Character extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: isActive ? 1 : 0.70,
scale: isActive ? 1 : 0.85,
duration: const Duration(milliseconds: 300),
child: image,
child: Container(
alignment: Alignment.bottomCenter,
child: image,
),
);
}
}

View File

@ -0,0 +1,95 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
class CharacterSpotlight extends StatelessWidget {
const CharacterSpotlight({
super.key,
required this.bodyHeight,
required this.viewPortFraction,
});
final double bodyHeight;
final double viewPortFraction;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final size = Size(constraints.maxHeight, constraints.maxWidth);
return SizedBox(
width: constraints.maxWidth * viewPortFraction * 1.4,
height: bodyHeight,
child: CustomPaint(
size: size,
painter: SpotlightBeam(),
child: CustomPaint(
size: size,
painter: SpotlightShadow(),
),
),
);
},
);
}
}
@visibleForTesting
class SpotlightBeam extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final pathLightBody = Path()
..moveTo(size.width / 3, 0)
..lineTo(0, size.height)
..lineTo(size.width, size.height)
..lineTo((size.width / 3) * 2, 0)
..close();
final paintLightBody = Paint()
..blendMode = BlendMode.overlay
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
PhotoboothColors.white.withOpacity(0.6),
PhotoboothColors.white.withOpacity(0),
],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
canvas.drawPath(pathLightBody, paintLightBody);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}
@visibleForTesting
class SpotlightShadow extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height);
canvas.save();
// Rotation of the canvas to paint the circle and we rotate to get
// the oval effect.
final matrix = Matrix4.identity()
..translate(center.dx, center.dy)
..rotateX(pi * 0.43);
canvas.transform(matrix.storage);
final paint = Paint()
..shader = RadialGradient(
colors: [
const Color(0XFFF4E4E4).withOpacity(0.5),
const Color(0XFFF4E4E4).withOpacity(0)
],
).createShader(
Rect.fromCircle(center: Offset.zero, radius: size.width / 2),
);
canvas
..drawCircle(Offset.zero, size.width / 2, paint)
..restore();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import 'package:io_photobooth/assets/assets.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
class NextButton extends StatelessWidget {
const NextButton({super.key});
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Semantics(
focusable: true,
button: true,
label: l10n.stickersNextButtonLabelText,
child: Material(
clipBehavior: Clip.hardEdge,
shape: const CircleBorder(),
color: PhotoboothColors.transparent,
child: InkWell(
key: const Key('stickersPage_next_inkWell'),
onTap: () {
Navigator.of(context).push(PhotoBoothPage.route());
},
child: Assets.icons.goNextButtonIcon.image(height: 100),
),
),
);
}
}

View File

@ -1,3 +1,5 @@
export 'character_selection_background.dart';
export 'character_selection_body.dart';
export 'character_selector.dart';
export 'character_spotlight.dart';
export 'next_button.dart';

View File

@ -8,7 +8,7 @@ void main() {
group('AppPageView', () {
const footerKey = Key('footer');
const bodyKey = Key('body');
const backgroundKey = Key('background');
const backgroundKey = Key('backgroundKey');
const firstOverlayKey = Key('firstOverlay');
const secondOverlayKey = Key('secondOverlayKey');
@ -70,7 +70,6 @@ void main() {
height: 200,
key: bodyKey,
),
background: Container(key: backgroundKey),
overlays: [
Container(key: firstOverlayKey),
Container(key: secondOverlayKey),

View File

@ -8,7 +8,7 @@ import 'package:mocktail/mocktail.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
import 'package:photos_repository/photos_repository.dart';
import 'helpers/helpers.dart';
import '../helpers/helpers.dart';
class _MockAuthenticationRepository extends Mock
implements AuthenticationRepository {}

View File

@ -1,9 +1,47 @@
import 'dart:io';
import 'package:alchemist/alchemist.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/l10n/l10n.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
// TODO(oscar): golden test having issues with gradients in CI
class _SubjectBuilder extends StatelessWidget {
const _SubjectBuilder({
required this.subject,
required this.size,
});
final CharacterSelectionView subject;
final Size size;
@override
Widget build(BuildContext context) {
return MediaQuery.fromWindow(
child: Directionality(
textDirection: TextDirection.ltr,
child: Localizations(
locale: Locale('en'),
delegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
child: Theme(
data: PhotoboothTheme.standard,
child: SizedBox.fromSize(
size: size,
child: subject,
),
),
),
),
);
}
}
void main() {
group('CharacterSelectionPage', () {
@ -28,6 +66,88 @@ void main() {
});
});
});
group('CharacterSelectionView', () {
testWidgets(
'renders with shadows',
(WidgetTester tester) async {
debugDisableShadows = false;
await tester.pumpSubjectView(CharacterSelectionView());
expect(find.byType(CharacterSpotlight), findsOneWidget);
debugDisableShadows = true;
},
);
testWidgets(
'renders without shadows',
(WidgetTester tester) async {
await tester.pumpSubjectView(CharacterSelectionView());
expect(find.byType(CharacterSpotlight), findsOneWidget);
},
);
goldenTest(
'character_selection_view_small',
fileName: 'character_selection_view_small',
pumpBeforeTest: precacheImages,
skip: Platform.environment.containsKey('GITHUB_ACTIONS'),
builder: () {
return GoldenTestScenario(
name: 'character_selection_view_small',
child: _SubjectBuilder(
subject: CharacterSelectionView(),
size: Size(PhotoboothBreakpoints.small, 850),
),
);
},
);
goldenTest(
'character_selection_view_medium',
fileName: 'character_selection_view_medium',
pumpBeforeTest: precacheImages,
skip: Platform.environment.containsKey('GITHUB_ACTIONS'),
builder: () {
return GoldenTestScenario(
name: 'character_selection_view_medium',
child: _SubjectBuilder(
subject: CharacterSelectionView(),
size: Size(PhotoboothBreakpoints.medium, 1000),
),
);
},
);
goldenTest(
'character_selection_view_large',
fileName: 'character_selection_view_large',
pumpBeforeTest: precacheImages,
skip: Platform.environment.containsKey('GITHUB_ACTIONS'),
builder: () {
return GoldenTestScenario(
name: 'character_selection_view_large',
child: _SubjectBuilder(
subject: CharacterSelectionView(),
size: Size(PhotoboothBreakpoints.large, 1200),
),
);
},
);
goldenTest(
'character_selection_view_xlarge',
fileName: 'character_selection_view_xlarge',
pumpBeforeTest: precacheImages,
skip: Platform.environment.containsKey('GITHUB_ACTIONS'),
builder: () {
return GoldenTestScenario(
name: 'character_selection_view_xlarge',
child: _SubjectBuilder(
subject: CharacterSelectionView(),
size: Size(PhotoboothBreakpoints.large + 300, 1500),
),
);
},
);
});
}
extension on WidgetTester {
@ -54,3 +174,28 @@ extension on WidgetTester {
),
);
}
extension on WidgetTester {
Future<void> pumpSubjectView(
CharacterSelectionView subject,
) =>
pumpWidget(
MediaQuery.fromWindow(
child: Directionality(
textDirection: TextDirection.ltr,
child: Localizations(
locale: Locale('en'),
delegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
child: Theme(
data: PhotoboothTheme.standard,
child: Scaffold(body: subject),
),
),
),
),
);
}

View File

@ -1,38 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import '../../helpers/helpers.dart';
void main() {
group('CharacterSelectionView', () {
test('can be instantiaded', () {
expect(CharacterSelectionView(), isA<CharacterSelectionView>());
});
group('renders', () {
testWidgets('successfully', (tester) async {
final subject = CharacterSelectionView();
await tester.pumpSubject(subject);
expect(find.byWidget(subject), findsOneWidget);
});
testWidgets('a CharacterSelectionBackground', (tester) async {
await tester.pumpSubject(const CharacterSelectionView());
expect(find.byType(CharacterSelectionBackground), findsOneWidget);
});
testWidgets('a CharacterSelectionBody', (tester) async {
await tester.pumpSubject(const CharacterSelectionView());
expect(find.byType(CharacterSelectionBody), findsOneWidget);
});
});
});
}
extension on WidgetTester {
Future<void> pumpSubject(
CharacterSelectionView subject,
) =>
pumpApp(Scaffold(body: subject));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View File

@ -1,40 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import '../../../helpers/helpers.dart';
void main() {
group('CharacterSelectionBackground', () {
test('can be instantiated', () {
expect(
CharacterSelectionBackground(),
isA<CharacterSelectionBackground>(),
);
});
group('renders', () {
testWidgets('successfully', (tester) async {
final subject = CharacterSelectionBackground();
await tester.pumpWidget(subject);
expect(find.byWidget(subject), findsOneWidget);
});
});
group('goldens', () {
String goldenPath(String name) => 'goldens/$name.png';
testWidgets(
'paints background',
tags: TestTag.golden,
(tester) async {
final subject = CharacterSelectionBackground();
await tester.pumpWidget(subject);
await expectLater(
find.byWidget(subject),
matchesGoldenFile(goldenPath('character_selection_background')),
);
},
);
});
});
}

View File

@ -0,0 +1,13 @@
import 'dart:collection';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
void main() {
group('Gradients', () {
test('verifies should not repaint', () async {
final painter = Gradients(gradient: UnmodifiableListView(List.empty()));
expect(painter.shouldRepaint(painter), false);
});
});
}

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
import '../../helpers/helpers.dart';
@ -13,7 +12,6 @@ void main() {
});
group('renders', () {
// TODO(oscar): add GoldenTest once assets are finalized
testWidgets('for PhotoboothBreakpoints.small', (tester) async {
tester.setDisplaySize(const Size(PhotoboothBreakpoints.small, 800));
final subject = CharacterSelectionBody();
@ -31,7 +29,7 @@ void main() {
await tester.pumpSubject(subject);
expect(find.byWidget(subject), findsOneWidget);
final finder = find.byWidgetPredicate(
(w) => w is CharacterSelector && w.viewportFraction == 0.3,
(w) => w is CharacterSelector && w.viewportFraction == 0.35,
);
expect(finder, findsOneWidget);
});
@ -73,28 +71,6 @@ void main() {
await tester.pumpSubject(CharacterSelectionBody());
expect(find.byType(CharacterSelector), findsOneWidget);
});
testWidgets('a FloatingActionButon', (tester) async {
await tester.pumpSubject(CharacterSelectionBody());
final finder = find.byType(FloatingActionButton);
await tester.ensureVisible(finder);
expect(finder, findsOneWidget);
});
});
group('navigates', () {
testWidgets(
'to PhotoBoothPage '
'when FloatingActionButton is pressed',
(tester) async {
await tester.pumpSubject(CharacterSelectionBody());
final finder = find.byType(FloatingActionButton);
await tester.ensureVisible(finder);
await tester.tap(finder);
await tester.pumpAndSettle();
expect(find.byType(PhotoBoothPage), findsOneWidget);
},
);
});
});
}

View File

@ -8,7 +8,7 @@ void main() {
group('CharacterSelector', () {
group('renders characters', () {
testWidgets('successfully for Breakpoint.small', (tester) async {
await tester.pumpSubject(CharacterSelector.small());
await tester.pumpSubject(CharacterSelector(viewportFraction: 0.55));
expect(find.byType(CharacterSelector), findsOneWidget);
for (final characterKey in CharacterSelectorState.characterKeys) {
expect(find.byKey(characterKey), findsOneWidget);
@ -16,7 +16,7 @@ void main() {
});
testWidgets('successfully for Breakpoint.medium', (tester) async {
await tester.pumpSubject(CharacterSelector.medium());
await tester.pumpSubject(CharacterSelector(viewportFraction: 0.3));
expect(find.byType(CharacterSelector), findsOneWidget);
for (final characterKey in CharacterSelectorState.characterKeys) {
expect(find.byKey(characterKey), findsOneWidget);
@ -24,15 +24,7 @@ void main() {
});
testWidgets('successfully for Breakpoint.large', (tester) async {
await tester.pumpSubject(CharacterSelector.large());
expect(find.byType(CharacterSelector), findsOneWidget);
for (final characterKey in CharacterSelectorState.characterKeys) {
expect(find.byKey(characterKey), findsOneWidget);
}
});
testWidgets('successfully for Breakpoint.xLarge', (tester) async {
await tester.pumpSubject(CharacterSelector.xLarge());
await tester.pumpSubject(CharacterSelector(viewportFraction: 0.2));
expect(find.byType(CharacterSelector), findsOneWidget);
for (final characterKey in CharacterSelectorState.characterKeys) {
expect(find.byKey(characterKey), findsOneWidget);
@ -43,7 +35,7 @@ void main() {
testWidgets(
'navigates to sparky on tap',
(WidgetTester tester) async {
await tester.pumpSubject(CharacterSelector.xLarge());
await tester.pumpSubject(CharacterSelector(viewportFraction: 0.2));
await tester.tap(find.byKey(CharacterSelectorState.sparkyKey));
await tester.pumpAndSettle();
final state = tester

View File

@ -0,0 +1,17 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
void main() {
group('SpotlightBeam', () {
test('verifies should not repaint', () async {
final painter = SpotlightBeam();
expect(painter.shouldRepaint(painter), false);
});
});
group('SpotlightShadow', () {
test('verifies should not repaint', () async {
final painter = SpotlightShadow();
expect(painter.shouldRepaint(painter), false);
});
});
}

View File

@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/photo_booth/photo_booth.dart';
import '../../helpers/helpers.dart';
void main() {
group('NextButton', () {
testWidgets('navigates to PhotoBoothPage on click', (tester) async {
await tester.pumpApp(NextButton());
await tester.tap(find.byType(NextButton));
await tester.pumpAndSettle();
expect(find.byType(PhotoBoothPage), findsOneWidget);
});
});
}

View File

@ -0,0 +1,21 @@
import 'dart:async';
import 'dart:io';
import 'package:alchemist/alchemist.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
Future<void> testExecutable(FutureOr<void> Function() testMain) async {
final enablePlatformTests =
!Platform.environment.containsKey('GITHUB_ACTIONS');
return AlchemistConfig.runWithConfig(
config: AlchemistConfig(
theme: PhotoboothTheme.standard,
platformGoldensConfig:
AlchemistConfig.current().platformGoldensConfig.copyWith(
enabled: enablePlatformTests,
renderShadows: enablePlatformTests,
),
),
run: testMain,
);
}

View File

@ -1,75 +1,16 @@
// ignore_for_file: prefer_const_constructors
import 'dart:async';
import 'package:camera_platform_interface/camera_platform_interface.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:io_photobooth/character_selection/character_selection.dart';
import 'package:io_photobooth/footer/footer.dart';
import 'package:io_photobooth/landing/landing.dart';
import 'package:mocktail/mocktail.dart';
import 'package:photobooth_ui/photobooth_ui.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import '../../helpers/helpers.dart';
class _MockCameraPlatform extends Mock
with MockPlatformInterfaceMixin
implements CameraPlatform {}
class _MockCameraDescription extends Mock implements CameraDescription {}
class _MockXFile extends Mock implements XFile {}
void main() {
void setUpPhotoboothPage() {
const cameraId = 1;
final xfile = _MockXFile();
when(() => xfile.path).thenReturn('');
final cameraPlatform = _MockCameraPlatform();
CameraPlatform.instance = cameraPlatform;
final event = CameraInitializedEvent(
cameraId,
1,
1,
ExposureMode.auto,
true,
FocusMode.auto,
true,
);
final cameraDescription = _MockCameraDescription();
when(cameraPlatform.availableCameras)
.thenAnswer((_) async => [cameraDescription]);
when(
() => cameraPlatform.createCamera(
cameraDescription,
ResolutionPreset.max,
),
).thenAnswer((_) async => 1);
when(() => cameraPlatform.initializeCamera(cameraId))
.thenAnswer((_) async => <void>{});
when(() => cameraPlatform.onCameraInitialized(cameraId)).thenAnswer(
(_) => Stream.value(event),
);
when(() => CameraPlatform.instance.onDeviceOrientationChanged())
.thenAnswer((_) => Stream.empty());
when(() => cameraPlatform.takePicture(any()))
.thenAnswer((_) async => xfile);
when(() => cameraPlatform.buildPreview(cameraId)).thenReturn(SizedBox());
when(() => cameraPlatform.pausePreview(cameraId))
.thenAnswer((_) => Future.value());
when(() => cameraPlatform.dispose(any())).thenAnswer((_) async => <void>{});
}
setUp(setUpPhotoboothPage);
tearDown(() {
CameraPlatform.instance = _MockCameraPlatform();
});
group('LandingPage', () {
testWidgets('renders landing view', (tester) async {
await tester.pumpApp(const LandingPage());