test: add full application BDD suite

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Daniel Noulet 2026-09-20 09:13:48 +02:00
parent bed3f9a019
commit 79e09f049f
14 changed files with 1408 additions and 4 deletions

2
bdd/.dockerignore Normal file
View file

@ -0,0 +1,2 @@
node_modules
reports

8
bdd/Dockerfile Normal file
View file

@ -0,0 +1,8 @@
FROM mcr.microsoft.com/playwright:v1.63.0-noble
WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["npm", "test"]

9
bdd/cucumber.mjs Normal file
View file

@ -0,0 +1,9 @@
export default {
paths: ["features/**/*.feature"],
import: ["support/**/*.js", "steps/**/*.js"],
format: ["progress", "html:reports/cucumber.html"],
formatOptions: {
snippetInterface: "async-await",
},
parallel: 1,
};

View file

@ -0,0 +1,86 @@
Feature: Recruitment workspace
As an interview team
We want one connected workspace for jobs, candidates, assessments, and interviews
So that we can move qualified people through a consistent hiring process
Scenario: User story - a valid interviewer signs in
Given the Go Guess application is open
When I sign in with email "integration@example.com" and password "integration-password"
Then I should see the heading "Your hiring pipeline, at a glance."
And I should be on the "/" page
Scenario: User story - invalid credentials are rejected
Given the Go Guess application is open
When I sign in with email "integration@example.com" and password "incorrect-password"
Then I should see the error "invalid email or password"
And I should see the heading "Sign in to your workspace"
Scenario: User story - an interviewer gets a quick pipeline overview
Given I am signed in as a valid interviewer
Then I should see the heading "Your hiring pipeline, at a glance."
And the dashboard should link to jobs, participants, and questions
And I should see "Integration Backend Engineer"
Scenario: User story - an interviewer creates a job posting
Given I am signed in as a valid interviewer
When I follow the "Jobs" link
And I follow the "Create job" link
And I fill in the form:
| Job title | BDD Platform Engineer |
| Position | Platform Engineer |
| Seniority | Senior |
| Labels | Backend |
| Description | Build reliable production infrastructure. |
| Required skills | Go, PostgreSQL |
| Additional skills | Docker |
And I click the "Create job" button
Then I should see the heading "BDD Platform Engineer"
And I should see "Build reliable production infrastructure."
And I should see "draft"
Scenario: User story - an interviewer adds a participant
Given I am signed in as a valid interviewer
When I follow the "Participants" link
And I follow the "Add participant" link
And I fill in the form:
| First name | Beatrice |
| Last name | Driven |
| Birthday | 1993-06-14 |
| Email address | beatrice.bdd@example.com |
| Contact information | Brussels, +32 470 11 22 33 |
And I click the "Create participant" button
Then I should see the heading "Beatrice Driven"
And I should see "beatrice.bdd@example.com"
Scenario: User story - an interviewer creates a reusable assessment question
Given I am signed in as a valid interviewer
When I follow the "Question library" link
And I fill in the form:
| Question prompt | How do you keep a deployment reversible? |
| Reference answer | Use versioned artifacts and tested rollback procedures. |
And I click the "Create question" button
Then I should see "How do you keep a deployment reversible?"
And I should see "Use versioned artifacts and tested rollback procedures."
Scenario: User story - an interviewer invites a matching candidate
Given I am signed in as a valid interviewer
When I follow the "Jobs" link
And I follow the "Integration Backend Engineer" link
And I select the "Candidate match" tab
Then I should see "Integration Candidate"
And I should see "100%"
When I select the "Invitations" tab
And I click the "Generate invitation" button
Then an invitation should be shown for "Integration Candidate"
Scenario: User story - an interviewer can reach collaboration areas
Given I am signed in as a valid interviewer
When I follow the "Inbox" link
Then I should see the heading "Inbox"
And I should see "Inbox zero"
When I follow the "Calendar" link
Then I should be on the "/calendar" page
And I should see "Schedule"
When I follow the "Users" link
Then I should see the heading "Interview team"
And I should see "Integration Interviewer"

1082
bdd/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

13
bdd/package.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "go-guess-bdd",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"test": "cucumber-js --config cucumber.mjs"
},
"devDependencies": {
"@cucumber/cucumber": "13.2.1",
"playwright": "1.63.0"
}
}

View file

@ -0,0 +1,93 @@
import assert from "node:assert/strict";
import { Given, Then, When } from "@cucumber/cucumber";
async function openLoginPage(world) {
await world.page.goto(world.baseURL);
await world.page
.getByRole("heading", { name: "Sign in to your workspace", exact: true })
.waitFor();
}
async function signIn(world, email, password) {
await world.page.getByLabel("Email address", { exact: true }).fill(email);
await world.page.getByLabel("Password", { exact: true }).fill(password);
await world.page
.getByRole("button", { name: "Sign in", exact: true })
.click();
}
Given("the Go Guess application is open", async function () {
await openLoginPage(this);
});
Given("I am signed in as a valid interviewer", async function () {
await openLoginPage(this);
await signIn(this, "integration@example.com", "integration-password");
await this.page
.getByRole("heading", {
name: "Your hiring pipeline, at a glance.",
exact: true,
})
.waitFor();
});
When(
"I sign in with email {string} and password {string}",
async function (email, password) {
await signIn(this, email, password);
},
);
When("I follow the {string} link", async function (name) {
await this.page.getByRole("link", { name, exact: true }).first().click();
});
When("I click the {string} button", async function (name) {
await this.page.getByRole("button", { name, exact: true }).click();
});
When("I select the {string} tab", async function (name) {
await this.page.getByRole("tab", { name: new RegExp(`^${name}`) }).click();
});
When("I fill in the form:", async function (table) {
for (const [label, value] of table.raw()) {
await this.page.getByLabel(label, { exact: true }).fill(value);
}
});
Then("I should see the heading {string}", async function (heading) {
await this.page
.getByRole("heading", { name: heading, exact: true })
.waitFor();
});
Then("I should see {string}", async function (text) {
await this.page.getByText(text, { exact: true }).first().waitFor();
});
Then("I should see the error {string}", async function (message) {
await this.page.getByRole("alert").filter({ hasText: message }).waitFor();
});
Then("I should be on the {string} page", async function (path) {
await this.page.waitForURL((url) => url.pathname === path);
assert.equal(new URL(this.page.url()).pathname, path);
});
Then(
"the dashboard should link to jobs, participants, and questions",
async function () {
for (const path of ["/jobs", "/participants", "/questions"]) {
await this.page.locator(`a[href="${path}"]`).first().waitFor();
}
},
);
Then("an invitation should be shown for {string}", async function (candidate) {
await this.page.getByText(candidate, { exact: true }).last().waitFor();
const invitationURL = await this.page
.locator(".invitation-row code")
.textContent();
assert.match(invitationURL ?? "", /\/participant\/[a-zA-Z0-9_-]+/);
});

24
bdd/support/hooks.js Normal file
View file

@ -0,0 +1,24 @@
import { After, Before, Status, setDefaultTimeout } from "@cucumber/cucumber";
import { chromium } from "playwright";
setDefaultTimeout(Number(process.env.BDD_STEP_TIMEOUT_MS ?? 15000));
Before(async function () {
this.browser = await chromium.launch({
headless: process.env.BDD_HEADLESS !== "false",
});
this.context = await this.browser.newContext();
this.page = await this.context.newPage();
this.baseURL = process.env.BDD_BASE_URL ?? "http://127.0.0.1:8080";
});
After(async function ({ result }) {
if (result?.status === Status.FAILED && this.page) {
await this.attach(
await this.page.screenshot({ fullPage: true }),
"image/png",
);
}
await this.context?.close();
await this.browser?.close();
});