Files
Maksim Horbachevsky dbae9f8014 Html formatter (#1553)
* Add attributes formatter (Firefox and Chrome output different order)

* Add html formatter to trigger pritter in VC / precommit hook

* Add html format trigger to tests
2022-04-09 00:43:34 -07:00

501 lines
13 KiB
JavaScript

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict
*/
import {expect, test as base} from '@playwright/test';
import prettier from 'prettier';
import {URLSearchParams} from 'url';
import {v4 as uuidv4} from 'uuid';
import {selectAll} from '../keyboardShortcuts/index.mjs';
export const E2E_PORT = process.env.E2E_PORT || 3000;
export const E2E_BROWSER = process.env.E2E_BROWSER;
export const IS_MAC = process.platform === 'darwin';
export const IS_WINDOWS = process.platform === 'win32';
export const IS_LINUX = !IS_MAC && !IS_WINDOWS;
export const IS_COLLAB =
process.env.E2E_EDITOR_MODE === 'rich-text-with-collab';
const IS_RICH_TEXT = process.env.E2E_EDITOR_MODE !== 'plain-text';
const IS_PLAIN_TEXT = process.env.E2E_EDITOR_MODE === 'plain-text';
export async function initialize({
page,
isCollab,
isCharLimit,
isCharLimitUtf8,
}) {
const appSettings = {};
appSettings.isRichText = IS_RICH_TEXT;
appSettings.disableBeforeInput =
process.env.E2E_EVENTS_MODE === 'legacy-events';
if (isCollab) {
appSettings.isCollab = isCollab;
appSettings.collabId = uuidv4();
}
if (appSettings.showNestedEditorTreeView === undefined) {
appSettings.showNestedEditorTreeView = true;
}
appSettings.isCharLimit = !!isCharLimit;
appSettings.isCharLimitUtf8 = !!isCharLimitUtf8;
const urlParams = appSettingsToURLParams(appSettings);
const url = `http://localhost:${E2E_PORT}/${
isCollab ? 'split/' : ''
}?${urlParams.toString()}`;
await page.goto(url);
}
export const test = base.extend({
isCharLimit: false,
isCharLimitUtf8: false,
isCollab: IS_COLLAB,
isPlainText: IS_PLAIN_TEXT,
isRichText: IS_RICH_TEXT,
});
export {expect} from '@playwright/test';
function appSettingsToURLParams(appSettings) {
const params = new URLSearchParams();
Object.entries(appSettings).forEach(([setting, value]) => {
params.append(setting, value);
});
return params;
}
export async function repeat(times, cb) {
for (let i = 0; i < times; i++) {
await cb();
}
}
export async function clickSelectors(page, selectors) {
for (let i = 0; i < selectors.length; i++) {
await waitForSelector(page, selectors[i]);
await click(page, selectors[i]);
}
}
async function assertHTMLOnPageOrFrame(
page,
pageOrFrame,
expectedHtml,
ignoreClasses,
ignoreInlineStyles,
) {
const actualHtml = await pageOrFrame.innerHTML('div[contenteditable="true"]');
const actual = prettifyHTML(actualHtml, {
ignoreClasses,
ignoreInlineStyles,
});
const expected = prettifyHTML(expectedHtml.replace(/\n/gm, ''), {
ignoreClasses,
ignoreInlineStyles,
});
expect(actual).toEqual(expected);
}
export async function assertHTML(
page,
expectedHtml,
{
ignoreSecondFrame = false,
ignoreClasses = false,
ignoreInlineStyles = false,
} = {},
) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
await assertHTMLOnPageOrFrame(
page,
leftFrame,
expectedHtml,
ignoreClasses,
ignoreInlineStyles,
);
if (!ignoreSecondFrame) {
let attempts = 0;
while (attempts < 4) {
const rightFrame = await page.frame('right');
let failed = false;
try {
await assertHTMLOnPageOrFrame(
page,
rightFrame,
expectedHtml,
ignoreClasses,
ignoreInlineStyles,
);
} catch (e) {
if (attempts === 5) {
throw e;
}
failed = true;
}
if (!failed) {
break;
}
attempts++;
await sleep(500);
}
}
} else {
await assertHTMLOnPageOrFrame(
page,
page,
expectedHtml,
ignoreClasses,
ignoreInlineStyles,
);
}
}
async function assertSelectionOnPageOrFrame(page, expected) {
// Assert the selection of the editor matches the snapshot
const selection = await page.evaluate(() => {
const rootElement = document.querySelector('div[contenteditable="true"]');
const getPathFromNode = (node) => {
const path = [];
if (node === rootElement) {
return [];
}
while (node !== null) {
const parent = node.parentNode;
if (parent === null || node === rootElement) {
break;
}
path.push(Array.from(parent.childNodes).indexOf(node));
node = parent;
}
return path.reverse();
};
const {anchorNode, anchorOffset, focusNode, focusOffset} =
window.getSelection();
return {
anchorOffset,
anchorPath: getPathFromNode(anchorNode),
focusOffset,
focusPath: getPathFromNode(focusNode),
};
}, expected);
expect(selection.anchorPath).toEqual(expected.anchorPath);
expect(selection.focusPath).toEqual(expected.focusPath);
if (Array.isArray(expected.anchorOffset)) {
const [start, end] = expected.anchorOffset;
expect(selection.anchorOffset).toBeGreaterThanOrEqual(start);
expect(selection.anchorOffset).toBeLessThanOrEqual(end);
} else {
expect(selection.anchorOffset).toEqual(expected.anchorOffset);
}
if (Array.isArray(expected.focusOffset)) {
const [start, end] = expected.focusOffset;
expect(selection.focusOffset).toBeGreaterThanOrEqual(start);
expect(selection.focusOffset).toBeLessThanOrEqual(end);
} else {
expect(selection.focusOffset).toEqual(expected.focusOffset);
}
}
export async function assertSelection(page, expected) {
if (IS_COLLAB) {
const frame = await page.frame('left');
await assertSelectionOnPageOrFrame(frame, expected);
} else {
await assertSelectionOnPageOrFrame(page, expected);
}
}
export async function isMac(page) {
return page.evaluate(
() =>
typeof window !== 'undefined' &&
/Mac|iPod|iPhone|iPad/.test(window.navigator.platform),
);
}
export async function supportsBeforeInput(page) {
return page.evaluate(() => {
if ('InputEvent' in window) {
return 'getTargetRanges' in new window.InputEvent('input');
}
return false;
});
}
export async function keyDownCtrlOrMeta(page) {
if (await isMac(page)) {
await page.keyboard.down('Meta');
} else {
await page.keyboard.down('Control');
}
}
export async function keyUpCtrlOrMeta(page) {
if (await isMac(page)) {
await page.keyboard.up('Meta');
} else {
await page.keyboard.up('Control');
}
}
export async function keyDownCtrlOrAlt(page) {
if (await isMac(page)) {
await page.keyboard.down('Alt');
} else {
await page.keyboard.down('Control');
}
}
export async function keyUpCtrlOrAlt(page) {
if (await isMac(page)) {
await page.keyboard.up('Alt');
} else {
await page.keyboard.up('Control');
}
}
async function copyToClipboardPageOrFrame(pageOrFrame) {
return await pageOrFrame.evaluate(() => {
const clipboardData = {};
const editor = document.querySelector('div[contenteditable="true"]');
const copyEvent = new ClipboardEvent('copy');
Object.defineProperty(copyEvent, 'clipboardData', {
value: {
setData(type, value) {
clipboardData[type] = value;
},
},
});
editor.dispatchEvent(copyEvent);
return clipboardData;
});
}
export async function copyToClipboard(page) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
return await copyToClipboardPageOrFrame(leftFrame);
} else {
return await copyToClipboardPageOrFrame(page);
}
}
async function pasteFromClipboardPageOrFrame(pageOrFrame, clipboardData) {
const canUseBeforeInput = supportsBeforeInput(pageOrFrame);
await pageOrFrame.evaluate(
async ({
clipboardData: _clipboardData,
canUseBeforeInput: _canUseBeforeInput,
}) => {
const editor = document.querySelector('div[contenteditable="true"]');
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
});
Object.defineProperty(pasteEvent, 'clipboardData', {
value: {
getData(type, value) {
return _clipboardData[type];
},
},
});
editor.dispatchEvent(pasteEvent);
if (!pasteEvent.defaultPrevented) {
if (_canUseBeforeInput) {
const inputEvent = new InputEvent('beforeinput', {
bubbles: true,
cancelable: true,
});
Object.defineProperty(inputEvent, 'inputType', {
value: 'insertFromPaste',
});
Object.defineProperty(inputEvent, 'dataTransfer', {
value: {
getData(type, value) {
return _clipboardData[type];
},
},
});
editor.dispatchEvent(inputEvent);
}
}
},
{canUseBeforeInput, clipboardData},
);
}
export async function pasteFromClipboard(page, clipboardData) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
await pasteFromClipboardPageOrFrame(leftFrame, clipboardData);
} else {
await pasteFromClipboardPageOrFrame(page, clipboardData);
}
}
export async function sleep(delay) {
await new Promise((resolve) => setTimeout(resolve, delay));
}
export async function focusEditor(page, parentSelector = '.editor-shell') {
const selector = `${parentSelector} div[contenteditable="true"]`;
if (IS_COLLAB) {
await page.waitForSelector('iframe[name="left"]');
const leftFrame = page.frame('left');
if ((await leftFrame.$$('.loading').length) !== 0) {
await leftFrame.waitForSelector('.loading', {
state: 'detached',
});
await sleep(500);
}
await leftFrame.focus(selector);
} else {
await page.focus(selector);
}
}
export async function getEditorElement(page, parentSelector = '.editor-shell') {
const selector = `${parentSelector} div[contenteditable="true"]`;
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
await leftFrame.waitForSelector(selector);
return leftFrame.$(selector);
} else {
await page.waitForSelector(selector);
return page.$(selector);
}
}
export async function waitForSelector(page, selector, options) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
await leftFrame.waitForSelector(selector, options);
} else {
await page.waitForSelector(selector, options);
}
}
export async function click(page, selector, options) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
await leftFrame.click(selector, options);
} else {
await page.click(selector, options);
}
}
export async function focus(page, selector, options) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
await leftFrame.focus(selector, options);
} else {
await page.focus(selector, options);
}
}
export async function selectOption(page, selector, options) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
await leftFrame.selectOption(selector, options);
} else {
await page.selectOption(selector, options);
}
}
export async function textContent(page, selector, options) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
return await leftFrame.textContent(selector, options);
} else {
return await page.textContent(selector, options);
}
}
export async function evaluate(page, fn, args) {
if (IS_COLLAB) {
const leftFrame = await page.frame('left');
return await leftFrame.evaluate(fn, args);
} else {
return await page.evaluate(fn, args);
}
}
export async function clearEditor(page) {
await selectAll(page);
await page.keyboard.press('Backspace');
await page.keyboard.press('Backspace');
}
export async function insertImage(page, caption = null) {
await waitForSelector(page, 'button .image');
await click(page, 'button .image');
await waitForSelector(page, '.editor-image img');
if (caption !== null) {
await click(page, '.editor-image img');
await click(page, '.image-caption-button');
await waitForSelector(page, '.editor-image img.focused', {
state: 'detached',
});
await focusEditor(page, '.image-caption-container');
await page.keyboard.type(caption);
}
}
export async function dragMouse(page, firstBoundingBox, secondBoundingBox) {
await page.mouse.move(
firstBoundingBox.x + firstBoundingBox.width / 2,
firstBoundingBox.y + firstBoundingBox.height / 2,
);
await page.mouse.down();
await page.mouse.move(
secondBoundingBox.x + secondBoundingBox.width / 2,
secondBoundingBox.y + secondBoundingBox.height / 2,
);
await page.mouse.up();
}
export function prettifyHTML(string, {ignoreClasses, ignoreInlineStyles} = {}) {
let output = string;
if (ignoreClasses) {
output = output.replace(/\sclass="([^"]*)"/g, '');
}
if (ignoreInlineStyles) {
output = output.replace(/\sstyle="([^"]*)"/g, '');
}
return prettier
.format(output, {
attributeGroups: ['$DEFAULT', '^data-'],
attributeSort: 'ASC',
htmlWhitespaceSensitivity: 'ignore',
parser: 'html',
})
.trim();
}
// This function does not suppose to do anything, it's only used as a trigger
// for prettier auto-formatting (https://prettier.io/blog/2020/08/24/2.1.0.html#api)
export function html(partials, ...params) {
let output = '';
for (let i = 0; i < partials.length; i++) {
output += partials[i];
if (i < partials.length - 1) {
output += params[i];
}
}
return output;
}