mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Silence frontend spec warnings
Filter the jquery-migrate banner through Vitest `onConsoleLog` in a new `vitest-base.config.ts` wired in via the builder `runnerConfig`, instead of reassigning the global `console.log`. Console warnings and errors that a spec asserts on are scoped per spec with `vi.spyOn`. Drop the redundant manual `ngAfterViewInit` calls and select items programmatically in the autocompleter spec, and wait on activation with `vi.waitFor` in the edit-form spec instead of fixed microtask ticks.
This commit is contained in:
@@ -141,6 +141,7 @@
|
||||
"options": {
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"buildTarget": "OpenProject:build",
|
||||
"runnerConfig": true,
|
||||
"providersFile": "src/test-providers.ts",
|
||||
"setupFiles": ["src/test-browser-polyfills.ts", "src/test-setup.ts"],
|
||||
"browsers": ["chromium", "firefox", "webkit"],
|
||||
|
||||
+24
-13
@@ -81,6 +81,7 @@ describe('autocompleter', () => {
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as WindowWithOpenProject).OpenProject;
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -127,9 +128,7 @@ describe('autocompleter', () => {
|
||||
it('should load items', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
@@ -182,9 +181,7 @@ describe('autocompleter', () => {
|
||||
it('should display formattedId in dropdown options', () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
@@ -215,11 +212,10 @@ describe('autocompleter', () => {
|
||||
});
|
||||
|
||||
it('should display classic formattedId in selected value label', () => {
|
||||
silenceDestroyedOutputWarning();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
@@ -237,8 +233,7 @@ describe('autocompleter', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// Select the first item (classic mode: #1)
|
||||
const firstOption = document.querySelector<HTMLElement>('.ng-option')!;
|
||||
firstOption.click();
|
||||
select.select(select.itemsList.items[0]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const labelElement = document.querySelector('.ng-value-label');
|
||||
@@ -253,11 +248,10 @@ describe('autocompleter', () => {
|
||||
});
|
||||
|
||||
it('should display semantic formattedId in selected value label', () => {
|
||||
silenceDestroyedOutputWarning();
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.advanceTimersByTime(0);
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
vi.advanceTimersByTime(1000);
|
||||
fixture.detectChanges();
|
||||
const select = fixture.componentInstance.ngSelectInstance;
|
||||
@@ -275,8 +269,7 @@ describe('autocompleter', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
// Select the semantic mode item (PROJ-2)
|
||||
const option = document.querySelector<HTMLElement>('.ng-option')!;
|
||||
option.click();
|
||||
select.select(select.itemsList.items[0]);
|
||||
fixture.detectChanges();
|
||||
|
||||
const labelElement = document.querySelector('.ng-value-label');
|
||||
@@ -299,7 +292,6 @@ describe('autocompleter', () => {
|
||||
|
||||
it('should load items with debounce', async () => {
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.ngAfterViewInit();
|
||||
|
||||
// Wait for ngAfterViewInit's internal setTimeout(25ms) and debounce to fire.
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
@@ -339,6 +331,25 @@ describe('autocompleter', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// NG0953 ("Unexpected emit for destroyed OutputRef") is emitted when ng-select
|
||||
// emits on an OutputRef during fixture teardown under fake timers. It is a real
|
||||
// lifecycle smell, not pure noise — silencing it here is a pragmatic stopgap so
|
||||
// these specs stay readable, not a fix. The proper fix is to stop the
|
||||
// emit-after-destroy in the component teardown path; until then this is
|
||||
// scoped to the two affected specs and passes every other warning through so it
|
||||
// does not hide unrelated regressions. Do not promote this to a global filter.
|
||||
function silenceDestroyedOutputWarning():void {
|
||||
const originalWarn = console.warn.bind(console);
|
||||
|
||||
vi.spyOn(console, 'warn').mockImplementation((message?:unknown, ...args:unknown[]) => {
|
||||
if (typeof message === 'string' && message.startsWith('NG0953: Unexpected emit for destroyed `OutputRef`')) {
|
||||
return;
|
||||
}
|
||||
|
||||
originalWarn(message, ...args);
|
||||
});
|
||||
}
|
||||
|
||||
describe('derived autocompleter', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
|
||||
@@ -30,10 +30,11 @@ import { ApplicationRef, Injector } from '@angular/core';
|
||||
import { EditForm } from 'core-app/shared/components/fields/edit/edit-form/edit-form';
|
||||
import { HalResource } from 'core-app/features/hal/resources/hal-resource';
|
||||
import { EditFieldHandler } from 'core-app/shared/components/fields/edit/editing-portal/edit-field-handler';
|
||||
import { vi } from 'vitest';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
import type { IFieldSchema } from 'core-app/shared/components/fields/field.base';
|
||||
|
||||
class TestEditForm extends EditForm<HalResource> {
|
||||
constructor(injector:Injector, private readonly requireVisibleSpy:(fieldName:string) => Promise<void>, private readonly activateFieldSpy:() => Promise<EditFieldHandler>, private readonly resetSpy:(fieldName:string, focus?:boolean) => void) {
|
||||
constructor(injector:Injector, private readonly requireVisibleSpy:(fieldName:string) => Promise<void>, private readonly resetSpy:(fieldName:string, focus?:boolean) => void) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
@@ -41,8 +42,10 @@ class TestEditForm extends EditForm<HalResource> {
|
||||
return this.requireVisibleSpy(fieldName);
|
||||
}
|
||||
|
||||
public activateField():Promise<EditFieldHandler> {
|
||||
return this.activateFieldSpy();
|
||||
// Concrete implementation required by the abstract base. The specs mock
|
||||
// `activate` directly, so this is never invoked.
|
||||
protected activateField(_form:EditForm, _schema:IFieldSchema, _fieldName:string, _errors:string[]):Promise<EditFieldHandler> {
|
||||
return Promise.resolve({} as EditFieldHandler);
|
||||
}
|
||||
|
||||
public reset(fieldName:string, focus?:boolean):void {
|
||||
@@ -55,10 +58,13 @@ class TestEditForm extends EditForm<HalResource> {
|
||||
}
|
||||
|
||||
describe('EditForm', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('does not require visibility twice for newly erroneous inactive fields', async () => {
|
||||
const tick = vi.fn();
|
||||
const requireVisible = vi.fn().mockResolvedValue(undefined);
|
||||
const activateField = vi.fn().mockResolvedValue({});
|
||||
const reset = vi.fn();
|
||||
const injector = {
|
||||
get: vi.fn().mockImplementation((token:unknown) => {
|
||||
@@ -70,7 +76,8 @@ describe('EditForm', () => {
|
||||
}),
|
||||
};
|
||||
|
||||
const form = new TestEditForm(injector, requireVisible, activateField, reset);
|
||||
const form = new TestEditForm(injector, requireVisible, reset);
|
||||
const activate = vi.spyOn(form, 'activate').mockResolvedValue({} as EditFieldHandler);
|
||||
const change = {
|
||||
inFlight: false,
|
||||
schema: {
|
||||
@@ -91,13 +98,17 @@ describe('EditForm', () => {
|
||||
showEditingBlockedError: vi.fn(),
|
||||
} as never;
|
||||
form.errorsPerAttribute = { foo: ['Required'] };
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
(form as unknown as {
|
||||
setErrorsForFields:(fields:string[]) => void;
|
||||
}).setErrorsForFields(['foo']);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
await vi.waitFor(() => {
|
||||
expect(activate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
expect(requireVisible).toHaveBeenCalledTimes(1);
|
||||
expect(activate).toHaveBeenCalledWith('foo', true);
|
||||
expect(consoleErrorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -155,7 +155,7 @@ describe('DynamicIconDirective', () => {
|
||||
});
|
||||
|
||||
it('should warn when rendering unknown icon', () => {
|
||||
vi.spyOn(console, 'warn');
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
component.iconName = 'unknown-icon';
|
||||
fixture.detectChanges();
|
||||
@@ -164,7 +164,7 @@ describe('DynamicIconDirective', () => {
|
||||
});
|
||||
|
||||
it('should not render anything for unknown icon', () => {
|
||||
vi.spyOn(console, 'warn');
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
component.iconName = 'unknown-icon';
|
||||
fixture.detectChanges();
|
||||
@@ -180,7 +180,7 @@ describe('DynamicIconDirective', () => {
|
||||
});
|
||||
|
||||
it('should handle empty icon name', () => {
|
||||
vi.spyOn(console, 'warn');
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
|
||||
component.iconName = '';
|
||||
fixture.detectChanges();
|
||||
@@ -195,7 +195,7 @@ describe('DynamicIconDirective', () => {
|
||||
});
|
||||
|
||||
it('should only render once when loaded', () => {
|
||||
vi.spyOn(console, 'warn');
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
const directive = fixture.debugElement.children[0].injector.get(DynamicIconDirective);
|
||||
vi.spyOn(directive as any, 'renderIcon');
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
// Loaded by @angular-builders/custom-esbuild:unit-test via the `runnerConfig`
|
||||
// option in angular.json. The builder layers its browser, setup-file and
|
||||
// reporter settings on top of this file through an internal plugin, so only
|
||||
// runner-level options that the builder does not manage belong here.
|
||||
export default defineConfig({
|
||||
test: {
|
||||
// jquery-migrate prints this banner to stdout at import time. It is
|
||||
// expected, carries no signal, and would otherwise appear once per worker.
|
||||
// Filtering here (reporter level) keeps it out of the output without
|
||||
// mutating the global `console`, which is the idiomatic Vitest mechanism.
|
||||
onConsoleLog(log:string):boolean | void {
|
||||
if (log.includes('JQMIGRATE: Migrate is installed')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user