OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 GEN('#include "chrome/test/data/webui/history_ui_browsertest.h"'); | |
6 | |
7 /** @const */ var TOTAL_RESULT_COUNT = 160; | |
8 /** @const */ var WAIT_TIMEOUT = 200; | |
9 | |
10 /** | |
11 * Test fixture for history WebUI testing. | |
12 * @constructor | |
13 * @extends {testing.Test} | |
14 */ | |
15 function HistoryUIBrowserTest() {} | |
16 | |
17 /** | |
18 * Create a fake history result with the given timestamp. | |
19 * @param {Number} timestamp Timestamp of the entry, in ms since the epoch. | |
20 * @param {String} url The URL to set on this entry. | |
21 * @return {Object} An object representing a history entry. | |
22 */ | |
23 function createHistoryEntry(timestamp, url) { | |
24 var d = new Date(timestamp); | |
25 // Extract domain from url. | |
26 var domainMatch = url.replace(/^.+?:\/\//, '').match(/[^/]+/); | |
27 var domain = domainMatch ? domainMatch[0] : ''; | |
28 return { | |
29 dateTimeOfDay: d.getHours() + ':' + d.getMinutes(), | |
30 dateRelativeDay: d.toDateString(), | |
31 allTimestamps: [timestamp], | |
32 starred: false, | |
33 time: timestamp, | |
34 title: d.toString(), // Use the stringified date as the title. | |
35 url: url, | |
36 domain: domain | |
37 }; | |
38 } | |
39 | |
40 /** | |
41 * Wait for the history backend to call the global function named | |
42 * |callbackName|, and then execute |afterFunction|. This allows tests to | |
43 * wait on asynchronous backend operations before proceeding. | |
44 */ | |
45 function waitForCallback(callbackName, afterFunction) { | |
46 var originalCallback = window[callbackName]; | |
47 | |
48 // Install a wrapper that temporarily replaces the original function. | |
49 window[callbackName] = function() { | |
50 window[callbackName] = originalCallback; | |
51 originalCallback.apply(this, arguments); | |
52 afterFunction(); | |
53 }; | |
54 } | |
55 | |
56 /** | |
57 * Asynchronously execute the global function named |functionName|. This | |
58 * should be used for all calls from backend stubs to the frontend. | |
59 */ | |
60 function callFrontendAsync(functionName) { | |
61 var args = Array.prototype.slice.call(arguments, 1); | |
62 setTimeout(function() { | |
63 window[functionName].apply(window, args); | |
64 }, 1); | |
65 } | |
66 | |
67 /** | |
68 * Checks that all the checkboxes in the [|start|, |end|] interval are checked | |
69 * and that their IDs are properly set. Does that against the checkboxes in | |
70 * |checked|, starting from the |startInChecked| position. | |
71 * @param {Array} checked An array of all the relevant checked checkboxes | |
72 * on this page. | |
73 * @param {Number} start The starting checkbox id. | |
74 * @param {Number} end The ending checkbox id. | |
75 */ | |
76 function checkInterval(checked, start, end) { | |
77 for (var i = start; i <= end; i++) | |
78 expectEquals('checkbox-' + i, checked[i - start].id); | |
79 } | |
80 | |
81 /** | |
82 * Returns a period of 7 days, |offset| weeks back from |today|. The behavior | |
83 * of this function should be identical to | |
84 * BrowsingHistoryHandler::SetQueryTimeInWeeks. | |
85 * @param {Number} offset Number of weeks to go back. | |
86 * @param {Date} today Which date to consider as "today" (since we're not using | |
87 * the actual current date in this case). | |
88 * @return {Object} An object containing the begin date and the end date of the | |
89 * computed period. | |
90 */ | |
91 function setQueryTimeInWeeks(offset, today) { | |
92 // Going back one day at a time starting from midnight will make sure that | |
93 // the other values get updated properly. | |
94 var endTime = new Date(today); | |
95 endTime.setHours(24, 0, 0, 0); | |
96 for (var i = 0; i < 7 * offset; i++) | |
97 endTime.setDate(endTime.getDate() - 1); | |
98 var beginTime = new Date(endTime); | |
99 for (var i = 0; i < 7; i++) | |
100 beginTime.setDate(beginTime.getDate() - 1); | |
101 return {'endTime': endTime, 'beginTime': beginTime}; | |
102 } | |
103 | |
104 /** | |
105 * Returns the period of a month, |offset| months back from |today|. The | |
106 * behavior of this function should be identical to | |
107 * BrowsingHistoryHandler::SetQueryTimeInMonths. | |
108 * @param {Number} offset Number of months to go back. | |
109 * @param {Date} today Which date to consider as "today" (since we're not using | |
110 * the actual current date in this case). | |
111 * @return {Object} An object containing the begin date and the end date of the | |
112 * computed period. | |
113 */ | |
114 function setQueryTimeInMonths(offset, today) { | |
115 var endTime = new Date(today); | |
116 var beginTime = new Date(today); | |
117 // Last day of this month. | |
118 endTime.setMonth(endTime.getMonth() + 1, 0); | |
119 // First day of the current month. | |
120 beginTime.setMonth(beginTime.getMonth(), 1); | |
121 for (var i = 0; i < offset; i++) { | |
122 beginTime.setMonth(beginTime.getMonth() - 1); | |
123 endTime.setMonth(endTime.getMonth() - 1); | |
124 } | |
125 return {'endTime': endTime, 'beginTime': beginTime}; | |
126 } | |
127 | |
128 /** | |
129 * Base fixture for History WebUI testing. | |
130 * @extends {testing.Test} | |
131 * @constructor | |
132 */ | |
133 function BaseHistoryWebUITest() {} | |
134 | |
135 BaseHistoryWebUITest.prototype = { | |
136 __proto__: testing.Test.prototype, | |
137 | |
138 /** | |
139 * Browse to the history page & call our preLoad(). | |
140 */ | |
141 browsePreload: 'chrome://history-frame', | |
142 | |
143 /** @override */ | |
144 typedefCppFixture: 'HistoryUIBrowserTest', | |
145 | |
146 /** @override */ | |
147 runAccessibilityChecks: true, | |
148 | |
149 /** @override */ | |
150 accessibilityIssuesAreErrors: true, | |
151 | |
152 /** @override */ | |
153 isAsync: true, | |
154 }; | |
155 | |
156 /** | |
157 * Fixture for History WebUI testing which returns some fake history results | |
158 * to the frontend. Other fixtures that want to stub out calls to the backend | |
159 * can extend this one. | |
160 * @extends {BaseHistoryWebUITest} | |
161 * @constructor | |
162 */ | |
163 function HistoryWebUIFakeBackendTest() { | |
164 } | |
165 | |
166 HistoryWebUIFakeBackendTest.prototype = { | |
167 __proto__: BaseHistoryWebUITest.prototype, | |
168 | |
169 /** | |
170 * Register handlers to stub out calls to the history backend. | |
171 * @override | |
172 */ | |
173 preLoad: function() { | |
174 this.registerMockHandler_( | |
175 'queryHistory', this.queryHistoryStub_.bind(this)); | |
176 }, | |
177 | |
178 /** | |
179 * Register a mock handler for a message to the history backend. | |
180 * @param handlerName The name of the message to mock. | |
181 * @param handler The mock message handler function. | |
182 */ | |
183 registerMockHandler_: function(handlerName, handler) { | |
184 // Mock4JS doesn't pass in the actual arguments to the stub, but it _will_ | |
185 // pass the original args to the matcher object. SaveMockArguments acts as | |
186 // a proxy for another matcher, but keeps track of all the arguments it was | |
187 // asked to match. | |
188 var savedArgs = new SaveMockArguments(); | |
189 | |
190 this.makeAndRegisterMockHandler([handlerName]); | |
191 this.mockHandler.stubs()[handlerName](savedArgs.match(ANYTHING)).will( | |
192 callFunctionWithSavedArgs(savedArgs, handler)); | |
193 }, | |
194 | |
195 /** | |
196 * Default stub for the queryHistory message to the history backend. | |
197 * Simulates an empty history database. Override this to customize this | |
198 * behavior for particular tests. | |
199 * @param {Array} arguments The original arguments to queryHistory. | |
200 */ | |
201 queryHistoryStub_: function(args) { | |
202 callFrontendAsync( | |
203 'historyResult', { term: args[0], finished: true }, []); | |
204 } | |
205 }; | |
206 | |
207 function queryHistoryImpl(args, beginTime, history) { | |
208 var searchText = args[0]; | |
209 var offset = args[1]; | |
210 var range = args[2]; | |
211 var endTime = args[3] || Number.MAX_VALUE; | |
212 var maxCount = args[4]; | |
213 | |
214 var results = []; | |
215 if (searchText) { | |
216 for (var k = 0; k < history.length; k++) { | |
217 // Search only by title in this stub. | |
218 if (history[k].title.indexOf(searchText) != -1) | |
219 results.push(history[k]); | |
220 } | |
221 } else { | |
222 results = history; | |
223 } | |
224 | |
225 // Advance past all entries newer than the specified end time. | |
226 var i = 0; | |
227 // Finished is set from the history database so this behavior may not be | |
228 // completely identical. | |
229 var finished = true; | |
230 while (i < results.length && results[i].time >= endTime) | |
231 ++i; | |
232 | |
233 if (beginTime) { | |
234 var j = i; | |
235 while (j < results.length && results[j].time >= beginTime) | |
236 ++j; | |
237 | |
238 finished = (j == results.length); | |
239 results = results.slice(i, j); | |
240 } else { | |
241 results = results.slice(i); | |
242 } | |
243 | |
244 if (maxCount) { | |
245 finished = (maxCount >= results.length); | |
246 results = results.slice(0, maxCount); | |
247 } | |
248 | |
249 var queryStartTime = ''; | |
250 var queryEndTime = ''; | |
251 if (results.length) { | |
252 queryStartTime = results[results.length - 1].dateRelativeDay; | |
253 queryEndTime = results[0].dateRelativeDay; | |
254 } else if (beginTime) { | |
255 queryStartTime = Date(beginTime); | |
256 queryEndTime = Date(endTime); | |
257 } | |
258 | |
259 callFrontendAsync( | |
260 'historyResult', | |
261 { | |
262 term: searchText, | |
263 finished: finished, | |
264 queryInterval: queryStartTime + ' - ' + queryEndTime, | |
265 }, | |
266 results); | |
267 } | |
268 | |
269 /** | |
270 * Fixture for History WebUI testing which returns some fake history results | |
271 * to the frontend. | |
272 * @extends {HistoryWebUIFakeBackendTest} | |
273 * @constructor | |
274 */ | |
275 function HistoryWebUITest() {} | |
276 | |
277 HistoryWebUITest.prototype = { | |
278 __proto__: HistoryWebUIFakeBackendTest.prototype, | |
279 | |
280 preLoad: function() { | |
281 HistoryWebUIFakeBackendTest.prototype.preLoad.call(this); | |
282 | |
283 this.registerMockHandler_( | |
284 'removeVisits', this.removeVisitsStub_.bind(this)); | |
285 | |
286 // Prepare a list of fake history results. The entries will begin at | |
287 // 1:00 AM on Sept 2, 2008, and will be spaced two minutes apart. | |
288 var timestamp = new Date(2008, 9, 2, 1, 0).getTime(); | |
289 this.fakeHistory_ = []; | |
290 | |
291 for (var i = 0; i < TOTAL_RESULT_COUNT; i++) { | |
292 this.fakeHistory_.push( | |
293 createHistoryEntry(timestamp, 'http://google.com/' + timestamp)); | |
294 timestamp -= 2 * 60 * 1000; // Next visit is two minutes earlier. | |
295 } | |
296 }, | |
297 | |
298 /** | |
299 * Stub for the 'queryHistory' message to the history backend. | |
300 * Simulates a history database using the fake history data that is | |
301 * initialized in preLoad(). | |
302 * @param {Array} arguments The original arguments to queryHistory. | |
303 */ | |
304 queryHistoryStub_: function(args) { | |
305 var searchText = args[0]; | |
306 var offset = args[1]; | |
307 var range = args[2]; | |
308 var endTime = args[3] || Number.MAX_VALUE; | |
309 var maxCount = args[4]; | |
310 if (range == HistoryModel.Range.ALL_TIME) { | |
311 queryHistoryImpl(args, null, this.fakeHistory_); | |
312 return; | |
313 } | |
314 if (range == HistoryModel.Range.WEEK) | |
315 var interval = setQueryTimeInWeeks(offset, this.today); | |
316 else | |
317 var interval = setQueryTimeInMonths(offset, this.today); | |
318 | |
319 args[3] = interval.endTime.getTime(); | |
320 queryHistoryImpl(args, interval.beginTime.getTime(), this.fakeHistory_); | |
321 }, | |
322 | |
323 /** | |
324 * Stub for the 'removeVisits' message to the history backend. | |
325 * This will modify the fake history data in the test instance, so that | |
326 * further 'queryHistory' messages will not contain the deleted entries. | |
327 * @param {Array} arguments The original arguments to removeVisits. | |
328 */ | |
329 removeVisitsStub_: function(args) { | |
330 for (var i = 0; i < args.length; ++i) { | |
331 var url = args[i].url; | |
332 var timestamps = args[i].timestamps; | |
333 assertEquals(timestamps.length, 1); | |
334 this.removeVisitsToUrl_(url, new Date(timestamps[0])); | |
335 } | |
336 callFrontendAsync('deleteComplete'); | |
337 }, | |
338 | |
339 /** | |
340 * Removes any visits to |url| on the same day as |date| from the fake | |
341 * history data. | |
342 * @param {string} url | |
343 * @param {Date} date | |
344 */ | |
345 removeVisitsToUrl_: function(url, date) { | |
346 var day = date.toDateString(); | |
347 var newHistory = []; | |
348 for (var i = 0, visit; visit = this.fakeHistory_[i]; ++i) { | |
349 if (url != visit.url || visit.dateRelativeDay != day) | |
350 newHistory.push(visit); | |
351 } | |
352 this.fakeHistory_ = newHistory; | |
353 } | |
354 }; | |
355 | |
356 /** | |
357 * Examines the time column of every entry on the page, and ensure that they | |
358 * are all the same width. | |
359 */ | |
360 function ensureTimeWidthsEqual() { | |
361 var times = document.querySelectorAll('.entry .time'); | |
362 var timeWidth = times[0].clientWidth; | |
363 for (var i = 1; i < times.length; ++i) { | |
364 assertEquals(timeWidth, times[i].clientWidth); | |
365 } | |
366 } | |
367 | |
368 // Times out on Mac: http://crbug.com/336845 | |
369 TEST_F('HistoryWebUIFakeBackendTest', 'DISABLED_emptyHistory', function() { | |
370 expectTrue($('newest-button').hidden); | |
371 expectTrue($('newer-button').hidden); | |
372 expectTrue($('older-button').hidden); | |
373 testDone(); | |
374 }); | |
375 | |
376 // Times out on Win: http://crbug.com/336845 | |
377 TEST_F('HistoryWebUITest', 'DISABLED_basicTest', function() { | |
378 var resultCount = document.querySelectorAll('.entry').length; | |
379 | |
380 // Check that there are two days of entries. | |
381 var dayHeaders = document.querySelectorAll('.day'); | |
382 assertEquals(2, dayHeaders.length); | |
383 expectNotEquals(dayHeaders[0].textContent, dayHeaders[1].textContent); | |
384 | |
385 // Check that the entries in each day are time-ordered, and that no | |
386 // duplicate URLs appear on a given day. | |
387 var urlsByDay = {}; | |
388 var lastDate = new Date(); | |
389 for (var day = 0; day < dayHeaders.length; ++day) { | |
390 var dayTitle = dayHeaders[day].textContent; | |
391 var dayResults = document.querySelectorAll('.day-results')[day]; | |
392 var entries = dayResults.querySelectorAll('.entry'); | |
393 expectGT(entries.length, 0); | |
394 | |
395 for (var i = 0, entry; entry = entries[i]; ++i) { | |
396 var time = entry.querySelector('.time').textContent; | |
397 expectGT(time.length, 0); | |
398 | |
399 var date = new Date(dayTitle + ' ' + time); | |
400 expectGT(lastDate, date); | |
401 lastDate = date; | |
402 | |
403 // Ensure it's not a duplicate URL for this day. | |
404 var dayAndUrl = day + entry.querySelector('a').href; | |
405 expectFalse(urlsByDay.hasOwnProperty(dayAndUrl)); | |
406 urlsByDay[dayAndUrl] = dayAndUrl; | |
407 | |
408 // Reconstruct the entry date from the title, and ensure that it's | |
409 // consistent with the date header and with the time. | |
410 var entryDate = new Date(entry.querySelector('.title').textContent); | |
411 expectEquals(entryDate.getYear(), date.getYear()); | |
412 expectEquals(entryDate.getMonth(), date.getMonth()); | |
413 expectEquals(entryDate.getDay(), date.getDay()); | |
414 expectEquals(entryDate.getHours(), date.getHours()); | |
415 expectEquals(entryDate.getMinutes(), date.getMinutes()); | |
416 } | |
417 } | |
418 | |
419 // Check that there are 3 page navigation links and that only the "Older" | |
420 // link is visible. | |
421 expectEquals(3, document.querySelectorAll('[is="action-link"]').length); | |
422 expectTrue($('newest-button').hidden); | |
423 expectTrue($('newer-button').hidden); | |
424 expectFalse($('older-button').hidden); | |
425 | |
426 ensureTimeWidthsEqual(); | |
427 | |
428 // Go to the next page. | |
429 $('older-button').click(); | |
430 waitForCallback('historyResult', function() { | |
431 resultCount += document.querySelectorAll('.entry').length; | |
432 | |
433 // Check that the two pages include all of the entries. | |
434 expectEquals(TOTAL_RESULT_COUNT, resultCount); | |
435 | |
436 // Check that the day header was properly continued -- the header for the | |
437 // last day on the first page should be a substring of the header on the | |
438 // second page. E.g. "Wed, Oct 8, 2008" and "Web, Oct 8, 2008 - cont'd". | |
439 var newDayHeaders = document.querySelectorAll('.day'); | |
440 expectEquals(1, newDayHeaders.length); | |
441 expectEquals(0, | |
442 newDayHeaders[0].textContent.indexOf(dayHeaders[1].textContent)); | |
443 | |
444 // Check that the "Newest" and "Newer" links are now visible, but the | |
445 // "Older" link is hidden. | |
446 expectEquals(3, document.querySelectorAll('[is="action-link"]').length); | |
447 expectFalse($('newest-button').hidden); | |
448 expectFalse($('newer-button').hidden); | |
449 expectTrue($('older-button').hidden); | |
450 | |
451 ensureTimeWidthsEqual(); | |
452 | |
453 // Go back to the first page, and check that the same day headers are there. | |
454 $('newest-button').click(); | |
455 var newDayHeaders = document.querySelectorAll('.day'); | |
456 expectEquals(2, newDayHeaders.length); | |
457 | |
458 expectNotEquals(newDayHeaders[0].textContent, | |
459 newDayHeaders[1].textContent); | |
460 expectEquals(dayHeaders[0].textContent, newDayHeaders[0].textContent); | |
461 expectEquals(dayHeaders[1].textContent, newDayHeaders[1].textContent); | |
462 | |
463 testDone(); | |
464 }); | |
465 }); | |
466 | |
467 /** | |
468 * Test bulk deletion of history entries. | |
469 * Disabled because it is currently very flaky on the Windows XP bot. | |
470 */ | |
471 TEST_F('HistoryWebUITest', 'DISABLED_bulkDeletion', function() { | |
472 var checkboxes = document.querySelectorAll( | |
473 '#results-display input[type=checkbox]'); | |
474 | |
475 // Immediately confirm the history deletion. | |
476 confirmDeletion = function(okCallback, cancelCallback) { | |
477 okCallback(); | |
478 }; | |
479 | |
480 // The "remove" button should be initially disabled. | |
481 var removeButton = $('remove-selected'); | |
482 expectTrue(removeButton.disabled); | |
483 | |
484 checkboxes[0].click(); | |
485 expectFalse(removeButton.disabled); | |
486 | |
487 var firstEntry = document.querySelector('.title a').textContent; | |
488 removeButton.click(); | |
489 | |
490 // After deletion, expect the results to be reloaded. | |
491 waitForCallback('historyResult', function() { | |
492 expectNotEquals(document.querySelector('.title a').textContent, firstEntry); | |
493 expectTrue(removeButton.disabled); | |
494 | |
495 // Delete the first 3 entries. | |
496 checkboxes = document.querySelectorAll( | |
497 '#results-display input[type=checkbox]'); | |
498 checkboxes[0].click(); | |
499 checkboxes[1].click(); | |
500 checkboxes[2].click(); | |
501 expectFalse(removeButton.disabled); | |
502 | |
503 var nextEntry = document.querySelectorAll('.title a')[3]; | |
504 removeButton.click(); | |
505 waitForCallback('historyResult', function() { | |
506 // The next entry after the deleted ones should now be the first. | |
507 expectEquals(document.querySelector('.title a').textContent, | |
508 nextEntry.textContent); | |
509 testDone(); | |
510 }); | |
511 }); | |
512 }); | |
513 | |
514 /** | |
515 * Test selecting multiple entries using shift click. | |
516 * Disabled due to time out on all platforms: crbug/375910 | |
517 */ | |
518 TEST_F('HistoryWebUITest', 'DISABLED_multipleSelect', function() { | |
519 var checkboxes = document.querySelectorAll( | |
520 '#results-display input[type=checkbox]'); | |
521 | |
522 var getAllChecked = function() { | |
523 return Array.prototype.slice.call(document.querySelectorAll( | |
524 '#results-display input[type=checkbox]:checked')); | |
525 }; | |
526 | |
527 // Make sure that nothing is checked. | |
528 expectEquals(0, getAllChecked().length); | |
529 | |
530 var shiftClick = function(el) { | |
531 el.dispatchEvent(new MouseEvent('click', { shiftKey: true })); | |
532 }; | |
533 | |
534 // Check the start. | |
535 shiftClick($('checkbox-4')); | |
536 // And the end. | |
537 shiftClick($('checkbox-9')); | |
538 | |
539 // See if they are checked. | |
540 var checked = getAllChecked(); | |
541 expectEquals(6, checked.length); | |
542 checkInterval(checked, 4, 9); | |
543 | |
544 // Extend the selection. | |
545 shiftClick($('checkbox-14')); | |
546 | |
547 checked = getAllChecked(); | |
548 expectEquals(11, checked.length); | |
549 checkInterval(checked, 4, 14); | |
550 | |
551 // Now do a normal click on a higher ID box and a shift click on a lower ID | |
552 // one (test the other way around). | |
553 $('checkbox-24').click(); | |
554 shiftClick($('checkbox-19')); | |
555 | |
556 checked = getAllChecked(); | |
557 expectEquals(17, checked.length); | |
558 // First set of checkboxes (11). | |
559 checkInterval(checked, 4, 14); | |
560 // Second set (6). | |
561 checkInterval(checked.slice(11), 19, 24); | |
562 | |
563 // Test deselection. | |
564 $('checkbox-26').click(); | |
565 shiftClick($('checkbox-20')); | |
566 | |
567 checked = getAllChecked(); | |
568 // checkbox-20 to checkbox-24 should be deselected now. | |
569 expectEquals(12, checked.length); | |
570 // First set of checkboxes (11). | |
571 checkInterval(checked, 4, 14); | |
572 // Only checkbox-19 should still be selected. | |
573 expectEquals('checkbox-19', checked[11].id); | |
574 | |
575 testDone(); | |
576 }); | |
577 | |
578 TEST_F('HistoryWebUITest', 'DISABLED_searchHistory', function() { | |
579 var getResultCount = function() { | |
580 return document.querySelectorAll('.entry').length; | |
581 }; | |
582 // See that all the elements are there. | |
583 expectEquals(RESULTS_PER_PAGE, getResultCount()); | |
584 | |
585 // See that the search works. | |
586 $('search-field').value = 'Thu Oct 02 2008'; | |
587 $('search-button').click(); | |
588 | |
589 waitForCallback('historyResult', function() { | |
590 expectEquals(31, getResultCount()); | |
591 | |
592 // Clear the search. | |
593 $('search-field').value = ''; | |
594 $('search-button').click(); | |
595 waitForCallback('historyResult', function() { | |
596 expectEquals(RESULTS_PER_PAGE, getResultCount()); | |
597 testDone(); | |
598 }); | |
599 }); | |
600 }); | |
601 | |
602 function setPageState(searchText, page, groupByDomain, range, offset) { | |
603 window.location = '#' + PageState.getHashString( | |
604 searchText, page, groupByDomain, range, offset); | |
605 } | |
606 | |
607 function RangeHistoryWebUITest() {} | |
608 | |
609 RangeHistoryWebUITest.prototype = { | |
610 __proto__: HistoryWebUITest.prototype, | |
611 | |
612 /** @override */ | |
613 preLoad: function() { | |
614 HistoryWebUITest.prototype.preLoad.call(this); | |
615 // Repeat the domain visits every 4 days. The nested lists contain the | |
616 // domain suffixes for the visits in a day. | |
617 var domainSuffixByDay = [ | |
618 [1, 2, 3, 4], | |
619 [1, 2, 2, 3], | |
620 [1, 2, 1, 2], | |
621 [1, 1, 1, 1] | |
622 ]; | |
623 | |
624 var buildDomainUrl = function(timestamp) { | |
625 var d = new Date(timestamp); | |
626 // Repeat the same setup of domains every 4 days. | |
627 var day = d.getDate() % 4; | |
628 // Assign an entry for every 6 hours so that we get 4 entries per day | |
629 // maximum. | |
630 var visitInDay = Math.floor(d.getHours() / 6); | |
631 return 'http://google' + domainSuffixByDay[day][visitInDay] + '.com/' + | |
632 timestamp; | |
633 }; | |
634 | |
635 // Prepare a list of fake history results. Start the results on | |
636 // 11:00 PM on May 2, 2012 and add 4 results every day (one result every 6 | |
637 // hours). | |
638 var timestamp = new Date(2012, 4, 2, 23, 0).getTime(); | |
639 this.today = new Date(2012, 4, 2); | |
640 this.fakeHistory_ = []; | |
641 | |
642 // Put in 2 days for May and 30 days for April so the results span over | |
643 // the month limit. | |
644 for (var i = 0; i < 4 * 32; i++) { | |
645 this.fakeHistory_.push( | |
646 createHistoryEntry(timestamp, buildDomainUrl(timestamp))); | |
647 timestamp -= 6 * 60 * 60 * 1000; | |
648 } | |
649 | |
650 // Leave March empty. | |
651 timestamp -= 31 * 24 * 3600 * 1000; | |
652 | |
653 // Put results in February. | |
654 for (var i = 0; i < 29 * 4; i++) { | |
655 this.fakeHistory_.push( | |
656 createHistoryEntry(timestamp, buildDomainUrl(timestamp))); | |
657 timestamp -= 6 * 60 * 60 * 1000; | |
658 } | |
659 }, | |
660 | |
661 setUp: function() { | |
662 // Show the filter controls as if the command line switch was active. | |
663 $('top-container').hidden = true; | |
664 $('history-page').classList.add('big-topbar-page'); | |
665 $('filter-controls').hidden = false; | |
666 expectFalse($('filter-controls').hidden); | |
667 }, | |
668 }; | |
669 | |
670 /** | |
671 * Disabled due intermitent failures on multiple OSes http://crbug.com/377338 | |
672 */ | |
673 TEST_F('RangeHistoryWebUITest', 'DISABLED_allView', function() { | |
674 // Check that we start off in the all time view. | |
675 expectTrue($('timeframe-controls').querySelector('input').checked); | |
676 // See if the correct number of days is shown. | |
677 var dayHeaders = document.querySelectorAll('.day'); | |
678 assertEquals(Math.ceil(RESULTS_PER_PAGE / 4), dayHeaders.length); | |
679 testDone(); | |
680 }); | |
681 | |
682 /** | |
683 * Checks whether the domains in a day are ordered decreasingly. | |
684 * @param {Element} element Ordered list containing the grouped domains for a | |
685 * day. | |
686 */ | |
687 function checkGroupedVisits(element) { | |
688 // The history page contains the number of visits next to a domain in | |
689 // parentheses (e.g. 'google.com (5)'). This function extracts that number | |
690 // and returns it. | |
691 var getNumberVisits = function(element) { | |
692 return parseInt(element.textContent.replace(/\D/g, ''), 10); | |
693 }; | |
694 | |
695 // Read the number of visits from each domain and make sure that it is lower | |
696 // than or equal to the number of visits from the previous domain. | |
697 var domainEntries = element.querySelectorAll('.number-visits'); | |
698 var currentNumberOfVisits = getNumberVisits(domainEntries[0]); | |
699 for (var j = 1; j < domainEntries.length; j++) { | |
700 var numberOfVisits = getNumberVisits(domainEntries[j]); | |
701 assertTrue(currentNumberOfVisits >= numberOfVisits); | |
702 currentNumberOfVisits = numberOfVisits; | |
703 } | |
704 } | |
705 | |
706 // Times out on Mac and Win: http://crbug.com/336845 | |
707 TEST_F('RangeHistoryWebUITest', 'DISABLED_weekViewGrouped', function() { | |
708 // Change to weekly view. | |
709 setPageState('', 0, HistoryModel.Range.WEEK, 0); | |
710 waitForCallback('historyResult', function() { | |
711 // See if the correct number of days is still shown. | |
712 var dayResults = document.querySelectorAll('.day-results'); | |
713 assertEquals(7, dayResults.length); | |
714 | |
715 // Check whether the results are ordered by visits. | |
716 for (var i = 0; i < dayResults.length; i++) | |
717 checkGroupedVisits(dayResults[i]); | |
718 | |
719 ensureTimeWidthsEqual(); | |
720 | |
721 testDone(); | |
722 }); | |
723 }); | |
724 | |
725 // Times out on Mac and Win: http://crbug.com/336845 | |
726 TEST_F('RangeHistoryWebUITest', 'DISABLED_monthViewGrouped', function() { | |
727 // Change to monthly view. | |
728 setPageState('', 0, HistoryModel.Range.MONTH, 0); | |
729 waitForCallback('historyResult', function() { | |
730 // See if the correct number of days is shown. | |
731 var monthResults = document.querySelectorAll('.month-results'); | |
732 assertEquals(1, monthResults.length); | |
733 | |
734 checkGroupedVisits(monthResults[0]); | |
735 ensureTimeWidthsEqual(); | |
736 | |
737 testDone(); | |
738 }); | |
739 }); | |
740 | |
741 // Times out on Mac: http://crbug.com/336845 | |
742 TEST_F('RangeHistoryWebUITest', 'DISABLED_monthViewEmptyMonth', function() { | |
743 // Change to monthly view. | |
744 setPageState('', 0, HistoryModel.Range.MONTH, 2); | |
745 | |
746 waitForCallback('historyResult', function() { | |
747 // See if the correct number of days is shown. | |
748 var resultsDisplay = $('results-display'); | |
749 assertEquals(0, resultsDisplay.querySelectorAll('.months-results').length); | |
750 var noResults = loadTimeData.getString('noResults'); | |
751 assertNotEquals(-1, $('results-header').textContent.indexOf(noResults)); | |
752 | |
753 testDone(); | |
754 }); | |
755 }); | |
756 | |
757 /** | |
758 * Fixture for History WebUI tests using the real history backend. | |
759 * @extends {BaseHistoryWebUITest} | |
760 * @constructor | |
761 */ | |
762 function HistoryWebUIRealBackendTest() {} | |
763 | |
764 HistoryWebUIRealBackendTest.prototype = { | |
765 __proto__: BaseHistoryWebUITest.prototype, | |
766 | |
767 /** @override */ | |
768 testGenPreamble: function() { | |
769 // Add some visits to the history database. | |
770 GEN(' AddPageToHistory(0, "http://google.com", "Google");'); | |
771 GEN(' AddPageToHistory(1, "http://example.com", "Example");'); | |
772 GEN(' AddPageToHistory(2, "http://google.com", "Google");'); | |
773 | |
774 // Add a visit on the next day. | |
775 GEN(' AddPageToHistory(36, "http://google.com", "Google");'); | |
776 }, | |
777 | |
778 /** @override */ | |
779 setUp: function() { | |
780 BaseHistoryWebUITest.prototype.setUp.call(this); | |
781 | |
782 // Enable when failure is resolved. | |
783 // AX_TEXT_04: http://crbug.com/560914 | |
784 this.accessibilityAuditConfig.ignoreSelectors( | |
785 'linkWithUnclearPurpose', | |
786 '#notification-bar > SPAN > A'); | |
787 }, | |
788 }; | |
789 | |
790 /** | |
791 * Simple test that verifies that the correct entries are retrieved from the | |
792 * history database and displayed in the UI. | |
793 */ | |
794 // Times out on Mac and Win: http://crbug.com/336845 | |
795 TEST_F('HistoryWebUIRealBackendTest', 'DISABLED_basic', function() { | |
796 // Check that there are two days of entries, and three entries in total. | |
797 assertEquals(2, document.querySelectorAll('.day').length); | |
798 assertEquals(3, document.querySelectorAll('.entry').length); | |
799 | |
800 testDone(); | |
801 }); | |
802 | |
803 // Times out on Mac: http://crbug.com/336845 | |
804 TEST_F('HistoryWebUIRealBackendTest', | |
805 'DISABLED_atLeastOneFocusable', function() { | |
806 var results = document.querySelectorAll('#results-display [tabindex="0"]'); | |
807 expectGE(results.length, 1); | |
808 testDone(); | |
809 }); | |
810 | |
811 // Times out on Mac: http://crbug.com/336845 | |
812 TEST_F('HistoryWebUIRealBackendTest', | |
813 'DISABLED_deleteRemovesEntry', function() { | |
814 assertTrue(historyModel.deletingHistoryAllowed); | |
815 | |
816 var visit = document.querySelector('.entry').visit; | |
817 visit.titleLink.focus(); | |
818 assertEquals(visit.titleLink, document.activeElement); | |
819 | |
820 var deleteKey = new KeyboardEvent('keydown', | |
821 {bubbles: true, cancelable: true, key: 'Delete'}); | |
822 | |
823 assertFalse(historyModel.isDeletingVisits()); | |
824 expectFalse(visit.titleLink.dispatchEvent(deleteKey)); | |
825 expectTrue(historyModel.isDeletingVisits()); | |
826 | |
827 expectNotEquals(visit.dropDown, document.activeElement); | |
828 testDone(); | |
829 }); | |
830 | |
831 /** | |
832 * Test individual deletion of history entries. | |
833 */ | |
834 TEST_F('HistoryWebUIRealBackendTest', 'singleDeletion', function() { | |
835 // Deletes the history entry represented by |entryElement|, and calls callback | |
836 // when the deletion is complete. | |
837 var removeEntry = function(entryElement, callback) { | |
838 var dropDownButton = entryElement.querySelector('.drop-down'); | |
839 var removeMenuItem = $('remove-visit'); | |
840 | |
841 assertFalse(dropDownButton.disabled); | |
842 assertFalse(removeMenuItem.disabled); | |
843 | |
844 waitForCallback('onEntryRemoved', callback); | |
845 | |
846 cr.dispatchSimpleEvent(dropDownButton, 'mousedown'); | |
847 | |
848 var e = new Event('command', {bubbles: true}); | |
849 e.command = removeMenuItem.command; | |
850 removeMenuItem.dispatchEvent(e); | |
851 }; | |
852 | |
853 var secondTitle = document.querySelectorAll('.entry a')[1].textContent; | |
854 var thirdTitle = document.querySelectorAll('.entry a')[2].textContent; | |
855 | |
856 // historyDeleted() should not be called when deleting individual entries | |
857 // using the drop down. | |
858 waitForCallback('historyDeleted', function() { | |
859 testDone([false, 'historyDeleted() called when deleting single entry']); | |
860 }); | |
861 | |
862 expectEquals(2, document.querySelectorAll('.day').length); | |
863 | |
864 // Delete the first entry. The previous second entry should now be the first. | |
865 removeEntry(document.querySelector('.entry'), function() { | |
866 expectEquals(secondTitle, document.querySelector('.entry a').textContent); | |
867 | |
868 // After removing the first entry, its day header should also be gone. | |
869 expectEquals(1, document.querySelectorAll('.day').length); | |
870 | |
871 // Delete another entry. The original third entry should now be the first. | |
872 removeEntry(document.querySelector('.entry'), function() { | |
873 expectEquals(thirdTitle, document.querySelector('.entry a').textContent); | |
874 testDone(); | |
875 }); | |
876 }); | |
877 }); | |
878 | |
879 TEST_F('HistoryWebUIRealBackendTest', 'leftRightChangeFocus', function() { | |
880 var visit = document.querySelector('.entry').visit; | |
881 visit.titleLink.focus(); | |
882 assertEquals(visit.titleLink, document.activeElement); | |
883 | |
884 var right = new KeyboardEvent('keydown', | |
885 {bubbles: true, cancelable: true, key: 'ArrowRight'}); | |
886 expectFalse(visit.titleLink.dispatchEvent(right)); | |
887 | |
888 assertEquals(visit.dropDown, document.activeElement); | |
889 | |
890 var left = new KeyboardEvent('keydown', | |
891 {bubbles: true, cancelable: true, key: 'ArrowLeft'}); | |
892 expectFalse(visit.dropDown.dispatchEvent(left)); | |
893 | |
894 expectEquals(visit.titleLink, document.activeElement); | |
895 testDone(); | |
896 }); | |
897 | |
898 TEST_F('HistoryWebUIRealBackendTest', 'showConfirmDialogAndCancel', function() { | |
899 waitForCallback('deleteComplete', function() { | |
900 testDone([false, "history deleted when it shouldn't have been"]); | |
901 }); | |
902 | |
903 document.querySelector('input[type=checkbox]').click(); | |
904 $('remove-selected').click(); | |
905 | |
906 assertTrue($('alertOverlay').classList.contains('showing')); | |
907 assertFalse($('history-page').contains(document.activeElement)); | |
908 | |
909 var esc = new KeyboardEvent('keydown', | |
910 {bubbles: true, cancelable: true, key: 'Escape'}); | |
911 | |
912 document.documentElement.dispatchEvent(esc); | |
913 assertFalse($('alertOverlay').classList.contains('showing')); | |
914 | |
915 testDone(); | |
916 }); | |
917 | |
918 TEST_F('HistoryWebUIRealBackendTest', 'showConfirmDialogAndRemove', function() { | |
919 document.querySelector('input[type=checkbox]').click(); | |
920 $('remove-selected').click(); | |
921 | |
922 assertTrue($('alertOverlay').classList.contains('showing')); | |
923 assertFalse($('history-page').contains(document.activeElement)); | |
924 | |
925 waitForCallback('deleteComplete', testDone); | |
926 | |
927 var enter = new KeyboardEvent('keydown', | |
928 {bubbles: true, cancelable: true, key: 'Enter'}); | |
929 document.documentElement.dispatchEvent(enter); | |
930 assertFalse($('alertOverlay').classList.contains('showing')); | |
931 }); | |
932 | |
933 // Times out on Mac: http://crbug.com/336845 | |
934 TEST_F('HistoryWebUIRealBackendTest', | |
935 'DISABLED_menuButtonActivatesOneRow', function() { | |
936 var entries = document.querySelectorAll('.entry'); | |
937 assertEquals(3, entries.length); | |
938 assertTrue(entries[0].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
939 assertTrue($('action-menu').hidden); | |
940 | |
941 // Show the menu via mousedown on the menu button. | |
942 var menuButton = entries[2].querySelector('.menu-button'); | |
943 menuButton.dispatchEvent(new MouseEvent('mousedown')); | |
944 expectFalse($('action-menu').hidden); | |
945 | |
946 // Check that the active item has changed. | |
947 expectTrue(entries[2].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
948 expectFalse(entries[0].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
949 | |
950 testDone(); | |
951 }); | |
952 | |
953 // Flaky: http://crbug.com/527434 | |
954 TEST_F('HistoryWebUIRealBackendTest', | |
955 'DISABLED_shiftClickActivatesOneRow', function () { | |
956 var entries = document.querySelectorAll('.entry'); | |
957 assertEquals(3, entries.length); | |
958 assertTrue(entries[0].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
959 | |
960 entries[0].visit.checkBox.focus(); | |
961 assertEquals(entries[0].visit.checkBox, document.activeElement); | |
962 | |
963 entries[0].visit.checkBox.click(); | |
964 assertTrue(entries[0].visit.checkBox.checked); | |
965 | |
966 var entryBox = entries[2].querySelector('.entry-box'); | |
967 entryBox.dispatchEvent(new MouseEvent('click', {shiftKey: true})); | |
968 assertTrue(entries[1].visit.checkBox.checked); | |
969 | |
970 // Focus shouldn't have changed, but the checkbox should toggle. | |
971 expectEquals(entries[0].visit.checkBox, document.activeElement); | |
972 | |
973 expectTrue(entries[0].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
974 expectFalse(entries[2].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
975 | |
976 var shiftDown = new MouseEvent('mousedown', {shiftKey: true, bubbles: true}); | |
977 entries[2].visit.checkBox.dispatchEvent(shiftDown); | |
978 expectEquals(entries[2].visit.checkBox, document.activeElement); | |
979 | |
980 // 'focusin' events aren't dispatched while tests are run in batch (e.g. | |
981 // --test-launcher-jobs=2). Simulate this. TODO(dbeam): fix instead. | |
982 cr.dispatchSimpleEvent(document.activeElement, 'focusin', true, true); | |
983 | |
984 expectFalse(entries[0].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
985 expectTrue(entries[2].classList.contains(cr.ui.FocusRow.ACTIVE_CLASS)); | |
986 | |
987 testDone(); | |
988 }); | |
989 | |
990 /** | |
991 * Fixture for History WebUI testing when deletions are prohibited. | |
992 * @extends {HistoryWebUIRealBackendTest} | |
993 * @constructor | |
994 */ | |
995 function HistoryWebUIDeleteProhibitedTest() {} | |
996 | |
997 HistoryWebUIDeleteProhibitedTest.prototype = { | |
998 __proto__: HistoryWebUIRealBackendTest.prototype, | |
999 | |
1000 /** @override */ | |
1001 testGenPreamble: function() { | |
1002 HistoryWebUIRealBackendTest.prototype.testGenPreamble.call(this); | |
1003 GEN(' SetDeleteAllowed(false);'); | |
1004 }, | |
1005 }; | |
1006 | |
1007 // Test UI when removing entries is prohibited. | |
1008 // Times out on Mac: http://crbug.com/336845 | |
1009 TEST_F('HistoryWebUIDeleteProhibitedTest', | |
1010 'DISABLED_deleteProhibited', function() { | |
1011 // No checkboxes should be created. | |
1012 var checkboxes = document.querySelectorAll( | |
1013 '#results-display input[type=checkbox]'); | |
1014 expectEquals(0, checkboxes.length); | |
1015 | |
1016 // The "remove" button should be disabled. | |
1017 var removeButton = $('remove-selected'); | |
1018 expectTrue(removeButton.disabled); | |
1019 | |
1020 // The "Remove from history" drop-down item should be disabled. | |
1021 var removeVisit = $('remove-visit'); | |
1022 expectTrue(removeVisit.disabled); | |
1023 | |
1024 testDone(); | |
1025 }); | |
1026 | |
1027 TEST_F('HistoryWebUIDeleteProhibitedTest', 'atLeastOneFocusable', function() { | |
1028 var results = document.querySelectorAll('#results-display [tabindex="0"]'); | |
1029 expectGE(results.length, 1); | |
1030 testDone(); | |
1031 }); | |
1032 | |
1033 TEST_F('HistoryWebUIDeleteProhibitedTest', 'leftRightChangeFocus', function() { | |
1034 var visit = document.querySelector('.entry').visit; | |
1035 visit.titleLink.focus(); | |
1036 assertEquals(visit.titleLink, document.activeElement); | |
1037 | |
1038 var right = new KeyboardEvent('keydown', | |
1039 {bubbles: true, cancelable: true, key: 'ArrowRight'}); | |
1040 expectFalse(visit.titleLink.dispatchEvent(right)); | |
1041 | |
1042 assertEquals(visit.dropDown, document.activeElement); | |
1043 | |
1044 var left = new KeyboardEvent('keydown', | |
1045 {bubbles: true, cancelable: true, key: 'ArrowLeft'}); | |
1046 expectFalse(visit.dropDown.dispatchEvent(left)); | |
1047 | |
1048 expectEquals(visit.titleLink, document.activeElement); | |
1049 testDone(); | |
1050 }); | |
1051 | |
1052 TEST_F('HistoryWebUIDeleteProhibitedTest', 'deleteIgnored', function() { | |
1053 assertFalse(historyModel.deletingHistoryAllowed); | |
1054 | |
1055 var visit = document.querySelector('.entry').visit; | |
1056 visit.titleLink.focus(); | |
1057 assertEquals(visit.titleLink, document.activeElement); | |
1058 | |
1059 var deleteKey = new KeyboardEvent('keydown', | |
1060 {bubbles: true, cancelable: true, key: 'Delete'}); | |
1061 | |
1062 assertFalse(historyModel.isDeletingVisits()); | |
1063 expectTrue(visit.titleLink.dispatchEvent(deleteKey)); | |
1064 expectFalse(historyModel.isDeletingVisits()); | |
1065 | |
1066 expectEquals(visit.titleLink, document.activeElement); | |
1067 testDone(); | |
1068 }); | |
1069 | |
1070 /** | |
1071 * Fixture for History WebUI testing IDN. | |
1072 * @extends {BaseHistoryWebUITest} | |
1073 * @constructor | |
1074 */ | |
1075 function HistoryWebUIIDNTest() {} | |
1076 | |
1077 HistoryWebUIIDNTest.prototype = { | |
1078 __proto__: BaseHistoryWebUITest.prototype, | |
1079 | |
1080 /** @override */ | |
1081 testGenPreamble: function() { | |
1082 // Add some visits to the history database. | |
1083 GEN(' AddPageToHistory(0, "http://xn--d1abbgf6aiiy.xn--p1ai/",' + | |
1084 ' "Some");'); | |
1085 | |
1086 // Clear AcceptLanguages to get domain in unicode. | |
1087 GEN(' ClearAcceptLanguages();'); | |
1088 }, | |
1089 }; | |
1090 | |
1091 /** | |
1092 * Simple test that verifies that the correct entries are retrieved from the | |
1093 * history database and displayed in the UI. | |
1094 */ | |
1095 // Times out on Mac: http://crbug.com/336845 | |
1096 TEST_F('HistoryWebUIIDNTest', 'DISABLED_basic', function() { | |
1097 // Check that there is only one entry and domain is in unicode. | |
1098 assertEquals(1, document.querySelectorAll('.domain').length); | |
1099 assertEquals("\u043f\u0440\u0435\u0437\u0438\u0434\u0435\u043d\u0442." + | |
1100 "\u0440\u0444", document.querySelector('.domain').textContent); | |
1101 | |
1102 testDone(); | |
1103 }); | |
1104 | |
1105 /** | |
1106 * Fixture for a test that uses the real backend and tests how the history | |
1107 * page deals with odd schemes in URLs. | |
1108 * @extends {HistoryWebUIRealBackendTest} | |
1109 */ | |
1110 function HistoryWebUIWithSchemesTest() {} | |
1111 | |
1112 HistoryWebUIWithSchemesTest.prototype = { | |
1113 __proto__: HistoryWebUIRealBackendTest.prototype, | |
1114 | |
1115 /** @override */ | |
1116 testGenPreamble: function() { | |
1117 // Add a bunch of entries on the same day, including some weird schemes. | |
1118 GEN(' AddPageToHistory(12, "http://google.com", "Google");'); | |
1119 GEN(' AddPageToHistory(13, "file:///tmp/foo", "");'); | |
1120 GEN(' AddPageToHistory(14, "mailto:chromium@chromium.org", "");'); | |
1121 GEN(' AddPageToHistory(15, "tel:555123456", "");'); | |
1122 }, | |
1123 | |
1124 setUp: function() { | |
1125 // Show the filter controls as if the command line switch was active. | |
1126 $('top-container').hidden = true; | |
1127 $('history-page').classList.add('big-topbar-page'); | |
1128 $('filter-controls').hidden = false; | |
1129 expectFalse($('filter-controls').hidden); | |
1130 }, | |
1131 }; | |
1132 | |
1133 TEST_F('HistoryWebUIWithSchemesTest', 'groupingWithSchemes', function() { | |
1134 // Switch to the week view. | |
1135 $('timeframe-controls').querySelectorAll('input')[1].click(); | |
1136 waitForCallback('historyResult', function() { | |
1137 // Each URL should be organized under a different "domain". | |
1138 expectEquals(document.querySelectorAll('.entry').length, 4); | |
1139 expectEquals(document.querySelectorAll('.site-domain-wrapper').length, 4); | |
1140 testDone(); | |
1141 }); | |
1142 }); | |
OLD | NEW |