mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Revert "refactor(devtools): implement iframe support for Angular DevTools' browser code (#53934)" (#54629)
This reverts commit dd3dac9cc9.
PR Close #54629
This commit is contained in:
@@ -213,19 +213,6 @@ export interface Route {
|
||||
isAux: boolean;
|
||||
}
|
||||
|
||||
export interface AngularDetection {
|
||||
// This is necessary because the runtime
|
||||
// message listener handles messages globally
|
||||
// including from other extensions. We don't
|
||||
// want to set icon and/or popup based on
|
||||
// a message coming from an unrelated extension.
|
||||
isAngularDevTools: true;
|
||||
isIvy: boolean;
|
||||
isAngular: boolean;
|
||||
isDebugMode: boolean;
|
||||
isSupportedAngularVersion: boolean;
|
||||
}
|
||||
|
||||
export type Topic = keyof Events;
|
||||
|
||||
export interface InjectorGraphViewQuery {
|
||||
@@ -286,13 +273,4 @@ export interface Events {
|
||||
) => void;
|
||||
|
||||
logProvider: (injector: SerializedInjector, providers: SerializedProviderRecord) => void;
|
||||
|
||||
contentScriptConnected: (frameId: number, name: string, url: string) => void;
|
||||
contentScriptDisconnected: (frameId: number, name: string, url: string) => void;
|
||||
enableFrameConnection: (frameId: number, tabId: number) => void;
|
||||
frameConnected: (frameId: number) => void;
|
||||
detectAngular: (detectionResult: AngularDetection) => void;
|
||||
backendReady: () => void;
|
||||
|
||||
log: (logEvent: {message: string; level: 'log' | 'warn' | 'debug' | 'error'}) => void;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_binary")
|
||||
load("//devtools/tools:ng_module.bzl", "ng_module")
|
||||
load("//devtools/tools:typescript.bzl", "ts_library", "ts_test_library")
|
||||
load("//devtools/tools:typescript.bzl", "ts_library")
|
||||
load("//tools:defaults.bzl", "esbuild")
|
||||
load("//devtools/tools:defaults.bzl", "karma_web_test_suite")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
@@ -16,6 +15,7 @@ ng_module(
|
||||
srcs = [
|
||||
"app.component.ts",
|
||||
"app.module.ts",
|
||||
"inject.ts",
|
||||
],
|
||||
angular_assets = [
|
||||
"app.component.html",
|
||||
@@ -35,8 +35,6 @@ ng_module(
|
||||
"//packages/core",
|
||||
"//packages/platform-browser",
|
||||
"//packages/platform-browser/animations",
|
||||
"@npm//@angular/material",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -80,10 +78,8 @@ ts_library(
|
||||
"chrome-application-operations.ts",
|
||||
],
|
||||
deps = [
|
||||
":chrome_application_environment",
|
||||
"//devtools/projects/ng-devtools",
|
||||
"//devtools/projects/protocol",
|
||||
"//packages/core",
|
||||
"@npm//@types",
|
||||
],
|
||||
)
|
||||
@@ -130,38 +126,7 @@ ts_library(
|
||||
srcs = [
|
||||
"background.ts",
|
||||
],
|
||||
deps = [
|
||||
":tab_manager",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/projects/shell-browser/src/app:detect_angular_for_extension_icon",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "tab_manager",
|
||||
srcs = [
|
||||
"tab_manager.ts",
|
||||
],
|
||||
deps = [
|
||||
"//devtools/projects/protocol",
|
||||
],
|
||||
)
|
||||
|
||||
karma_web_test_suite(
|
||||
name = "tab_manager_test",
|
||||
deps = [
|
||||
":tab_manager_test_lib",
|
||||
],
|
||||
)
|
||||
|
||||
ts_test_library(
|
||||
name = "tab_manager_test_lib",
|
||||
srcs = [
|
||||
"tab_manager_spec.ts",
|
||||
],
|
||||
deps = [
|
||||
":tab_manager",
|
||||
],
|
||||
deps = ["//devtools/projects/shell-browser/src/app:detect_angular_for_extension_icon"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
@@ -197,8 +162,6 @@ ts_library(
|
||||
"detect-angular-for-extension-icon.ts",
|
||||
],
|
||||
deps = [
|
||||
":same_page_message_bus",
|
||||
"//devtools/projects/protocol",
|
||||
"//devtools/projects/shared-utils",
|
||||
"@npm//@types",
|
||||
],
|
||||
|
||||
@@ -6,21 +6,39 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
|
||||
import {ChangeDetectorRef, Component, NgZone, OnInit} from '@angular/core';
|
||||
import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
|
||||
import {injectScripts} from './inject';
|
||||
import {ZoneAwareChromeMessageBus} from './zone-aware-chrome-message-bus';
|
||||
import {DevToolsComponent} from 'ng-devtools';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
providers: [
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(ngZone: NgZone): MessageBus<Events> {
|
||||
const port = chrome.runtime.connect({
|
||||
name: '' + chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
return new PriorityAwareMessageBus(new ZoneAwareChromeMessageBus(port, ngZone));
|
||||
},
|
||||
deps: [NgZone],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
private _cd = inject(ChangeDetectorRef);
|
||||
constructor(private _cd: ChangeDetectorRef) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
chrome.devtools.network.onNavigated.addListener(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
|
||||
injectScripts(['app/backend_bundle.js']);
|
||||
this._cd.detectChanges();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,20 +6,17 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {NgModule, NgZone} from '@angular/core';
|
||||
import {MatSelect} from '@angular/material/select';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
|
||||
import {ApplicationEnvironment, ApplicationOperations, DevToolsComponent} from 'ng-devtools';
|
||||
|
||||
import {AppComponent} from './app.component';
|
||||
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';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [BrowserAnimationsModule, DevToolsComponent, MatSelect],
|
||||
imports: [BrowserAnimationsModule, DevToolsComponent],
|
||||
bootstrap: [AppComponent],
|
||||
providers: [
|
||||
{
|
||||
@@ -30,17 +27,6 @@ import {Events, MessageBus, PriorityAwareMessageBus} from 'protocol';
|
||||
provide: ApplicationEnvironment,
|
||||
useClass: ChromeApplicationEnvironment,
|
||||
},
|
||||
{
|
||||
provide: MessageBus,
|
||||
useFactory(ngZone: NgZone): MessageBus<Events> {
|
||||
const port = chrome.runtime.connect({
|
||||
name: '' + chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
|
||||
return new PriorityAwareMessageBus(new ZoneAwareChromeMessageBus(port, ngZone));
|
||||
},
|
||||
deps: [NgZone],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {initializeExtendedWindowOperations} from './chrome-window-extensions';
|
||||
import {SamePageMessageBus} from './same-page-message-bus';
|
||||
|
||||
const messageBus = new SamePageMessageBus(
|
||||
`angular-devtools-backend-${location.href}`,
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
'angular-devtools-backend',
|
||||
'angular-devtools-content-script',
|
||||
);
|
||||
|
||||
let initialized = false;
|
||||
@@ -47,6 +47,4 @@ messageBus.on('handshake', () => {
|
||||
},
|
||||
false,
|
||||
);
|
||||
|
||||
messageBus.emit('backendReady');
|
||||
});
|
||||
|
||||
@@ -8,10 +8,164 @@
|
||||
|
||||
/// <reference types="chrome"/>
|
||||
|
||||
import {AngularDetection} from 'protocol';
|
||||
import {TabManager, Tabs} from './tab_manager';
|
||||
import {AngularDetection} from './detect-angular-for-extension-icon';
|
||||
|
||||
function getPopUpName(ng: AngularDetection): string {
|
||||
const isManifestV3 = chrome.runtime.getManifest().manifest_version === 3;
|
||||
|
||||
const browserAction = (() => {
|
||||
// Electron does not expose browserAction object,
|
||||
// Use empty calls as fallback if they are not defined.
|
||||
const noopAction = {setIcon: () => {}, setPopup: () => {}};
|
||||
|
||||
if (isManifestV3) {
|
||||
return chrome.action || noopAction;
|
||||
}
|
||||
|
||||
return chrome.browserAction || noopAction;
|
||||
})();
|
||||
|
||||
// By default use the black and white icon.
|
||||
// Replace it only when we detect an Angular app.
|
||||
browserAction.setIcon(
|
||||
{
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon-bw16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon-bw48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon-bw128.png`),
|
||||
},
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
const ports: {
|
||||
[tab: string]:
|
||||
| {
|
||||
'content-script': chrome.runtime.Port | null;
|
||||
devtools: chrome.runtime.Port | null;
|
||||
}
|
||||
| undefined;
|
||||
} = {};
|
||||
|
||||
chrome.runtime.onConnect.addListener((port) => {
|
||||
let tab: string | null = null;
|
||||
let name: 'devtools' | 'content-script' | null = null;
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Connection event in the background script');
|
||||
|
||||
if (isNumeric(port.name)) {
|
||||
tab = port.name;
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Angular devtools connected, injecting the content script', port.name, ports[tab]);
|
||||
|
||||
name = 'devtools';
|
||||
installContentScript(parseInt(port.name, 10));
|
||||
} else {
|
||||
if (!port.sender || !port.sender.tab) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error('Unable to access the port sender and sender tab');
|
||||
|
||||
return;
|
||||
}
|
||||
if (port.sender.tab.id === undefined) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error('Sender tab id is undefined');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Content script connected', port.sender.tab.id);
|
||||
tab = port.sender.tab.id.toString();
|
||||
name = 'content-script';
|
||||
}
|
||||
|
||||
let portsTab = ports[tab];
|
||||
if (!portsTab) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Creating a tab port');
|
||||
|
||||
portsTab = ports[tab] = {
|
||||
devtools: null,
|
||||
'content-script': null,
|
||||
};
|
||||
}
|
||||
|
||||
portsTab[name] = port;
|
||||
|
||||
if (portsTab.devtools && portsTab['content-script']) {
|
||||
doublePipe(portsTab.devtools, portsTab['content-script'], tab);
|
||||
}
|
||||
});
|
||||
|
||||
const isNumeric = (str: string): boolean => {
|
||||
return +str + '' === str;
|
||||
};
|
||||
|
||||
const installContentScript = (tabId: number) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Installing the content-script');
|
||||
|
||||
// We first inject the content-script and after that
|
||||
// invoke the global that it exposes.
|
||||
|
||||
if (isManifestV3) {
|
||||
chrome.scripting.executeScript(
|
||||
{files: ['app/content_script_bundle.js'], target: {tabId}},
|
||||
() => {
|
||||
chrome.scripting.executeScript({func: () => (globalThis as any).main(), target: {tabId}});
|
||||
},
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// manifest V2 APIs
|
||||
chrome.tabs.executeScript(tabId, {file: 'app/content_script_bundle.js'}, (result) => {
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
code: 'globalThis.main()',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const doublePipe = (
|
||||
devtoolsPort: chrome.runtime.Port | null,
|
||||
contentScriptPort: chrome.runtime.Port,
|
||||
tab: string,
|
||||
) => {
|
||||
if (devtoolsPort === null) {
|
||||
console.warn('DevTools port is equal to null');
|
||||
return;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Creating two-way communication channel', Date.now(), ports);
|
||||
|
||||
const onDevToolsMessage = (message: chrome.runtime.Port) => {
|
||||
contentScriptPort.postMessage(message);
|
||||
};
|
||||
devtoolsPort.onMessage.addListener(onDevToolsMessage);
|
||||
|
||||
const onContentScriptMessage = (message: chrome.runtime.Port) => {
|
||||
devtoolsPort.postMessage(message);
|
||||
};
|
||||
contentScriptPort.onMessage.addListener(onContentScriptMessage);
|
||||
|
||||
const shutdown = (source: string) => {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Disconnecting', source);
|
||||
|
||||
devtoolsPort.onMessage.removeListener(onDevToolsMessage);
|
||||
contentScriptPort.onMessage.removeListener(onContentScriptMessage);
|
||||
devtoolsPort.disconnect();
|
||||
contentScriptPort.disconnect();
|
||||
ports[tab] = undefined;
|
||||
};
|
||||
devtoolsPort.onDisconnect.addListener(shutdown.bind(null, 'devtools'));
|
||||
contentScriptPort.onDisconnect.addListener(shutdown.bind(null, 'content-script'));
|
||||
};
|
||||
|
||||
const getPopUpName = (ng: AngularDetection) => {
|
||||
if (!ng.isAngular) {
|
||||
return 'not-angular.html';
|
||||
}
|
||||
@@ -22,63 +176,29 @@ function getPopUpName(ng: AngularDetection): string {
|
||||
return 'production.html';
|
||||
}
|
||||
return 'supported.html';
|
||||
}
|
||||
};
|
||||
|
||||
if (chrome !== undefined && chrome.runtime !== undefined) {
|
||||
const isManifestV3 = chrome.runtime.getManifest().manifest_version === 3;
|
||||
|
||||
const browserAction = (() => {
|
||||
// Electron does not expose browserAction object,
|
||||
// Use empty calls as fallback if they are not defined.
|
||||
const noopAction = {setIcon: () => {}, setPopup: () => {}};
|
||||
|
||||
if (isManifestV3) {
|
||||
return chrome.action || noopAction;
|
||||
}
|
||||
|
||||
return chrome.browserAction || noopAction;
|
||||
})();
|
||||
|
||||
// By default use the black and white icon.
|
||||
// Replace it only when we detect an Angular app.
|
||||
browserAction.setIcon(
|
||||
{
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon-bw16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon-bw48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon-bw128.png`),
|
||||
},
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
chrome.runtime.onMessage.addListener((req: AngularDetection, sender) => {
|
||||
if (!req.isAngularDevTools) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender && sender.tab) {
|
||||
browserAction.setPopup({
|
||||
chrome.runtime.onMessage.addListener((req: AngularDetection, sender) => {
|
||||
if (!req.isAngularDevTools) {
|
||||
return;
|
||||
}
|
||||
if (sender && sender.tab) {
|
||||
browserAction.setPopup({
|
||||
tabId: sender.tab.id,
|
||||
popup: `popups/${getPopUpName(req)}`,
|
||||
});
|
||||
}
|
||||
if (sender && sender.tab && req.isAngular) {
|
||||
browserAction.setIcon(
|
||||
{
|
||||
tabId: sender.tab.id,
|
||||
popup: `popups/${getPopUpName(req)}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (sender && sender.tab && req.isAngular) {
|
||||
browserAction.setIcon(
|
||||
{
|
||||
tabId: sender.tab.id,
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon128.png`),
|
||||
},
|
||||
path: {
|
||||
16: chrome.runtime.getURL(`assets/icon16.png`),
|
||||
48: chrome.runtime.getURL(`assets/icon48.png`),
|
||||
128: chrome.runtime.getURL(`assets/icon128.png`),
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const tabs = {};
|
||||
TabManager.initialize(tabs);
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,8 +11,6 @@ import {ApplicationEnvironment, Environment} from 'ng-devtools';
|
||||
import {environment} from '../environments/environment';
|
||||
|
||||
export class ChromeApplicationEnvironment extends ApplicationEnvironment {
|
||||
frameSelectorEnabled = true;
|
||||
|
||||
override get environment(): Environment {
|
||||
return environment;
|
||||
}
|
||||
|
||||
@@ -11,29 +11,32 @@
|
||||
import {ApplicationOperations} from 'ng-devtools';
|
||||
import {DirectivePosition, ElementPosition} from 'protocol';
|
||||
|
||||
function runInInspectedWindow(script: string, frameURL?: URL): void {
|
||||
chrome.devtools.inspectedWindow.eval(script, {frameURL: frameURL?.toString?.()});
|
||||
}
|
||||
|
||||
export class ChromeApplicationOperations extends ApplicationOperations {
|
||||
override viewSource(position: ElementPosition, directiveIndex?: number, target?: URL): void {
|
||||
const viewSource = `inspect(inspectedApplication.findConstructorByPosition('${position}', ${directiveIndex}))`;
|
||||
runInInspectedWindow(viewSource, target);
|
||||
override viewSource(position: ElementPosition, directiveIndex: number): void {
|
||||
if (chrome.devtools) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`inspect(inspectedApplication.findConstructorByPosition('${position}', ${directiveIndex}))`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
override selectDomElement(position: ElementPosition, target?: URL): void {
|
||||
const selectDomElement = `inspect(inspectedApplication.findDomElementByPosition('${position}'))`;
|
||||
runInInspectedWindow(selectDomElement, target);
|
||||
override selectDomElement(position: ElementPosition): void {
|
||||
if (chrome.devtools) {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`inspect(inspectedApplication.findDomElementByPosition('${position}'))`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
override inspect(directivePosition: DirectivePosition, objectPath: string[], target?: URL): void {
|
||||
const args = {
|
||||
directivePosition,
|
||||
objectPath,
|
||||
};
|
||||
const inspect = `inspect(inspectedApplication.findPropertyByPosition('${JSON.stringify(
|
||||
args,
|
||||
)}'))`;
|
||||
runInInspectedWindow(inspect, target);
|
||||
override inspect(directivePosition: DirectivePosition, objectPath: string[]): void {
|
||||
if (chrome.devtools) {
|
||||
const args = {
|
||||
directivePosition,
|
||||
objectPath,
|
||||
};
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`inspect(inspectedApplication.findPropertyByPosition('${JSON.stringify(args)}'))`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,92 +9,59 @@
|
||||
import {ChromeMessageBus} from './chrome-message-bus';
|
||||
import {SamePageMessageBus} from './same-page-message-bus';
|
||||
|
||||
let backgroundDisconnected = false;
|
||||
let backendInstalled = false;
|
||||
let backendInitialized = false;
|
||||
export const main = () => {
|
||||
let backgroundDisconnected = false;
|
||||
let backendInitialized = false;
|
||||
|
||||
const port = chrome.runtime.connect({
|
||||
name: `${document.title || location.href}`,
|
||||
});
|
||||
// console.log('Content script executing', (window as any));
|
||||
|
||||
const handleDisconnect = (): void => {
|
||||
// console.log('Background disconnected', new Date());
|
||||
localMessageBus.emit('shutdown');
|
||||
localMessageBus.destroy();
|
||||
chromeMessageBus.destroy();
|
||||
backgroundDisconnected = true;
|
||||
};
|
||||
const port = chrome.runtime.connect({
|
||||
name: 'content-script',
|
||||
});
|
||||
|
||||
port.onDisconnect.addListener(handleDisconnect);
|
||||
|
||||
const detectAngularMessageBus = new SamePageMessageBus(
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
`angular-devtools-detect-angular-${location.href}`,
|
||||
);
|
||||
|
||||
detectAngularMessageBus.on('detectAngular', (detectionResult) => {
|
||||
// only install backend once
|
||||
if (backendInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isAngularDevTools !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isAngular !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isDebugMode !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detectionResult.isSupportedAngularVersion !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Defensive check against non html page. Realistically this should never happen.
|
||||
if (document.contentType !== 'text/html') {
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = chrome.runtime.getURL('app/backend_bundle.js');
|
||||
document.documentElement.appendChild(script);
|
||||
document.documentElement.removeChild(script);
|
||||
backendInstalled = true;
|
||||
});
|
||||
|
||||
const localMessageBus = new SamePageMessageBus(
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
`angular-devtools-backend-${location.href}`,
|
||||
);
|
||||
const chromeMessageBus = new ChromeMessageBus(port);
|
||||
|
||||
const handshakeWithBackend = (): void => {
|
||||
localMessageBus.emit('handshake');
|
||||
};
|
||||
|
||||
chromeMessageBus.onAny((topic, args) => {
|
||||
localMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
localMessageBus.onAny((topic, args) => {
|
||||
backendInitialized = true;
|
||||
chromeMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
if (!backendInitialized) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Attempting initialization', new Date());
|
||||
|
||||
const retry = () => {
|
||||
if (backendInitialized || backgroundDisconnected) {
|
||||
return;
|
||||
}
|
||||
handshakeWithBackend();
|
||||
setTimeout(retry, 500);
|
||||
const handleDisconnect = (): void => {
|
||||
// console.log('Background disconnected', new Date());
|
||||
localMessageBus.emit('shutdown');
|
||||
localMessageBus.destroy();
|
||||
chromeMessageBus.destroy();
|
||||
backgroundDisconnected = true;
|
||||
};
|
||||
retry();
|
||||
}
|
||||
|
||||
port.onDisconnect.addListener(handleDisconnect);
|
||||
|
||||
const localMessageBus = new SamePageMessageBus(
|
||||
'angular-devtools-content-script',
|
||||
'angular-devtools-backend',
|
||||
);
|
||||
const chromeMessageBus = new ChromeMessageBus(port);
|
||||
|
||||
const handshakeWithBackend = (): void => {
|
||||
localMessageBus.emit('handshake');
|
||||
};
|
||||
|
||||
chromeMessageBus.onAny((topic, args) => {
|
||||
localMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
localMessageBus.onAny((topic, args) => {
|
||||
backendInitialized = true;
|
||||
chromeMessageBus.emit(topic, args);
|
||||
});
|
||||
|
||||
if (!backendInitialized) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Attempting initialization', new Date());
|
||||
|
||||
const retry = () => {
|
||||
if (backendInitialized || backgroundDisconnected) {
|
||||
return;
|
||||
}
|
||||
handshakeWithBackend();
|
||||
setTimeout(retry, 500);
|
||||
};
|
||||
retry();
|
||||
}
|
||||
};
|
||||
|
||||
// expose to use as callback for chrome.tabs.executeScript in background.ts
|
||||
(globalThis as any).main = main;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {AngularDetection} from 'protocol';
|
||||
import {
|
||||
appIsAngular,
|
||||
appIsAngularInDevMode,
|
||||
@@ -14,12 +13,18 @@ import {
|
||||
appIsSupportedAngularVersion,
|
||||
} from 'shared-utils';
|
||||
|
||||
import {SamePageMessageBus} from './same-page-message-bus';
|
||||
|
||||
const detectAngularMessageBus = new SamePageMessageBus(
|
||||
`angular-devtools-detect-angular-${location.href}`,
|
||||
`angular-devtools-content-script-${location.href}`,
|
||||
);
|
||||
export interface AngularDetection {
|
||||
// This is necessary because the runtime
|
||||
// message listener handles messages globally
|
||||
// including from other extensions. We don't
|
||||
// want to set icon and/or popup based on
|
||||
// a message coming from an unrelated extension.
|
||||
isAngularDevTools: true;
|
||||
isIvy: boolean;
|
||||
isAngular: boolean;
|
||||
isDebugMode: boolean;
|
||||
isSupportedAngularVersion: boolean;
|
||||
}
|
||||
|
||||
function detectAngular(win: Window): void {
|
||||
const isAngular = appIsAngular();
|
||||
@@ -27,29 +32,20 @@ function detectAngular(win: Window): void {
|
||||
const isDebugMode = appIsAngularInDevMode();
|
||||
const isIvy = appIsAngularIvy();
|
||||
|
||||
const detection: AngularDetection = {
|
||||
isIvy,
|
||||
isAngular,
|
||||
isDebugMode,
|
||||
isSupportedAngularVersion,
|
||||
isAngularDevTools: true,
|
||||
};
|
||||
|
||||
// For the background script to toggle the icon.
|
||||
win.postMessage(detection, '*');
|
||||
|
||||
// For the content script to inject the backend.
|
||||
detectAngularMessageBus.emit('detectAngular', [
|
||||
win.postMessage(
|
||||
{
|
||||
isIvy,
|
||||
isAngular,
|
||||
isDebugMode,
|
||||
isSupportedAngularVersion,
|
||||
isAngularDevTools: true,
|
||||
},
|
||||
]);
|
||||
} as AngularDetection,
|
||||
'*',
|
||||
);
|
||||
|
||||
setTimeout(() => detectAngular(win), 1000);
|
||||
if (!isAngular) {
|
||||
setTimeout(() => detectAngular(win), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
detectAngular(window);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
const loadScripts = (urls: string[]) => {
|
||||
return urls
|
||||
.map((url, idx) => {
|
||||
return `
|
||||
const script${idx} = document.constructor.prototype.createElement.call(document, 'script');
|
||||
script${idx}.src = getScriptName("${url}");
|
||||
document.documentElement.appendChild(script${idx});
|
||||
script${idx}.parentNode.removeChild(script${idx});
|
||||
`;
|
||||
})
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
let loaded = false;
|
||||
export const injectScripts = (urls: string[], cb?: () => void) => {
|
||||
if (loaded) {
|
||||
// Not throwing a hard error here, because we don't want to stop the
|
||||
// execution when folks are not using Trusted Types or when they have
|
||||
// allowed redeclaration of a security policy.
|
||||
console.error('Trying to reinject scripts');
|
||||
}
|
||||
loaded = true;
|
||||
urls = urls.map((s) => chrome.runtime.getURL(s));
|
||||
const script = `
|
||||
(function () {
|
||||
let policy = null;
|
||||
if (window.trustedTypes && window.trustedTypes.createPolicy) {
|
||||
policy = window.trustedTypes.createPolicy('angular#devtools', {
|
||||
createScriptURL: url => ${JSON.stringify(urls)}.indexOf(url) >= 0 ? url : null
|
||||
});
|
||||
}
|
||||
const getScriptName = name => policy ? policy.createScriptURL(name) : name;
|
||||
${loadScripts(urls)}
|
||||
})();
|
||||
`;
|
||||
chrome.devtools.inspectedWindow.eval(script, (_, err) => {
|
||||
if (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(err);
|
||||
}
|
||||
cb?.();
|
||||
});
|
||||
};
|
||||
@@ -1,245 +0,0 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/// <reference types="chrome"/>
|
||||
|
||||
import {Events, Topic} from 'protocol';
|
||||
|
||||
export interface ContentScriptConnection {
|
||||
port: chrome.runtime.Port | null;
|
||||
enabled: boolean;
|
||||
frameId: 'devtools' | number;
|
||||
backendReady?: boolean;
|
||||
}
|
||||
|
||||
export interface DevToolsConnection {
|
||||
devtools: chrome.runtime.Port | null;
|
||||
contentScripts: {[name: string]: ContentScriptConnection};
|
||||
}
|
||||
|
||||
function isNumeric(str: string): boolean {
|
||||
return +str + '' === str;
|
||||
}
|
||||
|
||||
export interface Tabs {
|
||||
[tabId: string]: DevToolsConnection | undefined;
|
||||
}
|
||||
|
||||
export class TabManager {
|
||||
constructor(
|
||||
private tabs: Tabs,
|
||||
private runtime: typeof chrome.runtime,
|
||||
) {}
|
||||
|
||||
static initialize(tabs: Tabs, runtime: typeof chrome.runtime = chrome.runtime): TabManager {
|
||||
const manager = new TabManager(tabs, runtime);
|
||||
manager.initialize();
|
||||
return manager;
|
||||
}
|
||||
|
||||
private initialize(): void {
|
||||
this.runtime.onConnect.addListener((port) => {
|
||||
if (isNumeric(port.name)) {
|
||||
this.registerDevToolsForTab(port);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!port.sender ||
|
||||
!port.sender.tab ||
|
||||
port.sender.tab.id === undefined ||
|
||||
port.sender.frameId === undefined
|
||||
) {
|
||||
console.warn('Received a connection from an unknown sender', port);
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerContentScriptForTab(port);
|
||||
});
|
||||
}
|
||||
|
||||
private ensureTabExists(tabId: number): void {
|
||||
this.tabs[tabId] ??= {
|
||||
devtools: null,
|
||||
contentScripts: {},
|
||||
};
|
||||
}
|
||||
|
||||
private registerDevToolsForTab(port: chrome.runtime.Port): void {
|
||||
// For the devtools page, our port name is the tab id.
|
||||
const tabId = parseInt(port.name, 10);
|
||||
|
||||
this.ensureTabExists(tabId);
|
||||
const tab = this.tabs[tabId]!;
|
||||
|
||||
tab.devtools = port;
|
||||
tab.devtools.onDisconnect.addListener(() => {
|
||||
tab.devtools = null;
|
||||
|
||||
for (const connection of Object.values(tab.contentScripts)) {
|
||||
connection.enabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// DevTools may register after the content script has already registered. If that's the case,
|
||||
// we need to set up the double pipe between the devtools and each content script, and send
|
||||
// the contentScriptConnected message to the devtools page to inform it of all frames on the page.
|
||||
for (const [frameId, connection] of Object.entries(tab.contentScripts)) {
|
||||
if (connection.port === null || connection.backendReady !== true) {
|
||||
continue;
|
||||
}
|
||||
|
||||
tab.devtools!.postMessage({
|
||||
topic: 'contentScriptConnected',
|
||||
args: [parseInt(frameId, 10), connection.port.name, connection.port.sender!.url],
|
||||
});
|
||||
this.doublePipe(tab.devtools, connection);
|
||||
}
|
||||
}
|
||||
|
||||
private registerContentScriptForTab(port: chrome.runtime.Port): void {
|
||||
// A content script connection will have a sender and a tab id.
|
||||
const sender = port.sender!;
|
||||
const frameId = sender.frameId!;
|
||||
const tabId = sender.tab!.id!;
|
||||
|
||||
this.ensureTabExists(tabId);
|
||||
const tab = this.tabs[tabId]!;
|
||||
|
||||
if (tab.contentScripts[frameId] === undefined) {
|
||||
tab.contentScripts[frameId] = {
|
||||
port: null,
|
||||
enabled: false,
|
||||
frameId: -1,
|
||||
};
|
||||
}
|
||||
|
||||
const contentScript = tab.contentScripts[frameId]!;
|
||||
contentScript.port = port;
|
||||
contentScript.frameId = frameId;
|
||||
contentScript.enabled = contentScript.enabled ?? false;
|
||||
|
||||
// When the content script disconnects, clean up the connection state we're storing in the
|
||||
// background page.
|
||||
contentScript.port.onDisconnect.addListener(() => {
|
||||
delete tab.contentScripts[frameId];
|
||||
|
||||
if (Object.keys(tab.contentScripts).length === 0) {
|
||||
delete this.tabs[tabId];
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for the backendReady message from the content script. This message is sent when the
|
||||
// content script has loaded the backend bundle and is ready to receive messages from the
|
||||
// backend.
|
||||
contentScript.port!.onMessage.addListener((message) => {
|
||||
if (message.topic === 'backendReady') {
|
||||
contentScript.backendReady = true;
|
||||
}
|
||||
});
|
||||
|
||||
// If the devtools connection is already established, set up the double pipe between the
|
||||
// devtools and the content script.
|
||||
if (tab.devtools) {
|
||||
this.doublePipe(tab.devtools, tab.contentScripts[frameId]);
|
||||
}
|
||||
}
|
||||
|
||||
private doublePipe(
|
||||
devtoolsPort: chrome.runtime.Port | null,
|
||||
contentScriptConnection: ContentScriptConnection,
|
||||
): void {
|
||||
if (devtoolsPort === null) {
|
||||
throw new Error('DevTools port is equal to null');
|
||||
}
|
||||
|
||||
const contentScriptPort = contentScriptConnection.port;
|
||||
|
||||
if (contentScriptPort === null) {
|
||||
throw new Error('Content script port is equal to null');
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log('Creating two-way communication channel', Date.now(), this.tabs);
|
||||
|
||||
const onDevToolsMessage = (message: {topic: Topic; args: Parameters<Events[Topic]>}) => {
|
||||
if (message.topic === 'enableFrameConnection') {
|
||||
if (message.args.length !== 2) {
|
||||
throw new Error('Expected two arguments for enableFrameConnection');
|
||||
}
|
||||
|
||||
const [frameId, tabId] = message.args as [frameId: number, tabId: number];
|
||||
|
||||
if (frameId === contentScriptConnection.frameId) {
|
||||
const tab = this.tabs[tabId];
|
||||
|
||||
if (tab === undefined) {
|
||||
throw new Error(`Expected tab to be registered with tabId ${tabId}`);
|
||||
}
|
||||
|
||||
for (const frameId of Object.keys(tab.contentScripts)) {
|
||||
tab.contentScripts[frameId].enabled = false;
|
||||
}
|
||||
|
||||
contentScriptConnection.enabled = true;
|
||||
devtoolsPort.postMessage({
|
||||
topic: 'frameConnected',
|
||||
args: [contentScriptConnection.frameId],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Do not allow any message to be sent if a content script is not enabled. This is the
|
||||
// mechanism that lets us select which content script connection Angular Devtools is connected
|
||||
// to.
|
||||
if (!contentScriptConnection.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
contentScriptPort.postMessage(message);
|
||||
};
|
||||
devtoolsPort.onMessage.addListener(onDevToolsMessage);
|
||||
|
||||
const onContentScriptMessage = (message: {topic: Topic; args: Parameters<Events[Topic]>}) => {
|
||||
if (message.topic === 'backendReady') {
|
||||
devtoolsPort.postMessage({
|
||||
topic: 'contentScriptConnected',
|
||||
args: [
|
||||
contentScriptConnection.frameId,
|
||||
contentScriptConnection.port!.name,
|
||||
contentScriptConnection.port!.sender!.url,
|
||||
],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not allow any message to be sent if a content script is not enabled. This is the
|
||||
// mechanism that lets us select which content script connection Angular Devtools is connected
|
||||
// to.
|
||||
if (!contentScriptConnection.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
devtoolsPort.postMessage(message);
|
||||
};
|
||||
contentScriptPort.onMessage.addListener(onContentScriptMessage);
|
||||
|
||||
const shutdownContentScript = () => {
|
||||
devtoolsPort.onMessage.removeListener(onDevToolsMessage);
|
||||
devtoolsPort.postMessage({
|
||||
topic: 'contentScriptDisconnected',
|
||||
args: [contentScriptConnection.frameId, contentScriptConnection.port!.name],
|
||||
});
|
||||
|
||||
contentScriptPort.onMessage.removeListener(onContentScriptMessage);
|
||||
contentScriptPort.disconnect();
|
||||
};
|
||||
|
||||
contentScriptPort.onDisconnect.addListener(() => shutdownContentScript());
|
||||
}
|
||||
}
|
||||
@@ -1,355 +0,0 @@
|
||||
/**
|
||||
* @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 {DevToolsConnection, TabManager, Tabs} from './tab_manager';
|
||||
|
||||
interface MockSender {
|
||||
url: string;
|
||||
tab: {
|
||||
id: number;
|
||||
};
|
||||
frameId: number;
|
||||
}
|
||||
|
||||
const TEST_MESSAGE_ONE = {topic: 'test', args: ['test1']};
|
||||
const TEST_MESSAGE_TWO = {topic: 'test', args: ['test2']};
|
||||
|
||||
class MockPort {
|
||||
onMessageListeners: Function[] = [];
|
||||
onDisconnectListeners: Function[] = [];
|
||||
messagesPosted: any[] = [];
|
||||
name: string;
|
||||
sender?: MockSender;
|
||||
|
||||
constructor(
|
||||
public properties: {
|
||||
name: string;
|
||||
sender?: MockSender;
|
||||
},
|
||||
) {
|
||||
this.name = properties.name;
|
||||
this.sender = properties.sender;
|
||||
}
|
||||
|
||||
postMessage(message: any) {
|
||||
this.messagesPosted.push(message);
|
||||
}
|
||||
|
||||
onMessage = {
|
||||
addListener: (listener: Function) => {
|
||||
this.onMessageListeners.push(listener);
|
||||
},
|
||||
};
|
||||
|
||||
onDisconnect = {
|
||||
addListener: (listener: Function) => {
|
||||
this.onDisconnectListeners.push(listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function assertArrayHasObj<T>(array: T[], obj: T) {
|
||||
expect(array).toContain(jasmine.objectContaining(obj as object));
|
||||
}
|
||||
|
||||
function assertArrayDoesNotHaveObj<T extends object>(array: T[], obj: T) {
|
||||
expect(array).not.toContain(jasmine.objectContaining(obj));
|
||||
}
|
||||
|
||||
function mockSpyFunction(obj: any, property: string, returnValue: any) {
|
||||
(obj[property] as any).and.returnValue(() => returnValue);
|
||||
}
|
||||
|
||||
function mockSpyProperty(obj: any, property: string, value: any) {
|
||||
(Object.getOwnPropertyDescriptor(obj, property)!.get as any).and.returnValue(value);
|
||||
}
|
||||
|
||||
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 emitMessageToPort(port: MockPort, message: any) {
|
||||
port.onMessageListeners.forEach((listener) => listener(message));
|
||||
}
|
||||
|
||||
function emitDisconnectToPort(port: MockPort) {
|
||||
port.onDisconnectListeners.forEach((listener) => listener());
|
||||
}
|
||||
|
||||
function createDevToolsPort() {
|
||||
const port = new MockPort({
|
||||
name: tabId.toString(),
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
chromeRuntimeOnConnectListeners = [];
|
||||
chromeRuntime = jasmine.createSpyObj(
|
||||
'chrome.runtime',
|
||||
['getManifest', 'getURL'],
|
||||
['onConnect', 'onDisconnect'],
|
||||
);
|
||||
mockSpyFunction(chromeRuntime, 'getManifest', {manifest_version: 3});
|
||||
mockSpyFunction(chromeRuntime, 'getURL', (path: string) => path);
|
||||
mockSpyProperty(chromeRuntime, 'onConnect', {
|
||||
addListener: (listener: (port: MockPort) => void) => {
|
||||
chromeRuntimeOnConnectListeners.push(listener);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('Single Frame', () => {
|
||||
const testURL = 'http://example.com';
|
||||
const contentScriptFrameId = 0;
|
||||
// let contentPort: MockPort;
|
||||
|
||||
function createContentScriptPort() {
|
||||
const port = new MockPort({
|
||||
name: 'Content Script',
|
||||
sender: {
|
||||
url: testURL,
|
||||
tab: {
|
||||
id: tabId,
|
||||
},
|
||||
frameId: contentScriptFrameId,
|
||||
},
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tabs = {};
|
||||
tabManager = TabManager.initialize(tabs, chromeRuntime);
|
||||
});
|
||||
|
||||
it('should setup tab object in the tab manager', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab).toBeDefined();
|
||||
expect(tab!.devtools).toBe(devtoolsPort as unknown as chrome.runtime.Port);
|
||||
expect(tab!.contentScripts[contentScriptFrameId].port).toBe(
|
||||
contentScriptPort as unknown as chrome.runtime.Port,
|
||||
);
|
||||
});
|
||||
|
||||
it('should set frame connection as enabled when an enableFrameConnection message is recieved', () => {
|
||||
createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
// Test backendReady and contentScriptConnected messages.
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false);
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(true);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'frameConnected',
|
||||
args: [contentScriptFrameId],
|
||||
});
|
||||
});
|
||||
|
||||
it('should pipe messages from the content script and devtools script to each other when the content script frame is enabled', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
|
||||
// Verify that the double pipe is set up between the content script and the devtools page.
|
||||
emitMessageToPort(contentScriptPort, TEST_MESSAGE_ONE);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
assertArrayDoesNotHaveObj(contentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO);
|
||||
assertArrayHasObj(contentScriptPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
assertArrayDoesNotHaveObj(devtoolsPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false);
|
||||
emitMessageToPort(contentScriptPort, TEST_MESSAGE_ONE);
|
||||
expect(contentScriptPort.messagesPosted.length).toBe(0);
|
||||
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO);
|
||||
expect(devtoolsPort.messagesPosted.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should set backendReady when the contentPort recieves the backendReady message', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
emitMessageToPort(contentScriptPort, {topic: 'backendReady'});
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.backendReady).toBe(true);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'contentScriptConnected',
|
||||
args: [contentScriptFrameId, contentScriptPort.name, contentScriptPort.sender!.url],
|
||||
});
|
||||
});
|
||||
|
||||
it('should set tab.devtools to null when the devtoolsPort disconnects', () => {
|
||||
const contentScriptPort = createContentScriptPort();
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [contentScriptFrameId, tabId],
|
||||
});
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(true);
|
||||
|
||||
emitDisconnectToPort(devtoolsPort);
|
||||
expect(tab.devtools).toBeNull();
|
||||
expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Frames', () => {
|
||||
const topLevelFrameId = 0;
|
||||
const childFrameId = 1;
|
||||
|
||||
function createTopLevelContentScriptPort() {
|
||||
const port = new MockPort({
|
||||
name: 'Top level content script',
|
||||
sender: {
|
||||
url: 'TEST_URL',
|
||||
tab: {
|
||||
id: tabId,
|
||||
},
|
||||
frameId: topLevelFrameId,
|
||||
},
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
return port;
|
||||
}
|
||||
function createChildContentScriptPort() {
|
||||
const port = new MockPort({
|
||||
name: 'Child content script',
|
||||
sender: {
|
||||
url: 'TEST_URL_2',
|
||||
tab: {
|
||||
id: tabId,
|
||||
},
|
||||
frameId: childFrameId,
|
||||
},
|
||||
});
|
||||
connectToChromeRuntime(port);
|
||||
return port;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
tabs = {};
|
||||
tabManager = TabManager.initialize(tabs, chromeRuntime);
|
||||
});
|
||||
|
||||
it('should setup tab object in the tab manager', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
const topLevelContentScriptPort = createTopLevelContentScriptPort();
|
||||
const childContentScriptPort = createChildContentScriptPort();
|
||||
|
||||
tab = tabs[tabId]!;
|
||||
|
||||
expect(tab).toBeDefined();
|
||||
expect(tab!.devtools).toBe(devtoolsPort as unknown as chrome.runtime.Port);
|
||||
expect(tab!.contentScripts[topLevelFrameId].port).toBe(
|
||||
topLevelContentScriptPort as unknown as chrome.runtime.Port,
|
||||
);
|
||||
expect(tab!.contentScripts[childFrameId].port).toBe(
|
||||
childContentScriptPort as unknown as chrome.runtime.Port,
|
||||
);
|
||||
});
|
||||
|
||||
it('should setup message and disconnect listeners on devtools and content script ports', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
const topLevelContentScriptPort = createTopLevelContentScriptPort();
|
||||
const childContentScriptPort = createChildContentScriptPort();
|
||||
|
||||
// 1 listener to clean up tab object if this was the last content script connection.
|
||||
// 1 listener to cleanup the douple pipe between the content script and the devtools page.
|
||||
expect(topLevelContentScriptPort.onDisconnectListeners.length).toBe(2);
|
||||
expect(childContentScriptPort.onDisconnectListeners.length).toBe(2);
|
||||
|
||||
// 1 listener to clean up devtools connection
|
||||
expect(devtoolsPort.onDisconnectListeners.length).toBe(1);
|
||||
|
||||
// 1 listener set when the content script is registered to check for backendReady.
|
||||
// 1 listener set when the double pipe is set up between the content script and the devtools page.
|
||||
expect(topLevelContentScriptPort.onMessageListeners.length).toBe(2);
|
||||
expect(childContentScriptPort.onMessageListeners.length).toBe(2);
|
||||
});
|
||||
|
||||
it('should set the correct frame connection as enabled when an enableFrameConnection message is recieved', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
createTopLevelContentScriptPort();
|
||||
createChildContentScriptPort();
|
||||
|
||||
expect(tab?.contentScripts[topLevelFrameId]?.enabled).toBe(false);
|
||||
expect(tab?.contentScripts[childFrameId]?.enabled).toBe(false);
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [topLevelFrameId, tabId],
|
||||
});
|
||||
expect(tab?.contentScripts[topLevelFrameId]?.enabled).toBe(true);
|
||||
expect(tab?.contentScripts[childFrameId]?.enabled).toBe(false);
|
||||
assertArrayHasObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'frameConnected',
|
||||
args: [topLevelFrameId],
|
||||
});
|
||||
assertArrayDoesNotHaveObj(devtoolsPort.messagesPosted, {
|
||||
topic: 'frameConnected',
|
||||
args: [childFrameId],
|
||||
});
|
||||
});
|
||||
|
||||
it('should pipe messages from the correct content script and devtools script when that content script frame is enabled', () => {
|
||||
const devtoolsPort = createDevToolsPort();
|
||||
const topLevelContentScriptPort = createTopLevelContentScriptPort();
|
||||
const childContentScriptPort = createChildContentScriptPort();
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [topLevelFrameId, tabId],
|
||||
});
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_ONE);
|
||||
assertArrayHasObj(topLevelContentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
assertArrayDoesNotHaveObj(childContentScriptPort.messagesPosted, TEST_MESSAGE_ONE);
|
||||
|
||||
emitMessageToPort(devtoolsPort, {
|
||||
topic: 'enableFrameConnection',
|
||||
args: [childFrameId, tabId],
|
||||
});
|
||||
emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO);
|
||||
assertArrayHasObj(childContentScriptPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
assertArrayDoesNotHaveObj(topLevelContentScriptPort.messagesPosted, TEST_MESSAGE_TWO);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -27,7 +27,8 @@
|
||||
{
|
||||
"resources": [
|
||||
"app/backend_bundle.js",
|
||||
"app/detect_angular_for_extension_icon_bundle.js"
|
||||
"app/detect_angular_for_extension_icon_bundle.js",
|
||||
"devtools.html"
|
||||
],
|
||||
"matches": ["<all_urls>"],
|
||||
"extension_ids": []
|
||||
@@ -51,18 +52,7 @@
|
||||
"js": [
|
||||
"app/ng_validate_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"app/content_script_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -29,26 +29,13 @@
|
||||
"https://*/*",
|
||||
"file:///*"
|
||||
],
|
||||
"content_scripts": [
|
||||
{
|
||||
"content_scripts": [{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"app/ng_validate_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
},
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"app/content_script_bundle.js"
|
||||
],
|
||||
"run_at": "document_idle",
|
||||
"all_frames": true
|
||||
}
|
||||
]
|
||||
"run_at": "document_idle"
|
||||
}]
|
||||
}
|
||||
Reference in New Issue
Block a user