|  | <!DOCTYPE html> | 
|  | <!-- | 
|  | Copyright (C) 2016 The Android Open Source Project | 
|  |  | 
|  | 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. | 
|  | --> | 
|  |  | 
|  | <meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes"> | 
|  | <title>gr-account-list</title> | 
|  |  | 
|  | <script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script> | 
|  | <script src="../../../bower_components/web-component-tester/browser.js"></script> | 
|  | <link rel="import" href="../../../test/common-test-setup.html"/> | 
|  | <link rel="import" href="gr-account-list.html"> | 
|  |  | 
|  | <script>void(0);</script> | 
|  |  | 
|  | <test-fixture id="basic"> | 
|  | <template> | 
|  | <gr-account-list></gr-account-list> | 
|  | </template> | 
|  | </test-fixture> | 
|  |  | 
|  | <script> | 
|  | suite('gr-account-list tests', () => { | 
|  | let _nextAccountId = 0; | 
|  | const makeAccount = function() { | 
|  | const accountId = ++_nextAccountId; | 
|  | return { | 
|  | _account_id: accountId, | 
|  | }; | 
|  | }; | 
|  | const makeGroup = function() { | 
|  | const groupId = 'group' + (++_nextAccountId); | 
|  | return { | 
|  | id: groupId, | 
|  | _group: true, | 
|  | }; | 
|  | }; | 
|  |  | 
|  | let existingReviewer1; | 
|  | let existingReviewer2; | 
|  | let sandbox; | 
|  | let element; | 
|  |  | 
|  | function getChips() { | 
|  | return Polymer.dom(element.root).querySelectorAll('gr-account-chip'); | 
|  | } | 
|  |  | 
|  | setup(() => { | 
|  | sandbox = sinon.sandbox.create(); | 
|  | existingReviewer1 = makeAccount(); | 
|  | existingReviewer2 = makeAccount(); | 
|  |  | 
|  | stub('gr-rest-api-interface', { | 
|  | getConfig() { return Promise.resolve({}); }, | 
|  | }); | 
|  | element = fixture('basic'); | 
|  | element.accounts = [existingReviewer1, existingReviewer2]; | 
|  | }); | 
|  |  | 
|  | teardown(() => { | 
|  | sandbox.restore(); | 
|  | }); | 
|  |  | 
|  | test('account entry only appears when editable', () => { | 
|  | element.readonly = false; | 
|  | assert.isFalse(element.$.entry.hasAttribute('hidden')); | 
|  | element.readonly = true; | 
|  | assert.isTrue(element.$.entry.hasAttribute('hidden')); | 
|  | }); | 
|  |  | 
|  | test('addition and removal of account/group chips', () => { | 
|  | flushAsynchronousOperations(); | 
|  | sandbox.stub(element, '_computeRemovable').returns(true); | 
|  | // Existing accounts are listed. | 
|  | let chips = getChips(); | 
|  | assert.equal(chips.length, 2); | 
|  | assert.isFalse(chips[0].classList.contains('pendingAdd')); | 
|  | assert.isFalse(chips[1].classList.contains('pendingAdd')); | 
|  |  | 
|  | // New accounts are added to end with pendingAdd class. | 
|  | const newAccount = makeAccount(); | 
|  | element._handleAdd({ | 
|  | detail: { | 
|  | value: { | 
|  | account: newAccount, | 
|  | }, | 
|  | }, | 
|  | }); | 
|  | flushAsynchronousOperations(); | 
|  | chips = getChips(); | 
|  | assert.equal(chips.length, 3); | 
|  | assert.isFalse(chips[0].classList.contains('pendingAdd')); | 
|  | assert.isFalse(chips[1].classList.contains('pendingAdd')); | 
|  | assert.isTrue(chips[2].classList.contains('pendingAdd')); | 
|  |  | 
|  | // Removed accounts are taken out of the list. | 
|  | element.fire('remove', {account: existingReviewer1}); | 
|  | flushAsynchronousOperations(); | 
|  | chips = getChips(); | 
|  | assert.equal(chips.length, 2); | 
|  | assert.isFalse(chips[0].classList.contains('pendingAdd')); | 
|  | assert.isTrue(chips[1].classList.contains('pendingAdd')); | 
|  |  | 
|  | // Invalid remove is ignored. | 
|  | element.fire('remove', {account: existingReviewer1}); | 
|  | element.fire('remove', {account: newAccount}); | 
|  | flushAsynchronousOperations(); | 
|  | chips = getChips(); | 
|  | assert.equal(chips.length, 1); | 
|  | assert.isFalse(chips[0].classList.contains('pendingAdd')); | 
|  |  | 
|  | // New groups are added to end with pendingAdd and group classes. | 
|  | const newGroup = makeGroup(); | 
|  | element._handleAdd({ | 
|  | detail: { | 
|  | value: { | 
|  | group: newGroup, | 
|  | }, | 
|  | }, | 
|  | }); | 
|  | flushAsynchronousOperations(); | 
|  | chips = getChips(); | 
|  | assert.equal(chips.length, 2); | 
|  | assert.isTrue(chips[1].classList.contains('group')); | 
|  | assert.isTrue(chips[1].classList.contains('pendingAdd')); | 
|  |  | 
|  | // Removed groups are taken out of the list. | 
|  | element.fire('remove', {account: newGroup}); | 
|  | flushAsynchronousOperations(); | 
|  | chips = getChips(); | 
|  | assert.equal(chips.length, 1); | 
|  | assert.isFalse(chips[0].classList.contains('pendingAdd')); | 
|  | }); | 
|  |  | 
|  | test('_computeChipClass', () => { | 
|  | const account = makeAccount(); | 
|  | assert.equal(element._computeChipClass(account), ''); | 
|  | account._pendingAdd = true; | 
|  | assert.equal(element._computeChipClass(account), 'pendingAdd'); | 
|  | account._group = true; | 
|  | assert.equal(element._computeChipClass(account), 'group pendingAdd'); | 
|  | account._pendingAdd = false; | 
|  | assert.equal(element._computeChipClass(account), 'group'); | 
|  | }); | 
|  |  | 
|  | test('_computeRemovable', () => { | 
|  | const newAccount = makeAccount(); | 
|  | newAccount._pendingAdd = true; | 
|  | element.readonly = false; | 
|  | element.removableValues = []; | 
|  | assert.isFalse(element._computeRemovable(existingReviewer1)); | 
|  | assert.isTrue(element._computeRemovable(newAccount)); | 
|  |  | 
|  |  | 
|  | element.removableValues = [existingReviewer1]; | 
|  | assert.isTrue(element._computeRemovable(existingReviewer1)); | 
|  | assert.isTrue(element._computeRemovable(newAccount)); | 
|  | assert.isFalse(element._computeRemovable(existingReviewer2)); | 
|  |  | 
|  | element.readonly = true; | 
|  | assert.isFalse(element._computeRemovable(existingReviewer1)); | 
|  | assert.isFalse(element._computeRemovable(newAccount)); | 
|  | }); | 
|  |  | 
|  | test('submitEntryText', () => { | 
|  | element.allowAnyInput = true; | 
|  | flushAsynchronousOperations(); | 
|  |  | 
|  | const getTextStub = sandbox.stub(element.$.entry, 'getText'); | 
|  | getTextStub.onFirstCall().returns(''); | 
|  | getTextStub.onSecondCall().returns('test'); | 
|  | getTextStub.onThirdCall().returns('test@test'); | 
|  |  | 
|  | // When entry is empty, return true. | 
|  | const clearStub = sandbox.stub(element.$.entry, 'clear'); | 
|  | assert.isTrue(element.submitEntryText()); | 
|  | assert.isFalse(clearStub.called); | 
|  |  | 
|  | // When entry is invalid, return false. | 
|  | assert.isFalse(element.submitEntryText()); | 
|  | assert.isFalse(clearStub.called); | 
|  |  | 
|  | // When entry is valid, return true and clear text. | 
|  | assert.isTrue(element.submitEntryText()); | 
|  | assert.isTrue(clearStub.called); | 
|  | assert.equal(element.additions()[0].account.email, 'test@test'); | 
|  | }); | 
|  |  | 
|  | test('additions returns sanitized new accounts and groups', () => { | 
|  | assert.equal(element.additions().length, 0); | 
|  |  | 
|  | const newAccount = makeAccount(); | 
|  | element._handleAdd({ | 
|  | detail: { | 
|  | value: { | 
|  | account: newAccount, | 
|  | }, | 
|  | }, | 
|  | }); | 
|  | const newGroup = makeGroup(); | 
|  | element._handleAdd({ | 
|  | detail: { | 
|  | value: { | 
|  | group: newGroup, | 
|  | }, | 
|  | }, | 
|  | }); | 
|  |  | 
|  | assert.deepEqual(element.additions(), [ | 
|  | { | 
|  | account: { | 
|  | _account_id: newAccount._account_id, | 
|  | _pendingAdd: true, | 
|  | }, | 
|  | }, | 
|  | { | 
|  | group: { | 
|  | id: newGroup.id, | 
|  | _group: true, | 
|  | _pendingAdd: true, | 
|  | }, | 
|  | }, | 
|  | ]); | 
|  | }); | 
|  |  | 
|  | test('large group confirmations', () => { | 
|  | assert.isNull(element.pendingConfirmation); | 
|  | assert.deepEqual(element.additions(), []); | 
|  |  | 
|  | const group = makeGroup(); | 
|  | const reviewer = { | 
|  | group, | 
|  | count: 10, | 
|  | confirm: true, | 
|  | }; | 
|  | element._handleAdd({ | 
|  | detail: { | 
|  | value: reviewer, | 
|  | }, | 
|  | }); | 
|  |  | 
|  | assert.deepEqual(element.pendingConfirmation, reviewer); | 
|  | assert.deepEqual(element.additions(), []); | 
|  |  | 
|  | element.confirmGroup(group); | 
|  | assert.isNull(element.pendingConfirmation); | 
|  | assert.deepEqual(element.additions(), [ | 
|  | { | 
|  | group: { | 
|  | id: group.id, | 
|  | _group: true, | 
|  | _pendingAdd: true, | 
|  | confirmed: true, | 
|  | }, | 
|  | }, | 
|  | ]); | 
|  | }); | 
|  |  | 
|  | test('removeAccount fails if account is not removable', () => { | 
|  | element.readonly = true; | 
|  | const acct = makeAccount(); | 
|  | element.accounts = [acct]; | 
|  | element._removeAccount(acct); | 
|  | assert.equal(element.accounts.length, 1); | 
|  | }); | 
|  |  | 
|  | test('max-count', () => { | 
|  | element.maxCount = 1; | 
|  | const acct = makeAccount(); | 
|  | element._handleAdd({ | 
|  | detail: { | 
|  | value: { | 
|  | account: acct, | 
|  | }, | 
|  | }, | 
|  | }); | 
|  | flushAsynchronousOperations(); | 
|  | assert.isTrue(element.$.entry.hasAttribute('hidden')); | 
|  | }); | 
|  |  | 
|  | suite('allowAnyInput', () => { | 
|  | let entry; | 
|  |  | 
|  | setup(() => { | 
|  | entry = element.$.entry; | 
|  | sandbox.stub(entry, '_getReviewerSuggestions'); | 
|  | sandbox.stub(entry.$.input, '_updateSuggestions'); | 
|  | element.allowAnyInput = true; | 
|  | }); | 
|  |  | 
|  | test('adds emails', () => { | 
|  | const accountLen = element.accounts.length; | 
|  | element._handleAdd({detail: {value: 'test@test'}}); | 
|  | assert.equal(element.accounts.length, accountLen + 1); | 
|  | assert.equal(element.accounts[accountLen].email, 'test@test'); | 
|  | }); | 
|  |  | 
|  | test('toasts on invalid email', () => { | 
|  | const toastHandler = sandbox.stub(); | 
|  | element.addEventListener('show-alert', toastHandler); | 
|  | element._handleAdd({detail: {value: 'test'}}); | 
|  | assert.isTrue(toastHandler.called); | 
|  | }); | 
|  | }); | 
|  |  | 
|  | test('_accountMatches', () => { | 
|  | const acct = makeAccount(); | 
|  |  | 
|  | assert.isTrue(element._accountMatches(acct, acct)); | 
|  | acct.email = 'test'; | 
|  | assert.isTrue(element._accountMatches(acct, acct)); | 
|  | assert.isTrue(element._accountMatches({email: 'test'}, acct)); | 
|  |  | 
|  | assert.isFalse(element._accountMatches({}, acct)); | 
|  | assert.isFalse(element._accountMatches({email: 'test2'}, acct)); | 
|  | assert.isFalse(element._accountMatches({_account_id: -1}, acct)); | 
|  | }); | 
|  |  | 
|  | suite('keyboard interactions', () => { | 
|  | test('backspace at text input start removes last account', () => { | 
|  | const input = element.$.entry.$.input; | 
|  | sandbox.stub(element.$.entry, '_getReviewerSuggestions'); | 
|  | sandbox.stub(input, '_updateSuggestions'); | 
|  | sandbox.stub(element, '_computeRemovable').returns(true); | 
|  | // Next line is a workaround for Firefix not moving cursor | 
|  | // on input field update | 
|  | assert.equal(input.$.input.inputElement.selectionStart, 0); | 
|  | input.text = 'test'; | 
|  | MockInteractions.focus(input.$.input); | 
|  | flushAsynchronousOperations(); | 
|  | assert.equal(element.accounts.length, 2); | 
|  | MockInteractions.pressAndReleaseKeyOn( | 
|  | input.$.input.inputElement, 8); // Backspace | 
|  | assert.equal(element.accounts.length, 2); | 
|  | input.text = ''; | 
|  | MockInteractions.pressAndReleaseKeyOn( | 
|  | input.$.input.inputElement, 8); // Backspace | 
|  | assert.equal(element.accounts.length, 1); | 
|  | }); | 
|  |  | 
|  | test('arrow key navigation', () => { | 
|  | const input = element.$.entry.$.input; | 
|  | input.text = ''; | 
|  | element.accounts = [makeAccount(), makeAccount()]; | 
|  | MockInteractions.focus(input.$.input); | 
|  | flushAsynchronousOperations(); | 
|  | const chips = element.accountChips; | 
|  | const chipsOneSpy = sandbox.spy(chips[1], 'focus'); | 
|  | MockInteractions.pressAndReleaseKeyOn(input.$.input, 37); // Left | 
|  | assert.isTrue(chipsOneSpy.called); | 
|  | const chipsZeroSpy = sandbox.spy(chips[0], 'focus'); | 
|  | MockInteractions.pressAndReleaseKeyOn(chips[1], 37); // Left | 
|  | assert.isTrue(chipsZeroSpy.called); | 
|  | MockInteractions.pressAndReleaseKeyOn(chips[0], 37); // Left | 
|  | assert.isTrue(chipsZeroSpy.calledOnce); | 
|  | MockInteractions.pressAndReleaseKeyOn(chips[0], 39); // Right | 
|  | assert.isTrue(chipsOneSpy.calledTwice); | 
|  | }); | 
|  |  | 
|  | test('delete', done => { | 
|  | element.accounts = [makeAccount(), makeAccount()]; | 
|  | flush(() => { | 
|  | const focusSpy = sandbox.spy(element.accountChips[1], 'focus'); | 
|  | const removeSpy = sandbox.spy(element, '_removeAccount'); | 
|  | MockInteractions.pressAndReleaseKeyOn( | 
|  | element.accountChips[0], 8); // Backspace | 
|  | assert.isTrue(focusSpy.called); | 
|  | assert.isTrue(removeSpy.calledOnce); | 
|  |  | 
|  | MockInteractions.pressAndReleaseKeyOn( | 
|  | element.accountChips[1], 46); // Delete | 
|  | assert.isTrue(removeSpy.calledTwice); | 
|  | done(); | 
|  | }); | 
|  | }); | 
|  | }); | 
|  | }); | 
|  | </script> |