Angular组件测试:NgbDatepicker通过但报Popper相关split错误
Angular ng-bootstrap日期选择器测试报错解决
问题描述
开发了一个包含ng-bootstrap日期选择器的Angular独立组件,点击按钮切换日期选择器弹窗,选择日期后触发事件输出。组件代码如下:
组件模板
<button type="button" class="btn btn-outline-secondary" aria-label="Select Week Ending Date" id="week-ending-btn" (click)="picker.toggle()" > <i class="bi bi-calendar3"></i> Select Week Ending </button> <input #picker="ngbDatepicker" type="hidden" readonly ngbDatepicker navigation="arrows" positionTarget="#week-ending-btn" [(ngModel)]="weekEndingModel" (dateSelect)="onDateSelection($event, true)" />
组件类
@Component({ selector: 'app-week-ending-selector', standalone: true, imports: [FormsModule, NgbDatepickerModule], templateUrl: './week-ending-selector.component.html', }) export class WeekEndingSelectorComponent implements OnInit { private readonly dateService = inject(DateService); @Input() public date?: string; @Output() public readonly dateChanged = new EventEmitter<IWeekEndingSelectorChangeEvent>(); public weekEndingModel!: NgbDateStruct; public ngOnInit(): void { this.weekEndingModel = this.dateService.dateToNgbDate(this.dateService.getCurrentWeekEndingDate()); this.onDateSelection(this.weekEndingModel, false); } public onDateSelection(date: NgbDateStruct, shouldEmitEvent: boolean): void { const dateAsIso = this.dateService.ngbDateToIso(date); const weekEnding = this.dateService.getWeekEndingDate(dateAsIso); this.weekEndingFriendly = this.dateService.formatDateString(weekEnding, 'friendlyShort'); if (shouldEmitEvent) { this.dateChanged.emit({ friendly: this.weekEndingFriendly, route: this.dateService.formatDateString(weekEnding, 'isoShort'), year: weekEnding.getFullYear(), date: weekEnding, }); } } }
测试代码
it('should emit the value for the selected date', () => { spyOn(component.dateChanged, 'emit'); const thisWeek = getCurrentWeekEndingDate(); //a test helper function component.date = getCurrentWeekEndingDate(true); fixture.detectChanges(); //triggers ngOnInit //Click to open the date picker fixture.debugElement.query(By.css('#week-ending-btn')).click(); fixture.detectChanges(); //This will click on the last day of the first week in the calendar grid const day = fixture.debugElement.query(By.css('ngb-datepicker .ngb-dp-day:nth-child(7)')); day.click(); fixture.detectChanges(); //A bit of a hack, but we can force-parse the date from a string on an attribute const dateStr = day.getAttribute('aria-label')?.toString() ?? ''; const expectedDate = new Date(dateStr); expectedDate.setHours(23); expectedDate.setMinutes(59); expectedDate.setSeconds(59); expectedDate.setMilliseconds(999); expect(component.dateChanged.emit).toHaveBeenCalledOnceWith({ friendly: formatDateAsFriendly(expectedDate), route: formatDateAsIso(expectedDate), year: expectedDate.getFullYear(), date: expectedDate, }); });
测试执行通过,但控制台抛出错误:
'ERROR', TypeError: Cannot read properties of undefined (reading 'split') TypeError: Cannot read properties of undefined (reading 'split') at getPopperOptions (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@ng-bootstrap/ng-bootstrap/fesm2022/ng-bootstrap.mjs:3734:21) at Object.createPopper (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@ng-bootstrap/ng-bootstrap/fesm2022/ng-bootstrap.mjs:3815:57) at apply (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@ng-bootstrap/ng-bootstrap/fesm2022/ng-bootstrap.mjs:4118:39) at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:369:28) at ProxyZoneSpec.onInvoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone-testing.js:2081:39) at _ZoneDelegate.invoke (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:368:34) at ZoneImpl.run (http://localhost:9876/_karma_webpack_/webpack:/node_modules/zone.js/fesm2015/zone.js:111:43) at NgZone.runOutsideAngular (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@angular/core/fesm2022/core.mjs:7142:28) at NgbInputDatepicker.open (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@ng-bootstrap/ng-bootstrap/fesm2022/ng-bootstrap.mjs:4116:26) at NgbInputDatepicker.toggle (http://localhost:9876/_karma_webpack_/webpack:/node_modules/@ng-bootstrap/ng-bootstrap/fesm2022/ng-bootstrap.mjs
问题原因
这个错误是因为ng-bootstrap日期选择器在测试环境中调用Popper.js进行弹窗定位时,无法正确获取positionTarget指定的#week-ending-btn元素的位置信息,导致内部处理位置属性时触发undefined.split()报错。测试环境的DOM异步渲染特性,可能导致点击按钮时目标元素的位置数据尚未完全初始化。
解决方法
方案1:跳过弹窗渲染,直接测试核心逻辑
不需要实际打开弹窗,直接调用组件的onDateSelection方法传入模拟日期结构,绕过弹窗定位逻辑:
it('should emit the value for the selected date', () => { spyOn(component.dateChanged, 'emit'); // 构造模拟的NgbDateStruct对象 const testDate: NgbDateStruct = { year: 2024, month: 5, day: 31 }; // 直接调用日期选择处理方法 component.onDateSelection(testDate, true); const expectedDate = new Date(2024, 4, 31); // JavaScript中月份为0-based expectedDate.setHours(23, 59, 59, 999); expect(component.dateChanged.emit).toHaveBeenCalledOnceWith({ friendly: formatDateAsFriendly(expectedDate), route: formatDateAsIso(expectedDate), year: expectedDate.getFullYear(), date: expectedDate, }); });
方案2:等待DOM渲染稳定后操作
在测试中加入await fixture.whenStable(),等待Angular异步渲染完成,确保元素位置信息初始化:
it('should emit the value for the selected date', async () => { spyOn(component.dateChanged, 'emit'); const thisWeek = getCurrentWeekEndingDate(); component.date = getCurrentWeekEndingDate(true); fixture.detectChanges(); await fixture.whenStable(); // 等待组件初始化完成 // 点击按钮打开弹窗 fixture.debugElement.query(By.css('#week-ending-btn')).click(); fixture.detectChanges(); await fixture.whenStable(); // 等待弹窗渲染完成 const day = fixture.debugElement.query(By.css('ngb-datepicker .ngb-dp-day:nth-child(7)')); day.click(); fixture.detectChanges(); const dateStr = day.getAttribute('aria-label')?.toString() ?? ''; const expectedDate = new Date(dateStr); expectedDate.setHours(23, 59, 59, 999); expect(component.dateChanged.emit).toHaveBeenCalledOnceWith({ friendly: formatDateAsFriendly(expectedDate), route: formatDateAsIso(expectedDate), year: expectedDate.getFullYear(), date: expectedDate, }); });
方案3:Mock Popper定位逻辑
通过spyOn禁用日期选择器的真实弹窗打开逻辑,避免执行Popper定位代码:
import { NgbInputDatepicker } from '@ng-bootstrap/ng-bootstrap'; // 在测试的beforeEach中添加mock逻辑 beforeEach(async () => { await TestBed.configureTestingModule({ imports: [WeekEndingSelectorComponent] }).compileComponents(); fixture = TestBed.createComponent(WeekEndingSelectorComponent); component = fixture.componentInstance; // 获取模板中的picker实例 const pickerDebugEl = fixture.debugElement.query(By.css('input[ngbDatepicker]')); const pickerInstance = pickerDebugEl.componentInstance as NgbInputDatepicker; // Mock open方法,跳过真实的Popper初始化 spyOn(pickerInstance, 'open').and.callFake(() => { pickerInstance.isOpen = true; fixture.detectChanges(); }); }); it('should emit the value for the selected date', () => { spyOn(component.dateChanged, 'emit'); component.date = getCurrentWeekEndingDate(true); fixture.detectChanges(); fixture.debugElement.query(By.css('#week-ending-btn')).click(); fixture.detectChanges(); const day = fixture.debugElement.query(By.css('ngb-datepicker .ngb-dp-day:nth-child(7)')); day.click(); fixture.detectChanges(); const dateStr = day.getAttribute('aria-label')?.toString() ?? ''; const expectedDate = new Date(dateStr); expectedDate.setHours(23, 59, 59, 999); expect(component.dateChanged.emit).toHaveBeenCalledOnceWith({ friendly: formatDateAsFriendly(expectedDate), route: formatDateAsIso(expectedDate), year: expectedDate.getFullYear(), date: expectedDate, }); });
内容的提问来源于stack exchange,提问作者Chris Barr
相关产品推荐
相关产品推荐

