Add HTML5 contracts and operations renderer split
This commit is contained in:
@@ -95,145 +95,6 @@ def render_html5_project_rows(projects: Iterable[object]) -> str:
|
||||
return project_rows
|
||||
|
||||
|
||||
def render_html5_operations(
|
||||
jobs: Iterable[object],
|
||||
*,
|
||||
project_id: str = "",
|
||||
status: str = "",
|
||||
kind: str = "",
|
||||
) -> str:
|
||||
job_list = list(jobs)
|
||||
filter_query = _operation_filter_query(project_id=project_id, status=status, kind=kind)
|
||||
return _page(
|
||||
"SFERA HTML5 operations",
|
||||
f"""
|
||||
<main class="shell" data-html5-page="operations">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<p class="eyebrow">SFERA HTML5</p>
|
||||
<h1>Операции сервера</h1>
|
||||
<p class="lead">Очередь фоновых задач отрисовывается API-сервером и обновляется htmx polling без React runtime.</p>
|
||||
</div>
|
||||
<div class="hero-metrics">
|
||||
<strong>{len(job_list)}</strong>
|
||||
<span>jobs</span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="band" hx-ext="sse" sse-connect="/html5/operations/events{filter_query}">
|
||||
<div class="section-title">
|
||||
<h2>Очередь</h2>
|
||||
<a class="button" href="/html5">Проекты</a>
|
||||
</div>
|
||||
{_operation_filter_form(project_id=project_id, status=status, kind=kind)}
|
||||
<div data-html5-operations-summary-stream sse-swap="operations-summary" hx-swap="innerHTML">
|
||||
{render_html5_operation_summary(job_list)}
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table data-html5-operations>
|
||||
<thead>
|
||||
<tr><th>Job</th><th>Проект</th><th>Статус</th><th>Stage</th><th>Сообщение</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody
|
||||
data-html5-operations-body
|
||||
sse-swap="operations-jobs"
|
||||
hx-swap="innerHTML"
|
||||
>{render_html5_operation_rows(job_list)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{render_html5_operation_detail(None)}
|
||||
</section>
|
||||
</main>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def render_html5_operation_summary(jobs: Iterable[object]) -> str:
|
||||
job_list = list(jobs)
|
||||
counts = Counter(_enum_text(getattr(job, "status", "unknown")) for job in job_list)
|
||||
running = counts.get("RUNNING", 0)
|
||||
queued = counts.get("QUEUED", 0)
|
||||
succeeded = counts.get("SUCCEEDED", 0)
|
||||
failed = counts.get("FAILED", 0)
|
||||
return f"""
|
||||
<div
|
||||
class="ops-summary"
|
||||
data-html5-operations-summary
|
||||
>
|
||||
{_metric("Всего", len(job_list))}
|
||||
{_metric("В работе", running)}
|
||||
{_metric("В очереди", queued)}
|
||||
{_metric("Успешно", succeeded)}
|
||||
{_metric("Ошибки", failed)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def render_html5_operation_rows(jobs: Iterable[object]) -> str:
|
||||
rows = "\n".join(_operation_row(job) for job in jobs)
|
||||
if not rows:
|
||||
return '<tr><td colspan="6" class="muted">Фоновые операции пока не запускались</td></tr>'
|
||||
return rows
|
||||
|
||||
|
||||
def render_html5_operation_detail(job: object | None) -> str:
|
||||
if job is None:
|
||||
return """
|
||||
<div class="operation-detail" data-html5-operation-detail>
|
||||
<div class="panel-title flush">Детали операции</div>
|
||||
<p class="muted padded">Выберите job в таблице, чтобы сервер отрисовал payload, result и логи.</p>
|
||||
</div>
|
||||
"""
|
||||
job_id = str(getattr(job, "job_id", ""))
|
||||
kind = str(getattr(job, "kind", ""))
|
||||
status = _enum_text(getattr(job, "status", ""))
|
||||
payload = getattr(job, "payload", {}) or {}
|
||||
result = getattr(job, "result", {}) or {}
|
||||
error = str(getattr(job, "error", "") or "")
|
||||
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
|
||||
return f"""
|
||||
<div class="operation-detail" data-html5-operation-detail data-html5-operation-detail-id="{escape(job_id)}">
|
||||
<div class="panel-title flush">Детали операции</div>
|
||||
<article class="setup-detail">
|
||||
<strong>{escape(kind)} · {escape(status)}</strong>
|
||||
<span>{escape(job_id)}</span>
|
||||
<small>{escape(error or "no error")}</small>
|
||||
</article>
|
||||
<div class="report-grid">
|
||||
{_metric("Payload keys", len(payload))}
|
||||
{_metric("Result keys", len(result))}
|
||||
{_metric("Logs", len(logs))}
|
||||
{_metric("Updated", str(getattr(job, "updated_at", ""))[:19])}
|
||||
</div>
|
||||
<pre class="code">{escape(_compact_mapping(payload))}</pre>
|
||||
<pre class="code">{escape(_compact_mapping(result))}</pre>
|
||||
<ul class="job-log">{"".join(f"<li>{escape(str(item))}</li>" for item in logs[-8:]) or "<li>Лог пока пустой</li>"}</ul>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _operation_filter_form(*, project_id: str, status: str, kind: str) -> str:
|
||||
return f"""
|
||||
<form class="ops-filter" data-html5-operations-filter method="get" action="/html5/operations">
|
||||
<input name="project_id" value="{escape(project_id)}" placeholder="project_id" />
|
||||
<input name="status" value="{escape(status)}" placeholder="status" />
|
||||
<input name="kind" value="{escape(kind)}" placeholder="kind" />
|
||||
<button type="submit">Фильтр</button>
|
||||
<a class="button" href="/html5/operations">Сброс</a>
|
||||
</form>
|
||||
"""
|
||||
|
||||
|
||||
def _operation_filter_query(*, project_id: str, status: str, kind: str) -> str:
|
||||
params = []
|
||||
if project_id:
|
||||
params.append(f"project_id={quote(project_id)}")
|
||||
if status:
|
||||
params.append(f"status={quote(status)}")
|
||||
if kind:
|
||||
params.append(f"kind={quote(kind)}")
|
||||
return f"?{'&'.join(params)}" if params else ""
|
||||
|
||||
|
||||
def render_html5_editor(
|
||||
*,
|
||||
project_id: str,
|
||||
@@ -749,6 +610,7 @@ def render_html5_authoring_changes(project_id: str, changes: Iterable[object] |
|
||||
>
|
||||
<div class="panel-title">Authoring · {len(change_list)}</div>
|
||||
{_authoring_changes_summary(change_list)}
|
||||
{_authoring_recent_change(change_list)}
|
||||
<div class="review-list">{body}</div>
|
||||
{render_html5_authoring_change_detail(project_id, None)}
|
||||
</div>
|
||||
@@ -1500,44 +1362,6 @@ def _project_link(project: object, active_project_id: str) -> str:
|
||||
return f'<a class="project-link{active}" href="/html5/projects/{quote(project_id)}/editor">{escape(name)}</a>'
|
||||
|
||||
|
||||
def _operation_row(job: object) -> str:
|
||||
job_id = str(getattr(job, "job_id", ""))
|
||||
kind = str(getattr(job, "kind", ""))
|
||||
status = _enum_text(getattr(job, "status", ""))
|
||||
payload = getattr(job, "payload", {}) or {}
|
||||
project_id = str(payload.get("project_id") or "")
|
||||
stage = str(payload.get("stage") or "")
|
||||
message = str(payload.get("message") or getattr(job, "error", "") or "")
|
||||
project_link = (
|
||||
f'<a href="/html5/projects/{quote(project_id)}/setup">{escape(project_id)}</a>'
|
||||
if project_id
|
||||
else '<span class="muted">-</span>'
|
||||
)
|
||||
return f"""
|
||||
<tr data-html5-operation="{escape(job_id)}">
|
||||
<td><strong>{escape(kind)}</strong><small>{escape(job_id)}</small></td>
|
||||
<td>{project_link}</td>
|
||||
<td>{escape(status)}</td>
|
||||
<td>{escape(stage or "-")}</td>
|
||||
<td>{escape(message or "-")}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
hx-get="/html5/operations/jobs/{quote(job_id)}/detail"
|
||||
hx-target="[data-html5-operation-detail]"
|
||||
hx-swap="outerHTML"
|
||||
>Открыть</button>
|
||||
</td>
|
||||
</tr>"""
|
||||
|
||||
|
||||
def _compact_mapping(value: dict) -> str:
|
||||
if not value:
|
||||
return "{}"
|
||||
rows = [f"{key}: {value[key]}" for key in sorted(value)[:20]]
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def _topbar(project_id: str, project_nav: str) -> str:
|
||||
return f"""
|
||||
<header class="topbar" data-html5-topbar>
|
||||
@@ -1821,6 +1645,27 @@ def _authoring_changes_summary(changes: Iterable[object]) -> str:
|
||||
"""
|
||||
|
||||
|
||||
def _authoring_recent_change(changes: Iterable[object]) -> str:
|
||||
change_list = list(changes)
|
||||
if not change_list:
|
||||
return ""
|
||||
latest = change_list[0]
|
||||
change_id = str(getattr(latest, "change_id", ""))
|
||||
status = str(getattr(latest, "status", "") or "UNKNOWN")
|
||||
target = getattr(latest, "target", None)
|
||||
target_name = getattr(target, "qualified_name", None) or getattr(target, "name", None) or "target unavailable"
|
||||
version = getattr(latest, "version", None)
|
||||
version_id = str(getattr(latest, "version_id", "") or getattr(version, "version_id", "") or "version unavailable")
|
||||
approved_by = str(getattr(latest, "approved_by", "") or "not approved")
|
||||
return f"""
|
||||
<article class="authoring-change" data-html5-authoring-recent-change="{escape(change_id)}">
|
||||
<strong>{escape(status)} · {escape(str(target_name))}</strong>
|
||||
<span>{escape(version_id)}</span>
|
||||
<small>{escape(approved_by)} · {escape(change_id)}</small>
|
||||
</article>
|
||||
"""
|
||||
|
||||
|
||||
def _authoring_result_summary(state: str, diff: Iterable[object], checks: Iterable[object]) -> str:
|
||||
diff_list = list(diff)
|
||||
check_list = list(checks)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from html import escape
|
||||
from typing import Iterable
|
||||
from urllib.parse import quote
|
||||
|
||||
from api_server.html5 import _enum_text, _metric, _page
|
||||
|
||||
|
||||
def render_html5_operations(
|
||||
jobs: Iterable[object],
|
||||
*,
|
||||
project_id: str = "",
|
||||
status: str = "",
|
||||
kind: str = "",
|
||||
) -> str:
|
||||
job_list = list(jobs)
|
||||
filter_query = _operation_filter_query(project_id=project_id, status=status, kind=kind)
|
||||
return _page(
|
||||
"SFERA HTML5 operations",
|
||||
f"""
|
||||
<main class="shell" data-html5-page="operations">
|
||||
<section class="hero">
|
||||
<div>
|
||||
<p class="eyebrow">SFERA HTML5</p>
|
||||
<h1>Операции сервера</h1>
|
||||
<p class="lead">Очередь фоновых задач отрисовывается API-сервером и обновляется SSE без React runtime.</p>
|
||||
</div>
|
||||
<div class="hero-metrics">
|
||||
<strong>{len(job_list)}</strong>
|
||||
<span>jobs</span>
|
||||
</div>
|
||||
</section>
|
||||
<section class="band" hx-ext="sse" sse-connect="/html5/operations/events{filter_query}">
|
||||
<div class="section-title">
|
||||
<h2>Очередь</h2>
|
||||
<a class="button" href="/html5">Проекты</a>
|
||||
</div>
|
||||
{_operation_filter_form(project_id=project_id, status=status, kind=kind)}
|
||||
<div data-html5-operations-summary-stream sse-swap="operations-summary" hx-swap="innerHTML">
|
||||
{render_html5_operation_summary(job_list)}
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table data-html5-operations>
|
||||
<thead>
|
||||
<tr><th>Job</th><th>Проект</th><th>Статус</th><th>Stage</th><th>Сообщение</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody
|
||||
data-html5-operations-body
|
||||
sse-swap="operations-jobs"
|
||||
hx-swap="innerHTML"
|
||||
>{render_html5_operation_rows(job_list)}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{render_html5_operation_detail(None)}
|
||||
</section>
|
||||
</main>
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
def render_html5_operation_summary(jobs: Iterable[object]) -> str:
|
||||
job_list = list(jobs)
|
||||
counts = Counter(_enum_text(getattr(job, "status", "unknown")) for job in job_list)
|
||||
running = counts.get("RUNNING", 0)
|
||||
queued = counts.get("QUEUED", 0)
|
||||
succeeded = counts.get("SUCCEEDED", 0)
|
||||
failed = counts.get("FAILED", 0)
|
||||
return f"""
|
||||
<div
|
||||
class="ops-summary"
|
||||
data-html5-operations-summary
|
||||
>
|
||||
{_metric("Всего", len(job_list))}
|
||||
{_metric("В работе", running)}
|
||||
{_metric("В очереди", queued)}
|
||||
{_metric("Успешно", succeeded)}
|
||||
{_metric("Ошибки", failed)}
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def render_html5_operation_rows(jobs: Iterable[object]) -> str:
|
||||
rows = "\n".join(_operation_row(job) for job in jobs)
|
||||
if not rows:
|
||||
return '<tr><td colspan="6" class="muted">Фоновые операции пока не запускались</td></tr>'
|
||||
return rows
|
||||
|
||||
|
||||
def render_html5_operation_detail(job: object | None) -> str:
|
||||
if job is None:
|
||||
return """
|
||||
<div class="operation-detail" data-html5-operation-detail>
|
||||
<div class="panel-title flush">Детали операции</div>
|
||||
<p class="muted padded">Выберите job в таблице, чтобы сервер отрисовал payload, result и логи.</p>
|
||||
</div>
|
||||
"""
|
||||
job_id = str(getattr(job, "job_id", ""))
|
||||
kind = str(getattr(job, "kind", ""))
|
||||
status = _enum_text(getattr(job, "status", ""))
|
||||
payload = getattr(job, "payload", {}) or {}
|
||||
result = getattr(job, "result", {}) or {}
|
||||
error = str(getattr(job, "error", "") or "")
|
||||
logs = payload.get("logs") if isinstance(payload.get("logs"), list) else []
|
||||
return f"""
|
||||
<div class="operation-detail" data-html5-operation-detail data-html5-operation-detail-id="{escape(job_id)}">
|
||||
<div class="panel-title flush">Детали операции</div>
|
||||
<article class="setup-detail">
|
||||
<strong>{escape(kind)} · {escape(status)}</strong>
|
||||
<span>{escape(job_id)}</span>
|
||||
<small>{escape(error or "no error")}</small>
|
||||
</article>
|
||||
<div class="report-grid">
|
||||
{_metric("Payload keys", len(payload))}
|
||||
{_metric("Result keys", len(result))}
|
||||
{_metric("Logs", len(logs))}
|
||||
{_metric("Updated", str(getattr(job, "updated_at", ""))[:19])}
|
||||
</div>
|
||||
<pre class="code">{escape(_compact_mapping(payload))}</pre>
|
||||
<pre class="code">{escape(_compact_mapping(result))}</pre>
|
||||
<ul class="job-log">{"".join(f"<li>{escape(str(item))}</li>" for item in logs[-8:]) or "<li>Лог пока пустой</li>"}</ul>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
def _operation_row(job: object) -> str:
|
||||
job_id = str(getattr(job, "job_id", ""))
|
||||
kind = str(getattr(job, "kind", ""))
|
||||
status = _enum_text(getattr(job, "status", ""))
|
||||
payload = getattr(job, "payload", {}) or {}
|
||||
project_id = str(payload.get("project_id") or "")
|
||||
stage = str(payload.get("stage") or "")
|
||||
message = str(payload.get("message") or getattr(job, "error", "") or "")
|
||||
project_link = (
|
||||
f'<a href="/html5/projects/{quote(project_id)}/setup">{escape(project_id)}</a>'
|
||||
if project_id
|
||||
else '<span class="muted">-</span>'
|
||||
)
|
||||
return f"""
|
||||
<tr data-html5-operation="{escape(job_id)}">
|
||||
<td><strong>{escape(kind)}</strong><small>{escape(job_id)}</small></td>
|
||||
<td>{project_link}</td>
|
||||
<td>{escape(status)}</td>
|
||||
<td>{escape(stage or "-")}</td>
|
||||
<td>{escape(message or "-")}</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
hx-get="/html5/operations/jobs/{quote(job_id)}/detail"
|
||||
hx-target="[data-html5-operation-detail]"
|
||||
hx-swap="outerHTML"
|
||||
>Открыть</button>
|
||||
</td>
|
||||
</tr>"""
|
||||
|
||||
|
||||
def _compact_mapping(value: dict) -> str:
|
||||
if not value:
|
||||
return "{}"
|
||||
rows = [f"{key}: {value[key]}" for key in sorted(value)[:20]]
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def _operation_filter_form(*, project_id: str, status: str, kind: str) -> str:
|
||||
return f"""
|
||||
<form class="ops-filter" data-html5-operations-filter method="get" action="/html5/operations">
|
||||
<input name="project_id" value="{escape(project_id)}" placeholder="project_id" />
|
||||
<input name="status" value="{escape(status)}" placeholder="status" />
|
||||
<input name="kind" value="{escape(kind)}" placeholder="kind" />
|
||||
<button type="submit">Фильтр</button>
|
||||
<a class="button" href="/html5/operations">Сброс</a>
|
||||
</form>
|
||||
"""
|
||||
|
||||
|
||||
def _operation_filter_query(*, project_id: str, status: str, kind: str) -> str:
|
||||
params = []
|
||||
if project_id:
|
||||
params.append(f"project_id={quote(project_id)}")
|
||||
if status:
|
||||
params.append(f"status={quote(status)}")
|
||||
if kind:
|
||||
params.append(f"kind={quote(kind)}")
|
||||
return f"?{'&'.join(params)}" if params else ""
|
||||
@@ -51,7 +51,6 @@ from api_server.html5 import (
|
||||
render_html5_metadata_preview_result,
|
||||
render_html5_object_context,
|
||||
render_html5_object_report,
|
||||
render_html5_operation_detail,
|
||||
render_html5_project_setup,
|
||||
render_html5_project_rows,
|
||||
render_html5_project_report,
|
||||
@@ -59,15 +58,18 @@ from api_server.html5 import (
|
||||
render_html5_symbol_detail,
|
||||
render_html5_import_check,
|
||||
render_html5_import_job,
|
||||
render_html5_operation_rows,
|
||||
render_html5_operation_summary,
|
||||
render_html5_operations,
|
||||
render_html5_settings_panel,
|
||||
render_html5_setup_summary,
|
||||
render_html5_source,
|
||||
render_html5_status,
|
||||
render_html5_symbols,
|
||||
)
|
||||
from api_server.html5_operations import (
|
||||
render_html5_operation_detail,
|
||||
render_html5_operation_rows,
|
||||
render_html5_operation_summary,
|
||||
render_html5_operations,
|
||||
)
|
||||
from impact_engine import object_impact, routine_impact
|
||||
from incremental_indexer import rebuild_changed_file
|
||||
from integration_topology import IntegrationKind, build_integration_topology
|
||||
|
||||
@@ -28,6 +28,20 @@ def create_authoring_session(client: TestClient, project_id: str, task_id: str,
|
||||
assert session.status_code == 200
|
||||
|
||||
|
||||
def assert_html5_contract(text: str, *markers: str, full_page: bool = False) -> None:
|
||||
assert "__next" not in text
|
||||
assert "unpkg.com" not in text
|
||||
assert 'hx-trigger="every' not in text
|
||||
if full_page:
|
||||
assert "<!doctype html>" in text
|
||||
assert "/html5/assets/htmx.min.js" in text
|
||||
assert "/html5/assets/htmx-ext-sse.js" in text
|
||||
else:
|
||||
assert "<html" not in text
|
||||
for marker in markers:
|
||||
assert marker in text
|
||||
|
||||
|
||||
def test_cors_allows_lan_panel_origin():
|
||||
client = TestClient(app)
|
||||
response = client.options(
|
||||
@@ -245,6 +259,51 @@ def test_html5_server_rendered_project_editor(tmp_path: Path):
|
||||
assert "sse" in sse_asset.text
|
||||
|
||||
|
||||
def test_html5_contracts_are_server_rendered_and_stable(tmp_path: Path):
|
||||
client = TestClient(app)
|
||||
project_id = f"html5-contract-{uuid4()}"
|
||||
module = tmp_path / "contract_module.bsl"
|
||||
module.write_text(
|
||||
"Процедура ПроверитьКонтракт()\n"
|
||||
" Сообщить(\"contract\");\n"
|
||||
"КонецПроцедуры\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
indexed = client.post("/projects/index", json={"path": str(tmp_path), "project_id": project_id})
|
||||
assert indexed.status_code == 200
|
||||
settings = client.post(
|
||||
f"/projects/{project_id}/settings",
|
||||
json={"name": "HTML5 Contract", "structure_source": "XML_DUMP"},
|
||||
)
|
||||
assert settings.status_code == 200
|
||||
|
||||
full_pages = [
|
||||
("/html5", ("data-html5-page=\"projects\"", "data-html5-projects")),
|
||||
(f"/html5/projects/{project_id}/editor", ("data-html5-page=\"editor\"", "data-html5-editor")),
|
||||
(f"/html5/projects/{project_id}/setup", ("data-html5-page=\"setup\"", "data-html5-setup-summary")),
|
||||
("/html5/operations", ("data-html5-page=\"operations\"", "data-html5-operations-filter")),
|
||||
]
|
||||
for path, markers in full_pages:
|
||||
response = client.get(path)
|
||||
assert response.status_code == 200
|
||||
assert_html5_contract(response.text, *markers, full_page=True)
|
||||
|
||||
partials = [
|
||||
(f"/html5/projects/{project_id}/symbols", {"q": "Проверить"}, ("data-html5-symbol",)),
|
||||
(f"/html5/projects/{project_id}/report", {}, ("data-html5-project-report", "data-html5-project-summary")),
|
||||
(f"/html5/projects/{project_id}/review", {}, ("data-html5-review", "data-html5-review-summary")),
|
||||
(f"/html5/projects/{project_id}/flowchart", {}, ("data-html5-flowchart",)),
|
||||
(f"/html5/projects/{project_id}/authoring/changes", {}, ("data-html5-authoring-changes", "data-html5-authoring-summary")),
|
||||
(f"/html5/projects/{project_id}/setup/summary", {}, ("data-html5-setup-summary",)),
|
||||
("/html5/operations/jobs", {}, ("data-html5-operation",)),
|
||||
("/html5/operations/summary", {}, ("data-html5-operations-summary",)),
|
||||
]
|
||||
for path, params, markers in partials:
|
||||
response = client.get(path, params=params)
|
||||
assert response.status_code == 200
|
||||
assert_html5_contract(response.text, *markers)
|
||||
|
||||
|
||||
def test_html5_project_index_creates_project_with_fragment():
|
||||
client = TestClient(app)
|
||||
project_id = f"html5-created-{uuid4()}"
|
||||
@@ -2778,11 +2837,13 @@ def test_authoring_context_and_completion_preview(tmp_path: Path):
|
||||
assert "text/html" in html5_changes.headers["content-type"]
|
||||
assert "data-html5-authoring-changes" in html5_changes.text
|
||||
assert "data-html5-authoring-summary" in html5_changes.text
|
||||
assert "data-html5-authoring-recent-change" in html5_changes.text
|
||||
assert "workspace" in html5_changes.text
|
||||
assert "+3 / -0" in html5_changes.text
|
||||
assert "data-html5-authoring-detail" in html5_changes.text
|
||||
assert 'hx-target="[data-html5-authoring-detail]"' in html5_changes.text
|
||||
assert apply_payload["change_id"] in html5_changes.text
|
||||
assert apply_payload["version"]["version_id"] in html5_changes.text
|
||||
assert "<html" not in html5_changes.text
|
||||
|
||||
rollback = client.get(
|
||||
|
||||
Reference in New Issue
Block a user