Angular 18独立组件迁移:无法覆盖CdkCopyToClipboard指令
Angular 18独立组件迁移:单元测试中指令Mock失效问题
我正在将Angular 18应用迁移至独立组件架构,在单元测试中碰到了依赖指令Mock覆盖失效的问题。当前技术栈包含Angular 18、Material、NGMocks、Spectator(替代TestBed)以及Karma/Jasmine。
以CdkCopyToClipboard指令为例,我自定义了MockCdkCopyToClipboard指令用于捕获输入值,避免执行真实的复制逻辑,但迁移后原指令依然生效,Mock完全没起作用。被测组件本身直接导入了CdkCopyToClipboard、MatIcon、MatIconButton三个依赖。
我尝试改用TestBed实现,问题依旧。推测Spectator应该支持这类场景,但不清楚指令覆盖的正确配置方式。
被测组件代码
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' ); } }
测试代码
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(); }); });
问题根源与解决方法
问题核心
独立组件的imports数组优先级高于测试中的declarations,直接在测试里声明Mock指令不会覆盖组件自带的依赖,必须显式替换组件内部的导入项。
方法1:用Spectator的overrideComponent替换imports
修改createComponentFactory配置,通过overrideComponent直接替换组件的imports:
const createComponent = createComponentFactory({ component: CopyToClipboardComponent, overrideComponent: { component: CopyToClipboardComponent, override: { imports: [MockCdkCopyToClipboard, MockComponent(MatIcon), MockComponent(MatIconButton)], } } });
方法2:用NGMocks的MockDirective自动替换
利用NGMocks简化Mock创建,无需手动定义Mock类:
import { MockDirective } from 'ng-mocks'; import { CdkCopyToClipboard } from '@angular/cdk/clipboard'; // 移除自定义MockCdkCopyToClipboard类,直接使用MockDirective const createComponent = createComponentFactory({ component: CopyToClipboardComponent, overrideComponent: { component: CopyToClipboardComponent, override: { imports: [MockDirective(CdkCopyToClipboard), MockComponent(MatIcon), MockComponent(MatIconButton)], } } }); // 在测试用例中捕获输入值 it('should bind correct value to clipboard directive', () => { spectator.setInput('textToCopy', 'test'); spectator.detectChanges(); const mockDirective = spectator.query(MockDirective(CdkCopyToClipboard)); expect(mockDirective['cdkCopyToClipboard']).toEqual('test'); });
内容的提问来源于stack exchange,提问作者Martinspire
相关产品推荐
相关产品推荐

