Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,11 @@ date when the release is tagged.
42 MB index had passed the old 32 MiB limit, so search and the Topics
outline failed on every page.
- **Explorer reading defaults.** Desktop chapters open with the reader showing
their text again; only authored-area overviews and scopes over 120 nodes open
map-first, with the 0.9 zoom-out floor. On a phone, Back to a result whose
reader was closed no longer fails with "Could not open this view" (#507).
their text again, and arriving at a shelf or context chapter from the map,
search or the trail focuses its heading. Only authored-area overviews and
scopes over 120 nodes that draw cards open map-first, with the 0.9 zoom-out
floor. On a phone, Back to a result whose reader was closed no longer fails
with "Could not open this view" (#507, #520).
- **Explorer review follow-ups.** Fit retains a selected unlinked result.
Deferred list math cancels with its view and falls back to visible text on
unavailable typesetting. Preview formatting keeps currency and code literal;
Expand Down
6 changes: 3 additions & 3 deletions autoform_cli/assets/blueprint-dag.js
Original file line number Diff line number Diff line change
Expand Up @@ -1534,7 +1534,7 @@
return node.kind === "scope" || node.kind === "boundary" || node._region || node._overviewGroup;
});
var topicOverview = mapAllowed && data.nodes.length && (chapterOverview || topicCards) && !bundle.focus && !bundle.anchor && !(record && record.result);
var mapRequested = Boolean(bundle.mapFirst && (ui$scopeOverview || ui$overviewOnly || mapAllowed && topicCards) && !bundle.anchor && !(record && record.result));
var mapRequested = Boolean(bundle.mapFirst && ((sheetLayout() ? ui$scopeOverview : ui$coarseOverview) || ui$overviewOnly || mapAllowed && topicCards) && !bundle.anchor && !(record && record.result));
var readingRequested = Boolean(bundle.focus && !mapRequested || bundle.anchor || record && record.result);
setSheet(bundle.snapshot ? bundle.snapshot.readingOpen !== false && 1 : sheetLayout() && (topicOverview || mapRequested) ? false : readingRequested ||
!ui$overviewOnly && (sheetLayout() ? !ui$scopeOverview && Boolean(bundle.nodeId) : !ui$coarseOverview), ui$selected);
Expand Down Expand Up @@ -1667,8 +1667,8 @@
var scopeOverview = projected._scopeOverview;
if (scopeOverview && scopeOverview.groups.some(function (group) { return group.key === ui$overviewGroup; })) scopeOverview.selectedGroup = ui$overviewGroup;
ui$scopeOverview = Boolean(scopeOverview); ui$overviewGroup = scopeOverview && scopeOverview.selectedGroup || null;
// Only authored-area overviews and oversized coarsening open map-first; shelves and context cards keep the reader.
ui$coarseOverview = Boolean(overview && (!overview._scopeOverview.$structural || overview._scopeOverview.originalNodes > MAX_MAP_NODES));
// Only authored-area overviews and oversized coarsening that draws cards open map-first; shelves and context cards keep the reader.
ui$coarseOverview = Boolean(overview && (!overview._scopeOverview.$structural || overview._scopeOverview.originalNodes > MAX_MAP_NODES && overview.nodes.length));
host.classList.toggle("bp-dag-scope-overview", ui$coarseOverview);
host.classList.toggle("bp-dag-area-expanded", Boolean(navigation && ui$area));
adoptGraph(projected); ui$mapAllowed = mapAllowed; ui$activeDirty = true; ui$spotlight = ui$hovered = null;
Expand Down
4 changes: 2 additions & 2 deletions autoform_cli/assets/blueprint-explorer.min.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions tests/test_dag_viewer_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,10 @@ def test_oversized_flat_scope_defaults_to_bounded_native_shelves(desktop_page: o
_open_consumer(page, explorer_consumer, "flat")
playwright.expect(page.locator(".bp-scope-shelves")).to_be_visible()
playwright.expect(page.locator(".bp-dag-node")).to_have_count(0)
# With no card to draw, the oversized chapter opens with its reader, not a map-first overview.
playwright.expect(page.locator(".bp-dag-scope-overview")).to_have_count(0)
reader = page.locator(".bp-reading-pane").bounding_box()
assert reader is not None and reader["width"] >= 340, reader
assert page.locator(".bp-scope-shelves a[data-shelf-node-id]").count() <= 100
_browse_contents(page, "Statements in this topic")
playwright.expect(page.get_by_role("button", name="Back to map", exact=True)).to_be_enabled()
Expand Down
5 changes: 5 additions & 0 deletions tests/test_integrated_phone_back_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
_REPO = Path(__file__).resolve().parents[1]
_PHONE = {"viewport": {"width": 390, "height": 844}, "is_mobile": True, "has_touch": True, "device_scale_factor": 2}
_VIEWER = "document.querySelector('.bp-dag-viewer.bp-dag-integrated')"
_CARDS = ".bp-dag-node[data-autoform-node-id]"
_OPEN = f"() => {_VIEWER}?.classList.contains('bp-dag-sheet-open') === true"
_CLOSED = f"() => {_VIEWER}?.classList.contains('bp-dag-sheet-open') === false"
_STATE = f"""() => {{
Expand Down Expand Up @@ -100,6 +101,7 @@ def test_phone_back_after_closing_the_reader_and_opening_an_unrelated_result(
break
handle.click()
page.wait_for_function(_CLOSED)
first_cards = set(page.eval_on_selector_all(_CARDS, "els => els.map(e => e.dataset.autoformNodeId)"))
if entry == "search":
search = page.locator("input.bp-dag-search")
search.click()
Expand All @@ -110,6 +112,7 @@ def test_phone_back_after_closing_the_reader_and_opening_an_unrelated_result(
page.wait_for_function("(title) => document.title === title", arg=b["title"])
page.wait_for_function(_OPEN)
page.wait_for_load_state("networkidle")
second_cards = set(page.eval_on_selector_all(_CARDS, "els => els.map(e => e.dataset.autoformNodeId)"))
page.go_back()
page.wait_for_function("(title) => document.title === title", arg=a["title"])
page.wait_for_load_state("networkidle")
Expand All @@ -118,6 +121,8 @@ def test_phone_back_after_closing_the_reader_and_opening_an_unrelated_result(
finally:
context.close()
assert errors == []
# The failure needs B's map to share no card with A's. The index picked them; also check the cards each map draws on screen.
assert first_cards and second_cards and not first_cards & second_cards, (first_cards, second_cards)
assert "Could not open this view" not in back["error"], back
assert back["readingOpen"] is False, back
assert back["snapshot"] is True, back
68 changes: 66 additions & 2 deletions tests/test_integrated_reader_first_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
context cards, and a 14-statement chapter with one unlinked statement gets a
native shelf; neither is an overview. At 1440x900 both must open with a
reader at least 340 px wide and the chapter text visible, without the
overview's collapsed layout or its 0.9 zoom-out floor.
overview's collapsed layout or its 0.9 zoom-out floor. Arriving from a map
card focuses the reader's heading. A chapter of 121 linked statements is
coarsened, so it keeps the collapsed reader and the floor.
"""

from __future__ import annotations
Expand Down Expand Up @@ -59,8 +61,11 @@ def chapters_site(tmp_path_factory: pytest.TempPathFactory) -> Path:
# A chain of 13 statements and one with no local link.
shelf = [(f"s{k:02d}", f"Shelf {k}", [(f"Shelf {k - 1}", f"s{k - 1:02d}.md")] if 0 < k < 13 else []) for k in range(14)]
_chapter(roadmap, "shelf", "Shelf", shelf)
# Eleven chains of eleven linked statements, one over the map's limit of 120.
big = [(f"b{k:03d}", f"Big {k}", [(f"Big {k - 1}", f"b{k - 1:03d}.md")] if k % 11 else []) for k in range(121)]
_chapter(roadmap, "big", "Big", big)
(roadmap / "README.md").write_text(
"# Reader first\n\n" + "\n".join(f"- [{slug.title()}]({slug}/README.md)" for slug in ("alpha", "beta", "gamma", "hub", "shelf")) + "\n",
"# Reader first\n\n" + "\n".join(f"- [{slug.title()}]({slug}/README.md)" for slug in ("alpha", "beta", "gamma", "hub", "shelf", "big")) + "\n",
encoding="utf-8",
)
render_site(project / "blueprint", project / "site-src")
Expand Down Expand Up @@ -112,3 +117,62 @@ def test_desktop_chapter_opens_with_the_reader_and_zooms_out_freely(
context.close()
assert errors == []
assert scale < 0.9 - 1e-6, scale


@pytest.mark.parametrize("chapter", ["hub", "shelf"])
def test_a_map_card_arrival_at_a_reader_first_chapter_focuses_the_reading_heading(
webkit_browser: object, pinned_runtime: object, chapters_site: Path, chapter: str, # noqa: F811
) -> None:
errors: list[str] = []
with _served(chapters_site) as url:
context = webkit_browser.new_context(viewport={"width": 1440, "height": 900}, reduced_motion="reduce")
try:
page = context.new_page()
page.set_default_timeout(30_000)
page.on("pageerror", lambda error: errors.append(str(error)))
_routes(page, url, pinned_runtime, graph_client=True)
page.goto(f"{url}/roadmap/index.html")
page.wait_for_selector(".bp-dag-node", state="visible")
page.wait_for_load_state("networkidle")
page.locator(".bp-dag-node", has_text=chapter.title()).first.click()
page.wait_for_function("(title) => document.querySelector('.bp-reading-body h1')?.textContent.trim().startsWith(title)", arg=chapter.title())
# Focus lands after the reader's math pass, so wait up to 10 s for it rather than read it once.
where = page.evaluate("""async () => {
const heading = () => document.querySelector('.bp-reading-body h1');
for (let n = 0; n < 100 && document.activeElement !== heading(); n++) await new Promise(r => setTimeout(r, 100));
return document.activeElement === heading() ? null : document.activeElement.outerHTML.slice(0, 120);
}""")
finally:
context.close()
assert errors == []
assert where is None, where


def test_a_chapter_over_the_map_limit_opens_map_first_with_the_zoom_floor(
webkit_browser: object, pinned_runtime: object, chapters_site: Path, # noqa: F811
) -> None:
errors: list[str] = []
with _served(chapters_site) as url:
context = webkit_browser.new_context(viewport={"width": 1440, "height": 900}, reduced_motion="reduce")
try:
page = context.new_page()
page.set_default_timeout(30_000)
page.on("pageerror", lambda error: errors.append(str(error)))
_routes(page, url, pinned_runtime, graph_client=True)
page.goto(f"{url}/roadmap/big/index.html")
page.wait_for_selector(".bp-dag-node", state="visible")
page.wait_for_load_state("networkidle")
_frames(page)
viewer = page.locator(".bp-dag-viewer")
assert viewer.evaluate("element => element.classList.contains('bp-dag-scope-overview')") is True
reader = page.locator(".bp-reading-pane").bounding_box()
assert reader is not None and reader["width"] <= 64, reader
zoom_out = page.get_by_role("button", name="Zoom out", exact=True)
for _ in range(4):
zoom_out.click()
_frames(page)
scale = _scale(page)
finally:
context.close()
assert errors == []
assert scale >= 0.9 - 1e-6, scale
23 changes: 23 additions & 0 deletions tests/test_integrated_reader_focus.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,29 @@ def test_large_scope_landing_never_overrides_explicit_reading_or_saved_sheet_int
""")


def test_map_intent_on_a_shelf_scope_focuses_the_reader_on_desktop_and_the_map_on_a_phone() -> None:
_run(r"""
const project=updateProjection;
// Shelves or context cards make a scope overview that is not coarse.
updateProjection=function(){project();ui$scopeOverview=true;ui$coarseOverview=false;};
byId.get('A').result=false;
dom$nodeLayer={querySelector(selector){
assert.equal(selector,'.bp-dag-node[tabindex="0"]');return overlays.get('A');
}};
const desktop=bundle('A',null,true);desktop.anchor=null;desktop.mapFirst=true;
const desktopReady=applyRoute(desktop);
assert.equal(host.classList.contains('bp-dag-sheet-open'),true);
await frame();await desktopReady;
assert.equal(document.activeElement,desktop.pageHeading,'A desktop reader that opens takes the focus');
sheetLayout=()=>true;
const phone=bundle('A',null,true);phone.anchor=null;phone.mapFirst=true;
const phoneReady=applyRoute(phone);
assert.equal(host.classList.contains('bp-dag-sheet-open'),false);
await frame();await phoneReady;
assert.equal(phone.pageHeading.focuses.length,0);assert.equal(document.activeElement,overlays.get('A'));
""")


def test_map_intent_on_a_small_pure_overview_never_focuses_the_inert_reader() -> None:
_run(r"""
byId.get('A').result=false;
Expand Down
13 changes: 13 additions & 0 deletions tests/test_scoped_overview_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,19 @@ def test_zoom_out_returns_to_overview_without_changing_members_or_source_rows()
""")


def test_only_a_coarse_overview_stops_zoom_out_at_the_readable_scale() -> None:
_run(r"""
// Authored areas coarsen this scope, so zoom-out stops at 0.9.
assert.equal(ui$coarseOverview,true);assert(host.classList.contains('bp-dag-scope-overview'));
ui$transform.scale=.9;zoomAt(.8,450,350);assert.equal(ui$transform.scale,.9);
// Without areas the same results get structural shelves: a scope overview, but not a coarse one.
contextGraph=structuredClone(contextGraph);contextGraph.nodes.forEach(node=>{delete node.area;});updateProjection();
assert.equal(ui$scopeOverview,true);assert.equal(ui$coarseOverview,false);
assert(!host.classList.contains('bp-dag-scope-overview'));
ui$transform.scale=.9;zoomAt(.8,450,350);assert(ui$transform.scale<.9,String(ui$transform.scale));
""")


def test_hidden_original_endpoints_and_boundary_status_gate_cached_typed_flows() -> None:
_run(r"""
contextGraph=structuredClone(contextGraph);
Expand Down
Loading