Skip to content

The Fluent UI test suite re-declares the collection-item fixture's models #343

Description

@phmatray

Problem / motivation

#297 (PR #306) made "no test suite re-declares a collection-item shape the fixture provides" a build
failure. The guard works — and it scans exactly one assembly, FormCraft.ForMudBlazor.UnitTests.

FormCraft.ForFluentUI.UnitTests gained a full collection-field suite in #278 (merged as #291), and it
has already re-declared the fixture's models. Measured on dev:

// FormCraft.ForFluentUI.UnitTests/Components/CollectionFieldSubmitTests.cs
public class OrderModel
{
    public string Reference { get; set; } = string.Empty;
    public List<OrderItem> Items { get; set; } = [];
}

public class OrderItem
{
    public string ProductName { get; set; } = string.Empty;
}

OrderItem { string ProductName } is CollectionItemFixture.OrderItem, under the same name, in a
second assembly. Two more collection roots are alongside it —
CollectionFieldTests.cs:207 (List<OrderLine>) and RenderPipelineParityTests.cs:175
(List<ParityLine>) — which is the #205 story restarting from zero in the newer adapter: the same
shapes, under drifting vocabularies, with nothing to notice.

This is not a hypothetical tail. It is the exact condition #205 was filed for, reproduced in a suite
the guard cannot see.

Proposed solution

Two things, in order — and the first is the reason this is not a one-line change:

  1. The fixture is not reachable from the Fluent UI suite. CollectionItemFixture lives inside
    FormCraft.ForMudBlazor.UnitTests, and test projects do not reference one another. Sharing it means
    extracting the framework-agnostic half — the models and the factories, which name no MudBlazor type
    — into a small shared test-support project both suites reference. The builders return
    IFormConfiguration<T> and are equally framework-agnostic; only the RenderItemForm extension is
    bUnit/MudBlazor-flavoured and stays behind.
  2. Then make the guard scan both. TestAssemblyTypes() already exists; it needs to take an
    assembly rather than hard-coding its own, and each suite runs the guard over its own.

Note the ownership rule needs a second look for the cross-assembly case: it currently reads
"namespace-scope = shared, nested = copy", and the Fluent UI copies are public and namespace-scope,
so they would be classified as shared rather than flagged. Ownership has to become "declared in the
shared fixture assembly", which is what step 1 makes expressible.

Alternatives considered

  • Copy the fixture into the Fluent UI test project. Cheapest, and precisely the defect: two
    fixtures drift exactly the way the eleven per-suite copies did before Extract a shared collection-item test fixture instead of copying models per suite #205.
  • Leave it; the two suites are independent. Rejected on evidence — the duplication already exists,
    under the same type names, and the adapter parity work (Bring FormCraft.ForFluentUI to parity with the MudBlazor adapter #278) guarantees more of it as Fluent UI
    catches up with MudBlazor feature by feature.
  • Generalise the guard first, extract the fixture later. Rejected as the wrong order: a guard that
    flags the Fluent UI models before there is any shared fixture for them to adopt reports a problem
    with no legal fix, so the only available response is to allowlist all of them — which trains everyone
    to reach for the allowlist.

Area

FormCraft.ForFluentUI.UnitTests / FormCraft.ForMudBlazor.UnitTests — test infrastructure


Follow-up from #306. Related: #205, #258, #282, #297, #278

🧠 Brainstorm

Problem / context

Four issues have now worked the same seam. #205 extracted CollectionItemFixture after eleven suites
each carried their own copies; #258 migrated eleven more and closed with a name-based grep; #282 found
the five that grep could not see; #297 made the rule mechanical so a fifth human sweep is not needed.

The guard #297 shipped is genuinely good at its job within FormCraft.ForMudBlazor.UnitTests. Its
blind spot is structural rather than logical: Assembly.GetTypes() on its own assembly. The Fluent UI
adapter's test suite is a second assembly, it did not exist in a meaningful form when the fixture was
extracted, and it now models collections of its own.

The awkward part is that the obvious fix — "point the guard at both assemblies" — cannot come first.
The Fluent UI suite has no access to CollectionItemFixture, so flagging its models would report a
defect with no available remedy.

Approaches

A. Extract the framework-agnostic half of the fixture into a shared test-support project, then scan both.
Pros: removes the duplication rather than merely reporting it; the guard becomes cross-assembly for
free; the Fluent UI suite gets the same five-field-type coverage MudBlazor's suites get by default.
Cons: a new project in the solution; the fixture's ownership rule has to move from
"namespace-scope vs nested" to "declared in the shared assembly".

B. Scan both assemblies now, allowlist the Fluent UI models.
Pros: one-line-ish. Cons: the allowlist becomes where problems go to be silenced rather than where
exceptions are justified — the opposite of what #297 designed it for.

C. Leave the Fluent UI suite unguarded and document it.
Pros: zero work. Cons: the duplication is already three models deep and grows with every parity
issue; documenting a known gap is how #258's check came to be trusted for four years' worth of drift.

Recommendation

A, in the stated order. It is the only option where the guard's report has a legal fix at the moment
it fires. The extraction is also smaller than it sounds: the models are dumb POCOs and the builders
return IFormConfiguration<T>, so only the bUnit render extension is adapter-specific.

📋 Spec

Goal

Both adapter test suites draw their collection-item models from one shared fixture, and the shape guard
runs over both assemblies.

Scope

  • A shared test-support project holding the fixture's models, factories and item-form builders.
  • Both test projects reference it; the Fluent UI collection suites migrate onto it.
  • The guard takes an assembly, and runs from both suites.
  • Ownership becomes "declared in the shared fixture assembly", replacing namespace-scope vs nested.

Non-goals

Shape

flowchart TD
    S["FormCraft.TestSupport (new)<br/>models · factories · item-form builders<br/>+ CollectionItemShapeGuard"]
    S --> M["FormCraft.ForMudBlazor.UnitTests<br/>RenderItemForm (bUnit/Mud)<br/>guard over its own assembly"]
    S --> F["FormCraft.ForFluentUI.UnitTests<br/>its own render helper<br/>guard over its own assembly"]
Loading

Validation rules

  • Per-suite test counts unchanged for every migrated Fluent UI suite; project totals never fall.
  • The guard, run from the Fluent UI suite, reports zero offenders after migration and a non-zero
    count before it (capture the before-list — it is the evidence the migration was needed).
  • dotnet build -c Release --no-incremental → 0 warnings; ./build.sh Test → green.
  • No production file in git diff --name-only dev...HEAD.

Edge cases

  • The Fluent UI copies are public and namespace-scope, so today's ownership rule would call them
    shared. The rule must key on the declaring assembly before the guard is pointed at them, or it
    reports nothing and looks like it passed.
  • OrderModel exists in both assemblies under the same name. After extraction one must win;
    the Fluent UI one also carries a root Reference field, which is the NamedOrderModel shape rather
    than OrderModel's — check which fixture model each Fluent UI suite actually needs instead of
    assuming the name maps.
  • FormCraft.UnitTests is a third assembly. It has no collection-item models today; decide whether
    it references the shared project now or is left out until it needs to.

Assumptions

  • Base branch is dev. Test-only, so no version impact.
  • A shared test-support project is acceptable in the solution; if not, the fixture could instead be
    compiled into both suites via a linked-file glob, which is uglier but avoids a project.

🛠️ Implementation plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: one shared collection-item fixture, consumed by both adapter test suites, with the shape
guard running over both assemblies.

Architecture: test infrastructure only. No production file may be modified.

Tech stack: .NET 10 test projects (libraries multi-target net8.0;net10.0), xUnit v3 +
Microsoft.Testing.Platform, bUnit, Shouldly.

Global constraints:

  • Base branch dev; commit as Philippe Matray <phmatray@gmail.com>; conventional commits; PR title
    ends (#<issue>) and is linted by pr-title-lint.yml.
  • TreatWarningsAsErrors=true — any warning fails the build. That is the format/lint gate.
  • dotnet test --filter is inert (MTP0001). Filter via the test host directly, after a build:
    FormCraft.ForFluentUI.UnitTests/bin/Release/net10.0/FormCraft.ForFluentUI.UnitTests --filter-class <FQN>.
    Treat Zero tests ran (exit 8) as a hard stop.
  • Record per-suite test counts before starting and assert each is unchanged after.
  • CI gate is ./build.sh Test.

Task 1: Extract the framework-agnostic fixture into a shared test-support project

Files: create FormCraft.TestSupport/FormCraft.TestSupport.csproj and CollectionItemFixture.cs; modify FormCraft.sln, FormCraft.ForMudBlazor.UnitTests.csproj, FormCraft.ForMudBlazor.UnitTests/Fields/CollectionItemFixture.cs.

Interfaces: the existing models, factories and *ItemForm builders, moved verbatim; RenderItemForm stays in the MudBlazor suite.

  • Step 1: Record every suite's test count and the three project totals.
  • Step 2: Create the project (no bUnit/MudBlazor reference — if it needs one, the wrong half is moving) and add it to the solution.
  • Step 3: Move the models, factories and item-form builders across, leaving CollectionItemFixtureRenderExtensions in the MudBlazor suite; reference the new project from FormCraft.ForMudBlazor.UnitTests.
  • Step 4: Run dotnet build -c Release --no-incremental → 0 warnings, then dotnet test -c Release → PASS with every count unchanged.
  • Step 5: Commit: test: extract the collection-item fixture into a shared test-support project.

Task 2: Re-key the guard's ownership rule to the fixture assembly

Files: modify FormCraft.ForMudBlazor.UnitTests/Fields/CollectionItemShapeGuard.cs + its tests (move both to the shared project if the guard is to run from both suites).

Interfaces: TestAssemblyTypes(Assembly assembly); IsSharedShape keyed on the declaring assembly rather than on nesting.

  • Step 1: Write the failing test: a namespace-scope, public copy in a non-fixture assembly must be reported as an offender (today's nesting rule calls it shared).
  • Step 2: Run the guard suite → FAIL.
  • Step 3: Re-key IsSharedShape to "declared in the fixture's assembly" and thread an Assembly parameter through TestAssemblyTypes.
  • Step 4: Run dotnet test -c Release → PASS, MudBlazor's own guard still reporting zero offenders.
  • Step 5: Commit: test: key collection-item ownership on the fixture assembly.

Task 3: Point the guard at the Fluent UI suite and migrate what it flags

Files: create the guard entry point in FormCraft.ForFluentUI.UnitTests; modify Components/CollectionFieldSubmitTests.cs, Fields/CollectionFieldTests.cs, Components/RenderPipelineParityTests.cs.

Interfaces: none new — Tasks 1-2 provide them.

  • Step 1: Add the guard test to the Fluent UI suite and run it; record the offender list — it is the evidence this issue is about, and the before/after pair is what proves the migration landed.
  • Step 2: Record each flagged suite's test count.
  • Step 3: Migrate each flagged model onto the shared fixture, checking which fixture shape each suite actually needs (OrderModel there carries a root Reference, which is NamedOrderModel's shape, not OrderModel's).
  • Step 4: Allowlist, with a written reason, any model that is genuinely bespoke to the Fluent UI suite rather than migrating it.
  • Step 5: Run dotnet test -c Release → PASS, every recorded count unchanged, and the Fluent UI guard now reporting zero offenders.
  • Step 6: Run dotnet build -c Release --no-incremental → 0 warnings, then ./build.sh Test → green.
  • Step 7: Confirm git diff --name-only dev...HEAD lists no production file.
  • Step 8: Commit: test(fluentui): adopt the shared collection-item fixture.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions