test(devtools): run the orphaned app component spec

`app.component.spec.ts` has been in the tree since the initial DevTools commit
in 2020, but no revision of `shell-browser/src/app/BUILD.bazel` has ever listed
it, and that file has no globs. Two of the three specs in the directory are
wired up; this one has never run.

It could not run as written. The setup declared `AppComponent` and imported
`RouterModule.forRoot([])`, neither of which applies now that the component is
standalone and injects `MessageBus` and `DEEP_LINK_INSTANCE_ID`.

It now stubs the `chrome.devtools` surface that `ngOnInit` reads and covers
both lifecycle hooks: the listeners registered on init, the two performance
track emits, the three guards on the deep link listener, and the listeners
removed on destroy.

Follow-up to #70570, which fixed the same problem in ng-devtools-backend.
This commit is contained in:
Kam
2026-09-04 15:13:23 +03:00
committed by Kristiyan Kostadinov
parent 635ef1bd80
commit d6d51a0ef8
2 changed files with 159 additions and 20 deletions
@@ -34,6 +34,25 @@ ng_project(
],
)
ts_test_library(
name = "app_component_test_lib",
srcs = ["app.component.spec.ts"],
deps = [
":app",
"//:node_modules/@angular/core",
"//devtools/projects/ng-devtools",
"//devtools/projects/ng-devtools/src/lib/application-providers:deep_link",
"//devtools/projects/protocol",
],
)
zoneless_web_test_suite(
name = "app_component_test",
deps = [
":app_component_test_lib",
],
)
ts_project(
name = "ng_validate",
srcs = [
@@ -6,30 +6,150 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {TestBed, waitForAsync} from '@angular/core/testing';
import {RouterModule} from '@angular/router';
import {ApplicationOperations} from '../../../ng-devtools';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {DevToolsComponent} from '../../../ng-devtools';
import {DEEP_LINK_INSTANCE_ID} from '../../../ng-devtools/src/lib/application-providers/deep_link';
import {Events, MessageBus} from '../../../protocol';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(waitForAsync(() => {
const applicationOperationsSPy = jasmine.createSpyObj('messageBus', ['viewSource']);
TestBed.configureTestingModule({
declarations: [AppComponent],
imports: [RouterModule.forRoot([])],
providers: [
{
provide: ApplicationOperations,
useClass: applicationOperationsSPy,
},
],
});
}));
@Component({
selector: 'ng-devtools',
template: '',
})
export class MockDevToolsComponent {}
it('should create the app', () => {
interface ChromeEvent {
addListener(callback: () => void): void;
removeListener(callback: () => void): void;
}
/** An instance id that no test sets, so it detects a listener that should not have fired. */
const UNTOUCHED = -1;
/** Dispatches a deep link message on the window. */
function postDeepLink(data: unknown, origin = window.location.origin): void {
window.dispatchEvent(new MessageEvent('message', {origin, data}));
}
describe('AppComponent', () => {
let messageBus: jasmine.SpyObj<MessageBus<Events>>;
let onNavigated: jasmine.SpyObj<ChromeEvent>;
let onProfilingStarted: jasmine.SpyObj<ChromeEvent>;
let onProfilingStopped: jasmine.SpyObj<ChromeEvent>;
async function createFixture(): Promise<ComponentFixture<AppComponent>> {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
await fixture.whenStable();
return fixture;
}
beforeEach(() => {
messageBus = jasmine.createSpyObj('MessageBus', ['on', 'once', 'emit', 'destroy']);
onNavigated = jasmine.createSpyObj('onNavigated', ['addListener', 'removeListener']);
onProfilingStarted = jasmine.createSpyObj('onProfilingStarted', [
'addListener',
'removeListener',
]);
onProfilingStopped = jasmine.createSpyObj('onProfilingStopped', [
'addListener',
'removeListener',
]);
(globalThis as any).chrome = {
devtools: {
network: {onNavigated},
performance: {onProfilingStarted, onProfilingStopped},
},
};
TestBed.configureTestingModule({
providers: [{provide: MessageBus, useValue: messageBus}],
}).overrideComponent(AppComponent, {
remove: {imports: [DevToolsComponent]},
add: {imports: [MockDevToolsComponent]},
});
});
afterAll(() => {
delete (globalThis as any).chrome;
});
it('should create the app', async () => {
const fixture = await createFixture();
expect(fixture.componentInstance).toBeTruthy();
});
it('should listen for navigations and profiling on init', async () => {
await createFixture();
expect(onNavigated.addListener).toHaveBeenCalled();
expect(onProfilingStarted.addListener).toHaveBeenCalled();
expect(onProfilingStopped.addListener).toHaveBeenCalled();
});
it('should emit when profiling starts and stops', async () => {
await createFixture();
onProfilingStarted.addListener.calls.mostRecent().args[0]();
expect(messageBus.emit).toHaveBeenCalledWith('enablePerformanceTrack');
onProfilingStopped.addListener.calls.mostRecent().args[0]();
expect(messageBus.emit).toHaveBeenCalledWith('disablePerformanceTrack');
});
it('should stop listening for profiling once destroyed', async () => {
const fixture = await createFixture();
fixture.destroy();
expect(onProfilingStarted.removeListener).toHaveBeenCalled();
expect(onProfilingStopped.removeListener).toHaveBeenCalled();
});
it('should record the instance id sent by a same origin deep link', async () => {
await createFixture();
postDeepLink({type: 'angular-devtools-deep-link', instanceId: 7});
expect(TestBed.inject(DEEP_LINK_INSTANCE_ID)()).toBe(7);
});
it('should ignore a deep link from another origin', async () => {
await createFixture();
TestBed.inject(DEEP_LINK_INSTANCE_ID).set(UNTOUCHED);
postDeepLink({type: 'angular-devtools-deep-link', instanceId: 7}, 'https://malicious.example');
expect(TestBed.inject(DEEP_LINK_INSTANCE_ID)()).toBe(UNTOUCHED);
});
it('should ignore a message of another type', async () => {
await createFixture();
TestBed.inject(DEEP_LINK_INSTANCE_ID).set(UNTOUCHED);
postDeepLink({type: 'some-other-message', instanceId: 7});
expect(TestBed.inject(DEEP_LINK_INSTANCE_ID)()).toBe(UNTOUCHED);
});
it('should ignore a deep link whose instance id is not a number', async () => {
await createFixture();
TestBed.inject(DEEP_LINK_INSTANCE_ID).set(UNTOUCHED);
postDeepLink({type: 'angular-devtools-deep-link', instanceId: '7'});
expect(TestBed.inject(DEEP_LINK_INSTANCE_ID)()).toBe(UNTOUCHED);
});
it('should stop listening for deep links once destroyed', async () => {
const fixture = await createFixture();
TestBed.inject(DEEP_LINK_INSTANCE_ID).set(UNTOUCHED);
fixture.destroy();
postDeepLink({type: 'angular-devtools-deep-link', instanceId: 7});
expect(TestBed.inject(DEEP_LINK_INSTANCE_ID)()).toBe(UNTOUCHED);
});
});