// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {MarginsSetting, PrintPreviewMarginControlContainerElement, PrintPreviewMarginControlElement, PrintPreviewModelElement, Settings} from 'chrome://print/print_preview.js';
import {CustomMarginsOrientation, Margins, MarginsType, MeasurementSystem, MeasurementSystemUnitType, Size, State} from 'chrome://print/print_preview.js';
import {assertNotReached} from 'chrome://resources/js/assert.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {fakeDataBind} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';
suite('CustomMarginsTest', function() {
let container: PrintPreviewMarginControlContainerElement;
let model: PrintPreviewModelElement;
let sides: CustomMarginsOrientation[] = [];
let measurementSystem: MeasurementSystem;
const pixelsPerInch: number = 100;
const pointsPerInch: number = 72.0;
const defaultMarginPts: number = 36; // 0.5 inch
// Keys for the custom margins setting, in order.
const keys: string[] =
['marginTop', 'marginRight', 'marginBottom', 'marginLeft'];
setup(function() {
document.body.innerHTML = window.trustedTypes!.emptyHTML;
measurementSystem =
new MeasurementSystem(',', '.', MeasurementSystemUnitType.IMPERIAL);
model = document.createElement('print-preview-model');
document.body.appendChild(model);
model.set('settings.mediaSize.available', true);
sides = [
CustomMarginsOrientation.TOP,
CustomMarginsOrientation.RIGHT,
CustomMarginsOrientation.BOTTOM,
CustomMarginsOrientation.LEFT,
];
container =
document.createElement('print-preview-margin-control-container');
container.previewLoaded = false;
// 8.5 x 11, in points
container.pageSize = new Size(612, 794);
container.documentMargins = new Margins(
defaultMarginPts, defaultMarginPts, defaultMarginPts, defaultMarginPts);
container.state = State.NOT_READY;
});
function getControls(): PrintPreviewMarginControlElement[] {
return Array.from(
container.shadowRoot!.querySelectorAll('print-preview-margin-control'));
}
/*
* Completes setup of the test by setting the settings and adding the
* container to the document body.
* @return {!Promise} Promise that resolves when all controls have been
* added and initialization is complete.
*/
function finishSetup() {
// Wait for the control elements to be created before updating the state.
container.measurementSystem = measurementSystem;
document.body.appendChild(container);
const controlsAdded = eventToPromise('dom-change', container);
return controlsAdded.then(() => {
// 8.5 x 11, in pixels
const controls = getControls();
assertEquals(4, controls.length);
container.settings = model.settings;
fakeDataBind(model, container, 'settings');
container.state = State.READY;
container.updateClippingMask(new Size(850, 1100));
container.updateScaleTransform(pixelsPerInch / pointsPerInch);
container.previewLoaded = true;
flush();
});
}
/**
* @return Promise that resolves when transitionend has fired
* for all of the controls.
*/
function getAllTransitions(controls: PrintPreviewMarginControlElement[]):
Promise<any[]> {
return Promise.all(
controls.map(control => eventToPromise('transitionend', control)));
}
/**
* Simulates dragging the margin control.
* @param control The control to move.
* @param start The starting position for the control in pixels.
* @param end The ending position for the control in pixels.
*/
function dragControl(
control: PrintPreviewMarginControlElement, start: number, end: number) {
if (window.getComputedStyle(control).getPropertyValue('pointer-events') ===
'none') {
return;
}
let xStart = 0;
let yStart = 0;
let xEnd = 0;
let yEnd = 0;
switch (control.side) {
case CustomMarginsOrientation.TOP:
yStart = start;
yEnd = end;
break;
case CustomMarginsOrientation.RIGHT:
xStart = control.clipSize!.width - start;
xEnd = control.clipSize!.width - end;
break;
case CustomMarginsOrientation.BOTTOM:
yStart = control.clipSize!.height - start;
yEnd = control.clipSize!.height - end;
break;
case CustomMarginsOrientation.LEFT:
xStart = start;
xEnd = end;
break;
default:
assertNotReached();
}
// Simulate events in the same order they are fired by the browser.
// Need to provide a valid |pointerId| for setPointerCapture() to not
// throw an error.
control.dispatchEvent(new PointerEvent(
'pointerdown', {pointerId: 1, clientX: xStart, clientY: yStart}));
control.dispatchEvent(new PointerEvent(
'pointermove', {pointerId: 1, clientX: xEnd, clientY: yEnd}));
control.dispatchEvent(new PointerEvent(
'pointerup', {pointerId: 1, clientX: xEnd, clientY: yEnd}));
}
/**
* Tests setting the margin control with its textbox.
* @param control The control.
* @param key The control's key in the custom margin setting.
* @param currentValuePts The current margin value in points.
* @param input The new textbox input for the margin.
* @param invalid Whether the new value is invalid.
* @param newValuePts the new margin value in pts. If not
* specified, computes the value assuming it is in bounds and assuming
* the default measurement system.
* @return Promise that resolves when the test is complete.
*/
function testControlTextbox(
control: PrintPreviewMarginControlElement, key: string,
currentValuePts: number, input: string, invalid: boolean,
newValuePts?: number): Promise<void> {
if (newValuePts === undefined) {
newValuePts = invalid ? currentValuePts :
Math.round(parseFloat(input) * pointsPerInch);
}
assertEquals(
currentValuePts, container.getSettingValue('customMargins')[key]);
const controlTextbox = control.$.input;
controlTextbox.value = input;
controlTextbox.dispatchEvent(
new CustomEvent('input', {composed: true, bubbles: true}));
if (!invalid) {
return eventToPromise('text-change', control).then(() => {
assertEquals(
newValuePts, container.getSettingValue('customMargins')[key]);
assertFalse(control.invalid);
});
} else {
return eventToPromise('input-change', control).then(() => {
assertTrue(control.invalid);
});
}
}
/*
* Initializes the settings custom margins to some test values, and returns
* a map with the values.
*/
function setupCustomMargins(): Map<CustomMarginsOrientation, number> {
const orientationEnum = CustomMarginsOrientation;
const marginValues = new Map([
[orientationEnum.TOP, 72],
[orientationEnum.RIGHT, 36],
[orientationEnum.BOTTOM, 108],
[orientationEnum.LEFT, 18],
]);
model.settings.customMargins.value = {
marginTop: marginValues.get(orientationEnum.TOP),
marginRight: marginValues.get(orientationEnum.RIGHT),
marginBottom: marginValues.get(orientationEnum.BOTTOM),
marginLeft: marginValues.get(orientationEnum.LEFT),
};
return marginValues;
}
/*
* Tests that the custom margins and margin value are cleared when the
* setting |settingName| is set to have value |newValue|.
* @param settingName The name of the setting to check.
* @param newValue The value to set the setting to.
* @return Promise that resolves when the check is complete.
*/
function validateMarginsClearedForSetting(
settingName: keyof Settings, newValue: any) {
const marginValues = setupCustomMargins();
return finishSetup().then(() => {
// Simulate setting custom margins.
model.set('settings.margins.value', MarginsType.CUSTOM);
// Validate control positions are set based on the custom values.
const controls = getControls();
controls.forEach((control, index) => {
const side = sides[index]!;
assertEquals(side, control.side);
assertEquals(marginValues.get(side), control.getPositionInPts());
});
// Simulate setting the media size.
container.setSetting(settingName, newValue);
container.previewLoaded = false;
// Custom margins values should be cleared.
assertEquals(
'{}', JSON.stringify(container.getSettingValue('customMargins')));
// The margins-settings element will also set the margins type to DEFAULT.
model.set('settings.margins.value', MarginsType.DEFAULT);
// When preview loads, custom margins should still be empty, since
// custom margins are not selected. We do not want to set the sticky
// values until the user has selected custom margins.
container.previewLoaded = true;
assertEquals(
'{}', JSON.stringify(container.getSettingValue('customMargins')));
});
}
// Test that controls correctly appear when custom margins are selected and
// disappear when the preview is loading.
test('ControlsCheck', function() {
const getCustomMarginsValue = function(): MarginsSetting {
return container.getSettingValue('customMargins') as MarginsSetting;
};
return finishSetup()
.then(() => {
const controls = getControls();
assertEquals(4, controls.length);
// Controls are not visible when margin type DEFAULT is selected.
controls.forEach(control => {
assertEquals('0', window.getComputedStyle(control).opacity);
});
const onTransitionEnd = getAllTransitions(controls);
// Controls become visible when margin type CUSTOM is selected.
model.set('settings.margins.value', MarginsType.CUSTOM);
// Wait for the opacity transitions to finish.
return onTransitionEnd;
})
.then(function() {
// Verify margins are correctly set based on previous value.
assertEquals(defaultMarginPts, getCustomMarginsValue().marginTop);
assertEquals(defaultMarginPts, getCustomMarginsValue().marginLeft);
assertEquals(defaultMarginPts, getCustomMarginsValue().marginRight);
assertEquals(defaultMarginPts, getCustomMarginsValue().marginBottom);
// Verify there is one control for each side and that controls are
// visible and positioned correctly.
const controls = getControls();
controls.forEach((control, index) => {
assertFalse(control.invisible);
assertFalse(control.disabled);
assertEquals('1', window.getComputedStyle(control).opacity);
assertEquals(sides[index], control.side);
assertEquals(defaultMarginPts, control.getPositionInPts());
});
const onTransitionEnd = getAllTransitions(controls);
// Disappears when preview is loading or an error message is shown.
// Check that all the controls also disappear.
container.previewLoaded = false;
// Wait for the opacity transitions to finish.
return onTransitionEnd;
})
.then(function() {
const controls = getControls();
controls.forEach(control => {
assertEquals('0', window.getComputedStyle(control).opacity);
assertTrue(control.invisible);
assertTrue(control.disabled);
});
});
});
// Tests that the margin controls can be correctly set from the sticky
// settings.
test('SetFromStickySettings', function() {
return finishSetup().then(() => {
const controls = getControls();
// Simulate setting custom margins from sticky settings.
model.set('settings.margins.value', MarginsType.CUSTOM);
const marginValues = setupCustomMargins();
model.notifyPath('settings.customMargins.value');
flush();
// Validate control positions have been updated.
controls.forEach((control, index) => {
const side = sides[index]!;
assertEquals(side, control.side);
assertEquals(marginValues.get(side), control.getPositionInPts());
});
});
});
// Test that dragging margin controls updates the custom margins setting.
test('DragControls', function() {
/**
* Tests that the control can be moved from its current position (assumed
* to be the default margins) to newPositionInPts by dragging it.
* @param {!PrintPreviewMarginControlElement} control The control to test.
* @param {number} index The index of this control in the container's list
* of controls.
* @param {number} newPositionInPts The new position in points.
*/
const testControl = function(
control: PrintPreviewMarginControlElement, index: number,
newPositionInPts: number): Promise<void> {
const oldValue =
container.getSettingValue('customMargins') as {[k: string]: number};
assertEquals(defaultMarginPts, oldValue[keys[index]!]);
// Compute positions in pixels.
const oldPositionInPixels =
defaultMarginPts * pixelsPerInch / pointsPerInch;
const newPositionInPixels =
newPositionInPts * pixelsPerInch / pointsPerInch;
const whenDragChanged = eventToPromise('margin-drag-changed', container);
dragControl(control, oldPositionInPixels, newPositionInPixels);
return whenDragChanged.then(function() {
const newValue = container.getSettingValue('customMargins');
assertEquals(newPositionInPts, newValue[keys[index]!]);
});
};
return finishSetup().then(() => {
const controls = getControls();
model.set('settings.margins.value', MarginsType.CUSTOM);
flush();
// Wait for an animation frame. The position of the controls is set in
// an animation frame, and this needs to be initialized before dragging
// the control so that the computation of the new location is performed
// with the correct initial margin offset.
// Set all controls to 108 = 1.5" in points.
window.requestAnimationFrame(function() {
return testControl(controls[0]!, 0, 108)
.then(() => testControl(controls[1]!, 1, 108))
.then(() => testControl(controls[2]!, 2, 108))
.then(() => testControl(controls[3]!, 3, 108));
});
});
});
/**
* @param currentValue Current margin value in pts
* @param input String to set in margin textboxes
* @param invalid Whether the string is invalid
* @param newValuePts the new margin value in pts. If not
* specified, computes the value assuming it is in bounds and assuming
* the default measurement system.
* @return Promise that resolves when all controls have been
* tested.
*/
function testAllTextboxes(
controls: PrintPreviewMarginControlElement[], currentValue: number,
input: string, invalid: boolean, newValuePts?: number): Promise<void> {
return testControlTextbox(
controls[0]!, keys[0]!, currentValue, input, invalid,
newValuePts)
.then(
() => testControlTextbox(
controls[1]!, keys[1]!, currentValue, input, invalid,
newValuePts))
.then(
() => testControlTextbox(
controls[2]!, keys[2]!, currentValue, input, invalid,
newValuePts))
.then(
() => testControlTextbox(
controls[3]!, keys[3]!, currentValue, input, invalid,
newValuePts));
}
// Test that setting the margin controls with their textbox inputs updates
// the custom margins setting.
test(
'SetControlsWithTextbox', function() {
return finishSetup().then(() => {
const controls = getControls();
// Set a shorter delay for testing so the test doesn't take too
// long.
controls.forEach(c => {
c.getInput().setAttribute('data-timeout-delay', '1');
});
model.set('settings.margins.value', MarginsType.CUSTOM);
flush();
// Verify entering a new value updates the settings.
// Then verify entering an invalid value invalidates the control
// and does not update the settings.
const value1 = '1.75'; // 1.75 inches
const newMargin1 = Math.round(parseFloat(value1) * pointsPerInch);
const value2 = '.6';
const newMargin2 = Math.round(parseFloat(value2) * pointsPerInch);
const value3 = '2'; // 2 inches
const newMargin3 = Math.round(parseFloat(value3) * pointsPerInch);
const maxTopMargin = container.pageSize.height - newMargin3 -
72 /* MINIMUM_DISTANCE, see margin_control.js */;
return testAllTextboxes(controls, defaultMarginPts, value1, false)
.then(() => testAllTextboxes(controls, newMargin1, 'abc', true))
.then(
() => testAllTextboxes(controls, newMargin1, '1.2abc', true))
.then(
() => testAllTextboxes(controls, newMargin1, '1. 2', true))
.then(() => testAllTextboxes(controls, newMargin1, '.', true))
.then(() => testAllTextboxes(controls, newMargin1, value2, false))
.then(() => testAllTextboxes(controls, newMargin2, value3, false))
.then(
() => testControlTextbox(
controls[0]!, keys[0]!, newMargin3, '100', false,
maxTopMargin))
.then(
() => testControlTextbox(
controls[0]!, keys[0]!, maxTopMargin, '1,000', false,
maxTopMargin));
});
});
// Test that setting the margin controls with their textbox inputs updates
// the custom margins setting, using a metric measurement system with a ','
// as the decimal delimiter and '.' as the thousands delimiter. Regression
// test for https://crbug.com/1005816.
test(
'SetControlsWithTextboxMetric', function() {
measurementSystem =
new MeasurementSystem('.', ',', MeasurementSystemUnitType.METRIC);
return finishSetup().then(() => {
const controls = getControls();
// Set a shorter delay for testing so the test doesn't take too
// long.
controls.forEach(c => {
c.getInput().setAttribute('data-timeout-delay', '1');
});
model.set('settings.margins.value', MarginsType.CUSTOM);
flush();
// Verify entering a new value updates the settings.
// Then verify entering an invalid value invalidates the control
// and does not update the settings.
const pointsPerMM = pointsPerInch / 25.4;
const newMargin1 = '50,0';
const newMargin1Pts = Math.round(50 * pointsPerMM);
const newMargin2 = ',9';
const newMargin2Pts = Math.round(.9 * pointsPerMM);
const newMargin3 = '60';
const newMargin3Pts = Math.round(60 * pointsPerMM);
const maxTopMargin = container.pageSize.height - newMargin3Pts -
72 /* MINIMUM_DISTANCE, see margin_control.js */;
return testAllTextboxes(
controls, defaultMarginPts, newMargin1, false,
newMargin1Pts)
.then(
() => testAllTextboxes(
controls, newMargin1Pts, 'abc', true, newMargin1Pts))
.then(
() => testAllTextboxes(
controls, newMargin1Pts, '50,2abc', true, newMargin1Pts))
.then(
() => testAllTextboxes(
controls, newMargin1Pts, '10, 2', true, newMargin1Pts))
.then(
() => testAllTextboxes(
controls, newMargin1Pts, ',', true, newMargin1Pts))
.then(
() => testAllTextboxes(
controls, newMargin1Pts, newMargin2, false,
newMargin2Pts))
.then(
() => testAllTextboxes(
controls, newMargin2Pts, newMargin3, false,
newMargin3Pts))
.then(
() => testControlTextbox(
controls[0]!, keys[0]!, newMargin3Pts, '1.000.000', false,
maxTopMargin))
.then(
() => testControlTextbox(
controls[0]!, keys[0]!, maxTopMargin, '1.000', false,
maxTopMargin));
});
});
// Test that if there is a custom margins sticky setting, it is restored
// when margin setting changes.
test(
'RestoreStickyMarginsAfterDefault', function() {
const marginValues = setupCustomMargins();
return finishSetup().then(() => {
// Simulate setting custom margins.
const controls = getControls();
model.set('settings.margins.value', MarginsType.CUSTOM);
// Validate control positions are set based on the custom values.
controls.forEach((control, index) => {
const side = sides[index]!;
assertEquals(side, control.side);
assertEquals(marginValues.get(side), control.getPositionInPts());
});
// Simulate setting minimum margins.
model.set('settings.margins.value', MarginsType.MINIMUM);
// Validate control positions still reflect the custom values.
controls.forEach((control, index) => {
const side = sides[index]!;
assertEquals(side, control.side);
assertEquals(marginValues.get(side), control.getPositionInPts());
});
});
});
// Test that if the media size changes, the custom margins are cleared.
test(
'MediaSizeClearsCustomMargins', function() {
return validateMarginsClearedForSetting(
'mediaSize', {height_microns: 200000, width_microns: 200000})
.then(() => {
// Simulate setting custom margins again.
model.set('settings.margins.value', MarginsType.CUSTOM);
// Validate control positions are initialized based on the default
// values.
const controls = getControls();
controls.forEach((control, index) => {
const side = sides[index];
assertEquals(side, control.side);
assertEquals(defaultMarginPts, control.getPositionInPts());
});
});
});
// Test that if the orientation changes, the custom margins are cleared.
test(
'LayoutClearsCustomMargins', function() {
return validateMarginsClearedForSetting('layout', true).then(() => {
// Simulate setting custom margins again
model.set('settings.margins.value', MarginsType.CUSTOM);
// Validate control positions are initialized based on the default
// values.
const controls = getControls();
controls.forEach((control, index) => {
const side = sides[index];
assertEquals(side, control.side);
assertEquals(defaultMarginPts, control.getPositionInPts());
});
});
});
// Test that if the margins are not available, the custom margins setting is
// not updated based on the document margins - i.e. PDFs do not change the
// custom margins state.
test(
'IgnoreDocumentMarginsFromPDF', function() {
model.set('settings.margins.available', false);
return finishSetup().then(() => {
assertEquals(
'{}', JSON.stringify(container.getSettingValue('customMargins')));
});
});
// Test that if margins are not available but the user changes the media
// size, the custom margins are cleared.
test('MediaSizeClearsCustomMarginsPDF', function() {
model.set('settings.margins.available', false);
return validateMarginsClearedForSetting(
'mediaSize', {height_microns: 200000, width_microns: 200000});
});
function whenAnimationFrameDone() {
return new Promise(resolve => window.requestAnimationFrame(resolve));
}
// Test that if the user focuses a textbox that is not visible, the
// text-focus event is fired with the correct values to scroll by.
test(
'RequestScrollToOutOfBoundsTextbox', function() {
return finishSetup()
.then(() => {
// Wait for the controls to be set up, which occurs in an
// animation frame.
return whenAnimationFrameDone();
})
.then(() => {
const onTransitionEnd = getAllTransitions(getControls());
// Controls become visible when margin type CUSTOM is selected.
model.set('settings.margins.value', MarginsType.CUSTOM);
container.notifyPath('settings.customMargins.value');
flush();
return onTransitionEnd;
})
.then(() => {
// Zoom in by 2x, so that some margin controls will not be
// visible.
container.updateScaleTransform(pixelsPerInch * 2 / pointsPerInch);
flush();
return whenAnimationFrameDone();
})
.then(() => {
const controls = getControls();
assertEquals(4, controls.length);
// Focus the bottom control, which is currently not visible since
// the viewer is showing only the top left quarter of the page.
const bottomControl = controls[2]!;
const whenEventFired =
eventToPromise('text-focus-position', container);
bottomControl.$.input.focus();
// Workaround for mac so that this does not need to be an
// interactive test: manually fire the focus event from the
// control.
bottomControl.dispatchEvent(new CustomEvent(
'text-focus', {bubbles: true, composed: true}));
return whenEventFired;
})
.then((args) => {
// Shifts left by padding of 50px to ensure that the full textbox
// is visible.
assertEquals(50, args.detail.x);
// Offset top will be 2097 = 200 px/in / 72 pts/in * (794pts -
// 36ptx) - 9px radius of line
// Height of the clip box is 200 px/in * 11in = 2200px
// Shifts down by offsetTop = 2097 - height / 2 + padding =
// 1047px. This will ensure that the textbox is in the visible
// area.
assertEquals(1047, args.detail.y);
});
});
// Tests that the margin controls can be correctly set from the sticky
// settings.
test(
'ControlsDisabledOnError', function() {
return finishSetup().then(() => {
// Simulate setting custom margins.
model.set('settings.margins.value', MarginsType.CUSTOM);
const controls = getControls();
controls.forEach(control => assertFalse(control.disabled));
container.state = State.ERROR;
// Validate controls are disabled.
controls.forEach(control => assertTrue(control.disabled));
container.state = State.READY;
controls.forEach(control => assertFalse(control.disabled));
});
});
});