Add measures to viewport actions (pan and zoom)

This commit is contained in:
AzazelN28 2023-06-12 13:07:58 +02:00
parent 11f34c842d
commit e1cab37ad7
7 changed files with 316 additions and 101 deletions

Binary file not shown.

View File

@ -35,5 +35,23 @@ const dashboardTest = base.test.extend({
},
});
const performanceTest = base.test.extend({
page: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.enterEmail(process.env.LOGIN_EMAIL);
await loginPage.enterPwd(process.env.LOGIN_PWD);
await loginPage.clickLoginButton();
const dashboardPage = new DashboardPage(page);
await dashboardPage.waitForPageLoaded();
await dashboardPage.isHeaderDisplayed("Projects");
await dashboardPage.deleteProjectsIfExist();
await dashboardPage.deleteFilesIfExist();
await dashboardPage.importAndOpenFile('documents/Penpot - Design System v2.0.penpot');
await use(page);
},
});
exports.mainTest = mainTest;
exports.dashboardTest = dashboardTest;
exports.performanceTest = performanceTest;

32
package-lock.json generated
View File

@ -12,18 +12,18 @@
"prettier": "^2.7.1"
},
"devDependencies": {
"@playwright/test": "^1.31.2",
"@playwright/test": "^1.34.3",
"dotenv": "^16.0.3"
}
},
"node_modules/@playwright/test": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.2.tgz",
"integrity": "sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==",
"version": "1.34.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz",
"integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.31.2"
"playwright-core": "1.34.3"
},
"bin": {
"playwright": "cli.js"
@ -65,12 +65,12 @@
}
},
"node_modules/playwright-core": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.2.tgz",
"integrity": "sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==",
"version": "1.34.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
"integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
"dev": true,
"bin": {
"playwright": "cli.js"
"playwright-core": "cli.js"
},
"engines": {
"node": ">=14"
@ -93,14 +93,14 @@
},
"dependencies": {
"@playwright/test": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.31.2.tgz",
"integrity": "sha512-BYVutxDI4JeZKV1+ups6dt5WiqKhjBtIYowyZIJ3kBDmJgsuPKsqqKNIMFbUePLSCmp2cZu+BDL427RcNKTRYw==",
"version": "1.34.3",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz",
"integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==",
"dev": true,
"requires": {
"@types/node": "*",
"fsevents": "2.3.2",
"playwright-core": "1.31.2"
"playwright-core": "1.34.3"
}
},
"@types/node": {
@ -123,9 +123,9 @@
"optional": true
},
"playwright-core": {
"version": "1.31.2",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.31.2.tgz",
"integrity": "sha512-a1dFgCNQw4vCsG7bnojZjDnPewZcw7tZUNFN0ZkcLYKj+mPmXvg4MpaaKZ5SgqPsOmqIf2YsVRkgqiRDxD+fDQ==",
"version": "1.34.3",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
"integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
"dev": true
},
"prettier": {

View File

@ -8,6 +8,7 @@
"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:debug": "npx playwright test --debug --project=chrome -g 'PERF'",
"prettier": "npx prettier --write ."
},
"repository": {
@ -22,7 +23,7 @@
},
"homepage": "https://github.com/penpot/penpotqa#readme",
"devDependencies": {
"@playwright/test": "^1.31.2",
"@playwright/test": "^1.34.3",
"dotenv": "^16.0.3"
},
"dependencies": {

View File

@ -81,6 +81,15 @@ exports.DashboardPage = class DashboardPage extends BasePage {
this.projectOptionsMenuButton = page.locator(
'*[data-test="project-options"] .icon-actions'
);
this.projectOptions = page.locator('[data-test="project-options"]');
this.fileImport = page.locator('[data-test="file-import"]');
this.modal = page.locator('#modal');
this.modalCloseButton = page.locator('.modal-close-button');
this.modalTitle = page.locator('.modal-header-title h2');
this.modalCancelButton = page.locator('.modal-footer .action-buttons .cancel-button');
this.modalAcceptButton = page.locator('.modal-footer .action-buttons .accept-button');
this.feedbackBanner = page.locator('.feedback-banner')
this.feedbackBannerMessage = page.locator('.feedback-banner .message')
this.projectsSidebarItem = page.locator('li:has-text("Projects")');
this.draftsSidebarItem = page.locator('li:has-text("Drafts")');
this.pinnedProjectsSidebarItem = page.locator(
@ -712,4 +721,23 @@ exports.DashboardPage = class DashboardPage extends BasePage {
await this.continueButton.click();
await this.acceptButton.click();
}
async importFile(file) {
await this.projectOptions.click();
const fileChooserPromise = this.page.waitForEvent("filechooser");
await this.fileImport.click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(file);
await expect(this.modalTitle).toBeVisible();
await expect(this.modalTitle).toHaveText("Import Penpot files");
await this.modalAcceptButton.click();
await this.feedbackBanner.waitFor('visible');
await expect(this.feedbackBannerMessage).toHaveText("1 file has been imported successfully.");
await this.modalAcceptButton.click();
}
async importAndOpenFile(file) {
await this.importFile(file);
await this.openFile();
}
};

View File

@ -6,6 +6,16 @@ exports.PerformancePage = class PerformancePage extends BasePage {
*/
constructor(page) {
super(page);
this.viewportControls = page.locator('.viewport-controls');
}
async waitForPageLoaded() {
await this.page.waitForLoadState("networkidle");
}
async waitForViewportControls() {
await this.viewportControls.click();
}
/**
@ -16,8 +26,10 @@ exports.PerformancePage = class PerformancePage extends BasePage {
window.penpotPerformanceObserver = new PerformanceObserver((list) => {
console.log(list.getEntries());
});
window.penpotPerformanceObserver.observe({ entryTypes: ['longtask', 'taskattribution'] });
})
window.penpotPerformanceObserver.observe({
entryTypes: ["longtask", "taskattribution"],
});
});
}
/**
@ -29,7 +41,7 @@ exports.PerformancePage = class PerformancePage extends BasePage {
return this.page.evaluate(() => {
window.penpotPerformanceObserver.disconnect();
return window.penpotPerformanceObserver.takeRecords();
})
});
}
/**
@ -62,43 +74,43 @@ exports.PerformancePage = class PerformancePage extends BasePage {
* value and a timestamp.
*/
class TimestampedDataRecorder {
#index = 0
#values = null
#timestamps = null
#index = 0;
#values = null;
#timestamps = null;
constructor(maxRecords) {
this.#index = 0
this.#values = new Uint32Array(maxRecords)
this.#timestamps = new Float32Array(maxRecords)
this.#index = 0;
this.#values = new Uint32Array(maxRecords);
this.#timestamps = new Float32Array(maxRecords);
}
reset(clearData = true) {
this.#index = 0
this.#index = 0;
if (clearData) {
this.#values.fill(0)
this.#timestamps.fill(0)
this.#values.fill(0);
this.#timestamps.fill(0);
}
}
record(value) {
this.#values[this.#index] = value
this.#timestamps[this.#index] = performance.now()
this.#index = (this.#index + 1) % this.#values.length
this.#values[this.#index] = value;
this.#timestamps[this.#index] = performance.now();
this.#index = (this.#index + 1) % this.#values.length;
if (this.#index === 0) {
return false
return false;
}
return true
return true;
}
takeRecords() {
const records = []
const records = [];
for (let i = 0; i < this.#index; i++) {
records.push({
value: this.#values[i],
timestamp: this.#timestamps[i]
})
timestamp: this.#timestamps[i],
});
}
return records
return records;
}
}
@ -107,80 +119,82 @@ exports.PerformancePage = class PerformancePage extends BasePage {
* the number of frames.
*/
class FrameRateRecorder extends EventTarget {
#recorder = null
#frameId = null
#frameCount = 0
#framesPerSecond = 0
#startTime = 0
#shouldStopWhenFull = true
#recorder = null;
#frameId = null;
#frameCount = 0;
#framesPerSecond = 0;
#startTime = 0;
#shouldStopWhenFull = true;
constructor({ maxRecords = 60 * 60, shouldStopWhenFull = true }) {
super()
this.#frameId = null
this.#frameCount = 0
this.#startTime = 0
this.#shouldStopWhenFull = shouldStopWhenFull
this.#recorder = new TimestampedDataRecorder(maxRecords)
super();
this.#frameId = null;
this.#frameCount = 0;
this.#startTime = 0;
this.#shouldStopWhenFull = shouldStopWhenFull;
this.#recorder = new TimestampedDataRecorder(maxRecords);
}
get framesPerSecond() {
return this.#framesPerSecond
return this.#framesPerSecond;
}
#onFrame = (currentTime) => {
this.#frameCount++
let shouldStop = false
this.#frameCount++;
let shouldStop = false;
if (currentTime - this.#startTime >= 1000) {
this.#startTime = currentTime
this.#framesPerSecond = this.#frameCount
this.#frameCount = 0
this.#startTime = currentTime;
this.#framesPerSecond = this.#frameCount;
this.#frameCount = 0;
const shouldContinue = this.#recorder.record(this.#framesPerSecond)
const shouldContinue = this.#recorder.record(
this.#framesPerSecond,
);
if (!shouldContinue && this.#shouldStopWhenFull) {
shouldStop = true
shouldStop = true;
}
}
if (shouldStop) {
if (this.stop()) {
this.dispatchEvent(new Event('ended'))
this.dispatchEvent(new Event("ended"));
}
return
return;
}
this.#frameId = window.requestAnimationFrame(this.#onFrame)
}
this.#frameId = window.requestAnimationFrame(this.#onFrame);
};
takeRecords() {
return this.#recorder.takeRecords()
return this.#recorder.takeRecords();
}
start() {
if (this.#frameId !== null) {
return false
return false;
}
this.#recorder.reset()
this.#startTime = performance.now()
this.#frameId = window.requestAnimationFrame(this.#onFrame)
return true
this.#recorder.reset();
this.#startTime = performance.now();
this.#frameId = window.requestAnimationFrame(this.#onFrame);
return true;
}
stop() {
if (this.#frameId === null) {
return false
return false;
}
window.cancelAnimationFrame(this.#frameId)
this.#frameId = null
return true
window.cancelAnimationFrame(this.#frameId);
this.#frameId = null;
return true;
}
}
return FrameRateRecorder
return FrameRateRecorder;
}
// If the FrameRateRecorder is already defined, do nothing.
if (typeof window.FrameRateRecorder !== 'function') {
window.FrameRateRecorder = provide()
if (typeof window.FrameRateRecorder !== "function") {
window.FrameRateRecorder = provide();
}
})
});
}
/**
@ -188,19 +202,33 @@ exports.PerformancePage = class PerformancePage extends BasePage {
*
* @returns {Promise<void>}
*/
startRecordingFrameRate({ maxRecords = 60 * 60, shouldStopWhenFull = true } = {}) {
return this.page.evaluate(({ maxRecords, shouldStopWhenFull }) => {
if (typeof window.FrameRateRecorder !== 'function') {
throw new Error('FrameRateRecorder is not defined. Call `injectFrameRateRecorder` first.')
}
startRecordingFrameRate({
maxRecords = 60 * 60,
shouldStopWhenFull = true,
} = {}) {
return this.page.evaluate(
({ maxRecords, shouldStopWhenFull }) => {
if (typeof window.FrameRateRecorder !== "function") {
throw new Error(
"FrameRateRecorder is not defined. Call `injectFrameRateRecorder` first.",
);
}
window.penpotFrameRateRecorder_OnEnded = () => {
throw new Error('Insufficient buffer size to record frame rate.')
}
window.penpotFrameRateRecorder = new FrameRateRecorder({ maxRecords, shouldStopWhenFull })
window.penpotFrameRateRecorder.addEventListener('ended', window.penpotFrameRateRecorder_OnEnded)
window.penpotFrameRateRecorder.start()
}, { maxRecords, shouldStopWhenFull })
window.penpotFrameRateRecorder_OnEnded = () => {
throw new Error("Insufficient buffer size to record frame rate.");
};
window.penpotFrameRateRecorder = new FrameRateRecorder({
maxRecords,
shouldStopWhenFull,
});
window.penpotFrameRateRecorder.addEventListener(
"ended",
window.penpotFrameRateRecorder_OnEnded,
);
window.penpotFrameRateRecorder.start();
},
{ maxRecords, shouldStopWhenFull },
);
}
/**
@ -210,12 +238,119 @@ exports.PerformancePage = class PerformancePage extends BasePage {
*/
stopRecordingFrameRate() {
return this.page.evaluate(() => {
if (typeof window.FrameRateRecorder !== 'function') {
throw new Error('FrameRateRecorder is not defined. Call `injectFrameRateRecorder` first.')
if (typeof window.FrameRateRecorder !== "function") {
throw new Error(
"FrameRateRecorder is not defined. Call `injectFrameRateRecorder` first.",
);
}
window.penpotFrameRateRecorder.removeEventListener('ended', window.penpotFrameRateRecorder_OnEnded)
window.penpotFrameRateRecorder.stop()
return window.penpotFrameRateRecorder.takeRecords()
})
window.penpotFrameRateRecorder.removeEventListener(
"ended",
window.penpotFrameRateRecorder_OnEnded,
);
window.penpotFrameRateRecorder.stop();
return window.penpotFrameRateRecorder.takeRecords();
});
}
}
/**
* Starts all the recorders.
*
* @returns {Promise<void>}
*/
async startAll() {
return Promise.all([
this.startRecordingFrameRate(),
this.startObservingLongTasks(),
]);
}
/**
* Stops all the recorders.
*
* @returns {Promise<[FrameRateRecords, LongTasks]>}
*/
stopAll() {
return Promise.all([
this.stopRecordingFrameRate(),
this.stopObservingLongTasks(),
]);
}
/**
* Calculates the average frame rate.
*
* @param {Array<FrameRateEntry>} frameRateRecords
* @returns {number}
*/
calculateAverageFrameRate(frameRateRecords) {
if (frameRateRecords.length === 0)
return 0
return frameRateRecords.reduce((sum, record) => {
return sum + record.value;
}, 0) / frameRateRecords.length;
}
/**
* Calculates the average long task duration.
*
* @param {Array<LongTaskEntry>} longTasks
* @returns {number}
*/
calculateAverageLongTaskDuration(longTasks) {
if (longTasks.length === 0)
return 0
return longTasks.reduce((sum, record) => {
return sum + record.duration;
}, 0) / longTasks.length;
}
/**
* Calculates the averages of the frame rate
* and long task duration.
*
* @param {Array<FrameRateEntry>} frameRateRecords
* @param {Array<LongTaskEntry>} longTasks
* @returns {[number, number]}
*/
calculateAverages(frameRateRecords, longTasks) {
return [
this.calculateAverageFrameRate(frameRateRecords),
this.calculateAverageLongTaskDuration(longTasks),
];
}
/**
* Performs pan operations on the document.
*
* @param {number} dx
* @param {number} dy
* @param {number} steps
* @returns {Promise<void>}
*/
async pan(dx, dy, steps) {
await this.viewportControls.click();
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.up({ button: "middle" });
}
/**
* Performs zoom operations on the document.
*
* @param {number} dy
* @param {number} steps
* @returns {Promise<void>}
*/
async zoom(dy, steps) {
await this.viewportControls.click();
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");
}
};

View File

@ -1,9 +1,42 @@
const { test } = require("@playwright/test");
import { expect } from "@playwright/test";
import { performanceTest } from '../../fixtures.js';
import { PerformancePage } from "../../pages/performance-page";
test('PERF Pan viewport in workspace', async ({ page }) => {
performanceTest("PERF Viewport: Pan viewport in workspace", async ({ page }) => {
const performancePage = new PerformancePage(page);
await performancePage.waitForPageLoaded();
await performancePage.injectFrameRateRecorder();
await performancePage.waitForViewportControls();
await performancePage.startAll();
await performancePage.pan(10, 0, 10)
await performancePage.pan(0, 10, 10)
await performancePage.pan(-10, 0, 10)
await performancePage.pan(0, -10, 10)
const [frameRateRecords, longTasksRecords] = await performancePage.stopAll();
const averageFrameRate =
performancePage.calculateAverageFrameRate(frameRateRecords);
console.log("FPS", averageFrameRate);
expect(averageFrameRate).toBeGreaterThan(30);
const averageLongTaskDuration =
performancePage.calculateAverageLongTaskDuration(longTasksRecords);
console.log("LTS", averageLongTaskDuration);
expect(averageLongTaskDuration).toBeLessThan(100);
})
});
test('PERF Zoom viewport in workspace', async ({ page }) => {
})
performanceTest("PERF Viewport: Zoom viewport in workspace", async ({ page }) => {
const performancePage = new PerformancePage(page);
await performancePage.waitForPageLoaded();
await performancePage.injectFrameRateRecorder();
await performancePage.waitForViewportControls();
await performancePage.startAll();
await performancePage.zoom(10, 10);
await performancePage.zoom(-10, 10);
const [frameRateRecords, longTasksRecords] = await performancePage.stopAll();
const averageFrameRate = performancePage.calculateAverageFrameRate(frameRateRecords);
console.log("FPS", averageFrameRate);
expect(averageFrameRate).toBeGreaterThan(30);
const averageLongTaskDuration = performancePage.calculateAverageLongTaskDuration(longTasksRecords);
console.log("LTS", averageLongTaskDuration);
expect(averageLongTaskDuration).toBeLessThan(100);
});