createDeferredRegistrationsRunner

Create a runner executing deferred registration functions through the same sequence as a real application (strictly for testing purpose).

Reference

const runner = createDeferredRegistrationsRunner(runtime, localModules: [], options?: { context? })

Parameters

  • runtime: A FireflyRuntime instance.
  • localModules: An array of module registration functions.
  • options: An optional object literal of options:
    • context: An optional context object forwarded to the module registration functions.

Returns

An object literal with the following functions:

  • register(data?): Register the modules, then execute their deferred registration functions with the "register" operation. Can only be called once. Returns a Promise object resolving to an array of ModuleRegistrationError.
  • update(data?): Execute the deferred registration functions again with the "update" operation, as a transactional run. Must be called after register. Returns a Promise object resolving to an array of ModuleRegistrationError.

An update run drops the deferred navigation items of the previous run and replays only what the current run registers. It also reproduces everything the useDeferredRegistrations hook does around that run, because modules, plugins and third-party libraries rely on it to reset their per run state:

  1. DeferredRegistrationsUpdateStartedEvent is dispatched.
  2. The deferred registration functions are executed.
  3. The app router store deferredRegistrationsUpdatedAt value is updated and DeferredRegistrationsUpdatedEvent is dispatched.
  4. DeferredRegistrationsUpdateCompletedEvent is dispatched.

Usage

Create a runner

import { createDeferredRegistrationsRunner } from "@squide/firefly/testing";
import { EnvironmentVariablesPlugin, FireflyRuntime, type ModuleRegisterFunction } from "@squide/firefly";

const register: ModuleRegisterFunction<FireflyRuntime, unknown, FeatureFlags> = runtime => {
    return (deferredRuntime, data) => {
        if (data.isBillingEnabled) {
            deferredRuntime.registerNavigationItem({ $id: "billing", $label: "Billing", to: "/billing" });
        }
    };
};

const runtime = new FireflyRuntime({
    plugins: [x => new EnvironmentVariablesPlugin(x)]
});

const runner = createDeferredRegistrationsRunner(runtime, [register]);

Execute a registration run

await runner.register({ isBillingEnabled: true });

expect(runtime.getNavigationItems().length).toBe(1);

Execute an update run

An update run reproduces what happens when a feature flag or the session changes:

await runner.register({ isBillingEnabled: true });

await runner.update({ isBillingEnabled: false });

expect(runtime.getNavigationItems().length).toBe(0);

Test multiple modules

Deferred registration defects usually involve more than one module. Provide every module participating in the scenario, as they all execute within the same run:

const registerSection: ModuleRegisterFunction<FireflyRuntime, unknown, FeatureFlags> = () => {
    return (runtime, data) => {
        if (data.isBillingEnabled) {
            runtime.registerNavigationItem({ $id: "billing", $label: "Billing", children: [] });
        }
    };
};

const registerNestedItem: ModuleRegisterFunction<FireflyRuntime, unknown, FeatureFlags> = () => {
    return runtime => {
        runtime.registerNavigationItem({ $id: "invoices", $label: "Invoices", to: "/invoices" }, { sectionId: "billing" });
    };
};

const runner = createDeferredRegistrationsRunner(runtime, [registerSection, registerNestedItem]);

Test a module reacting to an update run

A module keeping state across its registrations usually resets that state when an update run starts. Since a runner dispatches the update events, the module reacts exactly as it would at runtime:

import { DeferredRegistrationsUpdateStartedEvent } from "@squide/firefly";

const register: ModuleRegisterFunction<FireflyRuntime, unknown, FeatureFlags> = runtime => {
    const registeredSections = new Set<string>();

    runtime.eventBus.addListener(DeferredRegistrationsUpdateStartedEvent, () => registeredSections.clear());

    return (deferredRuntime, data) => {
        if (!data.isBillingEnabled) {
            return;
        }

        // Register the section on the first item of the run.
        if (!registeredSections.has("billing")) {
            registeredSections.add("billing");
            deferredRuntime.registerNavigationItem({ $id: "billing", $label: "Billing", children: [] });
        }

        deferredRuntime.registerNavigationItem({ $id: "invoices", $label: "Invoices", to: "/invoices" }, { sectionId: "billing" });
    };
};
const runner = createDeferredRegistrationsRunner(runtime, [register]);

await runner.register({ isBillingEnabled: true });
await runner.update({ isBillingEnabled: true });

expect(runtime.getNavigationItems().length).toBe(1);

Without the listener, the section is registered on the first run only, and the update run leaves the items pending under a section that no longer exists.

Handle registration errors

Errors are collected rather than thrown, matching what a real registration run does:

const errors = await runner.register({ isBillingEnabled: true });

expect(errors.length).toBe(0);

Test a standalone deferred registration function

When a test targets a deferred registration function rather than a module, wrap the function into a module registration function:

const registerBillingNavigationItems: DeferredRegistrationFunction<FireflyRuntime, FeatureFlags> = (runtime, data) => {
    // ...
};

const runner = createDeferredRegistrationsRunner(runtime, [() => registerBillingNavigationItems]);

Provide a context

const runner = createDeferredRegistrationsRunner(runtime, [register], {
    context: { host: "sample" }
});

Assert that no registration is pending

Navigation items registered under a section that no longer exists are parked as pending rather than rejected, so a run that lost a section still resolves without errors. Assert on the resulting navigation items to catch it:

await runner.register({ isBillingEnabled: true });

await runner.update({ isBillingEnabled: true });

expect(runtime.getNavigationItems()).toMatchObject([{ $id: "billing", children: [{ $id: "invoices" }] }]);