[logging] Introduce play/pause functionality
Screencast:
https://screencast.googleplex.com/cast/NDg2MjA3MjI0OTY0NzEwNHwzNGZlZTE5NC0zYg
Bug: 109719
Change-Id: Ic4e2f19a503fa0e180a409a0868ae5b07286cb67
Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/731663
Kokoro: Kokoro <noreply+kokoro@google.com>
Reviewed-by: Solly Ross <sollyross@google.com>
Reviewed-by: Amy Hu <amyhu@google.com>
diff --git a/media/webview-logging.css b/media/webview-logging.css
index 16293fc..f07c533 100644
--- a/media/webview-logging.css
+++ b/media/webview-logging.css
@@ -69,6 +69,11 @@
color: var(--vscode-list-errorForeground);
}
+.log-entry[data-moniker="<VSCode>"] {
+ line-height: 75%;
+ opacity: 70%;
+}
+
#log-action-container {
display: flex;
height: var(--log-action-container-height);
diff --git a/src/ffx.ts b/src/ffx.ts
index 47a89d8..81e5b3d 100644
--- a/src/ffx.ts
+++ b/src/ffx.ts
@@ -311,17 +311,23 @@
* Runs ffx --target [device] --machine json log
*
* @param device the name of the target device to use on ffx log
+ * @param args additional arguments for the `ffx log` process.
* @param onData when data is received this command will be called
* @returns the ffx log process
*/
- public runLog(device: string | undefined, onData: (data: Object) => void): FfxLog {
- let args = (device) ? ['--target', device] : [];
- args = args.concat(['--machine', 'json', 'log']);
+ public runLog(
+ device: string | undefined,
+ args: string[],
+ onData: (data: Object) => void
+ ): FfxLog {
+ let ffxArgs = (device ? ['--target', device] : []);
+ ffxArgs.push('--machine', 'json', 'log');
+ ffxArgs.push(...args);
return new JsonStreamProcess(
- this.runFfxStreaming(args),
+ this.runFfxStreaming(ffxArgs),
onData,
(data) => {
- logger.warn(`Error [${this.path} ${args}]: ${data}`);
+ logger.warn(`Error [${this.path} ${ffxArgs}]: ${data}`);
});
}
diff --git a/src/logging/view_provider.ts b/src/logging/view_provider.ts
index a70a9a1..430049a 100644
--- a/src/logging/view_provider.ts
+++ b/src/logging/view_provider.ts
@@ -59,8 +59,15 @@
webviewView.webview.html = this._getHtmlForWebview(webviewView.webview);
- webviewView.webview.onDidReceiveMessage(data => {
- // No messages expected yet coming out of the webview.
+ webviewView.webview.onDidReceiveMessage(message => {
+ switch (message.command) {
+ case 'pauseLogStreaming':
+ this.process?.stop();
+ break;
+ case 'resumeLogStreaming':
+ this.startListeningForLogs('now');
+ break;
+ }
});
this.lastVisibilitySeen = this.view.visible;
@@ -111,9 +118,10 @@
await this.view?.webview.postMessage({ type: 'reset' });
}
- private startListeningForLogs() {
+ private startListeningForLogs(since?: string) {
this.process?.stop();
- this.process = this.ffx.runLog(this.currentDevice, (data: Object) => {
+ const args = since ? ['--since', since] : [];
+ this.process = this.ffx.runLog(this.currentDevice, args, (data: Object) => {
// this was... not marked async before, but postMessage is an async operation
// so there's not much to do about this here. We've gotta just go async & hope.
this.addLog(data).catch((err) => logger.error('unable to show log line', undefined, err));
diff --git a/src/test/suite/ffx.test.ts b/src/test/suite/ffx.test.ts
index 53bb938..7269e86 100644
--- a/src/test/suite/ffx.test.ts
+++ b/src/test/suite/ffx.test.ts
@@ -577,7 +577,7 @@
let received;
const TEST_DATA: Object = { 'foo': 'bar' };
stubbedSpawn.spawnStubInfo.callsFake(() => stubbedSpawn.spawnEvent);
- ffx.runLog('foo', (data) => {
+ ffx.runLog('foo', [], (data) => {
received = data;
});
assert.deepStrictEqual(
@@ -592,7 +592,7 @@
// spy on the singleton instead of the module method, since we can't
// modify singletons and such with ESM
let spiedWarn = sandbox.spy(logger.logger, 'warn');
- ffx.runLog('foo', (_) => { });
+ ffx.runLog('foo', [], (_) => { });
stubbedSpawn.spawnEvent.stderr?.emit('data', 'oh no');
assert(spiedWarn.calledOnce);
assert(
diff --git a/src/test/suite/logging/view_provider.test.ts b/src/test/suite/logging/view_provider.test.ts
index eaf171c..d118e9b 100644
--- a/src/test/suite/logging/view_provider.test.ts
+++ b/src/test/suite/logging/view_provider.test.ts
@@ -39,8 +39,9 @@
let fakeProcess = sandbox.createStubInstance(JsonStreamProcess);
ffx.runLog.returns(fakeProcess);
let provider = new LoggingViewProvider(uri, ffx);
- let emitter = new vscode.EventEmitter<void>();
- let view = fakeWebviewView(sandbox, true, emitter.event);
+ let visibilityEmitter = new vscode.EventEmitter<void>();
+ let view = fakeWebviewView(
+ sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -49,30 +50,32 @@
});
// This should trigger the creation of a process.
- emitter.fire();
+ visibilityEmitter.fire();
(view as any).visible = false;
- emitter.fire();
+ visibilityEmitter.fire();
assert.ok(fakeProcess.stop.calledOnce);
});
it('spawns an ffx process when the view appears', () => {
- let emitter = new vscode.EventEmitter<void>();
- let view = fakeWebviewView(sandbox, true, emitter.event);
+ 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()
});
- emitter.fire();
+ visibilityEmitter.fire();
assert.ok(ffx.runLog.calledOnce);
assert.deepStrictEqual(ffx.runLog.getCall(0).args[0], undefined);
});
it('spawns an ffx process when the view is resolved', () => {
- let emitter = new vscode.EventEmitter<void>();
- let view = fakeWebviewView(sandbox, true, emitter.event);
+ let visibilityEmitter = new vscode.EventEmitter<void>();
+ let view = fakeWebviewView(
+ sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -88,11 +91,13 @@
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);
+ sandbox.stub(ffx, 'runFfxStreaming').returns(
+ process.spawnEvent as ChildProcessWithoutNullStreams);
let provider = new LoggingViewProvider(uri, ffx);
- let emitter = new vscode.EventEmitter<void>();
- let view = fakeWebviewView(sandbox, true, emitter.event);
+ let visibilityEmitter = new vscode.EventEmitter<void>();
+ let view = fakeWebviewView(
+ sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -101,7 +106,7 @@
});
// The view should be visible now.
- emitter.fire();
+ visibilityEmitter.fire();
const data = '{"data":{"TargetLog": {}}}';
process.spawnEvent.stdout?.emit('data', data);
@@ -117,11 +122,13 @@
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);
+ sandbox.stub(ffx, 'runFfxStreaming').returns(
+ process.spawnEvent as ChildProcessWithoutNullStreams);
let provider = new LoggingViewProvider(uri, ffx);
- let emitter = new vscode.EventEmitter<void>();
- let view = fakeWebviewView(sandbox, true, emitter.event);
+ let visibilityEmitter = new vscode.EventEmitter<void>();
+ let view = fakeWebviewView(
+ sandbox, true, visibilityEmitter.event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -130,7 +137,7 @@
});
// Creates the process
- emitter.fire();
+ visibilityEmitter.fire();
const data = `{"data":{"TargetLog": {"payload": {"message": "foo\\nbar"}}}}
{"data": {"TargetLog": {"payload": {"message": "baz"}}}}`;
@@ -169,7 +176,8 @@
});
it('resets the view when the device changes and the view is visible', async () => {
- let view = fakeWebviewView(sandbox, true, new vscode.EventEmitter<any>().event);
+ let view = fakeWebviewView(
+ sandbox, true, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -187,7 +195,8 @@
});
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);
+ let view = fakeWebviewView(
+ sandbox, false, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -201,7 +210,8 @@
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);
+ let view = fakeWebviewView(
+ sandbox, true, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -217,7 +227,8 @@
});
it('changes the current device when the view is not visible', () => {
- let view = fakeWebviewView(sandbox, false, new vscode.EventEmitter<any>().event);
+ let view = fakeWebviewView(
+ sandbox, false, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -233,7 +244,8 @@
});
it('does nothing when the device is the same', async () => {
- let view = fakeWebviewView(sandbox, false, new vscode.EventEmitter<any>().event);
+ let view = fakeWebviewView(
+ sandbox, false, new vscode.EventEmitter<any>().event, new vscode.EventEmitter<any>().event);
provider.resolveWebviewView(view, {
state: undefined
}, {
@@ -249,6 +261,51 @@
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.runLog.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.runLog.calledOnce);
+ assert.ok(fakeProcess.stop.notCalled);
+
+ webviewMessagesEmitter.fire({ command: 'pauseLogStreaming' });
+
+ // No more calls should have happened on runLog and the process should have been stopped.
+ assert.ok(ffx.runLog.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.runLog.calledOnce);
+
+ webviewMessagesEmitter.fire({ command: 'resumeLogStreaming' });
+
+ assert.ok(ffx.runLog.calledTwice);
+ assert.deepStrictEqual(ffx.runLog.getCall(1).args[1], ['--since', 'now']);
+ });
+ });
});
/**
@@ -261,13 +318,13 @@
*/
function fakeWebviewView(
sandbox: SinonSandbox, visible: boolean,
- onDidChangeVisibility: vscode.Event<any>,
+ onDidChangeVisibility: vscode.Event<void>,
+ onDidReceiveMessage: vscode.Event<any>,
): vscode.WebviewView {
let webview = sandbox.createStubInstance(FakeWebview);
- webview.onDidReceiveMessage = sandbox.stub();
+ webview.onDidReceiveMessage = onDidReceiveMessage as any;
webview.postMessage = sandbox.stub();
webview.asWebviewUri.returns(vscode.Uri.file('/fake/file'));
- sandbox.spy(webview.onDidReceiveMessage);
let view = <vscode.WebviewView>{ visible };
// These exist in the WebviewView interface, but when doing
diff --git a/webviews/logging/components/log_row.ts b/webviews/logging/components/log_row.ts
index 6714da6..01c3841 100644
--- a/webviews/logging/components/log_row.ts
+++ b/webviews/logging/components/log_row.ts
@@ -25,6 +25,8 @@
} else if (this.log.data.SymbolizedTargetLog !== undefined) {
this.symbolized = this.log.data.SymbolizedTargetLog[1];
this.formatRow(this.log.data.SymbolizedTargetLog[0]);
+ } else if (this.log.data.ViewerEvent !== undefined) {
+ this.formatViewerEventRow(this.log.data.ViewerEvent);
}
}
@@ -84,6 +86,7 @@
/**
* Formats row for Malformed Logs.
+ * @param malformedLog the malformed log to render as a message.
*/
private formatMalformedRow(malformedLog: string) {
const fields = { 'message': formatMalformedLog(malformedLog) };
@@ -92,6 +95,16 @@
}
/**
+ * Formats row for an viewer synthesizedd log.
+ * @param message the message to display.
+ */
+ private formatViewerEventRow(message: string) {
+ const fields = { message, moniker: constant.VSCODE_SYNTHETIC_MONIKER };
+ this.createLogRowAttributes(fields);
+ this.createLogInnerHtml(fields);
+ }
+
+ /**
* Formats row for Target Log and Symbolized Logs.
*/
private formatRow(log: LogData) {
diff --git a/webviews/logging/components/log_view_actions.ts b/webviews/logging/components/log_view_actions.ts
index 0782fe8..03b3f7d 100644
--- a/webviews/logging/components/log_view_actions.ts
+++ b/webviews/logging/components/log_view_actions.ts
@@ -8,8 +8,16 @@
export const DONT_WRAP_LOGS: string = 'Don\'t wrap logs';
export const WRAP_LOGS: string = 'Wrap logs';
+export const RESUME_STREAMING_LOGS: string = 'Resume streaming logs';
+export const PAUSE_STREAMING_LOGS: string = 'Pause streaming logs';
+
const ACTION_ICON_CLASSES: string = 'codicon icons action-icon';
+export interface LogViewActionsParameters {
+ wrappingLogs?: boolean;
+ playActive?: boolean;
+}
+
/**
* Class to create action buttons that control the log list webview.
*/
@@ -47,19 +55,33 @@
@property({ attribute: true, reflect: true, type: Boolean })
public wrappingLogs: boolean;
+ @property({ attribute: true, reflect: true, type: Boolean })
+ public playActive: boolean = false;
+
public static readonly wrapLogsChangeEvent: string = 'wrap-logs-change';
public static readonly clearRequestedEvent: string = 'clear-requested';
+ public static readonly playPauseChangeEvent: string = 'play-pause-change';
- constructor(wrappingLogs: boolean) {
+ constructor(params: LogViewActionsParameters | undefined) {
super();
- this.wrappingLogs = wrappingLogs;
+ this.wrappingLogs = params?.wrappingLogs ?? false;
+ this.playActive = params?.playActive ?? false;
}
render() {
const wrapLogsTitle = this.wrappingLogs ? DONT_WRAP_LOGS : WRAP_LOGS;
+ const playPauseTitle = this.playActive ? PAUSE_STREAMING_LOGS : RESUME_STREAMING_LOGS;
+ const playPauseIcon = this.playActive ? 'debug-pause' : 'play';
return html`
<div
+ id="play-pause"
+ title="${playPauseTitle}"
+ class="${ACTION_ICON_CLASSES} codicon-${playPauseIcon}"
+ @click="${this.onPlayPauseButtonClick}"
+ ></div>
+
+ <div
id="clear"
title="Clear Logs"
class="${ACTION_ICON_CLASSES} codicon-clear-all"
@@ -90,4 +112,13 @@
}
}));
}
+
+ private onPlayPauseButtonClick() {
+ this.playActive = !this.playActive;
+ this.dispatchEvent(new CustomEvent(LogViewActions.playPauseChangeEvent, {
+ detail: {
+ playActive: this.playActive,
+ }
+ }));
+ }
}
diff --git a/webviews/logging/components/view.ts b/webviews/logging/components/view.ts
index d1ef29a..3daa5f7 100644
--- a/webviews/logging/components/view.ts
+++ b/webviews/logging/components/view.ts
@@ -8,14 +8,24 @@
import { State } from '../src/state';
import { LogList } from './log_list';
-export class LoggingView {
+export const RESUMING_LOG_STREAMING_LOG = 'Resuming log streaming, press [pause icon] to pause.';
+export const PAUSING_LOG_STREAMING_LOG = 'Playback has been paused, press [play icon] to resume.';
+
+export type ExternalActionRequestEvent =
+ | { type: 'pause-log-streaming' }
+ | { type: 'resume-log-streaming' };
+
+export class LoggingView extends EventTarget {
private logActionContainer: HTMLDivElement;
private logControl: LogControl;
private logList: LogList;
private logListContainer: HTMLDivElement;
private logViewActions: LogViewActions;
+ public static readonly externalActionRequestEvent: string = 'external-action-request-event';
+
constructor(private state: State, private root: HTMLElement) {
+ super();
this.logList = new LogList(state);
this.logControl = new LogControl(this.state.currentFilterText);
@@ -25,7 +35,14 @@
this.state.registerFilter(filter, text);
});
- this.logViewActions = new LogViewActions(this.state.shouldWrapLogs);
+ this.logViewActions = new LogViewActions({
+ wrappingLogs: this.state.shouldWrapLogs,
+ /**
+ * TODO(fxbug.dev/109719): make this behavior persisted and automatic
+ * with focus changes
+ */
+ playActive: true
+ });
this.logViewActions.addEventListener(LogViewActions.clearRequestedEvent, (e) => {
this.logList.reset();
});
@@ -35,6 +52,11 @@
this.logList.logWrapping = wrapLogs;
this.state.shouldWrapLogs = wrapLogs;
});
+ this.logViewActions.addEventListener(LogViewActions.playPauseChangeEvent, (e) => {
+ const event = e as CustomEvent;
+ const { playActive } = event.detail;
+ this.onPlayPauseChangeEvent(playActive);
+ });
this.logActionContainer = document.createElement('div');
this.logActionContainer.id = 'log-action-container';
@@ -65,4 +87,29 @@
this.logList.reset();
this.logControl.reset();
}
+
+ /**
+ * Handles a play/pause event emitted by the log actions view.
+ * @param playActive whether or not the streaming of logs is active
+ */
+ private onPlayPauseChangeEvent(playActive: boolean) {
+ const detail: ExternalActionRequestEvent =
+ playActive
+ ? { type: 'resume-log-streaming' }
+ : { type: 'pause-log-streaming' };
+ this.dispatchEvent(new CustomEvent(LoggingView.externalActionRequestEvent, {
+ detail,
+ }));
+
+ this.logList.addLog({
+ data: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ ViewerEvent: playActive
+ ? RESUMING_LOG_STREAMING_LOG
+ : PAUSING_LOG_STREAMING_LOG
+ },
+ timestamp: Date.now(),
+ version: 0
+ });
+ }
}
diff --git a/webviews/logging/index.ts b/webviews/logging/index.ts
index dfe859e..e379cfc 100644
--- a/webviews/logging/index.ts
+++ b/webviews/logging/index.ts
@@ -2,25 +2,51 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import { LoggingView } from './components/view';
-import { State } from './src/state';
+import { WebviewApi } from 'vscode-webview';
+import { ExternalActionRequestEvent, LoggingView } from './components/view';
+import { PersistedState, State } from './src/state';
//@ts-check
// This script will be run within the webview itself
// It cannot access the main VS Code APIs directly.
(function () {
- const view = new LoggingView(new State(acquireVsCodeApi()), document.body);
+ const vscode = acquireVsCodeApi() as WebviewApi<PersistedState>;
+ const view = new LoggingView(new State(vscode), document.body);
+
+ // TODO(fxbug.dev/109719): move this to the persisted state.
+ let playing = true;
// Handle messages sent from the extension to the webview
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'addLog':
- view.addLog(message.log);
+ if (playing) {
+ view.addLog(message.log);
+ }
break;
case 'reset':
view.reset();
}
});
+
+ // Forward webview messages to the extension.
+ view.addEventListener(LoggingView.externalActionRequestEvent, (e) => {
+ const event = (e as CustomEvent).detail as ExternalActionRequestEvent;
+ switch (event.type) {
+ case 'pause-log-streaming':
+ playing = false;
+ vscode.postMessage({
+ command: 'pauseLogStreaming'
+ });
+ break;
+ case 'resume-log-streaming':
+ playing = true;
+ vscode.postMessage({
+ command: 'resumeLogStreaming'
+ });
+ break;
+ }
+ });
}());
diff --git a/webviews/logging/src/constants.ts b/webviews/logging/src/constants.ts
index c352e72..234a846 100644
--- a/webviews/logging/src/constants.ts
+++ b/webviews/logging/src/constants.ts
@@ -8,6 +8,7 @@
export const MONIKER_FILTER_LABEL = 'Moniker:';
export const FFX_MONIKER = '<ffx>';
+export const VSCODE_SYNTHETIC_MONIKER = '<VSCode>';
export const SEARCH_PLACEHOLDER = 'Filter logs...';
diff --git a/webviews/logging/src/log_data.ts b/webviews/logging/src/log_data.ts
index 8f9e16c..aede082 100644
--- a/webviews/logging/src/log_data.ts
+++ b/webviews/logging/src/log_data.ts
@@ -14,6 +14,7 @@
FfxEvent?: FfxEventData,
MalformedTargetLog?: string;
SymbolizedTargetLog?: [LogData, string];
+ ViewerEvent?: string;
}
export type FfxEventData = 'TargetDisconnected' | 'LoggingStarted';
diff --git a/webviews/logging/test/log_row.test.ts b/webviews/logging/test/log_row.test.ts
index fc56f75..efad7f7 100644
--- a/webviews/logging/test/log_row.test.ts
+++ b/webviews/logging/test/log_row.test.ts
@@ -185,5 +185,21 @@
msgEl.innerText.should.equal(msg);
});
+ it('formats viewer synthesized messages', () => {
+ const msg = 'Logs are cool';
+ const data = {
+ data: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ ViewerEvent: msg,
+ },
+ timestamp: 0,
+ version: 0
+ };
+ const logRow = new LogRow(data, false).element;
+ const monikerEl = logRow.children[3] as HTMLElement;
+ const msgEl = logRow.children[6] as HTMLElement;
+ monikerEl.innerText.should.equal(constant.VSCODE_SYNTHETIC_MONIKER);
+ msgEl.innerText.should.equal(msg);
+ });
});
});
diff --git a/webviews/logging/test/log_view_actions.test.ts b/webviews/logging/test/log_view_actions.test.ts
index 2beeb9b..ecf9ecb 100644
--- a/webviews/logging/test/log_view_actions.test.ts
+++ b/webviews/logging/test/log_view_actions.test.ts
@@ -4,7 +4,7 @@
import chai from 'chai'; // not esm
import chaiDom from 'chai-dom'; // not esm
-import { DONT_WRAP_LOGS, LogViewActions, WRAP_LOGS } from '../components/log_view_actions';
+import { DONT_WRAP_LOGS, LogViewActions, PAUSE_STREAMING_LOGS, RESUME_STREAMING_LOGS, WRAP_LOGS } from '../components/log_view_actions';
before(function () {
chai.should();
@@ -26,13 +26,14 @@
describe('#constructor', () => {
it('crates log view actions buttons', async () => {
await logViewActions.updateComplete;
- logViewActions.shadowRoot!.children.length.should.equal(2);
- logViewActions.shadowRoot!.children[0].id.should.equal('clear');
- logViewActions.shadowRoot!.children[1].id.should.equal('wrap-logs');
+ logViewActions.shadowRoot!.children.length.should.equal(3);
+ logViewActions.shadowRoot!.children[0].id.should.equal('play-pause');
+ logViewActions.shadowRoot!.children[1].id.should.equal('clear');
+ logViewActions.shadowRoot!.children[2].id.should.equal('wrap-logs');
});
- it('takes state into account when creating the log wrap btn', async () => {
- let logViewActions = new LogViewActions(false);
+ it('correctly initializes the log-wrap btn', async () => {
+ let logViewActions = new LogViewActions({ wrappingLogs: false });
document.body.appendChild(logViewActions);
await logViewActions.updateComplete;
let wrapLogsButton = logViewActions.shadowRoot!.querySelector('#wrap-logs') as HTMLDivElement;
@@ -40,13 +41,32 @@
wrapLogsButton.title.should.equal(WRAP_LOGS);
logViewActions.remove();
- logViewActions = new LogViewActions(true);
+ logViewActions = new LogViewActions({ wrappingLogs: true });
document.body.appendChild(logViewActions);
await logViewActions.updateComplete;
wrapLogsButton = logViewActions.shadowRoot!.querySelector('#wrap-logs') as HTMLDivElement;
logViewActions.wrappingLogs.should.be.true;
wrapLogsButton.title.should.equal(DONT_WRAP_LOGS);
});
+
+ it('correctly initializes the play-pause btn', async () => {
+ let logViewActions = new LogViewActions({ playActive: true });
+ document.body.appendChild(logViewActions);
+ await logViewActions.updateComplete;
+ let playPauseButton = logViewActions.shadowRoot!.querySelector('#play-pause') as HTMLDivElement;
+ logViewActions.playActive.should.be.true;
+ playPauseButton.title.should.equal(PAUSE_STREAMING_LOGS);
+ playPauseButton.classList.contains('codicon-debug-pause').should.be.true;
+ logViewActions.remove();
+
+ logViewActions = new LogViewActions({ playActive: false });
+ document.body.appendChild(logViewActions);
+ await logViewActions.updateComplete;
+ playPauseButton = logViewActions.shadowRoot!.querySelector('#play-pause') as HTMLDivElement;
+ logViewActions.playActive.should.be.false;
+ playPauseButton.title.should.equal(RESUME_STREAMING_LOGS);
+ playPauseButton.classList.contains('codicon-play').should.be.true;
+ });
});
describe('on clear button press', () => {
@@ -99,4 +119,44 @@
wrapLogsButton.title.should.equal(WRAP_LOGS);
});
});
+
+ describe('on play/pause button press', () => {
+ it('emits a play-pause-change event and updates itself', async () => {
+ await logViewActions.updateComplete;
+ const playPauseButton = logViewActions
+ .shadowRoot!.querySelector('#play-pause') as HTMLDivElement;
+ let promise = new Promise<{ playActive: boolean }>((resolve) => {
+ logViewActions.addEventListener(LogViewActions.playPauseChangeEvent, (e) => {
+ const event = e as CustomEvent;
+ resolve(event.detail);
+ });
+ });
+ playPauseButton.click();
+
+ let payload = await promise;
+ payload.playActive.should.be.true;
+ await logViewActions.updateComplete;
+ logViewActions.playActive.should.be.true;
+ playPauseButton.title.should.equal(PAUSE_STREAMING_LOGS);
+ playPauseButton.classList.contains('codicon-debug-pause').should.be.true;
+ playPauseButton.classList.contains('codicon-play').should.be.false;
+
+ // Clicking again should toggle the state.
+
+ promise = new Promise<{ playActive: boolean }>((resolve) => {
+ logViewActions.addEventListener(LogViewActions.playPauseChangeEvent, (e) => {
+ const event = e as CustomEvent;
+ resolve(event.detail);
+ });
+ });
+ playPauseButton.click();
+
+ payload = await promise;
+ payload.playActive.should.be.false;
+ await logViewActions.updateComplete;
+ playPauseButton.title.should.equal(RESUME_STREAMING_LOGS);
+ playPauseButton.classList.contains('codicon-play').should.be.true;
+ playPauseButton.classList.contains('codicon-pause').should.be.false;
+ });
+ });
});
diff --git a/webviews/logging/test/view.test.ts b/webviews/logging/test/view.test.ts
index 37b5b9d..e86cb8c 100644
--- a/webviews/logging/test/view.test.ts
+++ b/webviews/logging/test/view.test.ts
@@ -4,7 +4,7 @@
import { CURRENT_VERSION, State } from '../src/state';
import { FakeWebviewAPi, logDataForTest } from './util';
-import { LoggingView } from '../components/view';
+import { ExternalActionRequestEvent, LoggingView, PAUSING_LOG_STREAMING_LOG, RESUMING_LOG_STREAMING_LOG } from '../components/view';
import * as constant from '../src/constants';
import chai from 'chai';
import chaiDom from 'chai-dom'; // not esm
@@ -148,5 +148,52 @@
state.shouldWrapLogs.should.be.true;
logList.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.true;
});
+
+ it('renders a synthetic log about pause/play and emits events', async () => {
+ const view = new LoggingView(state, root);
+ const logList = root.querySelector('#log-list') as HTMLElement;
+
+ let logViewActions = root.getElementsByTagName('log-view-actions')[0] as LogViewActions;
+ await logViewActions.updateComplete;
+ let playPauseButton =
+ logViewActions.shadowRoot!.querySelector('#play-pause') as HTMLDivElement;
+
+ let promise = new Promise<ExternalActionRequestEvent>((resolve) => {
+ view.addEventListener(LoggingView.externalActionRequestEvent, (e) => {
+ const event = e as CustomEvent;
+ resolve(event.detail);
+ });
+ });
+
+ playPauseButton.click();
+ let event = await promise;
+ await logViewActions.updateComplete;
+
+ event.should.deep.equal({ type: 'pause-log-streaming' });
+ let logLine = logList.children[1] as HTMLElement;
+ (logLine.querySelector('#message')! as HTMLTableCellElement)
+ .innerText.should.equal(PAUSING_LOG_STREAMING_LOG);
+ (logLine.querySelector('#moniker')! as HTMLTableCellElement)
+ .innerText.should.equal(constant.VSCODE_SYNTHETIC_MONIKER);
+
+ // Clicking the button again should show the "Play" state.
+ promise = new Promise<ExternalActionRequestEvent>((resolve) => {
+ view.addEventListener(LoggingView.externalActionRequestEvent, (e) => {
+ const event = e as CustomEvent;
+ resolve(event.detail);
+ });
+ });
+
+ playPauseButton.click();
+ event = await promise;
+ await logViewActions.updateComplete;
+
+ event.should.deep.equal({ type: 'resume-log-streaming' });
+ logLine = logList.children[2] as HTMLElement;
+ (logLine.querySelector('#message')! as HTMLTableCellElement)
+ .innerText.should.equal(RESUMING_LOG_STREAMING_LOG);
+ (logLine.querySelector('#moniker')! as HTMLTableCellElement)
+ .innerText.should.equal(constant.VSCODE_SYNTHETIC_MONIKER);
+ });
});
});