-
Notifications
You must be signed in to change notification settings - Fork 3.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: possibility to override global services in Angular component tests #24394
fix: possibility to override global services in Angular component tests #24394
Conversation
Thanks for taking the time to open a PR!
|
@yjaaidi: this would be a fix for the providers issue we talked about @astone123, @jordanpowell88: imho, this would make a much better developer experience without any workarounds |
Thank you so much for the PR @rainerhahnekamp! This was work I was just getting ready to start! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent job with this Rainer!
I've changed the base-branch to target release/11.0.0 since this will be a breaking change. We plan on a v11 release next so this will be released soon once we get this merged up. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tested and it works great! Thanks @rainerhahnekamp for the contribution.
Changes look good, sorry for juggling your branch between develop and 11.0.0 I need to get 11.0.0 updated so that switching base branches doesn't bring along a bunch of other commits.
@@ -343,6 +345,35 @@ describe("angular mount", () => { | |||
cy.get('img').should('be.visible').and('have.prop', 'naturalWidth').should('be.greaterThan', 0) | |||
}) | |||
|
|||
it('should not override transient service', () => { | |||
cy.mount(TransientServicesComponent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't know you could do this (rely on @Injectable({ providedIn: 'root' })
to inject into the TestModule). Really cool!
getTestBed().overrideComponent(componentFixture, { | ||
add: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know the difference between add
and set
here? Hard to find documentation on this and I'm curious
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, so with add
, it is kind of a merge between the existing providers and the ones that you add in the test. With set
, you replace them.
That's why you need to use set
when you want to be able to remove all component providers (as the last test does)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking more on this, what is the behavior when specifying componentProviders
when using the
cy.mount('<some-component />', { componentProviders: [SomeProvider], declarations: [SomeComponent] })
form? The providers would be attached to the WrapperComponent in this case, would SomeComponent
inherit the providers?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It will inherit it, but if SomeComponent
has its own providers, they will not be replaced. With a wrapper component it doesn't make any difference, if I use providers
or componentProviders
.
Theoretically, we could spend some effort trying to figure out that SomeComponent
is embedded, but this is not an ideal solution. Just think of the following case:
cy.mount('<div><component-a></component-a><component-b></component-b></div>', {componentProviders: [SomeProvider]});
Now we have two components. To which one should we assign the SomeProvider
?
The question is if it actually makes sense to provide componentProviders
for a WrapperComponent. It might lead to further confusion.
Let me check, how other libraries deal with this situation and I'll come back to you.
What are your thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need to run through some test-cases and map out how the providers at any given level are able to be overridden. It's been a while since I've worked closely with Angular, but I'm starting to think we could cut componentProviders
from the scope of this PR and move forward with the providers
switch to be module-level. There might be a use-case for it in the future but I don't see the benefits of componentProviders
clearly right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In Angular testing, you have two types of how to override providers.
The first is the providers of the TestBed.configureTestingModule
, and the second one is the one in TestBed.overrideComponent
. You need both.
With componentProviders
you can override the providers inside the @Component({providers: ...})
. providers
of the TetsingModule will not reach them.
In my test, you see the difference between these two.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A typical use case for componentProviders
is the @ngrx/component-store. This is a service, you usually provide inside the @Component({})
: https://ngrx.io/guide/component-store/initialization#lazy-initialization
Thanks @rainerhahnekamp for putting this together. TL;DR: let's use This is in fact an unavoidable breaking change that would make less impact if added sooner than later. The only thing though that I'd love to discuss here is the e.g. if a component depends on A & B and we just want to override A, we'd have to override B too... then when the component would add C, it would break test and we'd have to provide C too. BTW, I'm advocating for providing a nicer TestBed without What do you think? |
TL;DR: Also, I just realized that we have to change the signature of As mentioned by @rainerhahnekamp, we don't need |
Hi @yjaaidi, I think in most cases you definitely want to use I think we have two options: In the PR I went for |
@ZachJW34, I will remove the |
Hey @rainerhahnekamp, actually angular testing library is overriding existing providers using As we agree that I know that even the Angular core team is not very happy with the 👍 My favorite approach: I think that the best thing is to figure out a mix of your two suggestions: // default behavior is `add`
// this is more type-safe than MetadataOverride as add, set and remove are mutually exclusive
componentProviders: Provider[] | {set: Provider[]} | {remove: Provider[]};
// e.g.
componentProviders: [FakeStore],
componentProviders: {set: []}; // remove all providers
componentProviders: {remove: [FakeStore]} // remove FakeStore provider only 🤔 Meh approach: Then, there is the functional approach which I find interesting because it's the one providing the most flexibility but it's bit overengineered and harder to maintain as it gets coupled to the way we fetch component's metadata. componentProviders: Provider[] | (providers: Provider[]) => Provider[];
componentProviders: (providers) => [...providers.filter(provider => provider !== providerToRemove), myProvider] what do you think? |
@yjaaidi thanks for correcting me about the testing library. Didn't read the code carefully enough. The Let's see what our friends from Cypress think about it. |
Actually, we might also need The signature would become: interface MountOptions {
componentProviders: Provider[] | {set: Provider[]} | {add?: Provider[], remove?: Provider[]};
} and allow the following: mount(..., {componentProviders: {
add: [Y, Z],
remove: [X]
}}) where the implementation would something like this: if ('remove' in componentProviders) {
TestBed.overrideComponent({providers: {remove: ...}})
}
if ('add' in componentProviders) {
TestBed.overrideComponent({providers: {add: ...}})
} This might sound a bit far fetched as I can't figure out any decent test doing this but it's the most flexible approach offering all what you can do with |
npm/angular/src/mount.ts
Outdated
@@ -301,6 +329,7 @@ function setupComponent<T extends { ngOnChanges? (changes: SimpleChanges): void | |||
* }) | |||
* @returns Cypress.Chainable<MountResponse<T>> | |||
*/ | |||
// eslint-disable-next-line no-redeclare |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@yjaaidi , @jordanpowell88, @ZachJW34, I need your help, please.
I used function overloading to exclude componentProviders
when a template is used. Now the original JSDoc description for the mount
function doesn't show up in the IDE anymore. Any suggestions on how to fix that?
Thanks for all the conversation around this @rainerhahnekamp @yjaaidi, learning a lot here! Before we get into the weeds about the Before we move forward with making
@jordanpowell88 and I paired for a bit and started working through a solution that addresses this that I'd like to get feedback on. What I realized is that If we changed the overrideComponent(ComponentType, { set/add: providers }) This would look roughly like: cy.mount('<app-root></app-root>', {
componentProviders: [{
component: AppComponent,
providers: [{ provide: MyService, useValue: { message: 'Hello World' } }],
}],
declarations: [AppComponent],
}); With this API, we could have an equivalency between the template and the component form. We could tweak the Thoughts? |
I think we have to be a little bit careful here, not to overdo things. Most of the component tests will not need the If we start now to tweak the It might be better to expose the TestBed for the edge cases and give the developers full control over it. Otherwise, you would have to write wrapper code for the complete API, which has to be maintained, etc. If the TestBed would be available, we could even let go of the That would be my thoughts... |
@rainerhahnekamp that's a fair point. The |
I don't think standalone components will have any impact that might be relevant for this PR. To cut it short, if we can all agree, that If we can all agree on that, I would push the changes, and we are done. Getting access to inject (see #24429) should probably be discussed in another PR. Looking forward to @yjaaidi and @jordanpowell88 responses... |
I think reverting |
Totally agreed with @jordanpowell88 the main goal here is to make Then let's open an issue to think of a low level way of overriding more specific component providers. Now, the problem is that this will be a breaking change where we don't provide any way of overriding providers at the component level. It might surprise some users. We have to figure out a homogeneous solution for this issue and these:
There are a couple of related questions that we have to think of:
Maybe a hook approach can solve this.
|
- run `eslint --fix` on `mount.cy.ts` - add tests that use `TestBed.inject`
In my humble opinion, I see two steps here. The first step is to make sure that Cypress 11 covers all testing use cases for Angular. In v10, this is not the case. We do that via changing the The second step improves the developer experience. It would include all the issues, referenced by Younes. Unlike the first step, we don’t have here any time pressure. Step 1 is a breaking change and should be merged as quickly as possible. I think step 2 is only about additional features, which we can add in any minor version. I understand that the removal of I have added two tests that showcase the usage of Cheers! |
💯 agreed with @rainerhahnekamp Yup! Inject must be chained and executed only after the test module is set up. That's why I opened this issue #24429 in order to figure out an easier way of handling this |
Also, @rainerhahnekamp @jordanpowell88 @ZachJW34, we can release a new version of @jscutlery/cypress-angular which could be a wrapper of @cypress/angular in order to smoothen the DX. |
Looks like this PR is good to go, I'll need to get a build triggered for this PR (looks like there is some permission issues in CircleCI that are preventing outside contributor builds) and then we can get this merged. Thanks for all the feedback and creating the issues @rainerhahnekamp @yjaaidi! Using |
User facing changelog
Introduces
componentProviders
for Angular component testing which overrides a component's providers. Changes the existingproviders
so that they work exactly as the ones fromTestModuleMetadata::providers
.Additional details
This PR introduces a new property
componentProviders
and is a breaking change.componentProviders
overrides the providers of the component via theset
attributes.MountConfig::providers
changes its behavior. It provides and overrides now services in the global scope. So it acts exactly in the same way, as native Angular Testing, Spectator, and Testing Library (Angular).The name
MountConfig:componentProviders
was used in order to have a similar behavior as the existing testing libraries for Angular (Spectator, Testing Library).Furthermore, I would argue that it should use the
set
property inTestBed.overrideComponent
and not - as is currently the case - theadd
property. Again, this is to have full backwards-compatibility with the existing testing tools in Angular.References:
https://github.com/ngneat/spectator/blob/master/projects/spectator/src/lib/spectator/create-factory.ts#L47
https://github.com/testing-library/angular-testing-library/blob/main/projects/testing-library/src/lib/testing-library.ts#L276
I would like to point out that the current behavior of MountConfig::providers wrongly implies that it is TestingModuleMetadata::providers and is, therefore, really confusing.
Steps to test
Run the system tests for angular component testing
How has the user experience changed?
No visual changes. Overriding services works as we know it from native Angular testing.
PR Tasks
cypress-documentation
?type definitions
?