diff --git a/apps/example-app/src/app/examples/08-directive.spec.ts b/apps/example-app/src/app/examples/08-directive.spec.ts index c36822a3..5df9413a 100644 --- a/apps/example-app/src/app/examples/08-directive.spec.ts +++ b/apps/example-app/src/app/examples/08-directive.spec.ts @@ -3,8 +3,8 @@ import { render, screen, fireEvent } from '@testing-library/angular'; import { SpoilerDirective } from './08-directive'; test('it is possible to test directives', async () => { - await render(SpoilerDirective, { - template: '
', + await render('
', { + declarations: [SpoilerDirective], }); const directive = screen.getByTestId('dir'); @@ -25,8 +25,8 @@ test('it is possible to test directives with props', async () => { const hidden = 'SPOILER ALERT'; const visible = 'There is nothing to see here ...'; - await render(SpoilerDirective, { - template: '
', + await render('
', { + declarations: [SpoilerDirective], componentProperties: { hidden, visible, @@ -49,8 +49,8 @@ test('it is possible to test directives with props in template', async () => { const hidden = 'SPOILER ALERT'; const visible = 'There is nothing to see here ...'; - await render(SpoilerDirective, { - template: ``, + await render(``, { + declarations: [SpoilerDirective], }); expect(screen.queryByText(visible)).not.toBeInTheDocument(); diff --git a/projects/testing-library/src/lib/models.ts b/projects/testing-library/src/lib/models.ts index 6248ef92..b1984477 100644 --- a/projects/testing-library/src/lib/models.ts +++ b/projects/testing-library/src/lib/models.ts @@ -250,6 +250,9 @@ export interface RenderComponentOptions extends RenderComponentOptions { @@ -262,6 +265,8 @@ export interface RenderDirectiveOptions` * }) + * + * @deprecated Use `render(template, { declarations: [SomeDirective] })` instead. */ template: string; /** @@ -282,6 +287,27 @@ export interface RenderDirectiveOptions; } +// eslint-disable-next-line @typescript-eslint/ban-types +export interface RenderTemplateOptions + extends RenderComponentOptions { + /** + * @description + * An Angular component to wrap the component in. + * The template will be overridden with the `template` option. + * + * @default + * `WrapperComponent`, an empty component that strips the `ng-version` attribute + * + * @example + * const component = await render(SpoilerDirective, { + * template: `
` + * wrapper: CustomWrapperComponent + * }) + */ + wrapper?: Type; + componentProperties?: Partial; +} + export interface Config extends Pick, 'excludeComponentDeclaration'> { /** * DOM Testing Library config diff --git a/projects/testing-library/src/lib/testing-library.ts b/projects/testing-library/src/lib/testing-library.ts index 13f96f06..fe3a22ca 100644 --- a/projects/testing-library/src/lib/testing-library.ts +++ b/projects/testing-library/src/lib/testing-library.ts @@ -22,7 +22,7 @@ import { waitForOptions as dtlWaitForOptions, configure as dtlConfigure, } from '@testing-library/dom'; -import { RenderComponentOptions, RenderDirectiveOptions, RenderResult } from './models'; +import { RenderComponentOptions, RenderDirectiveOptions, RenderTemplateOptions, RenderResult } from './models'; import { getConfig } from './config'; const mountedFixtures = new Set>(); @@ -32,14 +32,24 @@ export async function render( component: Type, renderOptions?: RenderComponentOptions, ): Promise>; +/** + * @deprecated Use `render(template, { declarations: [DirectiveType] })` instead. + */ export async function render( component: Type, renderOptions?: RenderDirectiveOptions, ): Promise>; +export async function render( + template: string, + renderOptions?: RenderTemplateOptions, +): Promise>; export async function render( - sut: Type, - renderOptions: RenderComponentOptions | RenderDirectiveOptions = {}, + sut: Type | string, + renderOptions: + | RenderComponentOptions + | RenderDirectiveOptions + | RenderTemplateOptions = {}, ): Promise> { const { dom: domConfig, ...globalConfig } = getConfig(); const { @@ -69,7 +79,12 @@ export async function render( }); TestBed.configureTestingModule({ - declarations: addAutoDeclarations(sut, { declarations, excludeComponentDeclaration, template, wrapper }), + declarations: addAutoDeclarations(sut, { + declarations, + excludeComponentDeclaration, + template, + wrapper, + }), imports: addAutoImports({ imports: imports.concat(defaultImports), routes, @@ -176,7 +191,7 @@ export async function render( detectChanges, navigate, rerender, - debugElement: fixture.debugElement.query(By.directive(sut)), + debugElement: typeof sut === 'string' ? fixture.debugElement : fixture.debugElement.query(By.directive(sut)), container: fixture.nativeElement, debug: (element = fixture.nativeElement, maxLength, options) => Array.isArray(element) @@ -193,14 +208,18 @@ async function createComponent(component: Type): Promise( - component: Type, + sut: Type | string, { template, wrapper }: Pick, 'template' | 'wrapper'>, ): Promise> { + if (typeof sut === 'string') { + TestBed.overrideTemplate(wrapper, sut); + return createComponent(wrapper); + } if (template) { TestBed.overrideTemplate(wrapper, template); return createComponent(wrapper); } - return createComponent(component); + return createComponent(sut); } function setComponentProperties( @@ -248,7 +267,7 @@ function getChangesObj(oldProps: Partial | null, newProps: Par } function addAutoDeclarations( - component: Type, + sut: Type | string, { declarations, excludeComponentDeclaration, @@ -256,9 +275,13 @@ function addAutoDeclarations( wrapper, }: Pick, 'declarations' | 'excludeComponentDeclaration' | 'template' | 'wrapper'>, ) { + if (typeof sut === 'string') { + return [...declarations, wrapper]; + } + const wrappers = () => (template ? [wrapper] : []); - const components = () => (excludeComponentDeclaration ? [] : [component]); + const components = () => (excludeComponentDeclaration ? [] : [sut]); return [...declarations, ...wrappers(), ...components()]; } diff --git a/projects/testing-library/tests/directive.spec.ts b/projects/testing-library/tests/render-template.spec.ts similarity index 61% rename from projects/testing-library/tests/directive.spec.ts rename to projects/testing-library/tests/render-template.spec.ts index 08db0eb3..1bc4e30d 100644 --- a/projects/testing-library/tests/directive.spec.ts +++ b/projects/testing-library/tests/render-template.spec.ts @@ -1,8 +1,11 @@ +/* eslint-disable testing-library/no-container */ +/* eslint-disable testing-library/render-result-naming-convention */ import { Directive, HostListener, ElementRef, Input, Output, EventEmitter, Component } from '@angular/core'; import { render, fireEvent } from '../src/public_api'; @Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector selector: '[onOff]', }) export class OnOffDirective { @@ -21,6 +24,7 @@ export class OnOffDirective { } @Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector selector: '[update]', }) export class UpdateInputDirective { @@ -32,27 +36,53 @@ export class UpdateInputDirective { constructor(private el: ElementRef) {} } +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'greeting', + template: 'Hello {{ name }}!', +}) +export class GreetingComponent { + @Input() name = 'World'; +} + test('the directive renders', async () => { - const component = await render(OnOffDirective, { - template: '
', + const component = await render('
', { + declarations: [OnOffDirective], }); expect(component.container.querySelector('[onoff]')).toBeInTheDocument(); }); -test('uses the default props', async () => { +test('the component renders', async () => { + const component = await render('', { + declarations: [GreetingComponent], + }); + + expect(component.container.querySelector('greeting')).toBeInTheDocument(); + expect(component.getByText('Hello Angular!')); +}); + +test('the directive renders (compatibility with the deprecated signature)', async () => { const component = await render(OnOffDirective, { template: '
', }); + expect(component.container.querySelector('[onoff]')).toBeInTheDocument(); +}); + +test.only('uses the default props', async () => { + const component = await render('
', { + declarations: [OnOffDirective], + }); + fireEvent.click(component.getByText('init')); fireEvent.click(component.getByText('on')); fireEvent.click(component.getByText('off')); }); test('overrides input properties', async () => { - const component = await render(OnOffDirective, { - template: '
', + const component = await render('
', { + declarations: [OnOffDirective], }); fireEvent.click(component.getByText('init')); @@ -62,8 +92,8 @@ test('overrides input properties', async () => { test('overrides input properties via a wrapper', async () => { // `bar` will be set as a property on the wrapper component, the property will be used to pass to the directive - const component = await render(OnOffDirective, { - template: '
', + const component = await render('
', { + declarations: [OnOffDirective], componentProperties: { bar: 'hello', }, @@ -77,8 +107,8 @@ test('overrides input properties via a wrapper', async () => { test('overrides output properties', async () => { const clicked = jest.fn(); - const component = await render(OnOffDirective, { - template: '
', + const component = await render('
', { + declarations: [OnOffDirective], componentProperties: { clicked, }, @@ -93,8 +123,8 @@ test('overrides output properties', async () => { describe('removeAngularAttributes', () => { test('should remove angular attributes', async () => { - await render(OnOffDirective, { - template: '
', + await render('
', { + declarations: [OnOffDirective], removeAngularAttributes: true, }); @@ -103,8 +133,8 @@ describe('removeAngularAttributes', () => { }); test('is disabled by default', async () => { - await render(OnOffDirective, { - template: '
', + await render('
', { + declarations: [OnOffDirective], }); expect(document.querySelector('[ng-version]')).not.toBeNull(); @@ -113,8 +143,8 @@ describe('removeAngularAttributes', () => { }); test('updates properties and invokes change detection', async () => { - const component = await render(UpdateInputDirective, { - template: '
', + const component = await render('
', { + declarations: [UpdateInputDirective], componentProperties: { value: 'value1', },