SharedDocsLib/tests/test_server.py
2026-08-19 10:26:04 +00:00

366 lines
20 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_accessibility_ui_is_generated_by_runtime_for_existing_html():
templates = Path(docslib.__file__).with_name("templates")
index = (templates / "index.html").read_text(encoding="utf-8")
client = (templates / "client.js").read_text(encoding="utf-8")
css = (templates / "client.css").read_text(encoding="utf-8")
assert 'id="accessibility-button"' not in index
assert 'id="mobile-accessibility-button"' not in index
assert 'id="accessibility-view"' not in index
assert "function ensureAccessibilityUi()" in client
assert 'createAccessibilityButton("accessibility-button"' in client
assert 'createAccessibilityButton("mobile-accessibility-button"' in client
assert 'viewBox="0 -960 960 960" fill="currentColor"' in client
assert "M423.5-743.5Q400-767" in client
assert '<circle cx="12" cy="4.5"' not in client
assert "document.body.append(launcher)" in client
assert "elements.mobileTree.before(mobileActions)" in client
assert "(elements.pageTransition || elements.pageSurface).append(view)" in client
assert "--accessibility-font-family: system-ui" in css
assert "--accessibility-font-size: 16px" in css
assert ".accessibility-glyph path { fill: none" not in css
assert "@font-face" not in css
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.5.1";' 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 'new CustomEvent("on_page_change"' in system_js
assert "page," in system_js
assert "previousPage," in system_js
assert "number: page.number" in system_js
assert "title: page.title" in system_js
assert system_js.index('new CustomEvent("on_page_change"') < system_js.index("currentNumber = page.number")
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: var(--accessibility-font-family); 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 "globalThis.PrintFullDocumentation = PrintFullDocumentation" in system_js
assert 'printable.id = "docslib-full-print"' in system_js
assert "manifest.pages.forEach((page) =>" in system_js
assert 'window.addEventListener("afterprint", removeFullPrintDocument)' in system_js
assert "async function PrintFullDocumentation()" in system_js
assert "await waitForPrintAssets(printable)" in system_js
assert 'if (eager) element.loading = "eager"' in system_js
assert "hydrateAssets(article, { eager: true })" in system_js
assert 'image.addEventListener("load", resolve, { once: true })' in system_js
assert 'await image.decode().catch(() => {})' in system_js
assert "await document.fonts.ready" in system_js
system_css = manifest["client"]["css"]["system"]
assert 'body.docslib-print-all > .full-print-document { display: block; }' in system_css
assert 'color: #000 !important; background: #fff !important;' in system_css
assert ".print-page { break-after: page; page-break-after: always; }" in system_css
assert ".print-loading-overlay { position: fixed; inset: 0; z-index: 2147483647; display: grid; width: 100vw; height: 100dvh" in system_css
assert ".print-page-heading h1 { margin: 0; color: #000 !important; font-size: 1.35rem !important" in system_css
assert ".print-page-content h1 { font-size: 1.8rem !important" in system_css
assert "--shell: #000000" in html
assert 'id="desktop-search-input"' in html
assert '<header class="desktop-header">' in html
assert 'id="page-transition" class="page-transition"' in html
assert 'id="desktop-sidebar-toggle"' in html
assert 'aria-label="" aria-expanded="true">☰</button>' 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-theme-button"' in html
assert 'id="mobile-theme-icon"' 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 ".sidebar-header-buttons { 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: 18px; border: 1px solid var(--border); border-radius: 24px; background: var(--surface); }" in html
assert ".mobile-theme-button, .mobile-accessibility-button { display: grid; width: 54px; height: 54px; margin: 0; place-items: center; border: 1px solid var(--border); border-radius: 50%; color: var(--text); background: var(--surface); font-size: 1.35rem; }" in html
assert 'elements.mobileThemeButton.addEventListener("click", toggleTheme)' in system_js
assert 'if (mobileLayout.matches && mobileMode === "menu") skipNextPageAnimation = true' in system_js
assert "currentIndex >= 0 && !skipAnimation && !reducedMotion.matches" in system_js
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 ".desktop-sidebar-toggle { display: grid; width: 44px; height: 44px; flex: 0 0 44px" in html
assert ".desktop-sidebar > * { width: calc(var(--desktop-sidebar-width) - 36px); min-width: calc(var(--desktop-sidebar-width) - 36px); }" in system_css
assert "body.desktop-sidebar-collapsed .desktop-sidebar { width: 0; flex-basis: 0;" in html
assert 'elements.desktopSidebarToggle.addEventListener("click"' in system_js
assert 'try {\n elements.desktopSidebarToggle.addEventListener("click"' in system_js
assert "older compiled HTML may not include the sidebar toggle" in system_js
assert "const transitionElement = elements.pageTransition || elements.content" in system_js
assert "if (elements.mobileThemeIcon) elements.mobileThemeIcon.textContent = icon" in system_js
assert "if (elements.mobileThemeButton) elements.mobileThemeButton.addEventListener" in system_js
assert "ensureAccessibilityUi();" in system_js
assert 'new Set(["system-ui", "sans-serif", "serif", "monospace"])' in system_js
assert 'localStorage.setItem("docslib-accessibility-size"' in system_js
assert 'localStorage.setItem("docslib-accessibility-font"' in system_js
assert 'document.documentElement.style.setProperty("--accessibility-font-size"' in system_js
assert 'document.documentElement.style.setProperty("--accessibility-font-family"' in system_js
assert 'body.accessibility-open .content, body.accessibility-open .page-navigation { display: none; }' in system_css
assert ".accessibility-button { position: fixed; z-index: 40;" in system_css
assert ".accessibility-preview { max-width: 920px; margin-top: 56px; font-size: var(--accessibility-font-size); }" in system_css
assert ".glyph-button, .refresh-glyph { font-family: Arial" in system_css
assert 'document.body.classList.toggle("desktop-sidebar-collapsed", !open)' in system_js
assert "localStorage.setItem" not in system_js[system_js.index("function setDesktopSidebar"):system_js.index("function createAccessibilityButton")]
assert "animation: mobile-menu-enter .26s" in html
assert "transform: translateX(-100%)" in html
assert "const direction = currentIndex >= 0 && index < currentIndex ? -1 : 1" in system_js
assert 'const outgoing = transitionElement.animate(' in system_js
assert 'const incoming = transitionElement.animate(' in system_js
assert 'transform: `translateX(${-30 * direction}px)`' in system_js
assert 'transform: `translateX(${30 * direction}px)`' in system_js
assert 'opacity: 0' in system_js
assert "!reducedMotion.matches" in system_js
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.5.1"
@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;"