Flaxon 0.3.0 and Teloce-Py 0.3.0 are available on PyPI. Release instructions are maintained on this site; source links identify the framework snapshot. Check version and upgrade guidance.
14 | Full-stack verification
Goal
Exercise customer, staff and CMS workflows in a real browser.
What you are building
API tests cannot prove form listeners, history navigation or mobile layout. The browser script starts a disposable server, seeds a temporary staff account and tests desktop and 390-pixel workflows. It checks registration, login, projects, tasks, progress, help, navigation, refresh and logout. Run it in development and production mode to test MinifyJS output. Use labels, status messages and alerts; also rehearse with the keyboard.
Build this chapter
Stop the server. Work in project_manager. Follow each CREATE, whole-file replacement, or marked-block edit below in order.
Step 1: CREATE - make parent folders, then create this file - tests/test_backoffice.py
This file owns the feature named by its module or component. Read the route/form flow before continuing.
Create the parent directories, then save this complete file.
import asyncio
from flaxon import Flaxon
from flaxon.admin.cms import CMS, CMSField, ContentType
from flaxon.testing import TestClient
def staff(application, permissions):
admin = application.backoffice
admin.auth.add_user(
{
"username": "editor",
"password": "EditorLearning123!",
"roles": [],
"permissions": permissions,
}
)
token = asyncio.run(admin.auth.login("editor", "EditorLearning123!"))
return {"cookie": f"session_id={token}", "x-csrf-token": admin.csrf_token()}
def test_public_content_hides_drafts_and_private_fields(application, client):
articles = application.cms.content_types["help_article"]
articles.create({"title": "Hidden draft", "status": "draft"})
item = articles.create(
{
"title": "Published guide",
"body": "<p>Safe</p><script>bad()</script>",
"status": "published",
}
)
response = client.get("/api/content/articles")
assert [row["title"] for row in response.json()["data"]] == ["Published guide"]
detail = client.get("/api/content/articles/" + item["slug"]).json()["data"]
assert "<script>" not in detail["body"]
assert "created_at" not in detail
assert client.get("/api/content/articles/hidden-draft").status_code == 404
def test_application_accounts_do_not_have_staff_access(client, account):
assert account.call("/admin/cms/api/help_article/items").status_code == 401
def test_editor_can_write_drafts_but_cannot_publish(application, client):
headers = staff(
application,
[
"help_article.view_help_article",
"help_article.add_help_article",
"help_article.change_help_article",
],
)
path = "/admin/cms/api/help_article/items"
assert (
client.post(path, json_data={"title": "Draft"}, headers=headers).status_code
== 201
)
assert (
client.post(
path,
json_data={"title": "Forbidden", "status": "published"},
headers=headers,
).status_code
== 403
)
assert (
client.post(
"/admin/cms/api/import/help_article",
json_data=[{"title": "Forbidden", "status": "published"}],
headers=headers,
).status_code
== 403
)
def test_publisher_requires_csrf(application, client):
headers = staff(
application, ["help_article.add_help_article", "cms.publish_content"]
)
path = "/admin/cms/api/help_article/items"
assert (
client.post(
path,
json_data={"title": "Published", "status": "published"},
headers=headers,
).status_code
== 201
)
assert (
client.post(
path,
json_data={"title": "Missing token"},
headers={"cookie": headers["cookie"]},
).status_code
== 400
)
def test_standalone_cms_is_closed_by_default():
app = Flaxon("cms-test", debug=True)
cms = CMS(app)
cms.register(ContentType("article", fields=[CMSField("title")]))
assert (
TestClient(app)
.post("/admin/cms/api/article/items", json_data={"title": "Forbidden"})
.status_code
== 403
)
def test_database_and_cms_survive_new_application_instance(
application, client, account, tmp_path
):
from app import create_app
account.call("/api/projects/", "post", {"name": "Persistent"})
articles = application.cms.content_types["help_article"]
articles.create({"title": "Persistent article", "status": "published"})
application.cms._save(articles)
new_app = create_app(
tmp_path / "app.sqlite3", tmp_path / "admin.sqlite3", debug=True
)
async def read_project():
from models import Project
async with new_app.db:
return (await Project.all().first()).name
assert asyncio.run(read_project()) == "Persistent"
assert len(new_app.cms.content_types["help_article"].items) == 1
def test_staff_model_adapter_reads_domain_records_and_denies_ungranted_changes(
application, client, account
):
account.call("/api/projects/", "post", {"name": "Domain record"})
headers = staff(application, ["admin.view_dashboard", "project.view_project"])
response = client.get("/admin/project", headers=headers)
assert response.status_code == 200
assert "Domain record" in response.text
assert client.get("/admin/project/1/edit", headers=headers).status_code == 403
assert client.post("/admin/project/1/delete", headers=headers).status_code == 403
Say: "This file owns the feature named by its module or component. Read the route/form flow before continuing."
Type: Follow the action above in tests/test_backoffice.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 2: CREATE - make parent folders, then create this file - tests/test_cms_security.py
This file owns the feature named by its module or component. Read the route/form flow before continuing.
Create the parent directories, then save this complete file.
import asyncio
import pytest
from flaxon import Flaxon
from flaxon.admin import AdminDashboard
from flaxon.admin.cms import CMS, CMSField, ContentType
from flaxon.testing import TestClient
def setup_editor():
app = Flaxon("cms-security", debug=True)
admin = AdminDashboard(
app,
strict_permissions=True,
users=[
{
"username": "editor",
"password": "Editor123!",
"roles": [],
"permissions": [
"article.view_article",
"article.add_article",
"article.change_article",
"cms.restore_revision",
],
}
],
)
cms = CMS(app, auth=admin.auth)
content = cms.register(
ContentType("article", fields=[CMSField("title", required=True)])
)
token = asyncio.run(admin.auth.login("editor", "Editor123!"))
headers = {"cookie": f"session_id={token}", "x-csrf-token": admin.csrf_token()}
return TestClient(app), cms, content, headers
def test_standalone_cms_denies_anonymous_requests_by_default():
app = Flaxon("closed-cms")
cms = CMS(app)
cms.register(ContentType("article", fields=[CMSField("title")]))
client = TestClient(app)
assert client.get("/admin/cms/api/article/items").status_code == 403
assert (
client.post(
"/admin/cms/api/article/items", json_data={"title": "Anonymous"}
).status_code
== 403
)
@pytest.mark.parametrize("status", ["approved", "scheduled", "published"])
@pytest.mark.parametrize("method", ["create", "update", "import"])
def test_editor_cannot_publish_through_generic_write_paths(status, method):
client, cms, content, headers = setup_editor()
data = {"title": "Blocked", "status": status}
if method == "create":
response = client.post(
"/admin/cms/api/article/items", json_data=data, headers=headers
)
elif method == "update":
item = content.create({"title": "Draft"})
response = client.put(
f"/admin/cms/api/article/items/{item['id']}",
json_data=data,
headers=headers,
)
assert item["status"] == "draft"
else:
response = client.post(
"/admin/cms/api/import/article", json_data=[data], headers=headers
)
assert response.status_code == 403
assert not any(item["status"] == status for item in content.items.values())
def test_editor_cannot_restore_published_revision_or_edit_published_content():
client, cms, content, headers = setup_editor()
item = content.create({"title": "Published", "status": "published"})
assert (
client.put(
f"/admin/cms/api/article/items/{item['id']}",
json_data={"title": "Changed"},
headers=headers,
).status_code
== 403
)
content.update(item["id"], {"status": "draft"})
assert (
client.post(
f"/admin/cms/api/article/items/{item['id']}/restore/0", headers=headers
).status_code
== 403
)
assert item["status"] == "draft"
def test_draft_editing_and_publisher_creation_work_with_csrf():
client, cms, content, headers = setup_editor()
assert (
client.post(
"/admin/cms/api/article/items",
json_data={"title": "Draft"},
headers=headers,
).status_code
== 201
)
cms.auth.users["editor"]["permissions"].append("cms.publish_content")
assert (
client.post(
"/admin/cms/api/article/items",
json_data={"title": "Published", "status": "published"},
headers=headers,
).status_code
== 201
)
assert (
client.post(
"/admin/cms/api/article/items",
json_data={"title": "No CSRF"},
headers={"cookie": headers["cookie"]},
).status_code
== 400
)
def test_import_authorization_is_checked_before_any_row_is_created():
client, cms, content, headers = setup_editor()
response = client.post(
"/admin/cms/api/import/article",
json_data=[
{"title": "Draft"},
{"title": "Forbidden", "status": "published"},
],
headers=headers,
)
assert response.status_code == 403
assert content.items == {}
@pytest.mark.parametrize("action", ["publish", "unpublish", "custom"])
def test_actions_require_publisher_permission(action):
client, cms, content, headers = setup_editor()
item = content.create({"title": "Draft"})
content.register_action("custom", "Custom", lambda content_type, ids: None)
response = client.post(
f"/admin/cms/api/article/actions/{action}",
json_data={"ids": [item["id"]]},
headers=headers,
)
assert response.status_code == 403
Say: "This file owns the feature named by its module or component. Read the route/form flow before continuing."
Type: Follow the action above in tests/test_cms_security.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Step 3: CREATE - make parent folders, then create this file - scripts/browser_smoke.py
This file owns the feature named by its module or component. Read the route/form flow before continuing.
Create the parent directories, then save this complete file.
"""Run desktop/mobile workflows against a fresh temporary database. No real user data."""
import os
import subprocess
import sys
import secrets
import tempfile
import time
from pathlib import Path
import httpx
from playwright.sync_api import sync_playwright
ROOT = Path(__file__).resolve().parents[1]
BASE = "http://127.0.0.1:8123"
def main():
with tempfile.TemporaryDirectory() as directory:
production = "--production" in sys.argv
env = {
**os.environ,
"DATA_DIR": directory,
"FLAXON_ALLOWED_HOSTS": "127.0.0.1",
"FLAXON_DEBUG": "0" if production else "1",
"PUBLIC_ORIGIN": BASE,
"FLAXON_SECRET_KEY": secrets.token_urlsafe(48),
"COURSE_SMOKE_STAFF_PASSWORD": "SmokeAa1!" + secrets.token_urlsafe(24),
}
subprocess.run(
[sys.executable, "management.py", "migrate"], cwd=ROOT, env=env, check=True
)
# Bootstrap a staff account in the disposable Admin store before startup.
bootstrap = """
import os
from flaxon.admin.services import AdminAuth, AdminStore
from settings import ADMIN_DATABASE_PATH
store = AdminStore(str(ADMIN_DATABASE_PATH))
auth = AdminAuth(users=[], store=store, strict_permissions=True)
record = auth.add_user({'username': 'course-admin', 'password': os.environ['COURSE_SMOKE_STAFF_PASSWORD'], 'roles': ['administrator']})
store.set('users', 'course-admin', record)
"""
subprocess.run([sys.executable, "-c", bootstrap], cwd=ROOT, env=env, check=True)
log = open(Path(directory) / "server.log", "w")
server = subprocess.Popen(
[sys.executable, "-m", "uvicorn", "app:app", "--port", "8123"],
cwd=ROOT,
env=env,
stdout=log,
stderr=log,
)
try:
for _ in range(150):
try:
if httpx.get(BASE + "/", trust_env=False).status_code == 200:
break
except httpx.ConnectError:
time.sleep(0.1)
else:
raise RuntimeError("Server failed to start")
with sync_playwright() as playwright:
browser = playwright.chromium.launch(headless=True)
staff_context = browser.new_context()
staff_page = staff_context.new_page()
staff_page.goto(BASE + "/admin/login")
staff_page.get_by_label("Username", exact=True).fill("course-admin")
staff_page.get_by_label("Password", exact=True).fill(
env["COURSE_SMOKE_STAFF_PASSWORD"]
)
staff_page.get_by_role("button", name="Sign in", exact=True).click()
staff_page.wait_for_url("**/admin/")
assert (
staff_page.evaluate(
"async () => (await fetch('/admin/project')).status"
)
== 200
)
staff_page.goto(BASE + "/admin/cms/")
csrf = staff_page.locator('meta[name="csrf-token"]').get_attribute(
"content"
)
response = staff_page.evaluate(
"""async (csrf) => {
const response = await fetch('/admin/cms/api/help_article/items', {
method: 'POST', headers: {'Content-Type': 'application/json', 'X-CSRF-Token': csrf},
body: JSON.stringify({title: 'Browser help guide', summary: 'Published by staff', body: '<p>Safe help content.</p>', status: 'published'})
});
return {status: response.status, body: await response.text()};
}""",
csrf,
)
assert response["status"] == 201, response["body"]
staff_context.close()
print("Staff Admin login and CMS publication passed")
for width in [1440, 390]:
context = browser.new_context(
viewport={"width": width, "height": 900}
)
page = context.new_page()
errors = []
page.on("pageerror", lambda error: errors.append(str(error)))
page.goto(BASE + "/login")
register_button = page.get_by_role("button", name="Register instead")
assert register_button.evaluate(
"element => getComputedStyle(element).backgroundColor"
) == "rgb(228, 234, 245)", "Login scoped secondary button style missing"
assert page.evaluate(
"getComputedStyle(document.body).margin === '0px'"
), "Shell document reset missing"
register_button.click()
page.get_by_label("Name", exact=True).fill("Course Learner")
page.get_by_label("Email", exact=True).fill(
f"learner{width}@example.test"
)
page.get_by_label("Password", exact=True).fill("SmokeAa1!" + secrets.token_urlsafe(24))
page.get_by_role(
"button", name="Create account", exact=True
).click()
try:
page.wait_for_url("**/projects", timeout=8000)
except Exception:
print("Browser errors:", errors)
print("URL:", page.url)
print("Page:", page.locator("body").inner_text())
raise
page.get_by_label("Name", exact=True).fill("Browser project")
page.get_by_label("Description", exact=True).fill(
"Verified in the browser"
)
page.get_by_role(
"button", name="Create project", exact=True
).click()
page.get_by_role("link", name="Browser project").click()
page.get_by_role(
"heading", name="Browser project", exact=True
).wait_for()
add_task_button = page.get_by_role("button", name="Add task", exact=True)
assert add_task_button.evaluate(
"element => getComputedStyle(element).backgroundColor"
) == "rgb(37, 99, 235)", "TaskForm must own its scoped button style"
assert page.evaluate("""() => {
const probe = document.createElement('button');
probe.textContent = 'Unscoped probe';
document.body.appendChild(probe);
const colour = getComputedStyle(probe).backgroundColor;
probe.remove();
return colour !== 'rgb(37, 99, 235)';
}"""), "Component button styles leaked outside their scope"
assert not any('/assets/app.css' in entry['name'] for entry in
page.evaluate("performance.getEntriesByType('resource').map(entry => ({name: entry.name}))")
), "The SPA must not load a shared app.css stylesheet"
page.get_by_label("Task title").fill("Record the lesson")
page.get_by_role("button", name="Add task", exact=True).click()
page.get_by_text("Record the lesson", exact=True).wait_for()
page.get_by_label("Task status", exact=True).select_option("done")
page.get_by_text("100% complete", exact=True).wait_for()
screenshot_dir = os.getenv("COURSE_SCREENSHOT_DIR")
if screenshot_dir:
Path(screenshot_dir).mkdir(parents=True, exist_ok=True)
page.screenshot(path=str(Path(screenshot_dir) / f"project-{width}.png"), full_page=True)
page.get_by_label("Filter tasks").select_option("todo")
page.get_by_text(
"No tasks match this filter.", exact=True
).wait_for()
page.get_by_label("Filter tasks").select_option("all")
page.evaluate("window.courseNavigationMarker = 42")
page.get_by_role("link", name="Help", exact=True).click()
page.get_by_role("heading", name="Help centre").wait_for()
assert (
page.evaluate("window.courseNavigationMarker") == 42
), "SPA link reloaded the document"
page.get_by_role("link", name="Browser help guide").click()
page.get_by_role(
"heading", name="Browser help guide", exact=True
).wait_for()
page.get_by_text("Safe help content.", exact=True).wait_for()
page.go_back()
page.get_by_role("heading", name="Help centre").wait_for()
page.go_back()
page.get_by_role(
"heading", name="Browser project", exact=True
).wait_for()
page.reload()
page.get_by_text("100% complete", exact=True).wait_for()
assert page.evaluate(
"document.documentElement.scrollWidth <= innerWidth"
), "Mobile overflow"
page.get_by_role("link", name="Account", exact=True).click()
page.get_by_role("button", name="Sign out").click()
page.get_by_text("Signed out.", exact=True).wait_for()
assert page.request.get(BASE + "/api/projects/").status == 401
assert not errors, errors
context.close()
print(
f"{'Production MinifyJS' if production else 'Development'} browser workflow passed at {width}px"
)
browser.close()
finally:
server.terminate()
server.wait(timeout=10)
log.close()
if __name__ == "__main__":
main()
Say: "This file owns the feature named by its module or component. Read the route/form flow before continuing."
Type: Follow the action above in scripts/browser_smoke.py; explain each new function or binding as you add it.
Viewers should see: The saved file matches this step. After all steps, run the chapter checks below and show the expected result.
Run and check
python -m playwright install chromium
python -m pytest -q
python scripts/browser_smoke.py
python scripts/browser_smoke.py --production
Expected result
All application tests and both browser modes pass. Test data stays outside your learner database. No browser console errors occur in the verified workflow.
Common errors
Install Chromium before Playwright. Stop other servers using port 8123. Loopback production smoke verifies assets and behavior, not cloud infrastructure or TLS.
Short exercise
Complete the workflow using only the keyboard and check one layout change at both desktop and mobile sizes.
What to say and show
Say: "Exercise customer, staff and CMS workflows in a real browser. The server remains responsible for persistence and authorization; the browser presents the result."
Show the expected result above, then point to the file responsible for it. Run the chapter check before the next lesson.
Save your checkpoint
git add .
git commit -m "Complete chapter 14: Full-stack verification"
Type-check before shipping
python management.py typecheck
python management.py typecheck --json
Checking inherits tsconfig.json when present. Static TypeScript child props are checked, while dynamic component contracts and some template cases remain limited. Passing these checks complements API and browser tests; it does not replace them.
Current checker limitations in this course
The completed course is not yet type-clean on the 0.3.0 adapter. Its existing components report strict typing issues (including nullable state and empty-array inference), and the adapter does not fully model $emit, emits, automatic signal helpers, or all template narrowing. The command currently exits with findings; do not present it as a passing release gate for this unmodified course. Fix application typing issues and track the unsupported adapter cases separately. Keep API tests, browser tests and the production build as required checks. This limitation does not stop the development server. The simpler guessing-game example passes the published checker.