Skip to content

Commit

Permalink
initial commit for REDCap Cypress BDD + AI
Browse files Browse the repository at this point in the history
  • Loading branch information
irvins committed Sep 13, 2024
0 parents commit 3b63ebd
Show file tree
Hide file tree
Showing 14 changed files with 598 additions and 0 deletions.
101 changes: 101 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
/package-lock.json
/.pnp
.pnp.js

# Dependency directories
bower_components/

# Cypress logs and screenshots
cypress/screenshots/
cypress/videos/
results/

# dotenv environment variables file
.env
.env.local
.env.*.local

# Cypress local settings (to prevent committing Cypress-related settings that are local)
cypress.json
cypress.env.json

# Build artifacts (general)
dist/
build/
out/
coverage/
*.log

# Python bytecode
*.py[cod]
__pycache__/
*.so
*.dylib

# Virtual environments
.venv/
venv/
ENV/
env.bak/
env/

# Python-specific packaging
*.egg
*.egg-info/
dist/
build/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.tgz
*.bak
*.manifest
*.spec
*.lock

# PyInstaller
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Pyre type checker
.pyre/

# macOS
.DS_Store

# VS Code
.vscode/

# JetBrains IDEs
.idea/

# Docker related files
Dockerfile
docker-compose.override.yml

# Pytest
.pytest_cache/
htmlcov/
.tox/

# Jest
/coverage/

# Parcel
.cache/
.parcel-cache/

# Lock files
*.lock
172 changes: 172 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Cypress BDD with AI

This project implements BDD (Behavior-Driven Development) testing using Cypress. The setup leverages Docker to create consistent testing environments and integrates AI-driven Python scripts for utility automation, including step definition generation and DOM verification.

## Project Goals

- **BDD Testing**: Use Cypress for end-to-end testing with a behavior-driven approach.
- **Dockerized Setup**: Ensure reproducibility and isolated environments using Docker Compose.
- **AI Integration**: Automate step generation and DOM element verification with Python scripts interacting with AI:
- Automatically generate step definitions from feature files.
- Ensure that utility functions (selectors, DOM interactions) remain synchronized with the latest REDCap UI changes.

## Services Overview

The project is orchestrated with Docker Compose and has two main services:

- **ai\_scripts**: Handles AI-driven file generation and DOM consistency checks, ensuring the `redcap_dom.js` utility functions remain aligned with REDCap's structure.
- **cypress**: Executes headless BDD tests and is integrated into the CI/CD pipeline.

Additionally, Cypress can be run locally in an interactive UI mode for test development and debugging.

## Interaction with REDCap

This project is designed to interact with a Dockerized REDCap instance. Ensure the REDCap instance is running and accessible at the `CYPRESS_BASE_URL` before executing Cypress tests. The URL should point to the REDCap instance's address and port.

### Prerequisites

Ensure the following are installed:

- [Docker](https://www.docker.com/)
- [Docker Compose](https://docs.docker.com/compose/)
- [Node.js and npm](https://nodejs.org/) (for local Cypress UI)
- [Python 3](https://www.python.org/) (for running AI scripts)

### Environment Variables

Create a `.env` file in the root directory of your project with the following variables:

```env
CYPRESS_BASE_URL=http://localhost:80
CYPRESS_USERNAME=your-username
CYPRESS_PASSWORD=your-password
GPT_ENDPOINT=https://your-institution-gpt-endpoint
SUBSCRIPTION_KEY=your-subscription-key
```

### Key Features

- **Cypress UI Mode**: For local testing and debugging.
- **Headless Mode**: For CI/CD pipelines, executed via Docker Compose.
- **AI Scripts**: Generate Cypress step definitions and verify utility functions, keeping your test and DOM interactions up to date.


## Gherkin Syntax and Usage

Gherkin is a language used to write feature files for BDD. It is designed to be easy to understand by non-developers, providing a clear syntax for specifying test scenarios.

### Basic Syntax

Gherkin uses several keywords to define features, scenarios, and steps:

- **Feature**: Describes the feature being tested.
- **Scenario**: Represents a specific test case or example.
- **Given**, **When**, **Then**, **And**, **But**: Define steps within a scenario.

### Example

Here’s an example of a Gherkin feature file to test user login functionality with parameterized username and password:

**login.feature**:
```gherkin
Feature: User Login
Scenario: Successful login with valid credentials
Given the user navigates to the login page
When the user enters username "user1" and password "password123"
And the user clicks the login button
Then the user should be redirected to the dashboard
And the user should see the welcome message "Welcome, user1!"
```

### Storing Gherkin Files

In this system, Gherkin feature files are stored in the \`e2e/\` directory. You can create multiple \`.feature\` files for different scenarios and features.

### Transforming Gherkin to Steps

AI scripts are used to transform Gherkin feature files into step definitions needed for Cypress to execute the tests. The \`ai_worker.py\` script is responsible for this transformation.

Here’s an example of what the above Gherkin scenario might transform into:

**login_steps.js**:
```js
const { Given, When, Then } = require('cypress-cucumber-preprocessor/steps');

Given('the user navigates to the login page', () => {
cy.visit('/login');
});

When('the user enters username {string} and password {string}', (username, password) => {
cy.get('#username').type(username);
cy.get('#password').type(password);
});

When('the user clicks the login button', () => {
cy.get('button[type=submit]').click();
});

Then('the user should be redirected to the dashboard', () => {
cy.url().should('include', '/dashboard');
});

Then('the user should see the welcome message {string}', (welcomeMessage) => {
cy.contains(welcomeMessage).should('be.visible');
});
```

## Docker Setup

Build the Docker Containers:

```
docker compose build
```


## Usage

### Running Tests

You have two options for running Cypress tests.

#### 1. Running Tests in Headless Mode (One-Time Execution)

This option runs the tests once in headless mode and automatically shuts down the container when the tests finish. You don't need to start the services separately for this.

To run the tests in headless mode:
```
docker compose up cypress
```


#### 2. Running Tests with Cypress UI (Interactive Mode)

This option opens the Cypress UI, where you can interactively view and debug tests. For this, you need to run the tests locally (outside of Docker) using `npx` after installing the necessary dependencies:

First, ensure you have installed Cypress locally:
```
cd /cypress/
npm install
```
Then, run Cypress in interactive mode:
```
npx cypress open
```



### AI Scripts

The 'ai' service is designed for one-time executions to assist in generating the necessary files locally.
The generated files should then be checked into version control for use in testing.

**update_utilities.sh**: Updates utility functions based on the latest page structure.
```
docker compose run --rm ai sh /app/ai_scripts/update_utilities.sh
```

**regenerate_steps.sh**: Regenerates step definitions from Gherkin feature files.
```
docker compose run --rm ai sh /app/ai_scripts/regenerate_steps.sh
```
92 changes: 92 additions & 0 deletions ai_scripts/ai_worker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import argparse
import os
import openai
from pyppeteer import launch
import re
import asyncio
from dotenv import load_dotenv

# Load environment variables from .env file
load_dotenv()

GPT_ENDPOINT = os.getenv("GPT_ENDPOINT")
SUBSCRIPTION_KEY = os.getenv("SUBSCRIPTION_KEY")

# Initialize the OpenAI client with custom endpoint and subscription key
openai.api_base = GPT_ENDPOINT
openai.api_key = SUBSCRIPTION_KEY

# Function to scrape and update selectors
async def scrape_and_update_selectors():
browser = await launch()
page = await browser.newPage()
await page.goto('http://localhost:8888/login')

# Scrape a selector
username_selector = await page.evaluate("() => document.querySelector('input[name=\"username\"]').selectorText")
password_selector = await page.evaluate("() => document.querySelector('input[name=\"password\"]').selectorText")

await browser.close()

# Update Utility.js
auth_js_path = '/e2e/cypress/support/utility-functions/auth.js'

with open(auth_js_path, 'w') as file:
file.write(f"""
export const login = () => {{
cy.visit(Cypress.env('baseUrl') + '/login');
cy.get('{username_selector}').type(Cypress.env('USERNAME'));
cy.get('{password_selector}').type(Cypress.env('PASSWORD'));
cy.get('button[type="submit"]').click();
}};
""")

# Function to rename functions to be descriptive
def update_function_names():
auth_js_path = '/e2e/cypress/support/utility-functions/auth.js'

with open(auth_js_path, 'r') as file:
content = file.read()

content = re.sub(r'const login =', 'const userLoginWithValidCredentials =', content)

with open(auth_js_path, 'w') as file:
file.write(content)

# Function to generate step definitions from Gherkin
def generate_step_definitions():
for root, dirs, files in os.walk('/e2e/cypress/integration'):
for file in files:
if file.endswith('.feature'):
filepath = os.path.join(root, file)
with open(filepath, 'r') as feature_file:
steps = [line.strip() for line in feature_file if line.startswith(('Given', 'When', 'Then', 'And'))]

step_definitions = ""
for step in steps:
# Use OpenAI to generate step definitions
response = openai.Completion.create(
engine="davinci-codex",
prompt=f"Generate a step definition for '{step}':",
max_tokens=150
)
step_definitions += response.choices[0].text.strip() + "\n"

with open(filepath.replace('.feature', '_steps.js'), 'w') as step_file:
step_file.write(step_definitions)

def main():
parser = argparse.ArgumentParser()
parser.add_argument('--task', required=True, help="Specify the task to run: regenerate-steps, update-utilities")
args = parser.parse_args()

if args.task == 'regenerate-steps':
generate_step_definitions()
elif args.task == 'update-utilities':
asyncio.get_event_loop().run_until_complete(scrape_and_update_selectors())
update_function_names()
else:
print(f"Unknown task: {args.task}")

if __name__ == '__main__':
main()
11 changes: 11 additions & 0 deletions ai_scripts/regenerate_steps.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/sh

echo "Starting regenerate_steps.sh in Docker container..."

# Navigate to the script directory
cd /app/ai_scripts

# Run your Python script
# python3 ai_worker.py --task regenerate_steps

echo "Finished regenerate_steps.sh"
Loading

0 comments on commit 3b63ebd

Please sign in to comment.