feat: e2e testing with playwright and cucumber js #2914

This commit is contained in:
Yogesh070 2024-01-07 19:24:17 +05:45
parent 04e03a83b4
commit 7577743bda
11 changed files with 2876 additions and 1663 deletions

View File

@ -21,6 +21,7 @@ test: | test-frontend test-backend ## Run all tests
.PHONY: test-frontend .PHONY: test-frontend
test-frontend: ## Run frontend tests test-frontend: ## Run frontend tests
$Q cd frontend && npm run test:e2e tests/acceptance/features/
.PHONY: test-backend .PHONY: test-backend
test-backend: ## Run backend tests test-backend: ## Run backend tests

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,8 @@
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +", "clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
"lint": "eslint --ext .vue,.js src/", "lint": "eslint --ext .vue,.js src/",
"lint:fix": "eslint --ext .vue,.js --fix src/", "lint:fix": "eslint --ext .vue,.js --fix src/",
"format": "prettier --write ." "format": "prettier --write .",
"test:e2e": "cucumber-js --import tests/cucumber.conf.js --import tests/acceptance/stepDefinitions/**/*.js --format @cucumber/pretty-formatter"
}, },
"dependencies": { "dependencies": {
"ace-builds": "^1.23.4", "ace-builds": "^1.23.4",
@ -41,6 +42,9 @@
"whatwg-fetch": "^3.6.17" "whatwg-fetch": "^3.6.17"
}, },
"devDependencies": { "devDependencies": {
"@cucumber/cucumber": "^10.1.0",
"@cucumber/pretty-formatter": "^1.0.0",
"@playwright/test": "^1.40.1",
"@vitejs/plugin-legacy": "^4.1.1", "@vitejs/plugin-legacy": "^4.1.1",
"@vitejs/plugin-vue2": "^2.2.0", "@vitejs/plugin-vue2": "^2.2.0",
"@vue/eslint-config-prettier": "^8.0.0", "@vue/eslint-config-prettier": "^8.0.0",
@ -49,6 +53,7 @@
"eslint-plugin-prettier": "^5.0.0", "eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.16.1", "eslint-plugin-vue": "^9.16.1",
"jsdom": "^22.1.0", "jsdom": "^22.1.0",
"playwright": "^1.40.1",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "^3.0.1", "prettier": "^3.0.1",
"terser": "^5.19.2", "terser": "^5.19.2",

View File

@ -0,0 +1,23 @@
Feature: Login
As an admin
I want to login into the application
So I can manage my files
Background:
Given admin has browsed to the login page
Scenario: admin logs in with correct credentials
When admin logs in with username as 'admin' and password as 'admin'
Then admin should be navigated to homescreen
Scenario Outline: admin logs in with incorrect credentials
When admin logs in with username as "<username>" and password as "<password>"
Then admin should see "Wrong credentials" message
Examples:
| username | password |
| user99 | admin |
| admin | user99 |
| | admin |
| admin | |
| | |

View File

@ -0,0 +1,26 @@
Feature: Create a new resource
As a admin
I want to be able to create a new files and folders
So that I can organize my files and folders
Background:
Given "admin" has logged in
And admin has navigated to the homepage
Scenario: Create a new folder
When admin creates a new folder named "myFolder"
Then admin should be able to see a folder named "myFolder"
Scenario: Create a new file with content
When admin creates a new file named "myFile.txt" with content "Hello World"
Then admin should be able to see a file named "myFile.txt" with content "Hello World"
Scenario: Rename a file
Given admin has created a file named "oldfile.txt" with content "Hello World"
When admin renames a file "oldfile.txt" to "newfile.txt"
Then admin should be able to see file with "newfile.txt" name
Scenario: Delete a file
Given admin creates a new file named "delMyFile.txt" using API
When admin deletes a file named "delMyFile.txt"
Then admin shouln't see "delMyFile" in the UI

View File

@ -0,0 +1,47 @@
import { format } from "util";
import { filesToDelete, swapFileOnRename } from "../../helper/file_helper.js";
export class HomePage {
constructor() {
this.dialogInputSelector = '//input[@class="input input--block"]';
this.lastNavaigatedFolderSelector =
'//div[@class="breadcrumbs"]/span[last()]/a';
this.contentEditorSelector = '//textarea[@class="ace_text-input"]';
this.editorContent = '//div[@class="ace_line"]';
this.buttonSelector = `//button[@title="%s"]`;
this.fileSelector = `//div[@aria-label="%s"]`;
this.cardActionSelector = '//div[@class="card-action"]/button[@title="%s"]';
}
async createNewFolder(folderName) {
await global.page.click(format(this.buttonSelector, "New folder"));
await global.page.fill(this.dialogInputSelector, folderName);
await global.page.click(format(this.cardActionSelector, "Create"));
}
async createFileWithContent(filename, content) {
await global.page.click(format(this.buttonSelector, "New file"));
await global.page.fill(this.dialogInputSelector, filename);
await global.page.click(format(this.cardActionSelector, "Create"));
await global.page.fill(this.contentEditorSelector, content);
await global.page.click(format(this.buttonSelector, "Save"));
await global.page.click(format(this.buttonSelector, "Close"));
//saving the file info into global array to delete later
filesToDelete.push(filename);
}
async renameFile(oldfileName, newfileName) {
await global.page.click(format(this.fileSelector, oldfileName));
await global.page.click(format(this.buttonSelector, "Rename"));
await global.page.fill(this.dialogInputSelector, newfileName);
await global.page.click(format(this.cardActionSelector, "Rename"));
await swapFileOnRename(oldfileName, newfileName);
}
async deleteFile(filename) {
await global.page.click(format(this.fileSelector, filename));
await global.page.click(format(this.buttonSelector, "Delete"));
await global.page.click(format(this.cardActionSelector, "Delete"));
}
}

View File

@ -0,0 +1,33 @@
export class LoginPage {
constructor() {
this.usernameSelector = '//input[@type="text"]';
this.passwordSelector = '//input[@type="password"]';
this.loginButton = '//input[@type="submit"]';
this.wrongCredentialsDivSelector = '//div[@class="wrong"]';
this.baseURL = "http://localhost:8080/";
}
async navigateToLoginPage() {
await global.page.goto(this.baseURL + "login");
}
async loginWithUsernameAndPassword(username, password) {
await global.page.fill(this.usernameSelector, username);
await global.page.fill(this.passwordSelector, password);
await global.page.click(this.loginButton);
}
async loginBasedOnRole(role) {
this.navigateToLoginPage();
switch (role) {
case "admin":
await this.loginWithUsernameAndPassword("admin", "admin");
break;
case "user":
await this.loginWithUsernameAndPassword("user", "user");
break;
default:
throw new Error(`Invalid role ${role} passed`);
}
}
}

View File

@ -0,0 +1,107 @@
import { Given, When, Then } from "@cucumber/cucumber";
import { equal } from "assert";
import { format } from "util";
import { expect } from "@playwright/test";
import { HomePage } from "../PageObject/HomePage.js";
import { LoginPage } from "../PageObject/LoginPage.js";
import { createFile } from "../../helper/file_helper.js";
const login = new LoginPage();
const homepage = new HomePage();
Given("{string} has logged in", async function (role) {
await login.loginBasedOnRole(role);
});
Given("admin has navigated to the homepage", async function () {
await expect(global.page).toHaveURL(login.baseURL + "files/");
});
When("admin creates a new folder named {string}", async function (folderName) {
await homepage.createNewFolder(folderName);
});
Then(
"admin should be able to see a folder named {string}",
async function (folderName) {
const userCreatedFolderName = await global.page.innerHTML(
homepage.lastNavaigatedFolderSelector
);
equal(
userCreatedFolderName,
folderName,
`Expected "${folderName}" but recieved message "${userCreatedFolderName}" from UI`
);
}
);
Given(
"admin has created a file named {string} with content {string}",
async function (filename, content) {
await homepage.createFileWithContent(filename, content);
await expect(
global.page.locator(format(homepage.fileSelector, filename))
).toBeVisible();
}
);
When(
"admin creates a new file named {string} with content {string}",
async function (filename, content) {
await homepage.createFileWithContent(filename, content);
}
);
Given(
"admin creates a new file named {string} using API",
async function (filename) {
await createFile(filename);
await global.page.reload();
await expect(
global.page.locator(format(homepage.fileSelector, filename))
).toBeVisible();
}
);
Then(
"admin should be able to see a file named {string} with content {string}",
async function (filename, content) {
await expect(
global.page.locator(format(homepage.fileSelector, filename))
).toBeVisible();
await global.page.dblclick(format(homepage.fileSelector, filename));
const fileContent = await global.page.innerHTML(homepage.editorContent);
equal(
fileContent,
content,
`Expected content as "${content}" but recieved "${fileContent}"`
);
}
);
When(
"admin renames a file {string} to {string}",
async function (oldfileName, newfileName) {
await homepage.renameFile(oldfileName, newfileName);
}
);
Then(
"admin should be able to see file with {string} name",
async function (newfileName) {
await expect(
global.page.locator(format(homepage.fileSelector, newfileName))
).toBeVisible();
}
);
When("admin deletes a file named {string}", async function (filename) {
await homepage.deleteFile(filename);
});
Then("admin shouln't see {string} in the UI", async function (filename) {
await expect(
global.page.locator(format(homepage.fileSelector, filename))
).toBeHidden();
});

View File

@ -0,0 +1,34 @@
import { Given, When, Then } from "@cucumber/cucumber";
import { expect } from "@playwright/test";
import { equal } from "assert";
import { LoginPage } from "../PageObject/LoginPage.js";
const login = new LoginPage();
Given("admin has browsed to the login page", async () => {
await login.navigateToLoginPage();
await expect(global.page).toHaveURL(login.baseURL + "login");
});
When(
"admin logs in with username as {string} and password as {string}",
async (username, password) => {
await login.loginWithUsernameAndPassword(username, password);
}
);
Then("admin should be navigated to homescreen", async function () {
await expect(global.page).toHaveURL(login.baseURL + "files/");
});
Then("admin should see {string} message", async function (expectedMessage) {
const errorMessage = await global.page.innerHTML(
login.wrongCredentialsDivSelector
);
equal(
errorMessage,
expectedMessage,
`Expected message string "${expectedMessage}" but received message "${errorMessage}" from UI`
);
});

View File

@ -0,0 +1,32 @@
import {
Before,
BeforeAll,
AfterAll,
After,
setDefaultTimeout,
} from "@cucumber/cucumber";
import { chromium } from "@playwright/test";
import { cleanUpTempFiles } from "./helper/file_helper.js";
setDefaultTimeout(60000);
BeforeAll(async function () {
global.browser = await chromium.launch({
headless: true,
});
});
AfterAll(async function () {
await global.browser.close();
});
Before(async function () {
global.context = await global.browser.newContext();
global.page = await global.context.newPage();
});
After(async function () {
await global.page.close();
await global.context.close();
await cleanUpTempFiles();
});

View File

@ -0,0 +1,62 @@
const BASE_URL = "http://localhost:8080";
export async function getXauthToken() {
try {
const res = await fetch(`${BASE_URL}/api/login`, {
method: "POST",
body: JSON.stringify({
username: "admin",
password: "admin",
}),
});
return await res.text();
} catch (error) {
console.error("Error requesting acces token:", error);
}
}
export const filesToDelete = [];
export async function deleteFile(filename) {
try {
const res = await fetch(`${BASE_URL}/api/resources/${filename}`, {
method: "DELETE",
headers: {
"X-Auth": await getXauthToken(),
},
});
if (res.status == 200) {
//remove the deleted file from filesToDelete array
const fileIndex = filesToDelete.findIndex((file) => file == filename);
filesToDelete.splice(fileIndex, 1);
}
} catch (error) {
console.error("Error deleting file:", error);
}
}
export async function createFile(filename) {
try {
await fetch(`${BASE_URL}/api/resources/${filename}`, {
method: "POST",
headers: {
"X-Auth": await getXauthToken(),
},
});
} catch (error) {
console.error("Error creating file:", error);
}
}
export const swapFileOnRename = async (oldfileName, newfileName) => {
const fileToSwapIndex = filesToDelete.findIndex(
(file) => file == oldfileName
);
filesToDelete[fileToSwapIndex] = newfileName;
};
export async function cleanUpTempFiles() {
for (let i = 0; i < filesToDelete.length; i++) {
await deleteFile(filesToDelete[i]);
}
}