Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
282 / 282
100.00% covered (success)
100.00%
1 / 1
CRAP
n/a
0 / 0
createTestCollection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3// ╔════════════════════════════════════════════════════════════╗
4// ║ MIT Licence (#Expat) - https://opensource.org/licenses/MIT ║
5// ║ Copyright 2026 Frederic Poeydomenge <dyno@phexium.com>     ║
6// ╚════════════════════════════════════════════════════════════╝
7
8declare(strict_types=1);
9
10pest()->group('unit');
11
12use Phexium\Domain\Collection\AbstractCollection;
13
14function createTestCollection(array $items = []): object
15{
16    return new class($items) extends AbstractCollection {};
17}
18
19describe('Factory methods', function (): void {
20    it('creates a collection from array', function (): void {
21        $collection = createTestCollection()::fromArray([]);
22        expect($collection->items())->toBeEmpty();
23
24        $collection = createTestCollection()::fromArray(['foo']);
25        expect($collection->items())->toHaveCount(1);
26
27        $collection = createTestCollection()::fromArray(['key1' => 'a', 'key2' => 'b']);
28        expect($collection->items())->toHaveCount(2);
29    });
30
31    it('creates a collection from map', function (): void {
32        $array = ['foo', 'bar', 'baz'];
33        $callable = fn ($item) => strtoupper((string) $item);
34
35        $collection = createTestCollection()::fromMap($array, $callable);
36
37        expect($collection->items())->toHaveCount(3);
38        expect($collection->values())->toBe(['FOO', 'BAR', 'BAZ']);
39    });
40});
41
42describe('Element access', function (): void {
43    it('returns empty array when collection is empty', function (): void {
44        $collection = createTestCollection();
45
46        $items = $collection->items();
47        $values = $collection->values();
48
49        expect($items)->toBeArray();
50        expect($items)->toBeEmpty();
51        expect($values)->toBeEmpty();
52    });
53
54    it('returns all items', function (): void {
55        $collection = createTestCollection(['key1' => 'a', 'key2' => 'b']);
56
57        $items = $collection->items();
58        $values = $collection->values();
59
60        expect($items)->toBeArray();
61        expect($items)->toHaveCount(2);
62        expect($values)->toHaveCount(2);
63    });
64
65    it('returns the first and last item', function (): void {
66        $collection = createTestCollection([]);
67        expect($collection->first())->toBeFalse();
68        expect($collection->last())->toBeFalse();
69
70        $collection = createTestCollection(['qix']);
71        expect($collection->first())->toBe('qix');
72        expect($collection->last())->toBe('qix');
73
74        $collection = createTestCollection(['foo', 'bar', 'baz']);
75        expect($collection->first())->toBe('foo');
76        expect($collection->last())->toBe('baz');
77    });
78
79    it('returns the number of items', function (): void {
80        $collection = createTestCollection();
81        expect($collection->count())->toBe(0);
82        expect($collection)->toHaveCount(0);
83
84        $collection = createTestCollection(['foo']);
85        expect($collection->count())->toBe(1);
86        expect($collection)->toHaveCount(1);
87
88        $collection = createTestCollection(['key1' => 'a', 'key2' => 'b']);
89        expect($collection->count())->toBe(2);
90        expect($collection)->toHaveCount(2);
91    });
92
93    it('returns true when collection is empty', function (): void {
94        $collection = createTestCollection();
95
96        expect($collection->isEmpty())->toBeTrue();
97    });
98
99    it('returns false when collection has items', function (): void {
100        $collection = createTestCollection(['foo']);
101
102        expect($collection->isEmpty())->toBeFalse();
103    });
104
105    it('returns true when element exists', function (): void {
106        $collection = createTestCollection(['foo', 'bar']);
107
108        expect($collection->contains('foo'))->toBeTrue();
109        expect($collection->contains('bar'))->toBeTrue();
110    });
111
112    it('returns false when element does not exist', function (): void {
113        $collection = createTestCollection(['foo', 'bar']);
114
115        expect($collection->contains('qix'))->toBeFalse();
116    });
117
118    it('returns value at key or default', function (): void {
119        $collection = createTestCollection(['a' => 'foo', 'b' => 'bar']);
120
121        expect($collection->get('a'))->toBe('foo');
122        expect($collection->get('b'))->toBe('bar');
123        expect($collection->get('c'))->toBeNull();
124        expect($collection->get('c', 'default'))->toBe('default');
125    });
126
127    it('returns all keys', function (): void {
128        $collection = createTestCollection(['a' => 'foo', 'b' => 'bar', 'c' => 'baz']);
129
130        expect($collection->keys())->toBe(['a', 'b', 'c']);
131    });
132
133    it('returns numeric keys for indexed array', function (): void {
134        $collection = createTestCollection(['foo', 'bar', 'baz']);
135
136        expect($collection->keys())->toBe([0, 1, 2]);
137    });
138
139    it('returns true when key exists', function (): void {
140        $collection = createTestCollection(['a' => 'foo', 'b' => 'bar']);
141
142        expect($collection->containsKey('a'))->toBeTrue();
143        expect($collection->containsKey('b'))->toBeTrue();
144    });
145
146    it('returns false when key does not exist', function (): void {
147        $collection = createTestCollection(['a' => 'foo']);
148
149        expect($collection->containsKey('b'))->toBeFalse();
150        expect($collection->containsKey('c'))->toBeFalse();
151    });
152
153    it('supports array access', function (): void {
154        $collection = createTestCollection([1, 2]);
155        expect($collection)->toHaveCount(2);
156
157        $collection[] = 9; // [1,2,9]
158        expect($collection)->toHaveCount(3);
159        expect($collection[2])->toBe(9);
160
161        $collection[2] = 3; // [1,2,3]
162        expect($collection)->toHaveCount(3);
163        expect($collection[2])->toBe(3);
164
165        unset($collection[1]); // [1,-,3]
166        expect($collection)->toHaveCount(2);
167
168        expect(isset($collection[0]))->toBeTrue();
169        expect(isset($collection[1]))->toBeFalse();
170        expect(isset($collection[2]))->toBeTrue();
171
172        expect($collection[0])->toBe(1);
173        expect($collection[1])->toBeNull();
174        expect($collection[2])->toBe(3);
175    });
176});
177
178describe('Functional operations', function (): void {
179    it('maps items to a new collection', function (): void {
180        $collection = createTestCollection([1, 2, 3, 5, 8]);
181        expect($collection->map(fn ($item): float|int => $item * 2)->values())->toBe([2, 4, 6, 10, 16]);
182    });
183
184    it('maps items to a plain array', function (): void {
185        $collection = createTestCollection([1, 2, 3, 5, 8]);
186        expect($collection->mapToArray(fn ($item): float|int => $item * 2))->toBe([2, 4, 6, 10, 16]);
187    });
188
189    it('filters items into a new collection', function (): void {
190        $collection = createTestCollection([1, 2, 3, 4, 5]);
191        expect($collection->filter(fn ($item): bool => $item % 2 === 0)->values())->toBe([2, 4]);
192    });
193
194    it('reduces items to a single value', function (): void {
195        $collection = createTestCollection([1, 2, 3, 5, 8]);
196        expect($collection->reduce(fn ($carry, $item): array|float|int => $carry + $item, 0))->toBe(19);
197    });
198
199    it('calls a callback for each item', function (): void {
200        $collection = createTestCollection([1, 2, 3, 5, 8]);
201        $count = 0;
202        $collection->each(function ($item) use (&$count): void {
203            $count += $item;
204        });
205        expect($count)->toBe(19);
206    });
207
208    it('returns true if at least one item satisfies the condition', function (): void {
209        $collection = createTestCollection([1, 2, 3, 5, 8]);
210        expect($collection->some(fn ($item): bool => $item < 5))->toBeTrue();
211        expect($collection->some(fn ($item): bool => $item > 10))->toBeFalse();
212    });
213
214    it('returns true when all elements satisfy condition', function (): void {
215        $collection = createTestCollection([2, 4, 6, 8]);
216
217        expect($collection->every(fn ($item): bool => $item % 2 === 0))->toBeTrue();
218    });
219
220    it('returns false when at least one element does not satisfy condition', function (): void {
221        $collection = createTestCollection([2, 3, 4, 5]);
222
223        expect($collection->every(fn ($item): bool => $item % 2 === 0))->toBeFalse();
224    });
225
226    it('returns true for every() on empty collection', function (): void {
227        $collection = createTestCollection([]);
228
229        expect($collection->every(fn ($item): false => false))->toBeTrue();
230    });
231
232    it('finds first element that satisfies condition', function (): void {
233        $collection = createTestCollection([1, 2, 3, 4, 5]);
234
235        $result = $collection->find(fn ($item): bool => $item > 2);
236
237        expect($result)->toBe(3);
238    });
239
240    it('returns false when no element satisfies condition', function (): void {
241        $collection = createTestCollection([1, 2, 3]);
242
243        $result = $collection->find(fn ($item): bool => $item > 10);
244
245        expect($result)->toBeFalse();
246    });
247
248    it('removes duplicate string values', function (): void {
249        $collection = createTestCollection(['a', 'b', 'a', 'c', 'b', 'd']);
250
251        $unique = $collection->unique();
252
253        expect($unique)->toHaveCount(4);
254        expect($unique->values())->toBe(['a', 'b', 'c', 'd']);
255    });
256
257    it('removes duplicate numeric values', function (): void {
258        $collection = createTestCollection([1, 2, 3, 2, 4, 1, 5]);
259
260        $unique = $collection->unique();
261
262        expect($unique)->toHaveCount(5);
263        expect($unique->values())->toBe([1, 2, 3, 4, 5]);
264    });
265});
266
267describe('Mutation', function (): void {
268    it('adds an item to the collection', function (): void {
269        $collection = createTestCollection();
270        expect($collection)->toHaveCount(0);
271
272        $collection->add('foo');
273        expect($collection)->toHaveCount(1);
274
275        $collection->add('bar');
276        expect($collection)->toHaveCount(2);
277    });
278
279    it('removes first occurrence of element and returns true', function (): void {
280        $collection = createTestCollection(['foo', 'bar', 'baz']);
281        expect($collection)->toHaveCount(3);
282
283        $result = $collection->remove('bar');
284
285        expect($result)->toBeTrue();
286        expect($collection)->toHaveCount(2);
287        expect($collection->values())->toBe(['foo', 'baz']);
288    });
289
290    it('returns false when removing non-existent element', function (): void {
291        $collection = createTestCollection(['foo', 'bar', 'baz']);
292        expect($collection)->toHaveCount(3);
293
294        $result = $collection->remove('qix');
295
296        expect($result)->toBeFalse();
297        expect($collection)->toHaveCount(3);
298        expect($collection->values())->toBe(['foo', 'bar', 'baz']);
299    });
300
301    it('empties the collection on clear', function (): void {
302        $collection = createTestCollection(['foo', 'bar', 'baz']);
303        expect($collection)->toHaveCount(3);
304        expect($collection->isEmpty())->toBeFalse();
305        expect($collection->items())->not->toBeEmpty();
306
307        $collection->clear();
308
309        expect($collection)->toHaveCount(0);
310        expect($collection->isEmpty())->toBeTrue();
311        expect($collection->items())->toBeEmpty();
312    });
313});
314
315describe('Iteration', function (): void {
316    it('supports IteratorAggregate interface', function (): void {
317        $collection = createTestCollection(['foo', 'bar']);
318
319        $count = 0;
320
321        foreach ($collection as $item) {
322            ++$count;
323        }
324
325        expect($count)->toBe(2);
326    });
327});
328
329describe('Slicing and ordering', function (): void {
330    it('slices a portion of the collection', function (): void {
331        $collection = createTestCollection(['a', 'b', 'c', 'd', 'e']);
332
333        $sliced = $collection->slice(1, 3);
334
335        expect($sliced)->toHaveCount(3);
336        expect($sliced->values())->toBe(['b', 'c', 'd']);
337    });
338
339    it('preserves string keys when slicing', function (): void {
340        $collection = createTestCollection(['x' => 'a', 'y' => 'b', 'z' => 'c']);
341
342        $sliced = $collection->slice(1, 2);
343
344        expect($sliced->keys())->toBe(['y', 'z']);
345        expect($sliced->values())->toBe(['b', 'c']);
346    });
347
348    it('preserves numeric keys when slicing', function (): void {
349        $collection = createTestCollection(['a', 'b', 'c', 'd', 'e']);
350
351        $sliced = $collection->slice(1, 3);
352
353        expect($sliced->keys())->toBe([1, 2, 3]);
354        expect($sliced->values())->toBe(['b', 'c', 'd']);
355    });
356
357    it('takes first N elements', function (): void {
358        $collection = createTestCollection([1, 2, 3, 4, 5]);
359
360        $taken = $collection->take(3);
361
362        expect($taken)->toHaveCount(3);
363        expect($taken->values())->toBe([1, 2, 3]);
364    });
365
366    it('reverses the collection', function (): void {
367        $collection = createTestCollection(['a', 'b', 'c', 'd']);
368
369        $reversed = $collection->reverse();
370
371        expect($reversed->values())->toBe(['d', 'c', 'b', 'a']);
372    });
373
374    it('preserves string keys when reversing', function (): void {
375        $collection = createTestCollection(['x' => 'a', 'y' => 'b', 'z' => 'c']);
376
377        $reversed = $collection->reverse();
378
379        expect($reversed->keys())->toBe(['z', 'y', 'x']);
380        expect($reversed->values())->toBe(['c', 'b', 'a']);
381    });
382
383    it('preserves numeric keys when reversing', function (): void {
384        $collection = createTestCollection(['a', 'b', 'c', 'd']);
385
386        $reversed = $collection->reverse();
387
388        expect($reversed->keys())->toBe([3, 2, 1, 0]);
389        expect($reversed->values())->toBe(['d', 'c', 'b', 'a']);
390    });
391
392    it('sorts collection in ascending order', function (): void {
393        $collection = createTestCollection([5, 2, 8, 1, 9]);
394
395        $sorted = $collection->sort(fn ($a, $b): int => $a <=> $b);
396
397        expect($sorted->values())->toBe([1, 2, 5, 8, 9]);
398    });
399
400    it('sorts collection in descending order', function (): void {
401        $collection = createTestCollection([5, 2, 8, 1, 9]);
402
403        $sorted = $collection->sort(fn ($a, $b): int => $b <=> $a);
404
405        expect($sorted->values())->toBe([9, 8, 5, 2, 1]);
406    });
407});