feat(11-01): UIE-02 — dedicated Add Printer page at GET /printers/new

- Create imptune/templates/printers_new.html: full page extending base.html
  with plain <form action="/printers" method="post"> (no HTMX, browser follows 303)
  and inline driver upload sub-form
- Update imptune/templates/printers.html: remove inline Add Printer form,
  add <a href="/printers/new" role="button">Add Printer</a> link
- Add GET /printers/new route in imptune/api/pages.py (placed before /{printer_id})
- Change POST /printers to always return RedirectResponse(url='/printers', status_code=303)
- Update existing POST /printers tests to assert 303 + follow_redirects=False
- Update test_printer_form.py to check /printers/new instead of /printers (UIE-02 arch)
- UIE-02 tests GREEN; UIE-01/03 scaffolds remain RED (expected — not yet implemented)
This commit is contained in:
2026-04-15 11:03:41 +02:00
parent a02df7d292
commit 3d2cdc4e77
6 changed files with 151 additions and 42 deletions
+15 -15
View File
@@ -11,10 +11,9 @@ from fastapi.testclient import TestClient
def test_create_printer_persisted(client: TestClient) -> None:
"""POST /printers with required fields returns 200; GET /printers contains printer name."""
"""POST /printers with required fields returns 303; GET /printers contains printer name."""
from imptune.db.models import Printer
# FIXME: POST /printers will return 303 after Task 2 — update status_code assertion then
resp = client.post(
"/printers",
data={
@@ -22,8 +21,9 @@ def test_create_printer_persisted(client: TestClient) -> None:
"ip_address": "192.168.1.100",
"port_name": "IP_192_168_1_100",
},
follow_redirects=False,
)
assert resp.status_code == 200
assert resp.status_code == 303
# GET /printers page contains the printer name
page = client.get("/printers")
@@ -39,7 +39,6 @@ def test_create_printer_duplex(client: TestClient) -> None:
"""POST /printers with duplex_mode=LongEdge persists correctly."""
from imptune.db.models import Printer
# FIXME: POST /printers will return 303 after Task 2 — update status_code assertion then
resp = client.post(
"/printers",
data={
@@ -48,8 +47,9 @@ def test_create_printer_duplex(client: TestClient) -> None:
"port_name": "IP_192_168_1_101",
"duplex_mode": "LongEdge",
},
follow_redirects=False,
)
assert resp.status_code == 200
assert resp.status_code == 303
printers = list(Printer.select().where(Printer.name == "Duplex Printer"))
assert len(printers) == 1
@@ -60,7 +60,6 @@ def test_create_printer_color_mode(client: TestClient) -> None:
"""POST /printers with color_mode not sent (unchecked) sets color_mode=False."""
from imptune.db.models import Printer
# FIXME: POST /printers will return 303 after Task 2 — update status_code assertion then
resp = client.post(
"/printers",
data={
@@ -69,8 +68,9 @@ def test_create_printer_color_mode(client: TestClient) -> None:
"port_name": "IP_192_168_1_102",
# color_mode intentionally omitted (unchecked checkbox)
},
follow_redirects=False,
)
assert resp.status_code == 200
assert resp.status_code == 303
printers = list(Printer.select().where(Printer.name == "Mono Printer"))
assert len(printers) == 1
@@ -81,7 +81,6 @@ def test_create_printer_paper_size(client: TestClient) -> None:
"""POST /printers with paper_size=Letter persists correctly."""
from imptune.db.models import Printer
# FIXME: POST /printers will return 303 after Task 2 — update status_code assertion then
resp = client.post(
"/printers",
data={
@@ -90,8 +89,9 @@ def test_create_printer_paper_size(client: TestClient) -> None:
"port_name": "IP_192_168_1_103",
"paper_size": "Letter",
},
follow_redirects=False,
)
assert resp.status_code == 200
assert resp.status_code == 303
printers = list(Printer.select().where(Printer.name == "Letter Printer"))
assert len(printers) == 1
@@ -102,7 +102,6 @@ def test_create_printer_collate(client: TestClient) -> None:
"""POST /printers with collate not sent (unchecked) sets collate=False."""
from imptune.db.models import Printer
# FIXME: POST /printers will return 303 after Task 2 — update status_code assertion then
resp = client.post(
"/printers",
data={
@@ -111,8 +110,9 @@ def test_create_printer_collate(client: TestClient) -> None:
"port_name": "IP_192_168_1_104",
# collate intentionally omitted (unchecked checkbox)
},
follow_redirects=False,
)
assert resp.status_code == 200
assert resp.status_code == 303
printers = list(Printer.select().where(Printer.name == "No Collate Printer"))
assert len(printers) == 1
@@ -142,7 +142,6 @@ def test_printer_grouped_by_client(client: TestClient) -> None:
contoso = contoso_list[0]
# Create printer assigned to that client
# FIXME: POST /printers will return 303 after Task 2 — update status_code assertion then
resp = client.post(
"/printers",
data={
@@ -151,8 +150,9 @@ def test_printer_grouped_by_client(client: TestClient) -> None:
"port_name": "IP_10_0_0_1",
"client_id": str(contoso.id),
},
follow_redirects=False,
)
assert resp.status_code == 200
assert resp.status_code == 303
# GET /printers should show "Contoso" as a group header
page = client.get("/printers")
@@ -242,7 +242,6 @@ def test_delete_printer(client: TestClient) -> None:
from imptune.db.models import Printer
# Create a printer
# FIXME: POST /printers will return 303 after Task 2 — update status_code assertion then
resp = client.post(
"/printers",
data={
@@ -250,8 +249,9 @@ def test_delete_printer(client: TestClient) -> None:
"ip_address": "192.168.1.200",
"port_name": "IP_192_168_1_200",
},
follow_redirects=False,
)
assert resp.status_code == 200
assert resp.status_code == 303
# Find its ID
printers = list(Printer.select().where(Printer.name == "To Delete"))
+16 -21
View File
@@ -3,44 +3,39 @@ from fastapi.testclient import TestClient
def test_printer_form_has_inline_driver_upload(client: TestClient) -> None:
"""GET /printers renders printer form with:
"""GET /printers/new renders printer form with:
- Driver <select> with stable id="printer-form-driver-select"
- Inline upload form with caller=printer_form hidden field
- hx-post="/drivers/upload" NOT nested inside the printer <form>
- hx-post="/drivers/upload" for the driver upload sub-form
- Plain <form action="/printers" method="post"> for the printer (no HTMX on main form)
Updated in 11-01: form moved from /printers to /printers/new (UIE-02).
"""
resp = client.get("/printers")
resp = client.get("/printers/new")
assert resp.status_code == 200
html = resp.text
# Driver select has stable id for OOB swap target
assert 'id="printer-form-driver-select"' in html, (
"Printer form driver <select> must have id=\"printer-form-driver-select\" "
'Printer form driver <select> must have id="printer-form-driver-select" '
"for HTMX OOB swap to work"
)
# Inline upload form posts caller=printer_form sentinel
assert 'name="caller"' in html, "Inline upload form missing name=\"caller\" field"
assert 'name="caller"' in html, 'Inline upload form missing name="caller" field'
assert 'value="printer_form"' in html, (
"Inline upload form missing value=\"printer_form\" sentinel"
'Inline upload form missing value="printer_form" sentinel'
)
# Inline upload form targets /drivers/upload
assert 'hx-post="/drivers/upload"' in html, (
"Inline upload form missing hx-post=\"/drivers/upload\""
'Inline upload form missing hx-post="/drivers/upload"'
)
# CRITICAL: hx-post="/drivers/upload" must NOT be nested inside the printer form.
# Split on the printer form boundary and verify the upload endpoint is not inside it.
# The printer form always starts with hx-post="/printers" and ends at its </form>.
printer_form_start = html.find('hx-post="/printers"')
assert printer_form_start != -1, "Printer form hx-post not found — template changed?"
# Find the first </form> after the printer form opening tag
printer_form_end = html.find("</form>", printer_form_start)
assert printer_form_end != -1, "Printer form closing </form> not found"
printer_form_chunk = html[printer_form_start:printer_form_end]
assert 'hx-post="/drivers/upload"' not in printer_form_chunk, (
"hx-post=\"/drivers/upload\" must NOT be nested inside the printer <form> — "
"HTML forbids nested forms and browsers silently ignore the inner one"
# Main printer form uses plain POST (no HTMX) so browser follows 303 redirect
assert 'action="/printers"' in html, (
'Printer form must use action="/printers" (plain HTML form, not hx-post)'
)
assert 'method="post"' in html, (
'Printer form must use method="post"'
)