diff --git a/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/timeline/record-formatter/frame-merger.ts b/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/timeline/record-formatter/frame-merger.ts index 2da69004ef8b8..3eb311b9a26db 100644 --- a/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/timeline/record-formatter/frame-merger.ts +++ b/devtools/projects/ng-devtools/src/lib/devtools-tabs/profiler/timeline/record-formatter/frame-merger.ts @@ -56,7 +56,7 @@ export const mergeFrames = (frames: ProfilerFrame[]): ProfilerFrame | null => { if (!frames || !frames.length) { return null; } - const first = JSON.parse(JSON.stringify(frames[0])); + const first = JSON.parse(JSON.stringify(frames[0])) as ProfilerFrame; for (let i = 1; i < frames.length; i++) { mergeFrame(first, frames[i]); } diff --git a/devtools/projects/shell-browser/src/app/BUILD.bazel b/devtools/projects/shell-browser/src/app/BUILD.bazel index 06fe4ab97f310..49e41ea7525b8 100644 --- a/devtools/projects/shell-browser/src/app/BUILD.bazel +++ b/devtools/projects/shell-browser/src/app/BUILD.bazel @@ -58,6 +58,7 @@ ts_library( deps = [ "//devtools/projects/ng-devtools-backend", "//devtools/projects/ng-devtools-backend/src/lib:component_tree", + "//devtools/projects/protocol", "@npm//@types", ], ) diff --git a/devtools/projects/shell-browser/src/app/chrome-window-extensions.ts b/devtools/projects/shell-browser/src/app/chrome-window-extensions.ts index df84c43d47203..6f2e9aa5cd614 100644 --- a/devtools/projects/shell-browser/src/app/chrome-window-extensions.ts +++ b/devtools/projects/shell-browser/src/app/chrome-window-extensions.ts @@ -13,6 +13,8 @@ import { queryDirectiveForest, } from '../../../ng-devtools-backend/src/lib/component-tree'; +import {ElementPosition} from 'protocol'; + export const initializeExtendedWindowOperations = () => { extendWindowOperations(globalThis, {inspectedApplication: chromeWindowExtensions}); }; @@ -63,7 +65,10 @@ const chromeWindowExtensions = { return node.nativeElement; }, findPropertyByPosition: (args: any): any => { - const {directivePosition, objectPath} = JSON.parse(args); + const {directivePosition, objectPath} = JSON.parse(args) as { + directivePosition: {element: ElementPosition; directive: number}; + objectPath: string[]; + }; const node = queryDirectiveForest(directivePosition.element, buildDirectiveForest()); if (node === null) { console.error(`Cannot find element associated with node ${directivePosition}`); diff --git a/devtools/projects/shell-browser/src/app/tab_manager.ts b/devtools/projects/shell-browser/src/app/tab_manager.ts index e2fdcc2427147..3fb02bb798602 100644 --- a/devtools/projects/shell-browser/src/app/tab_manager.ts +++ b/devtools/projects/shell-browser/src/app/tab_manager.ts @@ -14,7 +14,7 @@ export interface ContentScriptConnection { port: chrome.runtime.Port | null; enabled: boolean; frameId: 'devtools' | number; - backendReady?: boolean; + backendReady?: Promise; } export interface DevToolsConnection { @@ -90,15 +90,18 @@ export class TabManager { // 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], + connection.backendReady!.then(() => { + if (connection.port === null) { + throw new Error( + 'Expected Content to have already connected before the backendReady event on the same page.', + ); + } + this.doublePipe(tab.devtools, connection); + tab.devtools!.postMessage({ + topic: 'contentScriptConnected', + args: [parseInt(frameId, 10), connection.port.name, connection.port.sender!.url], + }); }); - this.doublePipe(tab.devtools, connection); } } @@ -126,7 +129,7 @@ export class TabManager { // When the content script disconnects, clean up the connection state we're storing in the // background page. - contentScript.port.onDisconnect.addListener(() => { + port.onDisconnect.addListener(() => { delete tab.contentScripts[frameId]; if (Object.keys(tab.contentScripts).length === 0) { @@ -134,20 +137,33 @@ export class TabManager { } }); - // 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; - } - }); + contentScript.backendReady = new Promise((resolveBackendReady) => { + const onBackendReady = (message: {topic: string}) => { + if (message.topic === 'backendReady') { + // If DevTools is not yet connected, this resolve will enable devtools to eventually connect to this + // content script (even though the content script connected first) + resolveBackendReady(); + + // 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, contentScript); + + tab.devtools.postMessage({ + topic: 'contentScriptConnected', + args: [frameId, contentScript.port!.name, contentScript.port!.sender!.url], + }); + } - // 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]); - } + port.onMessage.removeListener(onBackendReady); + } + }; + + port.onMessage.addListener(onBackendReady); + port.onDisconnect.addListener(() => { + port.onMessage.removeListener(onBackendReady); + }); + }); } private doublePipe( @@ -206,18 +222,6 @@ export class TabManager { devtoolsPort.onMessage.addListener(onDevToolsMessage); const onContentScriptMessage = (message: {topic: Topic; args: Parameters}) => { - 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. @@ -237,9 +241,7 @@ export class TabManager { }); contentScriptPort.onMessage.removeListener(onContentScriptMessage); - contentScriptPort.disconnect(); }; - contentScriptPort.onDisconnect.addListener(() => shutdownContentScript()); } } diff --git a/devtools/projects/shell-browser/src/app/tab_manager_spec.ts b/devtools/projects/shell-browser/src/app/tab_manager_spec.ts index e01c6c775ebbb..4de8fd5a33c0b 100644 --- a/devtools/projects/shell-browser/src/app/tab_manager_spec.ts +++ b/devtools/projects/shell-browser/src/app/tab_manager_spec.ts @@ -44,6 +44,9 @@ class MockPort { addListener: (listener: Function): void => { this.onMessageListeners.push(listener); }, + removeListener: (listener: Function) => { + this.onMessageListeners = this.onMessageListeners.filter((l) => l !== listener); + }, }; onDisconnect = { @@ -89,6 +92,10 @@ describe('Tab Manager - ', () => { } } + function emitBackendReadyToPort(contentScriptPort: MockPort) { + emitMessageToPort(contentScriptPort, {topic: 'backendReady'}); + } + function emitDisconnectToPort(port: MockPort) { for (const listener of port.onDisconnectListeners) { listener(); @@ -122,7 +129,6 @@ describe('Tab Manager - ', () => { describe('Single Frame', () => { const testURL = 'http://example.com'; const contentScriptFrameId = 0; - // let contentPort: MockPort; function createContentScriptPort() { const port = new MockPort({ @@ -136,7 +142,6 @@ describe('Tab Manager - ', () => { }, }); connectToChromeRuntime(port); - emitMessageToPort(port, {topic: 'backendReady'}); return port; } @@ -145,101 +150,141 @@ describe('Tab Manager - ', () => { tabManager = TabManager.initialize(tabs, chromeRuntime); }); - it('should setup tab object in the tab manager', () => { - const contentScriptPort = createContentScriptPort(); - const devtoolsPort = createDevToolsPort(); - tab = tabs[tabId]!; + async function* eachOrderingOfDevToolsInitialization(): AsyncGenerator<{ + tab: DevToolsConnection; + contentScriptPort: MockPort; + devtoolsPort: MockPort; + }> { + { + // Content Script -> Backend Ready -> Devtools + const contentScriptPort = createContentScriptPort(); + emitBackendReadyToPort(contentScriptPort); + const devtoolsPort = createDevToolsPort(); + const tab = tabs[tabId]!; + await tab.contentScripts[contentScriptFrameId].backendReady; + yield {tab, contentScriptPort, devtoolsPort}; + delete tabs[tabId]; + } + + { + // Content Script -> Devtools -> Backend Ready + const contentScriptPort = createContentScriptPort(); + const devtoolsPort = createDevToolsPort(); + emitBackendReadyToPort(contentScriptPort); + const tab = tabs[tabId]!; + await tab.contentScripts[contentScriptFrameId].backendReady; + yield {tab, contentScriptPort, devtoolsPort}; + delete tabs[tabId]; + } + + { + // Devtools -> Content Script -> Backend Ready + const devtoolsPort = createDevToolsPort(); + const contentScriptPort = createContentScriptPort(); + emitBackendReadyToPort(contentScriptPort); + const tab = tabs[tabId]!; + await tab.contentScripts[contentScriptFrameId].backendReady; + yield {tab, contentScriptPort, devtoolsPort}; + } + } - 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 setup tab object in the tab manager', async () => { + for await (const { + tab, + contentScriptPort, + devtoolsPort, + } of eachOrderingOfDevToolsInitialization()) { + 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', () => { - const contentScriptPort = 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 set frame connection as enabled when an enableFrameConnection message is recieved', async () => { + for await (const {tab, devtoolsPort} of eachOrderingOfDevToolsInitialization()) { + 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 pipe messages from the content script and devtools script to each other when the content script frame is enabled', async () => { + for await (const { + contentScriptPort, + devtoolsPort, + } of eachOrderingOfDevToolsInitialization()) { + 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(); - tab = tabs[tabId]!; + it('should not pipe messages from the content script and devtools script to each other when the content script frame is disabled', async () => { + for await (const { + tab, + contentScriptPort, + devtoolsPort, + } of eachOrderingOfDevToolsInitialization()) { + expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false); - expect(tab?.contentScripts[contentScriptFrameId]?.enabled).toBe(false); + emitMessageToPort(contentScriptPort, TEST_MESSAGE_ONE); + assertArrayDoesNotHaveObj(contentScriptPort.messagesPosted, TEST_MESSAGE_ONE); - emitMessageToPort(contentScriptPort, TEST_MESSAGE_ONE); - assertArrayDoesNotHaveObj(contentScriptPort.messagesPosted, TEST_MESSAGE_ONE); - - emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO); - assertArrayDoesNotHaveObj(devtoolsPort.messagesPosted, TEST_MESSAGE_TWO); + emitMessageToPort(devtoolsPort, TEST_MESSAGE_TWO); + 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], - }); - - expect(tab?.contentScripts[contentScriptFrameId]?.backendReady).toBe(true); - assertArrayHasObj(devtoolsPort.messagesPosted, { - topic: 'contentScriptConnected', - args: [contentScriptFrameId, contentScriptPort.name, contentScriptPort.sender!.url], - }); + it('should set backendReady when the contentPort recieves the backendReady message', async () => { + for await (const { + contentScriptPort, + devtoolsPort, + } of eachOrderingOfDevToolsInitialization()) { + emitMessageToPort(devtoolsPort, { + topic: 'enableFrameConnection', + args: [contentScriptFrameId, tabId], + }); + + 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(); - tab = tabs[tabId]!; - - 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); + it('should set tab.devtools to null when the devtoolsPort disconnects', async () => { + for await (const {tab, devtoolsPort} of eachOrderingOfDevToolsInitialization()) { + 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); + } }); }); @@ -276,91 +321,146 @@ describe('Tab Manager - ', () => { return port; } + async function* eachOrderingOfDevToolsInitialization() { + { + // Devtools Connected -> Top Level Content Script Connected -> Top Level Content Script Backend Ready + // -> Child Content Script Connected -> Child Content Script Backend Ready + const devtoolsPort = createDevToolsPort(); + const topLevelContentScriptPort = createTopLevelContentScriptPort(); + emitBackendReadyToPort(topLevelContentScriptPort); + const childContentScriptPort = createChildContentScriptPort(); + emitBackendReadyToPort(childContentScriptPort); + const tab = tabs[tabId]!; + await tab.contentScripts[topLevelFrameId].backendReady; + await tab.contentScripts[childFrameId].backendReady; + yield {tab, topLevelContentScriptPort, childContentScriptPort, devtoolsPort}; + delete tabs[tabId]; + } + + { + // Top Level Content Script Connected -> Top Level Content Script Backend Ready -> Devtools Connected + // -> Child Content Script Connected -> Child Content Script Backend Ready + const topLevelContentScriptPort = createTopLevelContentScriptPort(); + emitBackendReadyToPort(topLevelContentScriptPort); + const devtoolsPort = createDevToolsPort(); + const childContentScriptPort = createChildContentScriptPort(); + emitBackendReadyToPort(childContentScriptPort); + const tab = tabs[tabId]!; + await tab.contentScripts[topLevelFrameId].backendReady; + await tab.contentScripts[childFrameId].backendReady; + yield {tab, topLevelContentScriptPort, childContentScriptPort, devtoolsPort}; + delete tabs[tabId]; + } + + { + // Top Level Content Script Connected -> Top Level Content Script Backend Ready -> Child Content Script Connected + // -> Child Content Script Backend Ready -> Devtools Connected + const topLevelContentScriptPort = createTopLevelContentScriptPort(); + emitBackendReadyToPort(topLevelContentScriptPort); + const childContentScriptPort = createChildContentScriptPort(); + emitBackendReadyToPort(childContentScriptPort); + const devtoolsPort = createDevToolsPort(); + tab = tabs[tabId]!; + await tab.contentScripts[topLevelFrameId].backendReady; + await tab.contentScripts[childFrameId].backendReady; + yield {tab, topLevelContentScriptPort, childContentScriptPort, devtoolsPort}; + delete tabs[tabId]; + } + + { + // Top Level Content Script Connected -> Devtools Connected -> Child Content Script Connected + // -> Top Level Content Script Backend Ready -> Child Content Script Backend Ready + const topLevelContentScriptPort = createTopLevelContentScriptPort(); + const devtoolsPort = createDevToolsPort(); + const childContentScriptPort = createChildContentScriptPort(); + emitBackendReadyToPort(topLevelContentScriptPort); + emitBackendReadyToPort(childContentScriptPort); + const tab = tabs[tabId]!; + await tab.contentScripts[topLevelFrameId].backendReady; + await tab.contentScripts[childFrameId].backendReady; + yield {tab, topLevelContentScriptPort, childContentScriptPort, devtoolsPort}; + } + } + 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 tab object in the tab manager', async () => { + for await (const { + tab, + topLevelContentScriptPort, + childContentScriptPort, + devtoolsPort, + } of eachOrderingOfDevToolsInitialization()) { + 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 setup message and disconnect listeners on devtools and content script ports', async () => { + for await (const { + topLevelContentScriptPort, + childContentScriptPort, + devtoolsPort, + } of eachOrderingOfDevToolsInitialization()) { + expect(topLevelContentScriptPort.onDisconnectListeners.length).toBeGreaterThan(0); + expect(childContentScriptPort.onDisconnectListeners.length).toBeGreaterThan(0); + expect(devtoolsPort.onDisconnectListeners.length).toBeGreaterThan(0); + expect(topLevelContentScriptPort.onMessageListeners.length).toBeGreaterThan(0); + } }); - it('should set the correct frame connection as enabled when an enableFrameConnection message is recieved', () => { - const devtoolsPort = createDevToolsPort(); - createTopLevelContentScriptPort(); - createChildContentScriptPort(); - tab = tabs[tabId]!; - - 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 set the correct frame connection as enabled when an enableFrameConnection message is recieved', async () => { + for await (const {tab, devtoolsPort} of eachOrderingOfDevToolsInitialization()) { + 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); + it('should pipe messages from the correct content script and devtools script when that content script frame is enabled', async () => { + for await (const { + topLevelContentScriptPort, + childContentScriptPort, + devtoolsPort, + } of eachOrderingOfDevToolsInitialization()) { + 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); + } }); }); }); diff --git a/devtools/tools/typescript.bzl b/devtools/tools/typescript.bzl index e6ff991d019d6..77954cb34775d 100644 --- a/devtools/tools/typescript.bzl +++ b/devtools/tools/typescript.bzl @@ -9,7 +9,7 @@ def ts_library(name, tsconfig = "//devtools:tsconfig.json", **kwargs): **kwargs ) -def ts_test_library(name, tsconfig = "//devtools:tsconfig.json", deps = [], **kwargs): +def ts_test_library(name, tsconfig = "//devtools:tsconfig_spec", deps = [], **kwargs): _ts_library( name = name, tsconfig = tsconfig, diff --git a/devtools/tsconfig.json b/devtools/tsconfig.json index da771d30e2361..ae627b9d88988 100644 --- a/devtools/tsconfig.json +++ b/devtools/tsconfig.json @@ -1,14 +1,6 @@ { "compileOnSave": false, "compilerOptions": { - "plugins": [ - { - "name": "@bazel/tsetse", - "disabledRules": [ - "must-type-assert-json-parse" - ] - } - ], "baseUrl": "../", "outDir": "bazel-out/darwin-fastbuild/bin", "sourceMap": true, diff --git a/devtools/tsconfig.spec.json b/devtools/tsconfig.spec.json index 1a7b5523ee2b2..9e8f927471a27 100644 --- a/devtools/tsconfig.spec.json +++ b/devtools/tsconfig.spec.json @@ -1,6 +1,14 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "plugins": [ + { + "name": "@bazel/tsetse", + "disabledRules": [ + "must-use-promises" + ] + } + ], "sourceMap": false, "types": ["jasmine", "node"], }