feat: e2e testing with playwright and cucumber js #2914
This commit is contained in:
parent
04e03a83b4
commit
7577743bda
1
Makefile
1
Makefile
@ -21,6 +21,7 @@ test: | test-frontend test-backend ## Run all tests
|
||||
|
||||
.PHONY: test-frontend
|
||||
test-frontend: ## Run frontend tests
|
||||
$Q cd frontend && npm run test:e2e tests/acceptance/features/
|
||||
|
||||
.PHONY: test-backend
|
||||
test-backend: ## Run backend tests
|
||||
|
||||
4167
frontend/package-lock.json
generated
4167
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,7 +11,8 @@
|
||||
"clean": "find ./dist -maxdepth 1 -mindepth 1 ! -name '.gitkeep' -exec rm -r {} +",
|
||||
"lint": "eslint --ext .vue,.js 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": {
|
||||
"ace-builds": "^1.23.4",
|
||||
@ -41,6 +42,9 @@
|
||||
"whatwg-fetch": "^3.6.17"
|
||||
},
|
||||
"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-vue2": "^2.2.0",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
@ -49,6 +53,7 @@
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-vue": "^9.16.1",
|
||||
"jsdom": "^22.1.0",
|
||||
"playwright": "^1.40.1",
|
||||
"postcss": "^8.4.31",
|
||||
"prettier": "^3.0.1",
|
||||
"terser": "^5.19.2",
|
||||
|
||||
23
frontend/tests/acceptance/features/login.feature
Normal file
23
frontend/tests/acceptance/features/login.feature
Normal 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 | |
|
||||
| | |
|
||||
26
frontend/tests/acceptance/features/resource.feature
Normal file
26
frontend/tests/acceptance/features/resource.feature
Normal 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
|
||||
47
frontend/tests/acceptance/pageObject/HomePage.js
Normal file
47
frontend/tests/acceptance/pageObject/HomePage.js
Normal 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"));
|
||||
}
|
||||
}
|
||||
33
frontend/tests/acceptance/pageObject/LoginPage.js
Normal file
33
frontend/tests/acceptance/pageObject/LoginPage.js
Normal 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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
107
frontend/tests/acceptance/stepDefinitions/homeContext.js
Normal file
107
frontend/tests/acceptance/stepDefinitions/homeContext.js
Normal 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();
|
||||
});
|
||||
34
frontend/tests/acceptance/stepDefinitions/loginContext.js
Normal file
34
frontend/tests/acceptance/stepDefinitions/loginContext.js
Normal 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`
|
||||
);
|
||||
});
|
||||
32
frontend/tests/cucumber.conf.js
Normal file
32
frontend/tests/cucumber.conf.js
Normal 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();
|
||||
});
|
||||
62
frontend/tests/helper/file_helper.js
Normal file
62
frontend/tests/helper/file_helper.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user