今天我們繼續來了解呈現如何在FastHTML app中呈現動態表格及使用Playwright進行互動測試。
您可以於FastHTML Gallery網站,實際測試今日的成果。
fast_app
instance與靜態表格一樣,建立一個名為app
的fast_app
instance及一個rt
object。
# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
from functools import cache
import polars as pl
from fasthtml.common import (H1, H2, Card, Div, Form, Grid, Input, Main,
Title, fast_app)
from great_tables import GT, html
from great_tables.data import sza
from ft_gt import gt2fasthtml
app, rt = fast_app()
get_sza_pivot()
函數建立get_sza_pivot()
函數,其會回傳一個Polars DataFrame。由於在此app
中,DataFrame並不會變動,所以這邊可以使用@cache裝飾在get_sza_pivot()
之上。
# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@cache
def get_sza_pivot():
return (
pl.from_pandas(sza)
.filter((pl.col("latitude") == "20") & (pl.col("tst") <= "1200"))
.select(pl.col("*").exclude("latitude"))
.drop_nulls()
.pivot(values="sza", index="month", on="tst", sort_columns=True)
)
請留意,因為今天的範例只會進行與gt
表格的互動,而不牽涉到DataFrame的整理,所以我將與DataFrame整理相關的程式合併於此函數。
get_gtbl()
函數建立get_gtbl()
函數,其接收color1
及color2
兩個變數,用以設定表格的背景顏色,最後則會回傳GT
instance。
# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@gt2fasthtml(id="gt")
def get_gtbl(color1: str = "#663399", color2: str = "#FFA500"):
return (
GT(get_sza_pivot(), rowname_col="month")
.data_color(
domain=[90, 0],
palette=[color1, "white", color2],
na_color="white",
)
.tab_header(
title="Solar Zenith Angles from 05:30 to 12:00",
subtitle=html("Average monthly values at latitude of 20°N."),
)
.sub_missing(missing_text="")
)
請留意此處我們將@gt2fasthtml(id="gt")
裝飾於get_gtbl()
上,這麼一來這段程式碼可以這麼理解:
get_gtbl()
變成了一個回傳id
為「"gt"」的div
tag。div
tag又包裹了一個NotStr
FastHTML component。NotStr
FastHTML component才真正包裹了HTML格式的gt
表格。post()
函數這裡我們使用@app.post("/submit")
定義post()
為使用者以HTTP POST
訪問「"/submit"」時使用的函數。相比於使用@rt("/submit")
,或許您會更習慣這種Flask或是FastAPI的用法。
# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@app.post("/submit")
def post(d: dict):
return get_gtbl(**d)
post()
所接收的d
是一個字典型別,當我們在網頁上改變稍後會建立的顏色選擇器顏色時,其會包含所選的color1
及color2
的值,像是{'color1': '#663399', 'color2': '#ffa500'}
;而post()
則會回傳get_gtbl(**d)
的結果。
也就是說,這個endpoint將根據所傳入的d
來生成新的gt
表格,並會自動將其包裹於適當的HTML tag或是FastHTML component中回傳。
homepage()
函數使用@rt("/")
來定義get()
,作為使用者以HTTP GET
訪問「"/"」時所使用的函數,其將會直接回傳各種HTML的tag或其內建的component。
# https://github.com/jrycw/ft-gt-demo/blob/master/app.py
@app.get("/")
def homepage():
return (
Title("FastHTML-GT Website"),
H1("Great Tables shown in FastHTML", style="text-align:center"),
Main(
Form(
hx_post="/submit",
hx_target="#gt",
hx_trigger="input",
hx_swap="outerHTML",
)(
Grid(
Div(),
Card(
H2("Color1"),
Input(type="color", id="color1", value="#663399"),
),
Card(
H2("Color2"),
Input(type="color", id="color2", value="#FFA500"),
),
Div(),
)
),
get_gtbl(),
cls="container",
),
)
其中Title
及H1
部份與靜態表格相同,不再重複解釋。
這裡值得注意的是Main
,其中get_gtbl()
是一個id
為「"gt"」的div
tag,而class
attribute設為container
,這兩個是比較好理解的部份。
接下來說說比較複雜的Form
,其接受許多HTMX參數的設定:
hx_post
對應HTMX中的hx-post,其功用是設定當form
進行HTTP POST
時所訪問的URL,即「"/submit"」。hx_target
對應HTMX中的hx-target,其功用是指定form
的回傳值所替換的對象,即id
為「"gt"」的tag,也就是get_gtbl()
。hx_trigger
對應HTMX中的hx-trigger,其功用是指定在什麼情況下,會觸發form
進行HTTP POST
,此處設定當任何input
tag有變動時。hx_swap
對應HTMX中的hx-swap,其功用是指定所需替換的部份,此處使用outerHTML
來指定將會完全替換hx-target
對象的所有HTML。可以理解為當form
每次被觸發後,都會將新的HTML結果(新表格)完全取代id
為「"gt"」的tag(舊表格)。而Form
產生的instance將用以設定網頁上所需出現的元素,其接受一個Grid
。在Grid
中:
Div
來調整整體間距。Card
來代表兩種顏色的版面。Card
中有一個H2
(對應HTML中h2
tag)來作為顏色選擇器的標題及一個Input
(對應HTML中input
tag)來作為顏色選擇器的widget。此處的Grid及Card是FastHTML所提供,用以方便排版的元素,預設為PicoCSS風格。
此外,Form
內其實可以接受HTML tag或FastHTML component,也就是說我們可以將Gird
移至Form
內。這邊只是展示其中一種寫法,您可以依照自己的喜好來選擇程式風格。
於命令列中執行下列指令:
uvicorn app:app --reload
接著打開瀏覽器前往預設網址,如http://127.0.0.1:8000/,就可以見到下面這個漂亮的表格:
這裡我們建立test_simple_app()
來測試:
title
tag是否為「"FastHTML-GT testing app"」。@gt2fasthtml
的情況下,表格的id
是否為「"gt"」。# https://github.com/jrycw/ft-gt-demo/blob/master/tests/test_simple_app.py
import pytest
from fasthtml.common import FastHTML, Title
from starlette.testclient import TestClient
from ft_gt import gt2fasthtml
@pytest.fixture
def simple_app(gtbl):
@gt2fasthtml(id="gt")
def get_tbl():
return gtbl
app = FastHTML()
@app.get("/")
def homepage():
div_comp = get_tbl()
return Title("FastHTML-GT testing app"), div_comp
yield app
@pytest.fixture
def client(simple_app):
yield TestClient(simple_app)
def test_simple_app(client):
resp = client.get("/")
resp_text = resp.text
assert "FastHTML-GT testing app" in resp_text
assert '<div id="gt">' in resp_text
首先建立run_server()
函數,其功用為呼叫uvicorn.run()
啟動伺服器。接著建立一個名為start_server
的pytest fixtxre(autouse
設為True
),其功用為於測試開始時,於背景自動呼叫run_server()
,並於測試結束時自動關閉。
# https://github.com/jrycw/ft-gt-demo/blob/master/tests/test_app.py
from multiprocessing import Process
import pytest
import uvicorn
from playwright.sync_api import expect, sync_playwright
from app import app # FastHTML app is in app.py
test_schema = "http"
test_app_loc = "app:app"
test_host = "127.0.0.1"
test_port = 8741
test_url = f"{test_schema}://{test_host}:{test_port}/"
def run_server():
uvicorn.run(
test_app_loc, host=test_host, port=test_port, log_level="info"
)
@pytest.fixture(scope="module", autouse=True)
def start_server():
process = Process(target=run_server, daemon=True)
process.start()
yield
process.terminate()
接著建立test_app()
函數,來模擬使用chromium
及firefox
兩種瀏覽器下,我們的表格是否可以順利調整背景顏色。此外,我們也順便測試了一下title
及h1
tag的設置是否正確。
# https://github.com/jrycw/ft-gt-demo/blob/master/tests/test_app.py
@pytest.mark.parametrize("core", ["chromium", "firefox"])
def test_app(core):
with sync_playwright() as p:
browser = getattr(p, core).launch(headless=True)
context = browser.new_context(record_video_dir="videos/")
page = context.new_page()
page.goto(test_url)
# test title tag
expect(page).to_have_title("FastHTML-GT Website")
# test h1 tag
first_h1_locator = page.locator("h1").first
expect(first_h1_locator).to_have_text(
"Great Tables shown in FastHTML"
)
expect(first_h1_locator).to_have_css("text-align", "center")
color1_value, color2_value = "#663399", "#ffa500"
color_picker1, color_picker2 = (
page.locator("#color1"),
page.locator("#color2"),
)
# test initial colors
expect(color_picker1).to_have_value(color1_value)
expect(color_picker2).to_have_value(color2_value)
# change color1
color_picker1.fill(color2_value)
expect(color_picker1).to_have_value(color2_value)
# change color2
color_picker2.fill(color1_value)
expect(color_picker2).to_have_value(color1_value)
context.close()
browser.close()
動態表格及其測試可參考ft-gt-demo repo。