Lägg till mer tester/workflow
This commit is contained in:
parent
8491c05ce6
commit
3d1e21ccd9
@ -5,3 +5,6 @@ venv/
|
||||
.git/
|
||||
.DS_Store
|
||||
todo.db
|
||||
node_modules/
|
||||
test-results/
|
||||
*.db
|
||||
45
.github/workflows/tests.yml
vendored
45
.github/workflows/tests.yml
vendored
@ -5,22 +5,51 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
steps:
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Python setup
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install Python dependencies
|
||||
run: |
|
||||
python -m pip install -U pip
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
pip install pytest
|
||||
|
||||
- name: Run tests
|
||||
# Node setup (Newman + Playwright)
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20"
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Install Playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
# Start Flask server
|
||||
- name: Start Flask server
|
||||
run: |
|
||||
pytest -q
|
||||
flask --app app:create_app run --port 5001 &
|
||||
sleep 5
|
||||
|
||||
# Run tests
|
||||
|
||||
- name: Run unit & integration tests (pytest)
|
||||
run: pytest -q
|
||||
|
||||
- name: Run API tests (Newman)
|
||||
run: npm run api-test
|
||||
|
||||
- name: Run E2E tests (Playwright)
|
||||
run: npm run e2e
|
||||
|
||||
26
.gitignore
vendored
26
.gitignore
vendored
@ -1,4 +1,6 @@
|
||||
# Python bytecode
|
||||
# =========================
|
||||
# Python
|
||||
# =========================
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
@ -7,11 +9,29 @@ __pycache__/
|
||||
venv/
|
||||
.env/
|
||||
|
||||
# Pytest
|
||||
# Pytest / coverage
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# =========================
|
||||
# SQLite
|
||||
# =========================
|
||||
*.db
|
||||
|
||||
# OS
|
||||
# =========================
|
||||
# Node / Frontend tooling
|
||||
# =========================
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
|
||||
# =========================
|
||||
# OS / Editor
|
||||
# =========================
|
||||
.DS_Store
|
||||
.vscode/
|
||||
|
||||
10
Dockerfile
10
Dockerfile
@ -5,10 +5,12 @@ ENV PYTHONUNBUFFERED=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt /app/requirements.txt
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY requirements.txt .
|
||||
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
COPY . /app
|
||||
EXPOSE 5001
|
||||
|
||||
CMD ["python", "app.py"]
|
||||
# Gunicorn: 2 workers räcker fint för en liten kursapp
|
||||
CMD ["gunicorn", "-w", "2", "-b", "0.0.0.0:5001", "wsgi:app"]
|
||||
|
||||
75
README.md
75
README.md
@ -1,7 +1,8 @@
|
||||
# TODO – Flask + SQLite
|
||||
|
||||
Ett enkelt TODO-system byggt med **Python**, **Flask** och **SQLite**.
|
||||
Projektet är uppsatt med **app factory-pattern**, **pytest-tester** och är redo att köras i **GitHub Actions CI**.
|
||||
Projektet använder **Flask app factory-pattern**, **pytest** för unit- och integrationstester samt **Postman/Newman** och **Playwright** för API- och E2E-tester.
|
||||
Projektet är förberett för **CI (GitHub Actions)**.
|
||||
|
||||
---
|
||||
|
||||
@ -11,7 +12,7 @@ Projektet är uppsatt med **app factory-pattern**, **pytest-tester** och är red
|
||||
- Uppdatera status (`not-started`, `in-progress`, `done`)
|
||||
- Ta bort TODO-poster
|
||||
- SQLite som databas
|
||||
- Tester med pytest
|
||||
- Tester på flera nivåer (unit, API, E2E)
|
||||
- CI-redo (GitHub Actions)
|
||||
|
||||
---
|
||||
@ -20,25 +21,41 @@ Projektet är uppsatt med **app factory-pattern**, **pytest-tester** och är red
|
||||
|
||||
```text
|
||||
.
|
||||
├── app.py # Flask app (app factory)
|
||||
├── db.py # Databasfunktioner (SQLite)
|
||||
├── todo.db # SQLite-databas (lokalt)
|
||||
├── requirements.txt # Runtime dependencies
|
||||
├── requirements-dev.txt # Dev/test dependencies
|
||||
├── templates/
|
||||
│ ├── base.html
|
||||
│ └── index.html
|
||||
├── static/
|
||||
│ └── style.css
|
||||
├── .github/
|
||||
│ └── workflows/
|
||||
│ └── tests.yml
|
||||
├── app/
|
||||
│ ├── __init__.py # Flask app (app factory)
|
||||
│ ├── db.py # Databasfunktioner (SQLite)
|
||||
│ ├── validation.py # Validering
|
||||
│ ├── static/
|
||||
│ │ └── style.css
|
||||
│ ├── templates/
|
||||
│ ├── base.html
|
||||
│ └── index.html
|
||||
├── tests/
|
||||
│ └── test_app.py
|
||||
├── Dockerfile
|
||||
├── pytest.ini
|
||||
├── requirements.txt
|
||||
├── requirements-dev.txt
|
||||
├── .gitignore
|
||||
│ ├── api/
|
||||
│ │ └── test_app.py
|
||||
│ ├── e2e/
|
||||
│ │ └── todo.spec.ts
|
||||
│ ├── postman/
|
||||
│ │ ├── todo.collection.json
|
||||
│ │ └── todo.env.json
|
||||
│ └── unit/
|
||||
│ └── test_validation.py
|
||||
├── .dockerignore
|
||||
├── .gitignore
|
||||
├── Dockerfile
|
||||
├── LICENSE
|
||||
├── package-lock.json
|
||||
├── package.json
|
||||
├── pytest.ini
|
||||
└── README.md
|
||||
├── requirements-dev.txt # Dev/test dependencies
|
||||
├── requirements.txt # Runtime dependencies
|
||||
├── todo.db # SQLite-databas (lokalt)
|
||||
└── wsgi.py
|
||||
|
||||
```
|
||||
|
||||
## 🚀 Kom igång lokalt
|
||||
@ -63,7 +80,7 @@ pip install -r requirements-dev.txt
|
||||
|
||||
### 4. Starta applikationen
|
||||
```bash
|
||||
python app.py
|
||||
python3 app.py
|
||||
```
|
||||
|
||||
### Öppna i webbläsaren:
|
||||
@ -75,15 +92,25 @@ http://127.0.0.1:5001
|
||||
|
||||
## 🧪 Tester
|
||||
|
||||
Kör alla tester lokalt med pytest:
|
||||
|
||||
Unit- och integrationstester:
|
||||
```bash
|
||||
pytest
|
||||
```
|
||||
|
||||
Med kodtäckning(coverage)
|
||||
API-tester (Postman / Newman):
|
||||
```bash
|
||||
pytest --cov=app --cov-report=term-missing --cov-fail-under=80
|
||||
npm install
|
||||
npm run api-test
|
||||
```
|
||||
|
||||
End-to-End tester (Playwright):
|
||||
```bash
|
||||
npm run e2e
|
||||
```
|
||||
|
||||
Köra alla tester:
|
||||
```bash
|
||||
npm run test:all
|
||||
```
|
||||
|
||||
## 🤖 CI – GitHub Actions
|
||||
@ -104,5 +131,5 @@ Exempel (snabbstart):
|
||||
|
||||
```bash
|
||||
docker build -t todo-app .
|
||||
docker run -p 5001:5001 todo-app
|
||||
docker run -d -p 5001:5001 todo-app
|
||||
```
|
||||
@ -1,8 +1,9 @@
|
||||
import os
|
||||
from datetime import datetime
|
||||
from flask import Flask, render_template, redirect, url_for, request, flash
|
||||
from app.validation import normalize_status, validate_todo_fields
|
||||
|
||||
from db import get_db, init_db
|
||||
from app.db import get_db, init_db
|
||||
|
||||
# =====================================================
|
||||
# Constants
|
||||
@ -24,7 +25,7 @@ def create_app(test_config: dict | None = None) -> Flask:
|
||||
# -------------------------------------------------
|
||||
# App Configuration
|
||||
# -------------------------------------------------
|
||||
default_db = os.path.join(os.path.dirname(__file__), "todo.db")
|
||||
default_db = os.path.join(os.path.dirname(os.path.dirname(__file__)), "todo.db")
|
||||
app.config.update(
|
||||
DB_PATH=default_db,
|
||||
TESTING=False,
|
||||
@ -81,15 +82,13 @@ def create_app(test_config: dict | None = None) -> Flask:
|
||||
"""
|
||||
title = (request.form.get("title") or "").strip()
|
||||
description = (request.form.get("description") or "").strip()
|
||||
status = request.form.get("status") or "not-started"
|
||||
status = normalize_status(request.form.get("status", ""))
|
||||
|
||||
if not title or not description:
|
||||
ok, errors = validate_todo_fields(title, description)
|
||||
if not ok:
|
||||
flash("Title och description krävs.", "danger")
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
if status not in STATUS_VALUES:
|
||||
status = "not-started"
|
||||
|
||||
now = datetime.now().isoformat(timespec="seconds")
|
||||
|
||||
with get_db(app) as conn:
|
||||
20
app/validation.py
Normal file
20
app/validation.py
Normal file
@ -0,0 +1,20 @@
|
||||
ALLOWED_STATUSES = {"not-started", "in-progress", "done"}
|
||||
|
||||
def normalize_status(status: str) -> str:
|
||||
"""
|
||||
Säkerställ att status alltid blir en av de tillåtna.
|
||||
Om okänd -> default "not-started".
|
||||
"""
|
||||
status = (status or "").strip()
|
||||
return status if status in ALLOWED_STATUSES else "not-started"
|
||||
|
||||
def validate_todo_fields(title: str, description: str) -> tuple[bool, list[str]]:
|
||||
"""
|
||||
Returnerar (ok, errors).
|
||||
"""
|
||||
errors = []
|
||||
if not (title or "").strip():
|
||||
errors.append("title_required")
|
||||
if not (description or "").strip():
|
||||
errors.append("description_required")
|
||||
return (len(errors) == 0, errors)
|
||||
2195
package-lock.json
generated
Normal file
2195
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
package.json
Normal file
32
package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "todo",
|
||||
"version": "1.0.0",
|
||||
"description": "Ett enkelt TODO-system byggt med **Python**, **Flask** och **SQLite**. Projektet är uppsatt med **app factory-pattern**, **pytest-tester** och är redo att köras i **GitHub Actions CI**.",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "flask --app app:create_app run --port 5001",
|
||||
"api-test": "newman run tests/postman/todo.collection.json -e tests/postman/todo.env.json",
|
||||
"e2e": "playwright test",
|
||||
"test:all": "start-server-and-test start http://127.0.0.1:5001/ \"npm run api-test && npm run e2e\""
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jwradhe/todo.git"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"bugs": {
|
||||
"url": "https://github.com/jwradhe/todo/issues"
|
||||
},
|
||||
"homepage": "https://github.com/jwradhe/todo#readme",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.57.0",
|
||||
"newman": "^6.1.0",
|
||||
"start-server-and-test": "^2.1.3"
|
||||
}
|
||||
}
|
||||
9
playwright.config.ts
Normal file
9
playwright.config.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { defineConfig } from "@playwright/test";
|
||||
|
||||
export default defineConfig({
|
||||
testDir: "./tests/e2e",
|
||||
use: {
|
||||
baseURL: process.env.BASE_URL || "http://127.0.0.1:5001",
|
||||
headless: true
|
||||
}
|
||||
});
|
||||
@ -1,2 +1,2 @@
|
||||
pytest>=8.0
|
||||
pytest-cov>=5.0
|
||||
pytest-cov>=5.0
|
||||
|
||||
@ -1 +1,2 @@
|
||||
Flask>=3.0
|
||||
Flask>=3.0
|
||||
gunicorn
|
||||
@ -25,6 +25,7 @@ def fetch_all(db_path: str, sql: str, params=()):
|
||||
# Tests
|
||||
# =====================================================
|
||||
|
||||
# Test that the dashboard loads successfully
|
||||
def test_dashboard_loads(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -33,7 +34,7 @@ def test_dashboard_loads(tmp_path):
|
||||
resp = client.get("/dashboard")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# Test that creating a todo inserts a row in the database
|
||||
def test_create_todo_inserts_row(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -49,7 +50,7 @@ def test_create_todo_inserts_row(tmp_path):
|
||||
row = fetch_one(str(db_path), "SELECT title, description, status FROM todo")
|
||||
assert row == ("Test", "Desc", "not-started")
|
||||
|
||||
|
||||
# Test that creating a todo requires title and description
|
||||
def test_create_requires_fields(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -65,7 +66,7 @@ def test_create_requires_fields(tmp_path):
|
||||
row = fetch_one(str(db_path), "SELECT COUNT(*) FROM todo")
|
||||
assert row[0] == 0
|
||||
|
||||
|
||||
# Test that updating a todo's status works
|
||||
def test_invalid_status_is_rejected_on_update(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -87,7 +88,7 @@ def test_invalid_status_is_rejected_on_update(tmp_path):
|
||||
status = fetch_one(str(db_path), "SELECT status FROM todo WHERE id=?", (todo_id,))[0]
|
||||
assert status == "not-started"
|
||||
|
||||
|
||||
# Test that updating a todo's status to 'done' works
|
||||
def test_update_status_to_done(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -107,7 +108,7 @@ def test_update_status_to_done(tmp_path):
|
||||
assert status == "done"
|
||||
assert edited is not None
|
||||
|
||||
|
||||
# Test that deleting a todo removes it from the database
|
||||
def test_delete_todo(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -125,6 +126,7 @@ def test_delete_todo(tmp_path):
|
||||
row = fetch_one(str(db_path), "SELECT COUNT(*) FROM todo")
|
||||
assert row[0] == 0
|
||||
|
||||
# Test that the dashboard lists created todos
|
||||
def test_dashboard_lists_created_todo(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -136,7 +138,7 @@ def test_dashboard_lists_created_todo(tmp_path):
|
||||
assert b"X" in resp.data
|
||||
assert b"Y" in resp.data
|
||||
|
||||
|
||||
# Test that creating a todo with invalid status defaults to 'not-started'
|
||||
def test_create_invalid_status_defaults_to_not_started(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -146,7 +148,7 @@ def test_create_invalid_status_defaults_to_not_started(tmp_path):
|
||||
row = fetch_one(str(db_path), "SELECT status FROM todo")
|
||||
assert row[0] == "not-started"
|
||||
|
||||
|
||||
# Test that deleting a nonexistent todo does not crash
|
||||
def test_delete_nonexistent_does_not_crash(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
@ -155,7 +157,7 @@ def test_delete_nonexistent_does_not_crash(tmp_path):
|
||||
resp = client.post("/todo/999/delete", follow_redirects=True)
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# Test that updating a nonexistent todo does not crash
|
||||
def test_update_nonexistent_does_not_crash(tmp_path):
|
||||
db_path = tmp_path / "test.db"
|
||||
app = create_app({"TESTING": True, "DB_PATH": str(db_path)})
|
||||
144
tests/e2e/todo.spec.ts
Normal file
144
tests/e2e/todo.spec.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test.describe("Todo lifecycle E2E", () => {
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
const title = `E2E Todo ${Date.now()}`;
|
||||
const description = "E2E Desc";
|
||||
|
||||
// Hitta create-formen
|
||||
const getCreateForm = (page: any) =>
|
||||
page.locator("form", { has: page.locator('input[name="title"]') });
|
||||
|
||||
// Hitta tabellraden för en titel
|
||||
const getRowByTitle = (page: any, t: string) =>
|
||||
page.locator("tr", { has: page.locator(".fw-semibold", { hasText: t }) });
|
||||
|
||||
test("user can create a todo and see it on dashboard", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const createForm = getCreateForm(page);
|
||||
|
||||
// Fyll i formuläret
|
||||
await createForm.locator('input[name="title"]').fill(title);
|
||||
await createForm.locator('textarea[name="description"]').fill(description);
|
||||
await createForm.locator('select[name="status"]').selectOption("not-started");
|
||||
|
||||
// Skicka
|
||||
await createForm.locator('button[type="submit"]').click();
|
||||
|
||||
// Syns i listan
|
||||
await expect(page.locator(".fw-semibold", { hasText: title })).toBeVisible();
|
||||
});
|
||||
|
||||
test("user can update status to done", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const row = getRowByTitle(page, title);
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
// Ändra status
|
||||
const statusSelect = row.locator('select[name="status"]');
|
||||
await Promise.all([
|
||||
page.waitForLoadState("networkidle"),
|
||||
statusSelect.selectOption("done"),
|
||||
]);
|
||||
|
||||
// Kontrollera att status är done
|
||||
const rowAfter = getRowByTitle(page, title);
|
||||
await expect(rowAfter.locator('select[name="status"]')).toHaveValue("done");
|
||||
});
|
||||
|
||||
test("user can change status multiple times", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const row = getRowByTitle(page, title);
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
const statusSelect = row.locator('select[name="status"]');
|
||||
|
||||
// not-started -> in-progress
|
||||
await Promise.all([
|
||||
page.waitForLoadState("networkidle"),
|
||||
statusSelect.selectOption("in-progress"),
|
||||
]);
|
||||
await expect(getRowByTitle(page, title).locator('select[name="status"]')).toHaveValue(
|
||||
"in-progress"
|
||||
);
|
||||
|
||||
// in-progress -> done
|
||||
await Promise.all([
|
||||
page.waitForLoadState("networkidle"),
|
||||
statusSelect.selectOption("done"),
|
||||
]);
|
||||
await expect(getRowByTitle(page, title).locator('select[name="status"]')).toHaveValue("done");
|
||||
});
|
||||
|
||||
test("cannot create todo without required fields", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const createForm = getCreateForm(page);
|
||||
|
||||
// Tomma fält (bara mellanslag)
|
||||
await createForm.locator('input[name="title"]').fill(" ");
|
||||
await createForm.locator('textarea[name="description"]').fill(" ");
|
||||
await createForm.locator('button[type="submit"]').click();
|
||||
|
||||
// Felmeddelande visas
|
||||
await expect(page.getByText("Title och description krävs.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("cannot create todo when description is missing", async ({ page }) => {
|
||||
const badTitle = `E2E Missing Desc ${Date.now()}`;
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const createForm = getCreateForm(page);
|
||||
|
||||
await createForm.locator('input[name="title"]').fill(badTitle);
|
||||
await createForm.locator('textarea[name="description"]').fill(" ");
|
||||
await createForm.locator('button[type="submit"]').click();
|
||||
|
||||
// Felmeddelande + ingen rad skapas
|
||||
await expect(page.getByText("Title och description krävs.")).toBeVisible();
|
||||
await expect(page.locator(".fw-semibold", { hasText: badTitle })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("invalid status is normalized to not-started on create", async ({ page }) => {
|
||||
const t = `E2E Weird Status ${Date.now()}`;
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const createForm = getCreateForm(page);
|
||||
|
||||
await createForm.locator('input[name="title"]').fill(t);
|
||||
await createForm.locator('textarea[name="description"]').fill("desc");
|
||||
|
||||
// Sätt ett ogiltigt status-värde
|
||||
await createForm.locator('select[name="status"]').evaluate((sel: HTMLSelectElement) => {
|
||||
sel.value = "hax-status";
|
||||
});
|
||||
|
||||
await createForm.locator('button[type="submit"]').click();
|
||||
|
||||
// Ska defaulta till not-started
|
||||
const row = getRowByTitle(page, t);
|
||||
await expect(row).toBeVisible();
|
||||
await expect(row.locator('select[name="status"]')).toHaveValue("not-started");
|
||||
|
||||
// Städa
|
||||
await row.locator('button[title="Delete"]').click();
|
||||
await expect(page.locator(".fw-semibold", { hasText: t })).toHaveCount(0);
|
||||
});
|
||||
|
||||
test("user can delete todo", async ({ page }) => {
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const row = getRowByTitle(page, title);
|
||||
await expect(row).toBeVisible();
|
||||
|
||||
// Ta bort
|
||||
await row.locator('button[title="Delete"]').click();
|
||||
|
||||
// Ska vara borta
|
||||
await expect(page.locator(".fw-semibold", { hasText: title })).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
58
tests/postman/todo.collection.json
Normal file
58
tests/postman/todo.collection.json
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Todo API",
|
||||
"_postman_id": "11111111-1111-1111-1111-111111111111",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "GET dashboard",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "{{baseUrl}}/dashboard"
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.test('status 200', function () {",
|
||||
" pm.response.to.have.status(200);",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "POST create todo",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{ "key": "Content-Type", "value": "application/x-www-form-urlencoded" }
|
||||
],
|
||||
"body": {
|
||||
"mode": "urlencoded",
|
||||
"urlencoded": [
|
||||
{ "key": "title", "value": "Newman todo", "type": "text" },
|
||||
{ "key": "description", "value": "Created by newman", "type": "text" },
|
||||
{ "key": "status", "value": "not-started", "type": "text" }
|
||||
]
|
||||
},
|
||||
"url": "{{baseUrl}}/todo/create"
|
||||
},
|
||||
"event": [
|
||||
{
|
||||
"listen": "test",
|
||||
"script": {
|
||||
"exec": [
|
||||
"pm.test('status is 200 or redirect', function () {",
|
||||
" pm.expect([200, 302]).to.include(pm.response.code);",
|
||||
"});"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
7
tests/postman/todo.env.json
Normal file
7
tests/postman/todo.env.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "22222222-2222-2222-2222-222222222222",
|
||||
"name": "local",
|
||||
"values": [
|
||||
{ "key": "baseUrl", "value": "http://127.0.0.1:5001", "enabled": true }
|
||||
]
|
||||
}
|
||||
21
tests/unit/test_validation.py
Normal file
21
tests/unit/test_validation.py
Normal file
@ -0,0 +1,21 @@
|
||||
from app.validation import normalize_status, validate_todo_fields
|
||||
|
||||
def test_normalize_status_accepts_allowed():
|
||||
assert normalize_status("done") == "done"
|
||||
assert normalize_status("in-progress") == "in-progress"
|
||||
|
||||
def test_normalize_status_defaults_on_invalid():
|
||||
assert normalize_status("weird") == "not-started"
|
||||
assert normalize_status("") == "not-started"
|
||||
assert normalize_status(None) == "not-started"
|
||||
|
||||
def test_validate_todo_fields_ok():
|
||||
ok, errors = validate_todo_fields("A", "B")
|
||||
assert ok is True
|
||||
assert errors == []
|
||||
|
||||
def test_validate_todo_fields_missing_both():
|
||||
ok, errors = validate_todo_fields("", "")
|
||||
assert ok is False
|
||||
assert "title_required" in errors
|
||||
assert "description_required" in errors
|
||||
Loading…
Reference in New Issue
Block a user