# SkillPatch skill: reqnroll-skill

This skill guides QA engineers and test architects in generating production-grade Reqnroll BDD automation scripts for web (Selenium 3/4) and mobile (Appium 2) testing in C#. It covers feature file authoring, step definitions, hooks, parallel NUnit execution, ScenarioContext-based driver sharing, and cloud integration with TestMu AI (LambdaTest). It also supports SpecFlow-to-Reqnroll migration workflows.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/reqnroll-skill
curl -sSL https://skillpatch.dev/install_skill/reqnroll-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (6)

- `SKILL.md`
- `reference/appium-patterns.md`
- `reference/cloud-integration.md`
- `reference/playbook.md`
- `reference/selenium-3-patterns.md`
- `reference/selenium-4-patterns.md`


### `SKILL.md`

````markdown
---
name: reqnroll-skill
description: >
  Generates production-grade Reqnroll BDD automation scripts for web (Selenium 3/4)
  and mobile (Appium 2) testing in C#. Supports parallel NUnit execution locally and
  on TestMu AI cloud. Use when the user asks to write BDD tests, automate with Reqnroll,
  create .feature files, write Gherkin scenarios, write step definitions, migrate from
  SpecFlow, or test on browsers/Android/iOS. Triggers on: "Reqnroll", "BDD", "Gherkin",
  ".feature file", "step definition", "SpecFlow migration", "Selenium C#", "Appium C#",
  "TestMu", "LambdaTest", "NUnit BDD", "reqnroll.actions.json".
languages:
  - C#
category: bdd-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

## Overview

This skill guides QA engineers and test architects in writing production-grade Reqnroll
BDD tests for web and mobile automation in C#. It covers three execution paths — Selenium 4
with manual driver management, Selenium 3 via the `Reqnroll.Actions` plugin, and Appium 2
for Android mobile — all targeting TestMu AI (LambdaTest) cloud infrastructure.

Reqnroll is the actively maintained open-source successor to SpecFlow. Existing SpecFlow
projects can migrate by swapping the NuGet package and namespace — no step definition
rewrites required.

## Key Execution Pathways

**Framework Selection:** Distinguishes between Selenium 4 (manual `DriverFactory`),
Selenium 3 (`Reqnroll.SpecFlowCompatibility.Actions.LambdaTest` plugin with
`IBrowserInteractions`), and Appium 2 (`Appium.WebDriver`, `AndroidDriver`).

**Cloud vs Local:** Reads `LT_USERNAME` and `LT_ACCESS_KEY` environment variables;
routes to `hub.lambdatest.com` (web) or `mobile-hub.lambdatest.com` (mobile).
Reports pass/fail to LambdaTest via `lambda-status` JavaScript executor calls in
`[AfterScenario]`.

**Parallelism:** Uses `[assembly: Parallelizable(ParallelScope.Fixtures)]` with
`[assembly: LevelOfParallelism(N)]` (NUnit). State is shared between step definition
classes via `ScenarioContext` (injected by Reqnroll's DI container), not static fields.

## Core Technical Patterns

### Feature Files (Gherkin)
Each `.feature` file maps to one test class. Scenarios are tagged (`@tagName`) for
selective filtering with `dotnet test --filter "Category=tagName"`. Background steps
run before every scenario in the file; Scenario Outlines drive data-driven testing via
`Examples` tables.

### Step Definitions
Classes are decorated with `[Binding]`. Constructor injection (via Reqnroll's built-in
DI) receives `ScenarioContext` or shared context objects. One `[Binding]` class per
concern keeps files small. Regex-based step patterns use `(.*)` or typed captures
(`(\d+)`) — no attribute-level type converters needed for primitives.

### Hooks
`[BeforeScenario]` initialises the driver (stored in `ScenarioContext["driver"]`) and
navigates to the base URL. `[AfterScenario]` reads `_scenarioContext.TestError` (web) or
`TestContext.CurrentContext.Result.Outcome.Status` (mobile) to emit
`lambda-status=passed/failed` before `driver.Quit()`.

### ScenarioContext Driver Sharing
Drivers are stored as `_scenarioContext["driver"] = driver` and retrieved with
`scenarioContext["driver"] as IWebDriver`. This is required for parallel execution —
`static` driver fields cause race conditions.

### Explicit Waits
`WebDriverWait` with `Until(d => d.FindElement(locator))` replaces `ImplicitWait`
for dynamic content. A `WaitAndFind(By)` helper method encapsulates the 10-second
default; a `WaitAndClick(By, int timeout)` variant handles clickability.

## Cloud Integration (TestMu / LambdaTest)

### Web (Selenium 4)
```csharp
var ltOptions = new Dictionary<string, object>
{
    { "build", "Build Name" },
    { "project", "Project Name" },
    { "w3c", true },
    { "selenium_version", "4.38.0" },
    { "sessionName", scenarioName },
    { "platformName", "Windows 11" }
};
var options = new ChromeOptions();
options.BrowserVersion = "latest";
options.AddAdditionalOption("LT:Options", ltOptions);
var driver = new RemoteWebDriver(
    new Uri($"https://{userName}:{accessKey}@hub.lambdatest.com/wd/hub"), options);
```

### Mobile (Appium 2)
```csharp
var ltOptions = new Dictionary<string, object>
{
    { "build", "Build Name" },
    { "project", "Project Name" },
    { "w3c", true },
    { "app", "proverbial-android" },         // lt:// URI or pre-uploaded alias
    { "platformName", "android" },
    { "deviceName", "Galaxy.*" },
    { "platformVersion", "14" },
    { "isRealMobile", true },
    { "autoAcceptAlerts", true },
    { "autoGrantPermissions", true },
    { "sessionName", scenarioName }
};
var appiumOptions = new AppiumOptions();
appiumOptions.AddAdditionalAppiumOption("LT:Options", ltOptions);
var driver = new AndroidDriver(
    new Uri($"https://{userName}:{accessKey}@mobile-hub.lambdatest.com/wd/hub"),
    appiumOptions);
```

### Reporting Pass/Fail
```csharp
// Web (AfterScenario)
if (_scenarioContext.TestError == null)
    ((IJavaScriptExecutor)driver).ExecuteScript("lambda-status=passed");
else
    ((IJavaScriptExecutor)driver).ExecuteScript("lambda-status=failed");

// Mobile (AfterScenario)
bool passed = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed;
((IJavaScriptExecutor)driver).ExecuteScript("lambda-status=" + (passed ? "passed" : "failed"));
```

## Quality Checkpoints

- Feature files use descriptive scenario names that double as the LambdaTest session name
- `ScenarioContext` used for driver sharing — never static fields in parallel runs
- `[assembly: Parallelizable(ParallelScope.Fixtures)]` declared once in any `.cs` file
- `LT_USERNAME` and `LT_ACCESS_KEY` read from environment — never hardcoded
- `lambda-status=passed/failed` emitted in every `[AfterScenario]` for cloud runs
- `WebDriverWait` used throughout — no unconditional `Thread.Sleep` except transient Appium delays
- Appium locators use `MobileBy.Id` (resource-id) or `MobileBy.AccessibilityId` before XPath
- `driver.Quit()` always called in `[AfterScenario]` to free cloud device slots
- `dotnet test --logger "console;verbosity=detailed"` surfaces per-scenario pass/fail

## Reference Structure

The `reference/` directory contains detailed playbook sections:

| File | Contents |
|------|----------|
| `playbook.md` | Full implementation guide: project setup, all three driver modes, parallel execution, CI/CD, debugging table, best practices checklist |
| `cloud-integration.md` | LambdaTest capability reference, `LT:Options` fields, tunnel setup, build/session naming, test observability |
| `selenium-4-patterns.md` | Selenium 4 patterns: `DriverFactory`, multi-browser, `ChromeOptions`/`FirefoxOptions`/`EdgeOptions`, screenshot on failure |
| `selenium-3-patterns.md` | Selenium 3 patterns: `Reqnroll.SpecFlowCompatibility.Actions.LambdaTest`, `IBrowserInteractions`, `reqnroll.actions.json` config |
| `appium-patterns.md` | Appium 2 patterns: `AndroidDriver`, `AppiumOptions`, gesture helpers, `MobileBy` locators, app lifecycle |

````


### `reference/appium-patterns.md`

````markdown
# Reqnroll — Appium 2 Patterns (Android / iOS)

## Overview

Reqnroll + Appium 2 uses the same `[Binding]` / `ScenarioContext` / hooks pattern as
the Selenium 4 path, swapping `IWebDriver` for `AppiumDriver` / `AndroidDriver` and
`RemoteWebDriver` for the Appium-specific driver class. Cloud target is
`mobile-hub.lambdatest.com`.

---

## Project Structure

```
appium/
├── Features/
│   └── proverbial-ops.feature
├── StepDefinitions/
│   └── ProverbialActionsStepDefinitions.cs
├── Support/
│   ├── DriverFactory.cs
│   └── Hooks.cs
├── reqnroll.cloud.csproj
└── reqnroll.cloud.sln
```

---

## NuGet Packages

```xml
<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk"           Version="18.0.1" />
  <PackageReference Include="Selenium.WebDriver"               Version="4.38.0" />
  <PackageReference Include="Selenium.Support"                 Version="4.38.0" />
  <PackageReference Include="Appium.WebDriver"                 Version="8.2.0" />
  <PackageReference Include="DotNetSeleniumExtras.WaitHelpers" Version="3.11.0" />
  <PackageReference Include="Reqnroll.NUnit"                   Version="3.3.4" />
  <PackageReference Include="nunit"                            Version="4.5.1" />
  <PackageReference Include="NUnit3TestAdapter"                Version="6.2.0" />
  <PackageReference Include="FluentAssertions"                 Version="8.8.0" />
</ItemGroup>
```

---

## DriverFactory (Android)

```csharp
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;

namespace appium.Support
{
    public static class DriverFactory
    {
        public static AndroidDriver CreateCloudAppiumDriver(string scenarioName)
        {
            string userName  = Environment.GetEnvironmentVariable("LT_USERNAME")   ?? "LT_USERNAME";
            string accessKey = Environment.GetEnvironmentVariable("LT_ACCESS_KEY") ?? "LT_ACCESS_KEY";

            var gridUrl = new Uri(
                $"https://{userName}:{accessKey}@mobile-hub.lambdatest.com/wd/hub");

            var ltOptions = new Dictionary<string, object>
            {
                { "build",                "[Appium 2] Reqnroll Demo" },
                { "project",              "Reqnroll_Appium2_Demo" },
                { "w3c",                  true },
                { "app",                  "proverbial-android" },
                { "platformName",         "android" },
                { "deviceName",           "Galaxy.*" },
                { "platformVersion",      "14" },
                { "isRealMobile",         true },
                { "autoAcceptAlerts",     true },
                { "autoGrantPermissions", true },
                { "sessionName",          scenarioName }
            };

            var options = new AppiumOptions();
            options.AddAdditionalAppiumOption("LT:Options", ltOptions);

            var driver = new AndroidDriver(gridUrl, options);
            driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
            return driver;
        }
    }
}
```

---

## Hooks

```csharp
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium.Android;
using Reqnroll;

namespace appium.Support
{
    [Binding]
    public sealed class Hooks
    {
        private readonly ScenarioContext _scenarioContext;

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

        [BeforeScenario]
        public void SetUp()
        {
            string scenarioName = _scenarioContext.ScenarioInfo.Title;
            var driver = DriverFactory.CreateCloudAppiumDriver(scenarioName);
            _scenarioContext["driver"] = driver;
        }

        [AfterScenario]
        public void TearDown()
        {
            bool passed = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed;
            if (_scenarioContext.TryGetValue("driver", out AndroidDriver driver))
            {
                ((IJavaScriptExecutor)driver).ExecuteScript(
                    "lambda-status=" + (passed ? "passed" : "failed"));
                driver.Quit();
            }
        }
    }
}
```

---

## Feature File

```gherkin
Feature: [Appium 2] Perform actions in Proverbial App

@operations
Scenario: @F1: Validate all UI actions in the Proverbial App
    When I toggle the text color
    And I change the text using the text button
    And I trigger the toast message
    And I tap the notification button
    And I open the geolocation screen
    And I return back to the home screen
    And I open the speed test screen
    And I return back to the home screen
    And I open the in-app browser
    And I enter the url "https://www.lambdatest.com"
    Then the url should be entered successfully
```

---

## Step Definitions

```csharp
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;
using OpenQA.Selenium.Support.UI;
using Reqnroll;
using SeleniumExtras.WaitHelpers;

namespace appium.StepDefinitions
{
    [Binding]
    public class ProverbialActionsStepDefinitions
    {
        private readonly AppiumDriver _driver;

        public ProverbialActionsStepDefinitions(ScenarioContext scenarioContext)
        {
            _driver = scenarioContext["driver"] as AppiumDriver;
        }

        private IWebElement WaitAndFind(By locator) =>
            new WebDriverWait(_driver, TimeSpan.FromSeconds(10))
                .Until(d => d.FindElement(locator));

        private IWebElement WaitAndClick(By locator, int timeout = 20)
        {
            var element = (IWebElement)new WebDriverWait(_driver, TimeSpan.FromSeconds(timeout))
                .Until(ExpectedConditions.ElementToBeClickable(locator));
            Thread.Sleep(800);
            element.Click();
            return element;
        }

        [When(@"I toggle the text color")]
        public void WhenIToggleTheTextColor()
        {
            var colorButton = WaitAndClick(MobileBy.Id("color"));
            Thread.Sleep(600);
            colorButton.Click();
        }

        [When(@"I change the text using the text button")]
        public void WhenIChangeTheTextUsingTheTextButton()
        {
            WaitAndClick(MobileBy.Id("Text"));
        }

        [When(@"I trigger the toast message")]
        public void WhenITriggerTheToastMessage()
        {
            WaitAndClick(MobileBy.Id("toast"));
        }

        [When(@"I tap the notification button")]
        public void WhenITapTheNotificationButton()
        {
            WaitAndClick(MobileBy.Id("notification"));
            Thread.Sleep(1500);
        }

        [When(@"I open the geolocation screen")]
        public void WhenIOpenTheGeolocationScreen()
        {
            WaitAndClick(MobileBy.Id("geoLocation"));
            Thread.Sleep(3000);
        }

        [When(@"I return back to the home screen")]
        public void WhenIReturnBackToTheHomeScreen()
        {
            // KEYCODE_BACK (0x4) via lambda-adb executor
            _driver.ExecuteScript("lambda-adb", new Dictionary<string, object>
            {
                { "command", "keyevent" },
                { "keycode", 0x4 }
            });
            Thread.Sleep(800);
        }

        [When(@"I open the speed test screen")]
        public void WhenIOpenTheSpeedTestScreen()
        {
            WaitAndClick(MobileBy.Id("speedTest"));
            Thread.Sleep(4000);
        }

        [When(@"I open the in-app browser")]
        public void WhenIOpenTheInAppBrowser()
        {
            WaitAndClick(
                MobileBy.XPath(
                    "//android.widget.FrameLayout[@content-desc='Browser']" +
                    "/android.widget.FrameLayout/android.widget.ImageView"),
                timeout: 30);
        }

        [When(@"I enter the url ""(.*)""")]
        public void WhenIEnterTheUrl(string urlText)
        {
            var urlField = WaitAndClick(MobileBy.Id("url"));
            urlField.SendKeys(urlText);
            Thread.Sleep(800);
            WaitAndClick(MobileBy.XPath("//*[@resource-id='com.lambdatest.proverbial:id/find']"))
                .Click();
            Thread.Sleep(10000);
        }

        [Then(@"the url should be entered successfully")]
        public void ThenTheUrlShouldBeEnteredSuccessfully()
        {
            Console.WriteLine("URL entry verified");
        }
    }
}
```

---

## Locator Strategy

| Priority | Locator | Example |
|----------|---------|---------|
| 1 | `MobileBy.AccessibilityId` | `MobileBy.AccessibilityId("login_button")` |
| 2 | `MobileBy.Id` (resource-id) | `MobileBy.Id("com.pkg:id/btn")` or short form `MobileBy.Id("btn")` |
| 3 | `MobileBy.AndroidUIAutomator` | `MobileBy.AndroidUIAutomator("new UiSelector().text(\"Login\")")` |
| 4 | `MobileBy.XPath` | Use sparingly; 10x slower than id/accessibilityId |

---

## LambdaTest ADB Execution

Send ADB key events via `lambda-adb` executor without shell access:

```csharp
_driver.ExecuteScript("lambda-adb", new Dictionary<string, object>
{
    { "command", "keyevent" },
    { "keycode", 0x4 }   // KEYCODE_BACK
});
```

Common keycodes:
| Key | Code |
|-----|------|
| Back | `0x4` |
| Home | `0x3` |
| Enter | `0x42` |
| Menu | `0x52` |

---

## iOS Support

For iOS, replace `AndroidDriver` with `IOSDriver` and adjust `LT:Options`:

```csharp
var ltOptions = new Dictionary<string, object>
{
    { "app",             "lt://YOUR_IOS_APP_ID" },
    { "platformName",    "ios" },
    { "deviceName",      "iPhone 15" },
    { "platformVersion", "17" },
    { "isRealMobile",    true },
    { "autoAcceptAlerts", true },
    { "sessionName",     scenarioName }
};
var options = new AppiumOptions();
options.AddAdditionalAppiumOption("LT:Options", ltOptions);
var driver = new IOSDriver(gridUrl, options);
```

iOS locators use `MobileBy.IosNSPredicate` or `MobileBy.AccessibilityId`:
```csharp
MobileBy.IosNSPredicate("type == 'XCUIElementTypeButton' AND label == 'Login'")
MobileBy.AccessibilityId("login_button")
```

````


### `reference/cloud-integration.md`

````markdown
# Reqnroll — Cloud Integration (TestMu AI / LambdaTest)

## Authentication

Set environment variables before running tests:

```bash
export LT_USERNAME=<your-username>
export LT_ACCESS_KEY=<your-access-key>
```

Credentials are available at https://accounts.lambdatest.com/security.

Read in C# with a safe fallback for local development:
```csharp
string userName  = Environment.GetEnvironmentVariable("LT_USERNAME")   ?? "LT_USERNAME";
string accessKey = Environment.GetEnvironmentVariable("LT_ACCESS_KEY") ?? "LT_ACCESS_KEY";
```

---

## Grid Endpoints

| Target | URL |
|--------|-----|
| Web (Selenium) | `https://{user}:{key}@hub.lambdatest.com/wd/hub` |
| Mobile (Appium) | `https://{user}:{key}@mobile-hub.lambdatest.com/wd/hub` |

---

## LT:Options — Web (Selenium 4)

```csharp
var ltOptions = new Dictionary<string, object>
{
    { "build",            "Build name shown in dashboard" },
    { "project",          "Project name for grouping" },
    { "w3c",              true },
    { "selenium_version", "4.38.0" },
    { "sessionName",      scenarioName },          // per-test session label
    { "platformName",     "Windows 11" },
    { "video",            true },                  // record video (optional)
    { "network",          true },                  // capture network logs (optional)
    { "console",          true },                  // capture browser console (optional)
    { "visual",           true },                  // capture screenshots (optional)
};
var options = new ChromeOptions();
options.BrowserVersion = "latest";
options.AddAdditionalOption("LT:Options", ltOptions);
```

### Supported platformName values
`Windows 11`, `Windows 10`, `macOS Ventura`, `macOS Monterey`, `macOS Big Sur`

### Supported browserVersion values
`latest`, `latest-1`, `latest-2`, or a specific version number (e.g. `"120.0"`)

---

## LT:Options — Mobile (Appium 2)

```csharp
var ltOptions = new Dictionary<string, object>
{
    { "build",                "Build name" },
    { "project",              "Project name" },
    { "w3c",                  true },
    { "app",                  "lt://APP_ID" },           // from LambdaTest App Upload API
    { "platformName",         "android" },               // or "ios"
    { "deviceName",           "Galaxy S23" },            // regex supported: "Galaxy.*"
    { "platformVersion",      "14" },
    { "isRealMobile",         true },
    { "autoAcceptAlerts",     true },
    { "autoGrantPermissions", true },
    { "sessionName",          scenarioName },
    { "video",                true },
    { "devicelog",            true },
    { "network",              true },
};
var options = new AppiumOptions();
options.AddAdditionalAppiumOption("LT:Options", ltOptions);
```

---

## App Upload (Appium)

Upload your APK/IPA to LambdaTest before the test run to get an `lt://` URI:

```bash
curl -u "$LT_USERNAME:$LT_ACCESS_KEY" \
     -X POST "https://manual-api.lambdatest.com/app/upload/realDevice" \
     -F "appFile=@/path/to/app.apk" \
     -F "name=MyApp"
```

Response:
```json
{ "app_url": "lt://APP12345678" }
```

Use `lt://APP12345678` as the `"app"` value in `LT:Options`, or register a named alias
(e.g. `"proverbial-android"`) via the LambdaTest dashboard.

---

## Reporting Pass / Fail

LambdaTest requires an explicit status update via JavaScript executor — it cannot infer
pass/fail from driver.Quit() alone.

### Selenium 4

```csharp
[AfterScenario]
public void TearDown()
{
    var platform = Environment.GetEnvironmentVariable("EXEC_PLATFORM")?.ToLower() ?? "local";
    if (_scenarioContext.TryGetValue("driver", out IWebDriver driver))
    {
        if (platform == "cloud")
        {
            var status = _scenarioContext.TestError == null ? "passed" : "failed";
            ((IJavaScriptExecutor)driver).ExecuteScript($"lambda-status={status}");
        }
        driver.Quit();
    }
}
```

### Appium

```csharp
[AfterScenario]
public void TearDown()
{
    bool passed = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed;
    if (_scenarioContext.TryGetValue("driver", out AndroidDriver driver))
    {
        ((IJavaScriptExecutor)driver).ExecuteScript(
            "lambda-status=" + (passed ? "passed" : "failed"));
        driver.Quit();
    }
}
```

---

## Build and Session Naming

Consistent build names allow filtering history in the LambdaTest dashboard:

```csharp
{ "build",       $"[Selenium 4] {Environment.GetEnvironmentVariable("BUILD_NUMBER") ?? "local"}" },
{ "sessionName", scenarioName }   // ScenarioContext.ScenarioInfo.Title
```

`sessionName` maps each Reqnroll scenario to an individual session entry in the dashboard,
making it easy to correlate logs, screenshots, and videos to a specific test.

---

## LambdaTest Tunnel (testing localhost / staging environments)

Start the tunnel binary before the test run:

```bash
./LambdaTestTunnel --user $LT_USERNAME --key $LT_ACCESS_KEY --tunnelName myTunnel
```

Add to `LT:Options`:
```csharp
{ "tunnel",     true },
{ "tunnelName", "myTunnel" }
```

---

## Makefile Targets

```makefile
DOTNET         := dotnet
REQNROLL_PROJECT := reqnroll.cloud.sln

build:
    $(DOTNET) build $(REQNROLL_PROJECT)

reqnroll-automation-test:
    $(DOTNET) test $(REQNROLL_PROJECT) --logger "console;verbosity=detailed"
```

```bash
# Set credentials, then:
make build
make reqnroll-automation-test
```

````


### `reference/playbook.md`

````markdown
# Reqnroll — Advanced Implementation Playbook

## §1 — Project Setup

### Selenium 4 (manual driver, recommended for new projects)

```xml
<!-- reqnroll.cloud.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk"  Version="18.0.1" />
    <PackageReference Include="Selenium.WebDriver"      Version="4.38.0" />
    <PackageReference Include="Reqnroll.NUnit"           Version="2.4.1" />
    <PackageReference Include="nunit"                    Version="4.4.0" />
    <PackageReference Include="NUnit3TestAdapter"        Version="5.2.0" />
    <PackageReference Include="FluentAssertions"         Version="8.8.0" />
  </ItemGroup>
</Project>
```

### Selenium 3 (Reqnroll.Actions plugin — IBrowserInteractions abstraction)

```xml
<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk"                           Version="18.0.1" />
  <PackageReference Include="Selenium.WebDriver"                               Version="3.141.0" />
  <PackageReference Include="Reqnroll.SpecFlowCompatibility.Actions.LambdaTest" Version="0.2.6" />
  <PackageReference Include="Reqnroll.NUnit"                                   Version="2.4.1" />
  <PackageReference Include="nunit"                                             Version="4.4.0" />
  <PackageReference Include="NUnit3TestAdapter"                                 Version="5.2.0" />
  <PackageReference Include="FluentAssertions"                                 Version="8.8.0" />
</ItemGroup>
```

> **Note:** `Reqnroll.SpecFlowCompatibility.Actions.LambdaTest` is only compatible with
> Selenium 3.141. For Selenium 4, use manual `DriverFactory` instead.

### Appium 2 (Android / iOS mobile)

```xml
<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk"         Version="18.0.1" />
  <PackageReference Include="Selenium.WebDriver"             Version="4.38.0" />
  <PackageReference Include="Selenium.Support"               Version="4.38.0" />
  <PackageReference Include="Appium.WebDriver"               Version="8.2.0" />
  <PackageReference Include="DotNetSeleniumExtras.WaitHelpers" Version="3.11.0" />
  <PackageReference Include="Reqnroll.NUnit"                 Version="3.3.4" />
  <PackageReference Include="nunit"                          Version="4.5.1" />
  <PackageReference Include="NUnit3TestAdapter"              Version="6.2.0" />
  <PackageReference Include="FluentAssertions"               Version="8.8.0" />
</ItemGroup>
```

---

## §2 — Feature File Structure

```gherkin
Feature: ECommerce Playground Search

@searchItems
Scenario: Search for iPod Nano
    Given I select the Software category
    When I search for iPod Nano
    Then I should get 4 results for iPod Nano

@searchItems
Scenario Outline: Search for multiple products
    Given I select the <category> category
    When I search for <product>
    Then I should get <count> results for <product>

    Examples:
      | category | product       | count |
      | Tablets  | HTC Touch HD  | 8     |
      | Software | iPod Nano     | 4     |
```

**Conventions:**
- One `Feature:` per file; file name mirrors the feature name (kebab-case)
- Tag scenarios with `@tagName` to filter: `dotnet test --filter "Category=tagName"`
- Use `Background:` for steps that repeat in every scenario of the file
- Keep scenario titles unique — they become the LambdaTest session name

---

## §3 — Step Definitions

```csharp
using NUnit.Framework;
using OpenQA.Selenium;
using Reqnroll;
using OpenQA.Selenium.Support.UI;

[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]

namespace MyProject.StepDefinitions
{
    [Binding]
    public class SearchStepDefinitions
    {
        private readonly IWebDriver _driver;

        public SearchStepDefinitions(ScenarioContext scenarioContext)
        {
            _driver = scenarioContext["driver"] as IWebDriver;
        }

        private IWebElement WaitAndFind(By locator) =>
            new WebDriverWait(_driver, TimeSpan.FromSeconds(10))
                .Until(d => d.FindElement(locator));

        private IReadOnlyCollection<IWebElement> WaitAndFindAll(By locator) =>
            new WebDriverWait(_driver, TimeSpan.FromSeconds(10))
                .Until(d => d.FindElements(locator));

        [Given(@"I select the (.*) category")]
        public void GivenISelectTheCategory(string category)
        {
            WaitAndFind(By.XPath(
                "(//div[@class='dropdown search-category']/button[@type='button'])[1]")
            ).Click();
            WaitAndFind(By.XPath($"(//a[text()='{category}'])[1]")).Click();
        }

        [When(@"I search for (.*)")]
        public void WhenISearchFor(string product)
        {
            WaitAndFind(By.XPath("(//input[@name='search'])[1]")).SendKeys(product);
            WaitAndFind(By.XPath("(//button[normalize-space()='Search'])[1]")).Click();
        }

        [Then(@"I should get (.*) results for (.*)")]
        public void ThenIShouldGetResults(int expected, string product)
        {
            int actual = WaitAndFindAll(
                By.XPath($"//div[@class='row']//div[@class='carousel-item active']/img[@alt='{product}']")
            ).Count;
            Assert.That(actual, Is.EqualTo(expected));
        }
    }
}
```

---

## §4 — Hooks (BeforeScenario / AfterScenario)

### Selenium 4 Hooks

```csharp
using OpenQA.Selenium;
using Reqnroll;

namespace MyProject.Support
{
    [Binding]
    public sealed class Hooks
    {
        private readonly ScenarioContext _scenarioContext;

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

        [BeforeScenario]
        public void SetUp()
        {
            string scenarioName = _scenarioContext.ScenarioInfo.Title;
            var driver = DriverFactory.CreateDriver(scenarioName);
            _scenarioContext["driver"] = driver;
            driver.Navigate().GoToUrl("https://ecommerce-playground.lambdatest.io/");
        }

        [AfterScenario]
        public void TearDown()
        {
            var platform = Environment.GetEnvironmentVariable("EXEC_PLATFORM")?.ToLower() ?? "local";
            if (_scenarioContext.TryGetValue("driver", out IWebDriver driver))
            {
                if (platform == "cloud")
                {
                    var status = _scenarioContext.TestError == null ? "passed" : "failed";
                    ((IJavaScriptExecutor)driver).ExecuteScript($"lambda-status={status}");
                }
                driver.Quit();
            }
        }
    }
}
```

### Appium Hooks

```csharp
using NUnit.Framework;
using NUnit.Framework.Interfaces;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium.Android;
using Reqnroll;

namespace MyProject.Support
{
    [Binding]
    public sealed class Hooks
    {
        private readonly ScenarioContext _scenarioContext;

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

        [BeforeScenario]
        public void SetUp()
        {
            string scenarioName = _scenarioContext.ScenarioInfo.Title;
            var driver = DriverFactory.CreateCloudAppiumDriver(scenarioName);
            _scenarioContext["driver"] = driver;
        }

        [AfterScenario]
        public void TearDown()
        {
            bool passed = TestContext.CurrentContext.Result.Outcome.Status == TestStatus.Passed;
            if (_scenarioContext.TryGetValue("driver", out AndroidDriver driver))
            {
                ((IJavaScriptExecutor)driver).ExecuteScript(
                    "lambda-status=" + (passed ? "passed" : "failed"));
                driver.Quit();
            }
        }
    }
}
```

---

## §5 — DriverFactory (Selenium 4 — LambdaTest Cloud)

```csharp
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Remote;

namespace MyProject.Support
{
    public static class DriverFactory
    {
        public static IWebDriver CreateDriver(string scenarioName)
        {
            return CreateCloudDriver(scenarioName);
        }

        private static IWebDriver CreateCloudDriver(string scenarioName)
        {
            string userName  = Environment.GetEnvironmentVariable("LT_USERNAME")   ?? "LT_USERNAME";
            string accessKey = Environment.GetEnvironmentVariable("LT_ACCESS_KEY") ?? "LT_ACCESS_KEY";
            string browser   = Environment.GetEnvironmentVariable("BROWSER")?.ToLower() ?? "chrome";
            string os        = Environment.GetEnvironmentVariable("PLATFORM") ?? "Windows 11";

            var gridUrl = new Uri($"https://{userName}:{accessKey}@hub.lambdatest.com/wd/hub");

            var ltOptions = new Dictionary<string, object>
            {
                { "build",            "Reqnroll Cloud Build" },
                { "project",          "Reqnroll_Selenium4_Demo" },
                { "w3c",              true },
                { "selenium_version", "4.38.0" },
                { "sessionName",      scenarioName },
                { "platformName",     os }
            };

            DriverOptions options = browser switch
            {
                "firefox" => BuildFirefox(ltOptions),
                "edge"    => BuildEdge(ltOptions),
                _         => BuildChrome(ltOptions)
            };

            var driver = new RemoteWebDriver(gridUrl, options);
            driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
            return driver;
        }

        private static ChromeOptions BuildChrome(Dictionary<string, object> lt)
        {
            var o = new ChromeOptions { BrowserVersion = "latest" };
            o.AddAdditionalOption("LT:Options", lt);
            return o;
        }

        private static FirefoxOptions BuildFirefox(Dictionary<string, object> lt)
        {
            var o = new FirefoxOptions { BrowserVersion = "latest" };
            o.AddAdditionalOption("LT:Options", lt);
            return o;
        }

        private static EdgeOptions BuildEdge(Dictionary<string, object> lt)
        {
            var o = new EdgeOptions { BrowserVersion = "latest" };
            o.AddAdditionalOption("LT:Options", lt);
            return o;
        }
    }
}
```

---

## §6 — Parallel Execution

NUnit parallel attributes control concurrency at the assembly level. Declare once in any
`.cs` file in the project (typically the step definitions file):

```csharp
[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]
```

`ParallelScope.Fixtures` runs each `[Binding]` class (scenario group) concurrently.
Each scenario gets its own `ScenarioContext` instance from Reqnroll's DI container,
so `_scenarioContext["driver"]` is always scenario-scoped — no locking required.

Run with:
```bash
dotnet test reqnroll.cloud.sln --logger "console;verbosity=detailed"
```

---

## §7 — Selenium 3 with Reqnroll.Actions (IBrowserInteractions)

`Reqnroll.SpecFlowCompatibility.Actions.LambdaTest` injects `IBrowserInteractions`
automatically. No `DriverFactory` or `Hooks` class needed for basic use.

```csharp
[Binding]
public class SearchStepDefinitions
{
    private readonly IBrowserInteractions _browser;

    public SearchStepDefinitions(IBrowserInteractions browser)
    {
        _browser = browser;
    }

    [BeforeScenario]
    public void SetUp()
    {
        _browser.GoToUrl("https://ecommerce-playground.lambdatest.io/");
    }

    [Given(@"I select the (.*) category")]
    public void GivenISelectTheCategory(string category)
    {
        _browser.WaitAndReturnElement(
            By.XPath("(//div[@class='dropdown search-category']/button)[1]")).Click();
        _browser.WaitAndReturnElement(
            By.XPath($"(//a[text()='{category}'])[1]")).Click();
    }
}
```

### reqnroll.actions.json (browser and cloud config)

```json
{
  "selenium": {
    "disabled": true,
    "defaultTimeout": 60,
    "pollingInterval": 5,
    "lambdatest": {
      "url": "https://${LT_USERNAME}:${LT_ACCESS_KEY}@hub.lambdatest.com/wd/hub"
    }
  }
}
```

Per-browser overrides live in separate files, e.g.
`reqnroll.actions.windows11.chrome.json`:
```json
{
  "selenium": {
    "capabilities": {
      "browserName": "Chrome",
      "browserVersion": "latest",
      "LT:Options": {
        "platformName": "Windows 11",
        "build": "Selenium3 Build",
        "w3c": true
      }
    }
  }
}
```

---

## §8 — Appium 2 DriverFactory (Android)

```csharp
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;

public static class DriverFactory
{
    public static AndroidDriver CreateCloudAppiumDriver(string scenarioName)
    {
        string userName  = Environment.GetEnvironmentVariable("LT_USERNAME")   ?? "LT_USERNAME";
        string accessKey = Environment.GetEnvironmentVariable("LT_ACCESS_KEY") ?? "LT_ACCESS_KEY";

        var gridUrl = new Uri(
            $"https://{userName}:{accessKey}@mobile-hub.lambdatest.com/wd/hub");

        var ltOptions = new Dictionary<string, object>
        {
            { "build",              "[Appium 2] Reqnroll Demo" },
            { "project",            "Reqnroll_Appium2_Demo" },
            { "w3c",                true },
            { "app",                "proverbial-android" },
            { "platformName",       "android" },
            { "deviceName",         "Galaxy.*" },
            { "platformVersion",    "14" },
            { "isRealMobile",       true },
            { "autoAcceptAlerts",   true },
            { "autoGrantPermissions", true },
            { "sessionName",        scenarioName }
        };

        var options = new AppiumOptions();
        options.AddAdditionalAppiumOption("LT:Options", ltOptions);

        var driver = new AndroidDriver(gridUrl, options);
        driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
        return driver;
    }
}
```

---

## §9 — CI/CD Integration (GitHub Actions)

```yaml
name: Reqnroll Tests
on: [push, pull_request]

jobs:
  selenium4-cloud:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'
      - name: Restore dependencies
        run: dotnet restore selenium_4/reqnroll.cloud.sln
      - name: Build
        run: dotnet build selenium_4/reqnroll.cloud.sln
      - name: Run tests
        run: dotnet test selenium_4/reqnroll.cloud.sln --logger "console;verbosity=detailed"
        env:
          LT_USERNAME:  ${{ secrets.LT_USERNAME }}
          LT_ACCESS_KEY: ${{ secrets.LT_ACCESS_KEY }}
          EXEC_PLATFORM: cloud

  appium-cloud:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'
      - name: Run Appium tests
        run: dotnet test appium/reqnroll.cloud.sln --logger "console;verbosity=detailed"
        env:
          LT_USERNAME:  ${{ secrets.LT_USERNAME }}
          LT_ACCESS_KEY: ${{ secrets.LT_ACCESS_KEY }}
```

---

## §10 — Debugging Quick Reference

| Problem | Cause | Fix |
|---------|-------|-----|
| Step not found | Pattern mismatch | Check regex captures; run `dotnet test --list-tests` |
| NullRef on `_driver` | ScenarioContext key wrong | Use exact same key string in `[BeforeScenario]` and step ctor |
| Tests run serially | Missing `Parallelizable` attribute | Add `[assembly: Parallelizable(ParallelScope.Fixtures)]` |
| Cloud session not marked | `lambda-status` not called | Check `EXEC_PLATFORM=cloud` env var in `[AfterScenario]` |
| `IBrowserInteractions` null | Using Selenium 4 with Actions plugin | Actions plugin only supports Selenium 3.141; use `DriverFactory` for Selenium 4 |
| `RemoteWebDriver` connection refused | Wrong grid URL | Verify `LT_USERNAME`/`LT_ACCESS_KEY` env vars are set |
| Appium element not found | Wrong resource-id prefix | Include full package prefix: `com.packagename:id/element_id` |
| App not installed on device | Invalid app alias | Pre-upload app to LambdaTest and use the returned `lt://` URI |
| Parallel race on driver | Static driver field | Replace with `ScenarioContext["driver"]` instance field |
| Feature not discovered | Wrong namespace/assembly | Ensure `[Binding]` attribute on step definition class |
| Scenario title in session name shows blank | `ScenarioInfo.Title` called too early | Access inside `[BeforeScenario]`, not constructor |

---

## §11 — Best Practices Checklist

- Use `ScenarioContext["driver"]` for driver sharing — never static fields in parallel runs
- Declare `[assembly: Parallelizable(ParallelScope.Fixtures)]` at the assembly level once
- Set `LT_USERNAME` and `LT_ACCESS_KEY` from environment — never commit credentials
- Emit `lambda-status=passed/failed` in every `[AfterScenario]` hook when running on cloud
- Use `WebDriverWait` for all element interactions — no unconditional `Thread.Sleep` in web tests
- Use `MobileBy.Id` / `MobileBy.AccessibilityId` before XPath in Appium tests
- Name scenarios descriptively — titles appear as LambdaTest session names
- Tag scenarios with `@tagName` to enable selective test runs in CI
- Always call `driver.Quit()` in `[AfterScenario]` to release cloud device/browser slots
- One `[Binding]` class per feature area keeps step files focused and maintainable
- Use `dotnet test --logger "console;verbosity=detailed"` to see per-scenario results locally
- Structure: `Features/`, `StepDefinitions/`, `Support/` (Hooks, DriverFactory)

````


### `reference/selenium-3-patterns.md`

````markdown
# Reqnroll — Selenium 3 Patterns (Reqnroll.Actions Plugin)

## Overview

`Reqnroll.SpecFlowCompatibility.Actions.LambdaTest` provides `IBrowserInteractions`,
a high-level abstraction over Selenium that handles driver lifecycle, implicit waits,
and cloud configuration automatically. It is **only compatible with Selenium 3.141**.

Use this approach when migrating from SpecFlow + SpecFlow.Actions.LambdaTest, or when
you prefer plugin-managed driver setup over a manual `DriverFactory`.

---

## Project Structure

```
MyProject/
├── Features/
│   └── search.feature
├── StepDefinitions/
│   └── SearchStepDefinitions.cs
├── reqnroll.actions.json                    # default config
├── reqnroll.actions.windows11.chrome.json  # per-browser capability override
├── reqnroll.cloud.csproj
└── reqnroll.cloud.sln
```

No `Support/Hooks.cs` or `Support/DriverFactory.cs` required — the plugin handles both.

---

## NuGet Packages

```xml
<ItemGroup>
  <PackageReference Include="Microsoft.NET.Test.Sdk"                           Version="18.0.1" />
  <PackageReference Include="Selenium.WebDriver"                               Version="3.141.0" />
  <PackageReference Include="Reqnroll.SpecFlowCompatibility.Actions.LambdaTest" Version="0.2.6" />
  <PackageReference Include="Reqnroll.NUnit"                                   Version="2.4.1" />
  <PackageReference Include="nunit"                                             Version="4.4.0" />
  <PackageReference Include="NUnit3TestAdapter"                                 Version="5.2.0" />
  <PackageReference Include="FluentAssertions"                                 Version="8.8.0" />
</ItemGroup>
```

---

## reqnroll.actions.json

Base configuration file — disables local Selenium, points to LambdaTest:

```json
{
  "selenium": {
    "disabled": true,
    "defaultTimeout": 60,
    "pollingInterval": 5,
    "lambdatest": {
      "url": "https://${LT_USERNAME}:${LT_ACCESS_KEY}@hub.lambdatest.com/wd/hub"
    }
  }
}
```

`${LT_USERNAME}` and `${LT_ACCESS_KEY}` are resolved from environment variables at runtime.

---

## Per-Browser Capability Files

The plugin merges capability files matching the pattern
`reqnroll.actions.<os>.<browser>.json`.

### reqnroll.actions.windows11.chrome.json

```json
{
  "selenium": {
    "capabilities": {
      "browserName": "Chrome",
      "browserVersion": "latest",
      "LT:Options": {
        "platformName": "Windows 11",
        "build": "[Selenium 3] Reqnroll Demo",
        "project": "Reqnroll_Selenium3_Demo",
        "w3c": true
      }
    }
  }
}
```

### reqnroll.actions.windows10.firefox.json

```json
{
  "selenium": {
    "capabilities": {
      "browserName": "Firefox",
      "browserVersion": "latest",
      "LT:Options": {
        "platformName": "Windows 10",
        "build": "[Selenium 3] Reqnroll Demo",
        "project": "Reqnroll_Selenium3_Demo",
        "w3c": true
      }
    }
  }
}
```

---

## Step Definitions with IBrowserInteractions

```csharp
using NUnit.Framework;
using OpenQA.Selenium;
using Reqnroll;
using Reqnroll.Actions.Selenium;
using FluentAssertions;

[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]

namespace MyProject.StepDefinitions
{
    [Binding]
    public class SearchStepDefinitions
    {
        private readonly IBrowserInteractions _browser;

        public SearchStepDefinitions(IBrowserInteractions browser)
        {
            _browser = browser;
        }

        [BeforeScenario]
        public void SetUp()
        {
            _browser.GoToUrl("https://ecommerce-playground.lambdatest.io/");
        }

        [Given(@"I select the (.*) category")]
        public void GivenISelectTheCategory(string category)
        {
            _browser.WaitAndReturnElement(
                By.XPath("(//div[@class='dropdown search-category']/button[@type='button'])[1]")
            ).Click();
            _browser.WaitAndReturnElement(
                By.XPath($"(//a[text()='{category}'])[1]")
            ).Click();
        }

        [When(@"I search for (.*)")]
        public void WhenISearchFor(string product)
        {
            _browser.WaitAndReturnElement(
                By.XPath("(//input[@name='search'])[1]")
            ).SendKeys(product);
            _browser.WaitAndReturnElement(
                By.XPath("(//button[normalize-space()='Search'])[1]")
            ).Click();
        }

        [Then(@"I should get (.*) results for (.*)")]
        public void ThenIShouldGetResults(int expected, string product)
        {
            int actual = _browser.WaitAndReturnElements(
                By.XPath(
                    $"//div[@class='row']//div[@class='carousel-item active']/img[@alt='{product}']")
            ).Count();
            Assert.That(actual, Is.EqualTo(expected));
        }
    }
}
```

### IBrowserInteractions key methods

| Method | Description |
|--------|-------------|
| `GoToUrl(string url)` | Navigate to URL |
| `WaitAndReturnElement(By locator)` | Wait and return first matching element |
| `WaitAndReturnElements(By locator)` | Wait and return all matching elements |
| `GetUrl()` | Return current page URL |
| `GetTitle()` | Return current page title |

---

## Migrating from SpecFlow to Reqnroll

The namespace swap is the primary change required:

| SpecFlow | Reqnroll |
|----------|----------|
| `using TechTalk.SpecFlow;` | `using Reqnroll;` |
| `using SpecFlow.Actions.Selenium;` | `using Reqnroll.Actions.Selenium;` |
| `SpecFlow.Actions.LambdaTest` NuGet | `Reqnroll.SpecFlowCompatibility.Actions.LambdaTest` NuGet |

Step definitions, feature files, and `[Binding]` / `[BeforeScenario]` / `[AfterScenario]`
attributes remain identical.

````


### `reference/selenium-4-patterns.md`

````markdown
# Reqnroll — Selenium 4 Patterns

## Overview

Selenium 4 with Reqnroll uses a manual `DriverFactory` + `ScenarioContext` pattern.
The Actions plugin (`Reqnroll.SpecFlowCompatibility.Actions.LambdaTest`) is **not**
compatible with Selenium 4; use this approach instead.

---

## Project Structure

```
MyProject/
├── Features/
│   ├── search.feature
│   └── checkout.feature
├── StepDefinitions/
│   └── SearchStepDefinitions.cs
├── Support/
│   ├── DriverFactory.cs
│   └── Hooks.cs
├── reqnroll.cloud.csproj
└── reqnroll.cloud.sln
```

---

## DriverFactory

```csharp
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Remote;

namespace MyProject.Support
{
    public static class DriverFactory
    {
        public static IWebDriver CreateDriver(string scenarioName) =>
            CreateCloudDriver(scenarioName);

        private static IWebDriver CreateCloudDriver(string scenarioName)
        {
            string userName  = Environment.GetEnvironmentVariable("LT_USERNAME")   ?? "LT_USERNAME";
            string accessKey = Environment.GetEnvironmentVariable("LT_ACCESS_KEY") ?? "LT_ACCESS_KEY";
            string browser   = Environment.GetEnvironmentVariable("BROWSER")?.ToLower() ?? "chrome";
            string os        = Environment.GetEnvironmentVariable("PLATFORM") ?? "Windows 11";

            var ltOptions = new Dictionary<string, object>
            {
                { "build",            "[Selenium 4] Reqnroll Demo" },
                { "project",          "Reqnroll_Selenium4_Demo" },
                { "w3c",              true },
                { "selenium_version", "4.38.0" },
                { "sessionName",      scenarioName },
                { "platformName",     os }
            };

            var gridUrl = new Uri($"https://{userName}:{accessKey}@hub.lambdatest.com/wd/hub");

            DriverOptions options = browser switch
            {
                "firefox" => BuildFirefox(ltOptions),
                "edge"    => BuildEdge(ltOptions),
                _         => BuildChrome(ltOptions)
            };

            var driver = new RemoteWebDriver(gridUrl, options);
            driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(10);
            return driver;
        }

        private static ChromeOptions BuildChrome(Dictionary<string, object> lt)
        {
            var o = new ChromeOptions { BrowserVersion = "latest" };
            o.AddAdditionalOption("LT:Options", lt);
            return o;
        }

        private static FirefoxOptions BuildFirefox(Dictionary<string, object> lt)
        {
            var o = new FirefoxOptions { BrowserVersion = "latest" };
            o.AddAdditionalOption("LT:Options", lt);
            return o;
        }

        private static EdgeOptions BuildEdge(Dictionary<string, object> lt)
        {
            var o = new EdgeOptions { BrowserVersion = "latest" };
            o.AddAdditionalOption("LT:Options", lt);
            return o;
        }
    }
}
```

---

## Hooks

```csharp
using OpenQA.Selenium;
using Reqnroll;

namespace MyProject.Support
{
    [Binding]
    public sealed class Hooks
    {
        private readonly ScenarioContext _scenarioContext;

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

        [BeforeScenario]
        public void SetUp()
        {
            string scenarioName = _scenarioContext.ScenarioInfo.Title;
            var driver = DriverFactory.CreateDriver(scenarioName);
            _scenarioContext["driver"] = driver;
            driver.Navigate().GoToUrl("https://ecommerce-playground.lambdatest.io/");
        }

        [AfterScenario]
        public void TearDown()
        {
            var platform = Environment.GetEnvironmentVariable("EXEC_PLATFORM")?.ToLower() ?? "local";
            if (_scenarioContext.TryGetValue("driver", out IWebDriver driver))
            {
                if (platform == "cloud")
                {
                    var status = _scenarioContext.TestError == null ? "passed" : "failed";
                    ((IJavaScriptExecutor)driver).ExecuteScript($"lambda-status={status}");
                }
                driver.Quit();
            }
        }
    }
}
```

---

## Step Definitions

```csharp
using NUnit.Framework;
using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using Reqnroll;

[assembly: Parallelizable(ParallelScope.Fixtures)]
[assembly: LevelOfParallelism(4)]

namespace MyProject.StepDefinitions
{
    [Binding]
    public class SearchStepDefinitions
    {
        private readonly IWebDriver _driver;

        public SearchStepDefinitions(ScenarioContext scenarioContext)
        {
            _driver = scenarioContext["driver"] as IWebDriver;
        }

        private IWebElement WaitAndFind(By locator) =>
            new WebDriverWait(_driver, TimeSpan.FromSeconds(10))
                .Until(d => d.FindElement(locator));

        private IReadOnlyCollection<IWebElement> WaitAndFindAll(By locator) =>
            new WebDriverWait(_driver, TimeSpan.FromSeconds(10))
                .Until(d => d.FindElements(locator));

        [Given(@"I select the (.*) category")]
        public void GivenISelectTheCategory(string category)
        {
            WaitAndFind(
                By.XPath("(//div[@class='dropdown search-category']/button[@type='button'])[1]")
            ).Click();
            WaitAndFind(By.XPath($"(//a[text()='{category}'])[1]")).Click();
        }

        [When(@"I search for (.*)")]
        public void WhenISearchFor(string product)
        {
            WaitAndFind(By.XPath("(//input[@name='search'])[1]")).SendKeys(product);
            WaitAndFind(By.XPath("(//button[normalize-space()='Search'])[1]")).Click();
        }

        [Then(@"I should get (.*) results for (.*)")]
        public void ThenIShouldGetResults(int expected, string product)
        {
            int actual = WaitAndFindAll(
                By.XPath(
                    $"//div[@class='row']//div[@class='carousel-item active']/img[@alt='{product}']")
            ).Count;
            Assert.That(actual, Is.EqualTo(expected));
        }
    }
}
```

---

## Screenshot on Failure

Add to `[AfterScenario]` in Hooks:

```csharp
[AfterScenario]
public void TearDown()
{
    if (_scenarioContext.TryGetValue("driver", out IWebDriver driver))
    {
        if (_scenarioContext.TestError != null)
        {
            var screenshot = ((ITakesScreenshot)driver).GetScreenshot();
            string path = Path.Combine("screenshots", $"{_scenarioContext.ScenarioInfo.Title}.png");
            Directory.CreateDirectory("screenshots");
            screenshot.SaveAsFile(path);
        }
        driver.Quit();
    }
}
```

---

## Multiple Feature Files + Parallel Execution

With `ParallelScope.Fixtures`, each `[Binding]` class runs in its own thread. Each
scenario in each feature file gets an isolated `ScenarioContext`, so multiple features
execute concurrently without shared state conflicts.

```
Feature file 1 (Scenario A, B) ──► Thread 1
Feature file 2 (Scenario C, D) ──► Thread 2
Feature file 3 (Scenario E)    ──► Thread 3
Feature file 4 (Scenario F, G) ──► Thread 4
```

Set `LevelOfParallelism` to the number of parallel cloud browser sessions available
on your LambdaTest plan.

````
