SharedDocsLib/tests/test_server.py

279 lines
13 KiB
Python

import base64
import io
import json
import os
import runpy
import zipfile
from pathlib import Path
from fastapi.testclient import TestClient
import docslib
from docslib import HeaderButton, __version__, build
from docslib.components import H1, Image, P
from docslib.hooks import Page
from docslib.server import create_app
def test_loader_renders_indexeddb_before_background_sync():
templates = Path(docslib.__file__).with_name("templates")
loader = (templates / "loader.js").read_text(encoding="utf-8")
client = (templates / "client.js").read_text(encoding="utf-8")
assert "const cached = await cacheGet();" in loader
assert "return cached;" in loader
assert "await cachePut(embedded);" in loader
assert "fetch(" not in loader
assert "renderAll();\n announceRendered();\n syncManifest();" in client
assert "manifest = remote" not in client
def test_all_endpoints_and_self_contained_build(tmp_path):
assets = tmp_path / "assets"
assets.mkdir()
image_bytes = b"not-a-real-png-but-good-enough-for-encoding"
(assets / "hero.png").write_bytes(image_bytes)
custom_css = tmp_path / "inject.css"
custom_css.write_text(".content { --custom-test: yes; }", encoding="utf-8")
custom_js = tmp_path / "inject.js"
custom_js.write_text('document.body.dataset.customTest = "yes";', encoding="utf-8")
@Page("1.1", "Child")
def child():
return P("Second page")
@Page("1.", "Home")
def home():
return [H1("Welcome"), Image("hero.png", "Hero")]
app = create_app(
title="Test docs",
assets_dir=assets,
custom_css=custom_css,
custom_js=custom_js,
manifest_path="http://127.0.0.1:9000/",
header_buttons=[
HeaderButton("Home", home),
HeaderButton.local("Child", "Child", section="1"),
HeaderButton.external("Source", "https://example.com"),
{"label": "Download", "href": "/index.zip"},
],
)
client = TestClient(app)
manifest_response = client.get("/manifest.json")
assert manifest_response.status_code == 200
manifest = manifest_response.json()
assert [page["number"] for page in manifest["pages"]] == ["1", "1.1"]
assert manifest["assets"]["hero.png"]["data"] == base64.b64encode(image_bytes).decode()
assert manifest["schema_version"] == 2
assert "html, body { width: 100%; height: 100dvh" in manifest["client"]["css"]["system"]
assert manifest["client"]["css"]["custom"] == ".content { --custom-test: yes; }"
assert "placeholderTick" in manifest["client"]["js"]["system"]
assert "COMPILED_FROM_VERSION" not in manifest["client"]["js"]["system"]
assert manifest["client"]["js"]["custom"] == 'document.body.dataset.customTest = "yes";'
assert len(manifest["revision"]) == 20
assert manifest["ui"]["header_buttons"] == [
{"label": "Home", "page": "1"},
{"label": "Child", "page": "1.1"},
{"label": "Source", "href": "https://example.com", "target": "_blank"},
{"label": "Download", "href": "/index.zip"},
]
assert "no-store" in manifest_response.headers["cache-control"]
assert "no-cache" in manifest_response.headers["cache-control"]
assert manifest_response.headers["pragma"] == "no-cache"
assert manifest_response.headers["expires"] == "0"
version_response = client.get("/version?no_cache=test")
assert version_response.status_code == 200
assert version_response.text == manifest["revision"]
assert "no-store" in version_response.headers["cache-control"]
assert "no-cache" in version_response.headers["cache-control"]
assert version_response.headers["pragma"] == "no-cache"
assert version_response.headers["expires"] == "0"
html_response = client.get("/index.html")
html = html_response.text
assert html_response.status_code == 200
assert "http://127.0.0.1:9000/manifest.json" in html
assert "http://127.0.0.1:9000/version" in html
assert 'globalThis.COMPILED_FROM_VERSION = "0.3.7";' in html
assert ".content { --custom-test: yes; }" in html
assert r'document.body.dataset.customTest = \"yes\";' in html
assert 'id="docslib-loading"' in html
assert "--docslib-loader-bg: #000000; --docslib-loader-text: #f4f4f4" in html
assert "--docslib-loader-bg: #ffffff; --docslib-loader-text: #111111" in html
assert "position: fixed; inset: 0; z-index: 2147483647; display: grid; width: 100vw; height: 100dvh" in html
assert "border: solid 4px currentColor; border-top: solid 4px transparent; border-radius: 9999px" in html
assert "animation: docslib-loader-spin .8s linear infinite" in html
assert "return cached" in html
assert 'id="docslib-loader-style"' in html
assert "docslib:rendered" in html
system_js = manifest["client"]["js"]["system"]
assert 'new CustomEvent("on_manifest_update"' in system_js
assert "currentManifest: manifest" in system_js
assert "previousRevision" in system_js
assert "cached," in system_js
assert "applied: false" in system_js
assert 'new CustomEvent("on_update_available"' in system_js
assert "currentRevision: lastSyncedRevision" in system_js
assert 'config.versionUrl || siblingResourceUrl(config.manifestUrl, "version")' in system_js
assert 'target.searchParams.set(' in system_js
assert '"no_cache"' in system_js
assert 'fetch(noCacheUrl(url), { cache: "no-store" })' in system_js
assert system_js.index("await fetchNoCache(versionUrl)") < system_js.index(
"await fetchNoCache(config.manifestUrl)"
)
assert system_js.index('new CustomEvent("on_update_available"') < system_js.index(
"await fetchNoCache(config.manifestUrl)"
)
assert "docslib-runtime-style" in html
assert "indexedDB.open" in html
assert "link.dataset.page = button.page" in html
assert 'class="brand-mark"' not in html
assert ">Knowledge base<" not in html
assert ">Search<" not in html
assert ">Ctrl K<" not in html
assert ">Up to date<" not in html
assert "manifestIdentity" in html
assert "100dvh" in html
assert "100vh" not in html
assert "html, body { width: 100%; height: 100dvh" in html
assert "min-height: calc(100dvh - 240px)" not in html
assert "overflow-y: auto" in html
assert "flex: 1 1 auto; flex-direction: column" in html
assert "padding: 0 18px; background: var(--shell)" in html
assert "--desktop-sidebar-width: clamp(270px, 25vw, 400px)" in html
assert "--selection-bg: #111111" in html
assert "--selection-bg: #f0f0f0" in html
assert ".mobile-menu-view .tree-link.active { color: var(--selection-text); background: var(--selection-bg); }" in html
assert "--elevated: #d4d4d4" in html
assert "--shadow: 0 24px 70px rgba(0, 0, 0, .4)" in html
assert "elements.pageSurface.scrollTo" in html
assert "font-family: system-ui; font-size: 16px" in html
assert ".glyph-button, .refresh-glyph { font-family: Arial" in html
assert ".nav-arrow { display: block; width: 10px; height: 10px; flex: 0 0 10px; border: 0 solid currentColor; transform: rotate(45deg); }" in html
assert ".nav-arrow-previous { border-bottom-width: 2px; border-left-width: 2px; }" in html
assert ".nav-arrow-next { border-top-width: 2px; border-right-width: 2px; }" in html
assert 'arrow.textContent = direction' not in html
assert "--shell: #000000" in html
assert 'id="desktop-search-input"' in html
assert '<header class="desktop-header">' in html
assert '<aside class="desktop-sidebar">' in html
assert 'class="sidebar-header-buttons"' in html
assert 'id="mobile-search-input"' in html
assert 'id="mobile-menu-view"' in html
assert 'id="mobile-search-view"' in html
assert "placeholderTick" in html
assert '".".repeat(dots)' in manifest["client"]["js"]["system"]
assert "setTimeout(placeholderTick, 5000)" in html
assert '.sidebar-header-buttons .header-link:not(.active):first-child' not in html
assert "border: 1px solid transparent" not in html
assert ".sidebar-header-buttons { display: flex; flex: 0 0 auto; flex-direction: column; gap: 4px; margin-bottom: 0; border: 1px solid var(--border); border-radius: 16px; background: var(--surface); }" in html
assert ".desktop-sidebar > .page-tree { flex: 1; padding: 24px 2px 70px; mask-image: linear-gradient(transparent 0px, #000 24px, #000 calc(100% - 100px), transparent); scrollbar-width: none; }" in manifest["client"]["css"]["system"]
assert ".mobile-header-buttons { display: flex; flex-direction: column; gap: 6px; margin-bottom: 38px; border: 1px solid var(--border); border-radius: 24px; background: var(--surface); }" in html
assert ".sidebar-header-buttons .header-link { color: var(--text); }" in html
assert ".sidebar-header-buttons .header-link:hover { background: var(--control); }" in html
assert ".sidebar-header-buttons .header-link.active { color: #050505; background: #f0f0f0; }" not in html
assert "--tree-line: #2c2c2c" in html
assert "height: 44px" in html
assert ".refresh-button { display: none;" in html
assert "refresh-expanded" in html
assert "mobile-search-open" in html
assert "flex-direction: column-reverse" in html
assert ".mobile-search-control input { padding-left: 30px; font-size: 1.3rem; }" in html
assert "background: transparent; font-size: 2.3rem" in html
assert ".mobile-page-tree .tree-link { min-height: 46px; padding: 9px 14px; border-radius: 24px" in html
assert ".mobile-menu-button { width: 72px; height: 72px; flex: 0 0 72px; border-radius: 50%; color: var(--text); background: var(--control); font-size: 1.6rem; }" in html
assert ".nav-card { display: flex; min-height: 48px; align-items: center; gap: 8px; padding: 7px 20px; border: 0; border-radius: 30px" in html
assert "background: linear-gradient(0deg, var(--shell) 0%, transparent)" in html
assert "<script src=" not in html
assert "<link rel=" not in html
zip_response = client.get("/index.zip")
assert zip_response.status_code == 200
with zipfile.ZipFile(io.BytesIO(zip_response.content)) as archive:
assert archive.namelist() == ["index.html"]
assert archive.read("index.html") == html_response.content
assert client.get("/live").status_code == 404
def test_build_writes_static_bundle(tmp_path):
assert __version__ == "0.3.7"
@Page("1", "Static page")
def static_page():
return H1("Static documentation")
output = tmp_path / "nested" / "site"
result = build(
build_directory=output,
title="Static docs",
manifest_path="https://docs.example/manifest.json",
header_buttons=[HeaderButton("Home", static_page)],
)
assert sorted(path.name for path in output.iterdir()) == [
"index.html",
"index.zip",
"manifest.json",
"version",
]
assert output.joinpath("index.html").read_bytes() == result.html
assert json.loads(output.joinpath("manifest.json").read_text(encoding="utf-8")) == result.manifest
assert output.joinpath("version").read_text(encoding="utf-8") == result.manifest["revision"]
assert "https://docs.example/manifest.json" in result.html.decode("utf-8")
assert "https://docs.example/version" in result.html.decode("utf-8")
with zipfile.ZipFile(output / "index.zip") as archive:
assert archive.namelist() == ["index.html"]
assert archive.read("index.html") == result.html
def test_live_preview_reloads_a_changed_main_source(tmp_path):
source = tmp_path / "docs.py"
source.write_text(
'from docslib.hooks import Page\n@Page("1", "Live")\ndef page(): return "<p>one</p>"\n',
encoding="utf-8",
)
runpy.run_path(str(source), run_name="__main__")
client = TestClient(create_app(live_preview=True))
initial = client.get("/manifest.json").json()
assert "one" in initial["pages"][0]["content"]
source.write_text(
'from docslib.hooks import Page\n@Page("1", "Live")\ndef page(): return "<p>two</p>"\n',
encoding="utf-8",
)
stat = source.stat()
os.utime(source, ns=(stat.st_atime_ns, stat.st_mtime_ns + 1_000_000_000))
live = client.get("/live").json()
assert live["changed"] is True
assert live["error"] is None
assert live["revision"] != initial["revision"]
assert client.get("/version").text == live["revision"]
assert "two" in client.get("/manifest.json").json()["pages"][0]["content"]
def test_live_preview_rebuilds_changed_custom_javascript(tmp_path):
custom_js = tmp_path / "inject.js"
custom_js.write_text("window.customVersion = 1;", encoding="utf-8")
@Page("1", "Runtime")
def runtime_page():
return "<p>runtime</p>"
client = TestClient(create_app(custom_js=custom_js, live_preview=True))
initial = client.get("/manifest.json").json()
assert initial["client"]["js"]["custom"] == "window.customVersion = 1;"
custom_js.write_text("window.customVersion = 2;", encoding="utf-8")
stat = custom_js.stat()
os.utime(custom_js, ns=(stat.st_atime_ns, stat.st_mtime_ns + 1_000_000_000))
live = client.get("/live").json()
updated = client.get("/manifest.json").json()
assert live["changed"] is True
assert live["revision"] != initial["revision"]
assert updated["client"]["js"]["custom"] == "window.customVersion = 2;"