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
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,30 @@ WeDevelop\E2e\Fixtures\FixtureLoader:
to by walking these classes in the order listed. `fixtures` maps the name used by
the Playwright client to a module-relative path of the fixture YAML.

### Config overrides during load
The most common write-time customisation is suppressing a side effect whose
trigger is a config static — e.g. a container model that auto-scaffolds children
in `onAfterWrite()` and would duplicate children the fixture declares itself.
For that case declare the statics to force per class; each is applied only while
the record is written and reverts immediately afterwards (it never leaks into
normal app code):

```yaml
WeDevelop\E2e\Fixtures\FixtureLoader:
config_overrides:
My\Module\Model\Section:
auto_scaffold: false
My\Module\Model\Row:
auto_scaffold: false
```

Reach for an `onBeforeLoad` extension (below) only when a static value can't
express it — dynamic values or non-config side effects.

### Extension hooks
To inject domain behavior around a load — for example suppressing model
auto-scaffolding or publishing extra records — add an `Extension` implementing
either hook and wire it via `WeDevelop\E2e\Fixtures\FixtureLoader.extensions`:
To inject domain behavior around a load — for example computing dynamic values or
publishing extra records — add an `Extension` implementing either hook and wire
it via `WeDevelop\E2e\Fixtures\FixtureLoader.extensions`:

```php
public function onBeforeLoad(string $name, FixtureFactory $factory): void
Expand Down
48 changes: 48 additions & 0 deletions src/Fixtures/FixtureLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@
use RuntimeException;
use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Control\Director;
use SilverStripe\Core\Config\Config;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Extensible;
use SilverStripe\Core\Injector\Injectable;
use SilverStripe\Core\Manifest\ModuleResourceLoader;
use SilverStripe\Dev\FixtureBlueprint;
use SilverStripe\Dev\FixtureFactory;
use SilverStripe\Dev\YamlFixture;
use SilverStripe\ORM\DataObject;
Expand Down Expand Up @@ -65,6 +67,24 @@ class FixtureLoader
*/
private static array $fixtures = [];

/**
* Config statics to force on specific classes while fixtures are written.
*
* For each class, the given key/value pairs are applied via a
* FixtureBlueprint `beforeCreate` callback, so the override is set inside
* FixtureBlueprint's own Config::nest()/unnest() window and reverts once the
* record is written — it never leaks into normal app code.
*
* This is the declarative form of the common `onBeforeLoad` use case:
* suppressing write-time side effects (auto-scaffolding, auto-publishing,
* denormalisation hooks) whose trigger is a config static. Anything a static
* value cannot express (dynamic values, non-config side effects) still
* belongs in an `onBeforeLoad` extension.
*
* @var array<class-string, array<string, scalar>>
*/
private static array $config_overrides = [];

/**
* Load a single named fixture into the database.
*
Expand Down Expand Up @@ -137,6 +157,10 @@ public function loadAll(): array
private function loadFixtureFromPath(string $name, string $path): FixtureResult
{
$factory = new FixtureFactory();
// Apply declarative config overrides before the onBeforeLoad hook so a
// consumer's dynamic extension can still override the same class (a later
// FixtureFactory::define() replaces an earlier blueprint for that class).
$this->applyConfigOverrides($factory);
// Extensible::extend() takes its arguments by reference (&...$arguments),
// which makes PHPStan widen every passed variable to the union of all of
// them for the rest of the scope. Pass throwaway aliases so the typed
Expand Down Expand Up @@ -319,6 +343,30 @@ private function resolveFixturePath(string $name): string
return $absolutePath;
}

/**
* Register blueprints that force {@see $config_overrides} statics during the
* write of each targeted class.
*
* Each override is set from a `beforeCreate` callback, which FixtureBlueprint
* runs inside its own Config::nest()/unnest() window — so the value is live
* for that record's write only and is restored immediately afterwards.
*/
private function applyConfigOverrides(FixtureFactory $factory): void
{
/** @var array<class-string, array<string, scalar>> $overrides */
$overrides = static::config()->get('config_overrides');

foreach ($overrides as $class => $settings) {
$blueprint = new FixtureBlueprint($class);
$blueprint->addCallback('beforeCreate', static function () use ($class, $settings): void {
foreach ($settings as $key => $value) {
Config::modify()->set($class, $key, $value);
}
});
$factory->define($class, $blueprint);
}
}

/**
* @return list<FixturePostAction>
*/
Expand Down
80 changes: 80 additions & 0 deletions tests/Integration/Fixtures/FixtureLoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
use SilverStripe\Dev\SapphireTest;
use SilverStripe\Versioned\Versioned;
use WeDevelop\E2e\Fixtures\FixtureLoader;
use WeDevelop\E2e\Tests\Support\E2eConfigProbeObject;
use WeDevelop\E2e\Tests\Support\E2eFixtureTestPage;
use WeDevelop\E2e\Tests\Support\E2eOtherTestPage;
use WeDevelop\E2e\Tests\Support\E2eScaffoldingObject;
use WeDevelop\E2e\Tests\Support\E2eVersionedObject;
use WeDevelop\E2e\Tests\Support\LoaderHookSpy;

Expand All @@ -24,6 +26,8 @@ final class FixtureLoaderTest extends SapphireTest
E2eFixtureTestPage::class,
E2eOtherTestPage::class,
E2eVersionedObject::class,
E2eScaffoldingObject::class,
E2eConfigProbeObject::class,
];

protected function setUp(): void
Expand All @@ -37,6 +41,7 @@ protected function setUp(): void
E2eFixtureTestPage::class,
]);
LoaderHookSpy::reset();
E2eConfigProbeObject::reset();
}

public function testLoadWritesFixtureAndReturnsResult(): void
Expand Down Expand Up @@ -327,6 +332,81 @@ public function testLoadAllDoesNotResetWhenAFixturePathIsInvalid(): void
self::assertCount($simplePageCountBefore, $this->draftPagesWithSegmentPrefix('e2e-simple'));
}

public function testConfigOverrideReachesRecordDuringWrite(): void
{
Config::modify()->set(FixtureLoader::class, 'fixtures', [
'config-overrides' => 'wedevelopnl/silverstripe-e2e:tests/Support/fixtures/config-overrides.yml',
]);
Config::modify()->set(FixtureLoader::class, 'config_overrides', [
E2eConfigProbeObject::class => ['probe_alpha' => 'overridden'],
]);

FixtureLoader::create()->load('config-overrides');

self::assertSame('overridden', E2eConfigProbeObject::$observed['probe_alpha']);
}

public function testConfigOverrideDoesNotLeakPastLoad(): void
{
Config::modify()->set(FixtureLoader::class, 'fixtures', [
'config-overrides' => 'wedevelopnl/silverstripe-e2e:tests/Support/fixtures/config-overrides.yml',
]);
Config::modify()->set(FixtureLoader::class, 'config_overrides', [
E2eConfigProbeObject::class => ['probe_alpha' => 'overridden'],
]);

FixtureLoader::create()->load('config-overrides');

// The override is scoped to each record's write by FixtureBlueprint's
// Config::nest()/unnest(); normal app code sees the declared default.
self::assertSame('default', Config::inst()->get(E2eConfigProbeObject::class, 'probe_alpha'));
}

public function testConfigOverridesApplyMultipleClassesAndKeys(): void
{
Config::modify()->set(FixtureLoader::class, 'fixtures', [
'config-overrides' => 'wedevelopnl/silverstripe-e2e:tests/Support/fixtures/config-overrides.yml',
]);
Config::modify()->set(FixtureLoader::class, 'config_overrides', [
E2eScaffoldingObject::class => ['auto_scaffold' => false],
E2eConfigProbeObject::class => [
'probe_alpha' => 'alpha-override',
'probe_beta' => 'beta-override',
],
]);

$childrenBefore = $this->countScaffoldChildren();

FixtureLoader::create()->load('config-overrides');

// auto_scaffold=false suppressed the child on E2eScaffoldingObject...
self::assertSame($childrenBefore, $this->countScaffoldChildren());
// ...and both keys on the second class took effect at write time.
self::assertSame('alpha-override', E2eConfigProbeObject::$observed['probe_alpha']);
self::assertSame('beta-override', E2eConfigProbeObject::$observed['probe_beta']);
}

public function testAbsentConfigOverridesIsNoOp(): void
{
Config::modify()->set(FixtureLoader::class, 'fixtures', [
'config-overrides' => 'wedevelopnl/silverstripe-e2e:tests/Support/fixtures/config-overrides.yml',
]);
// No config_overrides configured (default []): the model's own
// auto_scaffold default (true) stands and its config statics are untouched.
$childrenBefore = $this->countScaffoldChildren();

FixtureLoader::create()->load('config-overrides');

self::assertSame($childrenBefore + 1, $this->countScaffoldChildren());
self::assertSame('default', E2eConfigProbeObject::$observed['probe_alpha']);
self::assertSame('default', E2eConfigProbeObject::$observed['probe_beta']);
}

private function countScaffoldChildren(): int
{
return (int) E2eScaffoldingObject::get()->filter('Title', 'scaffolded-container')->count();
}

/**
* @return array<int, SiteTree>
*/
Expand Down
52 changes: 52 additions & 0 deletions tests/Support/E2eConfigProbeObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

declare(strict_types=1);

namespace WeDevelop\E2e\Tests\Support;

use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

/**
* Records the value of its config statics as observed during its own write.
*
* Lets tests prove that FixtureLoader.config_overrides reaches the record at
* write time (the overridden value is what {@see $observed} captures) and that
* the override is scoped to the write (the live config reverts afterwards).
*/
class E2eConfigProbeObject extends DataObject implements TestOnly
{
private static string $table_name = 'E2eConfigProbeObject';

/** @var array<string, string> */
private static array $db = [
'Title' => 'Varchar(255)',
];

private static string $probe_alpha = 'default';

private static string $probe_beta = 'default';

/**
* Config values seen during writes since the last reset, keyed by config
* name. Overwritten per write, so it reflects the most recent probe.
*
* @var array<string, string>
*/
public static array $observed = [];

public static function reset(): void
{
self::$observed = [];
}

protected function onBeforeWrite(): void
{
parent::onBeforeWrite();

self::$observed = [
'probe_alpha' => (string) static::config()->get('probe_alpha'),
'probe_beta' => (string) static::config()->get('probe_beta'),
];
}
}
58 changes: 58 additions & 0 deletions tests/Support/E2eScaffoldingObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace WeDevelop\E2e\Tests\Support;

use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;

/**
* Stand-in for a container model that auto-scaffolds a child on write.
*
* Mirrors the real-world case config_overrides exists for: a model whose
* onAfterWrite() creates extra records unless a config static disables it.
* When `auto_scaffold` is true a single child is scaffolded; a config override
* setting it false during the fixture write suppresses that child.
*/
class E2eScaffoldingObject extends DataObject implements TestOnly
{
private static string $table_name = 'E2eScaffoldingObject';

/** @var array<string, string> */
private static array $db = [
'Title' => 'Varchar(255)',
];

/**
* When true, writing this object scaffolds one child. Consumers suppress
* this during fixture loads via FixtureLoader.config_overrides.
*/
private static bool $auto_scaffold = true;

/** Marks factory-created children so they never scaffold recursively. */
private bool $isScaffoldChild = false;

public function markAsScaffoldChild(): void
{
$this->isScaffoldChild = true;
}

protected function onAfterWrite(): void
{
parent::onAfterWrite();

if ($this->isScaffoldChild) {
return;
}

if (!static::config()->get('auto_scaffold')) {
return;
}

$child = new self();
$child->markAsScaffoldChild();
$child->Title = 'scaffolded-' . $this->Title;
$child->write();
}
}
10 changes: 10 additions & 0 deletions tests/Support/fixtures/config-overrides.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
WeDevelop\E2e\Tests\Support\E2eFixtureTestPage:
page1:
Title: 'E2E Config Overrides Page'
URLSegment: 'e2e-config-overrides'
WeDevelop\E2e\Tests\Support\E2eScaffoldingObject:
container1:
Title: 'container'
WeDevelop\E2e\Tests\Support\E2eConfigProbeObject:
probe1:
Title: 'probe'
Loading