import 'dart:convert'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:qlyra/frontend/widgets/lottie_slash_icon.dart'; import 'package:lottie/lottie.dart'; const _asset = 'assets/lottie/ic_flash_on_to_off.json'; Map _doc() => jsonDecode(File(_asset).readAsStringSync()) as Map; List> _layers(Map doc) => (doc['layers'] as List).cast>(); List> _maskFrames(Map layer) { final masks = (layer['masksProperties'] as List).cast>(); return ((masks.single['pt'] as Map)['k'] as List) .cast>(); } List> _quad(Map frame) => ((((frame['s'] as List).first as Map)['v']) as List) .map((point) => (point as List).cast()) .toList(); Widget _host(bool slashed) => MaterialApp( home: Scaffold( body: Center( child: LottieSlashIcon( asset: _asset, slashed: slashed, color: const Color(0xFFFFFFFF), ), ), ), ); void main() { group('Ассет перечёркивания', () { test('квадратный, из двух слоёв, каждый со своей маской', () { final doc = _doc(); expect(doc['w'], doc['h']); expect(doc['op'], greaterThan(0)); final layers = _layers(doc); expect(layers.length, 2); for (final layer in layers) { expect(_maskFrames(layer).length, 2, reason: 'маска должна ехать'); final group = (layer['shapes'] as List).first as Map; final paths = (group['it'] as List).cast>().where( (item) => item['ty'] == 'sh', ); expect(paths, isNotEmpty); for (final path in paths) { expect( (path['ks'] as Map)['a'], 0, reason: 'глифы статичны — двигается только маска', ); } } }); test('маски слоёв дополняют друг друга', () { final layers = _layers(_doc()); final slashed = _maskFrames(layers.first); final plain = _maskFrames(layers.last); for (var frame = 0; frame < 2; frame++) { final a = _quad(slashed[frame]); final b = _quad(plain[frame]); expect( a.take(2), b.take(2), reason: 'обе маски должны делить одну и ту же диагональ', ); expect( a.skip(2), isNot(b.skip(2)), reason: 'маски должны смотреть в разные стороны от диагонали', ); } }); test('слои двигаются одинаково', () { final layers = _layers(_doc()); expect(layers.first['ks'], layers.last['ks']); }); testWidgets('композиция читается lottie без предупреждений', ( tester, ) async { final composition = await AssetLottie(_asset).load(); expect(composition.warnings, isEmpty); expect(composition.layers.length, 2); }); }); group('LottieSlashIcon', () { testWidgets('рисует lottie в обоих состояниях', (tester) async { await tester.pumpWidget(_host(false)); await tester.pump(); expect(find.byType(Lottie), findsOneWidget); await tester.pumpWidget(_host(true)); await tester.pump(const Duration(milliseconds: 100)); expect(find.byType(Lottie), findsOneWidget); await tester.pump(const Duration(milliseconds: 400)); expect(find.byType(Lottie), findsOneWidget); }); testWidgets('перечёркивание анимируется в обе стороны', (tester) async { await tester.pumpWidget(_host(false)); final controller = tester .widget(find.byType(LottieBuilder)) .controller!; expect(controller.value, 0); await tester.pumpWidget(_host(true)); await tester.pump(const Duration(milliseconds: 100)); expect(controller.value, greaterThan(0)); expect(controller.value, lessThan(1)); await tester.pump(const Duration(milliseconds: 400)); expect(controller.value, 1); await tester.pumpWidget(_host(false)); await tester.pump(const Duration(milliseconds: 100)); expect(controller.value, lessThan(1)); await tester.pump(const Duration(milliseconds: 400)); expect(controller.value, 0); }); }); }