aboutsummaryrefslogtreecommitdiff
path: root/contexts/data/lib/closure-library/closure/goog/editor/plugins/undoredo_test.html
blob: 8afea69a16b05a7fcf7b7d216478badeeb367756 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
goog.editor.plugins<!DOCTYPE html>
<!--
  All Rights Reserved.

  @author ajp@google.com (Andy Perelson)
-->
<html>
<!--
Copyright 2008 The Closure Library Authors. All Rights Reserved.

Use of this source code is governed by the Apache License, Version 2.0.
See the COPYING file for details.
-->
<head>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Trogedit Unit Tests - goog.editor.plugins.UndoRedo</title>
<script src="../../base.js"></script>
<script>
  goog.require('goog.array');
  goog.require('goog.dom');
  goog.require('goog.editor.Field');
  goog.require('goog.editor.Field.EventType');
  goog.require('goog.events');
  goog.require('goog.events.BrowserEvent');
  goog.require('goog.testing.MockClock');
  goog.require('goog.testing.PropertyReplacer');
  goog.require('goog.testing.StrictMock');
  goog.require('goog.testing.jsunit');
  goog.require('goog.editor.plugins.LoremIpsum');
  goog.require('goog.editor.plugins.UndoRedo');

</script>
</head>
<body>

<div id="testField"></div>

<script>

var mockEditableField;
var editableField;
var fieldHashCode;
var undoPlugin;
var state
var mockState;
var commands;
var clock;
var stubs = new goog.testing.PropertyReplacer();


function setUp() {
  mockEditableField = new goog.testing.StrictMock(goog.editor.Field);

  // Update the arg list verifier for dispatchCommandValueChange to
  // correctly compare arguments that are arrays (or other complex objects).
  mockEditableField.$registerArgumentListVerifier('dispatchEvent',
   function(expected, args) {
       return goog.array.equals(expected, args,
           function(a, b) { assertObjectEquals(a, b); return true; });
    });
  mockEditableField.getHashCode = function() {
    return 'fieldId';
  }

  undoPlugin = new goog.editor.plugins.UndoRedo();
  undoPlugin.registerFieldObject(mockEditableField);
  mockState = new goog.testing.StrictMock(
      goog.editor.plugins.UndoRedo.UndoState_);
  mockState.fieldHashCode = 'fieldId';
  mockState.isAsynchronous = function() {
    return false;
  };
  // Don't bother mocking the inherited event target pieces of the state.
  // If we don't do this, then mocked asynchronous undos are a lot harder and
  // that behavior is tested as part of the UndoRedoManager tests.
  mockState.addEventListener = goog.nullFunction;

  commands = [
    goog.editor.plugins.UndoRedo.COMMAND.REDO,
    goog.editor.plugins.UndoRedo.COMMAND.UNDO
  ];
  state = new goog.editor.plugins.UndoRedo.UndoState_('1', '', null,
      goog.nullFunction);

  clock = new goog.testing.MockClock(true);

  editableField = new goog.editor.Field('testField');
  fieldHashCode = editableField.getHashCode();
}


function tearDown() {
  // Reset field so any attempted access during disposes don't cause errors.
  mockEditableField.$reset();
  clock.dispose();
  undoPlugin.dispose();

  // NOTE(nicksantos): I think IE is blowing up on this call because
  // it is lame. It manifests its lameness by throwing an exception.
  // Kudos to XT for helping me to figure this out.
  try {
    goog.events.removeAll();
  } catch (e) {}

  if (!editableField.isUneditable()) {
    editableField.makeUneditable();
  }
  editableField.dispose();
  goog.dom.getElement('testField').innerHTML = '';
  stubs.reset();
}


// undo-redo plugin tests


function testQueryCommandValue() {
  assertFalse('Must return false for empty undo stack.',
      undoPlugin.queryCommandValue(goog.editor.plugins.UndoRedo.COMMAND.UNDO));

  assertFalse('Must return false for empty redo stack.',
      undoPlugin.queryCommandValue(goog.editor.plugins.UndoRedo.COMMAND.REDO));

  undoPlugin.undoManager_.addState(mockState);

  assertTrue('Must return true for a non-empty undo stack.',
      undoPlugin.queryCommandValue(goog.editor.plugins.UndoRedo.COMMAND.UNDO));
}


function testExecCommand() {
  undoPlugin.undoManager_.addState(mockState);

  mockState.undo();
  mockState.$replay();

  undoPlugin.execCommand(goog.editor.plugins.UndoRedo.COMMAND.UNDO);
  // Second undo should do nothing since only one item on stack.
  undoPlugin.execCommand(goog.editor.plugins.UndoRedo.COMMAND.UNDO);
  mockState.$verify();

  mockState.$reset();
  mockState.redo();
  mockState.$replay();
  undoPlugin.execCommand(goog.editor.plugins.UndoRedo.COMMAND.REDO);
  // Second redo should do nothing since only one item on stack.
  undoPlugin.execCommand(goog.editor.plugins.UndoRedo.COMMAND.REDO);
  mockState.$verify();
}

function testHandleKeyboardShortcut_TrogStates() {
  undoPlugin.undoManager_.addState(mockState);
  undoPlugin.undoManager_.addState(state);
  undoPlugin.undoManager_.undo();
  mockEditableField.$reset();

  var stubUndoEvent = {ctrlKey: true, altKey: false, shiftKey: false};
  var stubRedoEvent = {ctrlKey: true, altKey: false, shiftKey: true};
  var stubRedoEvent2 = {ctrlKey: true, altKey: false, shiftKey: false};
  var result;

  // Test handling Trogedit undos. Should always call EditableField's
  // execCommand. Since EditableField is mocked, this will not result in a call
  // to the mockState's undo and redo methods.
  mockEditableField.execCommand(goog.editor.plugins.UndoRedo.COMMAND.UNDO);
  mockEditableField.$replay();
  result = undoPlugin.handleKeyboardShortcut(stubUndoEvent, 'z', true);
  assertTrue('Plugin must return true when it handles shortcut.', result);
  mockEditableField.$verify();
  mockEditableField.$reset();

  mockEditableField.execCommand(goog.editor.plugins.UndoRedo.COMMAND.REDO);
  mockEditableField.$replay();
  result = undoPlugin.handleKeyboardShortcut(stubRedoEvent, 'z', true);
  assertTrue('Plugin must return true when it handles shortcut.', result);
  mockEditableField.$verify();
  mockEditableField.$reset();

  mockEditableField.execCommand(goog.editor.plugins.UndoRedo.COMMAND.REDO);
  mockEditableField.$replay();
  result = undoPlugin.handleKeyboardShortcut(stubRedoEvent2, 'y', true);
  assertTrue('Plugin must return true when it handles shortcut.', result);
  mockEditableField.$verify();
  mockEditableField.$reset();

  mockEditableField.$replay();
  result = undoPlugin.handleKeyboardShortcut(stubRedoEvent2, 'y', false);
  assertFalse('Plugin must return false when modifier is not pressed.', result);
  mockEditableField.$verify();
  mockEditableField.$reset();

  mockEditableField.$replay();
  result = undoPlugin.handleKeyboardShortcut(stubUndoEvent, 'f', true);
  assertFalse('Plugin must return false when it doesn\'t handle shortcut.',
      result);
  mockEditableField.$verify();
}

function testHandleKeyboardShortcut_NotTrogStates() {
  var stubUndoEvent = {ctrlKey: true, altKey: false, shiftKey: false};

  // Trogedit undo states all have a fieldHashCode, nulling that out makes this
  // state be treated as a non-Trogedit undo-redo state.
  state.fieldHashCode = null;
  undoPlugin.undoManager_.addState(state);
  mockEditableField.$reset();

  // Non-trog state shouldn't go through EditableField.execCommand, however,
  // we still exect command value change dispatch since undo-redo plugin
  // redispatches those anytime manager's state changes.
  mockEditableField.dispatchEvent({
    type: goog.editor.Field.EventType.COMMAND_VALUE_CHANGE,
    commands: commands});
  mockEditableField.$replay();
  var result = undoPlugin.handleKeyboardShortcut(stubUndoEvent, 'z', true);
  assertTrue('Plugin must return true when it handles shortcut.' , result);
  mockEditableField.$verify();
}

function testEnable() {
  assertFalse('Plugin must start disabled.',
      undoPlugin.isEnabled(editableField));

  editableField.makeEditable(editableField);
  editableField.setHtml(false, '<div>a</div>');
  undoPlugin.enable(editableField);

  assertTrue(undoPlugin.isEnabled(editableField));
  assertNotNull('Must have an event handler for enabled field.',
      undoPlugin.eventHandlers_[fieldHashCode]);

  var currentState = undoPlugin.currentStates_[fieldHashCode];
  assertNotNull('Enabled plugin must have a current state.', currentState);
  assertEquals('After enable, undo content must match the field content.',
      editableField.getElement().innerHTML, currentState.undoContent_);

  assertTrue('After enable, undo cursorPosition must match the field cursor' +
      'position.', cursorPositionsEqual(getCurrentCursorPosition(),
          currentState.undoCursorPosition_));

  assertUndefined('Current state must never have redo content.',
      currentState.redoContent_);
  assertUndefined('Current state must never have redo cursor position.',
      currentState.redoCursorPosition_);
};

function testDisable() {
  editableField.makeEditable(editableField);
  undoPlugin.enable(editableField);
  assertTrue('Plugin must be enabled so we can test disabling.',
      undoPlugin.isEnabled(editableField));

  var delayedChangeFired = false;
  goog.events.listenOnce(editableField,
      goog.editor.Field.EventType.DELAYEDCHANGE,
      function(e) {
        delayedChangeFired = true;
      })
  editableField.setHtml(false, 'foo');

  undoPlugin.disable(editableField);
  assertTrue('disable must fire pending delayed changes.', delayedChangeFired);
  assertEquals('disable must add undo state from pending change.',
      1, undoPlugin.undoManager_.undoStack_.length);

  assertFalse(undoPlugin.isEnabled(editableField));
  assertUndefined('Disabled plugin must not have current state.',
      undoPlugin.eventHandlers_[fieldHashCode]);
  assertUndefined('Disabled plugin must not have event handlers.',
      undoPlugin.eventHandlers_[fieldHashCode]);
}

function testUpdateCurrentState_() {
  editableField.registerPlugin(new goog.editor.plugins.LoremIpsum('LOREM'));
  editableField.makeEditable(editableField);
  editableField.getPluginByClassId('LoremIpsum').usingLorem_ = true;
  undoPlugin.updateCurrentState_(editableField);
  var currentState = undoPlugin.currentStates_[fieldHashCode];
  assertNotUndefined('Must create empty states for field using lorem ipsum.',
      undoPlugin.currentStates_[fieldHashCode]);
  assertEquals('', currentState.undoContent_);
  assertNull(currentState.undoCursorPosition_);

  editableField.getPluginByClassId('LoremIpsum').usingLorem_ = false;

  // Pretend foo is the default contents to test '' == default contents
  // behavior.
  editableField.getInjectableContents = function(contents, styles) {
    return contents == '' ? 'foo' : contents;
  }
  editableField.setHtml(false, 'foo');
  undoPlugin.updateCurrentState_(editableField);
  assertEquals(currentState, undoPlugin.currentStates_[fieldHashCode]);

  // NOTE(user): Because there is already a current state, this setHtml will add
  // a state to the undo stack.
  editableField.setHtml(false, '<div>a</div>');
  // Select some text so we have a valid selection that gets saved in the
  // UndoState.
  goog.dom.browserrange.createRangeFromNodeContents(
      editableField.getElement()).select();

  undoPlugin.updateCurrentState_(editableField);
  currentState = undoPlugin.currentStates_[fieldHashCode];
  assertNotNull('Must create state for field not using lorem ipsum',
      currentState);
  assertEquals(fieldHashCode, currentState.fieldHashCode);
  var content = editableField.getElement().innerHTML;
  var cursorPosition = getCurrentCursorPosition();
  assertEquals(content, currentState.undoContent_);
  assertTrue(cursorPositionsEqual(
      cursorPosition, currentState.undoCursorPosition_));
  assertUndefined(currentState.redoContent_);
  assertUndefined(currentState.redoCursorPosition_);

  undoPlugin.updateCurrentState_(editableField);
  assertEquals('Updating state when state has not changed must not add undo ' +
      'state to stack.', 1, undoPlugin.undoManager_.undoStack_.length);
  assertEquals('Updating state when state has not changed must not create ' +
      'a new state.', currentState, undoPlugin.currentStates_[fieldHashCode])
  assertUndefined('Updating state when state has not changed must not add ' +
      'redo content.', currentState.redoContent_);
  assertUndefined('Updating state when state has not changed must not add ' +
      'redo cursor position.', currentState.redoCursorPosition_);

  editableField.setHtml(false, '<div>b</div>');
  undoPlugin.updateCurrentState_(editableField);
  currentState = undoPlugin.currentStates_[fieldHashCode];
  assertNotNull('Must create state for field not using lorem ipsum',
      currentState);
  assertEquals(fieldHashCode, currentState.fieldHashCode);
  var newContent = editableField.getElement().innerHTML;
  var newCursorPosition = getCurrentCursorPosition();
  assertEquals(newContent, currentState.undoContent_);
  assertTrue(cursorPositionsEqual(
      newCursorPosition, currentState.undoCursorPosition_));
  assertUndefined(currentState.redoContent_);
  assertUndefined(currentState.redoCursorPosition_);

  var undoState = goog.array.peek(undoPlugin.undoManager_.undoStack_);
  assertNotNull('Must create state for field not using lorem ipsum',
      currentState);
  assertEquals(fieldHashCode, currentState.fieldHashCode);
  assertEquals(content, undoState.undoContent_);
  assertTrue(cursorPositionsEqual(
      cursorPosition, undoState.undoCursorPosition_));
  assertEquals(newContent, undoState.redoContent_);
  assertTrue(cursorPositionsEqual(
      newCursorPosition, undoState.redoCursorPosition_));
}

/**
 * Tests that change events get restarted properly after an undo call despite
 * an exception being thrown in the process (see bug/1991234).
 */
function testUndoRestartsChangeEvents() {
  undoPlugin.registerFieldObject(editableField);
  editableField.makeEditable(editableField);
  editableField.setHtml(false, '<div>a</div>');
  clock.tick(1000);
  undoPlugin.enable(editableField);

  // Change content so we can undo it.
  editableField.setHtml(false, '<div>b</div>');
  clock.tick(1000);

  var currentState = undoPlugin.currentStates_[fieldHashCode];
  stubs.set(editableField, 'setCursorPosition',
      goog.functions.error('Faking exception during setCursorPosition()'));
  try {
    currentState.undo();
  } catch (e) {
    fail('Exception should not have been thrown during undo()');
  }
  assertEquals('Change events should be on', 0,
      editableField.stoppedEvents_[goog.editor.Field.EventType.CHANGE]);
  assertEquals('Delayed change events should be on', 0,
      editableField.stoppedEvents_[goog.editor.Field.EventType.DELAYEDCHANGE]);
}

function testRefreshCurrentState() {
  editableField.makeEditable(editableField);
  editableField.setHtml(false, '<div>a</div>');
  clock.tick(1000);
  undoPlugin.enable(editableField);

  // Create current state and verify it.
  var currentState = undoPlugin.currentStates_[fieldHashCode];
  assertEquals(fieldHashCode, currentState.fieldHashCode);
  var content = editableField.getElement().innerHTML;
  var cursorPosition = getCurrentCursorPosition();
  assertEquals(content, currentState.undoContent_);
  assertTrue(cursorPositionsEqual(
      cursorPosition, currentState.undoCursorPosition_));

  // Update the field w/o dispatching delayed change, and verify that the
  // current state hasn't changed to reflect new values.
  editableField.setHtml(false, '<div>b</div>', true);
  clock.tick(1000);
  currentState = undoPlugin.currentStates_[fieldHashCode];
  assertEquals('Content must match old state.',
      content, currentState.undoContent_);
  assertTrue('Cursor position must match old state.',
      cursorPositionsEqual(
      cursorPosition, currentState.undoCursorPosition_));

  undoPlugin.refreshCurrentState(editableField);
  assertFalse('Refresh must not cause states to go on the undo-redo stack.',
      undoPlugin.undoManager_.hasUndoState());
  currentState = undoPlugin.currentStates_[fieldHashCode];
  content = editableField.getElement().innerHTML;
  cursorPosition = getCurrentCursorPosition();
  assertEquals('Content must match current field state.',
      content, currentState.undoContent_);
  assertTrue('Cursor position must match current field state.',
      cursorPositionsEqual(cursorPosition, currentState.undoCursorPosition_));

  undoPlugin.disable(editableField);
  assertUndefined(undoPlugin.currentStates_[fieldHashCode]);
  undoPlugin.refreshCurrentState(editableField);
  assertUndefined('Must not refresh current state of fields that do not have ' +
      'undo-redo enabled.', undoPlugin.currentStates_[fieldHashCode]);
};

/**
 * Returns the CursorPosition for the selection currently in the Field.
 * @return {goog.editor.plugins.UndoRedo.CursorPosition_}
 */
function getCurrentCursorPosition() {
  return undoPlugin.getCursorPosition_(editableField);
}

/**
 * Compares two cursor positions and returns whether they are equal.
 * @param {goog.editor.plugins.UndoRedo.CursorPosition_} a
 *     A cursor position.
 * @param {goog.editor.plugins.UndoRedo.CursorPosition_} b
 *     A cursor position.
 * @return {boolean} Whether the positions are equal.
 */
function cursorPositionsEqual(a, b) {
  if (!a && !b) {
    return true;
  } else if (a && b){
    return a.toString() == b.toString();
  }
  // Only one cursor position is an object, can't be equal.
  return false;
}
// Undo state tests


function testSetUndoState() {
  state.setUndoState('content', 'position');
  assertEquals('Undo content incorrectly set', 'content', state.undoContent_);
  assertEquals('Undo cursor position incorrectly set', 'position',
      state.undoCursorPosition_);
}

function testSetRedoState() {
  state.setRedoState('content', 'position');
  assertEquals('Redo content incorrectly set', 'content', state.redoContent_);
  assertEquals('Redo cursor position incorrectly set', 'position',
      state.redoCursorPosition_);
}

function testEquals() {
  assertTrue('A state must equal itself', state.equals(state));

  var state2 = new goog.editor.plugins.UndoRedo.UndoState_('1', '', null);
  assertTrue('A state must equal a state with the same hash code and content.',
      state.equals(state2));

  state2 = new goog.editor.plugins.UndoRedo.UndoState_('1', '', 'foo');
  assertTrue('States with different cursor positions must be equal',
      state.equals(state2));

  state2.setRedoState('bar', null);
  assertFalse('States with different redo content must not be equal',
      state.equals(state2));

  state2 = new goog.editor.plugins.UndoRedo.UndoState_('3', '', null);
  assertFalse('States with different field hash codes must not be equal',
      state.equals(state2));

  state2 = new goog.editor.plugins.UndoRedo.UndoState_('1', 'baz', null);
  assertFalse('States with different undoContent must not be equal',
      state.equals(state2));
}

/** @bug 1359214 */
function testClearUndoHistory() {
  var undoRedoPlugin = new goog.editor.plugins.UndoRedo();
  editableField.registerPlugin(undoRedoPlugin);
  editableField.makeEditable(editableField);

  editableField.dispatchChange();
  clock.tick(10000);

  editableField.getElement().innerHTML = 'y';
  editableField.dispatchChange();
  assertFalse(undoRedoPlugin.undoManager_.hasUndoState());

  clock.tick(10000);
  assertTrue(undoRedoPlugin.undoManager_.hasUndoState());

  editableField.getElement().innerHTML = 'z';
  editableField.dispatchChange();

  var numCalls = 0;
  goog.events.listen(editableField, goog.editor.Field.EventType.DELAYEDCHANGE,
      function() {
        numCalls++;
      });
  undoRedoPlugin.clearHistory();
  // 1 call from stopChangeEvents(). 0 calls from startChangeEvents().
  assertEquals('clearHistory must not cause delayed change when none pending',
      1, numCalls);

  clock.tick(10000);
  assertFalse(undoRedoPlugin.undoManager_.hasUndoState());
}
</script>
</body>
</html>