Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ REACT_APP_ENABLE_MESSAGING=true
REACT_APP_ENABLE_REGIONS=true
REACT_APP_ENABLE_TOOLTIPS=true
REACT_APP_ENABLE_STAKEHOLDERS=true
REACT_APP_KEYCLOAK_URL=https://dev-k8s.treetracker.org/keycloak
REACT_APP_KEYCLOAK_REALM=treetracker
REACT_APP_KEYCLOAK_CLIENT_ID=treetracker-admin-client
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,8 @@ yarn-debug.log*
yarn-error.log*
.nyc_output
*.orig

.claude
.drivers
CLAUDE.md

Empty file added .nojekyll
Empty file.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.eslintrc.js
reports/
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,50 @@ To run tests:

Make your own `/cypress/fixtures/login.json` file containing the actual credentials in order to run the cypress tests.

### BDD Tests (WebdriverIO + Cucumber)

Feature specs live in `features/` and are written in Gherkin. They run against the live dev server, so start it first:

```bash
npm start
```

#### Run BDD tests

In a separate terminal:

```bash
npm run wdio
```

Chrome will open visibly and execute each scenario. Results are printed to the terminal. A video of the run is saved to `reports/video/test-run.mp4`.

#### Generate the HTML report

After the test run, convert the raw Allure results into a browsable HTML report:

```bash
npx allure generate ./reports/allure-results --clean -o ./reports/allure-html
```

#### Open the report server

The report uses XHR requests to load data, so it must be served over HTTP — opening `index.html` directly in a browser will not work.

Run the built-in Allure server:

```bash
npx allure open ./reports/allure-html
```

This starts a local HTTP server and opens the report in your browser automatically (default port 4321).

Alternatively, run both steps in one command:

```bash
npm run wdio:report
```

## How to log

We use [loglevel](<(https://github.com/pimterry/loglevel)>) for logging, with some conventions. Using loglevel, we will be able to open/close a single file's log by chaining the level of log on the fly, even in production env.
Expand Down
14 changes: 14 additions & 0 deletions features/login.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
Feature: Login

Scenario: Login with wrong credentials shows an error message
Given I am on the login page
When I enter username "admin" and password "wrongpwd"
And I click the login button
Then I should see an error message


Scenario: Login with valid credentials succeeds
Given I am on the login page
When I enter username "user-test-treetracker-admin-client" and password "LjyxVk4t5^yx&!Gl"
And I click the login button
Then I should be redirected away from the login page
89 changes: 89 additions & 0 deletions features/page-objects/LoginPage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
class LoginPage {
get pageTitle() {
return $('#kc-page-title');
}

get loginForm() {
return $('#kc-form-login');
}

get usernameInput() {
return $('#username');
}

get passwordInput() {
return $('#password');
}

get submitButton() {
return $('#kc-login');
}

get invalidCredentialsError() {
return $('#input-error');
}

async isOpen() {
return this.loginForm.isExisting();
}

async waitForPage() {
await this.loginForm.waitForExist({
timeout: 60000,
timeoutMsg: 'Expected to be redirected to the Keycloak sign-in page',
});

await this.pageTitle.waitForDisplayed({ timeout: 10000 });
await this.usernameInput.waitForDisplayed({ timeout: 10000 });
await this.passwordInput.waitForDisplayed({ timeout: 10000 });
}

async enterCredentials(username, password) {
await this.waitForPage();
await this.usernameInput.setValue(username);
await this.passwordInput.setValue(password);
}

async submit() {
await this.submitButton.waitForDisplayed({ timeout: 10000 });
await this.submitButton.click();
}

async login(username, password) {
await this.enterCredentials(username, password);
await this.submit();
}

async waitForInvalidCredentials() {
await this.waitForPage();

await this.invalidCredentialsError.waitForDisplayed({
timeout: 10000,
timeoutMsg: 'Expected Keycloak to show invalid credential feedback',
});
}

async waitForSuccessfulRedirect() {
const baseUrl = browser.options.baseUrl;

await browser.waitUntil(
async () => {
const currentUrl = await browser.getUrl();

if (!baseUrl || !currentUrl.startsWith(baseUrl)) {
return false;
}

const currentPath = new URL(currentUrl).pathname;
return currentPath !== '/login' && currentPath !== '/auth/callback';
},
{
timeout: 60000,
interval: 500,
timeoutMsg: 'Expected successful login to redirect back to the app',
}
);
}
}

module.exports = new LoginPage();
97 changes: 97 additions & 0 deletions features/step-definitions/login.steps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
const { Before, Given, When, Then } = require('@cucumber/cucumber');
const LoginPage = require('../page-objects/LoginPage');

const LOG_OUT_BUTTON = '//button[normalize-space(.)="LOG OUT"]';

async function navigate(path) {
await browser.url(path, { wait: 'none' });
}

async function clearAppStorage() {
await navigate('/');

await browser.execute(() => {
window.localStorage.clear();
window.sessionStorage.clear();
});

await browser.deleteCookies();
}

async function isExisting(selector) {
return $(selector)
.isExisting()
.catch(() => false);
}

async function openKeycloakLoginPage() {
await navigate('/login');

if (await LoginPage.isOpen()) {
await LoginPage.waitForPage();
return;
}

await navigate('/account');

try {
await browser.waitUntil(
async () =>
(await LoginPage.isOpen()) || (await isExisting(LOG_OUT_BUTTON)),
{ timeout: 15000, interval: 250 }
);
} catch {
await navigate('/login');
await LoginPage.waitForPage();
return;
}

if (await LoginPage.isOpen()) {
await LoginPage.waitForPage();
return;
}

const logoutButton = $(LOG_OUT_BUTTON);
await logoutButton.waitForDisplayed({ timeout: 10000 });
await logoutButton.scrollIntoView();
await logoutButton.click();
await LoginPage.waitForPage();
}

async function ensureLoggedOut() {
await clearAppStorage();

await openKeycloakLoginPage();
}

async function assertInvalidCredentialFeedback() {
await LoginPage.waitForInvalidCredentials();
}

Before(async () => {
await ensureLoggedOut();
});

Given('I am on the login page', async () => {
await openKeycloakLoginPage();
await LoginPage.waitForPage();
});

When(
'I enter username {string} and password {string}',
async (username, password) => {
await LoginPage.enterCredentials(username, password);
}
);

When('I click the login button', async () => {
await LoginPage.submit();
});

Then('I should see an error message', async () => {
await assertInvalidCredentialFeedback();
});

Then('I should be redirected away from the login page', async () => {
await LoginPage.waitForSuccessfulRedirect();
});
Loading
Loading