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
|
.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
|
||||||
|
|||||||
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 {} +",
|
"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",
|
||||||
|
|||||||
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