I began migrating my angular 18 application to standalone components and I've run into issues overriding various imports. I'm using Angular 18, Material, NGMocks, Spectator (as testbed replacement) and Karma/Jasmine for running unit tests.
I have an example with overriding CdkCopyToClipboard directive and I simply cannot figure out why it isn't working or what setup the componentfactory needs to make it work.
So the test has a mock directive that instead of copying to clipboard, puts the value in a string. Not the best way, but the functionality isn't really the point of the question, its one of the examples that I failed to migrate. The component itself has a button that we show if there's anything to copy if there's text on the component input.
When moving to standalone components, there are now 3 items imported, 2 are material related, 1 is CDK related and it seems that the cdk one is always being used normally instead of taking the mocked directive.
I tried reverting to TestBed but also ran into issues there and overall I think Spectator should still work but the overriding part is difficult to figure out.
The example is on stackblitz: https://stackblitz.com/edit/stackblitz-starters-um7c3g?file=src%2Fcomponents%2Fcopy-to-clipboard.component.spectator.spec.ts
The component I'm testing:
import { CdkCopyToClipboard } from '@angular/cdk/clipboard';
import { Component, Input, OnChanges } from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
/**
* Show clipboard to copy text to clipboard
*/
@Component({
selector: 'app-copy-to-clipboard',
template: `
@if (showButton) {
<button
class="clipboard-button mat-icon-button-small"
mat-icon-button
type="button"
[cdkCopyToClipboard]="textToCopy"
>
<mat-icon>content_copy</mat-icon>
</button>
}
`,
standalone: true,
imports: [CdkCopyToClipboard, MatIcon, MatIconButton],
})
export class CopyToClipboardComponent implements OnChanges {
@Input() textToCopy!: string;
showButton = false;
ngOnChanges(): void {
this.showButton = this.show();
console.log('showButton', this.showButton);
}
show() {
return (
!!this.textToCopy &&
this.textToCopy !== '-' &&
this.textToCopy?.toLowerCase() !== 'null'
);
}
}
The tests:
import { Directive, Input } from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { Spectator, createComponentFactory } from '@ngneat/spectator';
import { MockComponent } from 'ng-mocks';
import { CopyToClipboardComponent } from './copy-to-clipboard.component';
// simple mock directive to capture the input. We're not going to test the cdk logic of the copy
let clipboardResult = '';
@Directive({ selector: '[cdkCopyToClipboard]', standalone: true })
class MockCdkCopyToClipboard {
// text to copy to clipboard
@Input() set cdkCopyToClipboard(value: string) {
console.log('text copied', value);
clipboardResult = value;
}
}
describe('CopyToClipboardComponent', () => {
let spectator: Spectator<CopyToClipboardComponent>;
const createComponent = createComponentFactory({
component: CopyToClipboardComponent,
declarations: [
MockComponent(MatIcon),
MockComponent(MatIconButton),
MockCdkCopyToClipboard,
],
});
beforeEach(() => {
spectator = createComponent();
clipboardResult = '';
});
it('should create', () => {
expect(spectator.component).toBeTruthy();
});
it('should show the clipboard button when there is text to copy', () => {
spectator.setInput('textToCopy', 'test');
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeTruthy();
expect(clipboardResult).toEqual('test');
});
it('should not show the clipboard button when there is no text to copy', () => {
spectator.setInput('textToCopy', '');
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeFalsy();
expect(clipboardResult).toEqual('');
});
it('should not show the clipboard button when the text to copy is "null"', () => {
spectator.setInput('textToCopy', null as unknown as string);
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeFalsy();
expect(clipboardResult).toEqual('');
});
it('should update the clipboard button when the input changes', () => {
spectator.setInput('textToCopy', 'initial');
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeTruthy();
expect(clipboardResult).toEqual('initial');
spectator.setInput('textToCopy', 'updated');
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeTruthy();
expect(clipboardResult).toEqual('updated');
});
it('should update the clipboard button when the input changes to an empty string', () => {
spectator.setInput('textToCopy', 'initial');
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeTruthy();
expect(clipboardResult).toEqual('initial');
spectator.setInput('textToCopy', '');
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeFalsy();
});
it('should update the clipboard button when the input changes to "null"', () => {
spectator.setInput('textToCopy', 'initial');
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeTruthy();
expect(clipboardResult).toEqual('initial');
spectator.setInput('textToCopy', null as unknown as string);
spectator.detectChanges();
expect(spectator.query('.clipboard-button')).toBeFalsy();
});
});
On the stackblitz I've also added a testbed alternative that also doesn't seem to work...