chromium/chrome/test/data/webui/chromeos/settings/os_apps_page/app_parental_controls_page/app_parental_controls_subpage_test.ts

// Copyright 2024 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://os-settings/lazy_load.js';

import {SettingsAppParentalControlsSubpageElement} from 'chrome://os-settings/lazy_load.js';
import {CrToggleElement, Router, routes, setAppParentalControlsProviderForTesting} from 'chrome://os-settings/os_settings.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {FakeAppParentalControlsHandler} from './fake_app_parental_controls_handler.js';
import {createApp} from './test_utils.js';

suite('AppParentalControlsSubpage', () => {
  let page: SettingsAppParentalControlsSubpageElement;
  let handler: FakeAppParentalControlsHandler;

  async function createPage(): Promise<void> {
    Router.getInstance().navigateTo(routes.APP_PARENTAL_CONTROLS);
    page = new SettingsAppParentalControlsSubpageElement();
    // Set verified to true to indicate pin was accepted on the apps page.
    page.set('isVerified', true);
    document.body.appendChild(page);
    await flushTasks();
  }

  function getApps(): NodeListOf<HTMLElement> {
    const appList = page.shadowRoot!.querySelector('#appParentalControlsList');
    assertTrue(!!appList);
    assertTrue(isVisible(appList));
    return appList.querySelectorAll<HTMLElement>('block-app-item');
  }

  setup(() => {
    handler = new FakeAppParentalControlsHandler();
    setAppParentalControlsProviderForTesting(handler);
  });

  teardown(() => {
    Router.getInstance().resetRouteForTesting();
    page.remove();
  });

  test('App list is in alphabetical order', async () => {
    const appTitle1 = 'Files';
    const appTitle2 = 'Chrome';
    handler.addAppForTesting(createApp('file-id', appTitle1, true));
    handler.addAppForTesting(createApp('chrome-id', appTitle2, false));

    await createPage();

    const apps = getApps();
    assertEquals(2, apps.length);
    assertTrue(!!apps[0]);
    assertTrue(!!apps[1]);

    const title1 = apps[0].shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!title1);
    assertEquals(title1.innerText, appTitle2);

    const title2 = apps[1].shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!title2);
    assertEquals(title2.innerText, appTitle1);

    await flushTasks();

    const blockedAppsCount =
        page.shadowRoot!.querySelector<HTMLElement>('#blockedAppsCount');
    assertTrue(!!blockedAppsCount);
    assertEquals(blockedAppsCount.innerText, '1 of 2 apps blocked');
  });

  test('Click toggle updates app blocked state', async () => {
    const app = createApp('file-id', 'Files', false);
    handler.addAppForTesting(app);

    await createPage();

    const apps = getApps();
    assertEquals(1, apps.length);

    const appElement = apps[0];
    assertTrue(!!appElement);
    const appToggle =
        appElement.shadowRoot!.querySelector<CrToggleElement>('.app-toggle');
    assertTrue(!!appToggle);
    assertTrue(appToggle.checked);

    appToggle.click();
    assertFalse(appToggle.checked);

    assertEquals(handler.getCallCount('updateApp'), 1);
    const args1 = handler.getArgs('updateApp')[0];
    assertEquals(2, args1.length);
    assertEquals(app.id, args1[0]);
    assertTrue(/*isBlocked*/ args1[1]);
    assertTrue(app.isBlocked);

    appToggle.click();
    assertTrue(appToggle.checked);

    assertEquals(handler.getCallCount('updateApp'), 2);
    const args2 = handler.getArgs('updateApp')[1];
    assertEquals(2, args2.length);
    assertEquals(app.id, args2[0]);
    assertFalse(/*isBlocked*/ args2[1]);
    assertFalse(app.isBlocked);
  });

  test('Installing and uninstalling app updates app list', async () => {
    const filesAppId = 'files-id';
    const filesAppTitle = 'Files';
    const app = createApp(filesAppId, filesAppTitle, false);
    handler.addAppForTesting(app);

    await createPage();

    const observer = handler.getObserverRemote();
    assertTrue(!!observer);

    assertEquals(1, getApps().length);

    const chromeAppId = 'chrome-id';
    const chromeAppTitle = 'Chrome';
    observer.onAppInstalledOrUpdated({
      id: chromeAppId,
      title: chromeAppTitle,
      isBlocked: false,
    });
    await flushTasks();

    let appsList = getApps();
    assertEquals(2, appsList.length);
    assertTrue(!!appsList[0]);
    assertTrue(!!appsList[1]);

    let title1 =
        appsList[0].shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!title1);
    assertEquals(title1.innerText, chromeAppTitle);

    const title2 =
        appsList[1].shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!title2);
    assertEquals(title2.innerText, filesAppTitle);

    observer.onAppUninstalled({
      id: chromeAppId,
      title: chromeAppTitle,
      isBlocked: false,
    });
    await flushTasks();

    appsList = getApps();
    assertEquals(1, getApps().length);
    assertTrue(!!appsList[0]);

    title1 = appsList[0].shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!title1);
    assertEquals(title1.innerText, filesAppTitle);
  });

  test('Clicking app row updates app blocked state', async () => {
    const app = createApp('file-id', 'Files', false);
    handler.addAppForTesting(app);

    await createPage();

    const apps = getApps();
    assertEquals(1, apps.length);

    const appElement = apps[0];
    assertTrue(!!appElement);
    appElement.click();

    assertEquals(handler.getCallCount('updateApp'), 1);
    const args1 = handler.getArgs('updateApp')[0];
    assertEquals(2, args1.length);
    assertEquals(app.id, args1[0]);
    assertTrue(/*isBlocked*/ args1[1]);
    assertTrue(app.isBlocked);

    appElement.click();

    assertEquals(handler.getCallCount('updateApp'), 2);
    const args2 = handler.getArgs('updateApp')[1];
    assertEquals(2, args2.length);
    assertEquals(app.id, args2[0]);
    assertFalse(/*isBlocked*/ args2[1]);
    assertFalse(app.isBlocked);
  });

  test('Remote app changes update toggle state', async () => {
    const app = createApp('file-id', 'Files', false);
    handler.addAppForTesting(app);

    await createPage();

    const apps = getApps();
    assertEquals(1, apps.length);

    const appElement = apps[0];
    assertTrue(!!appElement);
    const appToggle =
        appElement.shadowRoot!.querySelector<CrToggleElement>('.app-toggle');
    assertTrue(!!appToggle);
    assertTrue(appToggle.checked);

    const observer = handler.getObserverRemote();
    assertTrue(!!observer);

    // Dispatch readiness changed event with block state changed.
    observer.onAppInstalledOrUpdated({
      id: app.id,
      title: app.title,
      isBlocked: !app.isBlocked,
    });
    await flushTasks();

    assertFalse(appToggle.checked);
  });

  test(
      'Readiness events without block state changes do not update list',
      async () => {
        const app = createApp('file-id', 'Files', false);
        handler.addAppForTesting(app);

        await createPage();

        const observer = handler.getObserverRemote();
        assertTrue(!!observer);

        const apps = getApps();
        assertEquals(1, apps.length);

        const appElement = apps[0];
        assertTrue(!!appElement);
        const appToggle = appElement.shadowRoot!.querySelector<CrToggleElement>(
            '.app-toggle');
        assertTrue(!!appToggle);
        assertTrue(appToggle.checked);

        // Dispatch readiness changed event without any changes to block state.
        observer.onAppInstalledOrUpdated(app);

        assertTrue(appToggle.checked);
        assertFalse(app.isBlocked);

        await flushTasks();

        const blockedAppsCount =
            page.shadowRoot!.querySelector<HTMLElement>('#blockedAppsCount');
        assertTrue(!!blockedAppsCount);
        assertEquals(blockedAppsCount.innerText, '0 of 1 apps blocked');
      });

  test('Unverified page redirects to Apps', async () => {
    Router.getInstance().navigateTo(routes.APP_PARENTAL_CONTROLS);
    // Page is unverified by default and will redirect.
    page = new SettingsAppParentalControlsSubpageElement();
    document.body.appendChild(page);
    await flushTasks();

    assertEquals(Router.getInstance().currentRoute, routes.APPS);
  });

  test('Blocked apps string is not shown when there are no apps', async () => {
    await createPage();
    await flushTasks();

    const blockedAppsCount =
        page.shadowRoot!.querySelector<HTMLElement>('#blockedAppsCount');
    assertFalse(isVisible(blockedAppsCount));
  });
});

suite('AppParentalControlsSubpage search', () => {
  let page: SettingsAppParentalControlsSubpageElement;
  let handler: FakeAppParentalControlsHandler;

  async function createPage(): Promise<void> {
    Router.getInstance().navigateTo(routes.APP_PARENTAL_CONTROLS);
    page = new SettingsAppParentalControlsSubpageElement();
    // Set verified to true to indicate pin was accepted on the apps page.
    page.set('isVerified', true);
    document.body.appendChild(page);
    await flushTasks();
  }

  function getApps(): NodeListOf<HTMLElement> {
    const appList = page.shadowRoot!.querySelector('#appParentalControlsList');
    assertTrue(!!appList);
    assertTrue(isVisible(appList));
    return appList.querySelectorAll<HTMLElement>('block-app-item');
  }

  setup(() => {
    handler = new FakeAppParentalControlsHandler();
    setAppParentalControlsProviderForTesting(handler);
  });

  teardown(() => {
    Router.getInstance().resetRouteForTesting();
    page.remove();
  });

  test('Search returns one result', async () => {
    const filesAppTitle = 'Files';
    handler.addAppForTesting(createApp('file-id', filesAppTitle, false));
    handler.addAppForTesting(createApp('chrome-id', 'Chrome', false));

    await createPage();

    assertEquals(2, getApps().length);

    page.searchTerm = 'f';
    await flushTasks();

    assertEquals(1, getApps().length);
    const app = getApps()[0];
    assertTrue(!!app);
    const appTitle = app.shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!appTitle);
    assertEquals(appTitle.innerText, filesAppTitle);
  });

  test('Search returns multiple results', async () => {
    const cameraAppTitle = 'Camera';
    const chromeAppTitle = 'Chrome';
    handler.addAppForTesting(createApp('file-id', 'Files', false));
    handler.addAppForTesting(createApp('chrome-id', chromeAppTitle, false));
    handler.addAppForTesting(createApp('camera-id', cameraAppTitle, false));

    await createPage();

    assertEquals(3, getApps().length);

    page.searchTerm = 'c';
    await flushTasks();

    assertEquals(2, getApps().length);

    const app1 = getApps()[0];
    assertTrue(!!app1);
    const appTitle1 = app1.shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!appTitle1);
    assertEquals(appTitle1.innerText, cameraAppTitle);

    const app2 = getApps()[1];
    assertTrue(!!app2);
    const appTitle2 = app2.shadowRoot!.querySelector<HTMLElement>('.app-title');
    assertTrue(!!appTitle2);
    assertEquals(appTitle2.innerText, chromeAppTitle);
  });

  test('Search returns no results', async () => {
    const filesAppTitle = 'Files';
    handler.addAppForTesting(createApp('file-id', filesAppTitle, false));
    handler.addAppForTesting(createApp('chrome-id', 'Chrome', false));

    await createPage();

    assertEquals(2, getApps().length);

    page.searchTerm = 'zzz';
    await flushTasks();

    assertEquals(0, getApps().length);
    const emptyAppList =
        page.shadowRoot!.querySelector<HTMLElement>('#noAppsLabel');
    assertTrue(!!emptyAppList);
    assertTrue(isVisible(emptyAppList));
  });


  test('Blocked apps string is not shown when search string is not empty', async () => {
    handler.addAppForTesting(createApp('file-id', 'Files', false));
    handler.addAppForTesting(createApp('chrome-id', 'Chrome', false));

    await createPage();

    assertEquals(2, getApps().length);

    page.searchTerm = 'f';
    await flushTasks();

    const blockedAppsCount =
    page.shadowRoot!.querySelector<HTMLElement>('#blockedAppsCount');
    assertFalse(isVisible(blockedAppsCount));
  });
});