Keep projector controls inside the viewport
2 files changed,
+95
−7
internal/slides/templates/present.html+33 −7internal/slidestest/browser_present_test.go+62 −0
CI passed — run details
CI log
=== gate ===
=== rust: fmt
=== rust: clippy
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.10s
=== wasm: the artifact the host embeds
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
Finished `release` profile [optimized] target(s) in 0.11s
core/target/wasm32-unknown-unknown/release/slides_core.wasm (1228229 bytes)
=== the embedded wasm must match a fresh build
=== rust: test
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s
Running unittests src/lib.rs (core/target/debug/deps/slides_core-929bfceb2edd4a94)
running 30 tests
test check::tests::the_empty_document_passes ... ok
test layout::tests::aside_region_rect ... ok
test layout::tests::heading_region_rect ... ok
test layout::tests::default_region_by_kind ... ok
test layout::tests::body_region_rect_narrows_for_an_aside ... ok
test layout::tests::media_region_rect ... ok
test markdown::tests::empty_input_is_no_paragraphs_and_lines_keep_blanks ... ok
test markdown::tests::inline_marks ... ok
test layout::tests::to_canvas_narrows_body_when_an_aside_is_present ... ok
test layout::tests::to_canvas_splits_a_shared_region_equally_and_sets_z_by_index ... ok
test markdown::tests::marks_nest_and_links_carry_marks ... ok
test markdown::tests::paragraphs_split_on_blank_lines_and_list_markers ... ok
test layout::tests::to_flow_is_a_no_op_when_already_in_order ... ok
test layout::tests::to_flow_classifies_by_position_and_kind ... ok
test check::tests::an_element_inside_a_paragraph_is_reported ... ok
test check::tests::bare_text_at_fragment_level_is_reported ... ok
test schema::tests::an_empty_map_reads_as_a_set_mark ... ok
test check::tests::what_the_ops_write_passes ... ok
test check::tests::a_non_paragraph_child_is_reported ... ok
test check::tests::y_prosemirror_shapes_pass ... ok
test layout::tests::to_flow_orders_y_then_x ... ok
test schema::tests::an_href_map_without_a_string_url_reads_as_no_link ... ok
test schema::tests::href_written_as_a_plain_string_still_reads_back ... ok
test schema::tests::href_is_written_as_a_one_key_map_and_reads_back ... ok
test check::tests::an_unknown_formatting_attribute_is_reported ... ok
test view::tests::malformed_background_is_the_default ... ok
test view::tests::malformed_box_is_null_and_phone_keeps_only_typed_fields ... ok
test view::tests::assets_are_typed_or_skipped ... ok
test view::tests::malformed_fonts_are_null ... ok
test layout::tests::to_flow_keeps_a_concurrent_text_edit ... ok
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/no_host_imports.rs (core/target/debug/deps/no_host_imports-184c0d8bf00e14f9)
running 1 test
test no_host_imports ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/ops.rs (core/target/debug/deps/ops-256addce27f366d1)
running 12 tests
test add_slide_then_view_lists_it ... ok
test ops_are_atomic ... ok
test add_block_with_markdown_produces_runs ... ok
test layout_switch_assigns_boxes ... ok
test remove_block_drops_it_from_columns_children ... ok
test columns_children_must_name_blocks_on_the_slide ... ok
test duplicate_slide_remaps_columns_children ... ok
test columns_children_have_one_parent_and_no_cycles ... ok
test update_roundtrip ... ok
test move_block_keeps_a_concurrent_text_edit ... ok
test move_slide_keeps_a_concurrent_text_edit ... ok
test remaining_ops_roundtrip ... ok
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Doc-tests slides_core
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== rust: fmt (pdf)
=== rust: clippy (pdf)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.11s
=== wasm: the pdf artifact the host embeds
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.03s
Finished `release` profile [optimized] target(s) in 0.08s
core/pdf/target/wasm32-unknown-unknown/release/slides_pdf.wasm (5301002 bytes)
=== the embedded pdf wasm must match a fresh build
=== rust: test (pdf)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s
Running unittests src/lib.rs (core/pdf/target/debug/deps/slides_pdf-891fafaa44f9fd64)
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/no_host_imports.rs (core/pdf/target/debug/deps/no_host_imports-17ccbb1d62120093)
running 1 test
test no_host_imports ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/render.rs (core/pdf/target/debug/deps/render-2e1d7e8b19d78595)
running 6 tests
test a_null_zero_length_pair_is_no_frames_not_a_dereference ... ok
test text_with_no_fonts_at_all_still_renders_the_shapes ... ok
test raster_image_is_embedded ... ok
test abi_frames_round_trip ... ok
test renders_text_as_embedded_glyphs ... ok
test two_svgs_make_two_pages ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
Doc-tests slides_pdf
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== fixtures: node dependencies
v24.19.0
11.17.0
=== fixtures: npm ci (needs the registry — see .amadan/README.md)
added 19 packages, and audited 20 packages in 859ms
5 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
npm warn allow-scripts 1 package has install scripts not yet covered by allowScripts:
npm warn allow-scripts esbuild@0.28.2 (postinstall: node install.js)
npm warn allow-scripts
npm warn allow-scripts Run `npm approve-scripts --allow-scripts-pending` to review, or `npm approve-scripts <pkg>` to allow.
=== fixtures: the JS side's own tests
✔ one finished upload is register_asset plus the block that uses it, in one batch (28.941046ms)
✔ a record missing its numbers still makes an op the core accepts (1.254032ms)
✔ unusedAssets is what no slide refers to, and remove_asset drops one (8.50674ms)
✔ the Unused images list reconciles by id and names each Remove button (1.215991ms)
✔ an upload refusal is said in words the person can act on (0.273599ms)
✔ clampBox keeps the box inside the frame and no smaller than the minimum (1.854489ms)
✔ fixBox writes four decimals (0.309375ms)
✔ guides are the thirds plus every other block edge (0.364227ms)
✔ snapMove pulls the nearest edge onto a guide within one per cent, and leaves a far one (4.8495ms)
✔ snapResize moves only the edges the handle drags (0.390436ms)
✔ moveBox and resizeBox by handle (0.383984ms)
✔ nudge: arrows move by half a per cent, Shift by two, Alt resizes (0.320947ms)
✔ rectBox reads a block rectangle against its frame (0.260986ms)
✔ typing into a block is a text change on its slide only (17.079314ms)
✔ a block's props changing is structure on its slide (1.264021ms)
✔ adding, moving or removing a block is structure on its slide (2.367233ms)
✔ the notes are text on their slide (1.137676ms)
✔ a slide added, removed or moved is an order change (1.615324ms)
✔ one transaction touching several things reports each once (1.051456ms)
✔ what the ops write passes, and so does an empty document (21.621231ms)
✔ the shapes y-prosemirror and older documents write pass (5.753451ms)
✔ an unknown formatting attribute is reported with the core's message (1.972549ms)
✔ an element inside a paragraph, a non-paragraph child and bare text are reported (10.115993ms)
✔ the unknownmark poison shape is what the check refuses (1.729287ms)
✔ y-prosemirror 1.3.7 deletes the words under an unknown mark; sanitising first keeps them (8.895323ms)
✔ the vocabulary is the five marks and nothing else (0.24181ms)
✔ a reload reuses the id this tab stored for the deck (1.024285ms)
✔ two tabs, and two decks in one tab, get different ids (9.045583ms)
✔ a stored value that is not an id, or storage that throws, gives a fresh id (0.42999ms)
✔ a duplicated tab gives up the copied id; the original keeps it (10.233132ms)
✔ a reload finds no rival and keeps its id (5.342867ms)
✔ presence switches id: beats carry the new one and the old one is somebody else (4.104764ms)
✔ an empty body is no frames (8.168642ms)
✔ frames come back in order with their exact bytes (0.464454ms)
✔ a length prefix is big-endian (0.238694ms)
✔ a truncated frame throws rather than returning a short update (3.144759ms)
✔ deck controls are set_meta (25.977744ms)
✔ slide controls are set_slide, and background keeps its colour (3.15597ms)
✔ move up and down are move_slide with the right after (1.976135ms)
✔ block props: value kinds, region, a shape label (2.153165ms)
✔ box fields are set_block on the desktop and set_phone in the phone view (9.856872ms)
✔ move block and the layer buttons (2.116296ms)
✔ add block: region after the selection on flow, a default box on canvas, a heading level that fits (36.113915ms)
✔ a block is named by its type and its first words (12.424687ms)
✔ blockChange: a control element to its op (1.658555ms)
✔ the Blocks list reconciles by id and keeps its buttons (1.203459ms)
✔ open resolves to the button that closed it and the trimmed field (2.401777ms)
✔ a second open while the dialog is up joins the first (0.262418ms)
✔ a missing dialog mounts as nothing (0.146132ms)
✔ a tab advances its own cursor only when its POST is the very next row (2.881108ms)
✔ the cursor never moves backwards when a response is overtaken by this tab's own write (0.538461ms)
✔ a later response still carries the cursor forward (0.463011ms)
✔ a burst of notifications collapses into one fetch at a time (0.270854ms)
✔ an event naming a seq the tab already has fetches nothing (0.20408ms)
✔ a stopped subscriber drops a response that arrives late (0.272407ms)
✔ stop closes the stream, and start opens exactly one (0.400074ms)
✔ a hidden tab closes its stream on a shortened deadline, and activity reopens it (1.316789ms)
✔ switching away and back inside the grace never drops the stream, so nothing has to be caught up (0.363205ms)
✔ a visible tab that is only being READ closes when idle and comes back on any activity (0.380989ms)
✔ activity while streaming defers the idle close rather than reopening anything (0.221472ms)
✔ a stream failure is never reported as a save failure, and heals on activity (2.901847ms)
✔ an empty response advances the cursor without applying anything (0.43004ms)
✔ a failed fetch leaves the cursor alone so the next notification retries it (0.950067ms)
✔ a decode failure retries once through the raw log, then gives up (2.555904ms)
✔ updatesURL spells the query string exactly, in particular raw as the literal raw=1 (0.982728ms)
✔ empty_doc_views_as_defaults (23.769527ms)
✔ set_meta (9.028612ms)
✔ slides: add, move, remove, set_slide, set_notes (10.768548ms)
✔ duplicate_slide (4.320005ms)
✔ blocks: add, move, remove, set_block (4.554029ms)
✔ set_phone (3.011741ms)
✔ table cells follow rows and cols (3.79592ms)
✔ canvas blocks: box defaults from the region, z from the index (3.791741ms)
✔ assets: register and remove (4.285762ms)
✔ markdown: every mark, code blocks are plain lines (9.144547ms)
✔ batch_is_atomic (5.129911ms)
✔ a batch applies in one transaction with the given origin (1.260134ms)
✔ columns_children_validated (2.124381ms)
✔ layout_switch_round_trip (2.621024ms)
✔ view_matches_snapshot_fixtures (26.956315ms)
✔ columns children have one parent and no cycles (codex 7) (1.331365ms)
✔ canvas-to-flow keeps a concurrent text edit (codex 4) (5.532219ms)
✔ move_block keeps a concurrent text edit, and later inserts land where after says (codex 4) (6.081961ms)
✔ move_slide keeps a concurrent text edit, and later inserts land where after says (codex 4 at slide level) (4.27439ms)
✔ format: {name} and {n} interpolate; an unknown key is left as text (0.954064ms)
✔ a beat on connect, then one every twenty seconds (2.073156ms)
✔ a move posts once, 200ms after the last of a burst, and re-arms the beat (1.488218ms)
✔ a hidden tab stops beating; coming back posts at once (0.395425ms)
✔ applySet leaves this tab out and answers what changed (0.857374ms)
✔ a set with rubbish in it is read for what it does carry (0.239275ms)
✔ initials, and a client id per tab (3.821086ms)
✔ a resync that fails is retried with backoff until it succeeds, without ever running twice at once (2.235357ms)
✔ the browser's online event retries immediately, not just on the next backoff tick (0.900394ms)
✔ reportDivergence before any failure runs the first attempt without a caller ever calling online (8.415933ms)
✔ a permanent 4xx drops its batch instead of retrying it, and does not poison the next edit (84.473617ms)
✔ a network failure or a 5xx is retried, not dropped (60.932045ms)
✔ a 500 is retried the same way a network failure is, never dropped (60.819674ms)
✔ the core-written structure reads as ProseMirror marks (6.87135ms)
✔ a y-prosemirror round trip preserves bold and the link (3.324734ms)
✔ y-prosemirror writes href as the one-key map and marks as {} (0.780141ms)
✔ a plain-string href, written before the map shape, still reads (0.640812ms)
✔ textFragment writes the same shape as writeText (0.658645ms)
✔ safeHref is the server allowlist, and a disallowed href never draws an anchor (0.467259ms)
✔ parseHash splits the slide and the block, and survives a stray % (9.866109ms)
✔ destroy takes the listeners off, and a second mount leaves one of each (0.758882ms)
✔ formatHash is the inverse (0.921384ms)
✔ textBlocks finds the text-bearing blocks and skips the rest (2.192777ms)
✔ mountTarget is the element whose children are the paragraphs (0.313903ms)
✔ paragraphTag keeps the renderer's own markup per type (0.208989ms)
✔ blockFragment finds a block's text by slide and block id (9.199019ms)
✔ notesFragment finds the slide's notes (1.65618ms)
✔ blockIDs and slideIDs are the orders on the stage and the strip (1.654757ms)
✔ cellFragments are a table's cells, row-major (2.29595ms)
✔ a load that fails leaves the stage on the slide it was showing and says so (1.830895ms)
✔ slideAfter is the slide to show once one is gone (0.610175ms)
✔ the queue words map to savestatus states, failed staying failed (6.391057ms)
✔ a dropped batch outranks the queue, for the rest of the session (0.197928ms)
✔ the editor status: the save line and the load error are two lines (0.253842ms)
✔ mountSaveStatus loads estilo's module and mounts it on the line (0.851884ms)
✔ a save-status module that will not load says so and refuses to mount (0.449476ms)
✔ dropTarget: before puts it ahead of the item, after behind it, null means first (1.11854ms)
✔ a drop that leaves the order as it is is no move (0.218186ms)
✔ dropOp is the move_slide (0.856162ms)
✔ labelFor is the slide's first heading, else the numbered fallback (6.214467ms)
✔ flush re-points every dirty thumbnail and relabels it (2.190615ms)
✔ destroy takes the item-menu listener off; a second mount leaves one (0.334823ms)
✔ plan says what to add, drop and where each item goes (0.914762ms)
✔ sync moves only the items out of place and keeps focus (1.541637ms)
✔ desktop view: box and size are the block; hidden and order are always the phone map (1.71022ms)
✔ phone view: everything goes to set_phone, carrying the rest of the map (0.437243ms)
✔ the ops it makes are ones the document takes (17.573864ms)
✔ phoneMap reads only the four keys, and only well-typed (0.234757ms)
✔ the stored view is read defensively (0.452542ms)
✔ linkAt reads the link under the cursor and nothing elsewhere (5.066421ms)
✔ setLink marks the selection (3.908109ms)
✔ setLink on a collapsed cursor inside a link replaces that link (0.766796ms)
✔ setLink with nothing selected and no link does nothing (0.24221ms)
✔ removeLink clears the link under the cursor (0.455367ms)
✔ the keymap binds every documented shortcut (4.816218ms)
✔ Mod-b toggles bold through the keymap (0.956218ms)
✔ setLink refuses an address outside the allowlist and writes nothing (7.010138ms)
✔ the state machine runs choose → sign → put → register → op (3.194201ms)
✔ a file the deck will not take never reaches the network (0.765083ms)
✔ alt text is required before the bytes move, unless the image is decorative (0.357805ms)
✔ a step that fails stops the ones after it and reports its own code (0.724677ms)
✔ a failure that is not ours still becomes an UploadError (0.37149ms)
✔ a second pick while one is in flight is refused, not raced (0.571834ms)
✔ an unreadable size is zero rather than a failure (0.857996ms)
✔ a per-call applyOp wins, so an upload lands on the block it was chosen for (0.292013ms)
✔ checkFile takes exactly the four types (0.554722ms)
✔ sha256Hex is the hex digest of the bytes (15.962137ms)
✔ refusal reads this app's JSON error shape, and survives one that is not (0.544704ms)
✔ malformed fonts are null (4.879255ms)
✔ a malformed background is the theme default (2.356303ms)
✔ a malformed box is null (1.336716ms)
✔ the phone layer keeps only its typed fields, order as a whole number (0.997806ms)
✔ assets are typed or skipped (0.59162ms)
✔ every poison update is deterministic bytes that apply to any deck (26.058986ms)
ℹ tests 158
ℹ suites 0
ℹ pass 158
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 582.666645
=== the vectors must match a fresh generation
canvas: 22 ops, 3715 bytes
columns: 23 ops, 3134 bytes
every-block: 36 ops, 3180 bytes
table: 14 ops, 1509 bytes
title-only: 6 ops, 1136 bytes
=== the editor bundle must match a fresh build
wrote internal/slides/static/editor.0463180baf5aab9f.js (376416 bytes)
=== go: vet, gofmt, test, migration check
go vet ./...
go test ./...
? amadan.net/oficina/slides/cmd/slides [no test files]
ok amadan.net/oficina/slides/internal/slides 11.839s
ok amadan.net/oficina/slides/internal/slides/core 2.981s
ok amadan.net/oficina/slides/internal/slides/deck (cached)
ok amadan.net/oficina/slides/internal/slides/fonts (cached)
? amadan.net/oficina/slides/internal/slides/icons [no test files]
ok amadan.net/oficina/slides/internal/slides/layout (cached)
ok amadan.net/oficina/slides/internal/slides/live (cached)
ok amadan.net/oficina/slides/internal/slides/pdfcore (cached)
ok amadan.net/oficina/slides/internal/slides/present (cached)
ok amadan.net/oficina/slides/internal/slides/render (cached)
ok amadan.net/oficina/slides/internal/slides/svg (cached)
ok amadan.net/oficina/slides/internal/slidestest 37.338s
go run amadan.net/rastrillo/rastrillo/cmd/rastrillo migration check
rastrillo migration check: models and migrations agree
python3 scripts/credits.py --check
credits: checked
gate: ok
diff --git a/internal/slides/templates/present.html b/internal/slides/templates/present.html| index 2323771..548ab55 100644 |
| --- a/internal/slides/templates/present.html |
| +++ b/internal/slides/templates/present.html |
| @@ -18,15 +18,17 @@ |
| <meta http-equiv="refresh" content="5">{{end}} |
| <script defer src="{{asset "static/present.js"}}"></script> |
| <style> |
| - /* The stage fills what it is given and keeps the slide's aspect; |
| - everything else on this page is one line of chrome under it. */ |
| + /* The projector is a bounded black stage, never a document that can |
| + push its remote controls underneath the fold. The controls live on |
| + the stage so the slide can use every pixel behind them. */ |
| [slides-surface] { |
| - box-sizing: border-box; |
| + background: #000; |
| + block-size: 100dvh; |
| + box-sizing: border-box; |
| display: flex; |
| - flex-direction: column; |
| - gap: var(--rst-sp-3); |
| - min-block-size: 100dvh; |
| - padding: var(--rst-sp-3); |
| + inline-size: 100%; |
| + overflow: hidden; |
| + position: relative; |
| } |
| /* The projector is its own full-screen stage, not a document inside |
| the ordinary constrained rst-page wrapper. Its capability is what |
| @@ -39,13 +41,32 @@ |
| padding: 0; |
| } |
| [slides-stage] { |
| + align-items: center; |
| + display: flex; |
| + flex: 1 1 auto; |
| + inline-size: 100%; |
| + justify-content: center; |
| margin: 0; |
| + min-block-size: 0; |
| + min-inline-size: 0; |
| + } |
| + [slides-stage] > [slides-slide] { |
| + block-size: min(100dvh, calc(100vw * 9 / 16)); |
| + inline-size: min(100vw, calc(100dvh * 16 / 9)); |
| + max-block-size: 100%; |
| + max-inline-size: 100%; |
| } |
| [slides-bar] { |
| align-items: center; |
| + background: color-mix(in srgb, #000 72%, transparent); |
| + bottom: 0; |
| display: flex; |
| flex-wrap: wrap; |
| gap: var(--rst-sp-3); |
| + inset-inline: 0; |
| + justify-content: center; |
| + padding: var(--rst-sp-3); |
| + position: absolute; |
| } |
| [slides-bar] form { |
| display: inline; |
| @@ -67,8 +88,13 @@ |
| visibility: hidden; |
| } |
| [slides-blackout] { |
| + align-self: center; |
| + color: #fff; |
| font-weight: 600; |
| margin: 0; |
| + position: absolute; |
| + text-align: center; |
| + z-index: 1; |
| } |
| </style>{{end}} |
| {{define "main"}}<main id="main" slides-present-stage>{{template "content" .}}</main>{{end}} |
diff --git a/internal/slidestest/browser_present_test.go b/internal/slidestest/browser_present_test.go| index 13ed87b..6b74425 100644 |
| --- a/internal/slidestest/browser_present_test.go |
| +++ b/internal/slidestest/browser_present_test.go |
| @@ -10,16 +10,78 @@ |
| package slidestest |
| import ( |
| + "fmt" |
| "regexp" |
| "sync/atomic" |
| "testing" |
| "time" |
| + "github.com/chromedp/cdproto/emulation" |
| "github.com/chromedp/cdproto/page" |
| "github.com/chromedp/chromedp" |
| "github.com/chromedp/chromedp/kb" |
| ) |
| +// TestBrowserProjectorFitsTheViewport keeps the projector a stage rather |
| +// than a long document: its slide is bounded by the viewport and its |
| +// controls are overlaid where the presenter can still reach them. |
| +func TestBrowserProjectorFitsTheViewport(t *testing.T) { |
| + rig, mailer := newRig(t) |
| + browserSignIn(t, rig, mailer) |
| + |
| + var deck string |
| + rig.Run(chromedp.Evaluate(`(document.querySelector('a[href^="/d/"]')?.getAttribute('href') ?? '')`, &deck)) |
| + if !deckPath.MatchString(deck) { |
| + t.Fatalf("the deck list links no deck: %q", deck) |
| + } |
| + |
| + var path string |
| + rig.Run( |
| + chromedp.Navigate(rig.Origin+deck), |
| + chromedp.Click(`form[action$="/present"] button`, chromedp.ByQuery), |
| + waitFor(`location.pathname.endsWith('/notes')`), |
| + chromedp.Evaluate(`location.pathname + location.search`, &path), |
| + ) |
| + m := notesLocation.FindStringSubmatch(path) |
| + if m == nil { |
| + t.Fatalf("Present landed on %q, want /p/{session}/notes?t={token}", path) |
| + } |
| + |
| + for _, viewport := range []struct{ width, height int }{{1920, 1080}, {1280, 720}} { |
| + t.Run(fmt.Sprintf("%dx%d", viewport.width, viewport.height), func(t *testing.T) { |
| + var geometry struct { |
| + ScrollHeight float64 `json:"scrollHeight"` |
| + Viewport float64 `json:"viewport"` |
| + ScrollY float64 `json:"scrollY"` |
| + Top float64 `json:"top"` |
| + Bottom float64 `json:"bottom"` |
| + } |
| + rig.Run( |
| + emulation.SetDeviceMetricsOverride(int64(viewport.width), int64(viewport.height), 1, false), |
| + chromedp.Navigate(rig.Origin+m[1]+"?t="+m[2]), |
| + chromedp.WaitVisible(`[slides-bar]`, chromedp.ByQuery), |
| + chromedp.Evaluate(`(() => { |
| + window.scrollTo(0, 1); |
| + const controls = document.querySelector('[slides-bar]').getBoundingClientRect(); |
| + return { |
| + scrollHeight: document.documentElement.scrollHeight, |
| + viewport: window.innerHeight, |
| + scrollY: window.scrollY, |
| + top: controls.top, |
| + bottom: controls.bottom, |
| + }; |
| + })()`, &geometry), |
| + ) |
| + if geometry.ScrollHeight > geometry.Viewport || geometry.ScrollY != 0 { |
| + t.Errorf("projector scrolls: document %.0fpx, viewport %.0fpx, scrollY %.0f", geometry.ScrollHeight, geometry.Viewport, geometry.ScrollY) |
| + } |
| + if geometry.Top < 0 || geometry.Bottom > geometry.Viewport { |
| + t.Errorf("controls are outside the viewport: top %.0fpx, bottom %.0fpx, viewport %.0fpx", geometry.Top, geometry.Bottom, geometry.Viewport) |
| + } |
| + }) |
| + } |
| +} |
| + |
| var notesLocation = regexp.MustCompile(`^(/p/[23456789abcdefghjkmnpqrstuvwxyz]{10})/notes\?t=([A-Za-z0-9_-]{43})$`) |
| // TestBrowserPresentingScreensHoldStill: the presenter view and the |