chromium/chrome/test/data/webui/print_preview/pages_settings_test.ts

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'chrome://print/print_preview.js';

import type {PrintPreviewPagesSettingsElement, Range} from 'chrome://print/print_preview.js';
import {PagesValue} from 'chrome://print/print_preview.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {keyEventOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {fakeDataBind} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';

import {selectOption, triggerInputEvent} from './print_preview_test_utils.js';

suite('PagesSettingsTest', function() {
  let pagesSection: PrintPreviewPagesSettingsElement;

  const oneToHundred: number[] = Array.from({length: 100}, (_x, i) => i + 1);

  const limitError: string = 'Out of bounds page reference, limit is ';

  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    const model = document.createElement('print-preview-model');
    document.body.appendChild(model);

    pagesSection = document.createElement('print-preview-pages-settings');
    pagesSection.settings = model.settings;
    pagesSection.disabled = false;
    fakeDataBind(model, pagesSection, 'settings');
    document.body.appendChild(pagesSection);
  });

  /**
   * @param inputString The input value to set.
   * @return Promise that resolves when the input has been set and
   *     the input-change event has fired.
   */
  function setCustomInput(inputString: string): Promise<void> {
    const pagesInput = pagesSection.$.pageSettingsCustomInput.inputElement;
    return triggerInputEvent(pagesInput, inputString, pagesSection);
  }

  /**
   * @param expectedPages The expected pages value.
   * @param expectedPages The expected pages value.
   * @param expectedError The expected error message.
   * @param invalid Whether the pages setting should be invalid.
   */
  function validateState(
      expectedPages: number[], expectedRanges: Range[], expectedError: string,
      invalid: boolean) {
    const pagesValue = pagesSection.getSettingValue('pages');
    assertEquals(expectedPages.length, pagesValue.length);
    expectedPages.forEach((page: number, index: number) => {
      assertEquals(page, pagesValue[index]);
    });
    const rangesValue = pagesSection.getSettingValue('ranges');
    assertEquals(expectedRanges.length, rangesValue.length);
    expectedRanges.forEach((range: Range, index: number) => {
      assertEquals(range.to, rangesValue[index].to);
      assertEquals(range.from, rangesValue[index].from);
    });
    assertEquals(!invalid, pagesSection.getSetting('pages').valid);
    assertEquals(
        expectedError !== '',
        pagesSection.shadowRoot!.querySelector('cr-input')!.invalid);
    assertEquals(
        expectedError,
        pagesSection.shadowRoot!.querySelector('cr-input')!.errorMessage);
  }

  // Verifies that the pages setting updates correctly when the dropdown
  // changes.
  test('PagesDropdown', async () => {
    pagesSection.pageCount = 5;

    // Default value is all pages.
    const customInputCollapse =
        pagesSection.shadowRoot!.querySelector('cr-collapse')!;

    assertFalse(pagesSection.getSetting('ranges').setFromUi);
    validateState([1, 2, 3, 4, 5], [], '', false);
    assertFalse(customInputCollapse.opened);

    // Set selection to odd pages.
    await selectOption(pagesSection, PagesValue.ODDS.toString());
    assertFalse(customInputCollapse.opened);
    validateState(
        [1, 3, 5], [{from: 1, to: 1}, {from: 3, to: 3}, {from: 5, to: 5}], '',
        false);

    // Set selection to even pages.
    await selectOption(pagesSection, PagesValue.EVENS.toString());
    assertFalse(customInputCollapse.opened);
    validateState([2, 4], [{from: 2, to: 2}, {from: 4, to: 4}], '', false);

    // Set selection of pages 1 and 2.
    await selectOption(pagesSection, PagesValue.CUSTOM.toString());
    assertTrue(customInputCollapse.opened);

    await setCustomInput('1-2');
    validateState([1, 2], [{from: 1, to: 2}], '', false);
    assertTrue(pagesSection.getSetting('ranges').setFromUi);

    // Re-select "all".
    await selectOption(pagesSection, PagesValue.ALL.toString());
    assertFalse(customInputCollapse.opened);
    validateState([1, 2, 3, 4, 5], [], '', false);

    // Re-select custom. The previously entered value should be
    // restored.
    await selectOption(pagesSection, PagesValue.CUSTOM.toString());
    assertTrue(customInputCollapse.opened);
    validateState([1, 2], [{from: 1, to: 2}], '', false);

    // Set a selection equal to the full page range. This should set ranges to
    // empty, so that reselecting "all" does not regenerate the preview.
    await setCustomInput('1-5');
    validateState([1, 2, 3, 4, 5], [], '', false);
  });

  // Tests that the odd-only and even-only options are hidden when the document
  // has only one page.
  test('NoParityOptions', async () => {
    pagesSection.pageCount = 1;

    const oddOption = pagesSection.shadowRoot!.querySelector<HTMLOptionElement>(
        `[value="${PagesValue.ODDS}"]`)!;
    assertTrue(oddOption.hidden);

    const evenOption =
        pagesSection.shadowRoot!.querySelector<HTMLOptionElement>(
            `[value="${PagesValue.EVENS}"]`)!;
    assertTrue(evenOption.hidden);
  });

  // Tests that the odd-only and even-only selections are preserved when the
  // page counts change.
  test('ParitySelectionMemorized', async () => {
    const select = pagesSection.shadowRoot!.querySelector('select')!;

    pagesSection.pageCount = 2;
    assertEquals(PagesValue.ALL.toString(), select.value);

    await selectOption(pagesSection, PagesValue.ODDS.toString());
    assertEquals(PagesValue.ODDS.toString(), select.value);

    let whenValueChanged =
        eventToPromise('process-select-change', pagesSection);
    pagesSection.pageCount = 1;
    await whenValueChanged;
    assertEquals(PagesValue.ALL.toString(), select.value);

    whenValueChanged = eventToPromise('process-select-change', pagesSection);
    pagesSection.pageCount = 2;
    await whenValueChanged;
    assertEquals(PagesValue.ODDS.toString(), select.value);
  });

  // Tests that the page ranges set are valid for different user inputs.
  test('ValidPageRanges', async () => {
    pagesSection.pageCount = 100;
    const tenToHundred = Array.from({length: 91}, (_x, i) => i + 10);

    await selectOption(pagesSection, PagesValue.CUSTOM.toString());
    await setCustomInput('1, 2, 3, 1, 56');
    validateState(
        [1, 2, 3, 56], [{from: 1, to: 3}, {from: 56, to: 56}], '', false);

    await setCustomInput('1-3, 6-9, 6-10');
    validateState(
        [1, 2, 3, 6, 7, 8, 9, 10], [{from: 1, to: 3}, {from: 6, to: 10}], '',
        false);

    await setCustomInput('10-');
    validateState(tenToHundred, [{from: 10, to: 100}], '', false);

    await setCustomInput('10-100');
    validateState(tenToHundred, [{from: 10, to: 100}], '', false);

    await setCustomInput('-');
    validateState(oneToHundred, [], '', false);

    // https://crbug.com/806165
    await setCustomInput('1\u30012\u30013\u30011\u300156');
    validateState(
        [1, 2, 3, 56], [{from: 1, to: 3}, {from: 56, to: 56}], '', false);

    await setCustomInput('1,2,3\u30011\u300156');
    validateState(
        [1, 2, 3, 56], [{from: 1, to: 3}, {from: 56, to: 56}], '', false);

    // https://crbug.com/1015145
    // Tests that the pages gets sorted for an unsorted input.
    await setCustomInput('89-91, 3, 6, 46, 1, 4, 2-3');
    validateState(
        [1, 2, 3, 4, 6, 46, 89, 90, 91],
        [
          {from: 1, to: 4},
          {from: 6, to: 6},
          {from: 46, to: 46},
          {from: 89, to: 91},
        ],
        '', false);
  });

  // Tests that the correct error messages are shown for different user
  // inputs.
  test('InvalidPageRanges', async () => {
    pagesSection.pageCount = 100;
    const syntaxError = 'Invalid page range, use e.g. 1-5, 8, 11-13';

    await selectOption(pagesSection, PagesValue.CUSTOM.toString());
    await setCustomInput('10-100000');
    validateState(oneToHundred, [], limitError + '100', true);

    await setCustomInput('1, 2, 0, 56');
    validateState(oneToHundred, [], syntaxError, true);

    await setCustomInput('-1, 1, 2,, 56');
    validateState(oneToHundred, [], syntaxError, true);

    await setCustomInput('1,2,56-40');
    validateState(oneToHundred, [], syntaxError, true);

    await setCustomInput('101-110');
    validateState(oneToHundred, [], limitError + '100', true);

    await setCustomInput('1\u30012\u30010\u300156');
    validateState(oneToHundred, [], syntaxError, true);

    await setCustomInput('-1,1,2\u3001\u300156');
    validateState(oneToHundred, [], syntaxError, true);

    await setCustomInput('--');
    validateState(oneToHundred, [], syntaxError, true);

    await setCustomInput(' 1 1 ');
    validateState(oneToHundred, [], syntaxError, true);
  });

  // Tests that the pages are set correctly for different values of pages per
  // sheet, and that ranges remain fixed (since they are used for generating
  // the print preview ticket).
  test('NupChangesPages', async () => {
    pagesSection.pageCount = 100;
    await selectOption(pagesSection, PagesValue.CUSTOM.toString());
    await setCustomInput('1, 2, 3, 1, 56');
    let expectedRanges = [{from: 1, to: 3}, {from: 56, to: 56}];
    validateState([1, 2, 3, 56], expectedRanges, '', false);
    pagesSection.setSetting('pagesPerSheet', 2);
    validateState([1, 2], expectedRanges, '', false);
    pagesSection.setSetting('pagesPerSheet', 4);
    validateState([1], expectedRanges, '', false);
    pagesSection.setSetting('pagesPerSheet', 1);

    await setCustomInput('1-3, 6-9, 6-10');
    expectedRanges = [{from: 1, to: 3}, {from: 6, to: 10}];
    validateState([1, 2, 3, 6, 7, 8, 9, 10], expectedRanges, '', false);
    pagesSection.setSetting('pagesPerSheet', 2);
    validateState([1, 2, 3, 4], expectedRanges, '', false);
    pagesSection.setSetting('pagesPerSheet', 3);
    validateState([1, 2, 3], expectedRanges, '', false);

    await setCustomInput('1-3');
    expectedRanges = [{from: 1, to: 3}];
    validateState([1], expectedRanges, '', false);
    pagesSection.setSetting('pagesPerSheet', 1);
    validateState([1, 2, 3], expectedRanges, '', false);
  });

  // Note: Remaining tests in this file are interactive_ui_tests, and validate
  // some focus related behavior.
  // Tests that the clearing a valid input has no effect, clearing an invalid
  // input does not show an error message but does not reset the preview, and
  // changing focus from an empty input in either case fills in the dropdown
  // with the full page range.
  test('ClearInput', async () => {
    pagesSection.pageCount = 3;
    const input = pagesSection.$.pageSettingsCustomInput.inputElement;
    const select = pagesSection.shadowRoot!.querySelector('select')!;
    const allValue = PagesValue.ALL.toString();
    const customValue = PagesValue.CUSTOM.toString();
    assertEquals(allValue, select.value);

    // Selecting custom focuses the input.
    await Promise.all([
      selectOption(pagesSection, customValue),
      eventToPromise('focus', input),
    ]);
    input.focus();

    await setCustomInput('1-2');
    assertEquals(customValue, select.value);
    validateState([1, 2], [{from: 1, to: 2}], '', false);

    await setCustomInput('');
    assertEquals(customValue, select.value);
    validateState([1, 2], [{from: 1, to: 2}], '', false);
    let whenBlurred =
        eventToPromise('custom-input-blurred-for-test', pagesSection);
    input.blur();

    await whenBlurred;
    await pagesSection.$.pageSettingsCustomInput.updateComplete;
    // Blurring a blank field sets the full page range.
    assertEquals(customValue, select.value);
    validateState([1, 2, 3], [], '', false);
    assertEquals('1-3', input.value);
    input.focus();

    await setCustomInput('5');
    assertEquals(customValue, select.value);
    // Invalid input doesn't change the preview.
    validateState([1, 2, 3], [], limitError + '3', true);

    await setCustomInput('');
    assertEquals(customValue, select.value);
    validateState([1, 2, 3], [], '', false);
    whenBlurred = eventToPromise('custom-input-blurred-for-test', pagesSection);
    input.blur();

    // Blurring an invalid value that has been cleared should reset the
    // value to all pages.
    await whenBlurred;
    assertEquals(customValue, select.value);
    validateState([1, 2, 3], [], '', false);
    assertEquals('1-3', input.value);

    // Re-focus and clear the input and then select "All" in the
    // dropdown.
    input.focus();

    await setCustomInput('');
    select.focus();

    await selectOption(pagesSection, allValue);
    flush();
    assertEquals(allValue, select.value);
    validateState([1, 2, 3], [], '', false);

    // Reselect custom. This should focus the input.
    await Promise.all([
      selectOption(pagesSection, customValue),
      eventToPromise('focus', input),
    ]);
    // Input has been cleared.
    assertEquals('', input.value);
    validateState([1, 2, 3], [], '', false);
    whenBlurred = eventToPromise('custom-input-blurred-for-test', pagesSection);
    input.blur();

    await whenBlurred;
    assertEquals('1-3', input.value);

    // Change the page count. Since the range was set automatically, this
    // should reset it to the new set of all pages.
    pagesSection.pageCount = 2;
    await pagesSection.$.pageSettingsCustomInput.updateComplete;
    validateState([1, 2], [], '', false);
    assertEquals('1-2', input.value);
  });

  // Verifies that the input is never disabled when the validity of the
  // setting changes.
  test(
      'InputNotDisabledOnValidityChange', async () => {
        pagesSection.pageCount = 3;
        // In the real UI, the print preview app listens for this event from
        // this section and others and sets disabled to true if any change from
        // true to false is detected. Imitate this here. Since we are only
        // interacting with the pages input, at no point should the input be
        // disabled, as it will lose focus.
        pagesSection.addEventListener('setting-valid-changed', function(e) {
          assertFalse(pagesSection.$.pageSettingsCustomInput.disabled);
          pagesSection.set('disabled', !(e as CustomEvent<boolean>).detail);
          assertFalse(pagesSection.$.pageSettingsCustomInput.disabled);
        });

        await selectOption(pagesSection, PagesValue.CUSTOM.toString());
        await setCustomInput('1');
        validateState([1], [{from: 1, to: 1}], '', false);

        await setCustomInput('12');
        validateState([1], [{from: 1, to: 1}], limitError + '3', true);

        // Restore valid input
        await setCustomInput('1');
        validateState([1], [{from: 1, to: 1}], '', false);

        // Invalid input again
        await setCustomInput('8');
        validateState([1], [{from: 1, to: 1}], limitError + '3', true);

        // Clear input
        await setCustomInput('');
        validateState([1], [{from: 1, to: 1}], '', false);

        // Set valid input
        await setCustomInput('2');
        validateState([2], [{from: 2, to: 2}], '', false);
      });

  // Verifies that the enter key event is bubbled to the pages settings
  // element, so that it will be bubbled to the print preview app to trigger a
  // print.
  test(
      'EnterOnInputTriggersPrint', async () => {
        pagesSection.pageCount = 3;
        const input = pagesSection.$.pageSettingsCustomInput.inputElement;
        const whenPrintReceived = eventToPromise('keydown', pagesSection);

        // Setup an empty input by selecting custom..
        const customValue = PagesValue.CUSTOM.toString();
        const pagesSelect = pagesSection.shadowRoot!.querySelector('select')!;
        await Promise.all([
          selectOption(pagesSection, customValue),
          eventToPromise('focus', input),
        ]);
        assertEquals(customValue, pagesSelect.value);
        keyEventOn(input, 'keydown', 0, [], 'Enter');

        await whenPrintReceived;
        // Keep custom selected, but pages to print should still be all.
        assertEquals(customValue, pagesSelect.value);
        validateState([1, 2, 3], [], '', false);

        // Select a custom input of 1.
        await setCustomInput('1');
        assertEquals(customValue, pagesSelect.value);
        const whenSecondPrintReceived = eventToPromise('keydown', pagesSection);
        keyEventOn(input, 'keydown', 0, [], 'Enter');

        await whenSecondPrintReceived;
        assertEquals(customValue, pagesSelect.value);
        validateState([1], [{from: 1, to: 1}], '', false);
      });
});