chromium/chrome/test/data/webui/settings/route_test.ts

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

// clang-format off
import type {SettingsRoutes} from 'chrome://settings/settings.js';
import {resetRouterForTesting, buildRouter, loadTimeData, Route, Router, routes, resetPageVisibilityForTesting} from 'chrome://settings/settings.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';

// clang-format on

suite('Basic', function() {
  /**
   * Returns a new promise that resolves after a window 'popstate' event.
   */
  function whenPopState(causeEvent: () => void): Promise<void> {
    const promise = new Promise<void>(function(resolve) {
      window.addEventListener('popstate', function callback() {
        window.removeEventListener('popstate', callback);
        resolve();
      });
    });

    causeEvent();
    return promise;
  }

  teardown(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
  });

  /**
   * Tests a specific navigation situation.
   */
  function testNavigateBackUsesHistory(
      previousRoute: Route, currentRoute: Route,
      expectedNavigatePreviousResult: Route): Promise<void> {
    Router.getInstance().navigateTo(previousRoute);
    Router.getInstance().navigateTo(currentRoute);

    return whenPopState(function() {
             Router.getInstance().navigateToPreviousRoute();
           })
        .then(function() {
          assertEquals(
              expectedNavigatePreviousResult,
              Router.getInstance().getCurrentRoute());
        });
  }

  /**
   * Tests that |routeParamUpdate()| sets URL parameters as expected, doesn't
   * change the current or previous route, and that a back navigation still
   * works afterwards as expected.
   * @param route0 1st route that the test navigates to.
   * @param route1 2nd route that the test navigates to.
   * @param params Get applied after the 2nd navigation
   * @param expectedRoute Route on which a back navigation should land
   *     after the 1st and 2nd navigation.
   */
  async function testUpdateRouteParamsNavigation(
      route0: Route, route1: Route, params: URLSearchParams,
      expectedRoute: Route): Promise<void> {
    Router.getInstance().navigateTo(route0);
    Router.getInstance().navigateTo(route1);
    Router.getInstance().updateRouteParams(params);

    assertEquals(
        params.toString(),
        Router.getInstance().getQueryParameters().toString());
    assertEquals(route1, Router.getInstance().getCurrentRoute());

    await whenPopState(function() {
      Router.getInstance().navigateToPreviousRoute();
    });
    assertEquals(expectedRoute, Router.getInstance().getCurrentRoute());
  }

  test('tree structure', function() {
    // Set up root page routes.
    const BASIC = new Route('/');
    assertEquals(0, BASIC.depth);

    const ADVANCED = new Route('/advanced');
    assertFalse(ADVANCED.isSubpage());
    assertEquals(0, ADVANCED.depth);

    // Test a section route.
    const PRIVACY = ADVANCED.createSection('/privacy', 'privacy');
    assertEquals(ADVANCED, PRIVACY.parent);
    assertEquals(1, PRIVACY.depth);
    assertFalse(PRIVACY.isSubpage());
    assertFalse(BASIC.contains(PRIVACY));
    assertTrue(ADVANCED.contains(PRIVACY));
    assertTrue(PRIVACY.contains(PRIVACY));
    assertFalse(PRIVACY.contains(ADVANCED));

    // Test a subpage route.
    const SITE_SETTINGS = PRIVACY.createChild('/siteSettings');
    assertEquals('/siteSettings', SITE_SETTINGS.path);
    assertEquals(PRIVACY, SITE_SETTINGS.parent);
    assertEquals(2, SITE_SETTINGS.depth);
    assertTrue(SITE_SETTINGS.isSubpage());
    assertEquals('privacy', SITE_SETTINGS.section);
    assertFalse(BASIC.contains(SITE_SETTINGS));
    assertTrue(ADVANCED.contains(SITE_SETTINGS));
    assertTrue(PRIVACY.contains(SITE_SETTINGS));

    // Test a sub-subpage route.
    const SITE_SETTINGS_ALL = SITE_SETTINGS.createChild('all');
    assertEquals('/siteSettings/all', SITE_SETTINGS_ALL.path);
    assertEquals(SITE_SETTINGS, SITE_SETTINGS_ALL.parent);
    assertEquals(3, SITE_SETTINGS_ALL.depth);
    assertTrue(SITE_SETTINGS_ALL.isSubpage());

    // Test a non-subpage child of ADVANCED.
    const CLEAR_BROWSER_DATA = ADVANCED.createChild('/clearBrowserData');
    assertFalse(CLEAR_BROWSER_DATA.isSubpage());
    assertEquals('', CLEAR_BROWSER_DATA.section);
  });

  test('no duplicate routes', function() {
    const paths = new Set();
    Object.values(routes).forEach(function(route) {
      assertFalse(paths.has(route.path), route.path);
      paths.add(route.path);
    });
  });

  test('navigate back to parent previous route', function() {
    return testNavigateBackUsesHistory(
        routes.BASIC, routes.PEOPLE, routes.BASIC);
  });

  test(
      'navigate back to parent previous route, ignore non-history navigation',
      function() {
        return testUpdateRouteParamsNavigation(
            routes.BASIC, routes.PEOPLE, new URLSearchParams('param=test'),
            routes.BASIC);
      });

  test('navigate back to non-ancestor shallower route', function() {
    return testNavigateBackUsesHistory(
        routes.ADVANCED, routes.PEOPLE, routes.BASIC);
  });

  test(
      'navigate back to non-ancestor shallower route, ignore non-history navigation',
      function() {
        return testUpdateRouteParamsNavigation(
            routes.ADVANCED, routes.PEOPLE, new URLSearchParams('param=test'),
            routes.BASIC);
      });

  test('navigate back to sibling route', function() {
    return testNavigateBackUsesHistory(
        routes.APPEARANCE, routes.PEOPLE, routes.APPEARANCE);
  });

  test(
      'navigate back to sibling route, ignore non-history navigation',
      function() {
        return testUpdateRouteParamsNavigation(
            routes.APPEARANCE, routes.PEOPLE, new URLSearchParams('param=test'),
            routes.APPEARANCE);
      });

  test('navigate back to parent when previous route is deeper', function() {
    Router.getInstance().navigateTo(routes.SYNC);
    Router.getInstance().navigateTo(routes.PEOPLE);
    Router.getInstance().navigateToPreviousRoute();
    assertEquals(routes.BASIC, Router.getInstance().getCurrentRoute());
  });

  test('navigate back to BASIC when going back from root pages', function() {
    Router.getInstance().navigateTo(routes.PEOPLE);
    Router.getInstance().navigateTo(routes.ADVANCED);
    Router.getInstance().navigateToPreviousRoute();
    assertEquals(routes.BASIC, Router.getInstance().getCurrentRoute());
  });

  test('navigateTo respects removeSearch optional parameter', function() {
    const params = new URLSearchParams('search=foo');
    Router.getInstance().navigateTo(routes.BASIC, params);
    assertEquals(
        params.toString(),
        Router.getInstance().getQueryParameters().toString());

    Router.getInstance().navigateTo(
        routes.SITE_SETTINGS, undefined,
        /* removeSearch */ false);
    assertEquals(
        params.toString(),
        Router.getInstance().getQueryParameters().toString());

    Router.getInstance().navigateTo(
        routes.SEARCH_ENGINES, undefined,
        /* removeSearch */ true);
    assertEquals('', Router.getInstance().getQueryParameters().toString());
  });

  test('navigateTo ADVANCED forwards to BASIC', function() {
    Router.getInstance().navigateTo(routes.ADVANCED);
    assertEquals(routes.BASIC, Router.getInstance().getCurrentRoute());
  });

  test('popstate flag works', function() {
    const router = Router.getInstance();
    router.navigateTo(routes.BASIC);
    assertFalse(router.lastRouteChangeWasPopstate());

    router.navigateTo(routes.PEOPLE);
    assertFalse(router.lastRouteChangeWasPopstate());

    return whenPopState(function() {
             window.history.back();
           })
        .then(function() {
          assertEquals(routes.BASIC, router.getCurrentRoute());
          assertTrue(router.lastRouteChangeWasPopstate());

          router.navigateTo(routes.ADVANCED);
          assertFalse(router.lastRouteChangeWasPopstate());
        });
  });

  test('getRouteForPath trailing slashes', function() {
    assertEquals(routes.BASIC, Router.getInstance().getRouteForPath('/'));
    assertEquals(null, Router.getInstance().getRouteForPath('//'));

    // Simple path.
    assertEquals(
        routes.PEOPLE, Router.getInstance().getRouteForPath('/people/'));
    assertEquals(
        routes.PEOPLE, Router.getInstance().getRouteForPath('/people'));

    // Path with a slash.
    assertEquals(
        routes.SITE_SETTINGS_SITE_DETAILS,
        Router.getInstance().getRouteForPath('/content/siteDetails/'));
    assertEquals(
        routes.SITE_SETTINGS_SITE_DETAILS,
        Router.getInstance().getRouteForPath('/content/siteDetails'));
  });

  test('isNavigableDialog', function() {
    assertTrue(routes.CLEAR_BROWSER_DATA.isNavigableDialog);
    assertTrue(routes.CLEAR_BROWSER_DATA.parent === routes.PRIVACY);
    assertFalse(routes.CLEAR_BROWSER_DATA.isSubpage());

    assertTrue(routes.RESET_DIALOG.isNavigableDialog);
    assertTrue(routes.RESET_DIALOG.parent === routes.RESET);
    assertTrue(routes.TRIGGERED_RESET_DIALOG.isNavigableDialog);
    assertTrue(routes.TRIGGERED_RESET_DIALOG.parent === routes.RESET);

    // <if expr="chromeos_ash">
    // Regression test for b/265453606.
    assertFalse('SIGN_OUT' in routes);
    // </if>

    // <if expr="not chromeos_ash">
    assertTrue(routes.SIGN_OUT.isNavigableDialog);
    assertTrue(routes.SIGN_OUT.parent === routes.PEOPLE);
    assertTrue(routes.IMPORT_DATA.isNavigableDialog);
    assertTrue(routes.IMPORT_DATA.parent === routes.PEOPLE);
    // </if>

    assertFalse(routes.PRIVACY.isNavigableDialog);
    // <if expr="not is_chromeos">
    assertFalse(routes.DEFAULT_BROWSER.isNavigableDialog);
    // </if>
  });

  test('pageVisibility affects route availability', function() {
    resetPageVisibilityForTesting({
      appearance: false,
      autofill: false,
      defaultBrowser: false,
      onStartup: false,
      reset: false,
    });

    const router = buildRouter();
    const hasRoute = (route: string) =>
        router.getRoutes().hasOwnProperty(route);

    assertTrue(hasRoute('BASIC'));

    assertFalse(hasRoute('APPEARANCE'));
    assertFalse(hasRoute('AUTOFILL'));
    assertFalse(hasRoute('DEFAULT_BROWSER'));
    assertFalse(hasRoute('ON_STARTUP'));
    assertFalse(hasRoute('RESET'));
  });

  test(
      'getAbsolutePath works in direct and within-settings navigation',
      function() {
        Router.getInstance().resetRouteForTesting();
        // Check getting the absolute path while not inside settings returns the
        // correct path.
        window.location.href = 'https://example.com/path/to/page.html';
        assertEquals(
            'chrome://settings/cookies', routes.COOKIES.getAbsolutePath());

        // Check getting the absolute path while inside settings returns the
        // correct path for the current route and a different route.
        Router.getInstance().navigateTo(routes.DOWNLOADS);
        assertEquals(
            'chrome://settings/downloads',
            Router.getInstance().getCurrentRoute().getAbsolutePath());
        assertEquals(
            'chrome://settings/languages', routes.LANGUAGES.getAbsolutePath());
      });

  test('resetRouterForTesting updates routes', function() {
    resetRouterForTesting();
    const routesLocal1 = Router.getInstance().getRoutes();
    assertEquals(routes, routesLocal1);

    resetRouterForTesting();
    const routesLocal2 = Router.getInstance().getRoutes();
    assertNotEquals(routesLocal1, routesLocal2);
    assertEquals(routes, routesLocal2);
  });
});

suite('DynamicParameters', function() {
  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    window.history.replaceState({}, '', 'search?guid=a%2Fb&foo=42');
    const settingsUi = document.createElement('settings-ui');
    document.body.appendChild(settingsUi);
  });

  test('get parameters from URL and navigation', function(done) {
    assertEquals(routes.SEARCH, Router.getInstance().getCurrentRoute());
    assertEquals('a/b', Router.getInstance().getQueryParameters().get('guid'));
    assertEquals('42', Router.getInstance().getQueryParameters().get('foo'));

    const params = new URLSearchParams();
    params.set('bar', 'b=z');
    params.set('biz', '3');
    Router.getInstance().navigateTo(routes.SEARCH_ENGINES, params);
    assertEquals(routes.SEARCH_ENGINES, Router.getInstance().getCurrentRoute());
    assertEquals('b=z', Router.getInstance().getQueryParameters().get('bar'));
    assertEquals('3', Router.getInstance().getQueryParameters().get('biz'));
    assertEquals('?bar=b%3Dz&biz=3', window.location.search);

    window.addEventListener('popstate', function() {
      assertEquals('/search', Router.getInstance().getCurrentRoute().path);
      assertEquals(routes.SEARCH, Router.getInstance().getCurrentRoute());
      assertEquals(
          'a/b', Router.getInstance().getQueryParameters().get('guid'));
      assertEquals('42', Router.getInstance().getQueryParameters().get('foo'));
      done();
    });
    window.history.back();
  });
});

suite('NonExistentRoute', function() {
  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    window.history.replaceState({}, '', 'non/existent/route');
    const settingsUi = document.createElement('settings-ui');
    document.body.appendChild(settingsUi);
  });

  test('redirect to basic', function() {
    assertEquals(routes.BASIC, Router.getInstance().getCurrentRoute());
    assertEquals('/', location.pathname);
  });
});

suite('SafetyHubReachable', function() {
  let routes: SettingsRoutes;

  setup(function() {
    loadTimeData.overrideValues({enableSafetyHub: true});
    resetRouterForTesting();

    routes = Router.getInstance().getRoutes();
    Router.getInstance().navigateTo(routes.BASIC);
    return flushTasks();
  });

  test('SafetyHubRouteReachable', async function() {
    let path = Router.getInstance().getCurrentRoute().path;
    assertEquals('/', path);

    Router.getInstance().navigateTo(routes.SAFETY_HUB);
    await flushTasks();

    // Assert that the route is changed to safety hub.
    path = Router.getInstance().getCurrentRoute().path;
    assertEquals('/safetyCheck', path);
  });

  test('SafetyCheckRouteNotReachable', async function() {
    // When Safety Hub is enabled, SafetyCheck is not reachable.
    assertEquals(routes.SAFETY_CHECK, undefined);
  });
});

suite('SafetyHubNotReachable', function() {
  let routes: SettingsRoutes;

  setup(function() {
    loadTimeData.overrideValues({enableSafetyHub: false});
    resetRouterForTesting();

    routes = Router.getInstance().getRoutes();
  });

  test('SafetyHubRouteNotReachable', async function() {
    // Safety Hub should not be reachable.
    assertEquals(routes.SAFETY_HUB, undefined);
  });

  test('SafetyCheckRouteReachable', async function() {
    let path = Router.getInstance().getCurrentRoute().path;
    assertEquals('/', path);

    Router.getInstance().navigateTo(routes.SAFETY_CHECK);
    await flushTasks();

    // Assert that the route is changed to SafetyCheck.
    path = Router.getInstance().getCurrentRoute().path;
    assertEquals('/safetyCheck', path);
  });
});