mirror of
https://github.com/flutter/holobooth.git
synced 2025-08-06 14:50:05 +08:00
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:
35
.github/workflows/main.yaml
vendored
35
.github/workflows/main.yaml
vendored
@ -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
6
.vscode/tasks.json
vendored
@ -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": [
|
||||
{
|
||||
|
BIN
assets/icons/go_next_button_icon.png
Normal file
BIN
assets/icons/go_next_button_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.5 KiB |
@ -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');
|
||||
|
@ -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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
95
lib/character_selection/widgets/character_spotlight.dart
Normal file
95
lib/character_selection/widgets/character_spotlight.dart
Normal 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;
|
||||
}
|
31
lib/character_selection/widgets/next_button.dart
Normal file
31
lib/character_selection/widgets/next_button.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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),
|
||||
|
@ -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 {}
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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 |
@ -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')),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 21 KiB |
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
16
test/character_selection/widgets/next_button_test.dart
Normal file
16
test/character_selection/widgets/next_button_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
21
test/flutter_test_config.dart
Normal file
21
test/flutter_test_config.dart
Normal 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,
|
||||
);
|
||||
}
|
@ -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());
|
||||
|
Reference in New Issue
Block a user