Skip to content

Commit 8506e7e

Browse files
committed
✅ test: Add comprehensive test suite for RegionBuilder functionality.
1 parent 9a0fc66 commit 8506e7e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3143
-39
lines changed

docs/README.md

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,16 @@ This trivial example shows how a product may move through various states during
5050

5151
require 'vendor/autoload.php';
5252

53+
use Noem\State\Feature\Transitions\AddTransition;
54+
use Noem\State\Feature\Transitions\TransitionsFeature;
5355
use Noem\State\RegionBuilder;
5456

5557
$orderId = 12345;
5658

5759
// Define the state machine
5860
$region = (new RegionBuilder())
61+
// Enable the transitions feature
62+
->enableFeatures(new TransitionsFeature())
5963
// State configuration
6064
->setStates('Checkout', 'Pending', 'Processing', 'Shipped', 'Delivered')
6165
->markInitial('Checkout')
@@ -66,10 +70,10 @@ $region = (new RegionBuilder())
6670

6771
// Transitions
6872
// In a real application, these inspect the $trigger and allow/deny the transition based on it
69-
->pushTransition(from: 'Checkout', to: 'Pending', guard: fn(object $trigger): bool => true)
70-
->pushTransition(from: 'Pending', to: 'Processing', guard: fn(object $trigger): bool => true)
71-
->pushTransition(from: 'Processing', to: 'Shipped', guard: fn(object $trigger): bool => true)
72-
->pushTransition(from: 'Shipped', to: 'Delivered', guard: fn(object $trigger): bool => true)
73+
->addBuildStep(new AddTransition(from: 'Checkout', to: 'Pending', guard: fn(object $trigger): bool => true))
74+
->addBuildStep(new AddTransition(from: 'Pending', to: 'Processing', guard: fn(object $trigger): bool => true))
75+
->addBuildStep(new AddTransition(from: 'Processing', to: 'Shipped', guard: fn(object $trigger): bool => true))
76+
->addBuildStep(new AddTransition(from: 'Shipped', to: 'Delivered', guard: fn(object $trigger): bool => true))
7377

7478
// Entry events
7579
->onEnter(state: 'Pending', callback: function (object $trigger) {
@@ -108,9 +112,13 @@ within a state machine, making it convenient for implementing stateful behavior
108112

109113
declare(strict_types=1);
110114

115+
use Noem\State\Feature\Transitions\AddTransition;
116+
use Noem\State\Feature\Transitions\TransitionsFeature;
111117
use Noem\State\RegionBuilder;
112118

113119
$r = (new RegionBuilder())
120+
// Enable the transitions feature
121+
->enableFeatures(new TransitionsFeature())
114122
// Define all possible states
115123
->setStates('off', 'starting', 'on', 'error')
116124
// if not called, will default to the first entry
@@ -119,13 +127,13 @@ $r = (new RegionBuilder())
119127
->markFinal('error')
120128
// Define a transition from one state to another
121129
// <FROM> <TO> <PREDICATE>
122-
->pushTransition('off', 'starting', fn(object $trigger):bool => true)
130+
->addBuildStep(new AddTransition('off', 'starting', fn(object $trigger):bool => true))
123131
// no predicate means always true
124-
->pushTransition('starting', 'on')
125-
->pushTransition('on', 'error', function(\Throwable $exception){
132+
->addBuildStep(new AddTransition('starting', 'on'))
133+
->addBuildStep(new AddTransition('on', 'error', function(\Throwable $exception){
126134
echo 'Error: '. $exception->getMessage();
127135
return true;
128-
})
136+
}))
129137
// Add a callback that runs whenever the specified state is entered
130138
->onEnter('starting', function(object $trigger){
131139
echo 'Starting application';
@@ -161,11 +169,12 @@ This means you can -for example- only allow a transition when an Exception occur
161169

162170
```php
163171
$r = new RegionBuilder();
164-
$r->setStates('on', 'running', 'error')
165-
->pushTransition('on', 'error', function(\Throwable $exception){
172+
$r->enableFeatures(new TransitionsFeature())
173+
->setStates('on', 'running', 'error')
174+
->addBuildStep(new AddTransition('on', 'error', function(\Throwable $exception){
166175
echo 'Error: '. $exception->getMessage();
167176
return true;
168-
})
177+
}))
169178
;
170179
```
171180

@@ -174,8 +183,9 @@ and greatly helps serializing application state. The syntax is a little more com
174183

175184
```php
176185
$r = new RegionBuilder();
177-
$r->setStates('one', 'two', 'three')
178-
->pushTransition('one', 'two', fn(#[Name('hello-world')] Event $event): bool => true)
186+
$r->enableFeatures(new TransitionsFeature())
187+
->setStates('one', 'two', 'three')
188+
->addBuildStep(new AddTransition('one', 'two', fn(#[Name('hello-world')] Event $event): bool => true))
179189
;
180190
```
181191
Here, the `Name` attribute works in tandem with the internal `Event` interface that mandates a name.
@@ -261,8 +271,9 @@ $middleware = function (RegionBuilder $builder, \Closure $next) use (&$logs) {
261271
};
262272

263273
$region = (new RegionBuilder())
274+
->enableFeatures(new TransitionsFeature())
264275
->setStates('foo', 'bar')
265-
->pushTransition('foo', 'bar')
276+
->addBuildStep(new AddTransition('foo', 'bar'))
266277
->pushMiddleware($middleware)
267278
->build();
268279
```

tests/PHPUnit/Integration/Feature/AsyncFeature/AsyncFeatureTest.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use Noem\State\Feature\ExtendedState\ExtendedState;
1111
use Noem\State\Feature\Template\Compiler\TemplateFactory;
1212
use Noem\State\Feature\Template\Helpers;
13+
use Noem\State\Feature\Transitions\AddTransition;
14+
use Noem\State\Feature\Transitions\TransitionsFeature;
1315
use Noem\State\Test\Integration\RegionBuilderTestCase;
1416
use PHPUnit\Framework\Attributes\Test;
1517
use PHPUnit\Framework\Attributes\TestDox;
@@ -21,7 +23,8 @@ public function setUp(): void
2123
{
2224
parent::setUp();
2325
$this->builder->enableFeatures(
24-
new AsyncFeature()
26+
new AsyncFeature(),
27+
new TransitionsFeature()
2528
);
2629
}
2730

@@ -192,9 +195,9 @@ public function resolver()
192195
new ExtendedState()
193196
)
194197
->setStates('one', 'two')
195-
->pushTransition('one', 'two', function (object $t): bool {
198+
->addBuildStep(new AddTransition('one', 'two', function (object $t): bool {
196199
return $this->get('context') !== null;
197-
})
200+
}))
198201
->onEnter('two', function (object $t) {
199202
$t->out .= $this->get('context');
200203
})
@@ -243,10 +246,10 @@ public function resolveWithNestedContext()
243246
new ExtendedState()
244247
)
245248
->setStates('off', 'one', 'two')
246-
->pushTransition('off', 'one')
247-
->pushTransition('one', 'two', function (object $t): bool {
249+
->addBuildStep(new AddTransition('off', 'one'))
250+
->addBuildStep(new AddTransition('one', 'two', function (object $t): bool {
248251
return $this->get('context') !== null;
249-
})
252+
}))
250253
->onEnter('one', function (object $t) {
251254
$this->set('dependency', 'waiting');
252255
})
@@ -368,9 +371,9 @@ public function testFetch()
368371
$t->out .= '|END';
369372
yield;
370373
})
371-
->pushTransition('one', 'two', function (object $t): bool {
374+
->addBuildStep(new AddTransition('one', 'two', function (object $t): bool {
372375
return str_ends_with($t->out, 'END');
373-
})
376+
}))
374377
->build();
375378

376379
$payload = new \stdClass();

tests/PHPUnit/Integration/Feature/EventHooks/EventHooksTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Noem\State\Feature\EventHooks\Hook\Before;
1010
use Noem\State\Feature\ExtendedState\ExtendedState;
1111
use Noem\State\Feature\NamedEvents\Event;
12+
use Noem\State\Feature\Transitions\AddTransition;
1213
use Noem\State\Test\Integration\RegionBuilderTestCase;
1314
use PHPUnit\Framework\Attributes\Test;
1415
use PHPUnit\Framework\Attributes\TestDox;
@@ -98,7 +99,7 @@ public function hooksCanCauseTransitions()
9899
/**
99100
* We register a transition caused by an After event hook
100101
*/
101-
->pushTransition('one', 'two', #[After] fn(Event $t): bool => $guardSpy())
102+
->addBuildStep(new AddTransition('one', 'two', #[After] fn(Event $t): bool => $guardSpy()))
102103
->onAction('two', #[After] function (Event $t) use (&$actual) {
103104
$actual .= '/after';
104105
})

tests/PHPUnit/Integration/Feature/Loader/LoaderTest.php

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
use Noem\State\Feature\Loader\RegionLoader;
1414
use Noem\State\Feature\Loader\RegionSpawnRegistry;
1515
use Noem\State\Feature\OrthogonalRegions\OrthogonalRegions;
16+
use Noem\State\Feature\Transitions\AddTransition;
17+
use Noem\State\Feature\Transitions\TransitionsFeature;
1618
use Noem\State\Middleware\ChainException;
1719
use Noem\State\Test\Integration\RegionBuilderTestCase;
1820
use PHPUnit\Framework\Attributes\Test;
@@ -31,7 +33,8 @@ public function setUp(): void
3133
new ExtendedState(),
3234
new OrthogonalRegions(),
3335
new AsyncFeature(),
34-
new JsonSchemaFeature()
36+
new JsonSchemaFeature(),
37+
new TransitionsFeature()
3538
);
3639
}
3740

@@ -63,13 +66,13 @@ public function spawnSubMachine()
6366
->builder
6467
->newInstance()
6568
->setStates('checking', 'check_complete')
66-
->pushTransition(
69+
->addBuildStep(new AddTransition(
6770
'checking',
6871
'check_complete',
6972
function (object $t): bool {
7073
return $t->count >= 4;
7174
}
72-
)
75+
))
7376
->onEnter('check_complete', function (object $t) {
7477
$this->get('foo');
7578
})
@@ -82,7 +85,7 @@ function (object $t): bool {
8285
$region = $this
8386
->builder
8487
->setStates('off', 'on')
85-
->pushTransition('off', 'on')
88+
->addBuildStep(new AddTransition('off', 'on'))
8689
->build();
8790
$this->assertRegionContext($region, 'html', null);
8891
$counter = 0;
@@ -111,16 +114,16 @@ public function spawnMultipleSubMachines()
111114
->builder
112115
->newInstance()
113116
->setStates('off', 'checking', 'check_complete')
114-
->pushTransition('off', 'checking')
115-
->pushTransition(
117+
->addBuildStep(new AddTransition('off', 'checking'))
118+
->addBuildStep(new AddTransition(
116119
'checking',
117120
'check_complete',
118121
function (object $t): bool {
119122
$count = $this->get('count');
120123

121124
return $count >= 10;
122125
}
123-
)
126+
))
124127
->onEnter('checking', function (object $t) {
125128
$count = (int)$this->get('count');
126129

@@ -137,7 +140,7 @@ function (object $t): bool {
137140
$region = $this
138141
->builder
139142
->setStates('off', 'on')
140-
->pushTransition('off', 'on')
143+
->addBuildStep(new AddTransition('off', 'on'))
141144
->build();
142145
$this->assertRegionContext($region, 'html', null);
143146
$counter = 0;
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Noem\State\Test\Unit\Core\RegionBuilder;
6+
7+
use Noem\State\BuildStep;
8+
use Noem\State\Region;
9+
use Noem\State\RegionBuilder;
10+
use PHPUnit\Framework\Attributes\Group;
11+
use PHPUnit\Framework\TestCase;
12+
13+
/**
14+
* Acceptance Criterion: A builder can add custom build steps using addBuildStep with BuildStep interface
15+
*/
16+
#[Group('region-builder')]
17+
#[Group('build-steps')]
18+
class AddBuildStepTest extends TestCase
19+
{
20+
public function testAddBuildStepAcceptsBuildStepInterface(): void
21+
{
22+
$builder = new RegionBuilder();
23+
$builder->setStates('idle');
24+
25+
$buildStep = new class implements BuildStep {
26+
public function callback(RegionBuilder $builder, callable $next, callable $first): Region
27+
{
28+
return $next($builder);
29+
}
30+
};
31+
32+
$result = $builder->addBuildStep($buildStep);
33+
34+
$this->assertSame($builder, $result, 'addBuildStep should return builder for chaining');
35+
36+
$region = $builder->build();
37+
$this->assertInstanceOf(Region::class, $region);
38+
}
39+
40+
public function testAddBuildStepCanBeCalledMultipleTimes(): void
41+
{
42+
$builder = new RegionBuilder();
43+
$builder->setStates('idle');
44+
45+
$stepOne = new class implements BuildStep {
46+
public function callback(RegionBuilder $builder, callable $next, callable $first): Region
47+
{
48+
return $next($builder);
49+
}
50+
};
51+
52+
$stepTwo = new class implements BuildStep {
53+
public function callback(RegionBuilder $builder, callable $next, callable $first): Region
54+
{
55+
return $next($builder);
56+
}
57+
};
58+
59+
$builder->addBuildStep($stepOne)
60+
->addBuildStep($stepTwo);
61+
62+
$region = $builder->build();
63+
$this->assertInstanceOf(Region::class, $region);
64+
}
65+
66+
public function testBuildStepIsInvokedDuringBuild(): void
67+
{
68+
$builder = new RegionBuilder();
69+
$builder->setStates('idle');
70+
71+
$stepInvoked = false;
72+
73+
$buildStep = new class($stepInvoked) implements BuildStep {
74+
public function __construct(private bool &$invoked) {}
75+
76+
public function callback(RegionBuilder $builder, callable $next, callable $first): Region
77+
{
78+
$this->invoked = true;
79+
return $next($builder);
80+
}
81+
};
82+
83+
$builder->addBuildStep($buildStep);
84+
85+
$this->assertFalse($stepInvoked, 'Build step should not be invoked before build()');
86+
87+
$region = $builder->build();
88+
89+
$this->assertTrue($stepInvoked, 'Build step should be invoked during build()');
90+
$this->assertInstanceOf(Region::class, $region);
91+
}
92+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Noem\State\Test\Unit\Core\RegionBuilder;
6+
7+
use Noem\State\RegionBuilder;
8+
use PHPUnit\Framework\Attributes\Group;
9+
use PHPUnit\Framework\TestCase;
10+
11+
/**
12+
* Acceptance Criterion: A builder can add individual states using addState
13+
*/
14+
#[Group('region-builder')]
15+
#[Group('state-configuration')]
16+
class AddStateTest extends TestCase
17+
{
18+
public function testAddStateSingleState(): void
19+
{
20+
$builder = new RegionBuilder();
21+
22+
$result = $builder->addState('initial');
23+
24+
$this->assertSame($builder, $result, 'addState should return builder for chaining');
25+
26+
$region = $builder->build();
27+
28+
$this->assertTrue($region->isInState('initial'));
29+
}
30+
31+
public function testAddStateMultipleTimes(): void
32+
{
33+
$builder = new RegionBuilder();
34+
35+
$builder->addState('first')
36+
->addState('second')
37+
->addState('third');
38+
39+
$region = $builder->build();
40+
41+
$this->assertTrue($region->isInState('first'), 'Region should start in first added state');
42+
}
43+
44+
public function testAddStateCanBeMixedWithSetStates(): void
45+
{
46+
$builder = new RegionBuilder();
47+
48+
$builder->setStates('one', 'two')
49+
->addState('three');
50+
51+
$region = $builder->build();
52+
53+
$this->assertTrue($region->isInState('one'));
54+
}
55+
}

0 commit comments

Comments
 (0)