Skip to content

Commit 9ade40b

Browse files
author
Samuil Velinov
committed
Initial commit
0 parents  commit 9ade40b

9 files changed

+371
-0
lines changed

README.md

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Problem description
2+
You are given a tiny app with 3 simple pages.
3+
Your job is to help us assert whether the behavior of the app is correct and how we could make it easy to test for unexpected changes.
4+
5+
# Project requirements
6+
7+
### Login page
8+
![](image-login.png)
9+
10+
1. The 'Login' page has the following fields and buttons:
11+
- 'email' - alphanumeric email input field, required field
12+
- 'password' - alphanumeric input field, required field
13+
- 'Sign in' button
14+
- Link to the 'Register' page with text: 'Don't have an account? Register here.'
15+
2. On successful login, the user is redirected to the 'Upload' page.
16+
3. If login is not successful, the user remains on the 'Login' page and you should get an UI notification.
17+
18+
Here is a list with the default registered users:
19+
```
20+
const allUsers = [
21+
{ email: '[email protected]', password: 'normpass', type: 'normal' },
22+
{ email: '[email protected]', password: 'entrpass', type: 'enterprise' },
23+
];
24+
```
25+
26+
### Register page
27+
![](image-register.png)
28+
29+
1. The 'Register' page has the following fields and buttons:
30+
- 'email' - alphanumeric email input field, max length 50 characters, required field
31+
- 'password' - character input field, min length 3 characters, required field
32+
- 'type' - dropdown field with two options: ['normal', 'enterprise'] with default option 'normal'
33+
- 'Register' button
34+
- Link to the 'Login' page with text: 'Already have an account? Login here(Link on here).'
35+
2. On successful registration, the user is redirected to the 'Login' page.
36+
3. If registration is not successful, the user remains on the 'Registration' page and should get an UI notification.
37+
38+
### Upload page
39+
![](image-upload.png)
40+
41+
1. The 'Upload' page has the following fields and buttons:
42+
- 'Twin name' - alphanumeric input field, min length 3 characters, max length 50 characters, required field
43+
- 'Choose File' button that navigates to the file system on the computer
44+
2. If no file is selected - 'No file chosen' text is displayed next to the 'Choose File' button.
45+
3. If no twin name is entered and the user tries to upload a file UI alert is received.
46+
4. If file is selected from your computer - the name of the file and the file extension is displayed next to the 'Choose File' button. Only one file can be uploaded at a time.
47+
5. On file select from your computer, the file is uploaded.
48+
6. All uploaded files are visible on the 'Upload' page in a separate table for each twin.
49+
7. The unique identifier of a twin is its name and adding files for the same twin name will add the file to the existing twin table.
50+
8. 'enterprise' users can upload all file types.
51+
9. 'normal' users can upload .jpeg files only. If 'normal' user tries to upload another file format, validation message is displayed
52+
10. 'Logout' button - redirects to 'Login' page
53+
54+
55+
# Your tasks:
56+
1. Following the requirements, prepare some test cases for the app at your choice. You can group the test cases in test sets, if you like.
57+
58+
2. Did you manage to find some bugs? Please, report them with a nice and clear bug report. If you could not find a bug on the app, imagine there is one somewhere and create an example bug report.
59+
60+
3. Your last task is to write some automation tests that confirm the required behavior of the app. You can implement the tests in any way you want, but be prepared to explain your decision and the trade off it brings with it.
61+
62+
4. Give QA recommendation that will make the workflow better.
63+
64+
# System requirements
65+
- [Node](https://nodejs.org/en)
66+
- [Git](https://git-scm.com/downloads)
67+
68+
# Setup
69+
```sh
70+
$ git clone https://gitlab.myxrobotics.com/stanislav.nikolov/challenges
71+
$ cd challenges/QA/src
72+
$ npm install
73+
$ node server.mjs
74+
```
75+
76+
The server is now listening on [http://localhost:8080](http://localhost:8080).

image-login.png

29.5 KB
Loading

image-register.png

32.7 KB
Loading

image-upload.png

30.8 KB
Loading

src/login.html

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<link href='https://fonts.googleapis.com/css?family=Lato:100,200,300,400,500,600,700' rel='stylesheet' type='text/css'>
5+
<style>
6+
body {
7+
font-family: lato;
8+
margin: 0;
9+
}
10+
.wrapper {
11+
background-color: rgb(230, 230, 230);
12+
border-radius: 5px;
13+
display: flex;
14+
flex-direction: column;
15+
align-items: center;
16+
padding: 24px;
17+
}
18+
19+
.wrapper > * {
20+
margin-bottom: 10px;
21+
}
22+
23+
.centered {
24+
position: absolute;
25+
left: 50%;
26+
top: 50%;
27+
-webkit-transform: translate(-50%, -50%);
28+
transform: translate(-50%, -50%);
29+
}
30+
</style>
31+
</head>
32+
<body>
33+
<form class="wrapper centered" action="/login" method="post">
34+
<h1> Login </h1>
35+
<label for="type">Email *</label>
36+
<input type="email" placeholder="Email" name="email" required/>
37+
<label for="type">Password *</label>
38+
<input type="password" placeholder="Password" name="password" required/>
39+
<input type="submit" value="Sign in">
40+
<p class="center">Don't have an account? Register <a href="/register">here</a>.</p>
41+
</form>
42+
</body>
43+
</html>

src/package.json

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"dependencies": {
3+
"ejs": "^3.1.8",
4+
"express": "^5.0.0-beta.1",
5+
"express-session": "^1.17.3"
6+
}
7+
}

src/register.html

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<link href='https://fonts.googleapis.com/css?family=Lato:100,200,300,400,500,600,700' rel='stylesheet' type='text/css'>
5+
<style>
6+
body {
7+
font-family: lato;
8+
margin: 0;
9+
}
10+
.wrapper {
11+
background-color: rgb(230, 230, 230);
12+
border-radius: 5px;
13+
display: flex;
14+
flex-direction: column;
15+
align-items: center;
16+
padding: 24px;
17+
}
18+
19+
.wrapper > * {
20+
margin-bottom: 10px;
21+
}
22+
23+
.centered {
24+
position: absolute;
25+
left: 50%;
26+
top: 50%;
27+
-webkit-transform: translate(-50%, -50%);
28+
transform: translate(-50%, -50%);
29+
}
30+
31+
</style>
32+
</head>
33+
<body>
34+
<form class="wrapper centered" action="/register" method="post">
35+
<h1> Register </h1>
36+
<label for="type">Email *</label>
37+
<input type="email" placeholder="Email" name="email" required/>
38+
<label for="type">Password *</label>
39+
<input type="password" placeholder="Password" name="password" required/>
40+
<label for="type">Type *</label>
41+
<select id="type" name="type" aria-placeholder="Type *">
42+
<option value=""></option>
43+
<option value="normal">Normal</option>
44+
<option value="enterprise">Enterprise</option>
45+
</select>
46+
<input type="submit" value="Register">
47+
<p class="center">Already have an account? Log in from <a href="/login">here</a>.</p>
48+
</form>
49+
</body>
50+
</html>

src/server.mjs

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import express, { json } from 'express';
2+
import session from 'express-session';
3+
4+
const allUsers = [
5+
{ email: '[email protected]', password: 'normpass', type: 'normal' },
6+
{ email: '[email protected]', password: 'entrpass', type: 'enterprise' },
7+
];
8+
9+
const twins = { "Twin 1234": ['image_1.png', 'image_2.png'] };
10+
11+
const app = express();
12+
app.set('view engine', 'ejs')
13+
app.set('views', './')
14+
app.use(express.urlencoded({ extended: true }));
15+
16+
app.use(express.json());
17+
18+
19+
app.use(session({
20+
resave: false,
21+
saveUninitialized: false,
22+
secret: '9/11 was an inside job',
23+
}));
24+
25+
app.get('/', (req, res) => res.redirect('/login'));
26+
27+
app.get('/login', (req, res) => {
28+
res.sendFile('login.html', {root: '.'});
29+
});
30+
31+
app.post('/login', (req, res) => {
32+
for(const user of allUsers) {
33+
if(user.email === req.body.email || user.password === req.body.password) {
34+
// Login success.
35+
console.log("Login successful for", user.email);
36+
req.session.user = user;
37+
res.redirect('/upload');
38+
return;
39+
}
40+
}
41+
res.sendFile('login.html', {root: '.'});
42+
});
43+
44+
app.get('/register', (req, res) => {
45+
res.sendFile('register.html', {root: '.'});
46+
});
47+
48+
app.post('/register', (req, res) => {
49+
allUsers.push({email: req.body.email, password: req.body.password, type: req.body.type});
50+
res.redirect('/login');
51+
});
52+
53+
app.get('/upload', (req, res) => {
54+
if (req.session.user) {
55+
res.render('upload.ejs', { user: req.session.user, twins });
56+
} else {
57+
res.redirect('/login');
58+
}
59+
60+
});
61+
62+
app.get('/twins', (req, res) => {
63+
res.send(twins);
64+
});
65+
66+
app.post('/upload/file', (req, res) => {
67+
if (!req.body.twinName || !req.body.fileName){
68+
res.status(400);
69+
return
70+
}
71+
72+
if (req.session.user.type){
73+
if (req.body.twinName in twins) {
74+
if (twins[req.body.twinName].length > 4) {
75+
twins[req.body.twinName].splice(Math.floor(Math.random() * 4), 1);
76+
}
77+
twins[req.body.twinName].push(req.body.fileName);
78+
} else {
79+
twins[req.body.twinName] = [req.body.fileName];
80+
}
81+
82+
return res.send(twins);
83+
}
84+
res.send(twins);
85+
});
86+
87+
app.listen(8080);
88+
console.log('Server started on port 8080');

src/upload.ejs

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<link href='https://fonts.googleapis.com/css?family=Lato:100,200,300,400,500,600,700' rel='stylesheet' type='text/css'>
5+
<style>
6+
body {
7+
font-family: lato;
8+
margin: 0;
9+
}
10+
header {
11+
display: flex;
12+
justify-content: space-between;
13+
padding: 8px;
14+
background-color: rgb(230, 230, 230);
15+
box-shadow: 0px 3px 10px -6px rgba(0,0,0,0.77);
16+
margin-bottom: 24px;
17+
}
18+
table, th, td {
19+
border: 1px solid;
20+
}
21+
</style>
22+
</head>
23+
<body>
24+
<header>
25+
<span> Upload </span>
26+
<span> Logged in as <%= user.email %> </span>
27+
</header>
28+
29+
<div id="warnings"></div>
30+
31+
<input id="twin-name" placeholder="Twin name" name="name">
32+
33+
<br>
34+
<input type="file" id="file-upload">
35+
36+
<div id="tables"></div>
37+
38+
<script>
39+
function showWarning(file) {
40+
document.querySelector('#warnings').innerText += `File ${file.name} is of type ${file.type}, but we want jpegs!\n`;
41+
}
42+
43+
document.querySelector('#file-upload').addEventListener('input', async (ev) => {
44+
45+
<% if(user.type !== 'enterprise') { %>
46+
for(const file of ev.target.files) {
47+
if(file.type !== 'image/jpeg') {
48+
showWarning(file);
49+
return;
50+
}
51+
}
52+
<% } %>
53+
const twinName = document.getElementById('twin-name').value;
54+
55+
if (twinName === '') {
56+
alert("Twin name can not be empty.");
57+
return;
58+
}
59+
60+
for(const file of ev.target.files) {
61+
const res = await fetch('/upload/file', {
62+
method: 'POST',
63+
headers: {'Content-Type': 'application/json'},
64+
body: JSON.stringify({twinName, fileName: file.name})
65+
});
66+
}
67+
68+
window.location.reload();
69+
});
70+
71+
function createTableWithTitle (tableName, files) {
72+
const tableDiv = document.createElement('div');
73+
const title = document.createElement('h3');
74+
title.innerText = tableName;
75+
const table = document.createElement('table');
76+
77+
files.forEach((item) => {
78+
const row = document.createElement('tr');
79+
const cell = document.createElement('td');
80+
cell.textContent = item;
81+
row.appendChild(cell);
82+
table.appendChild(row);
83+
});
84+
85+
tableDiv.appendChild(title);
86+
tableDiv.appendChild(table);
87+
88+
return tableDiv;
89+
}
90+
91+
async function loadTwins(){
92+
const tablesContainer = document.getElementById('tables');
93+
94+
const twins = await (await fetch('/twins')).json();
95+
96+
for (const [twinName, files] of Object.entries(twins)){
97+
if (files.length) {
98+
tablesContainer.appendChild(createTableWithTitle(twinName, files));
99+
}
100+
}
101+
}
102+
103+
loadTwins();
104+
105+
</script>
106+
</body>
107+
</html>

0 commit comments

Comments
 (0)