blob: 8518a21ad0a3df5755137ffc82badc0b08394b5a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Rendering helpers for specs.
*
* The suite currently cannot see the bug it most needs to catch. A field assigned from
* an HTTP callback updates the component but never marks the view dirty, so the DOM keeps
* the old value — the cause of the empty dropdowns in `scripts/audit-async-state.mjs`.
* Two habits hide it:
*
* 1. `fixture.detectChanges()` checks the fixture's own view whether or not anything marked
* it dirty. A component that renders nothing in the running app renders correctly here.
*
* 2. Mocks return `of(value)`, which emits synchronously during construction — before the
* first change detection. The assignment therefore lands *ahead* of the initial render
* and is picked up by it, which no real HTTP response ever is.
*
* Both have to go, or the spec still passes against a broken component. {@link renderComponent}
* replaces the first by awaiting stability instead of forcing a check, and {@link asyncOf}
* replaces the second by emitting a macrotask later, the way a real response arrives.
*
* @example
* ```ts
* officesServiceSpy.getOffices.and.returnValue(asyncOf([{ id: 1, name: 'Head Office' }]));
*
* const fixture = await renderComponent(OfficeFormComponent, {
* imports: [TranslateModule.forRoot()],
* providers: [provideIonicTesting(), { provide: OfficesService, useValue: officesServiceSpy }],
* });
*
* // Fails while `offices` is a plain field, passes once it is a signal.
* expect(fixture.nativeElement.querySelectorAll('ion-select-option').length).toBe(1);
* ```
*/
import { Type } from '@angular/core';
import { ComponentFixture, TestBed, TestModuleMetadata } from '@angular/core/testing';
import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';
/**
* Emits `value` asynchronously, the way an HTTP response arrives.
*
* `of(value)` emits synchronously, so a subscribe callback assigning a plain field runs
* before the first change detection and its result is rendered regardless of whether the
* component ever notified Angular. That makes the spec agree with a component the
* application cannot render. Deferring by a macrotask restores the ordering that exposes it.
*/
export function asyncOf<T>(value: T): Observable<T> {
return of(value).pipe(delay(0));
}
/**
* Creates a component and lets it render the way the application would.
*
* `autoDetectChanges()` checks the view when Angular schedules a tick, and only then —
* the same rule the running app follows. That is the whole point: a component that
* updates state without notifying Angular schedules no tick, so the DOM stays stale and
* the assertion fails, instead of being rescued by a `detectChanges()` the app never makes.
*
* The initial render is not the interesting case — the app performs one too, and
* `ngOnInit` runs as part of it. What must be earned is every update after it, which is
* why mocks should emit through {@link asyncOf} rather than synchronously.
*
* Prefer this over `createComponent` + `detectChanges` in new specs.
*/
export async function renderComponent<T>(
component: Type<T>,
config: TestModuleMetadata = {},
): Promise<ComponentFixture<T>> {
const { imports = [], providers = [], ...rest } = config;
await TestBed.configureTestingModule({
imports: [component, ...(imports as unknown[])],
providers,
...rest,
}).compileComponents();
const fixture = TestBed.createComponent(component);
fixture.autoDetectChanges();
await fixture.whenStable();
return fixture;
}