Install Playwright and Prettier.

Add tests for login and creating shapes
This commit is contained in:
Aliaksei Kalennikau 2022-11-15 20:21:22 +01:00
parent aab7429a34
commit 9a3f8baa2e
19 changed files with 613 additions and 0 deletions

4
.gitignore vendored
View File

@ -102,3 +102,7 @@ dist
# TernJS port file
.tern-port
/test-results/
/playwright-report/
/playwright/.cache/
/.idea/

View File

@ -1,2 +1,3 @@
# penpotqa
QA Test for Penpot

20
fixtures.js Normal file
View File

@ -0,0 +1,20 @@
const base = require("@playwright/test");
const { LoginPage } = require("./pages/login-page.js");
const { DashboardPage } = require("./pages/dashboard-page.js");
const mainTest = 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.isHeaderDisplayed("Projects");
await dashboardPage.deleteFileIfExists();
await dashboardPage.createFile();
await use(page);
},
});
exports.mainTest = mainTest;

BIN
images/images.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

112
package-lock.json generated Normal file
View File

@ -0,0 +1,112 @@
{
"name": "penpotqa",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "penpotqa",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"prettier": "^2.7.1"
},
"devDependencies": {
"@playwright/test": "^1.27.1",
"dotenv": "^16.0.3"
}
},
"node_modules/@playwright/test": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
"integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
"dev": true,
"dependencies": {
"@types/node": "*",
"playwright-core": "1.27.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"node_modules/dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"dev": true,
"engines": {
"node": ">=12"
}
},
"node_modules/playwright-core": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
"integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
"dev": true,
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=14"
}
},
"node_modules/prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
}
},
"dependencies": {
"@playwright/test": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.27.1.tgz",
"integrity": "sha512-mrL2q0an/7tVqniQQF6RBL2saskjljXzqNcCOVMUjRIgE6Y38nCNaP+Dc2FBW06bcpD3tqIws/HT9qiMHbNU0A==",
"dev": true,
"requires": {
"@types/node": "*",
"playwright-core": "1.27.1"
}
},
"@types/node": {
"version": "18.11.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==",
"dev": true
},
"dotenv": {
"version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"dev": true
},
"playwright-core": {
"version": "1.27.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.27.1.tgz",
"integrity": "sha512-9EmeXDncC2Pmp/z+teoVYlvmPWUC6ejSSYZUln7YaP89Z6lpAaiaAnqroUt/BoLo8tn7WYShcfaCh+xofZa44Q==",
"dev": true
},
"prettier": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g=="
}
}
}

28
package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "penpotqa",
"version": "1.0.0",
"description": "QA Test for Penpot",
"main": "index.js",
"scripts": {
"test": "npx playwright test",
"prettier": "npx prettier --write ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/penpot/penpotqa.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/penpot/penpotqa/issues"
},
"homepage": "https://github.com/penpot/penpotqa#readme",
"devDependencies": {
"@playwright/test": "^1.27.1",
"dotenv": "^16.0.3"
},
"dependencies": {
"prettier": "^2.7.1"
}
}

39
pages/dashboard-page.js Normal file
View File

@ -0,0 +1,39 @@
const { expect } = require("@playwright/test");
exports.DashboardPage = class DashboardPage {
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
this.header = page.locator("h1");
this.numberOfFilesText = page.locator(
'div[class="project-name-wrapper"] span[class="info"]'
);
this.fileTile = page.locator('div[class="item-info"]');
this.deleteFileMenuItem = page.locator('a[data-test="file-delete"]');
this.deletesFileButton = page.locator('input[value="Delete files"]');
this.createFileButton = page.locator('button[class="create-new"]');
}
async isHeaderDisplayed(title) {
await expect(this.header).toBeVisible();
await expect(this.header).toHaveText(title);
}
async createFile() {
await this.createFileButton.click();
}
async deleteFile() {
await this.fileTile.click({ button: "right" });
await this.deleteFileMenuItem.click();
await this.deletesFileButton.click();
}
async deleteFileIfExists() {
const text = (await this.numberOfFilesText.innerText()).valueOf();
if (!text.includes("0 files")) {
await this.deleteFile();
}
}
};

68
pages/login-page.js Normal file
View File

@ -0,0 +1,68 @@
const { expect } = require("@playwright/test");
exports.LoginPage = class LoginPage {
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
this.page = page;
this.emailInput = page.locator("#email");
this.pwdInput = page.locator("#password");
this.loginButton = page.locator('input[name="submit"]');
this.emailInputError = page.locator(
'div[class=" invalid with-icon custom-input"] #email'
);
this.pwdInputError = page.locator(
'div[class=" invalid empty with-icon custom-input"] #password'
);
this.section = page.locator('section[class="auth-content"]');
this.loginErrorBanner = page.locator('div[data-test="login-banner"]');
}
async goto() {
await this.page.goto("/#/auth/login");
}
async enterEmail(loginEmail) {
await this.emailInput.fill(loginEmail);
}
async enterPwd(loginPwd) {
await this.pwdInput.fill(loginPwd);
}
async clickLoginButton() {
await this.loginButton.click();
}
async clickPwdInput() {
await this.pwdInput.click();
}
async isEmailInputErrorDisplayed() {
await expect(this.emailInputError).toBeVisible();
}
async isPwdInputErrorDisplayed() {
await expect(this.pwdInputError).toBeVisible();
}
async isLoginButtonDisplayed() {
await expect(this.loginButton).toBeVisible();
}
async isLoginButtonDisabled() {
await expect(this.loginButton).toBeDisabled();
}
async clickSection() {
await this.section.click();
}
async isLoginErrorBannerDisplayed() {
await expect(this.loginErrorBanner).toBeVisible();
}
async isLoginErrorMessageDisplayed(message) {
await expect(this.loginErrorBanner).toHaveText(message);
}
};

89
pages/main-page.js Normal file
View File

@ -0,0 +1,89 @@
const { expect } = require("@playwright/test");
const viewportLocator = 'div[class="viewport"]';
exports.MainPage = class MainPage {
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
this.page = page;
this.createBoardButton = page.locator('button[data-test="artboard-btn"]');
this.createRectangleButton = page.locator('button[data-test="rect-btn"]');
this.createEllipseButton = page.locator('button[data-test="ellipse-btn"]');
this.createTextButton = page.locator('button[alt="Text (T)"]');
this.uploadImageSelector = page.locator("#image-upload");
this.createCurveButton = page.locator('button[data-test="curve-btn"]');
this.createPathButton = page.locator('button[data-test="path-btn"]');
this.createdObject = page.locator('div *[id^="fills"]');
this.textbox = page.locator(
'div[role="textbox"] div[contenteditable="true"]'
);
this.viewport = page.locator(viewportLocator);
this.savedChangesIcon = page.locator('div[class="saved"]');
}
async clickCreateBoardButton() {
await this.createBoardButton.click();
}
async clickCreateRectangleButton() {
await this.createRectangleButton.click();
}
async clickCreateEllipseButton() {
await this.createEllipseButton.click();
}
async clickCreateTextButton() {
await this.createTextButton.click();
}
async typeText(text) {
await this.textbox.fill(text);
}
async uploadImage(imageUrl) {
await this.uploadImageSelector.setInputFiles(imageUrl);
}
async clickCreateCurveButton() {
await this.createCurveButton.click();
}
async clickCreatePathButton() {
await this.createPathButton.click();
}
async clickViewport() {
await this.viewport.click();
await this.viewport.click();
}
async clickViewportByCoordinates(x, y) {
await this.page.mouse.click(x, y);
}
async waitForChangeIsSaved() {
await expect(this.savedChangesIcon).toBeVisible();
}
async isCreatedObjectVisible() {
await expect(this.createdObject.nth(0)).toBeVisible();
}
async checkHtmlOfCreatedObject(expectedHTML) {
expect(await this.createdObject.nth(0).innerHTML()).toEqual(expectedHTML);
}
async checkPartialHtmlOfCreatedObject(expectedHTML) {
expect(await this.createdObject.nth(0).innerHTML()).toContain(expectedHTML);
}
async drawCurve(x1, y1, x2, y2) {
await this.page.mouse.move(x1, y1);
await this.page.mouse.down();
await this.page.mouse.move(x1, y1);
await this.page.mouse.move(x2, y2);
await this.page.mouse.up();
}
};

113
playwright.config.js Normal file
View File

@ -0,0 +1,113 @@
// @ts-check
const { devices } = require("@playwright/test");
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
require("dotenv").config();
/**
* @see https://playwright.dev/docs/test-configuration
* @type {import('@playwright/test').PlaywrightTestConfig}
*/
const config = {
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.BASE_URL,
browserName: "chromium",
headless: false,
viewport: null,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
launchOptions: {
args: ["--start-maximized"],
},
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
// use: {
// ...devices['Desktop Chrome'],
// },
},
// {
// name: 'firefox',
// use: {
// ...devices['Desktop Firefox'],
// },
// },
//
// {
// name: 'webkit',
// use: {
// ...devices['Desktop Safari'],
// },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
};
module.exports = config;

View File

@ -0,0 +1,93 @@
// @ts-check
const { mainTest } = require("../fixtures");
const { MainPage } = require("../pages/main-page");
const { expect } = require("@playwright/test");
mainTest("Create a board", async ({ page }) => {
const mainPage = new MainPage(page);
await mainPage.clickCreateBoardButton();
await mainPage.clickViewport();
await mainPage.waitForChangeIsSaved();
await mainPage.isCreatedObjectVisible();
await mainPage.checkHtmlOfCreatedObject(
'<rect rx="0" ry="0" x="630" y="410" transform="" width="100" height="100" class="frame-background" style="fill: rgb(255, 255, 255); fill-opacity: 1;"></rect>'
);
await expect(page).toHaveScreenshot("board.png");
});
mainTest("Create a rectangle", async ({ page }) => {
const mainPage = new MainPage(page);
await mainPage.clickCreateRectangleButton();
await mainPage.clickViewport();
await mainPage.waitForChangeIsSaved();
await mainPage.isCreatedObjectVisible();
await mainPage.checkHtmlOfCreatedObject(
'<rect rx="0" ry="0" x="630" y="410" transform="" width="100" height="100" style="fill: rgb(177, 178, 181); fill-opacity: 1;"></rect>'
);
await expect(page).toHaveScreenshot("rectangle.png");
});
mainTest("Create an ellipse", async ({ page }) => {
const mainPage = new MainPage(page);
await mainPage.clickCreateEllipseButton();
await mainPage.clickViewport();
await mainPage.waitForChangeIsSaved();
await mainPage.isCreatedObjectVisible();
await mainPage.checkHtmlOfCreatedObject(
'<ellipse rx="50" ry="50" cx="680" cy="460" transform="" style="fill: rgb(177, 178, 181); fill-opacity: 1;"></ellipse>'
);
await expect(page).toHaveScreenshot("ellipse.png");
});
mainTest("Create an text", async ({ page }) => {
const mainPage = new MainPage(page);
await mainPage.clickCreateTextButton();
await mainPage.clickViewport();
await mainPage.typeText("Hello World!");
await mainPage.waitForChangeIsSaved();
await mainPage.isCreatedObjectVisible();
await mainPage.checkHtmlOfCreatedObject(
'<text y="459" textLength="72.4375" lengthAdjust="spacingAndGlyphs" x="680" dominant-baseline="text-before-edge" style="text-transform: none; font-family: sourcesanspro; letter-spacing: normal; font-style: normal; font-weight: 400; white-space: pre; font-size: 14px; text-decoration: none solid rgb(0, 0, 0); direction: ltr; fill: rgb(0, 0, 0); fill-opacity: 1;">Hello World!</text>'
);
await expect(page).toHaveScreenshot("text.png");
});
mainTest("Import an image", async ({ page }) => {
const mainPage = new MainPage(page);
await mainPage.uploadImage("images/images.png");
await mainPage.clickViewport();
await mainPage.waitForChangeIsSaved();
await mainPage.isCreatedObjectVisible();
await mainPage.checkPartialHtmlOfCreatedObject(
'<rect rx="0" ry="0" x="528.5" y="395.5" transform="" width="303" height="130" fill='
);
await expect(page).toHaveScreenshot("image.png");
});
mainTest("Create a curve", async ({ page }) => {
const mainPage = new MainPage(page);
await mainPage.clickCreateCurveButton();
await mainPage.drawCurve(900, 300, 600, 200);
await mainPage.waitForChangeIsSaved();
await mainPage.isCreatedObjectVisible();
await mainPage.checkHtmlOfCreatedObject(
'<path rx="0" ry="0" d="M596,252L296,152"></path>'
);
await expect(page).toHaveScreenshot("curve.png");
});
mainTest("Create a path", async ({ page }) => {
const mainPage = new MainPage(page);
await mainPage.clickCreatePathButton();
await mainPage.clickViewportByCoordinates(500, 200);
await mainPage.clickViewportByCoordinates(1200, 700);
await mainPage.clickViewportByCoordinates(1000, 400);
await mainPage.clickViewportByCoordinates(500, 200);
await mainPage.clickViewport();
await mainPage.waitForChangeIsSaved();
await mainPage.isCreatedObjectVisible();
await mainPage.checkHtmlOfCreatedObject(
'<path rx="0" ry="0" d="M196,152L896,652L696,352L196,152ZM680,460"></path>'
);
await expect(page).toHaveScreenshot("path.png");
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

46
tests/login.spec.js Normal file
View File

@ -0,0 +1,46 @@
// @ts-check
const { test } = require("@playwright/test");
const { LoginPage } = require("../pages/login-page");
const { DashboardPage } = require("../pages/dashboard-page");
test("Login with an email address", async ({ page }) => {
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.isHeaderDisplayed("Projects");
});
test("Login with invalid email address", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.enterEmail("test@com");
await loginPage.enterPwd(process.env.LOGIN_PWD);
await loginPage.isEmailInputErrorDisplayed();
await loginPage.isLoginButtonDisplayed();
await loginPage.isLoginButtonDisabled();
});
test("Login with no password", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.enterEmail(process.env.LOGIN_EMAIL);
await loginPage.clickPwdInput();
await loginPage.clickSection();
await loginPage.isPwdInputErrorDisplayed();
await loginPage.isLoginButtonDisplayed();
await loginPage.isLoginButtonDisabled();
});
test("Login with incorrect password", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.enterEmail(process.env.LOGIN_EMAIL);
await loginPage.enterPwd("11223344");
await loginPage.clickLoginButton();
await loginPage.isLoginErrorMessageDisplayed(
"Username or password seems to be wrong."
);
});