mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(devtools): implement multiframe support in devtools page (#53934)
In the Angular DevTools Chrome DevTools page: - Angular DevTools is able to ask the background script to list each frame that has been registered on a page. - Angular Devtools is able to ask the background script to "enable" the connection on a particular frame. This enables the messaging between the content script <-> background script <-> devtools page - Implements detection of non unique urls on the inspected page Limitations: - The `inspectedWindow.eval` API is only able to target frames by frameURL. This means some features that integrate with Chrome DevTools like inspect element and open source will not be available when inspecting frames that do not have a unique url on the page. PR Close #53934
This commit is contained in:
parent
dd3dac9cc9
commit
ebcdc8dc96
27 changed files with 1024 additions and 166 deletions
|
|
@ -7,6 +7,7 @@ ng_module(
|
|||
srcs = ["devtools-app.component.ts"],
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/src:iframe_message_bus",
|
||||
"//packages/common",
|
||||
|
|
|
|||
|
|
@ -11,11 +11,13 @@ import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
|||
|
||||
import {IFrameMessageBus} from '../../../../../src/iframe-message-bus';
|
||||
import {DevToolsComponent} from 'ng-devtools';
|
||||
import {FrameManager} from '../../../../../projects/ng-devtools/src/lib/frame_manager';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [DevToolsComponent],
|
||||
providers: [
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(null)},
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@ export const subscribeToClientEvents = (
|
|||
|
||||
messageBus.on('logProvider', logProvider);
|
||||
|
||||
messageBus.on('log', ({message, level}) => {
|
||||
console[level](`[Angular DevTools]: ${message}`);
|
||||
});
|
||||
|
||||
if (appIsAngularInDevMode() && appIsSupportedAngularVersion() && appIsAngularIvy()) {
|
||||
setupInspector(messageBus);
|
||||
// Often websites have `scroll` event listener which triggers
|
||||
|
|
@ -216,8 +220,8 @@ const getRoutes = (messageBus: MessageBus<Events>) => {
|
|||
const checkForAngular = (messageBus: MessageBus<Events>): void => {
|
||||
const ngVersion = getAngularVersion();
|
||||
const appIsIvy = appIsAngularIvy();
|
||||
|
||||
if (!ngVersion) {
|
||||
setTimeout(() => checkForAngular(messageBus), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
load("//devtools/tools:ng_module.bzl", "ng_module")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
load("//devtools/tools:typescript.bzl", "ts_test_library")
|
||||
load("//devtools/tools:defaults.bzl", "karma_web_test_suite")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
|
|
@ -12,13 +14,18 @@ ng_module(
|
|||
name = "lib",
|
||||
srcs = glob(
|
||||
include = ["*.ts"],
|
||||
exclude = ["theme-service.ts"],
|
||||
exclude = [
|
||||
"theme-service.ts",
|
||||
"frame_manager.ts",
|
||||
"*_spec.ts",
|
||||
],
|
||||
),
|
||||
angular_assets = [
|
||||
"devtools.component.html",
|
||||
":devtools_component_styles",
|
||||
],
|
||||
deps = [
|
||||
":frame_manager",
|
||||
":theme",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs",
|
||||
"//devtools/projects/protocol",
|
||||
|
|
@ -34,6 +41,58 @@ ng_module(
|
|||
],
|
||||
)
|
||||
|
||||
ts_test_library(
|
||||
name = "devtools_test",
|
||||
srcs = ["devtools_spec.ts"],
|
||||
deps = [
|
||||
":frame_manager",
|
||||
":lib",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [
|
||||
":devtools_test",
|
||||
],
|
||||
)
|
||||
|
||||
ng_module(
|
||||
name = "frame_manager",
|
||||
srcs = glob(
|
||||
include = ["frame_manager.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
],
|
||||
)
|
||||
|
||||
ts_test_library(
|
||||
name = "test_frame_manager_lib",
|
||||
srcs = [
|
||||
"frame_manager_spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core/testing",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "test_frame_manager",
|
||||
deps = [
|
||||
":test_frame_manager_lib",
|
||||
],
|
||||
)
|
||||
|
||||
ng_module(
|
||||
name = "theme",
|
||||
srcs = glob(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ export interface Environment {
|
|||
LATEST_SHA: string;
|
||||
}
|
||||
|
||||
export const TOP_LEVEL_FRAME_ID = 0;
|
||||
|
||||
export interface Frame {
|
||||
id: number;
|
||||
name: string;
|
||||
url: URL;
|
||||
}
|
||||
|
||||
export abstract class ApplicationEnvironment {
|
||||
abstract get environment(): Environment;
|
||||
abstract frameSelectorEnabled: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
import {DirectivePosition, ElementPosition} from 'protocol';
|
||||
|
||||
export abstract class ApplicationOperations {
|
||||
abstract viewSource(position: ElementPosition, directiveIndex?: number): void;
|
||||
abstract selectDomElement(position: ElementPosition): void;
|
||||
abstract inspect(directivePosition: DirectivePosition, objectPath: string[]): void;
|
||||
abstract viewSource(position: ElementPosition, directiveIndex?: number, target?: URL): void;
|
||||
abstract selectDomElement(position: ElementPosition, target?: URL): void;
|
||||
abstract inspect(directivePosition: DirectivePosition, objectPath: string[], target?: URL): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ ng_module(
|
|||
":devtools_tabs_component_styles",
|
||||
],
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib:theme",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer",
|
||||
|
|
@ -42,6 +43,7 @@ ts_test_library(
|
|||
srcs = ["devtools-tabs.spec.ts"],
|
||||
deps = [
|
||||
":devtools-tabs",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib:theme",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer",
|
||||
|
|
@ -56,7 +58,6 @@ ts_test_library(
|
|||
],
|
||||
)
|
||||
|
||||
# todo(aleksanderbodurri): fix this test suite
|
||||
karma_web_test_suite(
|
||||
name = "test",
|
||||
deps = [
|
||||
|
|
@ -65,16 +66,3 @@ karma_web_test_suite(
|
|||
"//packages/platform-browser/animations",
|
||||
],
|
||||
)
|
||||
|
||||
# spec_bundle(
|
||||
# name = "test_bundle",
|
||||
# deps = [
|
||||
# ":devtools_tabs_test",
|
||||
# "//packages/platform-browser/animations",
|
||||
# "//packages/animations",
|
||||
# "//packages/common/http",
|
||||
# "//packages/core",
|
||||
# "//packages/core/src/util",
|
||||
# ],
|
||||
# platform = "browser",
|
||||
# )
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@
|
|||
<mat-icon> info </mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<select matTooltip="Select a frame to inspect with Angular Devtools" class="frame-selector" (change)="emitSelectedFrame($event.target.value)">
|
||||
@for (frame of frameManager.frames; track frame.id) {
|
||||
<option [value]="frame.id" [selected]="frameManager.isSelectedFrame(frame)">
|
||||
@if (frame.id === TOP_LEVEL_FRAME_ID) {
|
||||
top
|
||||
} @else {
|
||||
{{ frame.name }} ({{ frame.id }})
|
||||
}
|
||||
</option>
|
||||
} @empty {
|
||||
<option value="0" selected>top</option>
|
||||
}
|
||||
</select>
|
||||
|
||||
@for (tab of tabs; track $index) {
|
||||
<a class="mat-tab-link" mat-tab-link (click)="changeTab(tab)" [active]="activeTab === tab">
|
||||
{{ tab }}
|
||||
|
|
@ -27,19 +42,23 @@
|
|||
</section>
|
||||
}
|
||||
</nav>
|
||||
|
||||
<mat-tab-nav-panel #tabPanel>
|
||||
<div class="tab-content">
|
||||
<ng-directive-explorer
|
||||
[showCommentNodes]="showCommentNodes"
|
||||
[isHydrationEnabled]="isHydrationEnabled"
|
||||
[class.hidden]="activeTab !== 'Components'"
|
||||
(toggleInspector)="toggleInspector()"
|
||||
/>
|
||||
<ng-profiler [class.hidden]="activeTab !== 'Profiler'"/>
|
||||
<ng-router-tree [routes]="routes" [class.hidden]="activeTab !== 'Router Tree'"/>
|
||||
<ng-injector-tree [class.hidden]="activeTab !== 'Injector Tree'"/>
|
||||
</div>
|
||||
@if (!applicationEnvironment.frameSelectorEnabled || frameManager.selectedFrame !== null) {
|
||||
<div class="tab-content">
|
||||
<ng-directive-explorer
|
||||
[showCommentNodes]="showCommentNodes"
|
||||
[isHydrationEnabled]="isHydrationEnabled"
|
||||
[class.hidden]="activeTab !== 'Components'"
|
||||
(toggleInspector)="toggleInspector()"
|
||||
/>
|
||||
<ng-profiler [class.hidden]="activeTab !== 'Profiler'"/>
|
||||
<ng-router-tree [routes]="routes" [class.hidden]="activeTab !== 'Router Tree'"/>
|
||||
<ng-injector-tree [class.hidden]="activeTab !== 'Injector Tree'"/>
|
||||
</div>
|
||||
}
|
||||
</mat-tab-nav-panel>
|
||||
|
||||
<mat-menu #menu="matMenu">
|
||||
<div mat-menu-item disableRipple (click)="$event.stopPropagation(); toggleTimingAPI()">
|
||||
<mat-slide-toggle [checked]="timingAPIEnabled">
|
||||
|
|
@ -57,6 +76,7 @@
|
|||
</mat-slide-toggle>
|
||||
</div>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #info="matMenu">
|
||||
<a mat-menu-item href="https://angular.io/devtools" target="_blank">
|
||||
<mat-icon>library_books</mat-icon>
|
||||
|
|
|
|||
|
|
@ -119,3 +119,22 @@ mat-icon {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.frame-selector {
|
||||
background-color: #e2e2e2;
|
||||
border-radius: 2px;
|
||||
color: #474747;
|
||||
border: none;
|
||||
margin: 4px 4px 2px 4px;
|
||||
padding: 2px;
|
||||
outline-offset: -2px;
|
||||
width: 100px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:host-context(.dark-theme) {
|
||||
.frame-selector {
|
||||
background-color: #464646;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,16 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AfterViewInit, Component, Input, OnInit, ViewChild} from '@angular/core';
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
|
||||
import {MatIcon} from '@angular/material/icon';
|
||||
import {MatMenu, MatMenuItem, MatMenuTrigger} from '@angular/material/menu';
|
||||
|
|
@ -15,7 +24,8 @@ import {MatTabLink, MatTabNav, MatTabNavPanel} from '@angular/material/tabs';
|
|||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {Events, MessageBus, Route} from 'protocol';
|
||||
|
||||
import {ApplicationEnvironment} from '../application-environment/index';
|
||||
import {ApplicationEnvironment, Frame, TOP_LEVEL_FRAME_ID} from '../application-environment/index';
|
||||
import {FrameManager} from '../frame_manager';
|
||||
import {Theme, ThemeService} from '../theme-service';
|
||||
|
||||
import {DirectiveExplorerComponent} from './directive-explorer/directive-explorer.component';
|
||||
|
|
@ -52,25 +62,28 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
|||
@Input() angularVersion: string | undefined = undefined;
|
||||
@Input() isHydrationEnabled = false;
|
||||
|
||||
@Output() frameSelected = new EventEmitter<Frame>();
|
||||
@ViewChild(DirectiveExplorerComponent) directiveExplorer!: DirectiveExplorerComponent;
|
||||
@ViewChild('navBar', {static: true}) navbar!: MatTabNav;
|
||||
|
||||
applicationEnvironment = inject(ApplicationEnvironment);
|
||||
activeTab: Tabs = 'Components';
|
||||
|
||||
inspectorRunning = false;
|
||||
routerTreeEnabled = false;
|
||||
showCommentNodes = false;
|
||||
timingAPIEnabled = false;
|
||||
|
||||
currentTheme!: Theme;
|
||||
|
||||
routes: Route[] = [];
|
||||
|
||||
frameManager = inject(FrameManager);
|
||||
|
||||
TOP_LEVEL_FRAME_ID = TOP_LEVEL_FRAME_ID;
|
||||
|
||||
constructor(
|
||||
public tabUpdate: TabUpdate,
|
||||
public themeService: ThemeService,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _applicationEnvironment: ApplicationEnvironment,
|
||||
) {
|
||||
this.themeService.currentTheme
|
||||
.pipe(takeUntilDestroyed())
|
||||
|
|
@ -81,6 +94,11 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
|||
});
|
||||
}
|
||||
|
||||
emitSelectedFrame(frameId: string): void {
|
||||
const frame = this.frameManager.frames.find((frame) => frame.id === parseInt(frameId, 10));
|
||||
this.frameSelected.emit(frame);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.navbar.stretchTabs = false;
|
||||
}
|
||||
|
|
@ -95,7 +113,7 @@ export class DevToolsTabsComponent implements OnInit, AfterViewInit {
|
|||
}
|
||||
|
||||
get latestSHA(): string {
|
||||
return this._applicationEnvironment.environment.LATEST_SHA.slice(0, 8);
|
||||
return this.applicationEnvironment.environment.LATEST_SHA.slice(0, 8);
|
||||
}
|
||||
|
||||
changeTab(tab: Tabs): void {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {Theme, ThemeService} from '../theme-service';
|
|||
import {DevToolsTabsComponent} from './devtools-tabs.component';
|
||||
import {TabUpdate} from './tab-update/index';
|
||||
import {DirectiveExplorerComponent} from './directive-explorer/directive-explorer.component';
|
||||
import {FrameManager} from '../frame_manager';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-directive-explorer',
|
||||
|
|
@ -36,6 +37,7 @@ describe('DevtoolsTabsComponent', () => {
|
|||
beforeEach(() => {
|
||||
messageBusMock = jasmine.createSpyObj('messageBus', ['on', 'once', 'emit', 'destroy']);
|
||||
applicationEnvironmentMock = jasmine.createSpyObj('applicationEnvironment', ['environment']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MatTooltip, MatMenuModule, DevToolsTabsComponent],
|
||||
providers: [
|
||||
|
|
@ -43,6 +45,7 @@ describe('DevtoolsTabsComponent', () => {
|
|||
{provide: ThemeService, useFactory: () => ({currentTheme: new Subject<Theme>()})},
|
||||
{provide: MessageBus, useValue: messageBusMock},
|
||||
{provide: ApplicationEnvironment, useValue: applicationEnvironmentMock},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(123)},
|
||||
],
|
||||
}).overrideComponent(DevToolsTabsComponent, {
|
||||
remove: {imports: [DirectiveExplorerComponent]},
|
||||
|
|
@ -74,4 +77,24 @@ describe('DevtoolsTabsComponent', () => {
|
|||
expect(messageBusMock.emit).toHaveBeenCalledWith('inspectorEnd');
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('removeHighlightOverlay');
|
||||
});
|
||||
|
||||
it('should emit a selectedFrame when emitSelectedFrame is called', () => {
|
||||
let contentScriptConnected: Function = () => {};
|
||||
|
||||
// mock message bus on method with jasmine fake call in order to pick out callback
|
||||
// and call it with frame
|
||||
(messageBusMock.on as any).and.callFake((topic: string, cb: Function) => {
|
||||
if (topic === 'contentScriptConnected') {
|
||||
contentScriptConnected = cb;
|
||||
}
|
||||
});
|
||||
|
||||
const frameId = 1;
|
||||
expect(contentScriptConnected).toEqual(jasmine.any(Function));
|
||||
contentScriptConnected(frameId, 'name', 'http://localhost:4200/url');
|
||||
spyOn(comp.frameSelected, 'emit');
|
||||
comp.emitSelectedFrame('1');
|
||||
|
||||
expect(comp.frameSelected.emit).toHaveBeenCalledWith(comp.frameManager.frames[0]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -43,10 +43,14 @@ ts_test_library(
|
|||
srcs = ["directive-explorer.spec.ts"],
|
||||
deps = [
|
||||
":directive-explorer",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-operations",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/breadcrumbs",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-resolver",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/property-tab",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/tab-update",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
"//packages/core/testing",
|
||||
|
|
@ -59,8 +63,5 @@ karma_web_test_suite(
|
|||
name = "test",
|
||||
deps = [
|
||||
":directive_explorer_test",
|
||||
"//packages/common/http",
|
||||
"//packages/platform-browser",
|
||||
"//packages/platform-browser/animations",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
|
|
@ -32,6 +33,7 @@ import {
|
|||
|
||||
import {SplitComponent} from '../../../lib/vendor/angular-split/public_api';
|
||||
import {ApplicationOperations} from '../../application-operations/index';
|
||||
import {FrameManager} from '../../frame_manager';
|
||||
|
||||
import {BreadcrumbsComponent} from './directive-forest/breadcrumbs/breadcrumbs.component';
|
||||
import {FlatNode} from './directive-forest/component-data-source';
|
||||
|
|
@ -122,11 +124,12 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
private _refreshRetryTimeout: null | ReturnType<typeof setTimeout> = null;
|
||||
|
||||
constructor(
|
||||
private _appOperations: ApplicationOperations,
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _propResolver: ElementPropertyResolver,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
private _ngZone: NgZone,
|
||||
private readonly _appOperations: ApplicationOperations,
|
||||
private readonly _messageBus: MessageBus<Events>,
|
||||
private readonly _propResolver: ElementPropertyResolver,
|
||||
private readonly _cdr: ChangeDetectorRef,
|
||||
private readonly _ngZone: NgZone,
|
||||
private readonly _frameManager: FrameManager,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
|
@ -198,18 +201,37 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
(directive) => directive.name === directiveName,
|
||||
);
|
||||
|
||||
if (directiveIndex === -1) {
|
||||
// view the component definition
|
||||
this._appOperations.viewSource(this.currentSelectedElement.position);
|
||||
const selectedFrame = this._frameManager.selectedFrame;
|
||||
if (!this._frameManager.frameHasUniqueUrl(selectedFrame)) {
|
||||
this._messageBus.emit('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot view source.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// view the directive definition
|
||||
this._appOperations.viewSource(this.currentSelectedElement.position, directiveIndex);
|
||||
this._appOperations.viewSource(
|
||||
this.currentSelectedElement.position,
|
||||
directiveIndex !== -1 ? directiveIndex : undefined,
|
||||
new URL(selectedFrame!.url),
|
||||
);
|
||||
}
|
||||
|
||||
handleSelectDomElement(node: IndexedNode): void {
|
||||
this._appOperations.selectDomElement(node.position);
|
||||
const selectedFrame = this._frameManager.selectedFrame;
|
||||
if (!this._frameManager.frameHasUniqueUrl(selectedFrame)) {
|
||||
this._messageBus.emit('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot select DOM element.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this._appOperations.selectDomElement(node.position, new URL(selectedFrame!.url));
|
||||
}
|
||||
|
||||
highlight(node: FlatNode): void {
|
||||
|
|
@ -278,7 +300,19 @@ export class DirectiveExplorerComponent implements OnInit, OnDestroy {
|
|||
directivePosition: DirectivePosition;
|
||||
}): void {
|
||||
const objectPath = constructPathOfKeysToPropertyValue(node.prop);
|
||||
this._appOperations.inspect(directivePosition, objectPath);
|
||||
|
||||
const selectedFrame = this._frameManager.selectedFrame;
|
||||
if (!this._frameManager.frameHasUniqueUrl(selectedFrame)) {
|
||||
this._messageBus.emit('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot inspect object.`,
|
||||
},
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
this._appOperations.inspect(directivePosition, objectPath, new URL(selectedFrame!.url));
|
||||
}
|
||||
|
||||
hightlightHydrationNodes() {
|
||||
|
|
|
|||
|
|
@ -6,37 +6,94 @@
|
|||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {Events, MessageBus, PropertyQueryTypes} from 'protocol';
|
||||
|
||||
import {ApplicationOperations} from '../../application-operations';
|
||||
import {DirectivePosition, MessageBus, PropType, PropertyQueryTypes} from 'protocol';
|
||||
|
||||
import {DirectiveExplorerComponent} from './directive-explorer.component';
|
||||
import {DirectiveForestComponent} from './directive-forest/directive-forest.component';
|
||||
import {IndexedNode} from './directive-forest/index-forest';
|
||||
import {ElementPropertyResolver} from './property-resolver/element-property-resolver';
|
||||
|
||||
import SpyObj = jasmine.SpyObj;
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {FrameManager} from '../../frame_manager';
|
||||
import {TabUpdate} from '../tab-update';
|
||||
import {Component, EventEmitter, Input, Output, CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {ElementPropertyResolver, FlatNode} from './property-resolver/element-property-resolver';
|
||||
import {BreadcrumbsComponent} from './directive-forest/breadcrumbs/breadcrumbs.component';
|
||||
import {PropertyTabComponent} from './property-tab/property-tab.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-directive-forest',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class MockDirectiveForestComponent {
|
||||
@Input() forest: IndexedNode[] = [];
|
||||
@Input() currentSelectedElement: IndexedNode | null = null;
|
||||
@Input() showCommentNodes = false;
|
||||
@Output() selectNode = new EventEmitter<IndexedNode>();
|
||||
@Output() selectDomElement = new EventEmitter<IndexedNode>();
|
||||
@Output() setParents = new EventEmitter<IndexedNode>();
|
||||
@Output() highlightComponent = new EventEmitter<IndexedNode>();
|
||||
@Output() removeComponentHighlight = new EventEmitter<void>();
|
||||
@Output() toggleInspector = new EventEmitter<void>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-breadcrumbs',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class MockBreadcrumbsComponent {
|
||||
@Input() parents: IndexedNode[] = [];
|
||||
@Output() handleSelect = new EventEmitter<any>();
|
||||
@Output() mouseLeaveNode = new EventEmitter<any>();
|
||||
@Output() mouseOverNode = new EventEmitter<any>();
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-property-tab',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
class MockPropertyTabComponent {
|
||||
@Input() currentSelectedElement: IndexedNode | null = null;
|
||||
@Output() inspect = new EventEmitter<{node: FlatNode; directivePosition: DirectivePosition}>();
|
||||
@Output() viewSource = new EventEmitter<string>();
|
||||
}
|
||||
|
||||
describe('DirectiveExplorerComponent', () => {
|
||||
let messageBusMock: SpyObj<MessageBus<Events>>;
|
||||
let messageBusMock: SpyObj<any>;
|
||||
let fixture: ComponentFixture<DirectiveExplorerComponent>;
|
||||
let comp: DirectiveExplorerComponent;
|
||||
let applicationOperationsSpy: SpyObj<ApplicationOperations>;
|
||||
let contentScriptConnected = (frameId: number, name: string, url: string) => {};
|
||||
let frameConnected = (frameId: number) => {};
|
||||
|
||||
beforeEach(() => {
|
||||
applicationOperationsSpy = jasmine.createSpyObj<ApplicationOperations>('_appOperations', [
|
||||
'viewSource',
|
||||
'selectDomElement',
|
||||
'inspect',
|
||||
]);
|
||||
messageBusMock = jasmine.createSpyObj<MessageBus<Events>>('messageBus', [
|
||||
'on',
|
||||
'once',
|
||||
'emit',
|
||||
'destroy',
|
||||
]);
|
||||
|
||||
messageBusMock = jasmine.createSpyObj('messageBus', ['on', 'once', 'emit', 'destroy']);
|
||||
|
||||
messageBusMock.on.and.callFake((topic: string, cb: Function) => {
|
||||
if (topic === 'contentScriptConnected') {
|
||||
contentScriptConnected = cb as (frameId: number, name: string, url: string) => void;
|
||||
}
|
||||
if (topic === 'frameConnected') {
|
||||
frameConnected = cb as (frameId: number) => void;
|
||||
}
|
||||
});
|
||||
messageBusMock.emit.and.callFake((topic: string, args: any[]) => {
|
||||
if (topic === 'enableFrameConnection') {
|
||||
frameConnected(args[0]);
|
||||
}
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
|
|
@ -46,14 +103,24 @@ describe('DirectiveExplorerComponent', () => {
|
|||
provide: ElementPropertyResolver,
|
||||
useValue: new ElementPropertyResolver(messageBusMock),
|
||||
},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(123)},
|
||||
],
|
||||
}).overrideComponent(DirectiveExplorerComponent, {
|
||||
add: {schemas: [CUSTOM_ELEMENTS_SCHEMA]},
|
||||
remove: {imports: [DirectiveForestComponent]},
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DirectiveExplorerComponent);
|
||||
fixture = TestBed.overrideComponent(DirectiveExplorerComponent, {
|
||||
remove: {imports: [DirectiveForestComponent, BreadcrumbsComponent, PropertyTabComponent]},
|
||||
add: {
|
||||
imports: [MockDirectiveForestComponent, MockBreadcrumbsComponent, MockPropertyTabComponent],
|
||||
},
|
||||
}).createComponent(DirectiveExplorerComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
TestBed.inject(FrameManager);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create instance from class', () => {
|
||||
|
|
@ -61,8 +128,6 @@ describe('DirectiveExplorerComponent', () => {
|
|||
});
|
||||
|
||||
it('subscribe to backend events', () => {
|
||||
comp.subscribeToBackendEvents();
|
||||
expect(messageBusMock.on).toHaveBeenCalledTimes(2);
|
||||
expect(messageBusMock.on).toHaveBeenCalledWith(
|
||||
'latestComponentExplorerView',
|
||||
jasmine.any(Function),
|
||||
|
|
@ -73,7 +138,9 @@ describe('DirectiveExplorerComponent', () => {
|
|||
describe('refresh', () => {
|
||||
it('should emit getLatestComponentExplorerView event on refresh', () => {
|
||||
comp.refresh();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should emit getLatestComponentExplorerView event with null view query', () => {
|
||||
|
|
@ -110,7 +177,6 @@ describe('DirectiveExplorerComponent', () => {
|
|||
const position = [0];
|
||||
nodeMock.position = position;
|
||||
comp.handleNodeSelection(nodeMock);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(2);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('setSelectedComponent', [nodeMock.position]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('getLatestComponentExplorerView', [
|
||||
{
|
||||
|
|
@ -126,11 +192,9 @@ describe('DirectiveExplorerComponent', () => {
|
|||
describe('hydration', () => {
|
||||
it('should highlight hydration nodes', () => {
|
||||
comp.hightlightHydrationNodes();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('createHydrationOverlay');
|
||||
|
||||
comp.removeHydrationNodesHightlights();
|
||||
expect(messageBusMock.emit).toHaveBeenCalledTimes(2);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('removeHydrationOverlay');
|
||||
});
|
||||
|
||||
|
|
@ -146,4 +210,161 @@ describe('DirectiveExplorerComponent', () => {
|
|||
expect(toggle2).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applicaton operations', () => {
|
||||
describe('view source', () => {
|
||||
it('should not call application operations view source if no frames are detected', () => {
|
||||
const directiveName = 'test';
|
||||
comp.currentSelectedElement = {
|
||||
directives: [{name: directiveName}],
|
||||
position: [0],
|
||||
children: [] as IndexedNode[],
|
||||
} as IndexedNode;
|
||||
comp.viewSource(directiveName);
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not call application operations view source if a frame is selected that does not have a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url');
|
||||
|
||||
const directiveName = 'test';
|
||||
comp.currentSelectedElement = {
|
||||
directives: [{name: directiveName}],
|
||||
position: [0],
|
||||
children: [] as IndexedNode[],
|
||||
} as IndexedNode;
|
||||
|
||||
comp.viewSource(directiveName);
|
||||
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledTimes(0);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot view source.`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call application operations view source if a frame is selected that has a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url2');
|
||||
|
||||
const directiveName = 'test';
|
||||
comp.currentSelectedElement = {
|
||||
directives: [{name: directiveName}],
|
||||
position: [0],
|
||||
children: [] as IndexedNode[],
|
||||
} as IndexedNode;
|
||||
|
||||
comp.viewSource(directiveName);
|
||||
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(applicationOperationsSpy.viewSource).toHaveBeenCalledWith(
|
||||
[0], // current selected element position
|
||||
0, // directive index
|
||||
new URL('http://localhost:4200/url'), // selected frame url
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('select dom element', () => {
|
||||
it('should not call application operations select dom element if no frames are detected', () => {
|
||||
comp.handleSelectDomElement({position: [0], children: [] as IndexedNode[]} as IndexedNode);
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not call application operations select dom element if a frame is selected that does not have a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url');
|
||||
|
||||
comp.handleSelectDomElement({position: [0], children: [] as IndexedNode[]} as IndexedNode);
|
||||
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(0);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot select DOM element.`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call application operations select dom element if a frame is selected that has a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url2');
|
||||
|
||||
comp.handleSelectDomElement({position: [0], children: [] as IndexedNode[]} as IndexedNode);
|
||||
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledWith(
|
||||
[0], // current selected element position
|
||||
new URL('http://localhost:4200/url'), // selected frame url
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspect', () => {
|
||||
let node: FlatNode;
|
||||
let directivePosition: DirectivePosition;
|
||||
|
||||
beforeEach(() => {
|
||||
node = {
|
||||
expandable: true,
|
||||
prop: {
|
||||
name: 'foo',
|
||||
parent: null,
|
||||
descriptor: {
|
||||
expandable: true,
|
||||
editable: false,
|
||||
type: PropType.String,
|
||||
preview: 'preview',
|
||||
containerType: null,
|
||||
},
|
||||
},
|
||||
level: 1,
|
||||
};
|
||||
directivePosition = {element: [0], directive: 0};
|
||||
});
|
||||
|
||||
it('should not call application operations inspect if no frames are detected', () => {
|
||||
comp.inspect({node, directivePosition});
|
||||
expect(applicationOperationsSpy.selectDomElement).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should not call application operations inspect if a frame is selected that does not have a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url');
|
||||
|
||||
comp.inspect({node, directivePosition});
|
||||
|
||||
expect(applicationOperationsSpy.inspect).toHaveBeenCalledTimes(0);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('log', [
|
||||
{
|
||||
level: 'warn',
|
||||
message: `The currently inspected frame does not have a unique url on this page. Cannot inspect object.`,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should call application operations inspect if a frame is selected that has a unique url on the page', () => {
|
||||
contentScriptConnected(0, 'test1', 'http://localhost:4200/url');
|
||||
contentScriptConnected(1, 'test2', 'http://localhost:4200/url2');
|
||||
|
||||
comp.inspect({node, directivePosition});
|
||||
|
||||
expect(applicationOperationsSpy.inspect).toHaveBeenCalledTimes(1);
|
||||
expect(messageBusMock.emit).toHaveBeenCalledWith('enableFrameConnection', [0, 123]);
|
||||
expect(applicationOperationsSpy.inspect).toHaveBeenCalledWith(
|
||||
directivePosition,
|
||||
['foo'],
|
||||
new URL('http://localhost:4200/url'), // selected frame url
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,6 +47,8 @@ ng_module(
|
|||
"property-tab-body.component.html",
|
||||
] + _STYLE_LABELS,
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/ng-devtools/src/lib/application-environment",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:injector_tree_visualizer",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/dependency-injection:resolution_path",
|
||||
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<mat-toolbar>
|
||||
{{ directive }}
|
||||
<button matTooltip="Open source" (click)="handleViewSource($event)">
|
||||
<mat-icon> code </mat-icon>
|
||||
</button>
|
||||
{{ directive }}
|
||||
<button matTooltip="Open source" (click)="handleViewSource($event)">
|
||||
<mat-icon> code </mat-icon>
|
||||
</button>
|
||||
</mat-toolbar>
|
||||
|
|
@ -1,58 +1,57 @@
|
|||
<div class="mat-typography mat-app-background" style="height: 100%;">
|
||||
@if (angularExists === true) {
|
||||
@if (angularIsInDevMode) {
|
||||
@if (supportedVersion) {
|
||||
<div class="devtools-wrapper noselect" [@enterAnimation]>
|
||||
<ng-devtools-tabs [angularVersion]="angularVersion" [isHydrationEnabled]="hydration"/>
|
||||
</div>
|
||||
<div class="devtools mat-typography mat-app-background" style="height: 100%;">
|
||||
@switch (angularStatus) {
|
||||
@case (AngularStatus.EXISTS) {
|
||||
@if (angularIsInDevMode) {
|
||||
@if (supportedVersion) {
|
||||
<div class="devtools-wrapper noselect" [@enterAnimation]>
|
||||
<ng-devtools-tabs (frameSelected)="inspectFrame($event)" [isHydrationEnabled]="hydration" [angularVersion]="angularVersion"></ng-devtools-tabs>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="text-message">
|
||||
Angular Devtools only supports Angular versions 12 and above
|
||||
</p>
|
||||
}
|
||||
} @else {
|
||||
<p class="text-message">
|
||||
Angular Devtools only supports Angular versions 12 and above
|
||||
<p class="text-message" matTooltip="A dev build is when the `optimization` flag is set to `false` in the angular.json config file.">
|
||||
We detected an application built with production configuration. Angular DevTools only supports development build.
|
||||
</p>
|
||||
}
|
||||
} @else {
|
||||
<p class="text-message" title="A dev build is when the `optimization` flag is set to `true` in the angular.json config file.">
|
||||
We detected an application built with production configuration. Angular DevTools only supports development build.
|
||||
}
|
||||
@case (AngularStatus.DOES_NOT_EXIST) {
|
||||
<p class="text-message not-detected">
|
||||
<span class="info-icon" matTooltip="You see this message because the app is still loading, or this is not an Angular application">i</span>
|
||||
Angular application not detected.
|
||||
</p>
|
||||
}
|
||||
} @else {
|
||||
@if (angularExists === false) {
|
||||
<p class="text-message">
|
||||
<span class="info-icon" matTooltip="You see this message because the app is still loading, or this is not an Angular application">i</span
|
||||
>
|
||||
Angular application not detected.
|
||||
</p>
|
||||
} @else {
|
||||
@if (angularExists === null) {
|
||||
<div class="initializing">
|
||||
<div class="loading">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="120">
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="url(#b)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
<path fill="url(#c)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="49.009" x2="225.829" y1="213.75" y2="129.722" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E40035"></stop>
|
||||
<stop offset=".24" stop-color="#F60A48"></stop>
|
||||
<stop offset=".352" stop-color="#F20755"></stop>
|
||||
<stop offset=".494" stop-color="#DC087D"></stop>
|
||||
<stop offset=".745" stop-color="#9717E7"></stop>
|
||||
<stop offset="1" stop-color="#6C00F5"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="41.025" x2="156.741" y1="28.344" y2="160.344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF31D9"></stop>
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h223v236H0z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@case (AngularStatus.UNKNOWN) {
|
||||
<div class="initializing">
|
||||
<div class="loading">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 223 236" width="120">
|
||||
<g clip-path="url(#a)">
|
||||
<path fill="url(#b)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
<path fill="url(#c)" d="m222.077 39.192-8.019 125.923L137.387 0l84.69 39.192Zm-53.105 162.825-57.933 33.056-57.934-33.056 11.783-28.556h92.301l11.783 28.556ZM111.039 62.675l30.357 73.803H80.681l30.358-73.803ZM7.937 165.115 0 39.192 84.69 0 7.937 165.115Z"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="b" x1="49.009" x2="225.829" y1="213.75" y2="129.722" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#E40035"></stop>
|
||||
<stop offset=".24" stop-color="#F60A48"></stop>
|
||||
<stop offset=".352" stop-color="#F20755"></stop>
|
||||
<stop offset=".494" stop-color="#DC087D"></stop>
|
||||
<stop offset=".745" stop-color="#9717E7"></stop>
|
||||
<stop offset="1" stop-color="#6C00F5"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="41.025" x2="156.741" y1="28.344" y2="160.344" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF31D9"></stop>
|
||||
<stop offset="1" stop-color="#FF5BE1" stop-opacity="0"></stop>
|
||||
</linearGradient>
|
||||
<clipPath id="a">
|
||||
<path fill="#fff" d="M0 0h223v236H0z"></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
@ -9,13 +9,36 @@
|
|||
import {animate, style, transition, trigger} from '@angular/animations';
|
||||
import {Platform} from '@angular/cdk/platform';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {Component, Inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {Events, MessageBus} from 'protocol';
|
||||
import {interval} from 'rxjs';
|
||||
|
||||
import {FrameManager} from './frame_manager';
|
||||
import {ThemeService} from './theme-service';
|
||||
import {MatTooltip} from '@angular/material/tooltip';
|
||||
import {MatTooltip, MatTooltipModule} from '@angular/material/tooltip';
|
||||
import {DevToolsTabsComponent} from './devtools-tabs/devtools-tabs.component';
|
||||
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||
import {Frame} from './application-environment';
|
||||
|
||||
const DETECT_ANGULAR_ATTEMPTS = 10;
|
||||
|
||||
enum AngularStatus {
|
||||
/**
|
||||
* This page may have Angular but we don't know yet. We're still trying to detect it.
|
||||
*/
|
||||
UNKNOWN,
|
||||
|
||||
/**
|
||||
* We've given up on trying to detect Angular. We tried ${DETECT_ANGULAR_ATTEMPTS} times and
|
||||
* failed.
|
||||
*/
|
||||
DOES_NOT_EXIST,
|
||||
|
||||
/**
|
||||
* Angular was detected somewhere on the page.
|
||||
*/
|
||||
EXISTS,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ng-devtools',
|
||||
|
|
@ -28,10 +51,11 @@ import {DevToolsTabsComponent} from './devtools-tabs/devtools-tabs.component';
|
|||
]),
|
||||
],
|
||||
standalone: true,
|
||||
imports: [DevToolsTabsComponent, MatTooltip],
|
||||
imports: [DevToolsTabsComponent, MatTooltip, MatProgressSpinnerModule, MatTooltipModule],
|
||||
})
|
||||
export class DevToolsComponent implements OnInit, OnDestroy {
|
||||
angularExists: boolean | null = null;
|
||||
AngularStatus = AngularStatus;
|
||||
angularStatus: AngularStatus = AngularStatus.UNKNOWN;
|
||||
angularVersion: string | boolean | undefined = undefined;
|
||||
angularIsInDevMode = true;
|
||||
hydration: boolean = false;
|
||||
|
|
@ -39,26 +63,28 @@ export class DevToolsComponent implements OnInit, OnDestroy {
|
|||
|
||||
private readonly _firefoxStyleName = 'firefox_styles.css';
|
||||
private readonly _chromeStyleName = 'chrome_styles.css';
|
||||
|
||||
constructor(
|
||||
private _messageBus: MessageBus<Events>,
|
||||
private _themeService: ThemeService,
|
||||
private _platform: Platform,
|
||||
@Inject(DOCUMENT) private _document: Document,
|
||||
) {}
|
||||
private readonly _messageBus = inject<MessageBus<Events>>(MessageBus);
|
||||
private readonly _themeService = inject(ThemeService);
|
||||
private readonly _platform = inject(Platform);
|
||||
private readonly _document = inject(DOCUMENT);
|
||||
private readonly _frameManager = inject(FrameManager);
|
||||
|
||||
private _interval$ = interval(500).subscribe((attempt) => {
|
||||
if (attempt === 10) {
|
||||
this.angularExists = false;
|
||||
if (attempt === DETECT_ANGULAR_ATTEMPTS) {
|
||||
this.angularStatus = AngularStatus.DOES_NOT_EXIST;
|
||||
}
|
||||
this._messageBus.emit('queryNgAvailability');
|
||||
});
|
||||
|
||||
inspectFrame(frame: Frame) {
|
||||
this._frameManager.inspectFrame(frame);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this._themeService.initializeThemeWatcher();
|
||||
|
||||
this._messageBus.once('ngAvailability', ({version, devMode, ivy, hydration}) => {
|
||||
this.angularExists = !!version;
|
||||
this.angularStatus = version ? AngularStatus.EXISTS : AngularStatus.DOES_NOT_EXIST;
|
||||
this.angularVersion = version;
|
||||
this.angularIsInDevMode = devMode;
|
||||
this.ivy = ivy;
|
||||
|
|
|
|||
86
devtools/projects/ng-devtools/src/lib/devtools_spec.ts
Normal file
86
devtools/projects/ng-devtools/src/lib/devtools_spec.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Component} from '@angular/core';
|
||||
import {ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import {FrameManager} from './frame_manager';
|
||||
import {DevToolsComponent} from './devtools.component';
|
||||
import {DevToolsTabsComponent} from './devtools-tabs/devtools-tabs.component';
|
||||
import {MessageBus} from 'protocol';
|
||||
|
||||
@Component({
|
||||
selector: 'ng-devtools-tabs',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
export class MockNgDevToolsTabs {}
|
||||
|
||||
describe('DevtoolsComponent', () => {
|
||||
let fixture: ComponentFixture<DevToolsComponent>;
|
||||
let component: DevToolsComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
const mockMessageBus = jasmine.createSpyObj('MessageBus', ['on', 'emit', 'once']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [{provide: MessageBus, useValue: mockMessageBus}],
|
||||
}).overrideComponent(DevToolsComponent, {
|
||||
remove: {imports: [DevToolsTabsComponent], providers: [FrameManager]},
|
||||
add: {
|
||||
imports: [MockNgDevToolsTabs],
|
||||
providers: [{provide: FrameManager, useFactory: () => FrameManager.initialize(123)}],
|
||||
},
|
||||
});
|
||||
|
||||
fixture = TestBed.createComponent(DevToolsComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should render ng devtools tabs when Angular Status is EXISTS and is in dev mode and is supported version', () => {
|
||||
component.angularStatus = component.AngularStatus.EXISTS;
|
||||
component.angularIsInDevMode = true;
|
||||
component.angularVersion = '0.0.0';
|
||||
component.ivy = true;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('ng-devtools-tabs')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render Angular Devtools dev mode only support text when Angular Status is EXISTS and is angular is not in dev mode', () => {
|
||||
component.angularStatus = component.AngularStatus.EXISTS;
|
||||
component.angularIsInDevMode = false;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.devtools').textContent).toContain(
|
||||
'We detected an application built with production configuration. Angular DevTools only supports development build.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render version support message when Angular Status is EXISTS and angular version is not supported', () => {
|
||||
component.angularStatus = component.AngularStatus.EXISTS;
|
||||
component.angularIsInDevMode = true;
|
||||
component.angularVersion = '1.0.0';
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.devtools').textContent).toContain(
|
||||
'Angular Devtools only supports Angular versions 12 and above',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render Angular application not detected when Angular Status is DOES_NOT_EXIST', () => {
|
||||
component.angularStatus = component.AngularStatus.DOES_NOT_EXIST;
|
||||
fixture.detectChanges();
|
||||
// expect the text to be "Angular application not detected"
|
||||
expect(fixture.nativeElement.querySelector('.not-detected').textContent).toContain(
|
||||
'Angular application not detected',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render loading svg when Angular Status is UNKNOWN', () => {
|
||||
component.angularStatus = component.AngularStatus.UNKNOWN;
|
||||
fixture.detectChanges();
|
||||
expect(fixture.nativeElement.querySelector('.loading svg')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
133
devtools/projects/ng-devtools/src/lib/frame_manager.ts
Normal file
133
devtools/projects/ng-devtools/src/lib/frame_manager.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Injectable, inject} from '@angular/core';
|
||||
import {Events, MessageBus} from 'protocol';
|
||||
|
||||
import {Frame, TOP_LEVEL_FRAME_ID} from './application-environment';
|
||||
|
||||
@Injectable()
|
||||
export class FrameManager {
|
||||
private _selectedFrameId: number | null = null;
|
||||
private _frames = new Map<number, Frame>();
|
||||
private _inspectedWindowTabId: number | null = null;
|
||||
private _frameUrlToFrameIds = new Map<string, Set<number>>();
|
||||
private _messageBus = inject<MessageBus<Events>>(MessageBus);
|
||||
|
||||
get frames(): Frame[] {
|
||||
return Array.from(this._frames.values());
|
||||
}
|
||||
|
||||
get selectedFrame(): Frame | null {
|
||||
if (this._selectedFrameId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._frames.get(this._selectedFrameId) ?? null;
|
||||
}
|
||||
|
||||
static initialize(inspectedWindowTabIdTestOnly?: number | null) {
|
||||
const manager = new FrameManager();
|
||||
manager.initialize(inspectedWindowTabIdTestOnly);
|
||||
return manager;
|
||||
}
|
||||
|
||||
private initialize(inspectedWindowTabIdTestOnly?: number | null): void {
|
||||
if (inspectedWindowTabIdTestOnly === undefined) {
|
||||
this._inspectedWindowTabId = globalThis.chrome.devtools.inspectedWindow.tabId;
|
||||
} else {
|
||||
this._inspectedWindowTabId = inspectedWindowTabIdTestOnly;
|
||||
}
|
||||
|
||||
this._messageBus.on('frameConnected', (frameId: number) => {
|
||||
if (this._frames.has(frameId)) {
|
||||
this._selectedFrameId = frameId;
|
||||
}
|
||||
});
|
||||
|
||||
this._messageBus.on('contentScriptConnected', (frameId: number, name: string, url: string) => {
|
||||
// fragments are not considered when doing URL matching on a page
|
||||
// https://bugs.chromium.org/p/chromium/issues/detail?id=841429
|
||||
const urlWithoutHash = new URL(url);
|
||||
urlWithoutHash.hash = '';
|
||||
|
||||
this.addFrame({name, id: frameId, url: urlWithoutHash});
|
||||
|
||||
if (this.frames.length === 1) {
|
||||
this.inspectFrame(this._frames.get(frameId)!);
|
||||
}
|
||||
});
|
||||
|
||||
this._messageBus.on('contentScriptDisconnected', (frameId: number) => {
|
||||
if (!this._frames.has(frameId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeFrame(this._frames.get(frameId)!);
|
||||
|
||||
// Defensive check. This case should never happen, since we're always connected to at least
|
||||
// the top level frame.
|
||||
if (this.frames.length === 0) {
|
||||
this._selectedFrameId = null;
|
||||
console.error('Angular DevTools is not connected to any frames.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameId === this._selectedFrameId) {
|
||||
this._selectedFrameId = TOP_LEVEL_FRAME_ID;
|
||||
this.inspectFrame(this._frames.get(this._selectedFrameId!)!);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isSelectedFrame(frame: Frame): boolean {
|
||||
return this._selectedFrameId === frame.id;
|
||||
}
|
||||
|
||||
inspectFrame(frame: Frame): void {
|
||||
if (this._inspectedWindowTabId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._frames.has(frame.id)) {
|
||||
throw new Error('Attempted to inspect a frame that is not connected to Angular DevTools.');
|
||||
}
|
||||
|
||||
this._selectedFrameId = null;
|
||||
this._messageBus.emit('enableFrameConnection', [frame.id, this._inspectedWindowTabId]);
|
||||
}
|
||||
|
||||
frameHasUniqueUrl(frame: Frame | null): boolean {
|
||||
if (frame === null) {
|
||||
return false;
|
||||
}
|
||||
const frameUrl = frame.url.toString();
|
||||
const frameIds = this._frameUrlToFrameIds.get(frameUrl) ?? new Set<number>();
|
||||
return frameIds.size === 1;
|
||||
}
|
||||
|
||||
private addFrame(frame: Frame): void {
|
||||
this._frames.set(frame.id, frame);
|
||||
const frameUrl = frame.url.toString();
|
||||
const frameIdSet = this._frameUrlToFrameIds.get(frameUrl) ?? new Set<number>();
|
||||
frameIdSet.add(frame.id);
|
||||
this._frameUrlToFrameIds.set(frameUrl, frameIdSet);
|
||||
}
|
||||
|
||||
private removeFrame(frame: Frame): void {
|
||||
const frameId = frame.id;
|
||||
const frameUrl = frame.url.toString();
|
||||
const urlFrameIds = this._frameUrlToFrameIds.get(frameUrl) ?? new Set<number>();
|
||||
urlFrameIds.delete(frameId);
|
||||
if (urlFrameIds.size === 0) {
|
||||
this._frameUrlToFrameIds.delete(frameUrl);
|
||||
}
|
||||
this._frames.delete(frameId);
|
||||
}
|
||||
}
|
||||
195
devtools/projects/ng-devtools/src/lib/frame_manager_spec.ts
Normal file
195
devtools/projects/ng-devtools/src/lib/frame_manager_spec.ts
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright Google LLC All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Events, MessageBus} from 'protocol';
|
||||
import {FrameManager} from './frame_manager';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
import {Frame} from './application-environment';
|
||||
|
||||
describe('FrameManager', () => {
|
||||
let frameManager: FrameManager;
|
||||
let messageBus: MessageBus<Events>;
|
||||
let topicToCallback: {[topic: string]: Function | null};
|
||||
|
||||
function getFrameFromFrameManager(frameId: number): Frame | undefined {
|
||||
return frameManager.frames.find((f: Frame) => f.id === frameId);
|
||||
}
|
||||
|
||||
function frameConnected(frameId: number): void {
|
||||
topicToCallback['frameConnected']!(frameId);
|
||||
}
|
||||
|
||||
function contentScriptConnected(frameId: number, name: string, url: string): void {
|
||||
topicToCallback['contentScriptConnected']!(frameId, name, url);
|
||||
}
|
||||
|
||||
function contentScriptDisconnected(frameId: number): void {
|
||||
topicToCallback['contentScriptDisconnected']!(frameId);
|
||||
}
|
||||
|
||||
const topLevelFrameId = 0;
|
||||
const otherFrameId = 1;
|
||||
const tabId = 123;
|
||||
|
||||
beforeEach(() => {
|
||||
topicToCallback = {
|
||||
frameConnected: null,
|
||||
contentScriptConnected: null,
|
||||
contentScriptDisconnected: null,
|
||||
};
|
||||
messageBus = jasmine.createSpyObj('MessageBus', ['on', 'emit']);
|
||||
|
||||
(messageBus.on as any).and.callFake((topic: string, cb: Function) => {
|
||||
topicToCallback[topic] = cb;
|
||||
});
|
||||
|
||||
(messageBus.emit as any).and.callFake((topic: string, args: any[]) => {
|
||||
if (topic === 'enableFrameConnection') {
|
||||
frameConnected(args[0]);
|
||||
}
|
||||
});
|
||||
|
||||
const testModule = TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{provide: MessageBus, useValue: messageBus},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(123)},
|
||||
],
|
||||
});
|
||||
|
||||
frameManager = testModule.inject(FrameManager);
|
||||
});
|
||||
|
||||
it('should add frame when contentScriptConnected event is emitted', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
expect(frameManager.frames.length).toBe(1);
|
||||
expect(frameManager.frames[0].id).toBe(topLevelFrameId);
|
||||
expect(frameManager.frames[0].name).toBe('name');
|
||||
expect(frameManager.frames[0].url.toString()).toBe('http://localhost:4200/url');
|
||||
});
|
||||
|
||||
it('should set the selected frame to the first frame when there is only one frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('should set selected frame when frameConnected event is emitted', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
});
|
||||
|
||||
it('should remove frame when contentScriptDisconnected event is emitted', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
expect(frameManager.frames.length).toBe(2);
|
||||
contentScriptDisconnected(otherFrameId);
|
||||
expect(frameManager.frames.length).toBe(1);
|
||||
expect(frameManager.frames[0].id).toBe(topLevelFrameId);
|
||||
|
||||
const errorSpy = spyOn(console, 'error');
|
||||
contentScriptDisconnected(topLevelFrameId);
|
||||
expect(frameManager.frames.length).toBe(0);
|
||||
expect(errorSpy).toHaveBeenCalledWith('Angular DevTools is not connected to any frames.');
|
||||
});
|
||||
|
||||
it('should set selected frame to top level frame when contentScriptDisconnected event is emitted for selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
contentScriptDisconnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('should not set selected frame to top level frame when contentScriptDisconnected event is emitted for non selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(topLevelFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
contentScriptDisconnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('should not set selected frame to top level frame when contentScriptDisconnected event is emitted for non existing frame', () => {
|
||||
const nonExistingFrameId = 3;
|
||||
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
frameConnected(otherFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
contentScriptDisconnected(nonExistingFrameId);
|
||||
expect(frameManager.selectedFrame?.id).toBe(otherFrameId);
|
||||
});
|
||||
|
||||
it('isSelectedFrame should return true when frame matches selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
const otherFrame = getFrameFromFrameManager(otherFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
expect(otherFrame).toBeDefined();
|
||||
expect(frameManager.isSelectedFrame(topLevelFrame!)).toBe(true);
|
||||
});
|
||||
|
||||
it('isSelectedFrame should return false when frame does not match selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'http://localhost:4200/url2');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
const otherFrame = getFrameFromFrameManager(otherFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
expect(otherFrame).toBeDefined();
|
||||
expect(frameManager.isSelectedFrame(otherFrame!)).toBe(false);
|
||||
});
|
||||
|
||||
it('inspectFrame should emit enableFrameConnection message', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
frameManager.inspectFrame(topLevelFrame!);
|
||||
expect(messageBus.emit).toHaveBeenCalledWith('enableFrameConnection', [topLevelFrameId, tabId]);
|
||||
});
|
||||
|
||||
it('inspectFrame should set selected frame', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'http://localhost:4200/url');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'https://angular.dev/');
|
||||
const topLevelFrame = getFrameFromFrameManager(topLevelFrameId);
|
||||
expect(topLevelFrame).toBeDefined();
|
||||
frameManager.inspectFrame(topLevelFrame!);
|
||||
expect(frameManager.selectedFrame?.id).toBe(topLevelFrameId);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should return false when a two frames have the same url', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/');
|
||||
contentScriptConnected(otherFrameId, 'name2', 'https://angular.dev/');
|
||||
expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/');
|
||||
expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(false);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should return true when only one frame has a given url', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/');
|
||||
contentScriptConnected(otherFrameId, 'name', 'https://angular.dev/overview');
|
||||
expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/');
|
||||
expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(true);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should not consider url fragments as part of the url comparison', () => {
|
||||
contentScriptConnected(topLevelFrameId, 'name', 'https://angular.dev/guide/components');
|
||||
contentScriptConnected(
|
||||
otherFrameId,
|
||||
'name',
|
||||
'https://angular.dev/guide/components#using-components',
|
||||
);
|
||||
expect(frameManager.selectedFrame?.url.toString()).toBe('https://angular.dev/guide/components');
|
||||
expect(frameManager.frameHasUniqueUrl(frameManager.selectedFrame!)).toBe(false);
|
||||
});
|
||||
|
||||
it('frameHasUniqueUrl should return false when frame is null', () => {
|
||||
expect(frameManager.frameHasUniqueUrl(null)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -16,12 +16,14 @@ import {ChromeApplicationEnvironment} from './chrome-application-environment';
|
|||
import {ChromeApplicationOperations} from './chrome-application-operations';
|
||||
import {ZoneAwareChromeMessageBus} from './zone-aware-chrome-message-bus';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
import {FrameManager} from '../../../../projects/ng-devtools/src/lib/frame_manager';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserAnimationsModule, DevToolsComponent, MatSelect],
|
||||
bootstrap: [AppComponent],
|
||||
providers: [
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize()},
|
||||
{
|
||||
provide: ApplicationOperations,
|
||||
useClass: ChromeApplicationOperations,
|
||||
|
|
|
|||
|
|
@ -36,18 +36,18 @@ class MockPort {
|
|||
this.sender = properties.sender;
|
||||
}
|
||||
|
||||
postMessage(message: any) {
|
||||
postMessage(message: any): void {
|
||||
this.messagesPosted.push(message);
|
||||
}
|
||||
|
||||
onMessage = {
|
||||
addListener: (listener: Function) => {
|
||||
addListener: (listener: Function): void => {
|
||||
this.onMessageListeners.push(listener);
|
||||
},
|
||||
};
|
||||
|
||||
onDisconnect = {
|
||||
addListener: (listener: Function) => {
|
||||
addListener: (listener: Function): void => {
|
||||
this.onDisconnectListeners.push(listener);
|
||||
},
|
||||
};
|
||||
|
|
@ -72,22 +72,27 @@ function mockSpyProperty(obj: any, property: string, value: any) {
|
|||
describe('Tab Manager - ', () => {
|
||||
let tabs: Tabs;
|
||||
const tabId = 12345;
|
||||
// let devtoolsPort: MockPort;
|
||||
let chromeRuntime: jasmine.SpyObj<typeof chrome.runtime>;
|
||||
let tabManager: TabManager;
|
||||
let tab: DevToolsConnection;
|
||||
let chromeRuntimeOnConnectListeners: ((port: MockPort) => void)[] = [];
|
||||
|
||||
function connectToChromeRuntime(port: MockPort) {
|
||||
chromeRuntimeOnConnectListeners.forEach((listener) => listener(port));
|
||||
function connectToChromeRuntime(port: MockPort): void {
|
||||
for (const listener of chromeRuntimeOnConnectListeners) {
|
||||
listener(port);
|
||||
}
|
||||
}
|
||||
|
||||
function emitMessageToPort(port: MockPort, message: any) {
|
||||
port.onMessageListeners.forEach((listener) => listener(message));
|
||||
function emitMessageToPort(port: MockPort, message: any): void {
|
||||
for (const listener of port.onMessageListeners) {
|
||||
listener(message);
|
||||
}
|
||||
}
|
||||
|
||||
function emitDisconnectToPort(port: MockPort) {
|
||||
port.onDisconnectListeners.forEach((listener) => listener());
|
||||
for (const listener of port.onDisconnectListeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function createDevToolsPort() {
|
||||
|
|
@ -131,6 +136,7 @@ describe('Tab Manager - ', () => {
|
|||
},
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
emitMessageToPort(port, {topic: 'backendReady'});
|
||||
return port;
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +158,7 @@ describe('Tab Manager - ', () => {
|
|||
});
|
||||
|
||||
it('should set frame connection as enabled when an enableFrameConnection message is recieved', () => {
|
||||
createContentScriptPort();
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
|
|
@ -192,24 +198,27 @@ describe('Tab Manager - ', () => {
|
|||
it('should not pipe messages from the content script and devtools script to each other when the content script frame is disabled', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false);
|
||||
|
||||
emitMessageToPort(contentScriptPort, TEST_MESSAGE_ONE);
|
||||
expect(contentScriptPort.messagesPosted.length).toBe(0);
|
||||
assertArrayDoesNotHaveObj(contentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO);
|
||||
expect(devtoolsPort.messagesPosted.length).toBe(0);
|
||||
assertArrayDoesNotHaveObj(devtoolsPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
});
|
||||
|
||||
it('should set backendReady when the contentPort recieves the backendReady message', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
emitMessageToPort(contentScriptPort, {topic: 'backendReady'});
|
||||
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.backendReady).toBe(true);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'contentScriptConnected',
|
||||
|
|
@ -220,6 +229,7 @@ describe('Tab Manager - ', () => {
|
|||
it('should set tab.devtools to null when the devtoolsPort disconnects', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
|
|
@ -311,6 +321,7 @@ describe('Tab Manager - ', () => {
|
|||
const devtoolsPort = createDevToolsPort();
|
||||
createTopLevelContentScriptPort();
|
||||
createChildContentScriptPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab?.contentScripts[topLevelFrameId]?.enabled).toBe(false);
|
||||
expect(tab?.contentScripts[childFrameId]?.enabled).toBe(false);
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ ng_module(
|
|||
],
|
||||
deps = [
|
||||
"//devtools/projects/ng-devtools",
|
||||
"//devtools/projects/ng-devtools/src/lib:frame_manager",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/src:iframe_message_bus",
|
||||
"//packages/common",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
*/
|
||||
|
||||
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
|
||||
import {IFrameMessageBus} from '../../iframe-message-bus';
|
||||
import {DevToolsComponent} from 'ng-devtools';
|
||||
|
|
@ -15,21 +14,6 @@ import {DevToolsComponent} from 'ng-devtools';
|
|||
@Component({
|
||||
templateUrl: './devtools-app.component.html',
|
||||
styleUrls: ['./devtools-app.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
return new PriorityAwareMessageBus(
|
||||
new IFrameMessageBus(
|
||||
'angular-devtools',
|
||||
'angular-devtools-backend',
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
imports: [DevToolsComponent],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ import {NgModule} from '@angular/core';
|
|||
import {RouterModule} from '@angular/router';
|
||||
|
||||
import {AppDevToolsComponent} from './devtools-app.component';
|
||||
import {FrameManager} from '../../../projects/ng-devtools/src/lib/frame_manager';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
import {IFrameMessageBus} from '../../../src/iframe-message-bus';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
|
@ -24,5 +27,21 @@ import {AppDevToolsComponent} from './devtools-app.component';
|
|||
]),
|
||||
AppDevToolsComponent,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(): MessageBus<Events> {
|
||||
return new PriorityAwareMessageBus(
|
||||
new IFrameMessageBus(
|
||||
'angular-devtools',
|
||||
'angular-devtools-backend',
|
||||
// tslint:disable-next-line: no-non-null-assertion
|
||||
() => (document.querySelector('#sample-app') as HTMLIFrameElement).contentWindow!,
|
||||
),
|
||||
);
|
||||
},
|
||||
},
|
||||
{provide: FrameManager, useFactory: () => FrameManager.initialize(null)},
|
||||
],
|
||||
})
|
||||
export class DevToolsModule {}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {ApplicationEnvironment, Environment} from 'ng-devtools';
|
|||
import {environment} from './environments/environment';
|
||||
|
||||
export class DemoApplicationEnvironment extends ApplicationEnvironment {
|
||||
frameSelectorEnabled = false;
|
||||
override get environment(): Environment {
|
||||
return environment;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue