blob: d449c0b6bb0dc2cf7e5bde7d0402f43be622b464 [file] [log] [blame]
// 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 '../../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';