chromium/chrome/browser/resources/chromeos/accessibility/chromevox/common/spannable_test.js

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

GEN_INCLUDE(['../testing/chromevox_e2e_test_base.js']);

UnserializableSpan = function() {};

StatelessSerializableSpan = function() {};

NonStatelessSerializableSpan = function(value) {
  this.value = value;
};

/**
 * @param {!Object} obj object containing the
 *     serializable representation.
 * @return {!Object} The Spannable.
 */
NonStatelessSerializableSpan.fromJson = function(obj) {
  return new NonStatelessSerializableSpan(obj.value / 2);
};

/**
 * @return {Object} the json serializable form.
 */
NonStatelessSerializableSpan.prototype.toJson = function() {
  return {value: this.value * 2};
};

/**
 * @param {Spannable} spannable
 * @param {*} annotation
 */
function assertSpanNotFound(spannable, annotation) {
  assertFalse(spannable.hasSpan(annotation));
  assertException(
      'Span ' + annotation + ' shouldn\'t be in spannable ' + spannable,
      function() {
        spannable.getSpanStart(annotation);
      },
      'Error');
  assertException(
      'Span ' + annotation + ' shouldn\'t be in spannable ' + spannable,
      function() {
        spannable.getSpanEnd(annotation);
      },
      'Error');
  assertException(
      'Span ' + annotation + ' shouldn\'t be in spannable ' + spannable,
      function() {
        spannable.getSpanLength(annotation);
      },
      'Error');
}

/**
 * Test fixture.
 */
ChromeVoxSpannableUnitTest = class extends ChromeVoxE2ETest {
  /** @override */
  setUp() {
    super.setUp();
  }

  async setUpDeferred() {
    await super.setUpDeferred();

    Spannable.registerStatelessSerializableSpan(
        StatelessSerializableSpan, 'StatelessSerializableSpan');

    Spannable.registerSerializableSpan(
        NonStatelessSerializableSpan, 'NonStatelessSerializableSpan',
        NonStatelessSerializableSpan.fromJson,
        NonStatelessSerializableSpan.prototype.toJson);
  }
};

AX_TEST_F('ChromeVoxSpannableUnitTest', 'ToStringUnannotated', function() {
  assertEquals('', new Spannable().toString());
  assertEquals('hello world', new Spannable('hello world').toString());
});

/** Tests that toString works correctly on annotated strings. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'ToStringAnnotated', function() {
  const spannable = new Spannable('Hello Google');
  spannable.setSpan('http://www.google.com/', 6, 12);
  assertEquals('Hello Google', spannable.toString());
});

/** Tests the length calculation. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'LengthProperty', function() {
  const spannable = new Spannable('Hello');
  spannable.setSpan({}, 0, 3);
  assertEquals(5, spannable.length);
  spannable.append(' world');
  assertEquals(11, spannable.length);
  spannable.append(new Spannable(' from Spannable'));
  assertEquals(26, spannable.length);
});

/** Tests that a span can be added and retrieved at the beginning. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SpanBeginning', function() {
  const annotation = {};
  const spannable = new Spannable('Hello world');
  spannable.setSpan(annotation, 0, 5);
  assertTrue(spannable.hasSpan(annotation));
  assertSame(annotation, spannable.getSpan(0));
  assertSame(annotation, spannable.getSpan(3));
  assertUndefined(spannable.getSpan(5));
  assertUndefined(spannable.getSpan(8));
});

/** Tests that a span can be added and retrieved at the beginning. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SpanEnd', function() {
  const annotation = {};
  const spannable = new Spannable('Hello world');
  spannable.setSpan(annotation, 6, 11);
  assertTrue(spannable.hasSpan(annotation));
  assertUndefined(spannable.getSpan(3));
  assertUndefined(spannable.getSpan(5));
  assertSame(annotation, spannable.getSpan(6));
  assertSame(annotation, spannable.getSpan(10));
});

/** Tests that a zero-length span is not retrieved. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SpanZeroLength', function() {
  const annotation = {};
  const spannable = new Spannable('Hello world');
  spannable.setSpan(annotation, 3, 3);
  assertTrue(spannable.hasSpan(annotation));
  assertUndefined(spannable.getSpan(2));
  assertUndefined(spannable.getSpan(3));
  assertUndefined(spannable.getSpan(4));
});

/** Tests that a removed span is not returned. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'RemoveSpan', function() {
  const annotation = {};
  const spannable = new Spannable('Hello world');
  spannable.setSpan(annotation, 0, 3);
  assertSame(annotation, spannable.getSpan(1));
  spannable.removeSpan(annotation);
  assertFalse(spannable.hasSpan(annotation));
  assertUndefined(spannable.getSpan(1));
});

/** Tests that adding a span in one place removes it from another. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SetSpanMoves', function() {
  const annotation = {};
  const spannable = new Spannable('Hello world');
  spannable.setSpan(annotation, 0, 3);
  assertSame(annotation, spannable.getSpan(1));
  assertUndefined(spannable.getSpan(4));
  spannable.setSpan(annotation, 3, 6);
  assertUndefined(spannable.getSpan(1));
  assertSame(annotation, spannable.getSpan(4));
});

/** Tests that setSpan objects to out-of-range arguments. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SetSpanRangeError', function() {
  const spannable = new Spannable('Hello world');

  // Start index out of range.
  assertException('expected range error', function() {
    spannable.setSpan({}, -1, 0);
  }, 'RangeError');

  // End index out of range.
  assertException('expected range error', function() {
    spannable.setSpan({}, 0, 12);
  }, 'RangeError');

  // End before start.
  assertException('expected range error', function() {
    spannable.setSpan({}, 1, 0);
  }, 'RangeError');
});

/**
 * Tests that multiple spans can be retrieved at one point.
 * The first one added which applies should be returned by getSpan.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'MultipleSpans', function() {
  const annotation1 = {number: 1};
  const annotation2 = {number: 2};
  assertNotSame(annotation1, annotation2);
  const spannable = new Spannable('Hello world');
  spannable.setSpan(annotation1, 1, 4);
  spannable.setSpan(annotation2, 2, 7);
  assertSame(annotation1, spannable.getSpan(1));
  assertDeepEquals([annotation1], spannable.getSpans(1));
  assertSame(annotation1, spannable.getSpan(3));
  assertDeepEquals([annotation1, annotation2], spannable.getSpans(3));
  assertSame(annotation2, spannable.getSpan(6));
  assertDeepEquals([annotation2], spannable.getSpans(6));
});

/** Tests that appending appends the strings. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'AppendToString', function() {
  const spannable = new Spannable('Google');
  assertEquals('Google', spannable.toString());
  spannable.append(' Chrome');
  assertEquals('Google Chrome', spannable.toString());
  spannable.append(new Spannable('Vox'));
  assertEquals('Google ChromeVox', spannable.toString());
});

/**
 * Tests that appending Spannables combines annotations.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'AppendAnnotations', function() {
  const annotation1 = {number: 1};
  const annotation2 = {number: 2};
  assertNotSame(annotation1, annotation2);
  const left = new Spannable('hello');
  left.setSpan(annotation1, 0, 3);
  const right = new Spannable(' world');
  right.setSpan(annotation2, 0, 3);
  left.append(right);
  assertSame(annotation1, left.getSpan(1));
  assertSame(annotation2, left.getSpan(6));
});

/**
 * Tests that a span's bounds can be retrieved.
 */
AX_TEST_F(
    'ChromeVoxSpannableUnitTest', 'GetSpanStartAndEndAndLength', function() {
      const annotation = {};
      const spannable = new Spannable('potato wedges');
      spannable.setSpan(annotation, 8, 12);
      assertEquals(8, spannable.getSpanStart(annotation));
      assertEquals(12, spannable.getSpanEnd(annotation));
      assertEquals(4, spannable.getSpanLength(annotation));
    });

/**
 * Tests that an absent span's bounds are reported correctly.
 */
AX_TEST_F(
    'ChromeVoxSpannableUnitTest', 'GetSpanStartAndEndAndLengthAbsent',
    function() {
      const annotation = {};
      const spannable = new Spannable('potato wedges');
      assertSpanNotFound(spannable, annotation);
    });

/**
 * Test that a zero length span can still be found.
 */
AX_TEST_F(
    'ChromeVoxSpannableUnitTest', 'GetSpanStartAndEndAndLengthZeroLength',
    function() {
      const annotation = {};
      const spannable = new Spannable('potato wedges');
      spannable.setSpan(annotation, 8, 8);
      assertEquals(8, spannable.getSpanStart(annotation));
      assertEquals(8, spannable.getSpanEnd(annotation));
      assertEquals(0, spannable.getSpanLength(annotation));
    });

/**
 * Tests that == (but not ===) objects are treated distinctly when getting
 * span bounds.
 */
AX_TEST_F(
    'ChromeVoxSpannableUnitTest', 'GetSpanStartAndEndEquality', function() {
      // Note that 0 == '' and '' == 0 in JavaScript.
      const spannable = new Spannable('wat');
      spannable.setSpan(0, 0, 0);
      spannable.setSpan('', 1, 3);
      assertEquals(0, spannable.getSpanStart(0));
      assertEquals(0, spannable.getSpanEnd(0));
      assertEquals(1, spannable.getSpanStart(''));
      assertEquals(3, spannable.getSpanEnd(''));
    });

/**
 * Tests that substrings have the correct character sequence.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'Substring', function() {
  const assertSubstringResult = function(expected, initial, start, opt_end) {
    const spannable = new Spannable(initial);
    const substring = spannable.substring(start, opt_end);
    assertEquals(expected, substring.toString());
  };
  assertSubstringResult('Page', 'Google PageRank', 7, 11);
  assertSubstringResult('Goog', 'Google PageRank', 0, 4);
  assertSubstringResult('Rank', 'Google PageRank', 11, 15);
  assertSubstringResult('Rank', 'Google PageRank', 11);
});

/**
 * Tests that substring arguments are validated properly.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SubstringRangeError', function() {
  const assertRangeError = function(initial, start, opt_end) {
    const spannable = new Spannable(initial);
    assertException('expected range error', function() {
      spannable.substring(start, opt_end);
    }, 'RangeError');
  };
  assertRangeError('Google PageRank', -1, 5);
  assertRangeError('Google PageRank', 0, 99);
  assertRangeError('Google PageRank', 5, 2);
});

/**
 * Tests that spans in the substring range are preserved.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SubstringSpansIncluded', function() {
  const assertSpanIncluded = function(
      expectedSpanStart, expectedSpanEnd, initial, initialSpanStart,
      initialSpanEnd, start, opt_end) {
    const annotation = {};
    const spannable = new Spannable(initial);
    spannable.setSpan(annotation, initialSpanStart, initialSpanEnd);
    const substring = spannable.substring(start, opt_end);
    assertTrue(substring.hasSpan(annotation));
    assertEquals(expectedSpanStart, substring.getSpanStart(annotation));
    assertEquals(expectedSpanEnd, substring.getSpanEnd(annotation));
  };
  assertSpanIncluded(1, 5, 'potato wedges', 8, 12, 7);
  assertSpanIncluded(1, 5, 'potato wedges', 8, 12, 7, 13);
  assertSpanIncluded(1, 5, 'potato wedges', 8, 12, 7, 12);
  assertSpanIncluded(0, 4, 'potato wedges', 8, 12, 8, 12);
  assertSpanIncluded(0, 3, 'potato wedges', 0, 3, 0);
  assertSpanIncluded(0, 3, 'potato wedges', 0, 3, 0, 3);
  assertSpanIncluded(0, 3, 'potato wedges', 0, 3, 0, 6);
  assertSpanIncluded(0, 5, 'potato wedges', 8, 13, 8);
  assertSpanIncluded(0, 5, 'potato wedges', 8, 13, 8, 13);
  assertSpanIncluded(1, 6, 'potato wedges', 8, 13, 7, 13);

  // Note: we should keep zero-length spans, even at the edges of the range.
  assertSpanIncluded(0, 0, 'potato wedges', 0, 0, 0, 0);
  assertSpanIncluded(0, 0, 'potato wedges', 0, 0, 0, 6);
  assertSpanIncluded(1, 1, 'potato wedges', 8, 8, 7, 13);
  assertSpanIncluded(6, 6, 'potato wedges', 6, 6, 0, 6);
});

/**
 * Tests that spans outside the range are omitted.
 * It's fine to keep zero-length spans at the ends, though.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SubstringSpansExcluded', function() {
  const assertSpanExcluded = function(
      initial, spanStart, spanEnd, start, opt_end) {
    const annotation = {};
    const spannable = new Spannable(initial);
    spannable.setSpan(annotation, spanStart, spanEnd);
    const substring = spannable.substring(start, opt_end);
    assertSpanNotFound(substring, annotation);
  };
  assertSpanExcluded('potato wedges', 8, 12, 0, 6);
  assertSpanExcluded('potato wedges', 7, 12, 0, 6);
  assertSpanExcluded('potato wedges', 0, 6, 8);
  assertSpanExcluded('potato wedges', 6, 6, 8);
});

/**
 * Tests that spans which cross the boundary are clipped.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'SubstringSpansClipped', function() {
  const assertSpanIncluded = function(
      expectedSpanStart, expectedSpanEnd, initial, initialSpanStart,
      initialSpanEnd, start, opt_end) {
    const annotation = {};
    const spannable = new Spannable(initial);
    spannable.setSpan(annotation, initialSpanStart, initialSpanEnd);
    const substring = spannable.substring(start, opt_end);
    assertEquals(expectedSpanStart, substring.getSpanStart(annotation));
    assertEquals(expectedSpanEnd, substring.getSpanEnd(annotation));
  };
  assertSpanIncluded(0, 4, 'potato wedges', 7, 13, 8, 12);
  assertSpanIncluded(0, 0, 'potato wedges', 0, 6, 0, 0);
  assertSpanIncluded(0, 0, 'potato wedges', 0, 6, 6, 6);

  // The first of the above should produce "edge".
  assertEquals(
      'edge', new Spannable('potato wedges').substring(8, 12).toString());
});

/**
 * Tests that whitespace is trimmed.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'Trim', function() {
  const assertTrimResult = function(expected, initial) {
    assertEquals(expected, new Spannable(initial).trim().toString());
  };
  assertTrimResult('John F. Kennedy', 'John F. Kennedy');
  assertTrimResult('John F. Kennedy', '  John F. Kennedy');
  assertTrimResult('John F. Kennedy', 'John F. Kennedy     ');
  assertTrimResult('John F. Kennedy', '   \r\t   \nJohn F. Kennedy\n\n \n');
  assertTrimResult('', '');
  assertTrimResult('', '     \t\t    \n\r');
});

/**
 * Tests that trim keeps, drops and clips spans.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'TrimSpans', function() {
  const spannable = new Spannable(' \t Kennedy\n');
  spannable.setSpan('tab', 1, 2);
  spannable.setSpan('jfk', 3, 10);
  spannable.setSpan('jfk-newline', 3, 11);
  const trimmed = spannable.trim();
  assertSpanNotFound(trimmed, 'tab');
  assertEquals(0, trimmed.getSpanStart('jfk'));
  assertEquals(7, trimmed.getSpanEnd('jfk'));
  assertEquals(0, trimmed.getSpanStart('jfk-newline'));
  assertEquals(7, trimmed.getSpanEnd('jfk-newline'));
});

/**
 * Tests that when a string is all whitespace, we trim off the *end*.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'TrimAllWhitespace', function() {
  const spannable = new Spannable('    ');
  spannable.setSpan('cursor 1', 0, 0);
  spannable.setSpan('cursor 2', 2, 2);
  const trimmed = spannable.trim();
  assertEquals(0, trimmed.getSpanStart('cursor 1'));
  assertEquals(0, trimmed.getSpanEnd('cursor 1'));
  assertSpanNotFound(trimmed, 'cursor 2');
});

/**
 * Tests finding a span which is an instance of a given class.
 */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'GetSpanInstanceOf', function() {
  function ExampleConstructorBase() {}
  function ExampleConstructor1() {}
  function ExampleConstructor2() {}
  function ExampleConstructor3() {}
  ExampleConstructor1.prototype = new ExampleConstructorBase();
  ExampleConstructor2.prototype = new ExampleConstructorBase();
  ExampleConstructor3.prototype = new ExampleConstructorBase();
  const ex1 = new ExampleConstructor1();
  const ex2 = new ExampleConstructor2();
  const spannable = new Spannable('Hello world');
  spannable.setSpan(ex1, 0, 0);
  spannable.setSpan(ex2, 1, 1);
  assertEquals(ex1, spannable.getSpanInstanceOf(ExampleConstructor1));
  assertEquals(ex2, spannable.getSpanInstanceOf(ExampleConstructor2));
  assertUndefined(spannable.getSpanInstanceOf(ExampleConstructor3));
  assertEquals(ex1, spannable.getSpanInstanceOf(ExampleConstructorBase));
});

/** Tests trimming only left or right. */
AX_TEST_F('ChromeVoxSpannableUnitTest', 'TrimLeftOrRight', function() {
  const spannable = new Spannable('    ');
  spannable.setSpan('cursor 1', 0, 0);
  spannable.setSpan('cursor 2', 2, 2);
  const trimmed = spannable.trimLeft();
  assertEquals(0, trimmed.getSpanStart('cursor 1'));
  assertEquals(0, trimmed.getSpanEnd('cursor 1'));
  assertSpanNotFound(trimmed, 'cursor 2');

  const spannable2 = new Spannable('0  ');
  spannable2.setSpan('cursor 1', 0, 0);
  spannable2.setSpan('cursor 2', 2, 2);
  let trimmed2 = spannable2.trimLeft();
  assertEquals(0, trimmed2.getSpanStart('cursor 1'));
  assertEquals(0, trimmed2.getSpanEnd('cursor 1'));
  assertEquals(2, trimmed2.getSpanStart('cursor 2'));
  assertEquals(2, trimmed2.getSpanEnd('cursor 2'));
  trimmed2 = trimmed2.trimRight();
  assertEquals(0, trimmed2.getSpanStart('cursor 1'));
  assertEquals(0, trimmed2.getSpanEnd('cursor 1'));
  assertSpanNotFound(trimmed2, 'cursor 2');

  const spannable3 = new Spannable('  0');
  spannable3.setSpan('cursor 1', 0, 0);
  spannable3.setSpan('cursor 2', 2, 2);
  let trimmed3 = spannable3.trimRight();
  assertEquals(0, trimmed3.getSpanStart('cursor 1'));
  assertEquals(0, trimmed3.getSpanEnd('cursor 1'));
  assertEquals(2, trimmed3.getSpanStart('cursor 2'));
  assertEquals(2, trimmed3.getSpanEnd('cursor 2'));
  trimmed3 = trimmed3.trimLeft();
  assertSpanNotFound(trimmed3, 'cursor 1');
  assertEquals(0, trimmed3.getSpanStart('cursor 2'));
  assertEquals(0, trimmed3.getSpanEnd('cursor 2'));
});

AX_TEST_F('ChromeVoxSpannableUnitTest', 'Serialize', function() {
  const fresh = new Spannable('text');
  const freshStatelessSerializable = new StatelessSerializableSpan();
  const freshNonStatelessSerializable = new NonStatelessSerializableSpan(14);
  fresh.setSpan(new UnserializableSpan(), 0, 1);
  fresh.setSpan(freshStatelessSerializable, 0, 2);
  fresh.setSpan(freshNonStatelessSerializable, 3, 4);
  const thawn = Spannable.fromJson(fresh.toJson());
  const thawnStatelessSerializable =
      thawn.getSpanInstanceOf(StatelessSerializableSpan);
  const thawnNonStatelessSerializable =
      thawn.getSpanInstanceOf(NonStatelessSerializableSpan);
  assertEquals('text', thawn.toString());
  assertUndefined(thawn.getSpanInstanceOf(UnserializableSpan));
  assertDeepEquals(
      fresh.getSpanStart(freshStatelessSerializable),
      thawn.getSpanStart(thawnStatelessSerializable));
  assertDeepEquals(
      fresh.getSpanEnd(freshStatelessSerializable),
      thawn.getSpanEnd(thawnStatelessSerializable));
  assertDeepEquals(
      freshNonStatelessSerializable, thawnNonStatelessSerializable);
});

AX_TEST_F('ChromeVoxSpannableUnitTest', 'GetSpanIntervals', function() {
  function Foo() {}
  function Bar() {}
  const ms = new MultiSpannable('f12b45f78b01');
  const foo = new Foo();
  const bar = new Bar();
  ms.setSpan(foo, 0, 3);
  ms.setSpan(bar, 3, 6);
  ms.setSpan(foo, 6, 9);
  ms.setSpan(bar, 9, 12);
  assertEquals(2, ms.getSpansInstanceOf(Foo).length);
  assertEquals(2, ms.getSpansInstanceOf(Bar).length);

  const fooIntervals = ms.getSpanIntervals(foo);
  assertEquals(2, fooIntervals.length);
  assertEquals(0, fooIntervals[0].start);
  assertEquals(3, fooIntervals[0].end);
  assertEquals(6, fooIntervals[1].start);
  assertEquals(9, fooIntervals[1].end);

  const barIntervals = ms.getSpanIntervals(bar);
  assertEquals(2, barIntervals.length);
  assertEquals(3, barIntervals[0].start);
  assertEquals(6, barIntervals[0].end);
  assertEquals(9, barIntervals[1].start);
  assertEquals(12, barIntervals[1].end);
});