Skip to content

Commit

Permalink
Adding form submission tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DiogenesAnalytics committed Mar 7, 2024
1 parent 6a3360f commit 49b6cb5
Show file tree
Hide file tree
Showing 4 changed files with 323 additions and 3 deletions.
32 changes: 29 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

import pytest
from flask import Flask
from flask import render_template
from flask import request
from flask import send_from_directory
from selenium.webdriver.common.keys import Keys


def pytest_configure(config):
Expand Down Expand Up @@ -89,11 +91,13 @@ def submit_form():
# access form data submitted by the client
form_data = request.form

# print the form data
print("Form Data:", form_data)
# log data
print(request.form)

return "Form submitted successfully!\n"
# render the template with the form data
return render_template("form_response_template.html", form_data=form_data)

# return configured and route decorated Flask app
return app


Expand Down Expand Up @@ -125,6 +129,28 @@ def update_form_backend_config(
json.dump(config, json_file, indent=2)


@pytest.fixture(scope="session")
def form_inputs() -> Dict[str, Any]:
"""Defines the values to be submitted for each input type during form tests."""
return {
"email": "[email protected]",
"date": {"date": "01012000"},
"datetime-local": {
"date": "01012000",
"tab": Keys.TAB,
"time": "1200",
"period": "AM",
},
"number": "42",
"selectbox": None,
"tel": "18005554444",
"text": "Sample text for input of type=text.",
"textarea": "Sample text for Textarea.",
"time": {"time": "1200", "period": "AM"},
"url": "http://example.com",
}


@pytest.fixture(scope="session")
def sb_test_url() -> str:
"""Simply defines the test URL for seleniumbase fixture testing."""
Expand Down
37 changes: 37 additions & 0 deletions tests/templates/form_response_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Contact Form Response</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #222;
color: #ddd; /* Light text color */
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #333; /* Darker background color */
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
label {
font-weight: bold;
color: #fff; /* White label color */
}
</style>
</head>
<body>
<div class="container">
<h2>Contact Form Response</h2>
{% for key, value in form_data.items() %}
<label>{{ key }}:</label>
<p>{{ value }}</p>
{% endfor %}
</div>
</body>
</html>
45 changes: 45 additions & 0 deletions tests/test_fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import json
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Tuple

import pytest
Expand Down Expand Up @@ -37,6 +39,23 @@ def test_websrc_in_temp_dir(
assert check_files_subset(session_websrc_tmp_dir, website_files)


@pytest.mark.fixture
def test_config_keys_in_form_inputs(
default_site_config: Dict[str, Any], form_inputs: Dict[str, Any]
) -> None:
"""Check that keys from config.json are present in form input testing fixture."""
# get types from questions section of config.json
question_types = [q["type"] for q in default_site_config["questions"]]

# check config question types missing form inputs (if any)
missing_keys = set(question_types) - set(form_inputs)

# no missing keys
assert (
not missing_keys
), f"Keys found in config.json are absent from test inputs : {missing_keys}"


@pytest.mark.fixture
def test_hello_world_sb(sb: BaseCase, sb_test_url: str) -> None:
"""Just test if SeleniumBase can work on hello world example from docs."""
Expand Down Expand Up @@ -93,6 +112,32 @@ def test_serve_scripts_route(session_web_app: Flask) -> None:
assert response.status_code == 200


@pytest.mark.flask
@pytest.mark.fixture
def test_submit_form_route(session_web_app: Flask, submit_route: str) -> None:
"""Test the route for submitting a form."""
# get client
client = session_web_app.test_client()

# simulate a form submission
form_data = {
"name": "John Doe",
"email": "[email protected]",
"message": "This is a test message.",
}
response = client.post(submit_route, data=form_data)

# assert that the response status code is 200 (OK)
assert response.status_code == 200

# get content
content = response.data.decode("utf-8")

# Optionally, you can check the response content
assert "Contact Form Response" in content
assert all(form_data[key] in content for key in form_data)


@pytest.mark.flask
@pytest.mark.fixture
def test_port_in_app_config(session_web_app: Flask) -> None:
Expand Down
212 changes: 212 additions & 0 deletions tests/test_website.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,166 @@
"""Test all features of website."""

import json
from datetime import datetime
from typing import Any
from typing import Dict
from typing import Generator
from typing import Optional
from typing import Tuple

import pytest
from bs4 import BeautifulSoup
from flask import Flask
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from seleniumbase import BaseCase

from tests.schema import check_config_schema


def convert_to_isoformat(
date: Optional[str] = None,
time: Optional[str] = None,
period: Optional[str] = None,
**kwargs,
) -> str:
"""Converts a datetime-local test input into ISO 8601 format."""
# Initialize variables for date and time objects
date_obj = None
time_obj = None

# Check for valid input
if date is None and time is None:
raise ValueError("Either date or time must be provided")

# Convert date string to datetime object
if date is not None:
date_obj = datetime.strptime(date, "%d%m%Y")

# Convert time string to datetime object
if time is not None:
if period is None:
raise ValueError("Period (AM/PM) must be provided for time conversion")
time_str = f"{time} {period}"
time_obj = datetime.strptime(time_str, "%I%M %p")

# Determine final string based on date and time presence
final_str = ""
if date_obj and time_obj:
final_str = datetime.combine(date_obj.date(), time_obj.time()).strftime(
"%Y-%m-%dT%H:%M"
)
elif date_obj:
final_str = date_obj.strftime("%Y-%m-%d")
elif time_obj:
final_str = time_obj.strftime("%H:%M")

return final_str


def select_options(question: Dict[str, Any]) -> str:
"""Chose selection from options."""
# count number of options NOT disabled
for option in question["options"]:
# check if disabled present
if option.get("disabled", False):
# skip disabled options
continue

else:
# end
break

# get the first valid option
return option["value"]


def fill_out_form(
form_element: WebElement, config: Dict[str, Any], form_inputs: Dict[str, Any]
) -> Generator[Tuple[str, str], None, None]:
"""Programmatically fill out form and yield name/value pairs."""
# loop over questions
for question in config["questions"]:
# now get element with name
input_element = form_element.find_element(By.NAME, question["name"])

# get tag element type
tag_name = input_element.tag_name.lower()

# control flow for different types
if tag_name == "input" or tag_name == "textarea":
# get the type
input_type = input_element.get_attribute("type")

# check input_type
assert input_type is not None

# get test value for input type
test_value = form_inputs[input_type]

# check if date/time dict
if isinstance(test_value, dict):
# now loop over multiple input steps
for sub_input in test_value.values():
# send it
input_element.send_keys(sub_input)

# now update test value
test_value = convert_to_isoformat(**test_value)

else:
# just normal
input_element.send_keys(test_value)

# generate
yield question["name"], test_value

elif tag_name == "select":
# get sample selection from options
sample_option = select_options(question)

# find all option elements within the select element
option_elements = input_element.find_elements(By.TAG_NAME, "option")

# loop through the option elements and select the desired one
for option_element in option_elements:
# basically get first option that is not empty (i.e. a default)
if option_element.get_attribute("value") == sample_option:
# click it ...
option_element.click()

# generate
yield question["name"], sample_option


def extract_received_form_input(
response_html: str,
) -> Generator[Tuple[str, str], None, None]:
"""Extract input received from form submission."""
# parse the HTML content with BeautifulSoup
soup = BeautifulSoup(response_html, "html.parser")

# find the container element
container = soup.find("div", class_="container")

# find all label elements within the container
labels = container.find_all("label")

# iterate over the labels to retrieve the key-value pairs
for label in labels:
# get label as key
key = label.text.strip(":")

# now get immediate value element
value_element = label.find_next_sibling("p")

# clean it
received_value = value_element.text.strip()

# yield the key/value pair
yield key, received_value


@pytest.mark.website
def test_config_schema(default_site_config: Dict[str, Any]) -> None:
"""Check that the given config.json schema is correct."""
Expand Down Expand Up @@ -86,3 +238,63 @@ def test_form_backend_updated(

# now check that it is the right url
assert form_target == live_session_web_app_url + submit_route


@pytest.mark.website
def test_form_submission(
sb: BaseCase,
live_session_web_app_url: str,
form_inputs: Dict[str, Any],
session_web_app: Flask,
) -> None:
"""Check that the given form upon completion can be succesfully submitted."""
# get config file
client = session_web_app.test_client()
response = client.get("/config.json")

# convert the response content to JSON
config = json.loads(response.data)

# open the webpage
sb.open(live_session_web_app_url)

# find the form element
form_element = sb.get_element("form")

# fill out form
submitted_input = {
k: v for k, v in fill_out_form(form_element, config, form_inputs)
}

# get send button ...
send_button = form_element.find_element(By.ID, "send_button")

# ... now click it
send_button.click()

# check that the form was submitted
sb.assert_text("Contact Form Response")

# get the HTML content of the response
response_html = sb.get_page_source()

# get received input from Flask response html
received_input = {k: v for k, v in extract_received_form_input(response_html)}

# check keys are same
missing_keys = set(submitted_input) - set(received_input)
assert not missing_keys, f"Keys are not the same: {missing_keys}"

# now check values
for key in submitted_input.keys():
# get values
value1 = submitted_input[key]
value2 = received_input[key]

# check
assert (
value1 == value2
), f"Submitted input: {value1} differs from received: {value2}"

# save screenshot for confirmation
sb.save_screenshot_to_logs()

0 comments on commit 49b6cb5

Please sign in to comment.