Add basic tests to transform shapes

This commit is contained in:
AzazelN28 2023-06-14 09:27:08 +02:00
parent d1b712a111
commit f5bf979209
11 changed files with 806 additions and 35 deletions

BIN
documents/Firefox.penpot Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -36,7 +36,83 @@ const dashboardTest = base.test.extend({
});
const performanceTest = base.test.extend({
page: async ({ page }, use) => {
workingFile: [
"documents/Penpot - Design System v2.0.penpot",
{ option: true },
],
workingShapes: [
{
pageId: "582296a0-d6b1-11ec-a04a-cf2544e40df7",
singleId: "#shape-5bb9c720-d6b1-11ec-a04a-cf2544e40df7",
multipleFrameTitleId: "#frame-title-5bb92ae0-d6b1-11ec-a04a-cf2544e40df7",
multipleIds: [
"#shape-5bb951f0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bb97900-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bb9a010-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bb9c720-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bb9ee30-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bba3c50-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbab180-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbad890-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbaffa0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbb26b0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbb4dc0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbb74d0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbc1111-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbc8640-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbcad51-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbd4990-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbd70a0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbdbec0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbe8211-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbef741-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bbfe1a0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc02fc1-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc14131-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc22b91-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc2eee0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc33d00-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc38b20-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc3d941-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc49c91-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc55fe1-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc64a40-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc69861-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc7d0e0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bc90961-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bcade20-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bcb2c41-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bcbef91-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bccd9f1-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bce1270-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bce6091-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bcf4af1-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd05c61-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd146c0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd194e0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd1bbf0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd390b0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd42cf0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd51750-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd56570-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd58c80-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd628c0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd6ec10-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd7af60-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd899c0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bd98420-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bda6e80-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bdae3b0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bdb7ff0-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bdc4340-d6b1-11ec-a04a-cf2544e40df7",
"#shape-5bdd2da0-d6b1-11ec-a04a-cf2544e40df7",
],
frameTitleId: "#frame-title-5b3247a0-d6b1-11ec-a04a-cf2544e40df7",
frameId: "#frame-container-5b3247a0-d6b1-11ec-a04a-cf2544e40df7"
},
{ option: true },
],
page: async ({ page, workingFile }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.enterEmail(process.env.LOGIN_EMAIL);
@ -47,7 +123,7 @@ const performanceTest = base.test.extend({
await dashboardPage.isHeaderDisplayed("Projects");
await dashboardPage.deleteProjectsIfExist();
await dashboardPage.deleteFilesIfExist();
await dashboardPage.importAndOpenFile('documents/Penpot - Design System v2.0.penpot');
await dashboardPage.importAndOpenFile(workingFile);
await use(page);
},
});

15
helpers/angle.js Normal file
View File

@ -0,0 +1,15 @@
const RAD_TO_DEG = 180 / Math.PI;
const DEG_TO_RAD = Math.PI / 180;
export function degreesToRadians(degrees) {
return degrees * DEG_TO_RAD;
}
export function radiansToDegrees(radians) {
return radians * RAD_TO_DEG;
}
export default {
degreesToRadians,
radiansToDegrees
}

89
helpers/rect.js Normal file
View File

@ -0,0 +1,89 @@
/**
* Creates a DOMPoint.
*
* @param {number} [x=0]
* @param {number} [y=0]
* @param {number} [z=0]
* @param {number} [w=1]
* @returns {DOMPoint}
*/
export function createPoint(x = 0, y = 0, z = 0, w = 1) {
if (!("DOMPoint" in globalThis)) {
class DOMPoint {
static fromPoint({ x = 0, y = 0, z = 0, w = 1 }) {
return new DOMPoint(x, y, z, w);
}
constructor(x = 0, y = 0, z = 0, w = 1) {
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
}
globalThis.DOMPoint = DOMPoint;
}
return new DOMPoint(x, y, z, w);
}
/**
* Creates a DOMRect.
*
* @param {number} x
* @param {number} y
* @param {number} width
* @param {number} height
* @returns {DOMRect}
*/
export function createRect(x = 0, y = 0, width = 0, height = 0) {
if (!('DOMRect' in globalThis)) {
class DOMRect {
static fromRect({ x, y, width, height }) {
return new DOMRect(x, y, width, height)
}
constructor(x = 0, y = 0, width = 0, height = 0) {
this.x = x
this.y = y
this.width = width
this.height = height
}
get top() {
return this.y
}
get left() {
return this.x
}
get bottom() {
return this.y + this.height
}
get right() {
return this.x + this.width
}
}
globalThis.DOMRect = DOMRect;
}
return new DOMRect(x, y, width, height);
}
/**
* Returns the center point of a DOMRect.
*
* @param {DOMRect} rect
* @returns {DOMPoint}
*/
export function getCenterPoint(rect) {
return createPoint(rect.x + rect.width / 2, rect.y + rect.height / 2, 0, 1)
}
export default {
createRect,
createPoint,
getCenterPoint
}

View File

@ -7,7 +7,7 @@
"test": "npx playwright test --project=chrome -gv 'PERF'",
"firefox": "npx playwright test --project=firefox -gv 'PERF'",
"webkit": "npx playwright test --project=webkit -gv 'PERF'",
"performance": "npx playwright test --project=chrome -g 'PERF'",
"performance": "npx playwright test --retries=1 --project=chrome -g 'PERF'",
"performance:debug": "npx playwright test --debug --project=chrome -g 'PERF'",
"prettier": "npx prettier --write ."
},

View File

@ -1,25 +1,52 @@
const { expect } = require("@playwright/test");
const { BasePage } = require("./base-page");
import { expect } from "@playwright/test";
import { BasePage } from "./base-page";
import { createPoint, createRect, getCenterPoint } from "../helpers/rect";
import { degreesToRadians } from "../helpers/angle";
exports.PerformancePage = class PerformancePage extends BasePage {
/**
* @param {import('@playwright/test').Page} page
* Constructor
*
* @param {import("@playwright/test").Page} page
*/
constructor(page) {
super(page);
this.viewportControls = page.locator('.viewport-controls');
this.viewportControls = page.locator(".viewport-controls");
this.selectionHandlers = page.locator(".selection-handlers");
}
/**
* Wait for the page to be loaded.
*
* @returns {Promise<void>}
*/
async waitForPageLoaded() {
await this.page.waitForLoadState("networkidle");
}
/**
* Wait for the viewport controls to be visible.
*
* @returns {Promise<void>}
*/
async waitForViewportControls() {
await this.viewportControls.click();
}
/**
* Wait for the selection handlers to be visible.
*
* @returns {Promise<void>}
*/
async waitForSelectionHandlers() {
await this.page.waitForSelector(".selection-handlers", { delay: 100 });
}
/**
* Starts observing long tasks.
*
* @returns {Promise<void>}
*/
startObservingLongTasks() {
return this.page.evaluate(() => {
@ -283,12 +310,13 @@ exports.PerformancePage = class PerformancePage extends BasePage {
* @returns {number}
*/
calculateAverageFrameRate(frameRateRecords) {
if (frameRateRecords.length === 0)
return 0
if (frameRateRecords.length === 0) return 0;
return frameRateRecords.reduce((sum, record) => {
return sum + record.value;
}, 0) / frameRateRecords.length;
return (
frameRateRecords.reduce((sum, record) => {
return sum + record.value;
}, 0) / frameRateRecords.length
);
}
/**
@ -298,12 +326,13 @@ exports.PerformancePage = class PerformancePage extends BasePage {
* @returns {number}
*/
calculateAverageLongTaskDuration(longTasks) {
if (longTasks.length === 0)
return 0
if (longTasks.length === 0) return 0;
return longTasks.reduce((sum, record) => {
return sum + record.duration;
}, 0) / longTasks.length;
return (
longTasks.reduce((sum, record) => {
return sum + record.duration;
}, 0) / longTasks.length
);
}
/**
@ -321,6 +350,102 @@ exports.PerformancePage = class PerformancePage extends BasePage {
];
}
/**
* Returns the locator of the element.
*
* @param {string|Locator} selectorOrLocator
* @returns {Promise<Locator>}
*/
async getSelectorOrLocator(selectorOrLocator) {
if (typeof selectorOrLocator === "string") {
return this.page.locator(selectorOrLocator);
}
return selectorOrLocator;
}
/**
* Returns the bounding client rect of the element.
*
* @param {string|Locator} selector
* @returns {Promise<DOMRect>}
*/
async getBoundingClientRect(selectorOrLocator) {
const locator = await this.getSelectorOrLocator(selectorOrLocator);
return locator.boundingBox();
}
/**
* Returns the aggregated bounding client rect of the elements.
*
* @param {Array<string|Locator>} selectorsOrLocators
* @returns {Promise<DOMRect>}
*/
async getAggregatedBoundingClientRect(selectorsOrLocators) {
const boundingBoxes = await Promise.all(
selectorsOrLocators.map((selectorOrLocator) =>
this.getBoundingClientRect(selectorOrLocator),
),
);
const boundingClientRect = await Promise.all(
boundingBoxes.reduce((rect, boundingBox) => {
const { top, left, bottom, right } = boundingBox;
rect.top = Math.min(top, rect.top);
rect.left = Math.min(left, rect.left);
rect.bottom = Math.max(bottom, rect.bottom);
rect.right = Math.max(right, rect.right);
return rect;
}, createRect(Infinity, Infinity, -Infinity, -Infinity)),
);
return boundingClientRect
}
/**
* Undo the last action.
*/
async undo() {
await this.page.keyboard.down("Control");
await this.page.keyboard.press("KeyZ");
await this.page.keyboard.up("Control");
}
/**
* Redoes the last action.
*/
async redo() {
await this.page.keyboard.down("Control");
await this.page.keyboard.press("KeyY");
await this.page.keyboard.up("Control");
}
/**
* Waits for the page to be loaded.
*/
async setup() {
await this.waitForPageLoaded();
await this.injectFrameRateRecorder();
await this.waitForViewportControls();
await this.startAll();
}
/**
* Measures the frame rate and long task duration.
*
* @returns {Promise<[number, number]>}
*/
async measure() {
const [frameRateRecords, longTasksRecords] = await this.stopAll();
const averageFrameRate = this.calculateAverageFrameRate(frameRateRecords);
console.log("FPS", averageFrameRate);
expect(averageFrameRate).toBeGreaterThan(30);
const averageLongTaskDuration =
this.calculateAverageLongTaskDuration(longTasksRecords);
console.log("LTS", averageLongTaskDuration);
expect(averageLongTaskDuration).toBeLessThan(100);
return [averageFrameRate, averageLongTaskDuration];
}
/**
* Performs pan operations on the document.
*
@ -330,11 +455,27 @@ exports.PerformancePage = class PerformancePage extends BasePage {
* @returns {Promise<void>}
*/
async pan(dx, dy, steps) {
await this.viewportControls.click();
await this.waitForViewportControls();
await this.page.mouse.down({ button: "middle" });
for (let index = 0; index < steps; index++) {
await this.page.mouse.move(dx, dy);
}
await this.page.mouse.move(dx, dy, { steps });
await this.page.mouse.up({ button: "middle" });
}
/**
* Performs pan operations on the document.
*
* @param {number} fx
* @param {number} fy
* @param {number} tx
* @param {number} ty
* @param {number} steps
* @returns {Promise<void>}
*/
async panFromTo(fx, fy, tx, ty, steps) {
await this.waitForViewportControls();
await this.page.mouse.move(tx, ty);
await this.page.mouse.down({ button: "middle" });
await this.page.mouse.move(fx, fy, { steps });
await this.page.mouse.up({ button: "middle" });
}
@ -346,11 +487,335 @@ exports.PerformancePage = class PerformancePage extends BasePage {
* @returns {Promise<void>}
*/
async zoom(dy, steps) {
await this.viewportControls.click();
await this.waitForViewportControls();
await this.page.keyboard.down("Control");
for (let index = 0; index < steps; index++) {
await this.page.mouse.wheel(0, dy);
}
await this.page.keyboard.up("Control");
}
/**
* Centers an element in the viewport.
*
* @param {string} selector
* @returns {Promise<ElementHandle>}
*/
async centerInView(selectorOrLocator) {
await this.waitForViewportControls();
const viewportRect = await this.getBoundingClientRect(
this.viewportControls,
);
const centerPoint = getCenterPoint(viewportRect);
const element = await this.getSelectorOrLocator(selectorOrLocator);
const elementRect = await this.getBoundingClientRect(element);
const elementCenterPoint = getCenterPoint(elementRect);
await this.panFromTo(
centerPoint.x,
centerPoint.y,
elementCenterPoint.y,
elementCenterPoint.x,
10,
);
return element;
}
async centerSelectedInView() {
const viewportRect = await this.getBoundingClientRect(
this.viewportControls,
);
const centerPoint = getCenterPoint(viewportRect);
const elementRect = await this.getBoundingClientRect(this.selectionHandlers);
const elementCenterPoint = getCenterPoint(elementRect);
await this.panFromTo(
centerPoint.x,
centerPoint.y,
elementCenterPoint.y,
elementCenterPoint.x,
10,
);
}
/**
* Selects a document page.
*
* @param {string} pageId
* @returns {Promise<void>}
*/
async selectPage(pageId) {
const pageItem = await this.page.locator(`[data-test="page-${pageId}"]`);
await pageItem.scrollIntoViewIfNeeded();
await pageItem.click();
}
async selectFrame(frameId) {
const locator = await this.page.locator(frameId);
await this.centerInView(this.selectionHandlers);
await locator.click();
}
/**
* Selects a document shape.
*
* @param {string} shapeId
* @param {number} x
* @param {number} y
* @returns {Promise<DOMRect>}
*/
async selectShape(shapeId, { x: dx = 2, y: dy = 2 } = {}) {
const shape = await this.centerInView(shapeId);
const { x, y } = await this.getBoundingClientRect(shape);
await this.page.mouse.click(x + dx, y + dy);
return shape;
}
/**
* Selects multiple document shapes.
*
* @param {Array<string>} shapeIds
* @returns {Promise<void>}
*/
async selectShapes(shapeIds) {
await this.page.keyboard.down("Shift");
for (const shapeId of shapeIds) {
const locator = await this.page.locator(shapeId);
const { x, y, width, height } = await locator.boundingBox();
console.log(x, y, width, height);
await this.page.mouse.click(x + width / 2, y + height / 2);
await this.page.waitForTimeout(100);
}
await this.page.keyboard.up("Shift");
await this.page.waitForTimeout(100);
}
/**
* Selects
*
* @param {Array<string>} shapeIds
* @returns {Promise<DOMRect>}
*/
async selectShapesWithRect(shapeIds) {
// TODO: We should select the shapes with a rectangle.
const boundingClientRect = await this.page.evaluate((shapeIds) => {
const shapes = shapeIds.map((shapeId) => document.querySelector(shapeId));
if (shapes.some((shape) => !shape)) {
throw new Error(`Shape "shape-${shapeId}" not found.`);
}
const boundingClientRect = shapes.reduce((rect, shape) => {
const { top, left, bottom, right } = shape.getBoundingClientRect();
rect.top = Math.min(top, rect.top);
rect.left = Math.min(left, rect.left);
rect.bottom = Math.max(bottom, rect.bottom);
rect.right = Math.max(right, rect.right);
return rect;
}, new DOMRect(Infinity, Infinity, -Infinity, -Infinity));
return boundingClientRect;
}, shapeIds);
}
/**
* Returns a resize handler by its name.
*
* @param {string} name
* @returns {Promise<ElementHandle>}
*/
async getResizeHandler(name = "bottom-right") {
const names = [
"top",
"bottom",
"right",
"left",
"top-left",
"top-right",
"bottom-right",
"bottom-left",
];
// -}~:Ztva;:E]gU8u$[Vf
const handlers = await this.page.$$("g.resize-handler");
if (handlers.length < names.length) {
throw new Error(`Invalid amount of resize handlers.`);
}
const index = names.indexOf(name);
if (index === -1) {
throw new Error(`Invalid resize handler name "${name}".`);
}
const handler = handlers[index];
return handler;
}
/**
* Returns a rotate handler by its position.
*
* @param {string} position
* @returns {Promise<ElementHandle>}
*/
async getRotateHandler(position = "top-left") {
const positions = new Map([
["top-left", "0"],
["top-right", "90"],
["bottom-right", "180"],
["bottom-left", "270"],
]);
return this.page.$(`rect.cursor-rotate-${positions.get(position)}`);
}
/**
* Moves the passed selector or locator to the given coordinates.
*
* @param {string|Locator} selectorOrLocator
* @param {number} x
* @param {number} y
* @param {number} [steps=10]
* @returns {Promise<Locator>}
*/
async moveShapeBy(
selectorOrLocator,
x,
y,
{ x: dx = 2, y: dy = 2, steps = 10 } = {},
) {
const { x: ex, y: ey } = await this.getBoundingClientRect(
selectorOrLocator,
);
await this.page.mouse.click(ex + dx, ey + dy);
await this.page.waitForTimeout(100);
await this.page.mouse.click(ex + dx, ey + dy);
await this.page.mouse.down({ button: "left" });
await this.page.waitForTimeout(100);
await this.page.mouse.move(ex + x, ey + y, { steps });
await this.page.waitForTimeout(100);
await this.page.mouse.up({ button: "left" });
}
/**
* Moves the selected shape by the given coordinates.
*
* @param {number} x
* @param {number} y
* @param {object} [options={}]
* @returns {Promise<void>}
*/
async moveSelectedBy(x, y, { x: dx = 2, y: dy = 2, steps = 10 } = {}) {
await this.waitForSelectionHandlers();
const { x: ex, y: ey, width, height } = await this.getBoundingClientRect(this.selectionHandlers);
console.log(x, y, width, height)
await this.page.mouse.down({ button: "left" });
await this.page.waitForTimeout(100);
await this.page.mouse.move(ex + x, ey + y, { steps });
await this.page.waitForTimeout(100);
await this.page.mouse.up({ button: "left" });
}
/**
* Rotates the passed selector or locator to the given coordinates.
*
* @param {string|Locator} selectorOrLocator
* @param {number} degrees
* @param {number} [steps=10]
* @returns {Promise<Locator>}
*/
async rotateShapeBy(selectorOrLocator, degrees, { steps = 10 } = {}) {
const boundingBox = await this.getBoundingClientRect(selectorOrLocator);
const { x: centerX, y: centerY } = getCenterPoint(boundingBox);
const rotateHandler = await this.getRotateHandler("top-left");
const { x: rx, y: ry } = await this.getBoundingClientRect(rotateHandler);
const radians = degreesToRadians(degrees);
const dx = Math.cos(radians);
const dy = Math.sin(radians);
const d = Math.hypot(centerX - rx, centerY - ry);
const x = centerX + dx * d;
const y = centerY + dy * d;
await rotateHandler.click();
await this.page.waitForTimeout(100);
await this.page.mouse.down({ button: "left" });
await this.page.waitForTimeout(100);
await this.page.mouse.move(x, y, { steps });
await this.page.waitForTimeout(100);
await this.page.mouse.up({ button: "left" });
}
/**
* Rotates the selected shape by the given degrees.
*
* @param {number} degrees
* @param {object} [options={}]
* @returns {Promise<void>}
*/
async rotateSelectedBy(degrees, { steps = 10 } = {}) {
await this.waitForSelectionHandlers();
const { x: ex, y: ey } = await this.getBoundingClientRect(
this.selectionHandlers,
);
const { x: centerX, y: centerY } = getCenterPoint(boundingBox);
const rotateHandler = await this.getRotateHandler("top-left");
const { x: rx, y: ry } = await this.getBoundingClientRect(rotateHandler);
const radians = degreesToRadians(degrees);
const dx = Math.cos(radians);
const dy = Math.sin(radians);
const d = Math.hypot(centerX - rx, centerY - ry);
const x = centerX + dx * d;
const y = centerY + dy * d;
await rotateHandler.click();
await this.page.waitForTimeout(100);
await this.page.mouse.down({ button: "left" });
await this.page.waitForTimeout(100);
await this.page.mouse.move(x, y, { steps });
await this.page.waitForTimeout(100);
await this.page.mouse.up({ button: "left" });
}
/**
* Resizes the selected shapes by the given delta.
*
* @param {number} x
* @param {number} y
* @param {"top"|"bottom"|"right"|"left"|"top-left"|"top-right"|"bottom-right"|"bottom-left"} handler
* @returns {Promise<void>}
*/
async scaleShapeBy(
selectorOrLocator,
sx,
sy,
{ x: dx = 2, y: dy = 2, steps = 10, handler = "bottom-right" } = {},
) {
// Clicks on the element to select it.
const { x: ex, y: ey } = await this.getBoundingClientRect(selectorOrLocator);
await this.page.mouse.click(ex + dx, ey + dy);
await this.waitForSelectionHandlers();
const resizeHandler = await this.getResizeHandler(handler);
const { x, y } = await this.getBoundingClientRect(resizeHandler);
console.log(x, y, x + sx, y + sy);
await resizeHandler.click();
await this.page.waitForTimeout(100);
await this.page.mouse.down({ button: "left" });
await this.page.waitForTimeout(100);
await this.page.mouse.move(x + sx, y + sy, { steps });
await this.page.waitForTimeout(100);
await this.page.mouse.up({ button: "left" });
}
/**
* Resizes the selected shapes by the given delta.
*
* @param {number} sx
* @param {number} sy
* @param {object} [options]
* @returns {Promise<void>}
*/
async scaleSelectedBy(sx, sy, { x: dx = 2, y: dy = 2, steps = 10, handler = "bottom-right" } = {}) {
// Clicks on the element to select it.
await this.waitForSelectionHandlers();
const resizeHandler = await this.getResizeHandler(handler);
const { x, y } = await this.getBoundingClientRect(resizeHandler);
await resizeHandler.click();
await this.page.waitForTimeout(100);
await this.page.mouse.down({ button: "left" });
await this.page.waitForTimeout(100);
await this.page.mouse.move(x + sx, y + sy, { steps });
await this.page.waitForTimeout(100);
await this.page.mouse.up({ button: "left" });
}
};

View File

@ -39,6 +39,7 @@ const config = {
headless: true,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
video: "on",
},
projects: [
{

View File

@ -1,25 +1,150 @@
const { test } = require("@playwright/test");
import { expect } from "@playwright/test";
import { performanceTest } from "../../fixtures.js";
import { PerformancePage } from "../../pages/performance-page";
test('PERF Rotate single shape', async ({ page }) => {
performanceTest("PERF Shape (single): Move single shape", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
})
await performancePage.selectPage(workingShapes.pageId);
// IMPORTANT: We need to set the click position because we can have
// shapes overlapping each other and we need to make sure we click
// the right one.
await performancePage.selectShape(workingShapes.singleId, { x: 5, y: 10 });
await performancePage.zoom(-10, 10);
await performancePage.moveShapeBy(workingShapes.singleId, -100, -100, { x: 5, y: 10, steps: 100 });
test('PERF Scale single shape', async ({ page }) => {
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
})
performanceTest("PERF Shape (single): Rotate single shape", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
test('PERF Move single shape', async ({ page }) => {
await performancePage.selectPage(workingShapes.pageId);
// IMPORTANT: We need to set the click position because we can have
// shapes overlapping each other and we need to make sure we click
// the right one.
await performancePage.selectShape(workingShapes.singleId, { x: 5, y: 10 });
await performancePage.zoom(-10, 10);
await performancePage.rotateShapeBy(workingShapes.singleId, 45);
})
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
test('PERF Rotate multiple shapes', async ({ page }) => {
performanceTest("PERF Shape (single): Scale single shape", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
})
await performancePage.selectPage(workingShapes.pageId);
// IMPORTANT: We need to set the click position because we can have
// shapes overlapping each other and we need to make sure we click
// the right one.
await performancePage.selectShape(workingShapes.singleId, { x: 5, y: 10 });
await performancePage.zoom(-10, 10);
await performancePage.scaleShapeBy(workingShapes.singleId, 100, 100);
test('PERF Scale multiple shapes', async ({ page }) => {
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
})
performanceTest("PERF Shape (multiple): Move multiple shapes", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
test('PERF Move multiple shapes', async ({ page }) => {
await performancePage.selectPage(workingShapes.pageId);
await performancePage.selectFrame(workingShapes.multipleFrameTitleId);
await performancePage.selectShapes(workingShapes.multipleIds);
await performancePage.zoom(-10, 10);
await performancePage.moveSelectedBy(100, 100);
})
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
performanceTest("PERF Shape (multiple): Rotate multiple shapes", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
await performancePage.selectPage(workingShapes.pageId);
await performancePage.selectFrame(workingShapes.multipleFrameTitleId);
await performancePage.selectShapes(workingShapes.multipleIds);
await performancePage.zoom(-10, 10);
await performancePage.rotateSelectedBy(45);
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
performanceTest("PERF Shape (multiple): Scale multiple shapes", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
await performancePage.selectPage(workingShapes.pageId);
await performancePage.selectFrame(workingShapes.multipleFrameTitleId);
await performancePage.selectShapes(workingShapes.multipleIds);
await performancePage.zoom(-10, 10);
await performancePage.scaleSelectedBy(100, 100);
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
performanceTest("PERF Shape (frame): Move frame nested shapes", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
await performancePage.selectPage(workingShapes.pageId);
await performancePage.selectShape(workingShapes.frameTitleId, { x: 2, y: 2 });
await performancePage.zoom(-10, 10);
await performancePage.moveShapeBy(workingShapes.frameId, 100, 100, {
x: 5,
y: 50,
steps: 100,
});
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
performanceTest("PERF Shape (frame): Rotate frame nested shapes", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
await performancePage.selectPage(workingShapes.pageId);
await performancePage.selectShape(workingShapes.frameTitleId, {
x: 2,
y: 2,
});
await performancePage.zoom(-10, 10);
await performancePage.rotateShapeBy(workingShapes.frameId, 100, 100, {
x: 2,
y: 2,
steps: 100,
});
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});
performanceTest("PERF Shape (frame): Scale frame nested shapes", async ({ page, workingShapes }) => {
const performancePage = new PerformancePage(page);
await performancePage.setup();
await performancePage.selectPage(workingShapes.pageId);
await performancePage.selectShape(workingShapes.frameTitleId, {
x: 2,
y: 2,
});
await performancePage.zoom(-10, 10);
await performancePage.scaleShapeBy(workingShapes.frameId, 100, 100, {
x: 2,
y: 2,
steps: 100,
});
const [averageFrameRate, averageLongTaskDuration] =
await performancePage.measure();
});