| // Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| import 'package:analysis_server/src/computer/computer_color.dart'; |
| import 'package:analyzer/dart/analysis/results.dart'; |
| import 'package:analyzer/diagnostic/diagnostic.dart'; |
| import 'package:collection/collection.dart'; |
| import 'package:test/test.dart'; |
| import 'package:test_reflective_loader/test_reflective_loader.dart'; |
| |
| import '../../abstract_context.dart'; |
| |
| void main() { |
| defineReflectiveSuite(() { |
| defineReflectiveTests(ColorComputerTest); |
| }); |
| } |
| |
| @reflectiveTest |
| class ColorComputerTest extends AbstractContextTest { |
| /// A map of Dart source code that represents different types/formats |
| /// that are valid in const contexts. |
| /// |
| /// Values are the color that should be discovered (in 0xAARRGGBB format). |
| /// |
| /// Color values may not match the actual Flutter framework but are |
| /// values that are more identifyable for ease of testing. They are |
| /// defined in: |
| /// - test/mock_packages/flutter/lib/src/material/colors.dart. |
| /// - test/mock_packages/flutter/lib/src/cupertino/colors.dart. |
| /// |
| /// These values will be iterated in tests and inserted into various |
| /// code snippets for testing. |
| static const colorCodesConst = { |
| // dart:ui Colors |
| 'Colors.white': 0xFFFFFFFF, |
| 'Color(0xFF0000FF)': 0xFF0000FF, |
| 'Color.fromARGB(255, 0, 0, 255)': 0xFF0000FF, |
| 'Color.fromRGBO(0, 0, 255, 1)': 0xFF0000FF, |
| // Flutter Painting |
| 'ColorSwatch(0xFF89ABCD, {})': 0xFF89ABCD, |
| // Flutter Material |
| 'Colors.red': 0xFFFF0000, |
| 'Colors.redAccent': 0xFFFFAA00, |
| 'MaterialAccentColor(0xFF89ABCD, {})': 0xFF89ABCD, |
| // Flutter Cupertino |
| 'CupertinoColors.black': 0xFF000000, |
| 'CupertinoColors.systemBlue': 0xFF0000FF, |
| 'CupertinoColors.activeBlue': 0xFF0000FF, |
| }; |
| |
| /// A map of Dart source code that represents different types/formats |
| /// that are not valid in const contexts. |
| /// |
| /// Values are the color that should be discovered (in 0xAARRGGBB format). |
| static const colorCodesNonConst = { |
| // Flutter Material |
| 'Colors.red.shade100': 0x10FF0000, |
| 'Colors.red[100]': 0x10FF0000, |
| // Flutter Cupertino |
| 'CupertinoColors.systemBlue.color': 0xFF0000FF, |
| 'CupertinoColors.systemBlue.darkColor': 0xFF000099, |
| 'CupertinoColors.activeBlue.color': 0xFF0000FF, |
| 'CupertinoColors.activeBlue.darkColor': 0xFF000099, |
| 'CupertinoColors.activeBlue.highContrastColor': 0xFF000066, |
| 'CupertinoColors.activeBlue.darkHighContrastColor': 0xFF000033, |
| 'CupertinoColors.activeBlue.elevatedColor': 0xFF0000FF, |
| 'CupertinoColors.activeBlue.darkElevatedColor': 0xFF000099, |
| }; |
| |
| /// A map of Dart source code that creates multiple nested color references. |
| /// |
| /// The key is the source code, and the value is a map of the expressions and |
| /// colors that should be produced (where the null key represents the |
| /// entire expression). |
| static const colorCodesNested = { |
| // TODO(dantup): Remove this "const" when we can evaluate constructors |
| // in non-const contexts. |
| 'const CupertinoDynamicColor.withBrightness(color: CupertinoColors.white, darkColor: CupertinoColors.black)': |
| { |
| null: 0xFFFFFFFF, |
| 'CupertinoColors.white': 0xFFFFFFFF, |
| 'CupertinoColors.black': 0xFF000000, |
| }, |
| }; |
| |
| late String testPath; |
| late String otherPath; |
| |
| late ColorComputer computer; |
| |
| /// Tests that all of the known color codes replaced into [code] produce the |
| /// expected nested color values. |
| /// |
| /// If [onlyConst] is `true`, only the test values that are const will be |
| /// tested. |
| Future<void> checkAllColors(String code, {bool onlyConst = false}) async { |
| // Combine the flat and nested colours into the same format. |
| final allColorCodes = <String, Map<String?, int>>{ |
| ...colorCodesConst.map((key, value) => MapEntry(key, {key: value})), |
| if (!onlyConst) |
| ...colorCodesNonConst.map((key, value) => MapEntry(key, {key: value})), |
| ...colorCodesNested, |
| }; |
| |
| for (final entry in allColorCodes.entries) { |
| final colorDartCode = entry.key; |
| final expectedColorValues = entry.value.map( |
| // A null key means we should expect the full code. |
| (key, value) => MapEntry(key ?? colorDartCode, value), |
| ); |
| |
| await expectColors( |
| code.replaceAll('[[COLOR]]', colorDartCode), |
| expectedColorValues, |
| ); |
| } |
| } |
| |
| /// Checks that all of [expectedColorValues] are produced for [dartCode]. |
| Future<void> expectColors( |
| String dartCode, |
| Map<String, int> expectedColorValues, { |
| String? otherCode, |
| }) async { |
| dartCode = _withCommonImports(dartCode); |
| otherCode = otherCode != null ? _withCommonImports(otherCode) : null; |
| |
| newFile(testPath, content: dartCode); |
| if (otherCode != null) { |
| newFile(otherPath, content: otherCode); |
| final otherResult = |
| await session.getResolvedUnit(otherPath) as ResolvedUnitResult; |
| expectNoErrors(otherResult); |
| } |
| final result = |
| await session.getResolvedUnit(testPath) as ResolvedUnitResult; |
| expectNoErrors(result); |
| |
| computer = ColorComputer(result); |
| final colors = computer.compute(); |
| |
| expect( |
| colors, |
| hasLength(expectedColorValues.length), |
| reason: '${expectedColorValues.length} colors should be detected in:\n' |
| '$dartCode', |
| ); |
| |
| expectedColorValues.entries.forEachIndexed((i, expectedColor) { |
| final color = colors[i]; |
| final expectedColorCode = expectedColor.key; |
| final expectedColorValue = expectedColor.value; |
| final expectedAlpha = (0xff000000 & expectedColorValue) >> 24; |
| final expectedRed = (0x00ff0000 & expectedColorValue) >> 16; |
| final expectedGreen = (0x0000ff00 & expectedColorValue) >> 8; |
| final expectedBlue = (0x000000ff & expectedColorValue) >> 0; |
| |
| final regionText = |
| dartCode.substring(color.offset, color.offset + color.length); |
| expect( |
| regionText, |
| equals(expectedColorCode), |
| reason: 'Color $i expected $expectedColorCode but was $regionText', |
| ); |
| |
| void expectComponent(int actual, int expected, String name) => expect( |
| actual, |
| expected, |
| reason: '$name value for $expectedColorCode is not correct', |
| ); |
| |
| expectComponent(color.color.alpha, expectedAlpha, 'Alpha'); |
| expectComponent(color.color.red, expectedRed, 'Red'); |
| expectComponent(color.color.green, expectedGreen, 'Green'); |
| expectComponent(color.color.blue, expectedBlue, 'Blue'); |
| }); |
| } |
| |
| void expectNoErrors(ResolvedUnitResult result) { |
| // If the test code has errors, generate a suitable failure to help debug. |
| final errors = result.errors |
| .where((error) => error.severity == Severity.error) |
| .toList(); |
| if (errors.isNotEmpty) { |
| throw 'Code has errors: $errors\n\n${result.content}'; |
| } |
| } |
| |
| @override |
| void setUp() { |
| super.setUp(); |
| writeTestPackageConfig(flutter: true); |
| testPath = convertPath('/home/test/lib/test.dart'); |
| otherPath = convertPath('/home/test/lib/other_file.dart'); |
| } |
| |
| Future<void> test_collectionLiteral_const() async { |
| const testCode = ''' |
| main() { |
| const colors = [ |
| [[COLOR]], |
| ]; |
| } |
| '''; |
| await checkAllColors(testCode, onlyConst: true); |
| } |
| |
| Future<void> test_collectionLiteral_nonConst() async { |
| const testCode = ''' |
| main() { |
| final colors = [ |
| [[COLOR]], |
| ]; |
| } |
| '''; |
| await checkAllColors(testCode); |
| } |
| |
| Future<void> test_customClass() async { |
| const testCode = ''' |
| import 'other_file.dart'; |
| |
| void main() { |
| final a1 = MyTheme.staticWhite; |
| final a2 = MyTheme.staticMaterialRedAccent; |
| const theme = MyTheme(); |
| final b1 = theme.instanceWhite; |
| final b2 = theme.instanceMaterialRedAccent; |
| } |
| '''; |
| |
| const otherCode = ''' |
| class MyTheme { |
| static const Color staticWhite = Colors.white; |
| static const MaterialAccentColor staticMaterialRedAccent = Colors.redAccent; |
| |
| final Color instanceWhite; |
| final MaterialAccentColor instanceMaterialRedAccent; |
| |
| const MyTheme() |
| : instanceWhite = Colors.white, |
| instanceMaterialRedAccent = Colors.redAccent; |
| } |
| '''; |
| await expectColors( |
| testCode, |
| { |
| 'MyTheme.staticWhite': 0xFFFFFFFF, |
| 'MyTheme.staticMaterialRedAccent': 0xFFFFAA00, |
| 'theme.instanceWhite': 0xFFFFFFFF, |
| 'theme.instanceMaterialRedAccent': 0xFFFFAA00, |
| }, |
| otherCode: otherCode, |
| ); |
| } |
| |
| Future<void> test_local_const() async { |
| const testCode = ''' |
| main() { |
| const a = [[COLOR]]; |
| } |
| '''; |
| await checkAllColors(testCode, onlyConst: true); |
| } |
| |
| Future<void> test_local_nonConst() async { |
| const testCode = ''' |
| main() { |
| final a = [[COLOR]]; |
| } |
| '''; |
| await checkAllColors(testCode); |
| } |
| |
| Future<void> test_namedParameter_const() async { |
| const testCode = ''' |
| main() { |
| const w = Widget(color: [[COLOR]]); |
| } |
| |
| class Widget { |
| final Color? color; |
| const Widget({this.color}); |
| } |
| '''; |
| await checkAllColors(testCode, onlyConst: true); |
| } |
| |
| Future<void> test_namedParameter_nonConst() async { |
| const testCode = ''' |
| main() { |
| final w = Widget(color: [[COLOR]]); |
| } |
| |
| class Widget { |
| final Color? color; |
| Widget({this.color}); |
| } |
| '''; |
| await checkAllColors(testCode); |
| } |
| |
| Future<void> test_nested_const() async { |
| const testCode = ''' |
| main() { |
| const a = [[COLOR]]; |
| } |
| '''; |
| await checkAllColors(testCode, onlyConst: true); |
| } |
| |
| Future<void> test_nested_nonConst() async { |
| const testCode = ''' |
| main() { |
| final a = [[COLOR]]; |
| } |
| '''; |
| await checkAllColors(testCode); |
| } |
| |
| Future<void> test_topLevel_const() async { |
| const testCode = ''' |
| const a = [[COLOR]]; |
| '''; |
| await checkAllColors(testCode, onlyConst: true); |
| } |
| |
| Future<void> test_topLevel_nonConst() async { |
| const testCode = ''' |
| final a = [[COLOR]]; |
| '''; |
| await checkAllColors(testCode); |
| } |
| |
| String _withCommonImports(String code) => ''' |
| import 'package:flutter/cupertino.dart'; |
| import 'package:flutter/painting.dart'; |
| import 'package:flutter/material.dart'; |
| |
| $code'''; |
| } |