Closed egil closed 3 years ago
I like CascadingValues.Add (or perhaps AddCascadingValue(....)
instead.
I too dislike RootComponents.Add, what about WrapWithComponent<T>()
and WrapWithComponent(IComponent)
or EncloseWithinComponent
?
Or perhaps stick with an Add*
theme and go for something like AddCascadingValue
and AddParentComponent
I like CascadingValues.Add (or perhaps
AddCascadingValue(....)
instead.
I think having it as a collection like type (CascadingValues
) opens up to other possibilities, such seeing what values are registered, in a cleaner way.
I too dislike RootComponents.Add, what about
WrapWithComponent<T>()
andWrapWithComponent(IComponent)
orEncloseWithinComponent
?
Similarly, I think I prefer a collection like type for "components that should wrap the CUT" components for consistency with Services
and CascadingValues
. Is WrapperComponents
better than RootComponents
?
ParentComponents
might be more in line with normal terminology - e.g. when people are building a Tab control, they would have a Parent rather than a Root or a Wrapper
@SQL-MisterMagoo I like ParentComponents
better. You are right, it fits the terminology. A parent in a tree like structure like the render tree is the node above the current node. The current node being cut
.
@SQL-MisterMagoo / @mrpmorris added some additional notes about ordering and methods. What do you think?
The only thing that is unclear is if ParentComponents.Add adds it inside the last added parent, or as the parent of the last added parent - which is why I liked WrapWithComponent type method names
The only thing that is unclear is if ParentComponents.Add adds it inside the last added parent, or as the parent of the last added parent - which is why I liked WrapWithComponent type method names
@mrpmorris can you show how that would look Peter? I do not see how two calls to WrapWithComponent
make that more clear than two calls to ParentComponents.Add
do.
Pseudo code using html element names instead of component names
ctx.ParentComponents.Add("p");
ctx.ParentComponents.Add("div");
When I test my component, which will I get
A: In the order they appear in the code?
<p>
<div>
<TestSubject/>
</div>
</p>
If so, then you are actually adding a child to the previous parent
B: In the reverse order?
<div>
<p>
<TestSubject/>
</p>
</div>
If so, the order being opposite to the code is confusing because you have to read the code backwards to understand the resulting component structure
The following code
ctx.WrapWithComponent("p");
ctx.WrapWithComponent("div");
To me clearly means
1: Wrap the component with p
2: Then wrap that with div
But is still a bit back to front, because you have to read the code from bottom to top again - but at least I expect it now.
<div>
<p>
<TestSubject/>
</p>
</div>
How about something like HostingComponents?
ctx.HostingComponents.Add("p");
ctc.HostingComponents.Add("div");
To me this says
1: I want to add a p
to host whatever comes after
2: Then I want to add a div
inside that host, to host whatever comes after
3: Then I want my component inside the most recent host
Pseudo code using html element names instead of component names
ctx.ParentComponents.Add("p"); ctx.ParentComponents.Add("div");
When I test my component, which will I get
A: In the order they appear in the code?
<p> <div> <TestSubject/> </div> </p>
If so, then you are actually adding a child to the previous parent
This is the order I am proposing in the first post.
The following code
ctx.WrapWithComponent("p"); ctx.WrapWithComponent("div");
To me clearly means 1: Wrap the component with
p
2: Then wrap that withdiv
But is still a bit back to front, because you have to read the code from bottom to top again - but at least I expect it now.
<div> <p> <TestSubject/> </p> </div>
Hmm my head doesn't work like that at all. That would we counter intuitive to me. To me, there is no direct relationship between the two calls that makes it obvious that the second ctx.WrapWithComponent("..")
and refers to the first, and when we add add ctx.RenderComponent(..)
into the mix, which must appear after the two calls, then the order makes less sense I think:
ctx.WrapWithComponent("p");
ctx.WrapWithComponent("div");
ctx.RenderComponent(<TestSubject/>)
Would become:
<div> // 2nd call
<p> // 1st call
<TestSubject/> // 3rd call
</p>
</div>
Does that make more sense to you than:
ctx.ParentComponents.Add("div");
ctx.ParentComponents.Add("p");
ctx.RenderComponent(<TestSubject/>)
Becomes:
<div> // 1st call
<p> // 2nd call
<TestSubject/> // 3rd call
</p>
</div>
How about something like HostingComponents?
ctx.HostingComponents.Add("p"); ctc.HostingComponents.Add("div");
To me this says 1: I want to add a
p
to host whatever comes after 2: Then I want to add adiv
inside that host, to host whatever comes after 3: Then I want my component inside the most recent host
Hosting instead of Parent. Have to think about it. What do you relate to the word Hosting?
I have a test subject. If I do ParentComponents.Add("p") then I expect I have just parented my component in a p
. But when I call ParentComponents.Add the second time it does it mean I want add something inside my last added parent, or does it again mean I want again to add a parent to what I have so far? It's just not unambiguous, you have to know, or try it.
I know the answer, but the question shouldn't be possible. It would be better to find a name that forces the answer to be "what else could it mean?"
I'm not happy with any suggestions so far, including my own.
I think PageStructure.Add is pretty good. Structure
says "things built in addition to other things" - in the HTML / component world the structure is
<ThisFirst>
<ThisSecond>
And as components are ultimately hosted in pages, it makes sense that if you want to mock up a page to test a component within (how the user thinks they want to achieve their goal) then you'd look at PageStructure
.
PageStructure.Add("A") PageStructure.Add("B")
Clearly means to create A, and then create B, because we are building in addition to an existing structure - rather than adding a parent.
Or you could go for RenderStructure.Add()
if you want to avoid the word Page
.
I'm not happy with any suggestions so far, including my own.
Indeed. Naming is super hard, and is really important to get as unambiguous an API as possible. I appreciate all the input and viewpoints you provide.
Isn't structure
just a more abstract way to talk about a render tree, e.g. a hierarchical nesting of components?
To me, this ...
PageStructure.Add("Foo")
PageStructure.Add("Bar")
... doesn't look a lot different than the previous examples. Some will probably not get the structure reference. I wonder if we need to make it more visual in the code, by looking at a completely different approach, i.e. something where the calls are nested in code as well.
But then again, the nesting based on ordering of calls is already in play bUnit when using the ChildContent()
methods, e.g., to render:
<CascadingValue Value="foo" />
<CascadingValue Value="bar" />
<MyComp />
it can be done like this:
ctx.RenderComponent<MyComp >(
CascadingValue(foo),
CascadingValue(bar)
);
So it is like not a world ending thing if we cannot find a better way, since the chosen paradigm is discoverable.
I was thinking, maybe ParentComponents/CascadingValues
could have a custom ToString()
that returns the structure as a string :)
The code is the same, but adding to a page structure suggests building on what you already have (like a new floor on a building), whereas adding a parent can also mean adding something before what you already have (in genealogy when you add a parent you don't change your own parent, you discover a new parent at the beginning).
So the second ParentComponents.Add doesn't mean Add, but InsertBetweenSubjectAndPreviousParent - which is an awful name :) Whereas PageStructure.Add can only mean to add to the structure you already have.
Ahh, so the word Insert
might be better compared to Add
?
Also, what about RenderTree
instead of both ParentComponents
and CascadingValues
:
E.g.:
ctx.RenderTree.InsertComponent<CompDIV>();
ctx.RenderTree.InsertComponent<CompP>();
ctx.RenderTree.InsertValue(new MyOtherValue());
ctx.RenderTree.InsertValue("named cascading value", new MyValue());
ctx.RenderComponent<MyComp>();
Would result in the following render tree:
<CompDIV>
<CompP>
<CascadingValue Value=@(new MyOtherValue())>
<CascadingValue Name="named cascading value" Value=@(new MyValue())>
<MyComp>
Are you sure you are not heading towards re-inventing the RenderTreeBuilder (RTB) with methods renamed for confusion? OpenComponent -> InsertComponent
It feels like that is the direction you are heading - at least in terms of a tree of components (from the viewpoint of someone watching the discussion evolve).
The RenderTreeBuilder is a familiar API for a Blazor developer (at least one who is writing C# bUnit tests) and as far as possible it feels like it might be more productive to mimic a known API rather than coming up with new names for the same concepts.
I can see that helper methods like InsertValue (for a CascadingValue) have merit as the raw RTB code is messy for that, but I wonder if sticking to the same API as RTB as much as possible would make it easier to work with?
You might have a point @SQL-MisterMagoo, but as you say, the RTB API is messy, it's not meant for humans but for code generation, which is the reason I have different ways to pass parameters to components in bUnit in C# tests (e.g. see Passing Parameters to Components). The bUnit "RTB API" is not just for creating cascading values, but also for passing parameters and child content to components.
What do you think of having a property RenderTree
on the TestContext
, to which stuff can be added like cascading values and other components? Alternative name could RootRenderTree
, CoreRenderTree
, or BaseRenderTree
, to make it more explicit what it is.
As for using the Open
term vs Insert
or Add
, I am not sure. Open
would lack its counter part Close
, so I am more inclined to pick Add
or Insert
. Add
has the advantage that people will recognize it from e.g. List<T>
as something that appends something to the end of a collection, where as Insert
is actually something that can be used to insert anywhere in the collection. That might be a good idea to include though, to enable controlling whether to insert at the top or bottom of the tree.
Can you give the coder the ability to specify a renderfragment instead, and let them define where the component will go?
You could advise people to create these in one or more razor files and reference them in tests.
MyStatic = @<text>
<p>@context</p>
<text>
@mrpmorris is that RenderFragment syntax allowed in C# files?
Creating a RenderTree.Insert()
overload that takes a RenderFragment as input isn't a problem, but if folks have to define it it in a .razor file, they might as well just create a component and insert that, or am I missing something?
I would agree with what @SQL-MisterMagoo mentions, I immediately thought the proposed APIs looked like a renamed RTB.
Have you considered a fluent style API?
ctx.RenderComponent<MyComp>()
.WithCascadingValue(new MyOtherValue())
.WithParent<ComP>()
.WithParent<CompDiv>();
I quite like this style as for me it makes more sense, but I appreciate others aren't so keen on fluent APIs.
I quite like this style as for me it makes more sense, but I appreciate others aren't so keen on fluent APIs.
@chrissainty I generally do too. My only problem in this case is that it would require a .Build()
or something else to indicate to the test context that it can now start rendering, and that looks ugly in the basic case, e.g.:
// Fine in this case I guess, but still a little annoyed by the .Build()
ctx.RenderComponent<MyComp>()
.WithCascadingValue(new MyOtherValue())
.WithParent<ComP>()
.WithParent<CompDiv>()
.Build(); // or maybe Render()
// Sucks in this case...
ctx.RenderComponent<MyComp>().Build();
// compared to
ctx.RenderComponent<MyComp>();
That said, there is a parameter builder available through the RenderComponent
method, with all sorts of strongly typed magic to make sure that you cannot pass values to a parameter which doesnt match, e.g. for component:
public class NonBlazorTypesParams : ComponentBase
{
[Parameter]
public int Numbers { get; set; }
[Parameter]
public List<string> Lines { get; set; }
}
this works really well I think:
ctx.RenderComponent<NonBlazorTypesParams>(parameters => parameters
.Add(p => p.Numbers, 42)
.Add(p => p.Lines, new List<string> { "Hello", "World" })
);
Anyway, back on topic, I think it is important to stress, that this feature is for things you would want to setup to make a test component render, e.g. calls to RenderTree.Add
/ParentComponents.Add
could easily be placed in "Setup" method, detached from the actual test method.
ctx.RenderComponent<MyComp>() .WithCascadingValue(new MyOtherValue()) .WithParent<ComP>() .WithParent<CompDiv>();
Btw. @chrissainty, example looks like this with the current parameter builder:
ctx.RenderComponent<CompDiv>(cdp => cdp
.AddChildContent<ComP>(cpp => cpp
.AddChildContent<MyComp>(mcp => mcp
.Add(new MyOtherValue())
)
)
);
Assuming that CompDiv is the parent of ComP.
The coder needn't call Build, you could have an overload that accepts a builder type and calls Build on it.
The coder needn't call Build, you could have an overload that accepts a builder type and calls Build on it.
Hmm not following. Can you share an example?
public static void Add(this List<IComponent> list, IBUnitComponentBuilder builder)
{
list.Add(builder.Build());
}
I've just realised we've spent time discussing how we can, and not if we should.
What exactly is it we are trying to solve here? Why would, and how could, a component act differently when parented by another specific component?
What exactly is it we are trying to solve here? Why would, and how could, a component act differently when parented by another specific component?
UI controls, things like tabs or grids. If you're testing them as a whole then the parent component could be extremely important.
I really can't think of an example scenario. Can you give me one?
Actually, the most simple example of this would probably be testing form components, they require the EditContext to be cascaded, that could be done by manually adding an EditContext as a cascading value, but an EditForm could just as easily be added. A more advanced scenario might be a grid of where its made up of lots of smaller components.
I'm not saying this feature would be extensively used, but I think it's useful for more advanced scenarios where testing component combinations matter. But then again, maybe I've missed the point of this feature :).
Yes, we would use a cascading value to pass down an EditContext.
And that's the thing. At the moment there is no way for a component to know anything about its parent unless it is passed down via a cascading value. This means we never have to create a parent component in this way, we can create one in code pass it down as a cascading value.
Can you elaborate on your grid scenario? What exactly would the test be?
OK folks, let me set the stage a little more precisely.
But first, this is not about rendering the component under test. Rendering a component with various parameters seems to work quite well with the "component parameter builder" or the "parameter factory methods" (examples here.
This is about registering prerequisites for tests. The three prerequisites that a component can have in Blazor seems to be services, parent components, and cascading values.
E.g. consider these two scenarios:
<UserInfo>
, which uses an <AuthorizeView>
component. <AuthorizeView>
requires a bunch of auth related services to be registered in the Services
collection, and also a Task<AuthenticationState>
cascading value. That value can be provided by the <CascadingAuthenticationState>
component, as long as it is a parent of the custom <UserInfo>
component. If the user has multiple tests in the same test file for <UserInfo>
, they would need to have code like this in every test: ctx.RenderComponent<CascadingAuthenticationState>(parameters => parameters.AddChildContent<UserInfo>())
, which is unnecessary noise. Users should be able to say ctx.RenderComponent<UserInfo>(parameters => parameters ...)
.
Instead, I want to allow them to setup/register prerequisites for tests, that are not central to the test, e.g. say all components render in this test should be rendered inside the <CascadingAuthenticationState>
. It's the same concept as with services, that are added to the Services
collection, and are available to the components being rendered.
ctx.AddVendorNameTestHelpers()
in their library. This method registers any component library specific services and parent components required by the components in the component library. Then it becomes much easier for users to test their own components that use the 3rd party vendors component library.Hope it makes sense.
Surely this could be achieved by setting the expected cascading value, couldn't it?
Surely this could be achieved by setting the expected cascading value, couldn't it?
Yes, but that is currently only supported through parameters to the RenderComponent
method. I would like something separate to make the two scenarios described above possible.
So you only need t add ctx.AddCascadingValue
(if it doesn't already exist) and not a method for adding parent components, right?
So you only need t add
ctx.AddCascadingValue
(if it doesn't already exist) and not a method for adding parent components, right?
No, I do not think it is enough. Sometimes it is easier or even necessary to add a parent component.
Thus, I am thinking a ctx.RootRenderTree
property, with relevant Add
methods, is a good solution. It mirrors the ctx.Services
property with it's Add
methods.
I agree
It hit me today that this conceptually is very similar to @layout
in components, so that might actually be a good inspiration to the naming of things, e.g. ctx.AddLayout<TComponent>(component params)
.
Some test scenarios requires one or more non-test-specific parent components or cascading values to make the test possible.
An example would be stuff that would normally be placed in the
App.razor
file, e.g.<CascadingAuthenticationState>
or<TelerikRootComponent>
. These are not unique to the individual test, but they to be there to make components work correctly, i.e. they are sort of a cross-cutting concern.This would also allow component vendors and test doubles to register these as needs, similar to services. E.g. for the #151, we could have a single
ctx.AddTestAuthorization()
method call, that adds both services and parent component in one method call.It is also useful in razor based tests, since it would remove the added noise from the non-test-specific components in the razor code.
My idea is to have something similar to
Services
onITestContext
, such that this is possible:Render tree order rules:
Should there be a way to override the implicit orderinger?
CascadingValues methods:
Add(object value)
Add(string name, object? value)
object? this[string name] // indexer for named values?
object? this[int index]
int Count
GetEnumerator()
ParentComponents methods:
Add<TComponent>(params ComponentParameters[] parameters)
Add<TComponent>(Action<ComponentParameterBuilder<TComponent>> parameterBuilder)
IComponent this[int index]
int Count
GetEnumerator()
Edge cases:
RenderComponent
call has to return the nested instance, not the parent.