blob: a5d0643e017bd632d4babd46cbca82bd15da27a4 [file] [log] [blame]
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import '../../../test/common-test-setup';
import './gr-flows';
import {assert, fixture, html} from '@open-wc/testing';
import {GrFlows} from './gr-flows';
import {FlowInfo, FlowStageState, Timestamp} from '../../../api/rest-api';
import {queryAndAssert} from '../../../test/test-utils';
import {NumericChangeId} from '../../../types/common';
import sinon from 'sinon';
import {GrButton} from '../../shared/gr-button/gr-button';
import {GrDialog} from '../../shared/gr-dialog/gr-dialog';
import {FlowsModel, flowsModelToken} from '../../../models/flows/flows-model';
import {testResolver} from '../../../test/common-test-setup';
suite('gr-flows tests', () => {
let element: GrFlows;
let clock: sinon.SinonFakeTimers;
let flowsModel: FlowsModel;
setup(async () => {
clock = sinon.useFakeTimers();
flowsModel = testResolver(flowsModelToken);
// The model is created by the DI system. The test setup replaces the real
// model with a mock. To prevent real API calls, we stub the reload method.
sinon.stub(flowsModel, 'reload');
element = await fixture<GrFlows>(html`<gr-flows></gr-flows>`);
element['changeNum'] = 123 as NumericChangeId;
await element.updateComplete;
});
teardown(() => {
clock.restore();
});
test('renders create flow component and no flows', async () => {
flowsModel.setState({flows: [], loading: false, isEnabled: true});
await element.updateComplete;
assert.shadowDom.equal(
element,
/* HTML */ `
<div class="container">
<h2 class="main-heading">Create new flow</h2>
<gr-create-flow></gr-create-flow>
<hr />
<p>No flows found for this change.</p>
</div>
<dialog id="deleteFlowModal">
<gr-dialog confirm-label="Delete">
<div class="header" slot="header">Delete Flow</div>
<div class="main" slot="main">
Are you sure you want to delete this flow?
</div>
</gr-dialog>
</dialog>
`,
{ignoreAttributes: ['role']}
);
});
test('renders flows', async () => {
const flows: FlowInfo[] = [
{
uuid: 'flow1',
owner: {name: 'owner1'},
created: '2025-01-01T10:00:00.000Z' as Timestamp,
last_evaluated: '2025-01-01T11:00:00.000Z' as Timestamp,
stages: [
{
expression: {condition: 'label:Code-Review=+1'},
state: FlowStageState.DONE,
},
],
},
{
uuid: 'flow2',
owner: {name: 'owner2'},
created: '2025-01-02T10:00:00.000Z' as Timestamp,
stages: [
{
expression: {
condition: 'label:Verified=+1',
action: {name: 'submit'},
},
state: FlowStageState.PENDING,
},
],
},
];
flowsModel.setState({flows, loading: false, isEnabled: true});
await element.updateComplete;
// prettier formats the spacing for "last evaluated" incorrectly
assert.shadowDom.equal(
element,
/* prettier-ignore */ /* HTML */ `
<div class="container">
<h2 class="main-heading">Create new flow</h2>
<gr-create-flow></gr-create-flow>
<hr />
<div>
<div class="heading-with-button">
<h2 class="main-heading">Existing Flows</h2>
<gr-button
aria-label="Refresh flows"
link=""
title="Refresh flows"
>
<gr-icon icon="refresh"></gr-icon>
</gr-button>
</div>
<md-filled-select label="Filter by status">
<md-select-option value="all">
<div slot="headline">All</div>
</md-select-option>
<md-select-option value="DONE">
<div slot="headline">DONE</div>
</md-select-option>
<md-select-option value="FAILED">
<div slot="headline">FAILED</div>
</md-select-option>
<md-select-option value="PENDING">
<div slot="headline">PENDING</div>
</md-select-option>
<md-select-option value="TERMINATED">
<div slot="headline">TERMINATED</div>
</md-select-option>
</md-filled-select>
<div class="flow">
<div class="flow-header">
<gr-button link title="Delete flow">
<gr-icon icon="delete" filled></gr-icon>
</gr-button>
</div>
<div class="flow-id hidden">Flow flow1</div>
<div>
Created:
<gr-date-formatter withtooltip></gr-date-formatter>
</div>
<div>
Last Evaluated:
<gr-date-formatter withtooltip></gr-date-formatter>
</div>
<table>
<thead>
<tr>
<th>Status</th>
<th>Condition</th>
<th>Action</th>
<th>Parameters</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<gr-icon
aria-label="done"
filled
icon="check_circle"
></gr-icon>
</td>
<td>label:Code-Review=+1</td>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
<div class="flow">
<div class="flow-header">
<gr-button link title="Delete flow">
<gr-icon icon="delete" filled></gr-icon>
</button>
</div>
<div class="flow-id hidden">Flow flow2</div>
<div>
Created:
<gr-date-formatter withtooltip></gr-date-formatter>
</div>
<table>
<thead>
<tr>
<th>Status</th>
<th>Condition</th>
<th>Action</th>
<th>Parameters</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<gr-icon aria-label="pending" icon="timelapse"></gr-icon>
</td>
<td>label:Verified=+1</td>
<td>submit</td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<dialog id="deleteFlowModal">
<gr-dialog confirm-label="Delete">
<div class="header" slot="header">Delete Flow</div>
<div class="main" slot="main">
Are you sure you want to delete this flow?
</div>
</gr-dialog>
</dialog>
`,
{
ignoreAttributes: [
'style',
'class',
'account',
'changenum',
'datestr',
'aria-disabled',
'role',
'tabindex',
'md-menu-item',
],
}
);
});
test('deletes a flow after confirmation', async () => {
const flows: FlowInfo[] = [
{
uuid: 'flow1',
owner: {name: 'owner1'},
created: '2025-01-01T10:00:00.000Z' as Timestamp,
stages: [
{
expression: {condition: 'label:Code-Review=+1'},
state: FlowStageState.DONE,
},
],
},
];
const deleteFlowStub = sinon.stub(flowsModel, 'deleteFlow');
flowsModel.setState({flows, loading: false, isEnabled: true});
await element.updateComplete;
const deleteButton = queryAndAssert<GrButton>(element, '.flow gr-button');
deleteButton.click();
await element.updateComplete;
const dialog = queryAndAssert<HTMLDialogElement>(
element,
'#deleteFlowModal'
);
assert.isTrue(dialog.open);
const grDialog = queryAndAssert<GrDialog>(dialog, 'gr-dialog');
const confirmButton = queryAndAssert<GrButton>(grDialog, '#confirm');
confirmButton.click();
await element.updateComplete;
assert.isTrue(deleteFlowStub.calledOnceWith('flow1'));
});
test('cancel deleting a flow', async () => {
const flows: FlowInfo[] = [
{
uuid: 'flow1',
owner: {name: 'owner1'},
created: '2025-01-01T10:00:00.000Z' as Timestamp,
stages: [
{
expression: {condition: 'label:Code-Review=+1'},
state: FlowStageState.DONE,
},
],
},
];
const deleteFlowStub = sinon.stub(flowsModel, 'deleteFlow');
flowsModel.setState({flows, loading: false, isEnabled: true});
await element.updateComplete;
const deleteButton = queryAndAssert<GrButton>(element, '.flow gr-button');
deleteButton.click();
await element.updateComplete;
const dialog = queryAndAssert<HTMLDialogElement>(
element,
'#deleteFlowModal'
);
assert.isTrue(dialog.open);
const grDialog = queryAndAssert<GrDialog>(dialog, 'gr-dialog');
const cancelButton = queryAndAssert<GrButton>(grDialog, '#cancel');
cancelButton.click();
await element.updateComplete;
assert.isTrue(deleteFlowStub.notCalled);
assert.isFalse(dialog.open);
});
test('refreshes flows on button click', async () => {
const flow = {
uuid: 'flow1',
owner: {name: 'owner1'},
created: '2025-01-01T10:00:00.000Z' as Timestamp,
stages: [],
} as FlowInfo;
flowsModel.setState({flows: [flow], loading: false, isEnabled: true});
await element.updateComplete;
const reloadStub = flowsModel.reload as sinon.SinonStub;
reloadStub.resetHistory();
const refreshButton = queryAndAssert<GrButton>(
element,
'.heading-with-button gr-button'
);
refreshButton.click();
await element.updateComplete;
assert.isTrue(reloadStub.calledOnce);
});
suite('filter', () => {
const flows: FlowInfo[] = [
{
uuid: 'flow-done',
owner: {name: 'owner1'},
created: '2025-01-01T10:00:00.000Z' as Timestamp,
stages: [
{expression: {condition: 'cond-done'}, state: FlowStageState.DONE},
],
},
{
uuid: 'flow-pending',
owner: {name: 'owner2'},
created: '2025-01-02T10:00:00.000Z' as Timestamp,
stages: [
{
expression: {condition: 'cond-pending'},
state: FlowStageState.PENDING,
},
],
},
{
uuid: 'flow-failed',
owner: {name: 'owner3'},
created: '2025-01-03T10:00:00.000Z' as Timestamp,
stages: [
{
expression: {condition: 'cond-failed'},
state: FlowStageState.FAILED,
},
],
},
{
uuid: 'flow-terminated',
owner: {name: 'owner4'},
created: '2025-01-04T10:00:00.000Z' as Timestamp,
stages: [
{
expression: {condition: 'cond-terminated'},
state: FlowStageState.TERMINATED,
},
],
},
];
setup(async () => {
flowsModel.setState({flows, loading: false, isEnabled: true});
await element.updateComplete;
});
test('shows all flows by default', () => {
const flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 4);
});
test('filters by DONE', async () => {
element['statusFilter'] = FlowStageState.DONE;
await element.updateComplete;
const flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 1);
assert.include(flowElements[0].textContent, 'cond-done');
});
test('filters by PENDING', async () => {
element['statusFilter'] = FlowStageState.PENDING;
await element.updateComplete;
const flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 1);
assert.include(flowElements[0].textContent, 'cond-pending');
});
test('filters by FAILED', async () => {
element['statusFilter'] = FlowStageState.FAILED;
await element.updateComplete;
const flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 1);
assert.include(flowElements[0].textContent, 'cond-failed');
});
test('filters by TERMINATED', async () => {
element['statusFilter'] = FlowStageState.TERMINATED;
await element.updateComplete;
const flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 1);
assert.include(flowElements[0].textContent, 'cond-terminated');
});
test('shows all when filter is changed to all', async () => {
element['statusFilter'] = FlowStageState.DONE;
await element.updateComplete;
let flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 1);
element['statusFilter'] = 'all';
await element.updateComplete;
flowElements = element.shadowRoot!.querySelectorAll('.flow');
assert.equal(flowElements.length, 4);
});
});
});