# SkillPatch skill: lettuce-skill

Generates Lettuce BDD tests for Python projects, including Gherkin feature files, step definitions using regex patterns, and terrain setup/teardown configurations. Supports both local Selenium execution and cloud-based testing via LambdaTest/TestMu AI. Intended for legacy projects specifically using Lettuce; recommends migration to Behave for new projects.

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/lettuce-skill
curl -sSL https://skillpatch.dev/install_skill/lettuce-skill | tar -xz -C .claude/skills/
```

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


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: lettuce-skill
description: >
  Generates Lettuce BDD tests for Python with feature files and step definitions.
  Note: Lettuce is legacy/unmaintained; consider Behave for new projects. Use when
  user specifically mentions "Lettuce". Triggers on: "Lettuce", "lettuce test",
  "lettuce BDD".
languages:
  - Python
category: bdd-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Lettuce BDD Skill (Legacy)

> **Note:** Lettuce is largely unmaintained. For new Python BDD projects, use **Behave** instead.

## Core Patterns

### Feature File (features/login.feature)

```gherkin
Feature: User Login
  Scenario: Successful login
    Given I navigate to the login page
    When I enter "user@test.com" as email
    And I enter "password123" as password
    And I click login
    Then I should see the dashboard

  Scenario: Invalid login
    Given I navigate to the login page
    When I enter "bad@test.com" as email
    And I enter "wrong" as password
    And I click login
    Then I should see "Invalid credentials"
```

### Step Definitions (features/steps.py)

```python
from lettuce import step, world
from selenium import webdriver
from selenium.webdriver.common.by import By

@step(r'I navigate to the login page')
def navigate_to_login(step):
    world.browser = webdriver.Chrome()
    world.browser.get(world.base_url + '/login')

@step(r'I enter "([^"]*)" as email')
def enter_email(step, email):
    el = world.browser.find_element(By.ID, 'email')
    el.clear()
    el.send_keys(email)

@step(r'I enter "([^"]*)" as password')
def enter_password(step, password):
    el = world.browser.find_element(By.ID, 'password')
    el.clear()
    el.send_keys(password)

@step(r'I click login')
def click_login(step):
    world.browser.find_element(By.CSS_SELECTOR, 'button[type="submit"]').click()

@step(r'I should see the dashboard')
def see_dashboard(step):
    assert '/dashboard' in world.browser.current_url

@step(r'I should see "([^"]*)"')
def see_text(step, text):
    assert text in world.browser.page_source
```

### Terrain (Setup/Teardown — terrain.py)

```python
from lettuce import before, after, world

@before.all
def setup():
    world.base_url = 'http://localhost:3000'

@before.each_scenario
def before_scenario(scenario):
    pass

@after.each_scenario
def cleanup(scenario):
    if hasattr(world, 'browser'):
        world.browser.quit()

@after.all
def teardown(total):
    print(f"Ran {total.scenarios_ran} scenarios")
```

## Setup: `pip install lettuce selenium`
## Run: `lettuce` or `lettuce features/login.feature`

### Cloud Execution on TestMu AI

Set environment variables: `LT_USERNAME`, `LT_ACCESS_KEY`

```python
# terrain.py
from selenium import webdriver
import os

@before.all
def setup():
    lt_options = {
        "user": os.environ["LT_USERNAME"],
        "accessKey": os.environ["LT_ACCESS_KEY"],
        "build": "Lettuce Build",
        "platformName": "Windows 11",
        "video": True,
        "console": True,
    }
    options = webdriver.ChromeOptions()
    options.set_capability("LT:Options", lt_options)
    world.browser = webdriver.Remote(
        command_executor=f"https://{os.environ['LT_USERNAME']}:{os.environ['LT_ACCESS_KEY']}@hub.lambdatest.com/wd/hub",
        options=options,
    )
```
## Migration: Consider switching to `behave` for active maintenance

## Deep Patterns

For advanced patterns, debugging guides, CI/CD integration, and best practices,
see `reference/playbook.md`.

````


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

````markdown
# Lettuce BDD — Advanced Patterns & Playbook

## Step Definitions with Context

```python
from lettuce import step, world, before, after
import requests

@before.all
def setup():
    world.base_url = "http://localhost:3000"
    world.session = requests.Session()

@after.all
def teardown():
    world.session.close()

@step(r'I am authenticated as "(.*)" with password "(.*)"')
def auth_step(step, username, password):
    resp = world.session.post(f"{world.base_url}/auth",
        json={"username": username, "password": password})
    world.token = resp.json()["token"]
    world.session.headers["Authorization"] = f"Bearer {world.token}"

@step(r'I send a (\w+) request to "(.*)"')
def send_request(step, method, endpoint):
    world.response = getattr(world.session, method.lower())(
        f"{world.base_url}{endpoint}")

@step(r'the response status should be (\d+)')
def check_status(step, status):
    assert world.response.status_code == int(status), \
        f"Expected {status}, got {world.response.status_code}"

@step(r'the response should contain "(.*)"')
def check_body(step, text):
    assert text in world.response.text
```

## Feature File Patterns

```gherkin
Feature: User Management
  As an admin I want to manage users

  Background:
    Given I am authenticated as "admin" with password "secret"

  Scenario Outline: Create users with different roles
    When I create a user with name "<name>" and role "<role>"
    Then the response status should be 201
    And the user should have role "<role>"

    Examples:
      | name    | role    |
      | Alice   | editor  |
      | Bob     | viewer  |
      | Charlie | admin   |

  Scenario: Delete user
    Given a user "test-user" exists
    When I send a DELETE request to "/api/users/test-user"
    Then the response status should be 204
```

## Terrain (Environment Setup)

```python
# terrain.py
from lettuce import before, after, world
from selenium import webdriver

@before.each_scenario
def setup_browser(scenario):
    world.browser = webdriver.Chrome()
    world.browser.implicitly_wait(10)

@after.each_scenario
def teardown_browser(scenario):
    if hasattr(world, 'browser'):
        world.browser.quit()

@before.each_feature
def setup_db(feature):
    world.db = connect_test_db()
    world.db.seed()

@after.each_feature
def teardown_db(feature):
    world.db.truncate_all()
    world.db.close()
```

## Anti-Patterns

- ❌ Steps that depend on global state without explicit setup
- ❌ Overly specific step definitions — prefer reusable parameterized steps
- ❌ Missing cleanup in `@after` hooks — leads to resource leaks
- ❌ Business logic in step definitions — delegate to page objects or service classes

````


### `reference/playbook.md`

````markdown
# Lettuce — Advanced Implementation Playbook

> Lettuce is unmaintained. This playbook covers maintenance patterns for existing
> codebases and migration paths to Behave.

## §1 — Data-Driven Scenarios

```gherkin
Feature: User Registration
  Scenario Outline: Validate email formats
    Given I am on the registration page
    When I enter "<email>" as email
    And I submit the form
    Then I should see "<result>"

    Examples:
      | email            | result           |
      | user@example.com | Registration OK  |
      | invalid          | Invalid email    |
      | @nouser.com      | Invalid email    |
      | user@            | Invalid email    |
```

### Step with Table Data

```gherkin
  Scenario: Create multiple users
    Given the following users exist:
      | name    | email            | role   |
      | Alice   | alice@test.com   | admin  |
      | Bob     | bob@test.com     | user   |
    Then I should have 2 users in the system
```

```python
from lettuce import step, world

@step(r'the following users exist:')
def create_users(step):
    world.users = []
    for row in step.hashes:
        user = create_user(name=row['name'], email=row['email'], role=row['role'])
        world.users.append(user)

@step(r'I should have (\d+) users in the system')
def verify_user_count(step, count):
    assert len(world.users) == int(count), \
        f"Expected {count} users, got {len(world.users)}"
```

## §2 — Terrain Hooks with Error Handling

```python
# terrain.py
from lettuce import before, after, world
from selenium import webdriver
import os, time

@before.all
def setup():
    world.base_url = os.environ.get('BASE_URL', 'http://localhost:3000')
    world.screenshots_dir = 'screenshots'
    os.makedirs(world.screenshots_dir, exist_ok=True)

@before.each_scenario
def before_scenario(scenario):
    options = webdriver.ChromeOptions()
    if os.environ.get('CI'):
        options.add_argument('--headless=new')
        options.add_argument('--no-sandbox')
        options.add_argument('--disable-dev-shm-usage')
    world.browser = webdriver.Chrome(options=options)
    world.browser.implicitly_wait(10)
    world.browser.set_window_size(1920, 1080)

@after.each_scenario
def after_scenario(scenario):
    if hasattr(world, 'browser'):
        if scenario.failed:
            name = scenario.name.replace(' ', '_')
            ts = int(time.time())
            path = f"{world.screenshots_dir}/{name}_{ts}.png"
            world.browser.save_screenshot(path)
            print(f"Screenshot saved: {path}")
        world.browser.quit()

@after.all
def teardown(total):
    passed = total.scenarios_passed
    failed = total.scenarios_ran - passed
    print(f"\nResults: {passed} passed, {failed} failed out of {total.scenarios_ran}")
```

## §3 — API Testing Steps

```python
import requests

@step(r'I send a GET request to "([^"]*)"')
def send_get(step, endpoint):
    world.response = requests.get(world.base_url + endpoint,
        headers=getattr(world, 'headers', {}))

@step(r'I send a POST request to "([^"]*)" with:')
def send_post(step, endpoint):
    import json
    data = {row['key']: row['value'] for row in step.hashes}
    world.response = requests.post(world.base_url + endpoint,
        json=data, headers=getattr(world, 'headers', {}))

@step(r'the response status should be (\d+)')
def check_status(step, status):
    assert world.response.status_code == int(status), \
        f"Expected {status}, got {world.response.status_code}: {world.response.text}"

@step(r'the response should contain "([^"]*)"')
def check_body(step, text):
    assert text in world.response.text, \
        f"'{text}' not found in response: {world.response.text[:200]}"
```

## §4 — Debugging & Common Issues

| Problem | Cause | Fix |
|---------|-------|-----|
| `ImportError: No module named lettuce` | Not installed | `pip install lettuce` (Python 2.7/3.x) |
| Steps not discovered | Wrong directory | Steps must be in `features/steps/` or `features/steps.py` |
| `world` not shared | Scope issue | `world` persists across steps within a scenario |
| Browser not quitting | Missing cleanup | Always quit in `@after.each_scenario` |
| Unicode errors | Python 2 defaults | Use `# -*- coding: utf-8 -*-` at top of files |
| Feature not found | Wrong path | Run `lettuce features/specific.feature` |

## §5 — CI/CD Integration

```yaml
name: Lettuce Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.11' }
      - run: |
          pip install lettuce selenium requests
          pip install webdriver-manager
      - run: lettuce --verbosity=3
        env:
          CI: true
          BASE_URL: http://localhost:3000
      - uses: actions/upload-artifact@v4
        if: failure()
        with: { name: screenshots, path: screenshots/ }
```

## §6 — Migration to Behave (Recommended)

Lettuce → Behave mapping:

| Lettuce | Behave |
|---------|--------|
| `from lettuce import step, world` | `from behave import given, when, then` |
| `@step(r'pattern')` | `@given('pattern')`, `@when('pattern')`, `@then('pattern')` |
| `world.variable` | `context.variable` |
| `terrain.py` → `@before.all` | `environment.py` → `def before_all(context)` |
| `step.hashes` | `context.table` (with `table.rows`) |

### Migration Script

```python
#!/usr/bin/env python3
"""Convert Lettuce step files to Behave format."""
import re, sys

def convert(content):
    # Replace imports
    content = content.replace('from lettuce import step, world',
                              'from behave import given, when, then')
    content = content.replace('from lettuce import', 'from behave import')

    # Replace @step with appropriate BDD keyword
    content = re.sub(r"@step\(r?'I navigate", "@given('I navigate", content)
    content = re.sub(r"@step\(r?'I enter", "@when('I enter", content)
    content = re.sub(r"@step\(r?'I click", "@when('I click", content)
    content = re.sub(r"@step\(r?'I send", "@when('I send", content)
    content = re.sub(r"@step\(r?'I should", "@then('I should", content)
    content = re.sub(r"@step\(r?'the response", "@then('the response", content)

    # Replace world → context
    content = content.replace('world.', 'context.')

    # Replace step.hashes → context.table
    content = content.replace('step.hashes', 'context.table')

    return content

if __name__ == '__main__':
    for f in sys.argv[1:]:
        with open(f) as fh: original = fh.read()
        converted = convert(original)
        out = f.replace('.py', '_behave.py')
        with open(out, 'w') as fh: fh.write(converted)
        print(f"Converted: {f} → {out}")
```

### Feature files require NO changes — Gherkin syntax is identical.

## §7 — Best Practices

- Use `world` for shared state between steps (equivalent of Behave's `context`)
- Use terrain.py for all hooks — keep step files focused on step logic
- Always clean up browser in `@after.each_scenario`
- Take screenshots on failure for debugging
- Use Scenario Outline + Examples for data-driven tests
- Use step.hashes for table data
- **Plan migration to Behave** — Lettuce has no active development
- Keep features in business language, not technical implementation

````
