Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions TUnit.Assertions.Tests/TypeInferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,21 @@ await Assert.That(enumerable)
[Test]
public async Task PredicateAssertionsReturnItem()
{
// Contains with predicate returns the found item, not the collection
// This allows further assertions on the item itself
// Contains with predicate can be awaited to get the found item
// Or chained with .And to continue collection assertions
IEnumerable<int> enumerable = [1, 2, 3];

try
{
// Test 1: Await to get the found item
var item = await Assert.That(enumerable).Contains(x => x > 1);
await Assert.That(item).IsGreaterThan(0);

// Test 2: Chain with .And for collection assertions
await Assert.That(enumerable)
.Contains(x => x > 1) // Returns Assertion<int> with the found item
.Contains(x => x > 1)
.And
.IsGreaterThan(0); // Can assert on the found item
.Contains(x => x > 2); // Can chain multiple Contains
}
catch
{
Expand Down
62 changes: 40 additions & 22 deletions TUnit.Assertions/Conditions/CollectionAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -531,51 +531,69 @@ protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TValue> m

/// <summary>
/// Asserts that a collection contains an item matching the predicate.
/// When awaited, returns the found item for further assertions.
/// </summary>
public class CollectionContainsPredicateAssertion<TCollection, TItem> : Assertion<TItem>
public class CollectionContainsPredicateAssertion<TCollection, TItem> : Assertion<TCollection>
where TCollection : IEnumerable<TItem>
{
private readonly Func<TItem, bool> _predicate;
private TItem? _foundItem;

public CollectionContainsPredicateAssertion(
AssertionContext<TCollection> context,
Func<TItem, bool> predicate)
: base(context.Map<TItem>(collection =>
{
if (collection == null)
{
throw new ArgumentNullException(nameof(collection), "collection was null");
}

foreach (var item in collection)
{
if (predicate(item))
{
return item;
}
}

throw new InvalidOperationException("no item matching predicate found in collection");
}))
: base(context)
{
_predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TItem> metadata)
protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TCollection> metadata)
{
var value = metadata.Value;
var exception = metadata.Exception;

if (exception != null)
{
return Task.FromResult(AssertionResult.Failed(exception.Message));
return Task.FromResult(AssertionResult.Failed($"threw {exception.GetType().Name}"));
}

// If we got here, the item was found (the Map function succeeded)
return Task.FromResult(AssertionResult.Passed);
if (value == null)
{
return Task.FromResult(AssertionResult.Failed("collection was null"));
}

// Search for matching item
foreach (var item in value)
{
if (_predicate(item))
{
_foundItem = item;
return Task.FromResult(AssertionResult.Passed);
}
}

return Task.FromResult(AssertionResult.Failed("no item matching predicate found in collection"));
}

protected override string GetExpectation() => "to contain item matching predicate";

/// <summary>
/// Enables await syntax that returns the found item.
/// This allows both chaining (.And) and item capture (await).
/// </summary>
public new System.Runtime.CompilerServices.TaskAwaiter<TItem> GetAwaiter()
{
return ExecuteAndReturnItemAsync().GetAwaiter();
}

private async Task<TItem> ExecuteAndReturnItemAsync()
{
// Execute the assertion (will throw if item not found)
await AssertAsync();

// Return the found item
return _foundItem!;
}
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,12 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TItem>
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TCollection>
where TCollection : .<TItem>
{
public CollectionContainsPredicateAssertion(.<TCollection> context, <TItem, bool> predicate) { }
protected override .<.> CheckAsync(.<TItem> metadata) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
public new .<TItem> GetAwaiter() { }
protected override string GetExpectation() { }
}
public class CollectionCountAssertion<TValue> : .<TValue>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,12 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TItem>
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TCollection>
where TCollection : .<TItem>
{
public CollectionContainsPredicateAssertion(.<TCollection> context, <TItem, bool> predicate) { }
protected override .<.> CheckAsync(.<TItem> metadata) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
public new .<TItem> GetAwaiter() { }
protected override string GetExpectation() { }
}
public class CollectionCountAssertion<TValue> : .<TValue>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,11 +384,12 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TItem>
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TCollection>
where TCollection : .<TItem>
{
public CollectionContainsPredicateAssertion(.<TCollection> context, <TItem, bool> predicate) { }
protected override .<.> CheckAsync(.<TItem> metadata) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
public new .<TItem> GetAwaiter() { }
protected override string GetExpectation() { }
}
public class CollectionCountAssertion<TValue> : .<TValue>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,11 +382,12 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TItem>
public class CollectionContainsPredicateAssertion<TCollection, TItem> : .<TCollection>
where TCollection : .<TItem>
{
public CollectionContainsPredicateAssertion(.<TCollection> context, <TItem, bool> predicate) { }
protected override .<.> CheckAsync(.<TItem> metadata) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
public new .<TItem> GetAwaiter() { }
protected override string GetExpectation() { }
}
public class CollectionCountAssertion<TValue> : .<TValue>
Expand Down
Loading