Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
278 / 278
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('filters items into a new collection', function (): void {
185        $collection = createTestCollection([1, 2, 3, 4, 5]);
186        expect($collection->filter(fn ($item): bool => $item % 2 === 0)->values())->toBe([2, 4]);
187    });
188
189    it('reduces items to a single value', function (): void {
190        $collection = createTestCollection([1, 2, 3, 5, 8]);
191        expect($collection->reduce(fn ($carry, $item): array|float|int => $carry + $item, 0))->toBe(19);
192    });
193
194    it('calls a callback for each item', function (): void {
195        $collection = createTestCollection([1, 2, 3, 5, 8]);
196        $count = 0;
197        $collection->each(function ($item) use (&$count): void {
198            $count += $item;
199        });
200        expect($count)->toBe(19);
201    });
202
203    it('returns true if at least one item satisfies the condition', function (): void {
204        $collection = createTestCollection([1, 2, 3, 5, 8]);
205        expect($collection->some(fn ($item): bool => $item < 5))->toBeTrue();
206        expect($collection->some(fn ($item): bool => $item > 10))->toBeFalse();
207    });
208
209    it('returns true when all elements satisfy condition', function (): void {
210        $collection = createTestCollection([2, 4, 6, 8]);
211
212        expect($collection->every(fn ($item): bool => $item % 2 === 0))->toBeTrue();
213    });
214
215    it('returns false when at least one element does not satisfy condition', function (): void {
216        $collection = createTestCollection([2, 3, 4, 5]);
217
218        expect($collection->every(fn ($item): bool => $item % 2 === 0))->toBeFalse();
219    });
220
221    it('returns true for every() on empty collection', function (): void {
222        $collection = createTestCollection([]);
223
224        expect($collection->every(fn ($item): false => false))->toBeTrue();
225    });
226
227    it('finds first element that satisfies condition', function (): void {
228        $collection = createTestCollection([1, 2, 3, 4, 5]);
229
230        $result = $collection->find(fn ($item): bool => $item > 2);
231
232        expect($result)->toBe(3);
233    });
234
235    it('returns false when no element satisfies condition', function (): void {
236        $collection = createTestCollection([1, 2, 3]);
237
238        $result = $collection->find(fn ($item): bool => $item > 10);
239
240        expect($result)->toBeFalse();
241    });
242
243    it('removes duplicate string values', function (): void {
244        $collection = createTestCollection(['a', 'b', 'a', 'c', 'b', 'd']);
245
246        $unique = $collection->unique();
247
248        expect($unique)->toHaveCount(4);
249        expect($unique->values())->toBe(['a', 'b', 'c', 'd']);
250    });
251
252    it('removes duplicate numeric values', function (): void {
253        $collection = createTestCollection([1, 2, 3, 2, 4, 1, 5]);
254
255        $unique = $collection->unique();
256
257        expect($unique)->toHaveCount(5);
258        expect($unique->values())->toBe([1, 2, 3, 4, 5]);
259    });
260});
261
262describe('Mutation', function (): void {
263    it('adds an item to the collection', function (): void {
264        $collection = createTestCollection();
265        expect($collection)->toHaveCount(0);
266
267        $collection->add('foo');
268        expect($collection)->toHaveCount(1);
269
270        $collection->add('bar');
271        expect($collection)->toHaveCount(2);
272    });
273
274    it('removes first occurrence of element and returns true', function (): void {
275        $collection = createTestCollection(['foo', 'bar', 'baz']);
276        expect($collection)->toHaveCount(3);
277
278        $result = $collection->remove('bar');
279
280        expect($result)->toBeTrue();
281        expect($collection)->toHaveCount(2);
282        expect($collection->values())->toBe(['foo', 'baz']);
283    });
284
285    it('returns false when removing non-existent element', function (): void {
286        $collection = createTestCollection(['foo', 'bar', 'baz']);
287        expect($collection)->toHaveCount(3);
288
289        $result = $collection->remove('qix');
290
291        expect($result)->toBeFalse();
292        expect($collection)->toHaveCount(3);
293        expect($collection->values())->toBe(['foo', 'bar', 'baz']);
294    });
295
296    it('empties the collection on clear', function (): void {
297        $collection = createTestCollection(['foo', 'bar', 'baz']);
298        expect($collection)->toHaveCount(3);
299        expect($collection->isEmpty())->toBeFalse();
300        expect($collection->items())->not->toBeEmpty();
301
302        $collection->clear();
303
304        expect($collection)->toHaveCount(0);
305        expect($collection->isEmpty())->toBeTrue();
306        expect($collection->items())->toBeEmpty();
307    });
308});
309
310describe('Iteration', function (): void {
311    it('supports IteratorAggregate interface', function (): void {
312        $collection = createTestCollection(['foo', 'bar']);
313
314        $count = 0;
315
316        foreach ($collection as $item) {
317            ++$count;
318        }
319
320        expect($count)->toBe(2);
321    });
322});
323
324describe('Slicing and ordering', function (): void {
325    it('slices a portion of the collection', function (): void {
326        $collection = createTestCollection(['a', 'b', 'c', 'd', 'e']);
327
328        $sliced = $collection->slice(1, 3);
329
330        expect($sliced)->toHaveCount(3);
331        expect($sliced->values())->toBe(['b', 'c', 'd']);
332    });
333
334    it('preserves string keys when slicing', function (): void {
335        $collection = createTestCollection(['x' => 'a', 'y' => 'b', 'z' => 'c']);
336
337        $sliced = $collection->slice(1, 2);
338
339        expect($sliced->keys())->toBe(['y', 'z']);
340        expect($sliced->values())->toBe(['b', 'c']);
341    });
342
343    it('preserves numeric keys when slicing', function (): void {
344        $collection = createTestCollection(['a', 'b', 'c', 'd', 'e']);
345
346        $sliced = $collection->slice(1, 3);
347
348        expect($sliced->keys())->toBe([1, 2, 3]);
349        expect($sliced->values())->toBe(['b', 'c', 'd']);
350    });
351
352    it('takes first N elements', function (): void {
353        $collection = createTestCollection([1, 2, 3, 4, 5]);
354
355        $taken = $collection->take(3);
356
357        expect($taken)->toHaveCount(3);
358        expect($taken->values())->toBe([1, 2, 3]);
359    });
360
361    it('reverses the collection', function (): void {
362        $collection = createTestCollection(['a', 'b', 'c', 'd']);
363
364        $reversed = $collection->reverse();
365
366        expect($reversed->values())->toBe(['d', 'c', 'b', 'a']);
367    });
368
369    it('preserves string keys when reversing', function (): void {
370        $collection = createTestCollection(['x' => 'a', 'y' => 'b', 'z' => 'c']);
371
372        $reversed = $collection->reverse();
373
374        expect($reversed->keys())->toBe(['z', 'y', 'x']);
375        expect($reversed->values())->toBe(['c', 'b', 'a']);
376    });
377
378    it('preserves numeric keys when reversing', function (): void {
379        $collection = createTestCollection(['a', 'b', 'c', 'd']);
380
381        $reversed = $collection->reverse();
382
383        expect($reversed->keys())->toBe([3, 2, 1, 0]);
384        expect($reversed->values())->toBe(['d', 'c', 'b', 'a']);
385    });
386
387    it('sorts collection in ascending order', function (): void {
388        $collection = createTestCollection([5, 2, 8, 1, 9]);
389
390        $sorted = $collection->sort(fn ($a, $b): int => $a <=> $b);
391
392        expect($sorted->values())->toBe([1, 2, 5, 8, 9]);
393    });
394
395    it('sorts collection in descending order', function (): void {
396        $collection = createTestCollection([5, 2, 8, 1, 9]);
397
398        $sorted = $collection->sort(fn ($a, $b): int => $b <=> $a);
399
400        expect($sorted->values())->toBe([9, 8, 5, 2, 1]);
401    });
402});