chore: perauset customer project

This commit is contained in:
e2e
2026-07-11 08:34:30 +02:00
commit 0639909ec6
293 changed files with 28412 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
import { Then } from '@cucumber/cucumber'
import { strict as assert } from 'assert'
import type { PerausetWorld } from '../support/world.js'
Then('the audit log should contain an {string} entry for table {string}', function (this: PerausetWorld, action: string, tableName: string) {
assert.ok(this.lastBody, 'No response body')
assert.ok(Array.isArray(this.lastBody.items), `Expected body.items to be an array, got: ${JSON.stringify(this.lastBody)}`)
const match = this.lastBody.items.find(
(entry: any) => entry.action === action && entry.tableName === tableName
)
assert.ok(match, `No audit log entry with action="${action}" for table="${tableName}" found in: ${JSON.stringify(this.lastBody.items.slice(0, 5))}`)
})

View File

@@ -0,0 +1,6 @@
import { Given } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
Given('I login as {string}', async function (this: PerausetWorld, email: string) {
await this.login(email)
})

View File

@@ -0,0 +1,64 @@
import { When } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
When('I create a boat route with name {string} origin {string} destination {string}', async function (this: PerausetWorld, name: string, origin: string, destination: string) {
await this.post('/boats/routes', {
name,
origin,
destination,
})
})
When('I create a boat with name {string} capacity {int}', async function (this: PerausetWorld, name: string, capacity: number) {
await this.post('/boats', {
name,
passengerCapacity: capacity,
})
})
When('I create a boat trip for the route and boat departing {string}', async function (this: PerausetWorld, departureAt: string) {
const routeId = (this as any).routeId
const boatId = (this as any).boatId
await this.post('/boats/trips', {
routeId,
boatId,
scheduledDepartureAt: departureAt,
})
})
When('I open the boat trip', async function (this: PerausetWorld) {
const tripId = (this as any).tripId
await this.post(`/boats/trips/${tripId}/open`, {
tripId,
})
})
When('I request a seat on the boat trip', async function (this: PerausetWorld) {
const tripId = (this as any).tripId
await this.post(`/boats/trips/${tripId}/request-seat`, {
tripId,
requestedSeats: 1,
})
})
When('I confirm the boat request', async function (this: PerausetWorld) {
const boatRequestId = (this as any).boatRequestId
await this.post(`/boats/requests/${boatRequestId}/confirm`, {
requestId: boatRequestId,
})
})
When('I create a boat trip for the route without boat departing {string}', async function (this: PerausetWorld, departureAt: string) {
const routeId = (this as any).routeId
await this.post('/boats/trips', {
routeId,
scheduledDepartureAt: departureAt,
})
})
When('I cancel the boat trip', async function (this: PerausetWorld) {
const tripId = (this as any).tripId
await this.post(`/boats/trips/${tripId}/cancel`, {
tripId,
})
})

View File

@@ -0,0 +1,141 @@
import { Given, When, Then } from '@cucumber/cucumber'
import { expect } from '@playwright/test'
import { BrowserWorld } from '../../support/browser-world.js'
import { browserConfig } from '../../support/browser-config.js'
// ---------------------------------------------------------------------------
// Given
// ---------------------------------------------------------------------------
Given('I am on the login page', async function (this: BrowserWorld) {
await this.navigateTo('/login')
})
Given('I am logged in as {string}', async function (this: BrowserWorld, email: string) {
await this.login(email)
})
// ---------------------------------------------------------------------------
// When — navigation
// ---------------------------------------------------------------------------
When('I navigate to {string}', async function (this: BrowserWorld, path: string) {
await this.navigateTo(path)
})
When('I click {string}', async function (this: BrowserWorld, name: string) {
const page = this.getPage()
// Try button first, then link
const button = page.getByRole('button', { name })
if (await button.isVisible().catch(() => false)) {
await button.click()
} else {
await page.getByRole('link', { name }).click()
}
await page.waitForLoadState('networkidle').catch(() => {})
})
When('I click {string} in the sidebar', async function (this: BrowserWorld, name: string) {
const page = this.getPage()
// Mantine NavLink renders as an <a> WITHOUT href (navigation is via onClick), so
// it has no implicit "link" role — target the label text inside the navbar.
// Some labels are duplicated (e.g. "My Stays" maps to both /my/stays and /stays
// due to the nav.stays == nav.my_stays i18n collision); first() picks the
// personal-section entry, which appears first in DOM order.
const navItem = page
.locator('.mantine-AppShell-navbar')
.getByText(name, { exact: true })
.first()
await navItem.scrollIntoViewIfNeeded()
await navItem.click()
await page.waitForLoadState('networkidle').catch(() => {})
})
When('I click tab {string}', async function (this: BrowserWorld, name: string) {
const page = this.getPage()
await page.getByRole('tab', { name }).click()
await page.waitForLoadState('networkidle').catch(() => {})
})
// ---------------------------------------------------------------------------
// When — form interactions
// ---------------------------------------------------------------------------
When(
'I fill in {string} with {string}',
async function (this: BrowserWorld, label: string, value: string) {
await this.fillField(label, value)
}
)
When(
'I select {string} from {string}',
async function (this: BrowserWorld, value: string, label: string) {
await this.selectOption(label, value)
}
)
When(
'I pick date {string} for {string}',
async function (this: BrowserWorld, day: string, label: string) {
const page = this.getPage()
// Close any open calendar/dropdown first
await page.keyboard.press('Escape')
await page.waitForTimeout(300)
// Mantine DatePickerInput: the label text and button are wrapped in the same container
// Use a CSS selector to find the right DatePickerInput by its label
const wrapper = page.locator(`text="${label}" >> xpath=ancestor::div[contains(@class, "mantine-DatePickerInput-root") or contains(@class, "mantine-InputWrapper-root")][1]`)
const btn = wrapper.locator('button').first()
if (await btn.count() > 0) {
await btn.click({ timeout: 10_000 })
} else {
// Fallback: just try getByLabel
await page.getByLabel(label).click({ timeout: 10_000 })
}
// Wait for calendar popover to appear
await page.waitForTimeout(500)
// Click the day number — try table buttons with matching text
await page.locator('table button').filter({ hasText: new RegExp(`^${day}$`) }).first().click()
// Close the calendar popover
await page.waitForTimeout(300)
}
)
// ---------------------------------------------------------------------------
// Then — assertions
// ---------------------------------------------------------------------------
Then('I should see {string}', async function (this: BrowserWorld, text: string) {
await this.expectText(text)
})
Then('I should not see {string}', async function (this: BrowserWorld, text: string) {
await this.expectNoText(text)
})
Then('I should see heading {string}', async function (this: BrowserWorld, text: string) {
await this.expectHeading(text)
})
Then('I should be on {string}', async function (this: BrowserWorld, path: string) {
const page = this.getPage()
await page.waitForURL((url) => url.pathname.includes(path), { timeout: 10_000 })
expect(page.url()).toContain(path)
})
Then(
'the sidebar should show {string}',
async function (this: BrowserWorld, text: string) {
const page = this.getPage()
const sidebar = page.locator('.mantine-AppShell-navbar')
await expect(sidebar.getByText(text)).toBeVisible({ timeout: 10_000 })
}
)
Then(
'the {string} field should contain {string}',
async function (this: BrowserWorld, label: string, value: string) {
const page = this.getPage()
await expect(page.getByLabel(label)).toHaveValue(value, { timeout: 5_000 })
}
)

View File

@@ -0,0 +1,38 @@
import { When } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
When('I create a finance record {string} with amount {int} and type {string}', async function (this: PerausetWorld, name: string, amount: number, recordType: string) {
await this.post('/finance', {
name,
amount,
recordType,
category: 'operations',
})
})
When('I list finance records', async function (this: PerausetWorld) {
await this.get('/finance')
})
When('I update the finance record with name {string}', async function (this: PerausetWorld, name: string) {
const financeId = (this as any).financeId
await this.put(`/finance/${financeId}`, {
financeId,
name,
})
})
When('I link the finance record to entity type {string}', async function (this: PerausetWorld, entityType: string) {
const financeId = (this as any).financeId
const entityId = (this as any).itemId
await this.post('/finance/links', {
financeId,
entityType,
entityId,
})
})
When('I list finance links for the current finance record', async function (this: PerausetWorld) {
const financeId = (this as any).financeId
await this.get(`/finance/links?financeId=${financeId}`)
})

View File

@@ -0,0 +1,10 @@
import { When, Then } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
When('I send a GET request to {string}', async function (this: PerausetWorld, path: string) {
await this.get(path)
})
Then('the response status should be {int}', function (this: PerausetWorld, status: number) {
this.expectStatus(status)
})

View File

@@ -0,0 +1,49 @@
import { When } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
When('I create an inventory item {string} with unit {string} quantity {int} and minimum {int}', async function (this: PerausetWorld, name: string, unit: string, currentQuantity: number, minimumQuantity: number) {
await this.post('/inventory', {
name,
unit,
currentQuantity,
minimumQuantity,
category: 'kitchen',
})
})
When('I update the inventory item with name {string}', async function (this: PerausetWorld, name: string) {
const itemId = (this as any).itemId
await this.put(`/inventory/${itemId}`, {
itemId,
name,
})
})
When('I list inventory items', async function (this: PerausetWorld) {
await this.get('/inventory')
})
When('I request inventory for item with quantity {int} unit {string} and purpose {string}', async function (this: PerausetWorld, quantity: number, unit: string, purpose: string) {
const itemId = (this as any).itemId
await this.post('/inventory/requests', {
itemId,
quantity,
unit,
purpose,
})
})
When('I review the inventory request with status {string}', async function (this: PerausetWorld, status: string) {
const requestId = (this as any).requestId
await this.post(`/inventory/requests/${requestId}/review`, {
requestId,
status,
})
})
When('I fulfill the inventory request', async function (this: PerausetWorld) {
const requestId = (this as any).requestId
await this.post(`/inventory/requests/${requestId}/fulfill`, {
requestId,
})
})

View File

@@ -0,0 +1,22 @@
import { When } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
When('I create a meal service of type {string} on {string} with headcount {int}', async function (this: PerausetWorld, serviceType: string, serviceDate: string, plannedHeadcount: number) {
await this.post('/kitchen/meal-services', {
serviceType,
serviceDate,
plannedHeadcount,
})
})
When('I list meal services', async function (this: PerausetWorld) {
await this.get('/kitchen/meal-services')
})
When('I upsert my dietary profile as vegan with nut allergy', async function (this: PerausetWorld) {
await this.put('/kitchen/dietary-profile', {
isVegan: true,
hasNutAllergy: true,
allergyNotes: 'Severe nut allergy',
})
})

View File

@@ -0,0 +1,29 @@
import { Given, When } from '@cucumber/cucumber'
import { strict as assert } from 'assert'
import type { PerausetWorld } from '../support/world.js'
import { openDb } from '../support/db.js'
Given('a test notification exists for the current user', async function (this: PerausetWorld) {
const userId = await this.getSessionUserId()
const db = openDb()
try {
const row = db
.prepare(
`INSERT INTO notification (user_id, type, title, body)
VALUES (?, 'test', 'Test Notification', 'This is a test notification')
RETURNING notification_id`
)
.get(userId) as { notification_id: string }
;(this as any).notificationId = row.notification_id
} finally {
db.close()
}
})
When('I mark the notification as read', async function (this: PerausetWorld) {
const notificationId = (this as any).notificationId
assert.ok(notificationId, 'No notification ID stored')
await this.post(`/notifications/${notificationId}/read`, {
notificationId,
})
})

View File

@@ -0,0 +1,59 @@
import { When } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
When('I create a retreat {string} with slug {string} from {string} to {string}', async function (this: PerausetWorld, name: string, slug: string, startAt: string, endAt: string) {
await this.post('/retreats', {
slug,
name,
startAt,
endAt,
})
})
When('I list retreats', async function (this: PerausetWorld) {
await this.get('/retreats')
})
When('I update the retreat with name {string} and capacity {int}', async function (this: PerausetWorld, name: string, capacity: number) {
const retreatId = (this as any).retreatId
await this.put(`/retreats/${retreatId}`, {
retreatId,
name,
capacity,
})
})
When('I publish the retreat', async function (this: PerausetWorld) {
const retreatId = (this as any).retreatId
await this.post(`/retreats/${retreatId}/publish`, {
retreatId,
})
})
When('I add a person {string} with role {string} to the retreat', async function (this: PerausetWorld, fullName: string, roleType: string) {
const retreatId = (this as any).retreatId
await this.post(`/retreats/${retreatId}/persons`, {
retreatId,
fullName,
roleType,
email: `${fullName.toLowerCase().replace(/\s+/g, '.')}@test.org`,
})
})
When('I add a schedule item {string} from {string} to {string} at {string}', async function (this: PerausetWorld, title: string, startsAt: string, endsAt: string, location: string) {
const retreatId = (this as any).retreatId
await this.post(`/retreats/${retreatId}/schedule`, {
retreatId,
title,
startsAt,
endsAt,
location,
})
})
When('I cancel the retreat', async function (this: PerausetWorld) {
const retreatId = (this as any).retreatId
await this.post(`/retreats/${retreatId}/cancel`, {
retreatId,
})
})

View File

@@ -0,0 +1,22 @@
import { When, Then } from '@cucumber/cucumber'
import { strict as assert } from 'assert'
import type { PerausetWorld } from '../support/world.js'
import { getUserId } from '../support/db.js'
When('I set roles {string} for user {string}', async function (this: PerausetWorld, roles: string, email: string) {
// Look up userId from DB if not already cached
let userId = this.createdUsers[email]
if (!userId) {
userId = getUserId(email)
this.createdUsers[email] = userId
}
const rolesArray = roles.split(',').map((r) => r.trim())
await this.put(`/users/${userId}/roles`, { memberRoles: rolesArray })
})
Then('the response body field {string} should contain {string}', function (this: PerausetWorld, field: string, value: string) {
assert.ok(this.lastBody, 'No response body')
const arr = this.lastBody[field]
assert.ok(Array.isArray(arr), `Expected body.${field} to be an array, got: ${JSON.stringify(arr)}`)
assert.ok(arr.includes(value), `Expected body.${field} to contain "${value}", got: ${JSON.stringify(arr)}`)
})

View File

@@ -0,0 +1,51 @@
import { When, Given } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
import { openDb } from '../support/db.js'
When('I create a room with code {string} name {string} type {string} capacity {int}', async function (this: PerausetWorld, code: string, name: string, roomType: string, capacity: number) {
await this.post('/rooms', {
code,
name,
roomType,
capacity,
})
})
When('I allocate the room to the stay from {string} to {string}', async function (this: PerausetWorld, startsAt: string, endsAt: string) {
const roomId = (this as any).roomId
const stayId = (this as any).stayId
await this.post('/rooms/allocations', {
roomId,
stayId,
startsAt,
endsAt,
})
})
When('I allocate the room to stored stay {string} from {string} to {string}', async function (this: PerausetWorld, stayKey: string, startsAt: string, endsAt: string) {
const roomId = (this as any).roomId
const stayId = (this as any)[stayKey]
await this.post('/rooms/allocations', {
roomId,
stayId,
startsAt,
endsAt,
})
})
Given('the room allocation is active in the database', async function (this: PerausetWorld) {
const allocationId = (this as any).allocationId
const db = openDb()
try {
db.prepare(`UPDATE room_allocation SET status = 'active' WHERE allocation_id = ?`).run(allocationId)
} finally {
db.close()
}
})
When('I release the room allocation', async function (this: PerausetWorld) {
const allocationId = (this as any).allocationId
await this.post(`/rooms/allocations/${allocationId}/release`, {
allocationId,
})
})

View File

@@ -0,0 +1,19 @@
import { When, Then } from '@cucumber/cucumber'
import { strict as assert } from 'assert'
import type { PerausetWorld } from '../support/world.js'
When('I send a POST request to {string} with body:', async function (this: PerausetWorld, path: string, body: string) {
await this.post(path, JSON.parse(body))
})
Then('I store the response field {string} as {string}', function (this: PerausetWorld, field: string, key: string) {
assert.ok(this.lastBody, 'No response body')
const value = this.lastBody[field]
assert.ok(value !== undefined && value !== null, `Response body field "${field}" is missing. Body: ${JSON.stringify(this.lastBody)}`)
;(this as any)[key] = value
})
Then('the response body field {string} should be truthy', function (this: PerausetWorld, field: string) {
assert.ok(this.lastBody, 'No response body')
assert.ok(this.lastBody[field], `Expected body.${field} to be truthy, got: ${JSON.stringify(this.lastBody[field])}`)
})

View File

@@ -0,0 +1,57 @@
import { When, Given } from '@cucumber/cucumber'
import type { PerausetWorld } from '../support/world.js'
import { openDb } from '../support/db.js'
When('I submit a stay request with type {string} from {string} to {string}', async function (this: PerausetWorld, requestType: string, startAt: string, endAt: string) {
await this.post('/stays/requests', {
requestType,
requestedStartAt: startAt,
requestedEndAt: endAt,
})
})
When('I approve the stay request', async function (this: PerausetWorld) {
const requestId = (this as any).requestId
await this.post(`/stays/requests/${requestId}/approve`, {
requestId,
})
})
When('I reject the stay request with reason {string}', async function (this: PerausetWorld, reason: string) {
const requestId = (this as any).requestId
await this.post(`/stays/requests/${requestId}/reject`, {
requestId,
reviewNotes: reason,
})
})
When('I create a stay from the approved request', async function (this: PerausetWorld) {
const requestId = (this as any).requestId
await this.post('/stays', {
requestId,
})
})
Given('the stay is confirmed in the database', async function (this: PerausetWorld) {
const stayId = (this as any).stayId
const db = openDb()
try {
db.prepare(`UPDATE stay SET status = 'confirmed' WHERE stay_id = ?`).run(stayId)
} finally {
db.close()
}
})
When('I check in the stay', async function (this: PerausetWorld) {
const stayId = (this as any).stayId
await this.post(`/stays/${stayId}/check-in`, {
stayId,
})
})
When('I check out the stay', async function (this: PerausetWorld) {
const stayId = (this as any).stayId
await this.post(`/stays/${stayId}/check-out`, {
stayId,
})
})

View File

@@ -0,0 +1,63 @@
import { When } from '@cucumber/cucumber'
import { strict as assert } from 'assert'
import type { PerausetWorld } from '../support/world.js'
import { getUserId } from '../support/db.js'
When('I create a task template with title {string} category {string}', async function (this: PerausetWorld, title: string, category: string) {
await this.post('/tasks/templates', {
title,
category,
})
})
When('I create a task instance with title {string} category {string}', async function (this: PerausetWorld, title: string, category: string) {
await this.post('/tasks', {
title,
category,
})
})
When('I assign the task to user {string}', async function (this: PerausetWorld, email: string) {
const taskId = (this as any).taskId
// Look up userId from DB
let userId = this.createdUsers[email]
if (!userId) {
userId = getUserId(email)
this.createdUsers[email] = userId
}
await this.post(`/tasks/${taskId}/assign`, {
taskId,
userId,
})
})
When('I start the task', async function (this: PerausetWorld) {
const taskId = (this as any).taskId
await this.post(`/tasks/${taskId}/start`, {
taskId,
})
})
When('I complete the task', async function (this: PerausetWorld) {
const taskId = (this as any).taskId
await this.post(`/tasks/${taskId}/complete`, {
taskId,
})
})
When('I claim the task', async function (this: PerausetWorld) {
const taskId = (this as any).taskId
await this.post(`/tasks/${taskId}/claim`, {
taskId,
})
})
When('I mark the task as blocked with reason {string}', async function (this: PerausetWorld, reason: string) {
const taskId = (this as any).taskId
await this.post(`/tasks/${taskId}/block`, {
taskId,
reason,
})
})

View File

@@ -0,0 +1,37 @@
import { Given, When, Then } from '@cucumber/cucumber'
import { strict as assert } from 'assert'
import type { PerausetWorld } from '../support/world.js'
import { ensureUser } from '../support/db.js'
Given('a user {string} exists', function (this: PerausetWorld, email: string) {
this.createdUsers[email] = ensureUser(email, [])
})
Given('a user {string} exists with roles {string}', function (this: PerausetWorld, email: string, roles: string) {
const rolesArray = roles.split(',').map((r) => r.trim())
this.createdUsers[email] = ensureUser(email, rolesArray)
})
When('I get my own user profile', async function (this: PerausetWorld) {
const userId = await this.getSessionUserId()
await this.get(`/users/${userId}`)
})
When('I update my profile with displayName {string}', async function (this: PerausetWorld, displayName: string) {
const userId = await this.getSessionUserId()
await this.put(`/users/${userId}/profile`, { displayName })
})
Then('the response body should be a list with field {string}', function (this: PerausetWorld, field: string) {
assert.ok(this.lastBody, 'No response body')
assert.ok(Array.isArray(this.lastBody[field]), `Expected body.${field} to be an array, got: ${JSON.stringify(this.lastBody[field])}`)
})
Then('the response body field {string} should be {string}', function (this: PerausetWorld, field: string, value: string) {
assert.ok(this.lastBody, 'No response body')
assert.equal(
this.lastBody[field],
value,
`Expected body.${field} to be "${value}", got: "${this.lastBody[field]}"`
)
})