Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 29 additions & 25 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,12 @@ sharethrift/
│ │ ├── rest/ # REST API adapters
│ │ ├── persistence/ # MongoDB persistence layer
│ │ ├── application-services/ # Orchestration services
│ │ ├── ui-components/ # Shared React components
│ │ └── acceptance-tests/ # BDD acceptance tests (Serenity.js)
│ │ └── ui-components/ # Shared React components
│ └── sthrift-verification/ # Verification packages and shared test support
│ ├── acceptance-api/ # API-path acceptance tests (Serenity.js)
│ ├── acceptance-ui/ # UI-path acceptance tests (Serenity.js + jsdom)
│ ├── e2e-tests/ # Browser confidence tests (Playwright)
│ └── test-support/ # Shared features, pages, test data
│ └── cellix/ # Seedwork abstractions (shared across projects)
│ ├── domain-seedwork/ # DDD base classes
│ ├── event-bus-seedwork-node/ # Event bus abstractions
Expand Down Expand Up @@ -327,28 +331,26 @@ All packages use **strict TypeScript configuration**:

3. **Acceptance Tests / BDD** (Top)
- Framework: Serenity.js + Cucumber (Gherkin)
- Location: `packages/sthrift/acceptance-tests/`
- Test levels:
- **domain**: Direct domain layer testing
- **session**: GraphQL/MongoDB session management
- **e2e**: Full end-to-end with Playwright browser
- Backends:
- **GraphQL**: GraphQL resolver layer (mock app services)
- **MongoDB**: Full persistence stack (MongoMemoryServer)
- Locations:
- `packages/sthrift-verification/acceptance-api/`
- `packages/sthrift-verification/acceptance-ui/`
- Shared features/pages/test data:
- `packages/sthrift-verification/test-support/`
- Browser confidence tests:
- `packages/sthrift-verification/e2e-tests/`

Example test matrix:
```
Domain (domain) → In-memory aggregates
Session + GraphQL (session:graphql) → GraphQL backend
Session + MongoDB (session:mongodb) → MongoDB backend
E2E (e2e) → Playwright browser → Vite UI → GraphQL → MongoDB
Acceptance API → GraphQL backend → application services → domain → MongoDB
Acceptance UI → jsdom-rendered UI components + shared page objects
E2E → Playwright browser → Vite UI → GraphQL → MongoDB
```

### Acceptance Test Pattern (Serenity.js)

**File Structure**:
```
acceptance-tests/
acceptance-api/ or acceptance-ui/
├── src/
│ ├── step-definitions/ # Gherkin step implementations
│ ├── support/ # Hooks, configuration, world setup
Expand All @@ -358,10 +360,12 @@ acceptance-tests/
│ ├── contexts/ # Test context setup
│ │ ├── listing/ # Listing context (abilities, tasks, sessions)
│ │ └── reservation-request/
│ └── features/ # Gherkin feature files (.feature)
│ └── features/ # Optional local feature files (.feature)
└── test:* scripts run scenarios
```

Shared feature files live in `packages/sthrift-verification/test-support/src/scenarios/feature-files/`.

**Key Patterns**:
- **Abilities**: Actor capabilities registered in Cast
- **Tasks**: Actions the actor takes (calls domain/API)
Expand Down Expand Up @@ -426,8 +430,8 @@ Located in: `apps/docs/docs/decisions/`
4. **Create repository interface** (abstraction)
5. **Implement UnitOfWork** for consistency coordination
6. **Define permissions** (`{entity}.{role}.passport.ts`)
7. **Create acceptance tests** in `acceptance-tests/src/contexts/{context-name}/`
8. **Add to Cast** in acceptance test world setup
7. **Create acceptance tests** in `acceptance-api/src/contexts/{context-name}/` and/or `acceptance-ui/src/contexts/{context-name}/`
8. **Add to Cast** in the relevant acceptance package world setup
9. **Implement GraphQL resolver** in `packages/sthrift/graphql/`
10. **Implement MongoDB adapter** in `packages/sthrift/persistence/`

Expand All @@ -442,22 +446,22 @@ Located in: `apps/docs/docs/decisions/`

### Adding a New Acceptance Test

1. **Write feature file** (Gherkin) in `acceptance-tests/src/features/`
2. **Implement step definitions** in `acceptance-tests/src/step-definitions/`
1. **Write feature file** (Gherkin) in `test-support/src/scenarios/feature-files/`
2. **Implement step definitions** in `acceptance-api/src/step-definitions/` and/or `acceptance-ui/src/step-definitions/`
3. **Create abilities/tasks** if needed
4. **Set up world context** in hooks
5. **Run tests**: `pnpm run test:acceptance:domain` (or appropriate level)
5. **Run tests**: `pnpm run test:acceptance:api` and/or `pnpm run test:acceptance:ui`

### Running Acceptance Tests

```bash
# All acceptance tests
pnpm run test:acceptance:all

# Specific level
pnpm run test:acceptance:domain # Pure domain
pnpm run test:acceptance:session:graphql # Session + GraphQL
pnpm run test:acceptance:e2e # Full E2E with Playwright
# Specific package
pnpm run test:acceptance:api # API-path acceptance tests
pnpm run test:acceptance:ui # UI-path acceptance tests
pnpm run test:e2e # Full E2E with Playwright
```

## 🎯 Key Principles
Expand Down
9 changes: 7 additions & 2 deletions knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@
"entry": ["src/index.ts"],
"project": ["src/**/*.{ts,tsx}"]
},
"packages/sthrift-verification/acceptance-tests": {
"packages/sthrift-verification/acceptance-api": {
"entry": ["src/world.ts", "src/step-definitions/**/*.ts"],
"project": ["src/**/*.{ts,mjs,js}"]
},
"packages/sthrift-verification/acceptance-ui": {
"entry": ["src/world.ts", "src/step-definitions/**/*.ts"],
"project": ["src/**/*.{ts,mjs,js}"]
},
Expand All @@ -70,7 +74,8 @@
"packages/cellix/server-payment-seedwork",
"packages/cellix/arch-unit-tests",
"packages/sthrift-verification/arch-unit-tests",
"packages/sthrift-verification/acceptance-tests",
"packages/sthrift-verification/acceptance-api",
"packages/sthrift-verification/acceptance-ui",
"packages/sthrift-verification/e2e-tests",
"packages/sthrift/ui-components",
"apps/server-messaging-mock",
Expand Down
16 changes: 9 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"dev": "pnpm run build && pnpm run proxy:stop && pnpm run proxy:start && turbo run azurite gen:watch dev --parallel",
"start": "turbo run build && concurrently pnpm:start:* --kill-others-on-fail --workspace=@app/api",
"format": "turbo run format",
"proxy:stop": "portless proxy stop || true",
"proxy:start": "portless proxy start",
"proxy:stop": "PORTLESS_STATE_DIR=${HOME}/.portless portless proxy stop || true",
"proxy:start": "PORTLESS_STATE_DIR=${HOME}/.portless portless proxy start",
"gen": "graphql-codegen --config codegen.yml",
"gen:watch": "graphql-codegen --config codegen.yml --watch",
"tsbuild": "tsc --build",
Expand All @@ -32,11 +32,13 @@
"start-emulator:payment-server": "pnpm --filter=@cellix/mock-payment-server run start",
"start-emulator:messaging-server": "pnpm --filter=@app/mock-messaging-server run start",
"test:all": "turbo run test:all",
"test:acceptance:domain": "turbo run test:domain --filter=@sthrift-verification/acceptance-tests",
"test:acceptance:session": "turbo run test:session:graphql --filter=@sthrift-verification/acceptance-tests",
"test:acceptance:session:graphql": "turbo run test:session:graphql --filter=@sthrift-verification/acceptance-tests",
"test:acceptance:session:mongodb": "turbo run test:session:mongodb --filter=@sthrift-verification/acceptance-tests",
"test:acceptance:e2e": "turbo run test:e2e --filter=@sthrift-verification/e2e-tests",
"test:acceptance:api": "pnpm --filter=@sthrift-verification/acceptance-api run test",
"test:acceptance:ui": "pnpm --filter=@sthrift-verification/acceptance-ui run test",
"test:acceptance:all": "pnpm run test:acceptance:api && pnpm run test:acceptance:ui",
"test:coverage:acceptance:api": "pnpm --filter=@sthrift-verification/acceptance-api run test:coverage",
"test:coverage:acceptance:ui": "pnpm --filter=@sthrift-verification/acceptance-ui run test:coverage",
"test:coverage:acceptance": "pnpm run test:coverage:acceptance:api && pnpm run test:coverage:acceptance:ui",
"test:e2e": "turbo run test:e2e --filter=@sthrift-verification/e2e-tests",
"test:coverage": "turbo run test:coverage:ui && turbo run test:coverage:node && turbo run test:arch",
"test:coverage:node": "turbo run test:coverage:node",
"test:coverage:ui": "turbo run test:coverage:ui",
Expand Down
33 changes: 33 additions & 0 deletions packages/sthrift-verification/acceptance-api/.c8rc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"all": true,
"reporter": ["lcov"],
"reportsDirectory": "coverage",
"tempDirectory": ".c8-output",
"src": [
"../../../packages/sthrift/application-services/src",
"../../../packages/sthrift/data-sources-mongoose-models/src",
"../../../packages/sthrift/domain/src",
"../../../packages/sthrift/graphql/src",
"../../../packages/sthrift/persistence/src"
],
"exclude": [
"cucumber.js",
"src/**",
"**/node_modules/**",
"**/arch-unit-tests/**",
"**/*.test.*",
"**/*.spec.*",
"**/*.stories.*",
"**/*.d.ts",
"**/dist/**/*.map",
"**/packages/sthrift-verification/**",
"**/packages/cellix/test-utils/**",
"**/packages/cellix/vitest-config/**",
"coverage/**",
"coverage-c8/**",
"coverage-vitest/**",
".c8-output/**",
"**/generated.tsx"
],
"excludeNodeModules": false
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ reports/
target/
*.log
.portless/
.c8-output/
coverage/
coverage-c8
24 changes: 24 additions & 0 deletions packages/sthrift-verification/acceptance-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# ShareThrift Acceptance API Tests

Cucumber Screenplay acceptance tests for the ShareThrift API path.

## Scope

- GraphQL request path
- Application services
- Domain logic
- Persistence with the test MongoDB server

## Running Tests

```bash
pnpm run test
pnpm run test:coverage
```

## From monorepo root

```bash
pnpm run test:acceptance:api
pnpm run test:coverage:acceptance:api
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,15 @@ const terminalFormat = isAgent
: 'progress-bar';

export default {
paths: ['src/contexts/**/features/**/*.feature'],
paths: ['../test-support/src/scenarios/feature-files/**/*.feature'],
import: [
'src/world.ts',
'src/step-definitions/**/*.ts',
'src/shared/support/**/*.ts',
'src/step-definitions/index.ts',
],
format: [
terminalFormat,
'json:./reports/cucumber-report.json',
'html:./reports/cucumber-report.html',
'json:./reports/cucumber-report-api.json',
'html:./reports/cucumber-report-api.html',
],
formatOptions: {
snippetInterface: 'async-await',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
{
"name": "@sthrift-verification/acceptance-tests",
"name": "@sthrift-verification/acceptance-api",
"version": "1.0.0",
"description": "Cucumber Screenplay acceptance tests for ShareThrift domain (domain + session levels)",
"description": "Cucumber Screenplay acceptance tests for the ShareThrift API path",
"private": true,
"type": "module",
"scripts": {
"test:domain": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --world-parameters '{\"tasks\":\"domain\"}' --format json:./reports/cucumber-report-domain.json",
"test:session:graphql": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --world-parameters '{\"tasks\":\"session\",\"session\":\"graphql\"}' --format json:./reports/cucumber-report-session-graphql.json",
"test:session:mongodb": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --world-parameters '{\"tasks\":\"session\",\"session\":\"mongodb\"}' --format json:./reports/cucumber-report-session-mongodb.json",
"test:fast": "pnpm run test:domain && pnpm run test:session:graphql",
"test:all": "pnpm run test:domain && pnpm run test:session:graphql && pnpm run test:session:mongodb",
"clean": "rimraf dist reports target"
"test": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' cucumber-js --format json:./reports/cucumber-report-api.json",
"test:coverage": "LOG_LEVEL=warn NODE_OPTIONS='--import tsx/esm' c8 --clean --allowExternal --reports-dir=coverage --reporter=lcov --reporter=html -- cucumber-js --format json:./reports/cucumber-report-api.json",
"test:coverage:report": "c8 report --allowExternal --temp-directory=.c8-output --reports-dir=coverage --reporter=lcov --reporter=html --exclude='**/node_modules/**' --exclude='**/*.d.ts' --exclude='**/*.stories.*' --exclude='**/generated.*'",
"clean": "rimraf dist reports target coverage coverage-c8 coverage-vitest .c8-output"
},
"dependencies": {
"@cucumber/cucumber": "^12.7.0",
Expand All @@ -35,8 +33,10 @@
"@sthrift/domain": "workspace:*",
"@sthrift/graphql": "workspace:*",
"@sthrift/persistence": "workspace:*",
"@sthrift-verification/test-support": "workspace:*",
"@types/graphql-depth-limit": "^1.1.6",
"@types/node": "^24.6.1",
"c8": "^11.0.0",
"graphql-depth-limit": "^1.1.0",
"graphql-middleware": "^6.1.35",
"mongodb": "^6.15.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {
type Actor,
type AnswersQuestions,
notes,
Question,
type UsesAbilities,
} from '@serenity-js/core';
import { GraphQLClient } from '../../../shared/abilities/graphql-client.ts';

const GET_LISTING_QUERY = `
query GetListing($id: ObjectID!) {
itemListing(id: $id) { id state }
}
`;

export class ListingStatus extends Question<Promise<string>> {
constructor() {
super('listing status');
}

override answeredBy(
actor: AnswersQuestions & UsesAbilities,
): Promise<string> {
return this.resolveStatus(actor);
}

static of(): ListingStatus {
return new ListingStatus();
}

override toString(): string {
return 'the listing status';
}

private async resolveStatus(
actor: AnswersQuestions & UsesAbilities,
): Promise<string> {
const listingId = await this.readNote(actor, 'lastListingId');

const apiStatus = await this.readStatusFromApi(actor, listingId);
if (apiStatus) {
return this.normalizeStatus(apiStatus);
}

const notedStatus = await this.readNote(actor, 'lastListingStatus');
if (!notedStatus) {
throw new Error(
'No listing status found in the system or actor notes. Did the actor create a listing first?',
);
}

return this.normalizeStatus(notedStatus);
}

private async readStatusFromApi(
actor: AnswersQuestions & UsesAbilities,
listingId?: string,
): Promise<string | undefined> {
if (!listingId) {
return undefined;
}

try {
const graphql = GraphQLClient.as(actor as unknown as Actor);
const response = await graphql.execute(GET_LISTING_QUERY, {
id: listingId,
});
const listing = response.data.itemListing as
| Record<string, unknown>
| undefined;
return listing?.state ? String(listing.state) : undefined;
} catch {
return undefined;
}
}

private async readNote(
actor: AnswersQuestions & UsesAbilities,
key: 'lastListingId' | 'lastListingTitle' | 'lastListingStatus',
): Promise<string | undefined> {
try {
return await actor.answer(notes<Record<typeof key, string>>().get(key));
} catch {
return undefined;
}
}

private normalizeStatus(status: string): string {
const normalized = status.trim().toLowerCase();
if (normalized === 'published') {
return 'active';
}
return normalized;
}
}
Loading
Loading