| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import { |
| createTarget, |
| } from '../../testing/EnvironmentHelpers.js'; |
| import {setupRuntimeHooks} from '../../testing/RuntimeHelpers.js'; |
| import {setupSettingsHooks} from '../../testing/SettingsHelpers.js'; |
| import * as Host from '../host/host.js'; |
| import * as Platform from '../platform/platform.js'; |
| import * as Root from '../root/root.js'; |
| |
| import * as SDK from './sdk.js'; |
| |
| const {urlString} = Platform.DevToolsPath; |
| |
| describe('TargetManager', () => { |
| let targetManager: SDK.TargetManager.TargetManager; |
| let inspectedURLChangedHostApi: sinon.SinonStub; |
| |
| setupRuntimeHooks(); |
| setupSettingsHooks(); |
| |
| beforeEach(() => { |
| targetManager = SDK.TargetManager.TargetManager.instance({forceNew: true}); |
| |
| // TODO(crbug.com/451502260): Add Node.js specific host bindings. For now, we don't execute |
| // the InspectorFrontendHostStub, it only updates the document.title anyway. |
| inspectedURLChangedHostApi = |
| sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'inspectedURLChanged'); |
| }); |
| |
| afterEach(() => { |
| Root.DevToolsContext.setGlobalInstance(null); |
| }); |
| |
| function resourceTreeModel(target: SDK.Target.Target): SDK.ResourceTreeModel.ResourceTreeModel { |
| const model = target.model(SDK.ResourceTreeModel.ResourceTreeModel); |
| assert.exists(model); |
| return model; |
| } |
| |
| it('allows observing targets', () => { |
| const observer = sinon.spy(new SDK.TargetManager.Observer()); |
| const target1 = createTarget(); |
| targetManager.observeTargets(observer); |
| assert.isTrue(observer.targetAdded.calledOnceWith(target1)); |
| const target2 = createTarget(); |
| sinon.assert.calledTwice(observer.targetAdded); |
| sinon.assert.calledWith(observer.targetAdded, target2); |
| target2.dispose('YOLO!'); |
| assert.isTrue(observer.targetRemoved.calledOnceWith(target2)); |
| |
| targetManager.unobserveTargets(observer); |
| createTarget(); |
| sinon.assert.calledTwice(observer.targetAdded); |
| }); |
| |
| it('allows observing models', () => { |
| const observer = sinon.spy(new SDK.TargetManager.SDKModelObserver<SDK.ResourceTreeModel.ResourceTreeModel>()); |
| const target1 = createTarget(); |
| targetManager.observeModels(SDK.ResourceTreeModel.ResourceTreeModel, observer); |
| assert.isTrue(observer.modelAdded.calledOnceWith(resourceTreeModel(target1))); |
| const target2 = createTarget(); |
| sinon.assert.calledTwice(observer.modelAdded); |
| sinon.assert.calledWith(observer.modelAdded, resourceTreeModel(target2)); |
| target2.dispose('YOLO!'); |
| assert.isTrue(observer.modelRemoved.calledOnceWith(resourceTreeModel(target2))); |
| |
| targetManager.unobserveModels(SDK.ResourceTreeModel.ResourceTreeModel, observer); |
| createTarget(); |
| sinon.assert.calledTwice(observer.modelAdded); |
| }); |
| |
| it('allows overriding which models to autostart', () => { |
| const targetManager = new SDK.TargetManager.TargetManager( |
| new Root.DevToolsContext.DevToolsContext(), new Set([SDK.DebuggerModel.DebuggerModel])); |
| const target = createTarget({targetManager}); |
| |
| assert.isTrue(target.models().has(SDK.DebuggerModel.DebuggerModel)); |
| assert.isFalse(target.models().has(SDK.DOMModel.DOMModel)); |
| }); |
| |
| it('allows listening to models', () => { |
| const WillReloadPage = SDK.ResourceTreeModel.Events.WillReloadPage; |
| const thisObject = {}; |
| const listener = sinon.spy(); |
| const target1 = createTarget(); |
| |
| targetManager.addModelListener(SDK.ResourceTreeModel.ResourceTreeModel, WillReloadPage, listener, thisObject); |
| |
| resourceTreeModel(target1).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledOnce(listener); |
| sinon.assert.calledOn(listener, thisObject); |
| |
| const target2 = createTarget(); |
| resourceTreeModel(target2).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledTwice(listener); |
| sinon.assert.calledOn(listener, thisObject); |
| |
| targetManager.removeModelListener(SDK.ResourceTreeModel.ResourceTreeModel, WillReloadPage, listener, thisObject); |
| resourceTreeModel(target1).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledTwice(listener); |
| }); |
| |
| it('allows observing targets in scope', () => { |
| const target1 = createTarget(); |
| const target2 = createTarget(); |
| targetManager.setScopeTarget(target1); |
| const observer = sinon.spy(new SDK.TargetManager.Observer()); |
| |
| targetManager.observeTargets(observer, {scoped: true}); |
| assert.isTrue(observer.targetAdded.calledOnceWith(target1)); |
| |
| createTarget({parentTarget: target2}); |
| assert.isTrue(observer.targetAdded.calledOnceWith(target1)); |
| |
| const subtarget1 = createTarget({parentTarget: target1}); |
| sinon.assert.calledTwice(observer.targetAdded); |
| sinon.assert.calledWith(observer.targetAdded, subtarget1); |
| }); |
| |
| it('allows observing models in scope', () => { |
| const target1 = createTarget(); |
| const target2 = createTarget(); |
| targetManager.setScopeTarget(target1); |
| const observer = sinon.spy(new SDK.TargetManager.SDKModelObserver<SDK.ResourceTreeModel.ResourceTreeModel>()); |
| |
| targetManager.observeModels(SDK.ResourceTreeModel.ResourceTreeModel, observer, {scoped: true}); |
| assert.isTrue(observer.modelAdded.calledOnceWith(resourceTreeModel(target1))); |
| |
| createTarget({parentTarget: target2}); |
| sinon.assert.calledOnce(observer.modelAdded); |
| |
| const subtarget1 = createTarget({parentTarget: target1}); |
| sinon.assert.calledTwice(observer.modelAdded); |
| sinon.assert.calledWith(observer.modelAdded, resourceTreeModel(subtarget1)); |
| }); |
| |
| it('calls second observers even if the first is changing the scope', () => { |
| const target1 = createTarget(); |
| const target2 = createTarget(); |
| targetManager.setScopeTarget(target1); |
| const observer1 = sinon.stub(new SDK.TargetManager.SDKModelObserver<SDK.ResourceTreeModel.ResourceTreeModel>()); |
| observer1.modelRemoved.callsFake(() => targetManager.setScopeTarget(target2)); |
| const observer2 = sinon.spy(new SDK.TargetManager.SDKModelObserver<SDK.RuntimeModel.RuntimeModel>()); |
| |
| targetManager.observeModels(SDK.ResourceTreeModel.ResourceTreeModel, observer1, {scoped: true}); |
| targetManager.observeModels(SDK.RuntimeModel.RuntimeModel, observer2, {scoped: true}); |
| |
| target1.dispose('YOLO!'); |
| sinon.assert.calledOnce(observer1.modelRemoved); |
| sinon.assert.calledOnce(observer2.modelRemoved); |
| }); |
| |
| it('allows listening to models in scope', () => { |
| const WillReloadPage = SDK.ResourceTreeModel.Events.WillReloadPage; |
| const listener = sinon.spy(); |
| const target1 = createTarget(); |
| targetManager.setScopeTarget(target1); |
| const thisObject = {}; |
| |
| targetManager.addModelListener( |
| SDK.ResourceTreeModel.ResourceTreeModel, WillReloadPage, listener, thisObject, {scoped: true}); |
| |
| resourceTreeModel(target1).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledOnce(listener); |
| sinon.assert.calledOn(listener, thisObject); |
| |
| const target2 = createTarget(); |
| resourceTreeModel(target2).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledOnce(listener); |
| |
| const subtarget1 = createTarget({parentTarget: target1}); |
| resourceTreeModel(subtarget1).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledTwice(listener); |
| |
| targetManager.setScopeTarget(target2); |
| resourceTreeModel(target1).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledTwice(listener); |
| |
| resourceTreeModel(target2).dispatchEventToListeners(WillReloadPage); |
| sinon.assert.calledThrice(listener); |
| }); |
| |
| it('can transition between scopes', () => { |
| const target1 = createTarget(); |
| const target2 = createTarget(); |
| const targetObserver = sinon.spy(new SDK.TargetManager.Observer()); |
| const modelObserver = sinon.spy(new SDK.TargetManager.SDKModelObserver<SDK.ResourceTreeModel.ResourceTreeModel>()); |
| const scopeChangeListener = sinon.spy(); |
| |
| targetManager.observeTargets(targetObserver, {scoped: true}); |
| targetManager.observeModels(SDK.ResourceTreeModel.ResourceTreeModel, modelObserver, {scoped: true}); |
| targetManager.addScopeChangeListener(scopeChangeListener); |
| |
| assert.isTrue(targetObserver.targetAdded.calledOnceWith(target1)); |
| sinon.assert.calledOnce(modelObserver.modelAdded); |
| sinon.assert.notCalled(targetObserver.targetRemoved); |
| sinon.assert.notCalled(modelObserver.modelRemoved); |
| sinon.assert.notCalled(scopeChangeListener); |
| |
| targetObserver.targetAdded.resetHistory(); |
| modelObserver.modelAdded.resetHistory(); |
| |
| targetManager.setScopeTarget(target2); |
| assert.isTrue(targetObserver.targetRemoved.calledOnceWith(target1)); |
| sinon.assert.calledOnce(modelObserver.modelRemoved); |
| assert.isTrue(targetObserver.targetAdded.calledOnceWith(target2)); |
| sinon.assert.calledOnce(modelObserver.modelAdded); |
| assert.isTrue(targetObserver.targetAdded.calledAfter(targetObserver.targetRemoved)); |
| assert.isTrue(modelObserver.modelAdded.calledAfter(modelObserver.modelRemoved)); |
| sinon.assert.called(scopeChangeListener); |
| |
| targetObserver.targetAdded.resetHistory(); |
| targetObserver.targetRemoved.resetHistory(); |
| modelObserver.modelAdded.resetHistory(); |
| modelObserver.modelRemoved.resetHistory(); |
| scopeChangeListener.resetHistory(); |
| |
| targetManager.setScopeTarget(null); |
| sinon.assert.notCalled(targetObserver.targetAdded); |
| assert.isFalse(modelObserver.modelAdded.calledOnce); |
| assert.isTrue(targetObserver.targetRemoved.calledOnceWith(target1)); |
| sinon.assert.called(modelObserver.modelRemoved); |
| sinon.assert.called(scopeChangeListener); |
| |
| targetObserver.targetAdded.resetHistory(); |
| targetObserver.targetRemoved.resetHistory(); |
| modelObserver.modelAdded.resetHistory(); |
| modelObserver.modelRemoved.resetHistory(); |
| scopeChangeListener.resetHistory(); |
| |
| const target3 = createTarget(); |
| sinon.assert.notCalled(targetObserver.targetAdded); |
| sinon.assert.notCalled(modelObserver.modelAdded); |
| sinon.assert.notCalled(scopeChangeListener); |
| |
| targetManager.setScopeTarget(target3); |
| sinon.assert.called(targetObserver.targetAdded); |
| sinon.assert.called(modelObserver.modelAdded); |
| sinon.assert.called(scopeChangeListener); |
| }); |
| |
| it('short-cicuits setting the same scope target', () => { |
| const target1 = createTarget(); |
| targetManager.setScopeTarget(target1); |
| const targetObserver = sinon.spy(new SDK.TargetManager.Observer()); |
| const modelObserver = sinon.spy(new SDK.TargetManager.SDKModelObserver<SDK.ResourceTreeModel.ResourceTreeModel>()); |
| |
| targetObserver.targetAdded.resetHistory(); |
| targetObserver.targetRemoved.resetHistory(); |
| modelObserver.modelAdded.resetHistory(); |
| modelObserver.modelRemoved.resetHistory(); |
| |
| targetManager.setScopeTarget(target1); |
| sinon.assert.notCalled(targetObserver.targetAdded); |
| sinon.assert.notCalled(modelObserver.modelAdded); |
| sinon.assert.notCalled(targetObserver.targetRemoved); |
| sinon.assert.notCalled(modelObserver.modelRemoved); |
| }); |
| |
| it('notifies about inspected URL change', () => { |
| const targets = [createTarget(), createTarget()]; |
| const inspectedURLChangedEventListener = sinon.spy(); |
| targetManager.addEventListener(SDK.TargetManager.Events.INSPECTED_URL_CHANGED, inspectedURLChangedEventListener); |
| |
| targetManager.setScopeTarget(null); |
| assert.isTrue(inspectedURLChangedHostApi.notCalled && inspectedURLChangedEventListener.notCalled); |
| |
| targets.forEach(t => t.setInspectedURL(urlString`${`https://a.com/${t.id()}`}`)); |
| assert.isTrue(inspectedURLChangedHostApi.notCalled && inspectedURLChangedEventListener.notCalled); |
| |
| targetManager.setScopeTarget(targets[0]); |
| assert.isTrue(inspectedURLChangedHostApi.calledOnce && inspectedURLChangedEventListener.calledOnce); |
| assert.strictEqual(inspectedURLChangedHostApi.lastCall.firstArg, `https://a.com/${targets[0].id()}`); |
| assert.strictEqual(inspectedURLChangedEventListener.lastCall.firstArg.data, targets[0]); |
| |
| targetManager.setScopeTarget(targets[0]); |
| assert.isTrue(inspectedURLChangedHostApi.calledOnce && inspectedURLChangedEventListener.calledOnce); |
| |
| targets.forEach(t => t.setInspectedURL(urlString`${`https://b.com/${t.id()}`}`)); |
| assert.isTrue(inspectedURLChangedHostApi.calledTwice && inspectedURLChangedEventListener.calledTwice); |
| assert.strictEqual(inspectedURLChangedHostApi.lastCall.firstArg, `https://b.com/${targets[0].id()}`); |
| assert.strictEqual(inspectedURLChangedEventListener.lastCall.firstArg.data, targets[0]); |
| |
| targetManager.setScopeTarget(targets[1]); |
| assert.isTrue(inspectedURLChangedHostApi.calledThrice && inspectedURLChangedEventListener.calledThrice); |
| assert.strictEqual(inspectedURLChangedHostApi.lastCall.firstArg, `https://b.com/${targets[1].id()}`); |
| assert.strictEqual(inspectedURLChangedEventListener.lastCall.firstArg.data, targets[1]); |
| |
| targets.forEach(t => t.setInspectedURL(urlString`${`https://c.com/${t.id()}`}`)); |
| sinon.assert.callCount(inspectedURLChangedHostApi, 4); |
| sinon.assert.callCount(inspectedURLChangedEventListener, 4); |
| assert.strictEqual(inspectedURLChangedHostApi.lastCall.firstArg, `https://c.com/${targets[1].id()}`); |
| assert.strictEqual(inspectedURLChangedEventListener.lastCall.firstArg.data, targets[1]); |
| }); |
| }); |