chromium/chrome/test/data/webui/chromeos/crostini_installer_app_test.js

// Copyright 2019 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://crostini-installer/app.js';

import {BrowserProxy} from 'chrome://crostini-installer/browser_proxy.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';

const InstallerState = crostini.mojom.InstallerState;
const InstallerError = crostini.mojom.InstallerError;

class FakePageHandler extends TestBrowserProxy {
  constructor() {
    super([
      'install',
      'cancel',
      'cancelBeforeStart',
      'onPageClosed',
      'requestAmountOfFreeDiskSpace',
    ]);

    this.requestAmountOfFreeDiskSpaceResult_ = new Promise((resolve) => {
      this.resolveRequestAmountOfFreeDiskSpace_ = resolve;
    });
  }

  /** @override */
  install(diskSize, username) {
    this.methodCalled('install', [Number(diskSize), username]);
  }

  /** @override */
  cancel() {
    this.methodCalled('cancel');
  }

  /** @override */
  cancelBeforeStart() {
    this.methodCalled('cancelBeforeStart');
  }

  /** @override */
  onPageClosed() {
    this.methodCalled('onPageClosed');
  }

  /** @override */
  requestAmountOfFreeDiskSpace() {
    this.methodCalled('requestAmountOfFreeDiskSpace');
    return this.requestAmountOfFreeDiskSpaceResult_;
  }

  /**
   * Resolve the promise returned by `requestAmountOfFreeDiskSpace()`. Can only
   * be called once for the lifetime of the handler.
   */
  resolveRequestAmountOfFreeDiskSpace(
      ticks, defaultIndex, isLowSpaceAvailable) {
    this.resolveRequestAmountOfFreeDiskSpace_(
        {ticks, defaultIndex, isLowSpaceAvailable});
  }
}

class FakeBrowserProxy {
  constructor() {
    this.handler = new FakePageHandler();
    this.callbackRouter = new ash.crostiniInstaller.mojom.PageCallbackRouter();
    /** @type {appManagement.mojom.PageRemote} */
    this.page = this.callbackRouter.$.bindNewPipeAndPassRemote();
  }
}

suite('<crostini-installer-app>', () => {
  let fakeBrowserProxy;
  let app;

  setup(async () => {
    fakeBrowserProxy = new FakeBrowserProxy();
    BrowserProxy.setInstance(fakeBrowserProxy);

    app = document.createElement('crostini-installer-app');
    PolymerTest.clearBody();
    document.body.appendChild(app);

    await flushTasks();
  });

  teardown(function() {
    app.remove();
  });

  const clickButton = async (button) => {
    assertFalse(button.hidden);
    assertFalse(button.disabled);
    button.click();
    await flushTasks();
  };

  const getInstallButton = () => {
    return app.$$('#install');
  };

  const getCancelButton = () => {
    return app.$$('.cancel-button');
  };

  const clickNext = async () => {
    await clickButton(app.$.next);
  };

  const clickInstall = async () => {
    await clickButton(getInstallButton());
  };

  const clickCancel = async () => {
    await clickButton(getCancelButton());
  };

  const clickCustomSize = async () => {
    await clickButton(app.$$('#custom-size'));
  };

  /**
   * Checks whether a given element is hidden.
   * @param {!Element} element
   * @returns {boolean}
   */
  function isHidden(element) {
    return (
        !element || element.getBoundingClientRect().width <= 0 ||
        element.hidden);
  }

  const diskTicks = [
    {value: 1000, ariaValue: '1', label: '1'},
    {value: 2000, ariaValue: '2', label: '2'},
  ];

  test('installFlow', async () => {
    assertFalse(app.$$('#prompt-message').hidden);
    assertEquals(fakeBrowserProxy.handler.getCallCount('install'), 0);

    // It should wait for disk info to be available.
    await clickNext();
    await flushTasks();
    assertFalse(app.$$('#prompt-message').hidden);

    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, false);
    await flushTasks();
    assertFalse(app.$$('#configure-message').hidden);
    await clickCancel();  // Back to the prompt page.
    assertFalse(app.$$('#prompt-message').hidden);

    await clickNext();
    await flushTasks();
    assertFalse(app.$$('#configure-message').hidden);
    await clickInstall();
    const [diskSize, username] =
        await fakeBrowserProxy.handler.whenCalled('install');
    assertEquals(username, loadTimeData.getString('defaultContainerUsername'));
    assertFalse(app.$$('#installing-message').hidden);
    assertEquals(fakeBrowserProxy.handler.getCallCount('install'), 1);
    assertTrue(getInstallButton().hidden);

    fakeBrowserProxy.page.onProgressUpdate(
        InstallerState.kCreateDiskImage, 0.5);
    await flushTasks();
    assertTrue(
        !!app.$$('#installing-message > div').textContent.trim(),
        'progress message should be set');
    assertEquals(
        app.$$('#installing-message > paper-progress').getAttribute('value'),
        '50');

    assertEquals(fakeBrowserProxy.handler.getCallCount('onPageClosed'), 0);
    fakeBrowserProxy.page.onInstallFinished(InstallerError.kNone);
    await flushTasks();
    assertEquals(fakeBrowserProxy.handler.getCallCount('onPageClosed'), 1);
  });

  // We only proceed to the config page if disk info is available. Let's make
  // sure if the user click the next button multiple time very soon it dose not
  // blow up.
  test('multipleClickNextBeforeDiskAvailable', async () => {
    assertFalse(app.$$('#prompt-message').hidden);

    // It should wait for disk info to be available.
    await clickNext();
    await clickNext();
    await clickNext();
    await flushTasks();
    assertFalse(app.$$('#prompt-message').hidden);

    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, false);
    await flushTasks();
    // Enter configure page as usual
    assertFalse(app.$$('#configure-message').hidden);

    // Can back to prompt page as usual.
    await clickCancel();
    assertFalse(app.$$('#prompt-message').hidden);

    await clickNext();
    await flushTasks();
    // Re-enter configure page as usual
    assertFalse(app.$$('#configure-message').hidden);
  });

  test('straightToErrorPageIfMinDiskUnmet', async () => {
    assertFalse(app.$$('#prompt-message').hidden);

    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace([], 0, false);

    await clickNext();
    await flushTasks();
    assertFalse(app.$$('#error-message').hidden);
    assertTrue(
        !!app.$$('#error-message > div').textContent.trim(),
        'error message should be set');
    // We do not show retry button in this case.
    assertTrue(getInstallButton().hidden);
  });

  test('showWarningIfLowFreeSpace', async () => {
    assertFalse(app.$$('#prompt-message').hidden);

    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, true);

    await clickNext();
    await flushTasks();
    assertFalse(app.$$('#configure-message').hidden);
    assertFalse(isHidden(app.$$('#low-free-space-warning')));
  });

  diskTicks.forEach(async (_, defaultIndex) => {
    test(`configDiskSpaceWithDefault-${defaultIndex}`, async () => {
      assertFalse(app.$$('#prompt-message').hidden);

      fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
          diskTicks, defaultIndex, false);

      await clickNext();
      await flushTasks();

      assertFalse(app.$$('#configure-message').hidden);
      assertTrue(isHidden(app.$$('#low-free-space-warning')));
      assertTrue(isHidden(app.$$('#diskSlider')));

      await clickInstall();
      const [diskSize, username] =
          await fakeBrowserProxy.handler.whenCalled('install');
      assertEquals(Number(diskSize), diskTicks[defaultIndex].value);
      assertEquals(fakeBrowserProxy.handler.getCallCount('install'), 1);
    });
  });

  test('configDiskSpaceWithUserSelection', async () => {
    assertFalse(app.$$('#prompt-message').hidden);

    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, false);

    await clickNext();
    await flushTasks();
    await clickCustomSize();
    await flushTasks();

    assertFalse(app.$$('#configure-message').hidden);
    assertTrue(isHidden(app.$$('#low-free-space-warning')));
    assertFalse(isHidden(app.$$('#diskSlider')));

    app.$$('#diskSlider').value = 1;

    await clickInstall();
    const [diskSize, username] =
        await fakeBrowserProxy.handler.whenCalled('install');
    assertEquals(Number(diskSize), diskTicks[1].value);
    assertEquals(fakeBrowserProxy.handler.getCallCount('install'), 1);
  });

  test('configUsername', async () => {
    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, false);
    await clickNext();

    assertEquals(
        app.$.username.value,
        loadTimeData.getString('defaultContainerUsername'));

    // Test invalid usernames
    const invalidUsernames = [
      '0abcd',            // Invalid (number) starting character.
      'aBcd',             // Invalid (uppercase) character.
      'spa ce',           // Invalid (space) character.
      '-dash',            // Invalid (dash) starting character.
      'name\\backslash',  // Invalid (backslash) character.
      'name@mpersand',    // Invalid (ampersand) character.
      // Reserved users
      'root', 'daemon', 'bin', 'sys', 'sync', 'games', 'man', 'lp', 'mail',
      'news', 'uucp', 'proxy', 'www-data', 'backup', 'list', 'irc', 'gnats',
      'nobody', '_apt', 'systemd-timesync', 'systemd-network',
      'systemd-resolve', 'systemd-bus-proxy', 'messagebus', 'sshd', 'rtkit',
      'pulse', 'android-root', 'chronos-access', 'android-everybody',
      // End reserved users
    ];

    for (const username of invalidUsernames) {
      app.$.username.value = username;

      await flushTasks();
      assertTrue(app.$.username.invalid);
      assertTrue(!!app.$.username.errorMessage);
      assertTrue(app.$.install.disabled);
    }

    // Test the empty username. The username field should not show an error, but
    // we want the install button to be disabled.
    app.$.username.value = '';
    await flushTasks();
    assertFalse(app.$.username.invalid);
    assertFalse(!!app.$.username.errorMessage);
    assertTrue(app.$.install.disabled);

    // Test a valid username
    const validUsername = 'totally-valid_username';
    app.$.username.value = validUsername;
    await flushTasks();
    assertFalse(app.$.username.invalid);
    clickInstall();
    const [diskSize, username] =
        await fakeBrowserProxy.handler.whenCalled('install');
    assertEquals(username, validUsername);
    assertEquals(fakeBrowserProxy.handler.getCallCount('install'), 1);
  });

  test('errorCancel', async () => {
    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, false);
    await clickNext();
    await clickInstall();
    fakeBrowserProxy.page.onInstallFinished(InstallerError.kErrorOffline);
    await flushTasks();
    assertFalse(app.$$('#error-message').hidden);
    assertTrue(
        !!app.$$('#error-message > div').textContent.trim(),
        'error message should be set');

    await clickCancel();
    assertEquals(fakeBrowserProxy.handler.getCallCount('onPageClosed'), 1);
    assertEquals(fakeBrowserProxy.handler.getCallCount('cancelBeforeStart'), 0);
    assertEquals(fakeBrowserProxy.handler.getCallCount('cancel'), 0);
  });

  test('errorRetry', async () => {
    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, false);
    await clickNext();
    await clickInstall();
    fakeBrowserProxy.page.onInstallFinished(InstallerError.kErrorOffline);
    await flushTasks();
    assertFalse(app.$$('#error-message').hidden);
    assertTrue(
        !!app.$$('#error-message > div').textContent.trim(),
        'error message should be set');

    await clickInstall();
    assertEquals(fakeBrowserProxy.handler.getCallCount('install'), 2);
  });

  test('errorNeedUpdate', async () => {
    fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
        diskTicks, 0, false);
    await clickNext();
    await clickInstall();
    fakeBrowserProxy.page.onInstallFinished(InstallerError.kNeedUpdate);
    await flushTasks();

    assertEquals(app.$$('#title').innerText, 'ChromeOS update required');
    assertFalse(app.$$('#error-message').hidden);
    assertEquals(
        app.$$('#error-message').innerText,
        'To finish setting up Linux, update ChromeOS and try again.');
    assertFalse(app.$$('#settings').hidden);
    assertEquals(app.$$('#settings').innerText, 'Open Settings');
  });

  [clickCancel,
   () => fakeBrowserProxy.page.requestClose(),
  ].forEach((canceller, i) => test(`cancelBeforeStart-{i}`, async () => {
              await canceller();
              await flushTasks();
              assertEquals(
                  fakeBrowserProxy.handler.getCallCount('cancelBeforeStart'),
                  1);
              assertEquals(
                  fakeBrowserProxy.handler.getCallCount('onPageClosed'), 1);
              assertEquals(fakeBrowserProxy.handler.getCallCount('cancel'), 0);
            }));

  // This is a special case that requestClose is different from clicking cancel
  // --- instead of going back to the previous page, requestClose should close
  // the page immediately.
  test('requestCloseAtConfigPage', async () => {
    await clickNext();  // Progress to config page.
    await fakeBrowserProxy.page.requestClose();
    await flushTasks();
    assertEquals(fakeBrowserProxy.handler.getCallCount('cancelBeforeStart'), 1);
    assertEquals(fakeBrowserProxy.handler.getCallCount('onPageClosed'), 1);
    assertEquals(fakeBrowserProxy.handler.getCallCount('cancel'), 0);
  });


  [clickCancel,
   () => fakeBrowserProxy.page.requestClose(),
  ].forEach((canceller, i) => test(`cancelAfterStart-{i}`, async () => {
              fakeBrowserProxy.handler.resolveRequestAmountOfFreeDiskSpace(
                  diskTicks, 0, false);
              await clickNext();
              await clickInstall();
              await canceller();
              await flushTasks();
              assertEquals(fakeBrowserProxy.handler.getCallCount('cancel'), 1);
              assertEquals(
                  fakeBrowserProxy.handler.getCallCount('onPageClosed'), 0,
                  'should not close until onCanceled is called');
              assertTrue(getInstallButton().hidden);
              assertTrue(getCancelButton().disabled);

              fakeBrowserProxy.page.onCanceled();
              await flushTasks();
              assertEquals(
                  fakeBrowserProxy.handler.getCallCount('onPageClosed'), 1);
            }));
});