| // Copyright (c) 2022, 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 'dart:math' as math; |
| |
| import 'package:checks/context.dart'; |
| |
| import 'core.dart'; |
| |
| extension StringChecks on Subject<String> { |
| /// Expects that the value contains [pattern] according to [String.contains]; |
| void contains(Pattern pattern) { |
| context.expect(() => prefixFirst('contains ', literal(pattern)), (actual) { |
| if (actual.contains(pattern)) return null; |
| return Rejection( |
| which: prefixFirst('Does not contain ', literal(pattern)), |
| ); |
| }); |
| } |
| |
| Subject<int> get length => has((m) => m.length, 'length'); |
| |
| void isEmpty() { |
| context.expect(() => const ['is empty'], (actual) { |
| if (actual.isEmpty) return null; |
| return Rejection(which: ['is not empty']); |
| }); |
| } |
| |
| void isNotEmpty() { |
| context.expect(() => const ['is not empty'], (actual) { |
| if (actual.isNotEmpty) return null; |
| return Rejection(which: ['is empty']); |
| }); |
| } |
| |
| void startsWith(Pattern other) { |
| context.expect( |
| () => prefixFirst('starts with ', literal(other)), |
| (actual) { |
| if (actual.startsWith(other)) return null; |
| return Rejection( |
| which: prefixFirst('does not start with ', literal(other)), |
| ); |
| }, |
| ); |
| } |
| |
| void endsWith(String other) { |
| context.expect( |
| () => prefixFirst('ends with ', literal(other)), |
| (actual) { |
| if (actual.endsWith(other)) return null; |
| return Rejection( |
| which: prefixFirst('does not end with ', literal(other)), |
| ); |
| }, |
| ); |
| } |
| |
| /// Expects that the string matches the pattern [expected]. |
| /// |
| /// Fails if [expected] returns an empty result from calling `allMatches` with |
| /// the value. |
| /// |
| /// ``` |
| /// check(actual).matchesPattern('abc'); |
| /// check(actual).matchesPattern(RegExp(r'\d')); |
| /// ``` |
| void matchesPattern(Pattern expected) { |
| context.expect(() => prefixFirst('matches ', literal(expected)), (actual) { |
| if (expected.allMatches(actual).isNotEmpty) return null; |
| return Rejection( |
| which: prefixFirst('does not match ', literal(expected))); |
| }); |
| } |
| |
| /// Expects that the `String` contains each of the sub strings in expected |
| /// in the given order, with any content between them. |
| /// |
| /// For example, the following will succeed: |
| /// |
| /// check('abcdefg').containsInOrder(['a','e']); |
| void containsInOrder(Iterable<String> expected) { |
| context.expect(() => prefixFirst('contains, in order: ', literal(expected)), |
| (actual) { |
| var fromIndex = 0; |
| for (var s in expected) { |
| var index = actual.indexOf(s, fromIndex); |
| if (index < 0) { |
| return Rejection(which: [ |
| ...prefixFirst( |
| 'does not have a match for the substring ', literal(s)), |
| if (fromIndex != 0) |
| 'following the other matches up to character $fromIndex' |
| ]); |
| } |
| fromIndex = index + s.length; |
| } |
| return null; |
| }); |
| } |
| |
| /// Expects that the `String` contains exactly the same code units as |
| /// [expected]. |
| void equals(String expected) { |
| context.expect(() => prefixFirst('equals ', literal(expected)), |
| (actual) => _findDifference(actual, expected)); |
| } |
| |
| /// Expects that the `String` contains the same characters as [expected] if |
| /// both were lower case. |
| void equalsIgnoringCase(String expected) { |
| context.expect( |
| () => prefixFirst('equals ignoring case ', literal(expected)), |
| (actual) => _findDifference( |
| actual.toLowerCase(), expected.toLowerCase(), actual, expected)); |
| } |
| |
| /// Expects that the `String` contains the same content as [expected], |
| /// ignoring differences in whitsepace. |
| /// |
| /// All runs of whitespace characters are collapsed to a single space, and |
| /// leading and traiilng whitespace are removed before comparison. |
| /// |
| /// For example the following will succeed: |
| /// |
| /// check(' hello world ').equalsIgnoringWhitespace('hello world'); |
| /// |
| /// While the following will fail: |
| /// |
| /// check('helloworld').equalsIgnoringWhitespace('hello world'); |
| /// check('he llo world').equalsIgnoringWhitespace('hello world'); |
| void equalsIgnoringWhitespace(String expected) { |
| context.expect( |
| () => prefixFirst('equals ignoring whitespace ', literal(expected)), |
| (actual) { |
| final collapsedActual = _collapseWhitespace(actual); |
| final collapsedExpected = _collapseWhitespace(expected); |
| return _findDifference(collapsedActual, collapsedExpected, |
| collapsedActual, collapsedExpected); |
| }); |
| } |
| } |
| |
| Rejection? _findDifference(String actual, String expected, |
| [String? actualDisplay, String? expectedDisplay]) { |
| if (actual == expected) return null; |
| final escapedActual = escape(actual); |
| final escapedExpected = escape(expected); |
| final escapedActualDisplay = |
| actualDisplay != null ? escape(actualDisplay) : escapedActual; |
| final escapedExpectedDisplay = |
| expectedDisplay != null ? escape(expectedDisplay) : escapedExpected; |
| final minLength = math.min(escapedActual.length, escapedExpected.length); |
| var i = 0; |
| for (; i < minLength; i++) { |
| if (escapedActual.codeUnitAt(i) != escapedExpected.codeUnitAt(i)) { |
| break; |
| } |
| } |
| if (i == minLength) { |
| if (escapedExpected.length < escapedActual.length) { |
| if (expected.isEmpty) { |
| return Rejection(which: ['is not the empty string']); |
| } |
| return Rejection(which: [ |
| 'is too long with unexpected trailing characters:', |
| _trailing(escapedActualDisplay, i) |
| ]); |
| } else { |
| if (actual.isEmpty) { |
| return Rejection(actual: [ |
| 'an empty string' |
| ], which: [ |
| 'is missing all expected characters:', |
| _trailing(escapedExpectedDisplay, 0) |
| ]); |
| } |
| return Rejection(which: [ |
| 'is too short with missing trailing characters:', |
| _trailing(escapedExpectedDisplay, i) |
| ]); |
| } |
| } else { |
| final indentation = ' ' * (i > 10 ? 14 : i); |
| return Rejection(which: [ |
| 'differs at offset $i:', |
| '${_leading(escapedExpectedDisplay, i)}' |
| '${_trailing(escapedExpectedDisplay, i)}', |
| '${_leading(escapedActualDisplay, i)}' |
| '${_trailing(escapedActualDisplay, i)}', |
| '$indentation^' |
| ]); |
| } |
| } |
| |
| /// The truncated beginning of [s] up to the [end] character. |
| String _leading(String s, int end) => |
| (end > 10) ? '... ${s.substring(end - 10, end)}' : s.substring(0, end); |
| |
| /// The truncated remainder of [s] starting at the [start] character. |
| String _trailing(String s, int start) => (start + 10 > s.length) |
| ? s.substring(start) |
| : '${s.substring(start, start + 10)} ...'; |
| |
| /// Utility function to collapse whitespace runs to single spaces |
| /// and strip leading/trailing whitespace. |
| String _collapseWhitespace(String string) { |
| var result = StringBuffer(); |
| var skipSpace = true; |
| for (var i = 0; i < string.length; i++) { |
| var character = string[i]; |
| if (_isWhitespace(character)) { |
| if (!skipSpace) { |
| result.write(' '); |
| skipSpace = true; |
| } |
| } else { |
| result.write(character); |
| skipSpace = false; |
| } |
| } |
| return result.toString().trim(); |
| } |
| |
| bool _isWhitespace(String ch) => |
| ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t'; |