# PrideForge framework reference

Technical reference for PrideForge Academy lessons. Describes how the reference solution is structured so you can **use** and **extend** it without reading every source file first.

This document is the shareable lookup companion to the course — catalogs, filters, and file paths without long guided lessons.

**Related shareable docs:** [Step phrases](step-phrases.md) · [Tags & filters cheatsheet](tags-and-filters-cheatsheet.md)

---

## Resolution pipeline

Every interaction step follows the same path:

1. **Gherkin** — `When I set "WidgetsPage.DisplayName" to "Widget User"`
2. **Step definition** — `InteractionSteps.cs` calls `TestServices` / `FieldHandlerRegistry`
3. **LocatorResolver** — parses `WidgetsPage.DisplayName` → loads `WidgetsPage` from `PageRegistry` → reads the `DisplayName` property → returns `ILocator`
4. **FieldHandlerRegistry** — walks handlers by **priority band**; first handler whose `CanHandleAsync` returns true wins
5. **IFieldHandler** — `SetValueAsync` / `GetValueAsync` performs the Playwright interaction

Validation steps (`Then The "…" field should …`) use the same resolver and registry, then compare values.

**Tables vs fields:** Grids use `TableHelper` and table step definitions — not `FieldHandlerRegistry`. If the UI is rows, columns, criteria, or pagination, use table steps (see TableHelper below).

---

## PageRegistry

**File:** `Pages/General/PageRegistry.cs`

- Called once in `[BeforeTestRun]` via `PageRegistry.RegisterPages(typeof(BasePage).Assembly)`
- Scans the assembly for concrete classes inheriting `BasePage`
- Registers by class name (`WidgetsPage`) and short alias (`Widgets` → `WidgetsPage`)

**Feature syntax:** `"PageName.Member"` where `PageName` matches the registered page class (with or without the `Page` suffix).

---

## Page objects

**Pattern:** `Pages/{Area}/{Name}Page.cs` inheriting `BasePage`

```csharp
public sealed class WidgetsPage : BasePage
{
    public WidgetsPage(Hooks hooks) : base(hooks) { }

    public ILocator DisplayName => Page.Locator("#display-name");
}
```

- Properties return Playwright `ILocator` — selectors live here, not in Gherkin
- For compound controls, the property often points at a **wrapper** element; the handler finds the inner button/input
- No manual registration beyond existing in the assembly scanned by `PageRegistry`

---

## LocatorResolver

**File:** `Pages/General/Locators/LocatorResolver.cs`

- Parses `PageName.Member` and reflects on the page property (cached per type)
- Optional **fallback strategies** when the name is not `Page.Member` — controlled by `LocatorFallback.AllowedFallbacks` in `appsettings.json` (Role, Label, Placeholder, PlaywrightSelector)
- Caches resolved locators per scenario for performance

**When UI changes:** update the page object property once; scenarios keep the same `PageName.Member` string.

---

## FieldHandlerRegistry

**File:** `Pages/General/Locators/Fields/FieldHandlerRegistry.cs`

### Priority bands (lower number = tried first)

| Band | Purpose | Examples |
|------|---------|----------|
| 10–19 | HTML primitives (type-driven) | Checkbox, radio, native select, file, range |
| 20–29 | Library-specific widgets | Mud date/select, MUI chip, Quill |
| 30–39 | Library-agnostic compounds | Combobox, menu button, toggle switch |
| 40–49 | Read-only message containers | Alert, dialog, toast |
| 50–59 | Read-only static text | Label, span |
| 100 | Catch-all | Generic input/textarea/select |

Within a band, handlers are tried in registration order. The first handler where `CanHandleAsync(locator)` is true is used.

### Built-in handler catalog

Stock handlers ship in `Pages/General/Locators/Fields/` and register in `RegisterDefaultHandlers`. Gherkin never names a handler.

| Priority | Handler | Typical controls | Writable |
|----------|---------|------------------|----------|
| 10 | `CheckboxFieldHandler` | Checkbox inputs | Yes |
| 11 | `RadioFieldHandler` | Radio groups | Yes |
| 12 | `NativeSelectFieldHandler` | Native `<select>` | Yes |
| 13 | `FileFieldHandler` | File inputs | Yes |
| 14 | `RangeFieldHandler` | Range sliders | Yes |
| 20 | `MudSelectFieldHandler` | MudBlazor selects | Yes |
| 21 | `MudDatePickerFieldHandler` | Mud date pickers | Yes |
| 22 | `MuiChipFieldHandler` | MUI chips | Yes |
| 23 | `QuillFieldHandler` | Quill rich text | Yes |
| 30 | `ComboboxFieldHandler` | ARIA combobox patterns | Yes |
| 31 | `MenuButtonFieldHandler` | Menu buttons | Yes |
| 32 | `ToggleSwitchFieldHandler` | Toggle switches | Yes |
| 40 | `MessageContainerFieldHandler` | Alerts, dialogs, toasts | Read-only |
| 50 | `LabelFieldHandler` | Label text | Read-only |
| 51 | `SpanFieldHandler` | Static span text | Read-only |
| 100 | `InputFieldHandler` | Generic input/textarea/select fallback | Yes |

If `When I set` finds the element but behaves wrong, the catch-all at **100** may have won — tighten `CanHandleAsync` on a custom handler or fix the DOM match. Set `DebugMode: true` in `appsettings.json` to log which handler runs.

### Register at runtime

```csharp
registry.RegisterHandler(25, new MyCustomWidgetHandler());
```

Choose a priority that places your handler **before** the catch-all (100) and **before or after** related handlers depending on how specific `CanHandleAsync` is.

Project handlers should register from `Customization/Registrations/` — not by editing `RegisterDefaultHandlers` in a customer fork.

---

## IFieldHandler contract

**File:** `Pages/General/Locators/Fields/IFieldHandler.cs`

| Method | Purpose |
|--------|---------|
| `CanHandleAsync` | Return true when this handler owns the DOM shape |
| `GetValueAsync` | Read current value as a string for assertions |
| `SetValueAsync` | Apply a value from `When I set … to …` |
| `IsEditableAsync` | Support read-only / editable validation steps |
| `MatchesAsync` | Optional typed comparison helper |

Handlers should be **narrow** in `CanHandleAsync` — overly broad handlers steal controls from more specific ones.

---

## TestServices

**File:** `Pages/General/TestServices.cs`

Constructed per scenario in `[BeforeScenario]`. Step definitions receive it via `Hooks.Services` and should not new-up Playwright helpers directly.

| Member | Role |
|--------|------|
| `LocatorResolver` | `PageName.Member` → `ILocator` |
| `FieldHandlerRegistry` | Set, get, match field values |
| `WaitActions` | Stable waits before interactions |
| `InteractionActions` | Clicks, hovers, field set/get orchestration |
| `ValidationActions` | Field and page assertions |
| `NavigationActions` | Sitemap and menu navigation |
| `TableHelper` | Grid row/column/criteria operations |
| `AccessibilityActions` | axe scans and severity gates |
| `DynamicValueResolver` | Token resolution in step arguments |
| `ServerResolver` | Active environment / BaseUrl |
| `FileService` | JSON/CSV load paths for test data |

**Extension rule:** New UI control → new `IFieldHandler` + registration. New step phrase → thin method calling existing `TestServices` methods. New assertion type → extend `ValidationActions` or a focused helper.

---

## TableHelper vs field handlers

**File:** `Pages/General/TableHelper.cs` (or equivalent in your solution)

| Use field steps | Use table steps |
|-----------------|-----------------|
| Single input, button, label, widget | Grid with rows and columns |
| `When I set "Page.Field" to "x"` | `When I select the row where "Name" is "Acme"` |
| `Then The "Page.Field" field should be "x"` | `Then the table should have N rows` |

Local teaching fixture: `Samples/Data/local-site/tables.html` + `Tables.feature`.

Table IDs matter when a page has more than one grid — pass the table locator name from the page object.

---

## ServerResolver & environments

**Files:** `Utilities/ServerResolver.cs`, `Customization/Data/servers/*.json`

- `@env:local` tag → loads matching server JSON (`BaseUrl`, `Application`, `UsersFile`, `SitemapFile`)
- Override precedence: CLI `env` param → env var → scenario tag → feature tag → `appsettings.json` `Server`

**Application** field in server JSON selects the login page via `LoginPageFactory`.

Example server JSON:

```json
{
  "Name": "staging",
  "Application": "Orders",
  "BaseUrl": "https://staging.example.com",
  "UsersFile": "users/orders_users.json",
  "SitemapFile": "sitemaps/orders_sitemap.json",
  "RequiresAuth": true
}
```

---

## LoginPageFactory

**File:** `Utilities/LoginPageFactory.cs`

Maps `application` string (from server config) to a `LoginPageBase` implementation. Extend the switch expression when onboarding a new product login flow.

```
Given I login as user "admin"
  → Resolve active server JSON
  → Read Application (e.g. "Orders", "local")
  → LoginPageFactory.Create(application, hooks)
  → LoginPageBase.Navigate / Login / IsLoggedInAs
```

---

## Sitemaps

**Generation:** `tools/SitemapGenerator` CLI crawls the app and writes JSON under `Customization/Data/sitemaps/`.

**Consumption:** Navigation steps read server `SitemapFile` at runtime.

### SitemapGenerator CLI (common args)

| Argument | Purpose |
|----------|---------|
| `--startUri` | Crawl start URL (often server BaseUrl) |
| `--output` | Output path, e.g. `Customization/Data/sitemaps/orders_sitemap.json` |
| `--env` | Server key for auth before crawl |
| `--user` | User key from server users file for authenticated crawl |

Examples:

```bash
# Public site
dotnet run --project tools/SitemapGenerator -- \
  --startUri https://staging.example.com \
  --output Customization/Data/sitemaps/orders_sitemap.json

# Protected app
dotnet run --project tools/SitemapGenerator -- \
  --env staging --user admin \
  --startUri https://staging.example.com \
  --output Customization/Data/sitemaps/orders_sitemap.json
```

Review and hand-edit JSON after generation — deep links and non-standard nav often need manual entries.

Runtime steps:

- `When I open page "Orders" from the sitemap`
- `When I navigate to page "Settings" through the menu`

---

## Dynamic values

**File:** `Utilities/DynamicValueResolver.cs`

Resolves `{token}` expressions in step arguments before handlers run — dates, variables, JSON paths, captured UI values, file data tokens.

| Family | Examples |
|--------|----------|
| Dates | `{today}`, `{tomorrow}`, `{today + 7 days:yyyy-MM-dd}` |
| Variables | `{email}` after `Given I set variable "email" to "…"` |
| JSON | `{medium.color}` after `Given I load JSON data from "testdata/…"` |
| CSV rows | `{rows.2.color}` or `{rows.medium.color}` with key column |
| Captured UI | `{capturedOrderId}` after store-from-field steps |
| File data | `{data:primitives_demo.txt}` for file upload values |

---

## Local teaching fixtures

Offline `@env:local` scenarios use static HTML under `Samples/Data/local-site/` and Gherkin under `Samples/Features/Local/`.

| HTML fixture | Feature | Teaches |
|--------------|---------|---------|
| `primitives.html` | `Primitives.feature` | Native HTML handlers (radio, file, range, alerts) |
| `widgets.html` | `Widgets.feature` | Vendor and compound widgets |
| `tables.html` | `Tables.feature` | `TableHelper` grids |
| `validation.html` | `Validation.feature` | Editable vs read-only assertions |
| `accessibility_pass.html` / `accessibility_fail.html` | Accessibility features | axe scans |

Quick filter:

```bash
dotnet test --filter "TestCategory=env:local&TestCategory!=intentional-failure"
```

Fixture open steps (examples): `Given I open the primitives fixture`, `Given I open the widgets fixture`, `Given I open the tables fixture`.

---

## Tags, filters, and CI

Gherkin tags become NUnit `TestCategory` values. Filter **without** the `@` symbol.

### Filter operators

| Operator | Meaning | Example |
|----------|---------|---------|
| `=` | Equals | `TestCategory=env:local` |
| `!=` | Not equal | `TestCategory!=intentional-failure` |
| `~` | Contains | `FullyQualifiedName~Primitives` |
| `!~` | Does not contain | `FullyQualifiedName!~Widgets` |
| `&` | AND | `TestCategory=smoke&TestCategory=env:local` |
| `|` | OR | `Name~Login|Name~Logout` |

### Recommended tag families

| Tag | Purpose |
|-----|---------|
| `@env:{name}` | Select server JSON |
| `@smoke` | Fast PR / deploy gate |
| `@regression` | Nightly or release suite |
| `@accessibility` | Scenarios with axe scans |
| `@intentional-failure` | Teaching demos — exclude from release CI |

### CI gate filters

| Gate | Filter |
|------|--------|
| PR offline | `TestCategory=env:local&TestCategory!=intentional-failure` |
| PR smoke | `TestCategory=env:local&TestCategory=smoke&TestCategory!=intentional-failure` |
| Staging regression | `TestCategory=env:staging&TestCategory=regression&TestCategory!=intentional-failure` |
| Accessibility slice | `TestCategory=accessibility&TestCategory!=intentional-failure` |

### CI script environment variables

| Variable | Example | Purpose |
|----------|---------|---------|
| `PRIDEFORGE_FILTER` | `TestCategory!=intentional-failure` | NUnit filter |
| `PRIDEFORGE_ENV` | `staging` | Server override |
| `PRIDEFORGE_HEADLESS` | `true` | Headless browser |
| `PRIDEFORGE_WORKERS` | `4` | `NUnit.NumberOfTestWorkers` |

Shared script: `ci/run-prideforge-tests.sh` — restore, build, Playwright install, `dotnet test`, output under `Reports/`.

### Runtime overrides (local or CI)

```bash
dotnet test -- NUnit.TestParameters="env=staging"
Headless=true dotnet test
dotnet test -- NUnit.NumberOfTestWorkers=4
```

---

## Reports

- **Extent HTML** — `Reports/` after each run; step timeline and failure screenshots
- **Screenshots** — attached on scenario failure
- **Playwright trace** — optional zip when enabled in `appsettings.json`
- **Accessibility** — `{label}_axe.json`, `{label}_axe.png`, `{label}_page.html` when axe steps run

`dotnet test` exits `0` on pass, `1` on failure — pipelines should fail the job on non-zero exit.

---

## Extension checklist

| Task | Touch these |
|------|-------------|
| New screen / field | Page class + property; reuse existing steps |
| New control type | New `IFieldHandler` + `RegisterHandler(priority, …)` |
| New grid behavior | `TableHelper` or table step binding — not a field handler |
| New environment | `Customization/Data/servers/{name}.json`, `@env:{name}` tag |
| New login UI | `LoginPageBase` subclass + `LoginPageFactory` case |
| New sitemap | Run SitemapGenerator, edit JSON, point `SitemapFile` on server |
| New step phrase | New method in `StepDefinitions/` with `[Given|When|Then]` — last resort |

---

## Customization folder layout

```
Customization/
├── Data/
│   ├── servers/          Environment JSON
│   ├── sitemaps/         Navigation JSON
│   ├── testdata/         JSON/CSV for scenarios
│   └── auth/             Saved session files (per server/user)
├── Handlers/             IFieldHandler implementations
└── Registrations/        RegisterHandler at startup
```
