[logging] Make LogViewActions lit
This component is now backed by a lit element.
Additional integration tests for the state updates and interactions
with other components through events have been added.
Bug: 100046
Change-Id: Ie5998c4e413482b956ab110a3680bb9a93f854f7
Reviewed-on: https://fuchsia-review.googlesource.com/c/vscode-plugins/+/729642
Reviewed-by: Solly Ross <sollyross@google.com>
Kokoro: Kokoro <noreply+kokoro@google.com>
diff --git a/media/OWNERS b/media/OWNERS
new file mode 100644
index 0000000..2c82e23
--- /dev/null
+++ b/media/OWNERS
@@ -0,0 +1 @@
+miguelfrde@google.com
diff --git a/media/webview-logging.css b/media/webview-logging.css
index 63ad059..16293fc 100644
--- a/media/webview-logging.css
+++ b/media/webview-logging.css
@@ -2,8 +2,6 @@
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. */
-@import '@vscode/codicons/dist/codicon.css';
-
:root {
--log-list-buffer: calc(100vh - 50px);
--log-control-height: 24px;
@@ -80,29 +78,6 @@
width: 100%;
}
-.action-icon {
- background: none;
- border: none;
- color: var(--vscode-icon-foreground);
- height: var(--log-action-height);
- padding: var(--input-padding-vertical) var(--input-padding-horizontal);
- text-align: center;
-}
-
-.action-icon:hover {
- background: var(--vscode-toolbar-hoverBackground);
- border-radius: 5px;
- cursor: pointer;
-}
-
-#wrap-logs[wrap-active] {
- color: var(--vscode-icon-foreground);
-}
-
-#wrap-logs:not([wrap-active]) {
- color: var(--vscode-disabledForeground);
-}
-
.column-resize {
cursor: col-resize;
height: 100%;
diff --git a/webviews/logging/components/base_element.ts b/webviews/logging/components/base_element.ts
index 9a773c2..aea49ca 100644
--- a/webviews/logging/components/base_element.ts
+++ b/webviews/logging/components/base_element.ts
@@ -8,8 +8,14 @@
import vsCodeStyle from '../../../media/vscode.css';
// @ts-ignore
import resetStyle from '../../../media/reset.css';
+// @ts-ignore
+import codiconStyle from '../../../node_modules/@vscode/codicons/dist/codicon.css';
@customElement('base-element')
export class BaseElement extends LitElement {
- static styles = css`${unsafeCSS(vsCodeStyle)} ${unsafeCSS(resetStyle)}` as CSSResultGroup;
+ static styles = css`
+ ${unsafeCSS(vsCodeStyle)}
+ ${unsafeCSS(resetStyle)}
+ ${unsafeCSS(codiconStyle)}
+ ` as CSSResultGroup;
}
diff --git a/webviews/logging/components/log_view_actions.ts b/webviews/logging/components/log_view_actions.ts
index 9063bf7..b04c962 100644
--- a/webviews/logging/components/log_view_actions.ts
+++ b/webviews/logging/components/log_view_actions.ts
@@ -1,76 +1,92 @@
// 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 { State } from '../src/state';
-import { LogList } from './log_list';
+import { css, html } from 'lit';
+import { customElement, property } from 'lit/decorators.js';
+import { BaseElement } from './base_element';
export const DONT_WRAP_LOGS: string = 'Don\'t wrap logs';
export const WRAP_LOGS: string = 'Wrap logs';
-export const WRAP_ACTIVE_ATTR: string = 'wrap-active';
+
+const ACTION_ICON_CLASSES: string = 'codicon icons action-icon';
/**
* Class to create action buttons that control the log list webview.
*/
-export class LogViewActions {
- private root: HTMLDivElement;
- private clear: HTMLDivElement;
- private logWrap: HTMLDivElement;
+@customElement('log-view-actions')
+export class LogViewActions extends BaseElement {
+ static styles = [
+ BaseElement.styles,
+ css`
+ .action-icon {
+ background: none;
+ border: none;
+ color: var(--vscode-icon-foreground);
+ height: var(--log-action-height);
+ padding: var(--input-padding-vertical) var(--input-padding-horizontal);
+ text-align: center;
+ }
- /**
- * Container of action buttons that interact with the log viewer.
- * @param {LogList} logList The log list webview.
- */
- constructor(private logList: LogList, private state: State) {
- this.root = document.createElement('div');
- this.root.id = 'log-view-actions';
+ .action-icon:hover {
+ background: var(--vscode-toolbar-hoverBackground);
+ border-radius: 5px;
+ cursor: pointer;
+ }
- // Clear logs button
- this.clear = document.createElement('div');
- this.clear.id = 'clear';
- this.clear.title = 'Clear Logs';
- this.clear.classList.add('codicon', 'codicon-clear-all', 'icons', 'action-icon');
+ #wrap-logs {
+ color: var(--vscode-disabledForeground);
+ }
- // Clear logs onclick
- this.clear.addEventListener('click', (e) => {
- this.logList.reset();
- });
+ :host([wrappingLogs]) #wrap-logs {
+ color: var(--vscode-icon-foreground);
+ }
+ `
+ ];
- // Wrap logs button to toggle log line text wrapping.
- this.logWrap = document.createElement('div');
- this.logWrap.id = 'wrap-logs';
- this.logWrap.classList.add('codicon', 'codicon-word-wrap', 'icons', 'action-icon');
- this.updateLogWrapBtn(this.state.shouldWrapLogs);
+ @property({ attribute: true, reflect: true, type: Boolean })
+ public wrappingLogs: boolean = false;
- this.logWrap.addEventListener('click', (e) => {
- this.state.shouldWrapLogs = !this.state.shouldWrapLogs;
- this.updateLogWrapBtn(this.state.shouldWrapLogs);
- });
+ public static readonly wrapLogsChangeEvent: string = 'wrap-logs-change';
+ public static readonly clearRequestedEvent: string = 'clear-requested';
- // TODO(fxbug.dev/99730): Save/Load Filter Button
- // TODO(fxbug.dev/95332): Field Visibility Dropdown
- this.root.appendChild(this.clear);
- this.root.appendChild(this.logWrap);
+ constructor(wrappingLogs: boolean) {
+ super();
+ this.wrappingLogs = wrappingLogs;
}
- /**
- * Returns the core element.
- */
- get element(): HTMLDivElement {
- return this.root;
+ render() {
+ const wrapLogsTitle = this.wrappingLogs ? DONT_WRAP_LOGS : WRAP_LOGS;
+
+ return html`
+ <div
+ id="clear"
+ title="Clear Logs"
+ class="${ACTION_ICON_CLASSES} codicon-clear-all"
+ @click="${this.onClearButtonClick}"
+ ></div>
+
+ <div
+ id="wrap-logs"
+ title="Wrap Logs"
+ class="${ACTION_ICON_CLASSES} codicon-word-wrap"
+ @click="${this.onWrapLogsButtonClick}"
+ title = "${wrapLogsTitle}"
+ ></div>
+ `;
}
- /**
- * Updates the log text wrap button state (attribute, title, etc) accordingly.
- * @param shouldWrapLogs whether or not the log wrapping is active
- */
- private updateLogWrapBtn(shouldWrapLogs: boolean) {
- this.logList.logWrapping = shouldWrapLogs;
- if (this.state.shouldWrapLogs) {
- this.logWrap.title = DONT_WRAP_LOGS;
- this.logWrap.setAttribute(WRAP_ACTIVE_ATTR, '');
- } else {
- this.logWrap.removeAttribute(WRAP_ACTIVE_ATTR);
- this.logWrap.title = WRAP_LOGS;
- }
+ private onClearButtonClick() {
+ this.dispatchEvent(new CustomEvent(LogViewActions.clearRequestedEvent, {
+ detail: {},
+ }));
+ }
+
+ private onWrapLogsButtonClick() {
+ this.wrappingLogs = !this.wrappingLogs;
+ this.dispatchEvent(new CustomEvent(LogViewActions.wrapLogsChangeEvent, {
+ detail: {
+ wrapLogs: this.wrappingLogs,
+ }
+ }));
}
}
diff --git a/webviews/logging/components/view.ts b/webviews/logging/components/view.ts
index ef00676..d1ef29a 100644
--- a/webviews/logging/components/view.ts
+++ b/webviews/logging/components/view.ts
@@ -16,25 +16,34 @@
private logViewActions: LogViewActions;
constructor(private state: State, private root: HTMLElement) {
- this.logActionContainer = document.createElement('div');
- this.logActionContainer.id = 'log-action-container';
- this.logListContainer = document.createElement('div');
- this.logListContainer.id = 'log-list-container';
+ this.logList = new LogList(state);
this.logControl = new LogControl(this.state.currentFilterText);
-
this.logControl.addEventListener(LogControl.filterChangeEvent, (e) => {
const event = e as CustomEvent;
const { filter, text } = event.detail;
this.state.registerFilter(filter, text);
});
- this.logList = new LogList(state);
- this.logViewActions = new LogViewActions(this.logList, this.state);
+ this.logViewActions = new LogViewActions(this.state.shouldWrapLogs);
+ this.logViewActions.addEventListener(LogViewActions.clearRequestedEvent, (e) => {
+ this.logList.reset();
+ });
+ this.logViewActions.addEventListener(LogViewActions.wrapLogsChangeEvent, (e) => {
+ const event = e as CustomEvent;
+ const { wrapLogs } = event.detail;
+ this.logList.logWrapping = wrapLogs;
+ this.state.shouldWrapLogs = wrapLogs;
+ });
+
+ this.logActionContainer = document.createElement('div');
+ this.logActionContainer.id = 'log-action-container';
+ this.logListContainer = document.createElement('div');
+ this.logListContainer.id = 'log-list-container';
this.root.appendChild(this.logActionContainer);
this.logActionContainer.appendChild(this.logControl);
- this.logActionContainer.appendChild(this.logViewActions.element);
+ this.logActionContainer.appendChild(this.logViewActions);
this.root.appendChild(this.logListContainer);
this.logListContainer.appendChild(this.logList.element);
diff --git a/webviews/logging/test/log_view_actions.test.ts b/webviews/logging/test/log_view_actions.test.ts
index 722aaaa..2beeb9b 100644
--- a/webviews/logging/test/log_view_actions.test.ts
+++ b/webviews/logging/test/log_view_actions.test.ts
@@ -2,12 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import { State } from '../src/state';
-import { FakeWebviewAPi, logDataForTest } from './util';
-import chaiDom from 'chai-dom'; // not esm
import chai from 'chai'; // not esm
-import { DONT_WRAP_LOGS, LogViewActions, WRAP_ACTIVE_ATTR, WRAP_LOGS } from '../components/log_view_actions';
-import { LogList, WRAP_LOG_TEXT_ATTR } from '../components/log_list';
+import chaiDom from 'chai-dom'; // not esm
+import { DONT_WRAP_LOGS, LogViewActions, WRAP_LOGS } from '../components/log_view_actions';
before(function () {
chai.should();
@@ -15,74 +12,91 @@
});
describe('LogViewActions', () => {
- let root: HTMLElement;
- let vscode: FakeWebviewAPi;
- let state: State;
- let logList: LogList;
+ let logViewActions: LogViewActions;
beforeEach(() => {
- root = document.createElement('div');
- document.body.appendChild(root);
- vscode = new FakeWebviewAPi();
- state = new State(vscode);
- logList = new LogList(state);
+ logViewActions = document.createElement('log-view-actions') as LogViewActions;
+ document.body.appendChild(logViewActions);
});
afterEach(() => {
- root.remove();
+ logViewActions.remove();
});
describe('#constructor', () => {
- it('crates log view actions buttons', () => {
- const logViewActions = new LogViewActions(logList, state);
- const el = logViewActions.element;
- el.children.length.should.equal(2);
- el.children[0].id.should.equal('clear');
+ 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');
});
- it('takes state into account when creating the log wrap btn', () => {
- state.shouldWrapLogs = false;
- const logViewActions = new LogViewActions(logList, state);
- const wrapLogsButton = logViewActions.element.querySelector('#wrap-logs') as HTMLDivElement;
- wrapLogsButton.hasAttribute(WRAP_ACTIVE_ATTR).should.be.false;
+ it('takes state into account when creating the log wrap btn', async () => {
+ let logViewActions = new LogViewActions(false);
+ document.body.appendChild(logViewActions);
+ await logViewActions.updateComplete;
+ let wrapLogsButton = logViewActions.shadowRoot!.querySelector('#wrap-logs') as HTMLDivElement;
+ logViewActions.wrappingLogs.should.be.false;
wrapLogsButton.title.should.equal(WRAP_LOGS);
- logList.element.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.false;
+ logViewActions.remove();
+
+ logViewActions = new LogViewActions(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);
});
});
describe('on clear button press', () => {
- it('reset the log list', () => {
- const logViewActions = new LogViewActions(logList, state);
- const el = logViewActions.element;
- const clearButton = el.querySelector('#clear') as HTMLDivElement;
- logList.addLog(logDataForTest('core/foo'));
- const logsList = logList.element;
- logsList.children.length.should.equal(2);
- clearButton.dispatchEvent(new MouseEvent('click'));
- logsList.children.length.should.equal(1);
+ it('emits clear requested event', async () => {
+ await logViewActions.updateComplete;
+ const clearButton = logViewActions.shadowRoot!.querySelector('#clear') as HTMLDivElement;
+ const promise = new Promise<{}>((resolve) => {
+ logViewActions.addEventListener(LogViewActions.clearRequestedEvent, (e) => {
+ resolve({});
+ });
+ });
+ clearButton.click();
+ await promise;
});
});
describe('on wrap logs button press', () => {
- it('sets a wrap-log-text attribute in the log list', () => {
- const logViewActions = new LogViewActions(logList, state);
- const el = logViewActions.element;
- const wrapLogsButton = el.querySelector('#wrap-logs') as HTMLDivElement;
- wrapLogsButton.hasAttribute(WRAP_ACTIVE_ATTR).should.be.true;
- logList.element.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.true;
+ it('emits log wrapping event and updates itself', async () => {
+ await logViewActions.updateComplete;
+ const wrapLogsButton = logViewActions
+ .shadowRoot!.querySelector('#wrap-logs') as HTMLDivElement;
+ let promise = new Promise<{ wrapLogs: boolean }>((resolve) => {
+ logViewActions.addEventListener(LogViewActions.wrapLogsChangeEvent, (e) => {
+ const event = e as CustomEvent;
+ resolve(event.detail);
+ });
+ });
+ wrapLogsButton.click();
+
+ let payload = await promise;
+ payload.wrapLogs.should.be.true;
+ await logViewActions.updateComplete;
+ logViewActions.wrappingLogs.should.be.true;
wrapLogsButton.title.should.equal(DONT_WRAP_LOGS);
- state.shouldWrapLogs.should.be.true;
- wrapLogsButton.dispatchEvent(new MouseEvent('click'));
- wrapLogsButton.hasAttribute(WRAP_ACTIVE_ATTR).should.be.false;
- logList.element.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.false;
+ // Clicking again should toggle the state.
+
+ promise = new Promise<{ wrapLogs: boolean }>((resolve) => {
+ logViewActions.addEventListener(LogViewActions.wrapLogsChangeEvent, (e) => {
+ const event = e as CustomEvent;
+ resolve(event.detail);
+ });
+ });
+ wrapLogsButton.click();
+
+ payload = await promise;
+ payload.wrapLogs.should.be.false;
+ await logViewActions.updateComplete;
+ logViewActions.wrappingLogs.should.be.false;
wrapLogsButton.title.should.equal(WRAP_LOGS);
- state.shouldWrapLogs.should.be.false;
-
- wrapLogsButton.dispatchEvent(new MouseEvent('click'));
- wrapLogsButton.hasAttribute(WRAP_ACTIVE_ATTR).should.be.true;
- logList.element.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.true;
- state.shouldWrapLogs.should.be.true;
});
});
});
diff --git a/webviews/logging/test/view.test.ts b/webviews/logging/test/view.test.ts
index 884875b..37b5b9d 100644
--- a/webviews/logging/test/view.test.ts
+++ b/webviews/logging/test/view.test.ts
@@ -3,13 +3,15 @@
// found in the LICENSE file.
import { CURRENT_VERSION, State } from '../src/state';
-import { FakeWebviewAPi } from './util';
+import { FakeWebviewAPi, logDataForTest } from './util';
import { LoggingView } from '../components/view';
import * as constant from '../src/constants';
import chai from 'chai';
import chaiDom from 'chai-dom'; // not esm
import { LogControl } from '../components/log_control';
import { Filter, FilterExpression } from '../src/filter';
+import { LogViewActions } from '../components/log_view_actions';
+import { WRAP_LOG_TEXT_ATTR } from '../components/log_list';
before(function () {
chai.should();
@@ -46,7 +48,7 @@
const container = root.children[0];
container.children.length.should.equal(2);
container.children[0].tagName.toLowerCase().should.equal('log-control');
- container.children[1].id.should.equal('log-view-actions');
+ container.children[1].tagName.toLowerCase().should.equal('log-view-actions');
});
it('initializes log control with the current state filters', async () => {
@@ -56,13 +58,34 @@
fields: constant.LOGS_HEADERS,
wrappingLogs: true
});
- // @ts-ignore
new LoggingView(new State(vscode), root);
let logControl = root.getElementsByTagName('log-control')[0] as LogControl;
await logControl.updateComplete;
logControl.value.should.equal(filterText);
});
+ it('initializes log actions view with the current state wrapping logs', async () => {
+ vscode.setState({
+ version: CURRENT_VERSION,
+ filter: filterText,
+ fields: constant.LOGS_HEADERS,
+ wrappingLogs: true
+ });
+ new LoggingView(new State(vscode), root);
+ let logViewActions = root.getElementsByTagName('log-view-actions')[0] as LogViewActions;
+ await logViewActions.updateComplete;
+ logViewActions.wrappingLogs.should.be.true;
+ });
+
+ it('crates log list container with log list', () => {
+ new LoggingView(state, root);
+ const container = root.children[1];
+ container.children.length.should.equal(1);
+ container.children[0].id.should.equal('log-list');
+ });
+ });
+
+ describe('log control integration', () => {
it('updates the state on log control filter changes', async () => {
new LoggingView(state, root);
let logControl = root.getElementsByTagName('log-control')[0] as LogControl;
@@ -84,12 +107,46 @@
]));
state.currentFilterText.should.deep.equal(filterText);
});
+ });
- it('crates log list container with log list', () => {
- new LoggingView(state, root);
- const container = root.children[1];
- container.children.length.should.equal(1);
- container.children[0].id.should.equal('log-list');
+ describe('log actions view integration', () => {
+ it('clears the log list when the clear button is clicked', async () => {
+ const view = new LoggingView(state, root);
+ const logList = root.querySelector('#log-list') as HTMLElement;
+ view.addLog(logDataForTest('core/foo'));
+ logList.children.length.should.equal(2);
+
+ let logViewActions = root.getElementsByTagName('log-view-actions')[0] as LogViewActions;
+ await logViewActions.updateComplete;
+ let clearButton = logViewActions.shadowRoot!.querySelector('#clear') as HTMLDivElement;
+ clearButton.click();
+ logList.children.length.should.equal(1);
+ });
+
+ it('updates the state and log list when the wrap logs button is clicked', async () => {
+ const view = new LoggingView(state, root);
+ const logList = root.querySelector('#log-list') as HTMLElement;
+ view.addLog(logDataForTest('core/foo'));
+ logList.children.length.should.equal(2);
+
+ let logViewActions = root.getElementsByTagName('log-view-actions')[0] as LogViewActions;
+ await logViewActions.updateComplete;
+ let wrapLogsButton = logViewActions.shadowRoot!.querySelector('#wrap-logs') as HTMLDivElement;
+
+ // Clicking the button for the first should update the other elements to true.
+ // Since we started on true (default state value).
+ wrapLogsButton.click();
+ await logViewActions.updateComplete;
+
+ state.shouldWrapLogs.should.be.false;
+ logList.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.false;
+
+ // Clicking the button again should update the other elements to false.
+ wrapLogsButton.click();
+ await logViewActions.updateComplete;
+
+ state.shouldWrapLogs.should.be.true;
+ logList.hasAttribute(WRAP_LOG_TEXT_ATTR).should.be.true;
});
});
});