Skip to content

15A: Scenario suite — the load path, end to end - #28

Open
nikolaystrikhar wants to merge 1 commit into
14-activation-error-rewritefrom
15A-scenario-load
Open

15A: Scenario suite — the load path, end to end#28
nikolaystrikhar wants to merge 1 commit into
14-activation-error-rewritefrom
15A-scenario-load

Conversation

@nikolaystrikhar

Copy link
Copy Markdown
Contributor

What: a tests/unit/Scenario/ suite that bootstraps the library the way a host does — Config::set_hook_prefix(), Config::set_container(), Absorber::register(), Absorber::boot() — and then reaches it only through the hooks boot() wired, dispatching plugins_loaded for the load pass and all_admin_notices for the notices, against a real active_plugins, real site options and real files on disk; six load-path scenarios, the shared Bootstrap_Test_Case, an @after fixture cleanup, and the tests/README section every case is written up in.

Usage: the shape of a scenario — a bootstrap, a request, and assertions against what WordPress actually holds afterwards.

$constant = $this->register( [ 'activation_callback' => $record ] );

$this->boot();          // set_container() with a bare container, then boot()
$this->run_request();   // do_action( 'plugins_loaded' )

$this->assertSame( 1, $this->bundled_plugin_loads() );
$this->assertTrue( defined( $constant ) );
$this->assertSame( [ self::SLUG => true ], $this->activation_record() );

Why this way:

The hooks are dispatched, not the steps called. A request is do_action( 'plugins_loaded' ), so what runs is whatever boot() wired, in the order and at the priorities it wired it. That is the half where the bugs are — an admin-only add_action() that never ran, a step wired into a dispatch window that had already closed — and calling Loader::load_all() directly is exactly the arrangement that cannot see any of it. The container is handed over bare rather than pre-registered, for the same reason: boot() running the provider over it is one of the steps under test.

Neither request helper catches the redirect, because it cannot. Every plugins_loaded step wraps itself in catch ( Throwable ) so a hook this library owns can never white-screen a site — which means a stubbed redirect is swallowed and reported before it could leave the step, and a catch around the dispatch would pass whether or not the request redirected. Both helpers watch for the halt where the library announces it and assert both halves: that something reported, and that what it reported was this halt rather than some other failure inside the step.

The halted helper empties plugins_loaded before it throws. exit means the load pass one priority behind does not run either, so a scenario asserting nothing loaded after a redirect would otherwise be asserting it about a request production never serves. The hook is put back afterwards, since emptying it is a statement about that request and not about the process.

Each scenario writes its own fixture and its own guard constant. require_once dedupes by resolved path for the lifetime of the process and define() lasts just as long, so a shared fixture lets a later scenario pass without loading anything — including if the load logic were deleted outright. WithBundledPlugins now cleans up on its own @after hook rather than trusting a caller's tearDown: a failed assertion aborts the test where it stands, so a cleanup line at the end of a body is the one that does not run on the day it matters.

@nikolaystrikhar
nikolaystrikhar force-pushed the 15A-scenario-load branch 2 times, most recently from 61e8c72 to a88768b Compare August 13, 2026 11:17
Six scenarios that bootstrap the library the way a host plugin does -- set the
hook prefix, hand over a bare container, register, boot -- and then reach it only
through the hooks boot() wired. A request is do_action( 'plugins_loaded' ) and an
admin page load is do_action( 'all_admin_notices' ), so what runs is whatever
boot() wired, in the order and at the priorities it wired it. Calling the steps
directly is the one arrangement that cannot see an admin-only add_action() that
never ran, or a step wired into a dispatch window that had already closed.

Bootstrap_Test_Case carries the bootstrap, the two stubs and the helpers; the
conflict and host scenarios extend the same parent and land beside this file.
It is abstract and named _Test_Case so the runner never collects it.

Neither request helper catches the redirect. Every plugins_loaded step wraps
itself in catch ( Throwable ), so a stubbed redirect is swallowed and reported
before it can leave the step -- which means a catch around the dispatch would
pass whether or not the request redirected, and the helper asserting a request
did *not* redirect would assert nothing at all. Both watch for the halt where
the library announces it, and both halves are asserted: that something reported,
and that what it reported was this halt rather than some other failure inside
the step. The halted helper also empties plugins_loaded before it throws, since
exit means the load pass one priority behind does not run either, and puts the
hook back afterwards.

WithBundledPlugins cleans up on its own @after hook rather than trusting a
caller's tearDown: a failed assertion aborts the test where it stands, so a
cleanup line at the end of a body is the one that does not run on the day it
matters. Callers still clear it from tearDown, and the second call is a no-op.

tests/README gains a section for the suite -- what a scenario may call, the four
preconditions setUp establishes, and every case in prose with a mermaid diagram
of the flow. README links to it; it never had a link at all.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant