| // Copyright 2022 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import * as assert from 'assert'; |
| import * as vscode from 'vscode'; |
| |
| import { describe, it } from 'mocha'; |
| import { createSandbox, SinonSandbox, SinonStubbedInstance } from 'sinon'; |
| |
| import { LoggingViewProvider, TOGGLE_LOG_VIEW_COMMAND } from '../../../logging/view_provider'; |
| import { Ffx, FuchsiaDevice } from '../../../ffx'; |
| import { JsonStreamProcess } from '../../../process'; |
| import { StubbedSpawn } from '../utils'; |
| import { ChildProcessWithoutNullStreams } from 'child_process'; |
| |
| describe('Logging view provider tests', function () { |
| const sandbox = createSandbox(); |
| let ffx: SinonStubbedInstance<Ffx>; |
| let uri: vscode.Uri; |
| let provider: LoggingViewProvider; |
| let targetEventEmitter: vscode.EventEmitter<FuchsiaDevice>; |
| let executeCommandStub: any; |
| |
| this.beforeEach(() => { |
| executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand'); |
| uri = vscode.Uri.file('/fake/file'); |
| targetEventEmitter = new vscode.EventEmitter<FuchsiaDevice>(); |
| ffx = sandbox.createStubInstance(Ffx); |
| (ffx as Ffx).onSetTarget = targetEventEmitter.event; |
| provider = new LoggingViewProvider(uri, ffx); |
| }); |
| |
| this.afterEach(function () { |
| sandbox.restore(); |
| }); |
| |
| describe('visibility changes', () => { |
| it('stops the ffx process when the view is hidden', () => { |
| let fakeProcess = sandbox.createStubInstance(JsonStreamProcess); |
| ffx.runFfxJsonStreaming.returns(fakeProcess); |
| let provider = new LoggingViewProvider(uri, ffx); |
| let visibilityEmitter = new vscode.EventEmitter<void>(); |
| let view = fakeWebviewView( |
| sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| |
| // This should trigger the creation of a process. |
| visibilityEmitter.fire(); |
| |
| (view as any).visible = false; |
| visibilityEmitter.fire(); |
| assert.ok(fakeProcess.stop.calledOnce); |
| }); |
| |
| it('spawns an ffx process when the view appears', () => { |
| let visibilityEmitter = new vscode.EventEmitter<void>(); |
| let view = fakeWebviewView( |
| sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| visibilityEmitter.fire(); |
| assert.ok(ffx.runFfxJsonStreaming.calledOnce); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[0], undefined); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[1], ['log']); |
| }); |
| |
| it('spawns an ffx process when the view is resolved', () => { |
| let visibilityEmitter = new vscode.EventEmitter<void>(); |
| let view = fakeWebviewView( |
| sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| assert.ok(ffx.runFfxJsonStreaming.calledOnce); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[0], undefined); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[1], ['log']); |
| }); |
| }); |
| |
| describe('add a log', () => { |
| it('pushes a log to the webview when received', () => { |
| let process = new StubbedSpawn(sandbox); |
| let ffx = new Ffx('foo'); |
| sandbox.stub(ffx, 'runFfxStreaming').returns( |
| process.spawnEvent as ChildProcessWithoutNullStreams); |
| let provider = new LoggingViewProvider(uri, ffx); |
| |
| let visibilityEmitter = new vscode.EventEmitter<void>(); |
| let view = fakeWebviewView( |
| sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| |
| // The view should be visible now. |
| visibilityEmitter.fire(); |
| |
| const data = '{"data":{"TargetLog": {}}}'; |
| process.spawnEvent.stdout?.emit('data', data); |
| |
| let webview = view.webview as sinon.SinonStubbedInstance<FakeWebview>; |
| assert.ok(webview.postMessage.calledOnce); |
| assert.deepStrictEqual( |
| webview.postMessage.getCall(0).args[0], |
| { type: 'addLog', log: JSON.parse(data) } |
| ); |
| }); |
| |
| it('handles logs that contain new lines', () => { |
| let process = new StubbedSpawn(sandbox); |
| let ffx = new Ffx('foo'); |
| sandbox.stub(ffx, 'runFfxStreaming').returns( |
| process.spawnEvent as ChildProcessWithoutNullStreams); |
| let provider = new LoggingViewProvider(uri, ffx); |
| |
| let visibilityEmitter = new vscode.EventEmitter<void>(); |
| let view = fakeWebviewView( |
| sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| |
| // Creates the process |
| visibilityEmitter.fire(); |
| |
| const data = `{"data":{"TargetLog": {"payload": {"message": "foo\\nbar"}}}} |
| {"data": {"TargetLog": {"payload": {"message": "baz"}}}}`; |
| process.stdout.emit('data', data); |
| let webview = view.webview as SinonStubbedInstance<FakeWebview>; |
| assert.ok(webview.postMessage.calledTwice); |
| assert.deepStrictEqual( |
| webview.postMessage.getCall(0).args[0], |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| { type: 'addLog', log: { data: { TargetLog: { payload: { message: 'foo\nbar' } } } } } |
| ); |
| assert.deepStrictEqual( |
| webview.postMessage.getCall(1).args[0], |
| // eslint-disable-next-line @typescript-eslint/naming-convention |
| { type: 'addLog', log: { data: { TargetLog: { payload: { message: 'baz' } } } } } |
| ); |
| }); |
| }); |
| |
| describe('#resetAndShowView', () => { |
| it('toggles the view when there is no previous view', async () => { |
| await provider.resetAndShowView(undefined); |
| |
| assert.ok(executeCommandStub.calledOnce); |
| assert.strictEqual( |
| executeCommandStub.getCall(0).args[0], TOGGLE_LOG_VIEW_COMMAND); |
| }); |
| |
| it('changes the current device and toggles the view when no previous view', async () => { |
| assert.strictEqual(provider.currentTargetDevice, undefined); |
| await provider.resetAndShowView(new FuchsiaDevice({ nodename: 'foo' })); |
| assert.strictEqual(provider.currentTargetDevice, 'foo'); |
| assert.ok(executeCommandStub.calledOnce); |
| assert.strictEqual( |
| executeCommandStub.getCall(0).args[0], TOGGLE_LOG_VIEW_COMMAND); |
| }); |
| |
| it('resets the view when the device changes and the view is visible', async () => { |
| let view = fakeWebviewView( |
| sandbox, true, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| assert.ok(ffx.runFfxJsonStreaming.calledOnce); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[0], undefined); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[1], ['log']); |
| |
| await provider.resetAndShowView(new FuchsiaDevice({ nodename: 'foo' })); |
| let webview = view.webview as SinonStubbedInstance<FakeWebview>; |
| assert.ok(webview.postMessage.calledOnce); |
| assert.deepStrictEqual(webview.postMessage.getCall(0).args[0], { type: 'reset' }); |
| assert.ok(ffx.runFfxJsonStreaming.calledTwice); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(1).args[0], 'foo'); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[1], ['log']); |
| }); |
| |
| it('shows the view when the device changes and the view is not visible', async () => { |
| let view = fakeWebviewView( |
| sandbox, false, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| await provider.resetAndShowView(new FuchsiaDevice({ nodename: 'foo' })); |
| assert.ok((view as any).show.calledOnce); |
| }); |
| }); |
| |
| describe('on default target change', () => { |
| it('changes the current device and resets the view when visible', () => { |
| let view = fakeWebviewView( |
| sandbox, true, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| |
| targetEventEmitter.fire(new FuchsiaDevice({ nodename: 'bar' })); |
| assert.strictEqual(provider.currentTargetDevice, 'bar'); |
| |
| let webview = view.webview as SinonStubbedInstance<FakeWebview>; |
| assert.deepStrictEqual(webview.postMessage.getCall(0).args[0], { type: 'reset' }); |
| }); |
| |
| it('changes the current device when the view is not visible', () => { |
| let view = fakeWebviewView( |
| sandbox, false, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| |
| targetEventEmitter.fire(new FuchsiaDevice({ nodename: 'bar' })); |
| assert.strictEqual(provider.currentTargetDevice, 'bar'); |
| |
| let webview = view.webview as SinonStubbedInstance<FakeWebview>; |
| assert.ok(!webview.postMessage.calledOnce); |
| }); |
| |
| it('does nothing when the device is the same', async () => { |
| let view = fakeWebviewView( |
| sandbox, false, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event); |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| await provider.resetAndShowView(new FuchsiaDevice({ nodename: 'foo' })); |
| |
| targetEventEmitter.fire(new FuchsiaDevice({ nodename: 'foo' })); |
| assert.strictEqual(provider.currentTargetDevice, 'foo'); |
| |
| let webview = view.webview as SinonStubbedInstance<FakeWebview>; |
| assert.ok(!webview.postMessage.calledOnce); |
| }); |
| }); |
| |
| describe('when receiving messages from the webview', () => { |
| it('handles pause commands and stops the ffx log process', () => { |
| let fakeProcess = sandbox.createStubInstance(JsonStreamProcess); |
| ffx.runFfxJsonStreaming.returns(fakeProcess); |
| let provider = new LoggingViewProvider(uri, ffx); |
| let webviewMessagesEmitter = new vscode.EventEmitter<{ command: string }>(); |
| let view = fakeWebviewView( |
| sandbox, true, new vscode.EventEmitter<void>().event, webviewMessagesEmitter.event); |
| |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| assert.ok(ffx.runFfxJsonStreaming.calledOnce); |
| assert.ok(fakeProcess.stop.notCalled); |
| |
| webviewMessagesEmitter.fire({ command: 'pauseLogStreaming' }); |
| |
| // No more calls should have happened on runFfxJsonStreaming and the process should have |
| // been stopped. |
| assert.ok(ffx.runFfxJsonStreaming.calledOnce); |
| assert.ok(fakeProcess.stop.calledOnce); |
| }); |
| |
| it('handles resume commands and starts a ffx log process', () => { |
| let webviewMessagesEmitter = new vscode.EventEmitter<{ command: string }>(); |
| let view = fakeWebviewView( |
| sandbox, true, new vscode.EventEmitter<any>().event, webviewMessagesEmitter.event); |
| |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| assert.ok(ffx.runFfxJsonStreaming.calledOnce); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[1], ['log']); |
| |
| webviewMessagesEmitter.fire({ command: 'resumeLogStreaming' }); |
| |
| assert.ok(ffx.runFfxJsonStreaming.calledTwice); |
| assert.deepStrictEqual( |
| ffx.runFfxJsonStreaming.getCall(1).args[1], ['log', '--since', 'now']); |
| }); |
| |
| it('handles getMonotonicTime commands and starts an ffx target get-time process', () => { |
| let webviewMessagesEmitter = new vscode.EventEmitter<{ command: string }>(); |
| let view = fakeWebviewView( |
| sandbox, true, new vscode.EventEmitter<any>().event, webviewMessagesEmitter.event); |
| |
| provider.resolveWebviewView(view, { |
| state: undefined |
| }, { |
| isCancellationRequested: false, |
| onCancellationRequested: sandbox.stub() |
| }); |
| assert.ok(ffx.runFfxJsonStreaming.calledOnce); |
| assert.deepStrictEqual(ffx.runFfxJsonStreaming.getCall(0).args[1], ['log']); |
| |
| webviewMessagesEmitter.fire({ command: 'getMonotonicTime' }); |
| |
| assert.ok(ffx.runFfx.calledOnce); |
| assert.deepStrictEqual( |
| ffx.runFfx.getCall(0).args[0], ['target', 'get-time']); |
| }); |
| }); |
| }); |
| |
| /** |
| * Allows to fake the webview. |
| * |
| * @param sandbox the sinon sandbox backing up the test |
| * @param visible whether the view should be visible or not |
| * @param onDidChangeVisibility event fired when the visibility of the view changes |
| * @returns a fake webview |
| */ |
| function fakeWebviewView( |
| sandbox: SinonSandbox, visible: boolean, |
| onDidChangeVisibility: vscode.Event<void>, |
| onDidReceiveMessage: vscode.Event<any>, |
| ): vscode.WebviewView { |
| let webview = sandbox.createStubInstance(FakeWebview); |
| webview.onDidReceiveMessage = onDidReceiveMessage as any; |
| webview.postMessage = sandbox.stub(); |
| webview.asWebviewUri.returns(vscode.Uri.file('/fake/file')); |
| |
| let view = <vscode.WebviewView>{ visible }; |
| // These exist in the WebviewView interface, but when doing |
| // `sandbox.stub(view, 'prop')`, sinon claims they don't exist. When attempting to |
| // assign directly, tsc correctly complains that they are readonly properties. |
| (view as any).onDidChangeVisibility = onDidChangeVisibility; |
| (view as any).show = sandbox.stub(); |
| (view as any).webview = webview; |
| return view; |
| } |
| |
| class FakeWebview implements vscode.Webview { |
| options: vscode.WebviewOptions = <vscode.WebviewOptions>{}; |
| html: string = ''; |
| onDidReceiveMessage: vscode.Event<any> = <vscode.Event<any>>{}; |
| postMessage(message: any): Thenable<boolean> { |
| throw new Error('Method not implemented.'); |
| } |
| asWebviewUri(localResource: vscode.Uri): vscode.Uri { |
| throw new Error('Method not implemented.'); |
| } |
| cspSource: string = ''; |
| } |