mirror of
https://github.com/penpot/penpotqa.git
synced 2024-07-06 04:51:46 +00:00
Add basic tests to transform shapes
This commit is contained in:
parent
d1b712a111
commit
f5bf979209
BIN
documents/Firefox.penpot
Normal file
BIN
documents/Firefox.penpot
Normal file
Binary file not shown.
BIN
documents/Shape Transform_ Deeply nested.penpot
Normal file
BIN
documents/Shape Transform_ Deeply nested.penpot
Normal file
Binary file not shown.
BIN
documents/Shape Transform_ Multiple objects.penpot
Normal file
BIN
documents/Shape Transform_ Multiple objects.penpot
Normal file
Binary file not shown.
BIN
documents/Shape Transform_ Single object.penpot
Normal file
BIN
documents/Shape Transform_ Single object.penpot
Normal file
Binary file not shown.
80
fixtures.js
80
fixtures.js
|
@ -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
15
helpers/angle.js
Normal 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
89
helpers/rect.js
Normal 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
|
||||
}
|
|
@ -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 ."
|
||||
},
|
||||
|
|
|
@ -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" });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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: [
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user