blob: 1a00ef8d3443d84a2efcea3d89d00cd3961db811 [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 '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';