import { Component, input, Type } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { MatDialogRef } from '@angular/material/dialog'; import { TranslateModule } from '@ngx-translate/core'; import { DialogShellComponent } from './dialog-shell.component'; import { DialogFooterDirective, DialogFooterStartDirective, DialogHeaderEndDirective, } from './dialog-shell.directives'; @Component({ imports: [ DialogShellComponent, DialogFooterDirective, DialogFooterStartDirective, DialogHeaderEndDirective, ], // The slot wrappers sit at the top level of the content: a control-flow // block there would be projected into the body, so the conditional // action lives inside the footer wrapper. template: ` macOS

Body text

@if (withRemove()) { }
`, }) class FullHostComponent { readonly showClose = input(true); readonly withRemove = input(true); } @Component({ imports: [DialogShellComponent], template: `

Body only

`, }) class BareHostComponent {} describe('DialogShellComponent', () => { const close = jest.fn(); async function render( host: Type, inputs: Partial> = {} ) { await TestBed.configureTestingModule({ imports: [host, TranslateModule.forRoot()], providers: [{ provide: MatDialogRef, useValue: { close } }], }).compileComponents(); const fixture = TestBed.createComponent(host); for (const [name, value] of Object.entries(inputs)) { fixture.componentRef.setInput(name, value); } fixture.detectChanges(); return fixture.nativeElement as HTMLElement; } beforeEach(() => close.mockClear()); it('names the dialog by the heading and projects the header, body and footer slots in order, including a conditional action', async () => { const root = await render(FullHostComponent); const title = root.querySelector('h2.app-dialog__title'); expect(title?.textContent?.trim()).toBe('Remove playlist'); expect(title?.id).toMatch(/^mat-mdc-dialog-title-/); expect( root.querySelector('.app-dialog__subtitle')?.textContent?.trim() ).toBe('Pick how'); expect( root.querySelector('.app-dialog__icon mat-icon')?.textContent ).toBe('keyboard'); expect( root.querySelector('.app-dialog__header-end')?.textContent?.trim() ).toBe('macOS'); expect(root.querySelector('.app-dialog__body')?.textContent).toContain( 'Body text' ); const footerTexts = Array.from( root.querySelectorAll('.app-dialog__footer button') ).map((button) => button.textContent?.trim()); expect(footerTexts).toEqual(['Clear', 'Cancel', 'Remove']); }); it('drops a conditional action from the footer without losing the others', async () => { const root = await render(FullHostComponent, { withRemove: false }); const footerTexts = Array.from( root.querySelectorAll('.app-dialog__footer button') ).map((button) => button.textContent?.trim()); expect(footerTexts).toEqual(['Clear', 'Cancel']); }); it('renders no footer, icon tile or subtitle when nothing is projected or set', async () => { const root = await render(BareHostComponent); expect(root.querySelector('.app-dialog__footer')).toBeNull(); expect(root.querySelector('.app-dialog__icon')).toBeNull(); expect(root.querySelector('.app-dialog__subtitle')).toBeNull(); expect(root.querySelector('.app-dialog__body')?.textContent).toContain( 'Body only' ); }); it('closes through the header button, which comes last in the DOM so autofocus lands in the body', async () => { const root = await render(FullHostComponent); const buttons = Array.from(root.querySelectorAll('button')); const closeButton = buttons.at(-1) as HTMLButtonElement; expect(closeButton.classList).toContain('app-dialog__close'); expect(closeButton.getAttribute('aria-label')).toBe('CLOSE_DIALOG'); closeButton.click(); expect(close).toHaveBeenCalledTimes(1); }); it('hides the close button on request', async () => { const root = await render(FullHostComponent, { showClose: false }); expect(root.querySelector('.app-dialog__close')).toBeNull(); }); });