blob: 719f9be9995fd8c717dd1adfaa31180dbe58e73d [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { MobxLitElement } from '@adobe/lit-mobx';
import '@material/mwc-button';
import '@material/mwc-icon';
import { css, customElement, html } from 'lit-element';
import { repeat } from 'lit-html/directives/repeat';
import { styleMap } from 'lit-html/directives/style-map';
import { computed, observable, reaction } from 'mobx';
import '../components/left_panel';
import '../components/test_filter';
import '../components/test_nav_tree';
import '../components/variant_entry';
import { VariantEntryElement } from '../components/variant_entry';
import { AppState, consumeAppState } from '../context/app_state/app_state';
import { consumeUserConfigs, UserConfigs } from '../context/app_state/user_configs';
import { consumeInvocationState, InvocationState } from '../context/invocation_state/invocation_state';
import { ReadonlyVariant, TestNode, VariantStatus } from '../models/test_node';
/**
* Display a list of test results.
*/
export class TestResultsTabElement extends MobxLitElement {
@observable.ref appState!: AppState;
@observable.ref userConfigs!: UserConfigs;
@observable.ref invocationState!: InvocationState;
private disposers: Array<() => void> = [];
private async loadNextPage() {
try {
await this.invocationState.testLoader.loadNextPage();
} catch (e) {
this.dispatchEvent(new ErrorEvent('error', {
error: e,
message: e.toString(),
composed: true,
bubbles: true,
}));
}
}
@computed
private get hasSingleVariant() {
// this operation should be fast since the iterator is executed only when
// there's only one test.
return this.invocationState.selectedNode.testCount === 1 && [...this.invocationState.selectedNode.tests()].length === 1;
}
private toggleAllVariants(expand: boolean) {
this.shadowRoot!.querySelectorAll<VariantEntryElement>('milo-variant-entry')
.forEach((e) => e.expanded = expand);
}
connectedCallback() {
super.connectedCallback();
this.appState.selectedTabId = 'test-results';
// When a new test loader is received, load the first page and reset the
// selected node.
this.disposers.push(reaction(
() => this.invocationState.testLoader,
(testLoader) => {
this.loadNextPage();
this.invocationState.selectedNode = testLoader.node;
},
{fireImmediately: true},
));
}
disconnectedCallback() {
super.disconnectedCallback();
for (const disposer of this.disposers) {
disposer();
}
}
private renderAllVariants() {
const exoneratedVariants: ReadonlyVariant[] = [];
const expectedVariants: ReadonlyVariant[] = [];
const unexpectedVariants: ReadonlyVariant[] = [];
const flakyVariants: ReadonlyVariant[] = [];
for (const test of this.invocationState.selectedNode.tests()) {
for (const variant of test.variants) {
switch (variant.status) {
case VariantStatus.Exonerated:
exoneratedVariants.push(variant);
break;
case VariantStatus.Expected:
expectedVariants.push(variant);
break;
case VariantStatus.Unexpected:
unexpectedVariants.push(variant);
break;
case VariantStatus.Flaky:
flakyVariants.push(variant);
break;
default:
console.error('unexpected variant type', variant);
break;
}
}
}
return html`
${unexpectedVariants.length === 0 ? html`
<div class="list-entry">No unexpected test results.</div>
<hr class="divider">
` : ''}
${this.renderVariants(unexpectedVariants)}
${this.renderVariants(flakyVariants)}
${this.renderVariants(exoneratedVariants)}
${this.renderVariants(expectedVariants)}
`;
}
private renderVariants(variants: ReadonlyVariant[]) {
return html`
${repeat(
variants.map((v, i, variants) => [variants[i-1], v, variants[i+1]] as [ReadonlyVariant | undefined, ReadonlyVariant, ReadonlyVariant | undefined]),
([_, v]) => `${v.testId} ${v.variantHash}`,
([prev, v, next]) => html`
<milo-variant-entry
.variant=${v}
.prevTestId=${prev?.testId ?? ''}
.prevVariant=${prev?.testId === v.testId ? prev : null}
.expanded=${this.hasSingleVariant}
.displayVariantId=${prev?.testId === v.testId || next?.testId === v.testId}
></milo-variant-entry>
`)}
${variants.length !== 0 ? html`<hr class="divider">` : ''}
`;
}
private renderMain() {
const state = this.invocationState;
if (state.initialized && !state.invocationId) {
return html`
<div id="no-invocation">
No associated invocation.<br>
You need to integrate with ResultDB to see the test results.<br>
See <a href="http://go/resultdb" target="_blank">go/resultdb</a>
or ask <a href="mailto: luci-eng@google.com" target="_blank">luci-eng@</a> for help.
</div>
`;
}
return html`
<milo-left-panel>
<milo-test-nav-tree
.testLoader=${state.testLoader}
.onSelectedNodeChanged=${(node: TestNode) => state.selectedNode = node}
></milo-test-nav-tree>
</milo-left-panel>
<div id="test-result-view">
${this.renderAllVariants()}
<div class="list-entry">
<span>Showing ${state.selectedNode.testCount} tests.</span>
<span
id="load"
style=${styleMap({'display': state.testLoader.done ? 'none' : ''})}
>
<span
id="load-more"
style=${styleMap({'display': state.testLoader.isLoading ? 'none' : ''})}
@click=${this.loadNextPage}
>
Load More
</span>
<span
style=${styleMap({'display': state.testLoader.isLoading ? '' : 'none'})}
>
Loading <milo-dot-spinner></milo-dot-spinner>
</span>
<mwc-icon class="inline-icon" title="Newly loaded entries might be inserted into the list.">info</mwc-icon>
</span>
</div>
<span
class="list-entry"
style=${styleMap({'display': this.userConfigs.hints.showResultDbIntegrationHint && !state.testLoader.isLoading ? '' : 'none'})}
>
Don't see results of your test framework here?
This might be because they are not integrated with ResultDB yet.
Please ask <a href="mailto: luci-eng@google.com" target="_blank">luci-eng@</a> for help.
<span
id="hide-hint"
@click=${() => this.userConfigs.hints.showResultDbIntegrationHint = false}
>Don't show again</span>
</span>
</div>
`;
}
protected render() {
return html`
<div id="header">
<milo-test-filter></milo-test-filter>
<mwc-button
dense unelevated
@click=${() => this.toggleAllVariants(true)}
>Expand All</mwc-button>
<mwc-button
dense unelevated
@click=${() => this.toggleAllVariants(false)}
>Collapse All</mwc-button>
</div>
<div id="main">${this.renderMain()}</div>
`;
}
static styles = css`
:host {
display: grid;
grid-template-rows: auto 1fr;
overflow-y: hidden;
}
#header {
display: grid;
grid-template-columns: 1fr auto auto;
grid-gap: 5px;
height: 28px;
padding: 5px 10px 3px 10px;
}
milo-test-filter {
margin: 5px;
margin-bottom: 0px;
}
#main {
display: flex;
border-top: 1px solid var(--divider-color);
overflow-y: hidden;
}
#no-invocation {
padding: 10px;
}
#test-result-view {
flex: 1;
overflow-y: auto;
padding-top: 5px;
}
#test-result-view>* {
margin-bottom: 2px;
}
.divider {
border: none;
border-top: 1px solid var(--divider-color);
}
milo-test-nav-tree {
overflow: hidden;
}
.list-entry {
margin-left: 5px;
margin-top: 5px;
}
#load {
color: var(--active-text-color);
}
#load-more {
color: var(--active-text-color);
cursor: pointer;
}
.inline-icon {
color: var(--default-text-color);
--mdc-icon-size: 1.2em;
vertical-align: bottom;
}
#hide-hint {
margin-left: 5px;
color: var(--active-text-color);
cursor: pointer;
}
`;
}
customElement('milo-test-results-tab')(
consumeInvocationState(
consumeUserConfigs(
consumeAppState(TestResultsTabElement),
),
),
);