Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test Todo app #45

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions test_todo_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Testing FastHTML Todo App

This project implements a few simple unit tests for [FastHTML Todo list application](https://github.com/AnswerDotAI/fasthtml-example/tree/main/01_todo_app) using Pytest and [FastHTML Client](https://docs.fastht.ml/api/core.html#client).


When running in test mode, the app now relies on a in-memory instance of Sqlite to make sure data is reset on every run. Test client and database table instance are defined as Pytest fixtures in `tests/conftest.py`.

## Running Locally

To run the app locally:

1. Clone the repository
2. Navigate to the project directory
3. Run `poetry install` to install dependencies
4. Run the following command: `poetry run python main.py`
5. To run tests, use `poetry run pytest tests`

Binary file added test_todo_app/favicon.ico
Binary file not shown.
8 changes: 8 additions & 0 deletions test_todo_app/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from fasthtml.common import *
from todos import create_app

# actual app creation process moved to todos.py
# this way we can support different modes of the app (i.e. dev, test)
# you can further extend create_app to support a more fine-grained configuration
app, _ = create_app()
serve()
2 changes: 2 additions & 0 deletions test_todo_app/picovars.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:root { --pico-font-size: 100%; }

812 changes: 812 additions & 0 deletions test_todo_app/poetry.lock

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions test_todo_app/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[tool.poetry]
name = "test-todo-app"
version = "0.1.0"
description = "Test a FastHTML app"
authors = ["Philip Nuzhnyi <[email protected]>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
python-fasthtml = "^0.6.0"
pytest = "^8.3.3"


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
Empty file added test_todo_app/tests/__init__.py
Empty file.
10 changes: 10 additions & 0 deletions test_todo_app/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest
from typing import Tuple
from fasthtml.common import Client, Table
from todos import create_app

@pytest.fixture
def test_client_and_database() -> Tuple[Client, Table]:
"""test_client_and_database will be available to all test functions in the project"""
app, todos = create_app(mode="test")
return Client(app=app), todos
32 changes: 32 additions & 0 deletions test_todo_app/tests/test_routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
def test_landing_page(test_client_and_database):
client, todos = test_client_and_database
res = client.get("/")
assert res.status_code == 200
assert "Todo list" in res.text
assert todos.count == 0

def test_adding_new_todo(test_client_and_database):
client, todos = test_client_and_database
res = client.post("/", data={"title": "Test Todo"})
assert res.status_code == 200
assert todos.count == 1
assert todos.get(1).title == "Test Todo"

def test_updating_existing_todo(test_client_and_database):
client, todos = test_client_and_database
todos.insert({"title": "Test Todo"})

res = client.put("/", data={"id": 1, "title": "Updated Todo", "done": 1})
assert res.status_code == 200

assert todos.count == 1
assert todos.get(1).title == "Updated Todo"
assert todos.get(1).done == True

def test_deleting_existing_todo(test_client_and_database):
client, todos = test_client_and_database
todos.insert({"title": "Test Todo"})

res = client.delete("/todos/1")
assert res.status_code == 200
assert todos.count == 0
Binary file added test_todo_app/todo_screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions test_todo_app/todos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from fasthtml.common import *
from typing import Literal, Tuple

def create_app(mode: Literal["dev", "test"] = "dev") -> Tuple[FastHTML, Table]:
app,rt,todos,Todo = fast_app(
'data/todos.db' if mode == 'dev' else ':memory:',
hdrs=[Style(':root { --pico-font-size: 100%; }')],
id=int, title=str, done=bool, pk='id')

id_curr = 'current-todo'
def tid(id): return f'todo-{id}'

@patch
def __ft__(self:Todo):
show = AX(self.title, f'/todos/{self.id}', id_curr)
edit = AX('edit', f'/edit/{self.id}' , id_curr)
dt = ' ✅' if self.done else ''
return Li(show, dt, ' | ', edit, id=tid(self.id))

def mk_input(**kw): return Input(id="new-title", name="title", placeholder="New Todo", required=True, **kw)

@rt("/")
def get():
add = Form(Group(mk_input(), Button("Add")),
hx_post="/", target_id='todo-list', hx_swap="beforeend")
card = Card(Ul(*todos(), id='todo-list'),
header=add, footer=Div(id=id_curr)),
title = 'Todo list'
return Title(title), Main(H1(title), card, cls='container')

@rt("/todos/{id}")
def delete(id:int):
todos.delete(id)
return clear(id_curr)

@rt("/")
def post(todo:Todo): return todos.insert(todo), mk_input(hx_swap_oob='true')

@rt("/edit/{id}")
def get(id:int):
res = Form(Group(Input(id="title"), Button("Save")),
Hidden(id="id"), CheckboxX(id="done", label='Done'),
hx_put="/", target_id=tid(id), id="edit")
return fill_form(res, todos.get(id))

@rt("/")
def put(todo: Todo): return todos.upsert(todo), clear(id_curr)

@rt("/todos/{id}")
def get(id:int):
todo = todos.get(id)
btn = Button('delete', hx_delete=f'/todos/{todo.id}',
target_id=tid(todo.id), hx_swap="outerHTML")
return Div(Div(todo.title), btn)

return app, todos