1
0
Fork 0
mirror of https://github.com/plankanban/planka.git synced 2025-07-19 05:09:43 +02:00

test: Add BDD UI tests using Playwright (#911)

This commit is contained in:
Nalem7 2024-10-18 01:51:48 +05:45 committed by GitHub
parent 4efc3be8d5
commit 096feb35bb
17 changed files with 1260 additions and 120 deletions

View file

@ -0,0 +1,12 @@
module.exports = {
// environment
adminUser: {
email: 'demo@demo.demo',
password: 'demo',
},
baseUrl: process.env.BASE_URL ?? 'http://localhost:1337/',
// playwright
slowMo: parseInt(process.env.SLOW_MO, 10) || 1000,
timeout: parseInt(process.env.TIMEOUT, 10) || 6000,
headless: process.env.HEADLESS !== 'true',
};

View file

@ -1,25 +1,35 @@
const {
After,
Before,
AfterAll,
BeforeAll,
setDefaultTimeout,
} = require("@cucumber/cucumber");
const { createSession, closeSession } = require("nightwatch-api");
// cucumber.conf.js file
setDefaultTimeout(60000);
// runs before all scenarios
BeforeAll(async function () {});
const { Before, BeforeAll, AfterAll, After, setDefaultTimeout } = require('@cucumber/cucumber');
const { chromium } = require('playwright');
const { deleteProject } = require('./testHelpers/apiHelpers');
const config = require('./config');
// runs before each scenario
setDefaultTimeout(config.timeout);
// launch the browser
BeforeAll(async function () {
global.browser = await chromium.launch({
// makes true for CI
headless: config.headless,
slowMo: config.slowMo,
});
});
// close the browser
AfterAll(async function () {
await global.browser.close();
});
// Create a new browser context and page per scenario
Before(async function () {
await createSession();
global.context = await global.browser.newContext();
global.page = await global.context.newPage();
});
// runs after each scenario
// Cleanup after each scenario
After(async function () {
await closeSession();
await deleteProject();
await global.page.close();
await global.context.close();
});
// runs after all scenarios
AfterAll(async function () {});

View file

@ -0,0 +1,10 @@
Feature: dashboard
As a admin
I want to create a project
So that I can manage project
Scenario: create a new project
Given user has browsed to the login page
And user has logged in with email "demo@demo.demo" and password "demo"
When the user creates a project with name "testproject" using the webUI
Then the created project "testproject" should be opened

View file

@ -1,9 +1,27 @@
Feature: login
As a user
As a admin
I want to log in
So that I can manage project
Scenario: User logs in with valid credentials
Given user has browsed to the login page
When user logs in with email "demo@demo.demo" and password "demo" using the webUI
Then the user should be in the dashboard page
Scenario: User logs in with valid credentials
Given user has browsed to the login page
When user logs in with username "demo@demo.demo" and password "demo" using the webUI
Then the user should be in dashboard page
Scenario Outline: login with invalid username and invalid password
Given user has browsed to the login page
When user logs in with username "<username>" and password "<password>" using the webUI
Then user should see the error message "<message>"
Examples:
| username | password | message |
| spiderman | spidy123 | Invalid credentials |
| ironman | iron123 | Invalid credentials |
| aquaman | aqua123 | Invalid credentials |
Scenario: User can log out
Given user has logged in with email "demo@demo.demo" and password "demo"
When user logs out using the webUI
Then the user should be in the login page

View file

@ -0,0 +1,16 @@
class DashboardPage {
constructor() {
this.createProjectIconSelector = `.Projects_addTitle__tXhB4`;
this.projectTitleInputSelector = `input[name="name"]`;
this.createProjectButtonSelector = `//button[text()="Create project"]`;
this.projectTitleSelector = `//div[@class="item Header_item__OOEY7 Header_title__l+wMf"][text()="%s"]`;
}
async createProject(project) {
await page.click(this.createProjectIconSelector);
await page.fill(this.projectTitleInputSelector, project);
await page.click(this.createProjectButtonSelector);
}
}
module.exports = DashboardPage;

View file

@ -0,0 +1,38 @@
const config = require(`../config`);
class LoginPage {
constructor() {
// url
this.homeUrl = config.baseUrl;
this.loginUrl = `${this.homeUrl}login`;
// selectors
this.loginButtonSelector = `//i[@class="right arrow icon"]`;
this.usernameSelector = `//input[@name='emailOrUsername']`;
this.passwordSelector = `//input[@name='password']`;
this.errorMessageSelector = `//div[@class='ui error visible message']`;
this.userActionSelector = `//span[@class="User_initials__9Wp90"]`;
this.logOutSelector = `//a[@class="item UserStep_menuItem__5pvtT"][contains(text(),'Log Out')]`;
}
async goToLoginUrl() {
await page.goto(this.loginUrl);
}
async logOut() {
await page.click(this.userActionSelector);
await page.click(this.logOutSelector);
}
async login(username, password) {
await page.fill(this.usernameSelector, username);
await page.fill(this.passwordSelector, password);
await page.click(this.loginButtonSelector);
}
async getErrorMessage() {
return page.innerText(this.errorMessageSelector);
}
}
module.exports = LoginPage;

View file

@ -1,20 +0,0 @@
module.exports = {
url: function () {
return this.api.launchUrl + "/dashboard";
},
commands: {
isDashboardPage: async function () {
let result = false;
await this.waitForElementVisible("@dashboardHeader");
await this.isVisible("@dashboardHeader", (res) => {
result = res.value;
});
return result;
},
},
elements: {
dashboardHeader: {
selector: "a.Header_title__3SEjb",
},
},
};

View file

@ -1,26 +0,0 @@
module.exports = {
url: function () {
return this.api.launchUrl + "/login";
},
commands: {
logIn: function (email, password) {
return this.waitForElementVisible("@emailInput")
.setValue("@emailInput", email)
.waitForElementVisible("@passwordInput")
.setValue("@passwordInput", password)
.waitForElementVisible("@loginBtn")
.click("@loginBtn");
},
},
elements: {
emailInput: {
selector: "input[name=emailOrUsername]",
},
passwordInput: {
selector: "input[name=password]",
},
loginBtn: {
selector: "form button",
},
},
};

View file

@ -0,0 +1,17 @@
const { When, Then } = require('@cucumber/cucumber');
const util = require('util');
const { expect } = require('playwright/test');
const DashboardPage = require('../pageObjects/DashboardPage');
const dashboardPage = new DashboardPage();
When('the user creates a project with name {string} using the webUI', async function (project) {
await dashboardPage.createProject(project);
});
Then('the created project {string} should be opened', async function (project) {
expect(
await page.locator(util.format(dashboardPage.projectTitleSelector, project)),
).toBeVisible();
});

View file

@ -1,26 +1,53 @@
const { Given, When, Then } = require("@cucumber/cucumber");
const { client } = require("nightwatch-api");
const assert = require("assert");
const { Given, When, Then } = require('@cucumber/cucumber');
const loginPage = client.page.loginPage();
const dashboardPage = client.page.dashboardPage();
// import expect for assertion
const { expect } = require('@playwright/test');
Given("user has browsed to the login page", function () {
return loginPage.navigate();
// import assert
const assert = require('assert');
const LoginPage = require('../pageObjects/LoginPage');
const loginPage = new LoginPage();
Given('user has browsed to the login page', async function () {
await loginPage.goToLoginUrl();
await expect(page).toHaveURL(loginPage.loginUrl);
});
When(
"user logs in with username/email {string} and password {string} using the webUI",
function (username, password) {
return loginPage.logIn(username, password);
}
Given(
'user has logged in with email {string} and password {string}',
async function (username, password) {
await loginPage.goToLoginUrl();
await loginPage.login(username, password);
await expect(page).toHaveURL(loginPage.homeUrl);
},
);
Then("the user should be in the dashboard page", async function () {
const isDashboard = await dashboardPage.isDashboardPage();
assert.strictEqual(
isDashboard,
true,
"Expected to see dashboard page but not visible"
When(
'user logs in with username {string} and password {string} using the webUI',
async function (username, password) {
await loginPage.login(username, password);
},
);
Then('the user should be in dashboard page', async function () {
await expect(page).toHaveURL(loginPage.homeUrl);
});
Then('user should see the error message {string}', async function (errorMessage) {
const actualErrorMessage = await loginPage.getErrorMessage();
assert.equal(
actualErrorMessage,
errorMessage,
`Expected message to be "${errorMessage}" but receive "${actualErrorMessage}"`,
);
});
When('user logs out using the webUI', async function () {
await loginPage.logOut();
});
Then('the user should be in the login page', async function () {
await expect(page).toHaveURL(loginPage.loginUrl);
});

View file

@ -0,0 +1,57 @@
const axios = require('axios');
const config = require('../config');
async function getXauthToken() {
try {
const res = await axios.post(
`${config.baseUrl}api/access-tokens`,
{
emailOrUsername: config.adminUser.email,
password: config.adminUser.password,
},
{
headers: {
'Content-Type': 'application/json',
},
},
);
return res.data.item;
} catch (error) {
return `Error requesting access token: ${error.message}`;
}
}
async function getProjectIDs() {
try {
const res = await axios.get(`${config.baseUrl}api/projects`, {
headers: {
Authorization: `Bearer ${await getXauthToken()}`,
},
});
return res.data.items.map((project) => project.id);
} catch (error) {
return `Error requesting projectIDs: ${error.message}`;
}
}
async function deleteProject() {
try {
const projectIDs = await getProjectIDs();
await Promise.all(
projectIDs.map(async (project) => {
await axios.delete(`${config.baseUrl}api/projects/${project}`, {
headers: {
Authorization: `Bearer ${await getXauthToken()}`,
},
});
}),
);
return true;
} catch (error) {
return `Error deleting project: ${error.message}`;
}
}
module.exports = {
deleteProject,
};

23
client/tests/setup-symlinks.sh Executable file
View file

@ -0,0 +1,23 @@
#!/bin/bash
# This script sets up symbolic links between the client build files and the server directories,
# Navigate to the root directory of the git repository
cd "$(git rev-parse --show-toplevel)" || { echo "Failed to navigate to the git repository root"; exit 1; }
# Store paths for the client build, server public directory, and server views directory
CLIENT_PATH=$(pwd)/client/build
SERVER_PUBLIC_PATH=$(pwd)/server/public
SERVER_VIEWS_PATH=$(pwd)/server/views
# Create symbolic links for the necessary client assets in the server's public and views directories
ln -s ${CLIENT_PATH}/asset-manifest.json ${SERVER_PUBLIC_PATH}/asset-manifest.json && echo "Linked asset-manifest.json successfully"
ln -s ${CLIENT_PATH}/favicon.ico ${SERVER_PUBLIC_PATH}/favicon.ico && echo "Linked favicon.ico successfully"
ln -s ${CLIENT_PATH}/logo192.png ${SERVER_PUBLIC_PATH}/logo192.png && echo "Linked logo192.png successfully"
ln -s ${CLIENT_PATH}/logo512.png ${SERVER_PUBLIC_PATH}/logo512.png && echo "Linked logo512.png successfully"
ln -s ${CLIENT_PATH}/manifest.json ${SERVER_PUBLIC_PATH}/manifest.json && echo "Linked manifest.json successfully"
ln -s ${CLIENT_PATH}/robots.txt ${SERVER_PUBLIC_PATH}/robots.txt && echo "Linked robots.txt successfully"
ln -s ${CLIENT_PATH}/static ${SERVER_PUBLIC_PATH}/static && echo "Linked static folder successfully"
ln -s ${CLIENT_PATH}/index.html ${SERVER_VIEWS_PATH}/index.ejs && echo "Linked index.html to index.ejs successfully"
echo "Setup symbolic links completed successfully."