| /** |
| * @license |
| * Copyright 2022 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import * as sinon from 'sinon'; |
| import {createChange} from '../../../test/test-data-generators'; |
| import { |
| NumericChangeId, |
| ChangeInfo, |
| ChangeStatus, |
| HttpMethod, |
| PatchSetNum, |
| } from '../../../api/rest-api'; |
| import {GrChangeListBulkAbandonFlow} from './gr-change-list-bulk-abandon-flow'; |
| import '../../../test/common-test-setup'; |
| import { |
| BulkActionsModel, |
| bulkActionsModelToken, |
| LoadingState, |
| } from '../../../models/bulk-actions/bulk-actions-model'; |
| import './gr-change-list-bulk-abandon-flow'; |
| import {fixture, waitUntil, assert} from '@open-wc/testing'; |
| import {wrapInProvider} from '../../../models/di-provider-element'; |
| import {html} from 'lit'; |
| import {getAppContext} from '../../../services/app-context'; |
| import { |
| waitUntilObserved, |
| stubRestApi, |
| queryAndAssert, |
| mockPromise, |
| query, |
| } from '../../../test/test-utils'; |
| import {GrButton} from '../../shared/gr-button/gr-button'; |
| import {ProgressStatus} from '../../../constants/constants'; |
| import {RequestPayload} from '../../../types/common'; |
| import {ErrorCallback} from '../../../api/rest'; |
| |
| const change1: ChangeInfo = {...createChange(), _number: 1 as NumericChangeId}; |
| const change2: ChangeInfo = {...createChange(), _number: 2 as NumericChangeId}; |
| |
| suite('gr-change-list-bulk-abandon-flow tests', () => { |
| let element: GrChangeListBulkAbandonFlow; |
| let model: BulkActionsModel; |
| let getChangesStub: sinon.SinonStub; |
| |
| async function selectChange(change: ChangeInfo) { |
| model.addSelectedChangeNum(change._number); |
| await waitUntilObserved(model.selectedChangeNums$, selectedChangeNums => |
| selectedChangeNums.includes(change._number) |
| ); |
| await element.updateComplete; |
| } |
| |
| setup(async () => { |
| model = new BulkActionsModel(getAppContext().restApiService); |
| getChangesStub = stubRestApi('getDetailedChangesWithActions'); |
| |
| element = ( |
| await fixture( |
| wrapInProvider( |
| html`<gr-change-list-bulk-abandon-flow></gr-change-list-bulk-abandon-flow>`, |
| bulkActionsModelToken, |
| model |
| ) |
| ) |
| ).querySelector('gr-change-list-bulk-abandon-flow')!; |
| await element.updateComplete; |
| }); |
| |
| test('render', async () => { |
| const changes: ChangeInfo[] = [{...change1, actions: {abandon: {}}}]; |
| getChangesStub.returns(changes); |
| model.sync(changes); |
| await waitUntilObserved( |
| model.loadingState$, |
| state => state === LoadingState.LOADED |
| ); |
| await selectChange(change1); |
| await element.updateComplete; |
| |
| assert.shadowDom.equal( |
| element, |
| /* HTML */ ` |
| <gr-button |
| aria-disabled="false" |
| flatten="" |
| id="abandon" |
| role="button" |
| tabindex="0" |
| > |
| Abandon |
| </gr-button> |
| <dialog id="actionModal" tabindex="-1"> |
| <gr-dialog role="dialog"> |
| <div slot="header">1 changes to abandon</div> |
| <div slot="main"> |
| <table> |
| <thead> |
| <tr> |
| <th>Subject</th> |
| <th>Status</th> |
| </tr> |
| </thead> |
| <tbody> |
| <tr> |
| <td>Change: Test subject</td> |
| <td id="status">Status: NOT STARTED</td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </gr-dialog> |
| </dialog> |
| ` |
| ); |
| }); |
| |
| test('button state updates as changes are updated', async () => { |
| const changes: ChangeInfo[] = [{...change1, actions: {abandon: {}}}]; |
| getChangesStub.returns(changes); |
| model.sync(changes); |
| await waitUntilObserved( |
| model.loadingState$, |
| state => state === LoadingState.LOADED |
| ); |
| await selectChange(change1); |
| await element.updateComplete; |
| |
| assert.isFalse(queryAndAssert<GrButton>(element, '#abandon').disabled); |
| |
| changes.push({...change2, actions: {}}); |
| getChangesStub.restore(); |
| getChangesStub.returns(changes); |
| model.sync(changes); |
| await waitUntilObserved( |
| model.loadingState$, |
| state => state === LoadingState.LOADED |
| ); |
| await selectChange(change2); |
| await element.updateComplete; |
| |
| assert.isTrue(queryAndAssert<GrButton>(element, '#abandon').disabled); |
| }); |
| |
| test('abandon button is enabled if change is already abandoned', async () => { |
| const changes: ChangeInfo[] = [ |
| {...change1, actions: {}, status: ChangeStatus.ABANDONED}, |
| ]; |
| getChangesStub.returns(changes); |
| model.sync(changes); |
| await waitUntilObserved( |
| model.loadingState$, |
| state => state === LoadingState.LOADED |
| ); |
| await selectChange(change1); |
| await element.updateComplete; |
| |
| assert.isFalse(queryAndAssert<GrButton>(element, '#abandon').disabled); |
| |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').click(); |
| |
| await waitUntil( |
| () => |
| queryAndAssert<HTMLTableDataCellElement>( |
| element, |
| '#status' |
| ).innerText.trim() === `Status: ${ProgressStatus.SUCCESSFUL}` |
| ); |
| }); |
| |
| test('progress updates as request is resolved', async () => { |
| const changes: ChangeInfo[] = [{...change1, actions: {abandon: {}}}]; |
| getChangesStub.returns(changes); |
| model.sync(changes); |
| await waitUntilObserved( |
| model.loadingState$, |
| state => state === LoadingState.LOADED |
| ); |
| await selectChange(change1); |
| await element.updateComplete; |
| |
| assert.equal( |
| queryAndAssert<HTMLTableDataCellElement>( |
| element, |
| '#status' |
| ).innerText.trim(), |
| `Status: ${ProgressStatus.NOT_STARTED}` |
| ); |
| |
| const executeChangeAction = mockPromise<Response>(); |
| stubRestApi('executeChangeAction').returns(executeChangeAction); |
| |
| assert.isNotOk( |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').disabled |
| ); |
| assert.isNotOk( |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').disabled |
| ); |
| |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').click(); |
| await element.updateComplete; |
| |
| assert.isTrue( |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').disabled |
| ); |
| assert.isTrue( |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').disabled |
| ); |
| |
| assert.equal( |
| queryAndAssert<HTMLTableDataCellElement>( |
| element, |
| '#status' |
| ).innerText.trim(), |
| `Status: ${ProgressStatus.RUNNING}` |
| ); |
| |
| executeChangeAction.resolve(new Response()); |
| await waitUntil( |
| () => |
| element.progress.get(1 as NumericChangeId) === ProgressStatus.SUCCESSFUL |
| ); |
| |
| assert.isTrue( |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').disabled |
| ); |
| assert.isNotOk( |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').disabled |
| ); |
| |
| assert.equal( |
| queryAndAssert<HTMLTableDataCellElement>( |
| element, |
| '#status' |
| ).innerText.trim(), |
| `Status: ${ProgressStatus.SUCCESSFUL}` |
| ); |
| }); |
| |
| test('failures are reflected to the progress dialog', async () => { |
| const changes: ChangeInfo[] = [{...change1, actions: {abandon: {}}}]; |
| getChangesStub.returns(changes); |
| model.sync(changes); |
| await waitUntilObserved( |
| model.loadingState$, |
| state => state === LoadingState.LOADED |
| ); |
| await selectChange(change1); |
| await element.updateComplete; |
| |
| assert.equal( |
| queryAndAssert<HTMLTableDataCellElement>( |
| element, |
| '#status' |
| ).innerText.trim(), |
| `Status: ${ProgressStatus.NOT_STARTED}` |
| ); |
| |
| stubRestApi('executeChangeAction').callsFake( |
| ( |
| _changeNum: NumericChangeId, |
| _method: HttpMethod | undefined, |
| _endpoint: string, |
| _patchNum?: PatchSetNum, |
| _payload?: RequestPayload, |
| errFn?: ErrorCallback |
| ) => |
| Promise.resolve(new Response()).then(res => { |
| errFn && errFn(); |
| return res; |
| }) |
| ); |
| |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').click(); |
| await element.updateComplete; |
| |
| assert.equal( |
| queryAndAssert<HTMLTableDataCellElement>( |
| element, |
| '#status' |
| ).innerText.trim(), |
| `Status: ${ProgressStatus.RUNNING}` |
| ); |
| |
| await waitUntil( |
| () => element.progress.get(1 as NumericChangeId) === ProgressStatus.FAILED |
| ); |
| |
| assert.equal( |
| queryAndAssert<HTMLTableDataCellElement>( |
| element, |
| '#status' |
| ).innerText.trim(), |
| `Status: ${ProgressStatus.FAILED}` |
| ); |
| }); |
| |
| test('closing dialog triggers a reload', async () => { |
| const changes: ChangeInfo[] = [ |
| {...change1, actions: {abandon: {}}}, |
| {...change2, actions: {abandon: {}}}, |
| ]; |
| getChangesStub.returns(changes); |
| |
| const fireStub = sinon.stub(element, 'dispatchEvent'); |
| |
| stubRestApi('executeChangeAction').callsFake( |
| ( |
| _changeNum: NumericChangeId, |
| _method: HttpMethod | undefined, |
| _endpoint: string, |
| _patchNum?: PatchSetNum, |
| _payload?: RequestPayload, |
| errFn?: ErrorCallback |
| ) => |
| Promise.resolve(new Response()).then(res => { |
| errFn && errFn(); |
| return res; |
| }) |
| ); |
| |
| model.sync(changes); |
| await waitUntilObserved( |
| model.loadingState$, |
| state => state === LoadingState.LOADED |
| ); |
| await selectChange(change1); |
| await selectChange(change2); |
| await element.updateComplete; |
| |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#confirm').click(); |
| |
| await waitUntil( |
| () => element.progress.get(2 as NumericChangeId) === ProgressStatus.FAILED |
| ); |
| |
| assert.isFalse(fireStub.called); |
| |
| queryAndAssert<GrButton>(query(element, 'gr-dialog'), '#cancel').click(); |
| |
| await waitUntil(() => fireStub.called); |
| assert.equal(fireStub.lastCall.args[0].type, 'reload'); |
| }); |
| }); |