blob: ba076b2255217d7c567e84cf5a2e41034d2eae89 [file] [log] [blame]
/*
gpuBenchmarking finishes a gesture by sending a completion callback to the
renderer after the final input event. When the renderer receives the callback
it requests a new frame. This should flush all input through the system - and
DOM events should synchronously run here - and produce a compositor frame.
The callback is resolved when the frame is presented to the screen.
For methods in this file, the callback is the resolve method of the Promise
returned.
Example:
await mouseMoveTo(10,10);
The await returns after the mousemove event fired.
Note:
Given the event handler runs synchronous code, the await returns after
the event handler finished running.
*/
function waitForCompositorCommit() {
return new Promise((resolve) => {
if (window.testRunner) {
// After doing the composite, we also allow any async tasks to run before
// resolving, via setTimeout().
testRunner.updateAllLifecyclePhasesAndCompositeThen(() => {
window.setTimeout(resolve, 0);
});
} else {
// Fall back to just rAF twice.
window.requestAnimationFrame(() => {
window.requestAnimationFrame(resolve);
});
}
});
}
// Returns a promise that resolves when the given condition is met or rejects
// after 200 animation frames.
function waitFor(condition, error_message = 'Reaches the maximum frames.') {
const MAX_FRAME = 200;
return new Promise((resolve, reject) => {
function tick(frames) {
// We requestAnimationFrame either for 200 frames or until condition is
// met.
if (frames >= MAX_FRAME)
reject(error_message);
else if (condition())
resolve();
else
requestAnimationFrame(tick.bind(this, frames + 1));
}
tick(0);
});
}
// Returns a promise that only gets resolved when the condition is met.
function waitUntil(condition) {
return new Promise((resolve, reject) => {
function tick() {
if (condition())
resolve();
else
requestAnimationFrame(tick.bind(this));
}
tick();
});
}
// Returns a promise that resolves when the given condition holds for 10
// animation frames or rejects if the condition changes to false within 10
// animation frames.
function conditionHolds(condition, error_message = 'Condition is not true anymore.') {
const MAX_FRAME = 10;
return new Promise((resolve, reject) => {
function tick(frames) {
// We requestAnimationFrame either for 10 frames or until condition is
// violated.
if (frames >= MAX_FRAME)
resolve();
else if (!condition())
reject(error_message);
else
requestAnimationFrame(tick.bind(this, frames + 1));
}
tick(0);
});
}
// TODO: Frames are animated every 1ms for testing. It may be better to have the
// timeout based on time rather than frame count.
function waitForAnimationEnd(getValue, max_frame, max_unchanged_frame) {
const MAX_FRAME = max_frame;
const MAX_UNCHANGED_FRAME = max_unchanged_frame;
var last_changed_frame = 0;
var last_position = getValue();
return new Promise((resolve, reject) => {
function tick(frames) {
// We requestAnimationFrame either for MAX_FRAME or until
// MAX_UNCHANGED_FRAME with no change have been observed.
if (frames >= MAX_FRAME || frames - last_changed_frame > MAX_UNCHANGED_FRAME) {
resolve();
} else {
current_value = getValue();
if (last_position != current_value) {
last_changed_frame = frames;
last_position = current_value;
}
requestAnimationFrame(tick.bind(this, frames + 1));
}
}
tick(0);
})
}
// TODO(bokan): Replace uses of the above with this one.
function waitForAnimationEndTimeBased(getValue) {
// Give up if the animation still isn't done after this many milliseconds.
const TIMEOUT_MS = 1000;
// If the value is unchanged for this many milliseconds, we consider the
// animation ended and return.
const END_THRESHOLD_MS = 200;
const START_TIME = performance.now();
let last_changed_time = START_TIME;
let last_value = getValue();
return new Promise((resolve, reject) => {
function tick() {
let cur_time = performance.now();
if (cur_time - last_changed_time > END_THRESHOLD_MS) {
resolve();
return;
}
if (cur_time - START_TIME > TIMEOUT_MS) {
reject(new Error("Timeout waiting for animation to end"));
return;
}
let current_value = getValue();
if (last_value != current_value) {
last_changed_time = cur_time;
last_value = current_value;
}
requestAnimationFrame(tick);
}
tick();
})
}
function waitForScrollEvent(eventTarget) {
return new Promise((resolve, reject) => {
const scrollListener = () => {
eventTarget.removeEventListener('scroll', scrollListener);
resolve();
};
eventTarget.addEventListener('scroll', scrollListener);
});
}
// Event driven scroll promise. This method has the advantage over timing
// methods, as it is more forgiving to delays in event dispatch or between
// chained smooth scrolls. It has an additional advantage of completing sooner
// once the end condition is reached.
// The promise is resolved when the result of calling getValue matches the
// target value. The timeout timer starts once the first event has been
// received.
function waitForScrollEnd(eventTarget, getValue, targetValue) {
// Give up if the animation still isn't done after this many milliseconds from
// the time of the first scroll event.
const TIMEOUT_MS = 1000;
return new Promise((resolve, reject) => {
let timeout = undefined;
const scrollListener = () => {
if (!timeout)
timeout = setTimeout(reject, TIMEOUT_MS);
if (getValue() == targetValue) {
clearTimeout(timeout);
eventTarget.removeEventListener('scroll', scrollListener);
resolve();
}
};
if (getValue() == targetValue)
resolve();
else
eventTarget.addEventListener('scroll', scrollListener);
});
}
// Enums for gesture_source_type parameters in gpuBenchmarking synthetic
// gesture methods. Must match C++ side enums in synthetic_gesture_params.h
const GestureSourceType = (function() {
var isDefined = (window.chrome && chrome.gpuBenchmarking);
return {
DEFAULT_INPUT: isDefined && chrome.gpuBenchmarking.DEFAULT_INPUT,
TOUCH_INPUT: isDefined && chrome.gpuBenchmarking.TOUCH_INPUT,
MOUSE_INPUT: isDefined && chrome.gpuBenchmarking.MOUSE_INPUT,
TOUCHPAD_INPUT: isDefined && chrome.gpuBenchmarking.TOUCHPAD_INPUT,
PEN_INPUT: isDefined && chrome.gpuBenchmarking.PEN_INPUT,
ToString: function(value) {
if (!isDefined)
return 'Synthetic gestures unavailable';
switch (value) {
case chrome.gpuBenchmarking.DEFAULT_INPUT:
return 'DefaultInput';
case chrome.gpuBenchmarking.TOUCH_INPUT:
return 'Touchscreen';
case chrome.gpuBenchmarking.MOUSE_INPUT:
return 'MouseWheel/Touchpad';
case chrome.gpuBenchmarking.PEN_INPUT:
return 'Pen';
default:
return 'Invalid';
}
}
}
})();
// Enums used as input to the |modifier_keys| parameters of methods in this
// file like smoothScrollWithXY and wheelTick.
const Modifiers = (function() {
return {
ALT: "Alt",
CONTROL: "Control",
META: "Meta",
SHIFT: "Shift",
CAPSLOCK: "CapsLock",
NUMLOCK: "NumLock",
ALTGRAPH: "AltGraph",
}
})();
// Enums used as input to the |modifier_buttons| parameters of methods in this
// file like smoothScrollWithXY and wheelTick.
const Buttons = (function() {
return {
LEFT: "Left",
MIDDLE: "Middle",
RIGHT: "Right",
BACK: "Back",
FORWARD: "Forward",
}
})();
// Use this for speed to make gestures (effectively) instant. That is, finish
// entirely within one Begin|Update|End triplet. This is in physical
// pixels/second.
// TODO(bokan): This isn't really instant but high enough that it works for
// current purposes. This should be replaced with the Infinity value and
// the synthetic gesture code modified to guarantee the single update behavior.
// https://crbug.com/893608
const SPEED_INSTANT = 400000;
// This will be replaced by smoothScrollWithXY.
function smoothScroll(pixels_to_scroll, start_x, start_y, gesture_source_type,
direction, speed_in_pixels_s, precise_scrolling_deltas,
scroll_by_page, cursor_visible, scroll_by_percentage,
modifier_keys) {
let pixels_to_scroll_x = 0;
let pixels_to_scroll_y = 0;
if (direction == "down") {
pixels_to_scroll_y = pixels_to_scroll;
} else if (direction == "up") {
pixels_to_scroll_y = -pixels_to_scroll;
} else if (direction == "right") {
pixels_to_scroll_x = pixels_to_scroll;
} else if (direction == "left") {
pixels_to_scroll_x = -pixels_to_scroll;
} else if (direction == "upleft") {
pixels_to_scroll_x = -pixels_to_scroll;
pixels_to_scroll_y = -pixels_to_scroll;
} else if (direction == "upright") {
pixels_to_scroll_x = pixels_to_scroll;
pixels_to_scroll_y = -pixels_to_scroll;
} else if (direction == "downleft") {
pixels_to_scroll_x = -pixels_to_scroll;
pixels_to_scroll_y = pixels_to_scroll;
} else if (direction == "downright") {
pixels_to_scroll_x = pixels_to_scroll;
pixels_to_scroll_y = pixels_to_scroll;
}
return smoothScrollWithXY(pixels_to_scroll_x, pixels_to_scroll_y, start_x,
start_y, gesture_source_type, speed_in_pixels_s,
precise_scrolling_deltas, scroll_by_page,
cursor_visible, scroll_by_percentage, modifier_keys);
}
// Perform a percent based scroll using smoothScrollWithXY
function percentScroll(percent_to_scroll_x, percent_to_scroll_y, start_x, start_y, gesture_source_type) {
return smoothScrollWithXY(percent_to_scroll_x, percent_to_scroll_y, start_x, start_y,
gesture_source_type,
undefined /* speed_in_pixels_s - not defined for percent based scrolls */,
false /* precise_scrolling_deltas */,
false /* scroll_by_page */,
true /* cursor_visible */,
true /* scroll_by_percentage */);
}
// modifier_keys means the keys pressed while doing the mouse wheel scroll, it
// should be one of the values in the |Modifiers| or a comma separated string
// to specify multiple values.
// modifier_buttons means the mouse buttons pressed while doing the mouse wheel
// scroll, it should be one of the values in the |Buttons| or a comma separated
// string to specify multiple values.
function smoothScrollWithXY(pixels_to_scroll_x, pixels_to_scroll_y, start_x,
start_y, gesture_source_type, speed_in_pixels_s,
precise_scrolling_deltas, scroll_by_page,
cursor_visible, scroll_by_percentage, modifier_keys,
modifier_buttons) {
return new Promise((resolve, reject) => {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.smoothScrollByXY(pixels_to_scroll_x,
pixels_to_scroll_y,
resolve,
start_x,
start_y,
gesture_source_type,
speed_in_pixels_s,
precise_scrolling_deltas,
scroll_by_page,
cursor_visible,
scroll_by_percentage,
modifier_keys,
modifier_buttons);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
// modifier_keys means the keys pressed while doing the mouse wheel scroll, it
// should be one of the values in the |Modifiers| or a comma separated string
// to specify multiple values.
// modifier_buttons means the mouse buttons pressed while doing the mouse wheel
// scroll, it should be one of the values in the |Buttons| or a comma separated
// string to specify multiple values.
function wheelTick(scroll_tick_x, scroll_tick_y, center, speed_in_pixels_s,
modifier_keys, modifier_buttons) {
if (typeof(speed_in_pixels_s) == "undefined")
speed_in_pixels_s = SPEED_INSTANT;
// Do not allow precise scrolling deltas for tick wheel scroll.
return smoothScrollWithXY(scroll_tick_x * pixelsPerTick(),
scroll_tick_y * pixelsPerTick(),
center.x, center.y, GestureSourceType.MOUSE_INPUT,
speed_in_pixels_s, false /* precise_scrolling_deltas */,
false /* scroll_by_page */, true /* cursor_visible */,
false /* scroll_by_percentage */, modifier_keys,
modifier_buttons);
}
const LEGACY_MOUSE_WHEEL_TICK_MULTIPLIER = 120;
// Returns the number of pixels per wheel tick which is a platform specific value.
function pixelsPerTick() {
// Comes from ui/events/event.cc
if (navigator.platform.indexOf("Win") != -1)
return 120;
if (navigator.platform.indexOf("Mac") != -1 || navigator.platform.indexOf("iPhone") != -1 ||
navigator.platform.indexOf("iPod") != -1 || navigator.platform.indexOf("iPad") != -1) {
return 40;
}
// Some android devices return android while others return Android.
if (navigator.platform.toLowerCase().indexOf("android") != -1)
return 64;
// Comes from ui/events/event.cc
return 53;
}
// Note: unlike other functions in this file, the |direction| parameter here is
// the "finger direction". This means |y| pixels "up" causes the finger to move
// up so the page scrolls down (i.e. scrollTop increases).
function swipe(pixels_to_scroll, start_x, start_y, direction, speed_in_pixels_s, fling_velocity, gesture_source_type) {
return new Promise((resolve, reject) => {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.swipe(direction,
pixels_to_scroll,
resolve,
start_x,
start_y,
speed_in_pixels_s,
fling_velocity,
gesture_source_type);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
function pinchBy(scale, centerX, centerY, speed_in_pixels_s, gesture_source_type) {
return new Promise((resolve, reject) => {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.pinchBy(scale,
centerX,
centerY,
resolve,
speed_in_pixels_s,
gesture_source_type);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
function mouseMoveTo(xPosition, yPosition) {
return new Promise(function(resolve, reject) {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.pointerActionSequence([
{source: 'mouse',
actions: [
{ name: 'pointerMove', x: xPosition, y: yPosition },
]}], resolve);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
function mouseDownAt(xPosition, yPosition) {
return new Promise(function(resolve, reject) {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.pointerActionSequence([
{source: 'mouse',
actions: [
{ name: 'pointerDown', x: xPosition, y: yPosition },
]}], resolve);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
function mouseUpAt(xPosition, yPosition) {
return new Promise(function(resolve, reject) {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.pointerActionSequence([
{source: 'mouse',
actions: [
{ name: 'pointerMove', x: xPosition, y: yPosition },
{ name: 'pointerUp' },
]}], resolve);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
// Simulate a mouse click on point.
function mouseClickOn(x, y, button = 0 /* left */, keys = '') {
return new Promise((resolve, reject) => {
if (window.chrome && chrome.gpuBenchmarking) {
let pointerActions = [{
source: 'mouse',
actions: [
{ 'name': 'pointerMove', 'x': x, 'y': y },
{ 'name': 'pointerDown', 'x': x, 'y': y, 'button': button, 'keys': keys },
{ 'name': 'pointerUp', 'button': button },
]
}];
chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
// Simulate a mouse double click on point.
function mouseDoubleClickOn(x, y, button = 0 /* left */, keys = '') {
return new Promise((resolve, reject) => {
if (window.chrome && chrome.gpuBenchmarking) {
let pointerActions = [{
source: 'mouse',
actions: [
{ 'name': 'pointerMove', 'x': x, 'y': y },
{ 'name': 'pointerDown', 'x': x, 'y': y, 'button': button, 'keys': keys },
{ 'name': 'pointerUp', 'button': button },
{ 'name': 'pointerDown', 'x': x, 'y': y, 'button': button, 'keys': keys },
{ 'name': 'pointerUp', 'button': button },
]
}];
chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
// Simulate a mouse press on point for a certain time.
function mousePressOn(x, y, t) {
return new Promise((resolve, reject) => {
if (window.chrome && chrome.gpuBenchmarking) {
let pointerActions = [{
source: 'mouse',
actions: [
{ 'name': 'pointerMove', 'x': x, 'y': y },
{ 'name': 'pointerDown', 'x': x, 'y': y },
{ 'name': 'pause', duration: t},
{ 'name': 'pointerUp' },
]
}];
chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
// Simulate a mouse drag and drop. mouse down at {start_x, start_y}, move to
// {end_x, end_y} and release.
function mouseDragAndDrop(start_x, start_y, end_x, end_y, button = 0 /* left */, t = 0) {
return new Promise((resolve, reject) => {
if (window.chrome && chrome.gpuBenchmarking) {
let pointerActions = [{
source: 'mouse',
actions: [
{ 'name': 'pointerMove', 'x': start_x, 'y': start_y },
{ 'name': 'pointerDown', 'x': start_x, 'y': start_y, 'button': button },
{ 'name': 'pause', 'duration': t},
{ 'name': 'pointerMove', 'x': end_x, 'y': end_y },
{ 'name': 'pause', 'duration': t},
{ 'name': 'pointerUp', 'button': button },
]
}];
chrome.gpuBenchmarking.pointerActionSequence(pointerActions, resolve);
} else {
reject('This test requires chrome.gpuBenchmarking');
}
});
}
// Helper functions used in some of the gesture scroll layouttests.
function recordScroll() {
scrollEventsOccurred++;
}
function notScrolled() {
return scrolledElement.scrollTop == 0 && scrolledElement.scrollLeft == 0;
}
function checkScrollOffset() {
// To avoid flakiness up to two pixels off per gesture is allowed.
var pixels = 2 * (gesturesOccurred + 1);
var result = approx_equals(scrolledElement.scrollTop, scrollAmountY[gesturesOccurred], pixels) &&
approx_equals(scrolledElement.scrollLeft, scrollAmountX[gesturesOccurred], pixels);
if (result)
gesturesOccurred++;
return result;
}
// This promise gets resolved in iframe onload.
var iframeLoadResolve;
iframeOnLoadPromise = new Promise(function(resolve) {
iframeLoadResolve = resolve;
});
// Include run-after-layout-and-paint.js to use this promise.
function WaitForlayoutAndPaint() {
return new Promise((resolve, reject) => {
if (typeof runAfterLayoutAndPaint !== 'undefined')
runAfterLayoutAndPaint(resolve);
else
reject('This test requires run-after-layout-and-paint.js');
});
}
function touchTapOn(xPosition, yPosition) {
return new Promise(function(resolve, reject) {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.pointerActionSequence( [
{source: 'touch',
actions: [
{ name: 'pointerDown', x: xPosition, y: yPosition },
{ name: 'pointerUp' }
]}], resolve);
} else {
reject();
}
});
}
function touchDragTo(drag) {
const PREVENT_FLING_PAUSE = 40;
return new Promise(function(resolve, reject) {
if (window.chrome && chrome.gpuBenchmarking) {
chrome.gpuBenchmarking.pointerActionSequence( [
{source: 'touch',
actions: [
{ name: 'pointerDown', x: drag.start_x, y: drag.start_y },
{ name: 'pause', duration: PREVENT_FLING_PAUSE },
{ name: 'pointerMove', x: drag.end_x, y: drag.end_y},
{ name: 'pause', duration: PREVENT_FLING_PAUSE },
{ name: 'pointerUp', x: drag.end_x, y: drag.end_y }
]}], resolve);
} else {
reject();
}
});
}
function doubleTapAt(xPosition, yPosition) {
// This comes from config constants in gesture_detector.cc.
const DOUBLE_TAP_MINIMUM_DURATION_MS = 40;
return new Promise(function(resolve, reject) {
if (!window.chrome || !chrome.gpuBenchmarking) {
reject("chrome.gpuBenchmarking not found.");
return;
}
chrome.gpuBenchmarking.pointerActionSequence( [{
source: 'touch',
actions: [
{ name: 'pointerDown', x: xPosition, y: yPosition },
{ name: 'pointerUp' },
{ name: 'pause', duration: DOUBLE_TAP_MINIMUM_DURATION_MS },
{ name: 'pointerDown', x: xPosition, y: yPosition },
{ name: 'pointerUp' }
]
}], resolve);
});
}
function approx_equals(actual, expected, epsilon) {
return actual >= expected - epsilon && actual <= expected + epsilon;
}
function clientToViewport(client_point) {
const viewport_point = {
x: (client_point.x - visualViewport.offsetLeft) * visualViewport.scale,
y: (client_point.y - visualViewport.offsetTop) * visualViewport.scale
};
return viewport_point;
}
// Returns the center point of the given element's rect in visual viewport
// coordinates. Returned object is a point with |x| and |y| properties.
function elementCenter(element) {
const rect = element.getBoundingClientRect();
const center_point = {
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2
};
return clientToViewport(center_point);
}
// Returns a position in the given element with an offset of |x| and |y| from
// the element's top-left position. Returned object is a point with |x| and |y|
// properties. The returned coordinate is in visual viewport coordinates.
function pointInElement(element, x, y) {
const rect = element.getBoundingClientRect();
const point = {
x: rect.x + x,
y: rect.y + y
};
return clientToViewport(point);
}
// Waits for 'time' ms before resolving the promise.
function waitForMs(time) {
return new Promise((resolve) => {
window.setTimeout(function() { resolve(); }, time);
});
}
// Requests an animation frame.
function raf() {
return new Promise((resolve) => {
requestAnimationFrame(() => {
resolve();
});
});
}