---
name: ao-gherkin-step-generator
description: "Generate SpecFlow step definition classes from Gherkin feature files with real API integration test implementations. Creates WebApplicationFactory infrastructure, typed API clients, and state management."
category: utility
invokes: []
invoked_by: []
state_files:
  read: []
  write: []
---

# Gherkin Step Generator Workflow

## Purpose

Transform Gherkin feature files into **executable SpecFlow integration tests** with real API call implementations. Unlike simple scaffolding, this skill generates:
- Complete test infrastructure (WebApplicationFactory, authentication bypass, API clients)
- **Real step implementations** that make HTTP calls and verify responses
- Proper state management across steps

## Applicability

- **Framework**: SpecFlow (TechTalk.SpecFlow)
- **Test Runner**: NUnit 4.x
- **Gherkin Languages**: Norwegian (`Gitt`/`Når`/`Så`), English (`Given`/`When`/`Then`)
- **Test Type**: API Integration Tests (not UI tests)
- **Generated Files**: `.feature.cs` files are auto-generated by SpecFlow — do NOT edit them manually

## Required NuGet Packages

```xml
<PackageReference Include="SpecFlow" Version="3.9.74" />
<PackageReference Include="SpecFlow.NUnit" Version="3.9.74" />
<PackageReference Include="NUnit" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
<PackageReference Include="FluentAssertions" Version="8.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
```

## Prerequisites

Before using this skill, verify:

1. SpecFlow is installed in the project (check for `SpecFlow` package reference in .csproj)
2. A `specflow.json` configuration exists (language, binding culture settings)
3. The test project builds successfully
4. **For WebApplicationFactory**: Main project's `Program.cs` must have `public partial class Program { }` at the end

## Procedure

### Phase 1: Discovery

1. **User provides feature file(s)**:
   - Single file path: `IntegrationTests/Features/MyFeature.feature`
   - Directory path: `IntegrationTests/Features/Kunde/` (processes all .feature files)
   - Attached file content directly

2. **Detect Gherkin language**:
   - Check first line for `#language: no` (Norwegian) or `#language: en` (English)
   - Default to English if not specified

3. **Parse specflow.json** (if exists):
   ```json
   {
     "language": { "feature": "no" },
     "bindingCulture": { "name": "no" }
   }
   ```

### Phase 2: Step Extraction

1. **Extract all steps** from feature file(s):
   - `Gitt`/`Given` steps → `[Given(...)]` attributes
   - `Når`/`When` steps → `[When(...)]` attributes
   - `Så`/`Then` steps → `[Then(...)]` attributes
   - `Og`/`And` steps → Inherit type from previous step

2. **Normalize step text**:
   - Remove leading keywords (`Gitt at`, `Når`, `Så`, `Og`)
   - Preserve quoted strings as capture groups
   - Preserve numbers as capture groups
   - Preserve table placeholders `{...}` as parameters

3. **Deduplicate steps**:
   - Identify unique step patterns across all scenarios
   - Group similar steps with parameterized variations

### Phase 3: Pattern Generation

Convert natural language steps to regex patterns:

| Step Text | Regex Pattern | Parameters |
|-----------|---------------|------------|
| `jeg trykker på knappen "LAGRE"` | `@"jeg trykker på knappen ""(.*)"""` | `string buttonName` |
| `feltet "Navn" har verdien "Helse Bergen"` | `@"feltet ""(.*)"" har verdien ""(.*)"""` | `string fieldName, string value` |
| `kunde med organisasjonsnummer 123456785 eksisterer` | `@"kunde med organisasjonsnummer (\d+) eksisterer"` | `int orgNumber` |
| `jeg er innlogget som {role}` | `@"jeg er innlogget som (.*)"` | `string role` |

**Regex Pattern Rules**:

1. **Quoted strings** → `""(.*)""` with `string` parameter
2. **Numbers** → `(\d+)` with `int` parameter (or `decimal` if contains `.`)
3. **Placeholders `{name}`** → `(.*)` with `string` parameter named after placeholder
4. **Literal text** → Escaped for regex special characters
5. **Norwegian characters** → Preserve (å, æ, ø) — they work in regex patterns

### Phase 4: Binding Class Generation

Generate step definition class with **REAL implementations** (not pending exceptions):

```csharp
using FluentAssertions;
using TechTalk.SpecFlow;
using {Namespace}.Infrastructure;

namespace {Namespace}.Features.{FeatureArea};

[Binding]
public class {FeatureName}Steps
{
    private readonly ScenarioContext _scenarioContext;
    private TestContext Context => _scenarioContext.GetTestContext();

    public {FeatureName}Steps(ScenarioContext scenarioContext)
    {
        _scenarioContext = scenarioContext;
    }

    // --- Given Steps (setup preconditions + state) ---

    [Given(@"jeg er innlogget som (.*)")]
    public void GivenJegErInnloggetSom(string role)
    {
        // Set permissions based on role
        if (role.Contains("admin"))
        {
            Context.UserPermissions.Add("CreateCustomer");
            Context.UserPermissions.Add("EditCustomer");
            Context.UserPermissions.Add("DeleteCustomer");
        }
        Context.CurrentUserId = $"test-{role}-user";
    }

    [Given(@"jeg er på siden ""(.*)""")]
    public void GivenJegErPaSiden(string pageName)
    {
        Context.CurrentPage = pageName;  // Track navigation state
    }

    // --- When Steps (actions that call APIs) ---

    [When(@"jeg oppretter en kunde med navn ""(.*)""")]
    public async Task WhenJegOppretterKundeMedNavn(string name)
    {
        var request = new CreateCustomerRequest { Name = name };
        var response = await Context.ApiClient.CreateAsync(request);

        Context.LastStatusCode = (int)response.StatusCode;

        if (response.IsSuccessStatusCode)
        {
            var created = await response.Content.ReadFromJsonAsync<CustomerDto>();
            Context.CreatedEntityIds.Add(created!.Id);  // Track for cleanup
        }
    }

    // --- Then Steps (assertions using FluentAssertions) ---

    [Then(@"kunden skal være opprettet")]
    public void ThenKundenSkalVaereOpprettet()
    {
        Context.LastStatusCode.Should().Be(201, "customer should be created with 201 Created");
        Context.CreatedEntityIds.Should().NotBeEmpty("created customer ID should be tracked");
    }

    [Then(@"jeg skal fortsatt være på siden ""(.*)""")]
    public void ThenJegSkalFortsattVaerePaSiden(string pageName)
    {
        Context.CurrentPage.Should().Be(pageName);  // Verify navigation state
    }
}
```

**CRITICAL**: Never use `throw new PendingStepException()` - always implement real logic!

**Class Naming Convention**:
- Feature: `OpprettKunde` → Class: `OpprettKundeSteps`
- Feature: `HentKunde` → Class: `HentKundeSteps`

**Method Naming Convention**:
- `Given at jeg...` → `GivenAtJeg...`
- `When jeg trykker...` → `WhenJegTrykker...`
- `Then skal...` → `ThenSkal...`

### Phase 5: Output Options

Present options to user:

| Option | Description |
|--------|-------------|
| **A) Generate single file** | One binding class for all steps |
| **B) Generate per-feature** | One binding class per .feature file |
| **C) Generate shared + feature-specific** | Common steps in shared class, feature-specific in separate classes |

### Phase 6: File Placement

1. **Determine output directory**:
   - Same directory as .feature files (default)
   - Or user-specified location

2. **Create file(s)**:
   - `{FeatureName}Steps.cs` for feature-specific steps
   - `CommonSteps.cs` for shared steps (if Option C)
   - `Hooks.cs` for setup/teardown hooks (if requested)

3. **Update project** (if needed):
   - Files are auto-included in modern SDK-style projects

## Table Parameter Handling

For steps with tables:

**Feature file**:
```gherkin
Gitt at følgende valideringsregler gjelder
    | Felt | Obligatorisk | Maks lengde |
    | Navn | Ja           | 50          |
```

**Generated step**:
```csharp
[Given(@"følgende valideringsregler gjelder")]
public void GivenFolgendeValideringsreglerGjelder(Table table)
{
    // Table has: table.Rows[0]["Felt"], table.Rows[0]["Obligatorisk"], etc.
    foreach (var row in table.Rows)
    {
        var felt = row["Felt"];
        var obligatorisk = row["Obligatorisk"];
        var maksLengde = row["Maks lengde"];
        // TODO: Process validation rules
    }
    throw new PendingStepException();
}
```

## Context Sharing

For steps that need to share state:

```csharp
// Store data in Given step
[Given(@"kunden {(.*)} eksisterer")]
public void GivenKundenEksisterer(string customerId)
{
    var customer = CreateTestCustomer(customerId);
    _scenarioContext["Customer"] = customer;
}

// Retrieve in Then step
[Then(@"skal kunden bli opprettet")]
public void ThenSkalKundenBliOpprettet()
{
    var customer = _scenarioContext.Get<Customer>("Customer");
    customer.Should().NotBeNull();
}
```

## API Integration Test Infrastructure (REQUIRED)

When generating real integration tests, the following infrastructure is **mandatory**:

### 1. WebApplicationFactory (Infrastructure/BackofficeWebApplicationFactory.cs)

```csharp
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace {Namespace}.Infrastructure;

public class {ServiceName}WebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");

        builder.ConfigureAppConfiguration((context, config) =>
        {
            config.AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["ConnectionStrings:Database"] = GetTestConnectionString(),
                // Disable external services
                ["Redis:Enabled"] = "false",
                ["ExternalAuth:Enabled"] = "false",
            });
        });

        builder.ConfigureServices(services =>
        {
            // Add test authentication bypass
            services.AddAuthentication("TestScheme")
                .AddScheme<TestAuthenticationOptions, TestAuthenticationHandler>(
                    "TestScheme", options => { });
        });
    }

    private static string GetTestConnectionString()
    {
        return Environment.GetEnvironmentVariable("TEST_DB_CONNECTION")
            ?? "Host=localhost;Database=test_db;Username=postgres;Password=postgres";
    }
}
```

### 2. TestAuthenticationHandler (Infrastructure/TestAuthenticationHandler.cs)

```csharp
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace {Namespace}.Infrastructure;

public class TestAuthenticationOptions : AuthenticationSchemeOptions
{
    public string UserId { get; set; } = "test-user-id";
    public string UserName { get; set; } = "test-user";
    public List<string> Roles { get; set; } = ["Admin"];
}

public class TestAuthenticationHandler : AuthenticationHandler<TestAuthenticationOptions>
{
    public TestAuthenticationHandler(
        IOptionsMonitor<TestAuthenticationOptions> options,
        ILoggerFactory logger,
        UrlEncoder encoder)
        : base(options, logger, encoder) { }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new List<Claim>
        {
            new(ClaimTypes.NameIdentifier, Options.UserId),
            new(ClaimTypes.Name, Options.UserName),
            new("sub", Options.UserId),
        };

        foreach (var role in Options.Roles)
            claims.Add(new Claim(ClaimTypes.Role, role));

        var identity = new ClaimsIdentity(claims, "TestScheme");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        return Task.FromResult(AuthenticateResult.Success(ticket));
    }
}
```

### 3. TestContext (Infrastructure/TestContext.cs)

```csharp
namespace {Namespace}.Infrastructure;

/// <summary>
/// Shared test state across SpecFlow steps within a scenario.
/// </summary>
public class TestContext : IDisposable
{
    public HttpClient HttpClient { get; set; } = null!;
    public {Entity}ApiClient ApiClient { get; set; } = null!;

    // Authentication state
    public string CurrentUserId { get; set; } = "test-user-id";
    public HashSet<string> UserPermissions { get; } = [];

    // Navigation/UI state (CRITICAL for state-dependent steps)
    public string? CurrentPage { get; set; }
    public bool IsDialogOpen { get; set; }
    public string? DialogType { get; set; }

    // Form/input state
    public Dictionary<string, object?> FormData { get; } = new();

    // API response state
    public int LastStatusCode { get; set; }
    public string? LastErrorMessage { get; set; }

    // Cleanup tracking
    public List<Guid> CreatedEntityIds { get; } = [];

    public void Dispose() => HttpClient?.Dispose();
}
```

### 4. TestHooks (Infrastructure/TestHooks.cs)

```csharp
using TechTalk.SpecFlow;

namespace {Namespace}.Infrastructure;

[Binding]
public class TestHooks
{
    private static {ServiceName}WebApplicationFactory? _factory;
    private readonly ScenarioContext _scenarioContext;

    public TestHooks(ScenarioContext scenarioContext)
    {
        _scenarioContext = scenarioContext;
    }

    [BeforeTestRun]
    public static void BeforeTestRun()
    {
        _factory = new {ServiceName}WebApplicationFactory();
    }

    [AfterTestRun]
    public static void AfterTestRun()
    {
        _factory?.Dispose();
        _factory = null;
    }

    [BeforeScenario]
    public void BeforeScenario()
    {
        var httpClient = _factory!.CreateClient();
        var apiClient = new {Entity}ApiClient(httpClient);

        var testContext = new TestContext
        {
            HttpClient = httpClient,
            ApiClient = apiClient
        };

        _scenarioContext["TestContext"] = testContext;
    }

    [AfterScenario]
    public async Task AfterScenario()
    {
        var context = _scenarioContext.Get<TestContext>("TestContext");

        // Clean up created entities
        foreach (var id in context.CreatedEntityIds)
        {
            try { await context.ApiClient.DeleteAsync(id); }
            catch { /* Ignore cleanup errors */ }
        }

        context.Dispose();
    }
}

public static class TestContextExtensions
{
    public static TestContext GetTestContext(this ScenarioContext ctx)
        => ctx.Get<TestContext>("TestContext");
}
```

### 5. Typed API Client (Infrastructure/{Entity}ApiClient.cs)

```csharp
using System.Net.Http.Json;

namespace {Namespace}.Infrastructure;

public class {Entity}ApiClient
{
    private readonly HttpClient _client;

    public {Entity}ApiClient(HttpClient client) => _client = client;

    public async Task<HttpResponseMessage> CreateAsync({CreateRequest} request)
        => await _client.PostAsJsonAsync("/api/{entities}", request);

    public async Task<{Entity}Dto?> GetByIdAsync(Guid id)
        => await _client.GetFromJsonAsync<{Entity}Dto>($"/api/{entities}/{id}");

    public async Task<IEnumerable<{Entity}Dto>> GetAllAsync()
        => await _client.GetFromJsonAsync<IEnumerable<{Entity}Dto>>("/api/{entities}") ?? [];

    public async Task<HttpResponseMessage> DeleteAsync(Guid id)
        => await _client.DeleteAsync($"/api/{entities}/{id}");
}
```

## STATE MANAGEMENT RULES (CRITICAL)

**Given steps that establish preconditions MUST also initialize ALL related state.**

### Example: Dialog Step Missing State

❌ **WRONG** - Missing page context:
```csharp
[Given(@"I am in the cancel confirmation dialog")]
public void GivenIAmInCancelDialog()
{
    Context.IsDialogOpen = true;
    Context.DialogType = "CancelConfirmation";
    // BUG: CurrentPage not set - subsequent "stay on page" assertion fails!
}
```

✅ **CORRECT** - Complete state initialization:
```csharp
[Given(@"I am in the cancel confirmation dialog")]
public void GivenIAmInCancelDialog()
{
    // The dialog appears on the create page - must set that context!
    Context.CurrentPage = "Create Customer";
    Context.IsDialogOpen = true;
    Context.DialogType = "CancelConfirmation";
}
```

### State Dependency Chain

When implementing steps, trace the full state dependency:

```
Scenario: Cancel creation and stay
  Given I am in the cancel confirmation dialog  ← Must set: CurrentPage, IsDialogOpen, DialogType
  When I click "Cancel"                          ← Uses: DialogType, Updates: IsDialogOpen
  Then dialog closes                             ← Asserts: IsDialogOpen = false
  And I should still be on "Create Customer"     ← Asserts: CurrentPage (FAILS if not set!)
```

## Step Analysis Report

Before generating, present analysis to user:

```markdown
## Step Analysis for: OpprettKunde.feature

### Statistics
- Total scenarios: 45
- Unique Given steps: 23
- Unique When steps: 15
- Unique Then steps: 31
- Steps with tables: 8
- Parameterized steps: 42

### Step Categories
1. **Authentication** (3 steps)
   - `jeg er innlogget som selger/admin-bruker...`
   - `jeg har rettigheten "..."`

2. **Field Manipulation** (12 steps)
   - `feltet "..." har verdien "..."`
   - `feltet "..." er tomt`
   - `jeg tømmer feltet "..."`

3. **Navigation** (8 steps)
   - `jeg står på dashboardet...`
   - `jeg trykker på knappen "..."`

4. **Validation** (15 steps)
   - `skal jeg få en valideringsfeil`
   - `kunden skal ikke være opprettet`

### Shared Steps (appear in multiple features)
- `jeg er innlogget som...` → Consider CommonSteps.cs
- `jeg trykker på knappen "..."` → Consider CommonSteps.cs

### Feature-Specific Steps
- `kunden skal bli opprettet med sektoren "..."` → OpprettKundeSteps.cs
```

## Modes

| Mode | Description |
|------|-------------|
| **analyze** | Show step analysis without generating files |
| **generate** | Generate step definition files |
| **scaffold** | Generate steps + hooks + test infrastructure |
| **update** | Add new steps to existing binding classes |

## Completion Criteria

- [ ] All unique steps extracted from feature file(s)
- [ ] Regex patterns validated (no compilation errors)
- [ ] Step definition class compiles
- [ ] No duplicate step definitions
- [ ] Tables handled correctly
- [ ] User confirmed output location
- [ ] **Infrastructure created**: WebApplicationFactory, TestAuthenticationHandler, TestContext, TestHooks
- [ ] **Real implementations**: No `PendingStepException()` - all steps have actual logic
- [ ] **State management verified**: Given steps initialize all state needed by subsequent steps
- [ ] **Tests pass**: At least one scenario executes successfully

## Anti-patterns (avoid)

- ❌ **Using `throw new PendingStepException()`** — This creates dead code that never tests anything
- ❌ **Given steps without complete state setup** — If step implies page context, SET the CurrentPage
- ❌ Editing auto-generated `.feature.cs` files (SpecFlow regenerates them)
- ❌ Hardcoding test data in step implementations
- ❌ Creating overly specific step patterns (prefer parameterized)
- ❌ Mixing UI and API steps in same binding class
- ❌ Ignoring table structure in step definitions
- ❌ Duplicating steps across multiple binding classes
- ❌ **Forgetting cleanup** — Always track created entities and delete in AfterScenario
- ❌ **Missing partial class** — Program.cs needs `public partial class Program { }` for WebApplicationFactory

## Norwegian Gherkin Keywords Reference

| English | Norwegian |
|---------|-----------|
| Feature | Egenskap |
| Background | Bakgrunn |
| Scenario | Scenario |
| Scenario Outline | Scenariomal |
| Examples | Eksempler |
| Given | Gitt |
| When | Når |
| Then | Så |
| And | Og |
| But | Men |
| Rule | Regel |

## Example Session

**User**: Generate steps for OpprettKunde.feature

**Agent**:
1. Reads `OpprettKunde.feature`
2. Detects `#language: no`
3. Extracts 89 unique steps
4. Presents analysis report
5. Asks: "Generate single file or per-category?"
6. User: "Single file"
7. Generates `OpprettKundeSteps.cs`
8. Shows preview
9. User confirms
10. Writes file to `IntegrationTests/Features/Kunde/Hovedfunksjoner/OpprettKundeSteps.cs`

## Usage Invocation

```
/ao-gherkin-step-generator [feature-file-or-directory] [mode]

Examples:
/ao-gherkin-step-generator IntegrationTests/Features/Kunde/OpprettKunde.feature generate
/ao-gherkin-step-generator IntegrationTests/Features/Kunde/ analyze
/ao-gherkin-step-generator attached-feature.feature scaffold
```
