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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions