blob: edb531fdc0f03bb1e155eb5c6c0ae2c5fcad9562 [file] [log] [blame]
// 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 = '';
}