|  | /** | 
|  | * @license | 
|  | * Copyright 2022 Google LLC | 
|  | * SPDX-License-Identifier: Apache-2.0 | 
|  | */ | 
|  | import { | 
|  | GrChangeListSection, | 
|  | computeLabelShortcut, | 
|  | } from './gr-change-list-section'; | 
|  | import '../../../test/common-test-setup'; | 
|  | import './gr-change-list-section'; | 
|  | import '../gr-change-list-item/gr-change-list-item'; | 
|  | import { | 
|  | createChange, | 
|  | createAccountDetailWithId, | 
|  | createAccountWithEmail, | 
|  | createServerInfo, | 
|  | } from '../../../test/test-data-generators'; | 
|  | import {ChangeInfoId, NumericChangeId, Timestamp} from '../../../api/rest-api'; | 
|  | import { | 
|  | queryAll, | 
|  | query, | 
|  | queryAndAssert, | 
|  | stubFlags, | 
|  | waitUntilObserved, | 
|  | } from '../../../test/test-utils'; | 
|  | import {GrChangeListItem} from '../gr-change-list-item/gr-change-list-item'; | 
|  | import {ChangeListSection} from '../gr-change-list/gr-change-list'; | 
|  | import {fixture, html, assert} from '@open-wc/testing'; | 
|  | import {ColumnNames} from '../../../constants/constants'; | 
|  | import {testResolver} from '../../../test/common-test-setup'; | 
|  | import {UserModel, userModelToken} from '../../../models/user/user-model'; | 
|  |  | 
|  | suite('gr-change-list section', () => { | 
|  | let element: GrChangeListSection; | 
|  | let userModel: UserModel; | 
|  |  | 
|  | setup(async () => { | 
|  | userModel = testResolver(userModelToken); | 
|  | const changeSection: ChangeListSection = { | 
|  | name: 'test', | 
|  | query: 'test', | 
|  | results: [ | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 0 as NumericChangeId, | 
|  | id: '0' as ChangeInfoId, | 
|  | }, | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 1 as NumericChangeId, | 
|  | id: '1' as ChangeInfoId, | 
|  | }, | 
|  | ], | 
|  | emptyStateSlotName: 'test', | 
|  | }; | 
|  | element = await fixture<GrChangeListSection>( | 
|  | html`<gr-change-list-section | 
|  | .account=${createAccountDetailWithId(1)} | 
|  | .config=${createServerInfo()} | 
|  | .visibleChangeTableColumns=${Object.values(ColumnNames)} | 
|  | .changeSection=${changeSection} | 
|  | ></gr-change-list-section> ` | 
|  | ); | 
|  | }); | 
|  |  | 
|  | test('renders headers when no changes are selected', () => { | 
|  | // TODO: Check table elements. The shadowDom helper does not understand | 
|  | // tables interacting with display: contents, even wrapping the element in a | 
|  | // table, does not help. | 
|  | assert.shadowDom.equal( | 
|  | element, | 
|  | /* prettier-ignore */ /* HTML */ ` | 
|  | <td class="selection"> | 
|  | <input class="selection-checkbox" type="checkbox"/> | 
|  | </td> | 
|  | # | 
|  | SubjectStatusOwnerReviewersCommentsRepoBranchUpdatedSize Status | 
|  | <gr-change-list-item | 
|  | aria-label="Test subject, section: test" | 
|  | role="button" | 
|  | tabindex="0" | 
|  | > | 
|  | </gr-change-list-item> | 
|  | <gr-change-list-item | 
|  | aria-label="Test subject, section: test" | 
|  | role="button" | 
|  | tabindex="0" | 
|  | > | 
|  | </gr-change-list-item> | 
|  | ` | 
|  | ); | 
|  | }); | 
|  |  | 
|  | test('renders action bar when some changes are selected', async () => { | 
|  | assert.isNotOk(query(element, 'gr-change-list-action-bar')); | 
|  | element.bulkActionsModel.setState({ | 
|  | ...element.bulkActionsModel.getState(), | 
|  | selectedChangeNums: [1 as NumericChangeId], | 
|  | }); | 
|  | await waitUntilObserved( | 
|  | element.bulkActionsModel.selectedChangeNums$, | 
|  | s => s.length === 1 | 
|  | ); | 
|  |  | 
|  | element.requestUpdate(); | 
|  | await element.updateComplete; | 
|  | assert.shadowDom.equal( | 
|  | element, | 
|  | /* prettier-ignore */ /* HTML */ ` | 
|  | <td class="selection"> | 
|  | <input class="selection-checkbox" type="checkbox" /> | 
|  | </td> | 
|  | <gr-change-list-action-bar></gr-change-list-action-bar> | 
|  | <gr-change-list-item | 
|  | aria-label="Test subject, section: test" | 
|  | role="button" | 
|  | tabindex="0" | 
|  | > | 
|  | </gr-change-list-item> | 
|  | <gr-change-list-item | 
|  | aria-label="Test subject, section: test" | 
|  | checked="" | 
|  | role="button" | 
|  | tabindex="0" | 
|  | > | 
|  | </gr-change-list-item> | 
|  | ` | 
|  | ); | 
|  | }); | 
|  |  | 
|  | suite('bulk actions selection', () => { | 
|  | let isEnabled: sinon.SinonStub; | 
|  | setup(async () => { | 
|  | isEnabled = stubFlags('isEnabled'); | 
|  | isEnabled.returns(true); | 
|  | element.requestUpdate(); | 
|  | await element.updateComplete; | 
|  | }); | 
|  |  | 
|  | test('changing section triggers model sync', async () => { | 
|  | const syncStub = sinon.stub(element.bulkActionsModel, 'sync'); | 
|  | assert.isFalse(syncStub.called); | 
|  | element.changeSection = { | 
|  | name: 'test', | 
|  | query: 'test', | 
|  | results: [ | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 1 as NumericChangeId, | 
|  | id: '1' as ChangeInfoId, | 
|  | }, | 
|  | ], | 
|  | emptyStateSlotName: 'test', | 
|  | }; | 
|  | await element.updateComplete; | 
|  |  | 
|  | assert.isTrue(syncStub.called); | 
|  | }); | 
|  |  | 
|  | test('actions header is enabled/disabled based on selected changes', async () => { | 
|  | element.bulkActionsModel.setState({ | 
|  | ...element.bulkActionsModel.getState(), | 
|  | selectedChangeNums: [], | 
|  | }); | 
|  | await waitUntilObserved( | 
|  | element.bulkActionsModel.selectedChangeNums$, | 
|  | s => s.length === 0 | 
|  | ); | 
|  | assert.isFalse(element.numSelected > 0); | 
|  |  | 
|  | element.bulkActionsModel.setState({ | 
|  | ...element.bulkActionsModel.getState(), | 
|  | selectedChangeNums: [1 as NumericChangeId], | 
|  | }); | 
|  | await waitUntilObserved( | 
|  | element.bulkActionsModel.selectedChangeNums$, | 
|  | s => s.length === 1 | 
|  | ); | 
|  | assert.isTrue(element.numSelected > 0); | 
|  | }); | 
|  |  | 
|  | test('select all checkbox checks all when none are selected', async () => { | 
|  | element.changeSection = { | 
|  | name: 'test', | 
|  | query: 'test', | 
|  | results: [ | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 1 as NumericChangeId, | 
|  | id: '1' as ChangeInfoId, | 
|  | }, | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 2 as NumericChangeId, | 
|  | id: '2' as ChangeInfoId, | 
|  | }, | 
|  | ], | 
|  | emptyStateSlotName: 'test', | 
|  | }; | 
|  | userModel.setAccount({ | 
|  | ...createAccountWithEmail('abc@def.com'), | 
|  | registered_on: '2015-03-12 18:32:08.000000000' as Timestamp, | 
|  | }); | 
|  | await element.updateComplete; | 
|  | let rows = queryAll(element, 'gr-change-list-item'); | 
|  | assert.lengthOf(rows, 2); | 
|  | assert.isFalse( | 
|  | queryAndAssert<HTMLInputElement>(rows[0], 'input').checked | 
|  | ); | 
|  | assert.isFalse( | 
|  | queryAndAssert<HTMLInputElement>(rows[1], 'input').checked | 
|  | ); | 
|  |  | 
|  | const checkbox = queryAndAssert<HTMLInputElement>(element, 'input'); | 
|  | checkbox.click(); | 
|  | await waitUntilObserved( | 
|  | element.bulkActionsModel.selectedChangeNums$, | 
|  | s => s.length === 2 | 
|  | ); | 
|  | await element.updateComplete; | 
|  |  | 
|  | rows = queryAll(element, 'gr-change-list-item'); | 
|  | assert.lengthOf(rows, 2); | 
|  | assert.isTrue(queryAndAssert<HTMLInputElement>(rows[0], 'input').checked); | 
|  | assert.isTrue(queryAndAssert<HTMLInputElement>(rows[1], 'input').checked); | 
|  | }); | 
|  |  | 
|  | test('checkbox matches partial and fully selected state', async () => { | 
|  | element.changeSection = { | 
|  | name: 'test', | 
|  | query: 'test', | 
|  | results: [ | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 1 as NumericChangeId, | 
|  | id: '1' as ChangeInfoId, | 
|  | }, | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 2 as NumericChangeId, | 
|  | id: '2' as ChangeInfoId, | 
|  | }, | 
|  | ], | 
|  | emptyStateSlotName: 'test', | 
|  | }; | 
|  | userModel.setAccount({ | 
|  | ...createAccountWithEmail('abc@def.com'), | 
|  | registered_on: '2015-03-12 18:32:08.000000000' as Timestamp, | 
|  | }); | 
|  | await element.updateComplete; | 
|  | const rows = queryAll(element, 'gr-change-list-item'); | 
|  |  | 
|  | // zero case | 
|  | let checkbox = queryAndAssert<HTMLInputElement>(element, 'input'); | 
|  | assert.isFalse(checkbox.checked); | 
|  | assert.isFalse(checkbox.indeterminate); | 
|  |  | 
|  | // partial case | 
|  | queryAndAssert<HTMLInputElement>(rows[0], 'input').click(); | 
|  | await element.updateComplete; | 
|  |  | 
|  | checkbox = queryAndAssert<HTMLInputElement>(element, 'input'); | 
|  | assert.isTrue(checkbox.indeterminate); | 
|  |  | 
|  | // plural case | 
|  | queryAndAssert<HTMLInputElement>(rows[1], 'input').click(); | 
|  | await element.updateComplete; | 
|  |  | 
|  | checkbox = queryAndAssert<HTMLInputElement>(element, 'input'); | 
|  | assert.isFalse(checkbox.indeterminate); | 
|  | assert.isTrue(checkbox.checked); | 
|  |  | 
|  | // Clicking Check All checkbox when all checkboxes selected unselects | 
|  | // all checkboxes | 
|  | queryAndAssert<HTMLInputElement>(element, 'input'); | 
|  | checkbox.click(); | 
|  | await element.updateComplete; | 
|  |  | 
|  | assert.isFalse( | 
|  | queryAndAssert<HTMLInputElement>(rows[0], 'input').checked | 
|  | ); | 
|  | assert.isFalse( | 
|  | queryAndAssert<HTMLInputElement>(rows[1], 'input').checked | 
|  | ); | 
|  | }); | 
|  | }); | 
|  |  | 
|  | test('no checkbox when logged out', async () => { | 
|  | element.changeSection = { | 
|  | name: 'test', | 
|  | query: 'test', | 
|  | results: [ | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 1 as NumericChangeId, | 
|  | id: '1' as ChangeInfoId, | 
|  | }, | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 2 as NumericChangeId, | 
|  | id: '2' as ChangeInfoId, | 
|  | }, | 
|  | ], | 
|  | emptyStateSlotName: 'test', | 
|  | }; | 
|  | userModel.setAccount(undefined); | 
|  | await element.updateComplete; | 
|  | const rows = queryAll(element, 'gr-change-list-item'); | 
|  | assert.lengthOf(rows, 2); | 
|  | assert.isUndefined(query<HTMLInputElement>(rows[0], 'input')); | 
|  | }); | 
|  |  | 
|  | test('colspans', async () => { | 
|  | element.visibleChangeTableColumns = []; | 
|  | element.changeSection = {results: [{...createChange()}]}; | 
|  | await element.updateComplete; | 
|  | const tdItemCount = queryAll<HTMLTableElement>(element, 'td').length; | 
|  |  | 
|  | element.labelNames = []; | 
|  | assert.equal(tdItemCount, element.computeColspan(element.computeColumns())); | 
|  | }); | 
|  |  | 
|  | test('computeItemSelected', () => { | 
|  | element.selectedIndex = 1; | 
|  | assert.isTrue(element.computeItemSelected(1)); | 
|  | assert.isFalse(element.computeItemSelected(2)); | 
|  | }); | 
|  |  | 
|  | test('computed fields', () => { | 
|  | assert.equal(computeLabelShortcut('Code-Review'), 'CR'); | 
|  | assert.equal(computeLabelShortcut('Verified'), 'V'); | 
|  | assert.equal(computeLabelShortcut('Library-Compliance'), 'LC'); | 
|  | assert.equal(computeLabelShortcut('PolyGerrit-Review'), 'PR'); | 
|  | assert.equal(computeLabelShortcut('polygerrit-review'), 'PR'); | 
|  | assert.equal( | 
|  | computeLabelShortcut('Invalid-Prolog-Rules-Label-Name--Verified'), | 
|  | 'V' | 
|  | ); | 
|  | assert.equal(computeLabelShortcut('Some-Special-Label-7'), 'SSL7'); | 
|  | assert.equal(computeLabelShortcut('--Too----many----dashes---'), 'TMD'); | 
|  | assert.equal( | 
|  | computeLabelShortcut('Really-rather-entirely-too-long-of-a-label-name'), | 
|  | 'RRETL' | 
|  | ); | 
|  | }); | 
|  |  | 
|  | suite('empty section slots', () => { | 
|  | test('empty section', async () => { | 
|  | element.changeSection = {results: []}; | 
|  | await element.updateComplete; | 
|  | const listItems = queryAll<GrChangeListItem>( | 
|  | element, | 
|  | 'gr-change-list-item' | 
|  | ); | 
|  | assert.equal(listItems.length, 0); | 
|  | const noChangesMsg = queryAll<HTMLTableRowElement>(element, '.noChanges'); | 
|  | assert.equal(noChangesMsg.length, 1); | 
|  | }); | 
|  |  | 
|  | test('are shown on empty sections with slot name', async () => { | 
|  | const section = { | 
|  | name: 'test', | 
|  | query: 'test', | 
|  | results: [], | 
|  | emptyStateSlotName: 'test', | 
|  | }; | 
|  | element.changeSection = section; | 
|  | await element.updateComplete; | 
|  |  | 
|  | assert.isEmpty(queryAll(element, 'gr-change-list-item')); | 
|  | queryAndAssert(element, 'slot[name="test"]'); | 
|  | }); | 
|  |  | 
|  | test('are not shown on empty sections without slot name', async () => { | 
|  | const section = {name: 'test', query: 'test', results: []}; | 
|  | element.changeSection = section; | 
|  | await element.updateComplete; | 
|  |  | 
|  | assert.isEmpty(queryAll(element, 'gr-change-list-item')); | 
|  | assert.notExists(query(element, 'slot[name="test"]')); | 
|  | }); | 
|  |  | 
|  | test('are not shown on non-empty sections with slot name', async () => { | 
|  | const section = { | 
|  | name: 'test', | 
|  | query: 'test', | 
|  | emptyStateSlotName: 'test', | 
|  | results: [ | 
|  | { | 
|  | ...createChange(), | 
|  | _number: 0 as NumericChangeId, | 
|  | labels: {Verified: {approved: {}}}, | 
|  | }, | 
|  | ], | 
|  | }; | 
|  | element.changeSection = section; | 
|  | await element.updateComplete; | 
|  |  | 
|  | assert.isNotEmpty(queryAll(element, 'gr-change-list-item')); | 
|  | assert.notExists(query(element, 'slot[name="test"]')); | 
|  | }); | 
|  | }); | 
|  |  | 
|  | suite('dashboard queries', () => { | 
|  | test('query without age and limit unchanged', () => { | 
|  | const query = 'status:closed owner:me'; | 
|  | assert.deepEqual(element.processQuery(query), query); | 
|  | }); | 
|  |  | 
|  | test('query with age and limit', () => { | 
|  | const query = 'status:closed age:1week limit:10 owner:me'; | 
|  | const expectedQuery = 'status:closed owner:me'; | 
|  | assert.deepEqual(element.processQuery(query), expectedQuery); | 
|  | }); | 
|  |  | 
|  | test('query with age', () => { | 
|  | const query = 'status:closed age:1week owner:me'; | 
|  | const expectedQuery = 'status:closed owner:me'; | 
|  | assert.deepEqual(element.processQuery(query), expectedQuery); | 
|  | }); | 
|  |  | 
|  | test('query with limit', () => { | 
|  | const query = 'status:closed limit:10 owner:me'; | 
|  | const expectedQuery = 'status:closed owner:me'; | 
|  | assert.deepEqual(element.processQuery(query), expectedQuery); | 
|  | }); | 
|  |  | 
|  | test('query with age as value and not key', () => { | 
|  | const query = 'status:closed random:age'; | 
|  | const expectedQuery = 'status:closed random:age'; | 
|  | assert.deepEqual(element.processQuery(query), expectedQuery); | 
|  | }); | 
|  |  | 
|  | test('query with limit as value and not key', () => { | 
|  | const query = 'status:closed random:limit'; | 
|  | const expectedQuery = 'status:closed random:limit'; | 
|  | assert.deepEqual(element.processQuery(query), expectedQuery); | 
|  | }); | 
|  |  | 
|  | test('query with -age key', () => { | 
|  | const query = 'status:closed -age:1week'; | 
|  | const expectedQuery = 'status:closed'; | 
|  | assert.deepEqual(element.processQuery(query), expectedQuery); | 
|  | }); | 
|  | }); | 
|  | }); |