| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // clang-format off |
| // #import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js'; |
| // |
| // #import {eventToPromise, flushTasks} from 'chrome://test/test_util.m.js'; |
| // #import {keyDownOn, keyEventOn, tap} from 'chrome://resources/polymer/v3_0/iron-test-helpers/mock-interactions.js'; |
| // #import {Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| // clang-format on |
| |
| suite('cr-dialog', function() { |
| function pressEnter(element) { |
| MockInteractions.keyEventOn(element, 'keypress', 13, undefined, 'Enter'); |
| } |
| |
| /** |
| * Creates and shows two nested cr-dialogs. |
| * @return {!Array<!CrDialogElement>} An array of 2 dialogs. The first dialog |
| * is the outer dialog, and the second is the inner dialog. |
| */ |
| function createAndShowNestedDialogs() { |
| document.body.innerHTML = ` |
| <cr-dialog id="outer"> |
| <div slot="title">outer dialog title</div> |
| <div slot="body"> |
| <cr-dialog id="inner"> |
| <div slot="title">inner dialog title</div> |
| <div slot="body">body</div> |
| </cr-dialog> |
| </div> |
| </cr-dialog>`; |
| |
| const outer = document.body.querySelector('#outer'); |
| assertTrue(!!outer); |
| const inner = document.body.querySelector('#inner'); |
| assertTrue(!!inner); |
| |
| outer.showModal(); |
| inner.showModal(); |
| return [outer, inner]; |
| } |
| |
| setup(function() { |
| PolymerTest.clearBody(); |
| // Ensure svg, which is referred to by a relative URL, is loaded from |
| // chrome://resources and not chrome://test |
| const base = document.createElement('base'); |
| base.href = 'chrome://resources/cr_elements/'; |
| document.head.appendChild(base); |
| }); |
| |
| test('cr-dialog-open event fires when opened', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body">body</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| const whenFired = test_util.eventToPromise('cr-dialog-open', dialog); |
| dialog.showModal(); |
| return whenFired; |
| }); |
| |
| test('close event bubbles', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body">body</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| const whenFired = test_util.eventToPromise('close', dialog); |
| dialog.close(); |
| return whenFired.then(() => { |
| assertEquals('success', dialog.getNative().returnValue); |
| }); |
| }); |
| |
| // cr-dialog has to catch and re-fire 'close' events fired from it's native |
| // <dialog> child to force them to bubble in Shadow DOM V1. Ensure that this |
| // mechanism does not interfere with nested <cr-dialog> 'close' events. |
| test('close events not fired from <dialog> are not affected', function() { |
| const dialogs = createAndShowNestedDialogs(); |
| const outer = dialogs[0]; |
| const inner = dialogs[1]; |
| |
| let whenFired = test_util.eventToPromise('close', window); |
| inner.close(); |
| |
| return whenFired |
| .then(e => { |
| // Check that the event's target is the inner dialog. |
| assertEquals(inner, e.target); |
| whenFired = test_util.eventToPromise('close', window); |
| outer.close(); |
| return whenFired; |
| }) |
| .then(e => { |
| // Check that the event's target is the outer dialog. |
| assertEquals(outer, e.target); |
| }); |
| }); |
| |
| test('cancel and close events bubbles when cancelled', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body">body</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| const whenCancelFired = test_util.eventToPromise('cancel', dialog); |
| const whenCloseFired = test_util.eventToPromise('close', dialog); |
| dialog.cancel(); |
| return Promise.all([whenCancelFired, whenCloseFired]).then(() => { |
| assertEquals('', dialog.getNative().returnValue); |
| }); |
| }); |
| |
| // cr-dialog has to catch and re-fire 'cancel' events fired from it's native |
| // <dialog> child to force them to bubble in Shadow DOM V1. Ensure that this |
| // mechanism does not interfere with nested <cr-dialog> 'cancel' events. |
| test('cancel events not fired from <dialog> are not affected', function() { |
| const dialogs = createAndShowNestedDialogs(); |
| const outer = dialogs[0]; |
| const inner = dialogs[1]; |
| |
| let whenFired = test_util.eventToPromise('cancel', window); |
| inner.cancel(); |
| |
| return whenFired |
| .then(e => { |
| // Check that the event's target is the inner dialog. |
| assertEquals(inner, e.target); |
| whenFired = test_util.eventToPromise('cancel', window); |
| outer.cancel(); |
| return whenFired; |
| }) |
| .then(e => { |
| // Check that the event's target is the outer dialog. |
| assertEquals(outer, e.target); |
| }); |
| }); |
| |
| test('focuses title on show', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body"><button>button</button></div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| const button = document.body.querySelector('button'); |
| |
| assertNotEquals(dialog, document.activeElement); |
| assertNotEquals(button, document.activeElement); |
| |
| dialog.showModal(); |
| |
| expectEquals(dialog, document.activeElement); |
| expectNotEquals(button, document.activeElement); |
| }); |
| |
| test('enter keys should trigger action buttons once', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body"> |
| <button class="action-button">button</button> |
| <button id="other-button">other button</button> |
| </div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| const actionButton = document.body.querySelector('.action-button'); |
| |
| dialog.showModal(); |
| |
| // MockInteractions triggers event listeners synchronously. |
| let clickedCounter = 0; |
| actionButton.addEventListener('click', function() { |
| clickedCounter++; |
| }); |
| |
| // Enter key on the action button should only fire the click handler once. |
| MockInteractions.tap(actionButton, 'keypress', 13, undefined, 'Enter'); |
| assertEquals(1, clickedCounter); |
| |
| // Enter keys on other buttons should be ignored. |
| clickedCounter = 0; |
| const otherButton = document.body.querySelector('#other-button'); |
| assertTrue(!!otherButton); |
| pressEnter(otherButton); |
| assertEquals(0, clickedCounter); |
| |
| // Enter keys on the close icon in the top-right corner should be ignored. |
| pressEnter(dialog.$.close); |
| assertEquals(0, clickedCounter); |
| }); |
| |
| test('enter keys find the first non-hidden non-disabled button', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body"> |
| <button id="hidden" class="action-button" hidden>hidden</button> |
| <button class="action-button" disabled>disabled</button> |
| <button class="action-button" disabled hidden>disabled hidden</button> |
| <button id="active" class="action-button">active</button> |
| </div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| const hiddenButton = document.body.querySelector('#hidden'); |
| const actionButton = document.body.querySelector('#active'); |
| dialog.showModal(); |
| |
| // MockInteractions triggers event listeners synchronously. |
| hiddenButton.addEventListener('click', function() { |
| assertNotReached('Hidden button received a click.'); |
| }); |
| let clicked = false; |
| actionButton.addEventListener('click', function() { |
| clicked = true; |
| }); |
| |
| pressEnter(dialog); |
| assertTrue(clicked); |
| }); |
| |
| test('enter keys from certain inputs only are processed', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body"> |
| <input></input> |
| <input type="text"></input> |
| <input type="password"></input> |
| <input type="checkbox"></input> |
| <foobar></foobar> |
| <button class="action-button">active</button> |
| <cr-input></cr-input> |
| </div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| |
| const inputElement = document.body.querySelector('input:not([type])'); |
| const inputTextElement = document.body.querySelector('input[type="text"]'); |
| const inputPasswordElement = |
| document.body.querySelector('input[type="password"]'); |
| const inputCheckboxElement = |
| document.body.querySelector('input[type="checkbox"]'); |
| const otherElement = document.body.querySelector('foobar'); |
| const actionButton = document.body.querySelector('.action-button'); |
| const crInputElement = document.body.querySelector('cr-input'); |
| |
| // MockInteractions triggers event listeners synchronously. |
| let clickedCounter = 0; |
| actionButton.addEventListener('click', function() { |
| clickedCounter++; |
| }); |
| |
| // Only certain types of <input> trigger a dialog submit. |
| pressEnter(otherElement); |
| assertEquals(0, clickedCounter); |
| // "type" defaults to text, which triggers the click. |
| pressEnter(inputElement); |
| assertEquals(1, clickedCounter); |
| pressEnter(inputTextElement); |
| assertEquals(2, clickedCounter); |
| pressEnter(inputPasswordElement); |
| assertEquals(3, clickedCounter); |
| pressEnter(inputCheckboxElement); |
| assertEquals(3, clickedCounter); |
| // Also trigger dialog submit if code synthesizes enter on a cr-input |
| // without targeting the underlying input. |
| pressEnter(crInputElement); |
| assertEquals(4, clickedCounter); |
| }); |
| |
| // Test that enter key presses trigger an action button click, even if the |
| // even was retargeted, e.g. because the input was really a cr-input, the |
| // cr-input was part of another custom element, etc. |
| test('enter keys are processed even if event was retargeted', function() { |
| document.body.innerHTML = ` |
| <dom-module id="test-element"> |
| <template><input></input></template> |
| </dom-module> |
| |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body"> |
| <test-element></test-element> |
| <button class="action-button">active</button> |
| </div> |
| </cr-dialog>`; |
| |
| Polymer({ |
| is: 'test-element', |
| }); |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| |
| const inputWrapper = document.body.querySelector('test-element'); |
| assertTrue(!!inputWrapper); |
| const inputElement = inputWrapper.shadowRoot.querySelector('input'); |
| const actionButton = document.body.querySelector('.action-button'); |
| assertTrue(!!inputElement); |
| assertTrue(!!actionButton); |
| |
| // MockInteractions triggers event listeners synchronously. |
| let clickedCounter = 0; |
| actionButton.addEventListener('click', function() { |
| clickedCounter++; |
| }); |
| |
| pressEnter(inputElement); |
| assertEquals(1, clickedCounter); |
| }); |
| |
| test('focuses [autofocus] instead of title when present', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body"><button autofocus>button</button></div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| const button = document.body.querySelector('button'); |
| |
| assertNotEquals(dialog, document.activeElement); |
| assertNotEquals(button, document.activeElement); |
| |
| dialog.showModal(); |
| |
| expectNotEquals(dialog, document.activeElement); |
| expectEquals(button, document.activeElement); |
| }); |
| |
| // Ensuring that intersectionObserver does not fire any callbacks before the |
| // dialog has been opened. |
| test('body scrollable border not added before modal shown', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body">body</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| assertFalse(dialog.open); |
| const bodyContainer = dialog.$$('.body-container'); |
| assertTrue(!!bodyContainer); |
| const topShadow = dialog.$$('#cr-container-shadow-top'); |
| assertTrue(!!topShadow); |
| const bottomShadow = dialog.$$('#cr-container-shadow-bottom'); |
| assertTrue(!!bottomShadow); |
| |
| return test_util.flushTasks().then(() => { |
| assertFalse(topShadow.classList.contains('has-shadow')); |
| assertFalse(bottomShadow.classList.contains('has-shadow')); |
| }); |
| }); |
| |
| test('dialog body scrollable border when appropriate', function(done) { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| <div slot="body"> |
| <div style="height: 100px">tall content</div> |
| </div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| const bodyContainer = dialog.$$('.body-container'); |
| assertTrue(!!bodyContainer); |
| const topShadow = dialog.$$('#cr-container-shadow-top'); |
| assertTrue(!!topShadow); |
| const bottomShadow = dialog.$$('#cr-container-shadow-bottom'); |
| assertTrue(!!bottomShadow); |
| |
| dialog.showModal(); // Attach the dialog for the first time here. |
| |
| let observerCount = 0; |
| |
| // Needs to setup the observer before attaching, since InteractionObserver |
| // calls callback before MutationObserver does. |
| const observer = new MutationObserver(function(changes) { |
| // Only care about class mutations. |
| if (changes[0].attributeName != 'class') { |
| return; |
| } |
| |
| observerCount++; |
| switch (observerCount) { |
| case 1: // Triggered when scrolled to bottom. |
| assertFalse(bottomShadow.classList.contains('has-shadow')); |
| assertTrue(topShadow.classList.contains('has-shadow')); |
| bodyContainer.scrollTop = 0; |
| break; |
| case 2: // Triggered when scrolled back to top. |
| assertTrue(bottomShadow.classList.contains('has-shadow')); |
| assertFalse(topShadow.classList.contains('has-shadow')); |
| bodyContainer.scrollTop = 2; |
| break; |
| case 3: // Triggered when finally scrolling to middle. |
| assertTrue(bottomShadow.classList.contains('has-shadow')); |
| assertTrue(topShadow.classList.contains('has-shadow')); |
| observer.disconnect(); |
| done(); |
| break; |
| } |
| }); |
| observer.observe(topShadow, {attributes: true}); |
| observer.observe(bottomShadow, {attributes: true}); |
| |
| // Height is normally set via CSS, but mixin doesn't work with innerHTML. |
| bodyContainer.style.height = '60px'; // Element has "min-height: 60px". |
| bodyContainer.scrollTop = 100; |
| }); |
| |
| test('dialog `open` attribute updated when Escape is pressed', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| |
| assertTrue(dialog.open); |
| assertTrue(dialog.hasAttribute('open')); |
| |
| const e = new CustomEvent('cancel', {cancelable: true}); |
| dialog.getNative().dispatchEvent(e); |
| |
| assertFalse(dialog.open); |
| assertFalse(dialog.hasAttribute('open')); |
| }); |
| |
| test('dialog cannot be cancelled when `no-cancel` is set', function() { |
| document.body.innerHTML = ` |
| <cr-dialog no-cancel> |
| <div slot="title">title</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| |
| assertTrue(dialog.$.close.hidden); |
| |
| // Hitting escape fires a 'cancel' event. Cancelling that event prevents the |
| // dialog from closing. |
| let e = new CustomEvent('cancel', {cancelable: true}); |
| dialog.getNative().dispatchEvent(e); |
| assertTrue(e.defaultPrevented); |
| |
| dialog.noCancel = false; |
| |
| e = new CustomEvent('cancel', {cancelable: true}); |
| dialog.getNative().dispatchEvent(e); |
| assertFalse(e.defaultPrevented); |
| }); |
| |
| test('dialog close button shown when showCloseButton is true', function() { |
| document.body.innerHTML = ` |
| <cr-dialog show-close-button> |
| <div slot="title">title</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| assertTrue(dialog.open); |
| |
| assertFalse(dialog.$.close.hidden); |
| assertEquals('flex', window.getComputedStyle(dialog.$.close).display); |
| dialog.$.close.click(); |
| assertFalse(dialog.open); |
| }); |
| |
| test('dialog close button hidden when showCloseButton is false', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| |
| assertTrue(dialog.$.close.hidden); |
| assertEquals('none', window.getComputedStyle(dialog.$.close).display); |
| }); |
| |
| test('keydown should be consumed when the property is true', function() { |
| document.body.innerHTML = ` |
| <cr-dialog consume-keydown-event> |
| <div slot="title">title</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| assertTrue(dialog.open); |
| assertTrue(dialog.consumeKeydownEvent); |
| |
| function assertKeydownNotReached() { |
| assertNotReached('keydown event was propagated'); |
| } |
| document.addEventListener('keydown', assertKeydownNotReached); |
| |
| return test_util.flushTasks().then(() => { |
| MockInteractions.keyDownOn(dialog, 65, undefined, 'a'); |
| MockInteractions.keyDownOn(document.body, 65, undefined, 'a'); |
| document.removeEventListener('keydown', assertKeydownNotReached); |
| }); |
| }); |
| |
| test('keydown should be propagated when the property is false', function() { |
| document.body.innerHTML = ` |
| <cr-dialog> |
| <div slot="title">title</div> |
| </cr-dialog>`; |
| |
| const dialog = document.body.querySelector('cr-dialog'); |
| dialog.showModal(); |
| assertTrue(dialog.open); |
| assertFalse(dialog.consumeKeydownEvent); |
| |
| let keydownCounter = 0; |
| function assertKeydownCount() { |
| keydownCounter++; |
| } |
| document.addEventListener('keydown', assertKeydownCount); |
| |
| return test_util.flushTasks().then(() => { |
| MockInteractions.keyDownOn(dialog, 65, undefined, 'a'); |
| assertEquals(1, keydownCounter); |
| document.removeEventListener('keydown', assertKeydownCount); |
| }); |
| }); |
| |
| test('show on attach', () => { |
| document.body.innerHTML = ` |
| <cr-dialog show-on-attach> |
| <div slot="title">title</div> |
| </cr-dialog>`; |
| const dialog = document.body.querySelector('cr-dialog'); |
| assertTrue(dialog.open); |
| }); |
| }); |