chromium/chrome/test/data/webui/settings/fingerprint_progress_arc_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://settings/lazy_load.js';

import type {FingerprintProgressArcElement} from 'chrome://settings/lazy_load.js';
import {FINGERPRINT_CHECK_DARK_URL, FINGERPRINT_CHECK_LIGHT_URL, FINGERPRINT_SCANNED_ICON_DARK, FINGERPRINT_SCANNED_ICON_LIGHT, PROGRESS_CIRCLE_BACKGROUND_COLOR_DARK, PROGRESS_CIRCLE_BACKGROUND_COLOR_LIGHT, PROGRESS_CIRCLE_FILL_COLOR_DARK, PROGRESS_CIRCLE_FILL_COLOR_LIGHT} from 'chrome://settings/lazy_load.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals} from 'chrome://webui-test/chai_assert.js';
import {MockController} from 'chrome://webui-test/mock_controller.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';

class FakeMediaQueryList extends EventTarget implements MediaQueryList {
  private listener_: ((e: MediaQueryListEvent) => any)|null = null;
  private matches_: boolean = false;
  private media_: string;

  constructor(media: string) {
    super();
    this.media_ = media;
  }

  addListener(listener: (e: MediaQueryListEvent) => any) {
    this.listener_ = listener;
  }

  removeListener(listener: (e: MediaQueryListEvent) => any) {
    assertEquals(listener, this.listener_);
    this.listener_ = null;
  }

  onchange() {
    if (this.listener_) {
      this.listener_(new MediaQueryListEvent(
          'change', {media: this.media_, matches: this.matches_}));
    }
  }

  get media(): string {
    return this.media_;
  }

  get matches(): boolean {
    return this.matches_;
  }

  set matches(matches: boolean) {
    if (this.matches_ !== matches) {
      this.matches_ = matches;
      this.onchange();
    }
  }
}

/** @fileoverview Suite of tests for fingerprint-progress-arc. */
suite('cr_fingerprint_progress_arc_test', function() {
  /**
   * An object descrbing a 2d point.
   */
  interface Point {
    x: number;
    y: number;
  }

  const canvasColor: string = 'rgba(255, 255, 255, 1.0)';

  let progressArc: FingerprintProgressArcElement;
  let canvas: HTMLCanvasElement;
  let mockController: MockController;
  let fakeMediaQueryList: FakeMediaQueryList;

  function clearCanvas(canvas: HTMLCanvasElement) {
    const ctx = canvas.getContext('2d')!;
    ctx.fillStyle = canvasColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  }

  setup(function() {
    mockController = new MockController();
    const matchMediaMock =
        mockController.createFunctionMock(window, 'matchMedia');
    fakeMediaQueryList = new FakeMediaQueryList('(prefers-color-scheme: dark)');
    matchMediaMock.returnValue = fakeMediaQueryList;

    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    progressArc = document.createElement('fingerprint-progress-arc');
    document.body.appendChild(progressArc);

    // Override some parameters and function for testing purposes.
    canvas = progressArc.$.canvas;
    canvas.width = 300;
    canvas.height = 150;
    progressArc.circleRadius = 50;
    clearCanvas(progressArc.$.canvas);
    flush();
  });

  teardown(function() {
    mockController.reset();
  });

  /**
   * Helper function which gets the (r, g, b, a)-formatted color at |point| on
   * the canvas.
   */
  function getColor(point: Point): string {
    const ctx = canvas.getContext('2d')!;
    const pixel = ctx.getImageData(point.x, point.y, 1, 1).data;
    return `rgba(${pixel[0]!}, ${pixel[1]!}, ${pixel[2]!}, ${
        (pixel[3]! / 255).toFixed(1)})`;
  }

  /**
   * Helper function which checks that |expectedColor| matches the canvas's
   * color at each point in |listOfPoints|.
   */
  function assertListOfColorsEqual(
      expectedColor: string, listOfPoints: Point[]) {
    for (const point of listOfPoints) {
      assertEquals(expectedColor, getColor(point));
    }
  }

  /**
   * Helper class that, given a set of arguments for a |setProgress()| function
   * call, can be used to perform that function call and verify that the
   * resulting progress circle is drawn correctly.
   */
  class SetProgressTestCase {
    // Angles on HTML canvases start at 0 radians on the positive x-axis and
    // increase in the clockwise direction. Progress is drawn starting from the
    // top of the circle (3pi/2 rad). In the verification step, a test case
    // checks the colors drawn at angles 7pi/4, pi/4, 3pi/4, and 5pi/4 rad
    // (respectively 12.5%, 37.5%, 62.5%, and 87.5% progress completed).
    static progressCheckPoints: Array<[Point, number]> = [
      [{x: 185, y: 40} /**  7pi/4 rad */, 12.5],
      [{x: 185, y: 110} /**  pi/4 rad */, 37.5],
      [{x: 115, y: 110} /** 3pi/4 rad */, 62.5],
      [{x: 115, y: 40} /**  5pi/4 rad */, 87.5],
    ];

    // |setProgress()| arguments.
    private prevPercentComplete_: number;
    private currPercentComplete_: number;
    private isComplete_: boolean;

    // Points expected to be part of the circle's progress arc.
    private progressPoints_: Point[] = [];

    // Points expected to be part of the circle's background arc.
    private backgroundPoints_: Point[] = [];

    constructor(
        prevPercentComplete: number, currPercentComplete: number,
        isComplete: boolean) {
      this.prevPercentComplete_ = prevPercentComplete;
      this.currPercentComplete_ = currPercentComplete;
      this.isComplete_ = isComplete;

      const endPercent = isComplete ? 100 : Math.min(100, currPercentComplete);
      for (const [point, percent] of SetProgressTestCase.progressCheckPoints) {
        if (percent <= endPercent) {
          this.progressPoints_.push(point);
        } else {
          this.backgroundPoints_.push(point);
        }
      }
    }

    // Draws a progress circle on a fresh canvas using the test case's
    // |setProgress()| arguments and verifies that the circle's colors match the
    // expected colors.
    async run() {
      clearCanvas(canvas);
      assertListOfColorsEqual(
          canvasColor, this.progressPoints_.concat(this.backgroundPoints_));

      progressArc.setProgress(
          this.prevPercentComplete_, this.currPercentComplete_,
          this.isComplete_);
      await eventToPromise('fingerprint-progress-arc-drawn', progressArc);

      this.verifyProgressCircleColors();
    }

    // Helper function used to compare the current progress circle colors
    // against the test case's expected colors. Assumes that drawing has
    // already been done.
    verifyProgressCircleColors() {
      const isDarkMode = fakeMediaQueryList.matches;
      assertListOfColorsEqual(
          isDarkMode ? PROGRESS_CIRCLE_FILL_COLOR_DARK :
                       PROGRESS_CIRCLE_FILL_COLOR_LIGHT,
          this.progressPoints_);
      assertListOfColorsEqual(
          isDarkMode ? PROGRESS_CIRCLE_BACKGROUND_COLOR_DARK :
                       PROGRESS_CIRCLE_BACKGROUND_COLOR_LIGHT,
          this.backgroundPoints_);
    }
  }

  /**
   * Test setting progress while in light mode, switching to dark mode,
   * continuing to set progress, switching back to light mode, and setting
   * progress again.
   */
  test('TestSetProgress', async () => {
    const lightModeTestCases1 = [
      // Verify that no progress is drawn when no progress has been made.
      new SetProgressTestCase(0, 0, false),
      // Verify that progress is drawn starting from the top of the circle
      // (3pi/2 rad) and moving clockwise.
      new SetProgressTestCase(0, 40, false),
    ];

    const darkModeTestCases = [
      // Verify that the progress drawn includes progress made previously.
      new SetProgressTestCase(40, 80, false),
      // Verify that progress past 100% gets capped rather than wrapping around.
      new SetProgressTestCase(80, 160, false),
    ];

    // Verify that if the enrollment is complete, maximum progress is drawn.
    const lightModeTestCases2 = [new SetProgressTestCase(80, 80, true)];

    // Make some progress in light mode.
    for (const testCase of lightModeTestCases1) {
      await testCase.run();
    }

    // Switch to dark mode and verify that the progress circle is redrawn.
    fakeMediaQueryList.matches = true;
    lightModeTestCases1.slice(-1)[0]!.verifyProgressCircleColors();

    // Make some progress in dark mode.
    for (const testCase of darkModeTestCases) {
      await testCase.run();
    }

    // Switch back to light mode and verify that the progress circle is redrawn.
    fakeMediaQueryList.matches = false;
    darkModeTestCases.slice(-1)[0]!.verifyProgressCircleColors();

    // Finish making progress in light mode.
    for (const testCase of lightModeTestCases2) {
      await testCase.run();
    }
  });

  test('TestSwitchToDarkMode', function() {
    const fingerprintScanned = progressArc.$.fingerprintScanned;
    const scanningAnimation = progressArc.$.scanningAnimation;

    progressArc.setProgress(0, 1, true);
    assertEquals(FINGERPRINT_SCANNED_ICON_LIGHT, fingerprintScanned.icon);
    assertEquals(FINGERPRINT_CHECK_LIGHT_URL, scanningAnimation.animationUrl);

    fakeMediaQueryList.matches = true;
    assertEquals(FINGERPRINT_SCANNED_ICON_DARK, fingerprintScanned.icon);
    assertEquals(FINGERPRINT_CHECK_DARK_URL, scanningAnimation.animationUrl);
  });
});