summaryrefslogtreecommitdiffstats
path: root/comm/mail/base/test/browser/browser_orderableTreeListbox.js
blob: 54634cbd8841be573f655c39cb65539abe3387b7 (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
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, you can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint mozilla/no-arbitrary-setTimeout: off */

let dragService = Cc["@mozilla.org/widget/dragservice;1"].getService(
  Ci.nsIDragService
);

let WAIT_TIME = 0;

let tabmail = document.getElementById("tabmail");
registerCleanupFunction(() => {
  tabmail.closeOtherTabs(tabmail.tabInfo[0]);
  Services.prefs.clearUserPref("ui.prefersReducedMotion");
  Services.prefs.clearUserPref("mailnews.default_view_flags");
});

async function withMotion(subtest) {
  Services.prefs.setIntPref("ui.prefersReducedMotion", 0);
  WAIT_TIME = 300;
  await TestUtils.waitForCondition(
    () => !matchMedia("(prefers-reduced-motion)").matches
  );
  return subtest();
}

async function withoutMotion(subtest) {
  Services.prefs.setIntPref("ui.prefersReducedMotion", 1);
  WAIT_TIME = 0;
  await TestUtils.waitForCondition(
    () => matchMedia("(prefers-reduced-motion)").matches
  );
  await subtest();
}

let win, doc, list, dataTransfer;

async function orderWithKeys(key) {
  selectHandler.reset();
  orderedHandler.reset();

  list.addEventListener("select", selectHandler);
  list.addEventListener("ordered", orderedHandler);
  EventUtils.synthesizeKey(key, { altKey: true }, win);
  await new Promise(resolve => win.setTimeout(resolve, WAIT_TIME));
  list.removeEventListener("select", selectHandler);
  list.removeEventListener("ordered", orderedHandler);

  await checkNoTransformations();
}

async function startDrag(index) {
  let listRect = list.getBoundingClientRect();
  let clientY = listRect.top + index * 32 + 4;

  dragService.startDragSessionForTests(Ci.nsIDragService.DRAGDROP_ACTION_NONE);
  [, dataTransfer] = EventUtils.synthesizeDragOver(
    list.rows[index],
    list,
    null,
    null,
    win,
    win,
    {
      clientY,
      _domDispatchOnly: true,
    }
  );

  await new Promise(resolve => setTimeout(resolve, WAIT_TIME));
}

async function continueDrag(index) {
  let listRect = list.getBoundingClientRect();
  let destClientX = listRect.left + listRect.width / 2;
  let destClientY = listRect.top + index * 32 + 4;
  let destScreenX = win.mozInnerScreenX + destClientX;
  let destScreenY = win.mozInnerScreenY + destClientY;

  let result = EventUtils.sendDragEvent(
    {
      type: "dragover",
      screenX: destScreenX,
      screenY: destScreenY,
      clientX: destClientX,
      clientY: destClientY,
      dataTransfer,
      _domDispatchOnly: true,
    },
    list,
    win
  );

  await new Promise(resolve => setTimeout(resolve, WAIT_TIME));
  return result;
}

async function endDrag(index) {
  let listRect = list.getBoundingClientRect();
  let clientY = listRect.top + index * 32 + 4;

  EventUtils.synthesizeDropAfterDragOver(false, dataTransfer, list, win, {
    clientY,
    _domDispatchOnly: true,
  });
  list.dispatchEvent(new CustomEvent("dragend", { bubbles: true }));
  dragService.endDragSession(true);

  await new Promise(resolve => setTimeout(resolve, WAIT_TIME));
}

function checkRowOrder(expectedOrder) {
  expectedOrder = expectedOrder.split(" ").map(i => `row-${i}`);
  Assert.equal(list.rowCount, expectedOrder.length, "rowCount is correct");
  Assert.deepEqual(
    list.rows.map(row => row.id),
    expectedOrder,
    "order in DOM is correct"
  );

  let apparentOrder = list.rows.sort(
    (a, b) => a.getBoundingClientRect().top - b.getBoundingClientRect().top
  );
  Assert.deepEqual(
    apparentOrder.map(row => row.id),
    expectedOrder,
    "order on screen is correct"
  );

  if (orderedHandler.orderAtEvent) {
    Assert.deepEqual(
      orderedHandler.orderAtEvent,
      expectedOrder,
      "order at the last 'ordered' event was correct"
    );
  }
}

function checkYPositions(...expectedPositions) {
  let offset = list.getBoundingClientRect().top;

  for (let i = 0; i < 5; i++) {
    let id = `row-${i + 1}`;
    let row = doc.getElementById(id);
    Assert.equal(
      row.getBoundingClientRect().top - offset,
      expectedPositions[i],
      id
    );
  }
}

async function checkNoTransformations() {
  for (let row of list.children) {
    await TestUtils.waitForCondition(
      () => win.getComputedStyle(row).transform == "none",
      `${row.id} has no transforms`
    );
    Assert.equal(
      row
        .getAnimations()
        .filter(animation => animation.transitionProperty != "opacity").length,
      0,
      `${row.id} has no animations`
    );
  }
}

let selectHandler = {
  seenEvent: null,

  reset() {
    this.seenEvent = null;
  },
  handleEvent(event) {
    this.seenEvent = event;
  },
};

let orderedHandler = {
  seenEvent: null,
  orderAtEvent: null,

  reset() {
    this.seenEvent = null;
    this.orderAtEvent = null;
  },
  handleEvent(event) {
    if (this.seenEvent) {
      throw new Error("we already have an 'ordered' event");
    }
    this.seenEvent = event;
    this.orderAtEvent = list.rows.map(row => row.id);
  },
};

/** Test Alt+Up and Alt+Down. */
async function subtestKeyReorder() {
  list.focus();
  list.selectedIndex = 0;

  // Move row 1 down the list to the bottom.

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 1 3 3-1 3-2 3-3 4 5 5-1 5-2");

  // Some additional checks to prove the right row is selected.

  Assert.ok(!selectHandler.seenEvent);
  Assert.equal(list.selectedIndex, 3, "correct index is selected");
  Assert.equal(
    list.querySelector(".selected").id,
    "row-1",
    "correct row is selected"
  );

  EventUtils.synthesizeKey("KEY_ArrowUp", {}, win);
  Assert.equal(
    list.querySelector(".selected").id,
    "row-2-2",
    "key press moved to the correct row"
  );
  EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2 1");

  // Move row 1 back to the top.

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 1 3 3-1 3-2 3-3 4 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");

  // Move row 3 around. Row 3 has children, so we're checking they move with it.

  list.selectedIndex = 4;

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 3 3-1 3-2 3-3 2 2-1 2-2 4 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("3 3-1 3-2 3-3 1 2 2-1 2-2 4 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 3 3-1 3-2 3-3 2 2-1 2-2 4 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 2 2-1 2-2 4 3 3-1 3-2 3-3 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 2 2-1 2-2 4 5 5-1 5-2 3 3-1 3-2 3-3");

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 2 2-1 2-2 4 3 3-1 3-2 3-3 5 5-1 5-2");

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
}

/** Drag the first item to the end. */
async function subtestDragReorder1() {
  orderedHandler.reset();
  list.addEventListener("ordered", orderedHandler);

  checkYPositions(1, 33, 129, 257, 289);

  await startDrag(0);
  checkYPositions(1, 33, 129, 257, 289);

  await continueDrag(2);
  checkYPositions(52, 1, 129, 257, 289);
  await continueDrag(3);
  checkYPositions(84, 1, 129, 257, 289);
  await continueDrag(4);
  checkYPositions(116, 1, 129, 257, 289);
  await continueDrag(5);
  checkYPositions(148, 1, 129, 257, 289);
  await continueDrag(6);
  checkYPositions(180, 1, 97, 257, 289);
  await continueDrag(7);
  checkYPositions(212, 1, 97, 257, 289);
  await continueDrag(8);
  checkYPositions(244, 1, 97, 225, 289);
  await continueDrag(9);
  checkYPositions(276, 1, 97, 225, 289);
  await continueDrag(10);
  checkYPositions(308, 1, 97, 225, 257);
  await continueDrag(11);
  checkYPositions(340, 1, 97, 225, 257);
  await continueDrag(12);
  checkYPositions(353, 1, 97, 225, 257);

  await endDrag(12);
  list.removeEventListener("ordered", orderedHandler);

  Assert.ok(orderedHandler.seenEvent);
  checkYPositions(353, 1, 97, 225, 257);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2 1");
  await checkNoTransformations();
}

/** Drag the (now) last item back to the start. */
async function subtestDragReorder2() {
  orderedHandler.reset();
  list.addEventListener("ordered", orderedHandler);

  await startDrag(11);
  checkYPositions(340, 1, 97, 225, 257);

  await continueDrag(9);
  checkYPositions(276, 1, 97, 225, 289);

  await continueDrag(7);
  checkYPositions(212, 1, 97, 257, 289);

  await continueDrag(4);
  checkYPositions(116, 1, 129, 257, 289);

  await continueDrag(1);
  checkYPositions(20, 33, 129, 257, 289);

  await endDrag(0);
  list.removeEventListener("ordered", orderedHandler);

  Assert.ok(orderedHandler.seenEvent);
  checkYPositions(1, 33, 129, 257, 289);
  checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
  await checkNoTransformations();
}

/**
 * Listen for the 'ordering' event and prevent dropping on some rows.
 *
 * In this test, we'll prevent dragging an item below the last one - row-5 and
 * its descendants. Other use cases may be possible but haven't been needed
 * yet, so they are untested.
 */
async function subtestDragUndroppable() {
  let originalGetter = list.__lookupGetter__("_orderableChildren");
  list.__defineGetter__("_orderableChildren", function () {
    let rows = [...this.children];
    rows.pop();
    return rows;
  });

  orderedHandler.reset();
  list.addEventListener("ordered", orderedHandler);

  checkYPositions(1, 33, 129, 257, 289);

  await startDrag(0);
  checkYPositions(1, 33, 129, 257, 289);

  await continueDrag(8);
  checkYPositions(244, 1, 97, 225, 289);
  await continueDrag(9);
  checkYPositions(257, 1, 97, 225, 289);
  await continueDrag(10);
  checkYPositions(257, 1, 97, 225, 289);
  await continueDrag(11);
  checkYPositions(257, 1, 97, 225, 289);
  await continueDrag(12);
  checkYPositions(257, 1, 97, 225, 289);

  await endDrag(12);
  list.removeEventListener("ordered", orderedHandler);

  Assert.ok(orderedHandler.seenEvent);
  checkYPositions(257, 1, 97, 225, 289);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");
  await checkNoTransformations();

  // Move row-3 down with the keyboard.

  list.selectedIndex = 7;
  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");

  // It should not move further down.

  await orderWithKeys("KEY_ArrowDown");
  Assert.ok(!orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 1 4 5 5-1 5-2");

  // Reset the order.

  await orderWithKeys("KEY_ArrowUp");
  Assert.ok(orderedHandler.seenEvent);
  checkRowOrder("2 2-1 2-2 3 3-1 3-2 3-3 4 1 5 5-1 5-2");

  orderedHandler.reset();
  await startDrag(8);
  await continueDrag(1);
  await endDrag(1);
  checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");

  list.__defineGetter__("_orderableChildren", originalGetter);
}

add_setup(async function () {
  // Make sure the whole test runs with an unthreaded view in all folders.
  Services.prefs.setIntPref("mailnews.default_view_flags", 0);

  let tab = tabmail.openTab("contentTab", {
    url: "chrome://mochitests/content/browser/comm/mail/base/test/browser/files/orderableTreeListbox.xhtml",
  });

  await BrowserTestUtils.browserLoaded(tab.browser);
  tab.browser.focus();

  win = tab.browser.contentWindow;
  doc = win.document;

  list = doc.querySelector(`ol[is="orderable-tree-listbox"]`);
  Assert.ok(!!list, "the list exists");

  checkRowOrder("1 2 2-1 2-2 3 3-1 3-2 3-3 4 5 5-1 5-2");
  Assert.equal(list.selectedIndex, 0, "selectedIndex is set to 0");
});

add_task(async function testKeyReorder() {
  await withMotion(subtestKeyReorder);
});
add_task(async function testDragReorder1() {
  await withMotion(subtestDragReorder1);
});
add_task(async function testDragReorder2() {
  await withMotion(subtestDragReorder2);
});
add_task(async function testDragUndroppable() {
  await withMotion(subtestDragUndroppable);
});

add_task(async function testKeyReorderReducedMotion() {
  await withoutMotion(subtestKeyReorder);
});
add_task(async function testDragReorder1ReducedMotion() {
  await withoutMotion(subtestDragReorder1);
});
add_task(async function testDragReorder2ReducedMotion() {
  await withoutMotion(subtestDragReorder2);
});
add_task(async function testDragUndroppableReducedMotion() {
  await withoutMotion(subtestDragUndroppable);
});