Skip to content

feat: support componentImports option for standalone components #307

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions projects/testing-library/src/lib/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ export interface RenderComponentOptions<ComponentType, Q extends Queries = typeo
* })
*/
componentProviders?: any[];
/**
* @description
* A collection of imports to override a standalone component's imports with.
*
* @default
* undefined
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent with the other default properties, I usually use an empty array.

Suggested change
* undefined
* []

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I responded to this comment below.

*
* @example
* const component = await render(AppComponent, {
* ɵcomponentImports: [
* MockChildComponent
* ]
* })
*
* @experimental
*/
ɵcomponentImports?: (Type<any> | any[])[];
/**
* @description
* Queries to bind. Overrides the default set from DOM Testing Library unless merged.
Expand Down
42 changes: 28 additions & 14 deletions projects/testing-library/src/lib/testing-library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export async function render<SutType, WrapperType = SutType>(
wrapper = WrapperComponent as Type<WrapperType>,
componentProperties = {},
componentProviders = [],
ɵcomponentImports: componentImports,
excludeComponentDeclaration = false,
routes = [],
removeAngularAttributes = false,
Expand Down Expand Up @@ -83,6 +84,7 @@ export async function render<SutType, WrapperType = SutType>(
providers: [...providers],
schemas: [...schemas],
});
overrideComponentImports(sut, componentImports);

await TestBed.compileComponents();

Expand Down Expand Up @@ -128,23 +130,23 @@ export async function render<SutType, WrapperType = SutType>(
const [path, params] = (basePath + href).split('?');
const queryParams = params
? params.split('&').reduce((qp, q) => {
const [key, value] = q.split('=');
const currentValue = qp[key];
if (typeof currentValue === 'undefined') {
qp[key] = value;
} else if (Array.isArray(currentValue)) {
qp[key] = [...currentValue, value];
} else {
qp[key] = [currentValue, value];
}
return qp;
}, {} as Record<string, string | string[]>)
const [key, value] = q.split('=');
const currentValue = qp[key];
if (typeof currentValue === 'undefined') {
qp[key] = value;
} else if (Array.isArray(currentValue)) {
qp[key] = [...currentValue, value];
} else {
qp[key] = [currentValue, value];
}
return qp;
}, {} as Record<string, string | string[]>)
: undefined;

const navigateOptions: NavigationExtras | undefined = queryParams
? {
queryParams,
}
queryParams,
}
: undefined;

const doNavigate = () => {
Expand Down Expand Up @@ -264,6 +266,18 @@ function setComponentProperties<SutType>(
return fixture;
}

function overrideComponentImports<SutType>(sut: Type<SutType> | string, imports: (Type<any> | any[])[] | undefined) {
if (imports) {
if (typeof sut === 'function' && ɵisStandalone(sut)) {
TestBed.overrideComponent(sut, { set: { imports } });
} else {
throw new Error(
`Error while rendering ${sut}: Cannot specify componentImports on a template or non-standalone component.`,
);
}
}
}

function hasOnChangesHook<SutType>(componentInstance: SutType): componentInstance is SutType & OnChanges {
return (
'ngOnChanges' in componentInstance && typeof (componentInstance as SutType & OnChanges).ngOnChanges === 'function'
Expand Down Expand Up @@ -397,7 +411,7 @@ if (typeof process === 'undefined' || !process.env?.ATL_SKIP_AUTO_CLEANUP) {
}

@Component({ selector: 'atl-wrapper-component', template: '' })
class WrapperComponent {}
class WrapperComponent { }

/**
* Wrap findBy queries to poke the Angular change detection cycle
Expand Down
49 changes: 47 additions & 2 deletions projects/testing-library/tests/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { render, fireEvent, screen } from '../src/public_api';
<button>button</button>
`,
})
class FixtureComponent {}
class FixtureComponent { }

test('creates queries and events', async () => {
const view = await render(FixtureComponent);
Expand Down Expand Up @@ -48,6 +48,51 @@ describe('standalone', () => {
});
});

describe('standalone with child', () => {
@Component({
selector: 'child-fixture',
template: `<span>A child fixture</span>`,
standalone: true,
})
class ChildFixture { }

@Component({
selector: 'child-fixture',
template: `<span>A mock child fixture</span>`,
standalone: true,
})
class MockChildFixture { }

@Component({
selector: 'parent-fixture',
template: `<h1>Parent fixture</h1>
<div><child-fixture></child-fixture></div> `,
standalone: true,
imports: [ChildFixture],
})
class ParentFixture { }

it('renders the standalone component with child', async () => {
await render(ParentFixture);
expect(screen.getByText('Parent fixture'));
expect(screen.getByText('A child fixture'));
});

it('renders the standalone component with child', async () => {
await render(ParentFixture, { ɵcomponentImports: [MockChildFixture] });
expect(screen.getByText('Parent fixture'));
expect(screen.getByText('A mock child fixture'));
});

it('rejects render of template with componentImports set', () => {
const result = render(`<div><parent-fixture></parent-fixture></div>`, {
imports: [ParentFixture],
ɵcomponentImports: [MockChildFixture],
});
return expect(result).rejects.toMatchObject({ message: /Error while rendering/ });
});
});

describe('removeAngularAttributes', () => {
it('should remove angular attribute', async () => {
await render(FixtureComponent, {
Expand All @@ -72,7 +117,7 @@ describe('animationModule', () => {
@NgModule({
declarations: [FixtureComponent],
})
class FixtureModule {}
class FixtureModule { }
describe('excludeComponentDeclaration', () => {
it('does not throw if component is declared in an imported module', async () => {
await render(FixtureComponent, {
Expand Down