Skip to content

Make tests more resilient#124

Merged
lfoppiano merged 6 commits into
mainfrom
feature/test-resilience
Mar 21, 2026
Merged

Make tests more resilient#124
lfoppiano merged 6 commits into
mainfrom
feature/test-resilience

Conversation

@lfoppiano

@lfoppiano lfoppiano commented Mar 20, 2026

Copy link
Copy Markdown
Collaborator

Yet another try...

Summary by CodeRabbit

  • New Features

    • Viewer avoids unnecessary re-rendering while hidden and uses a resize observer to update sizing when it becomes visible.
  • Tests

    • Switched to assertion-based waits for more reliable synchronization.
    • Reduced fixed wait times to speed up tests.
    • Removed several outdated/skipped performance/responsive/interactive tests.
    • Server health check now respects configured timeout.
  • Chores

    • CI workflows updated to use newer action versions.

Signed-off-by: Luca Foppiano <luca@foppiano.org>
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ce2388a7-326e-4906-b69e-2b12bc42021f

📥 Commits

Reviewing files that changed from the base of the PR and between 31dcc55 and b37859f.

📒 Files selected for processing (1)
  • tests/test_unwrap_no_args.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/test_unwrap_no_args.py

📝 Walkthrough

Walkthrough

Replaces non-asserting Playwright visibility checks with expect(...).not_to_be_visible(), shortens/removes many fixed wait_for_timeout() delays, removes several skipped multi‑viewer/performance tests, simplifies some container-dimension assertions, and adds a ResizeObserver to PdfViewer to avoid resize work when container width is zero.

Changes

Cohort / File(s) Summary
Test: visibility → expect()
tests/conftest.py, tests/test_alignment_center.py, tests/test_alignment_left.py, tests/test_alignment_right.py, tests/test_invalid_params.py, tests/test_page_separator_disabled.py, tests/test_page_separator_enabled.py, tests/test_tabs_setWidth.py, tests/test_tabs_setHeight.py, tests/test_unwrap_height.py, tests/test_unwrap_no_args.py, tests/test_unwrap_width.py, tests/test_unwrap_width_height.py, tests/test_zoom_auto.py, tests/test_zoom_auto_height.py, tests/test_zoom_numeric.py
Replaced page.get_by_role(...).is_hidden() boolean checks with expect(page.get_by_role(...)).not_to_be_visible() assertions to synchronize on the "Running..." indicator.
Test: reduced/removed fixed waits
tests/test_accessibility.py, tests/test_edge_cases.py, tests/test_resolution_boost.py, tests/test_responsive_design.py, tests/test_unwrap_no_args.py, tests/test_unwrap_width*.py
Shortened or removed unconditional page.wait_for_timeout() delays (commonly 1000ms→500ms or removed), relying more on element/frame waits.
Test: long hard-wait removals
tests/test_browser_compatibility.py, tests/test_rendering_text.py, tests/test_unwrap_*.py
Removed longer unconditional waits (2000–3000ms) prior to asserting viewer/frame/canvas state.
Test: removed modules & tests
tests/test_error_handling.py, tests/test_multiple_instances.py, tests/test_performance.py, tests/test_interactive_features.py, tests/test_responsive_design.py
Deleted entire test modules or multiple permanently-skipped tests targeting multi‑viewer, performance, or interactive scenarios not applicable to the single‑viewer example app.
Test: assertion simplification & decorator change
tests/test_rendering_text.py, tests/test_tabs_setHeight.py, tests/test_responsive_design.py
Streamlined container-dimension assertions (removed redundant proportional checks; simplified width/height constraints) and removed a @pytest.mark.skip(...) to enable a previously-skipped test.
Frontend: ResizeObserver and early-return
streamlit_pdf_viewer/frontend/src/PdfViewer.vue
Added a ResizeObserver on pdfContainer, track lastContainerWidth, early-return when clientWidth === 0, and call debouncedHandleResize() only when width transitions from zero/uninitialized to positive; observer connected on mount and disconnected on unmount.
CI: action version bumps & push trigger
.github/workflows/ci-build.yml, .github/workflows/ci-bump-version.yml, .github/workflows/ci-release.yml
Bumped GitHub Action versions (actions/checkout, setup-python, setup-node) and limited push trigger to the main branch in CI workflows.

Sequence Diagram(s)

(omitted — conditions for diagram generation not met)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Add more tests #116: Touches the same test fixtures and go_to_app load-wait synchronization logic adjusted here.
  • Fix unit tests #120: Overlaps changes to accessibility tests and canvas/wait handling that were also modified in this PR.
  • Fix or disable non-working tests #112: Modifies PdfViewer.vue behavior and related test synchronization/selectors similar to the frontend change in this PR.

Poem

🐰
I nibbled sleepy waits and swapped a check for care,
Now matchers wake the tests and canvases breathe air.
I watch widths yawn from zero, hop when they grow bright,
Less fluff, more tune — I twitch my nose and dance into the night. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Make tests more resilient' directly aligns with the primary changeset objective: replacing fragile boolean visibility checks with assertion-based waits, reducing/removing fixed delays, and removing skipped/problematic tests to improve test reliability and stability.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/test-resilience

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
tests/test_edge_cases.py (1)

147-147: Replace fixed sleep with a deterministic wait after viewport resize.

Line 147 uses a hard delay after page.set_viewport_size(), which is a common flake source in Playwright tests. Instead of waiting a fixed duration, wait for the viewport dimensions to actually be applied:

♻️ Proposed change
     for viewport in viewports:
         page.set_viewport_size(viewport)
-        page.wait_for_timeout(500)
+        page.wait_for_function(
+            "(size) => window.innerWidth === size.width && window.innerHeight === size.height",
+            viewport,
+        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_edge_cases.py` at line 147, Replace the fixed sleep after
page.set_viewport_size with a deterministic wait that verifies the viewport was
applied: after calling page.set_viewport_size(...) (the symbol to change), call
a wait that polls the page (e.g., page.wait_for_function or an equivalent
helper) to assert window.innerWidth and window.innerHeight match the expected
dimensions instead of page.wait_for_timeout(500); this ensures the test proceeds
only when the viewport change actually took effect.
tests/test_resolution_boost.py (1)

85-85: Replace fixed timeout with explicit state-based wait for consistency and reliability.

This is the only fixed sleep in the test's critical path. The rest of the file uses Playwright's .wait_for(state="visible") pattern (lines 39–40, 59–113). Replace page.wait_for_timeout(500) with an explicit wait for the elements that wait_for_canvases() depends on, such as waiting for the iframe or its child elements to become visible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_resolution_boost.py` at line 85, Replace the fixed sleep
page.wait_for_timeout(500) with an explicit state-based wait for the elements
wait_for_canvases() depends on: wait for the iframe and/or its child canvas
elements to be visible using Playwright's wait_for_selector or wait_for with
state="visible" before calling wait_for_canvases(); update the test to target
the same iframe/canvas selectors already used elsewhere in the file so the test
waits deterministically for the UI to be ready rather than using a hard timeout.
tests/test_accessibility.py (1)

110-110: Replace fixed timeout with condition-based wait after viewport resize.

page.wait_for_timeout(500) is timing-sensitive and can cause flakiness in CI. The same test suite already uses condition-based waits elsewhere (e.g., locator.wait_for(state='visible')). Replace this with a wait on the iframe or pdfViewer to be visible, following the pattern used in test_resolution_boost.py.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_accessibility.py` at line 110, Replace the brittle fixed sleep in
tests/test_accessibility.py (the page.wait_for_timeout(500) call) with a
condition-based wait: locate the iframe or the pdfViewer element used in this
test and call its locator.wait_for(state='visible') (or equivalent visible/state
check) to wait for the resize to complete, following the pattern used in
test_resolution_boost.py; update the test to wait on the same unique selector
used elsewhere (iframe or pdfViewer locator) rather than using
page.wait_for_timeout.
tests/test_rendering_text.py (2)

103-114: Consider using wait_for_canvases for render stability.

The removed delays (3000ms + 2000ms per AI summary) were likely compensating for PDF rendering time. The current approach only waits for the first canvas to be visible in the DOM (canvas.wait_for(state='visible')), which confirms DOM presence but not render completion.

Per the relevant code snippets, other tests use wait_for_canvases() which polls until canvas count stabilizes, providing better render-completion guarantees. Since this test verifies text content visibility (line 116-118), incomplete rendering could cause flakiness.

💡 Suggested improvement
+from tests import wait_for_canvases
+
     # Wait for canvas elements to be rendered (PDF content)
-    canvas = pdf_viewer_1.locator('canvas').first
-    canvas.wait_for(timeout=10000, state='visible')
+    canvas_locator = pdf_viewer_1.locator('canvas')
+    wait_for_canvases(canvas_locator)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_rendering_text.py` around lines 103 - 114, Replace the single DOM
presence check on the first canvas with the shared render-stability helper: call
wait_for_canvases(pdf_viewer_1) (the test-suite helper that polls until canvas
count stabilizes) before measuring pdf_container_1.bounding_box() and before the
text-visibility assertions; remove the canvas.wait_for(...) usage and ensure
wait_for_canvases is imported/available in this test so the PDF rendering is
fully completed prior to the size and text assertions.

112-114: Height assertion range is overly permissive.

The range 299 <= height <= 8000 provides minimal validation. If the expected height is ~300px (based on similar tests like test_tabs_setHeight.py line 90), consider tightening this:

-    assert 299 <= b_box_1['height'] <= 8000
+    assert 299 <= b_box_1['height'] <= 301

If the height genuinely varies widely, document why in a comment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_rendering_text.py` around lines 112 - 114, The height assertion on
b_box_1 from bounding_box() is too loose (299 <= height <= 8000); tighten it to
validate expected ~300px by asserting a narrower range (e.g., 290 <=
b_box_1['height'] <= 350) or use a proximity check (abs(b_box_1['height'] - 300)
<= 10 or pytest.approx(300, rel=0.05)); if the height truly can vary widely, add
a short comment above the assertion explaining why the larger range is necessary
and reference the similar expectation in test_tabs_setHeight.py and the
bounding_box()/b_box_1 symbols to locate the check.
tests/test_unwrap_no_args.py (1)

97-104: Reduced wait time may introduce flakiness for viewport responsiveness test.

The wait after set_viewport_size was reduced from 1000ms to 500ms. Since the assertion at line 104 checks that dimensions have changed, if the resize or re-layout takes longer than 500ms, this test may become flaky.

Consider using a polling approach or an element-based wait (e.g., waiting for the bounding box to change) rather than a fixed timeout for more resilient synchronization.

💡 Alternative: poll until dimensions change
# Example approach using expect with a custom condition
from playwright.sync_api import expect

# After set_viewport_size, poll until the box changes
def wait_for_resize():
    for _ in range(20):  # 20 * 100ms = 2s max
        new_box = iframe_component.bounding_box()
        if new_box['width'] != initial_width or new_box['height'] != initial_height:
            return new_box
        page.wait_for_timeout(100)
    return iframe_component.bounding_box()

new_frame_box = wait_for_resize()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_unwrap_no_args.py` around lines 97 - 104, The 500ms fixed sleep
after page.set_viewport_size is flaky; replace the static wait with a polling or
element-based wait that repeatedly calls iframe_component.bounding_box() until
its width or height differs from initial_width/initial_height (or a timeout is
reached), removing page.wait_for_timeout(500) and using a loop/expect-like retry
to return the new_frame_box only when the dimensions change (or fail after a
sane max wait).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_interactive_features.py`:
- Around line 18-20: The test locates zoom_controls via
iframe_frame.locator('button.zoom-button, .zoom-controls, [class*="zoom"]') but
contains no assertions; update the test to either assert presence (e.g., expect
the locator to have a positive count and to be visible) and/or verify basic
functionality (click the control via zoom_controls.first() and assert the
expected change in the UI or the iframe content), or remove the locator and the
test if zoom functionality cannot be reliably tested; reference the
zoom_controls locator and iframe_frame.locator when making the changes.

---

Nitpick comments:
In `@tests/test_accessibility.py`:
- Line 110: Replace the brittle fixed sleep in tests/test_accessibility.py (the
page.wait_for_timeout(500) call) with a condition-based wait: locate the iframe
or the pdfViewer element used in this test and call its
locator.wait_for(state='visible') (or equivalent visible/state check) to wait
for the resize to complete, following the pattern used in
test_resolution_boost.py; update the test to wait on the same unique selector
used elsewhere (iframe or pdfViewer locator) rather than using
page.wait_for_timeout.

In `@tests/test_edge_cases.py`:
- Line 147: Replace the fixed sleep after page.set_viewport_size with a
deterministic wait that verifies the viewport was applied: after calling
page.set_viewport_size(...) (the symbol to change), call a wait that polls the
page (e.g., page.wait_for_function or an equivalent helper) to assert
window.innerWidth and window.innerHeight match the expected dimensions instead
of page.wait_for_timeout(500); this ensures the test proceeds only when the
viewport change actually took effect.

In `@tests/test_rendering_text.py`:
- Around line 103-114: Replace the single DOM presence check on the first canvas
with the shared render-stability helper: call wait_for_canvases(pdf_viewer_1)
(the test-suite helper that polls until canvas count stabilizes) before
measuring pdf_container_1.bounding_box() and before the text-visibility
assertions; remove the canvas.wait_for(...) usage and ensure wait_for_canvases
is imported/available in this test so the PDF rendering is fully completed prior
to the size and text assertions.
- Around line 112-114: The height assertion on b_box_1 from bounding_box() is
too loose (299 <= height <= 8000); tighten it to validate expected ~300px by
asserting a narrower range (e.g., 290 <= b_box_1['height'] <= 350) or use a
proximity check (abs(b_box_1['height'] - 300) <= 10 or pytest.approx(300,
rel=0.05)); if the height truly can vary widely, add a short comment above the
assertion explaining why the larger range is necessary and reference the similar
expectation in test_tabs_setHeight.py and the bounding_box()/b_box_1 symbols to
locate the check.

In `@tests/test_resolution_boost.py`:
- Line 85: Replace the fixed sleep page.wait_for_timeout(500) with an explicit
state-based wait for the elements wait_for_canvases() depends on: wait for the
iframe and/or its child canvas elements to be visible using Playwright's
wait_for_selector or wait_for with state="visible" before calling
wait_for_canvases(); update the test to target the same iframe/canvas selectors
already used elsewhere in the file so the test waits deterministically for the
UI to be ready rather than using a hard timeout.

In `@tests/test_unwrap_no_args.py`:
- Around line 97-104: The 500ms fixed sleep after page.set_viewport_size is
flaky; replace the static wait with a polling or element-based wait that
repeatedly calls iframe_component.bounding_box() until its width or height
differs from initial_width/initial_height (or a timeout is reached), removing
page.wait_for_timeout(500) and using a loop/expect-like retry to return the
new_frame_box only when the dimensions change (or fail after a sane max wait).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c57342bc-70b8-483d-a905-38b1d4c9b5b1

📥 Commits

Reviewing files that changed from the base of the PR and between b2f01e5 and 50f7377.

📒 Files selected for processing (27)
  • tests/conftest.py
  • tests/e2e_utils.py
  • tests/test_accessibility.py
  • tests/test_alignment_center.py
  • tests/test_alignment_left.py
  • tests/test_alignment_right.py
  • tests/test_browser_compatibility.py
  • tests/test_edge_cases.py
  • tests/test_error_handling.py
  • tests/test_interactive_features.py
  • tests/test_invalid_params.py
  • tests/test_multiple_instances.py
  • tests/test_page_separator_disabled.py
  • tests/test_page_separator_enabled.py
  • tests/test_performance.py
  • tests/test_rendering_text.py
  • tests/test_resolution_boost.py
  • tests/test_responsive_design.py
  • tests/test_tabs_setHeight.py
  • tests/test_tabs_setWidth.py
  • tests/test_unwrap_height.py
  • tests/test_unwrap_no_args.py
  • tests/test_unwrap_width.py
  • tests/test_unwrap_width_height.py
  • tests/test_zoom_auto.py
  • tests/test_zoom_auto_height.py
  • tests/test_zoom_numeric.py
💤 Files with no reviewable changes (4)
  • tests/test_browser_compatibility.py
  • tests/test_error_handling.py
  • tests/test_multiple_instances.py
  • tests/test_performance.py

Comment thread tests/test_interactive_features.py Outdated
Signed-off-by: Luca Foppiano <luca@foppiano.org>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
streamlit_pdf_viewer/frontend/src/PdfViewer.vue (1)

490-490: Cancel pending debounced resize on unmount.

Pending debounced calls can fire after component unmount. Add debouncedHandleResize.cancel() to clear the timer queue during cleanup.

🔧 Proposed fix
     onUnmounted(() => {
       window.removeEventListener("resize", debouncedHandleResize);
       document.removeEventListener('click', handleClickOutside);
+      debouncedHandleResize.cancel();
       observer.disconnect();
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue` at line 490, The component
cleanup currently only calls observer.disconnect() but doesn't cancel the
pending debounced resize, which can run after unmount; update the
unmount/cleanup routine to call debouncedHandleResize.cancel() in addition to
observer.disconnect() so any queued debouncedHandleResize timers are cleared
(locate the cleanup that currently calls observer.disconnect() and add a call to
debouncedHandleResize.cancel()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue`:
- Around line 468-476: The IntersectionObserver is created unconditionally which
can crash in environments that don't support it; before instantiating the const
observer = new IntersectionObserver(...), add a feature check for
window?.IntersectionObserver (or typeof IntersectionObserver !== 'undefined')
and only create and use observer when available—otherwise ensure
wasVisible/value and any teardown logic (e.g., observer.disconnect) gracefully
skip usage; keep references to wasVisible.value and debouncedHandleResize inside
the guarded block so their calls only occur when IntersectionObserver exists.

---

Nitpick comments:
In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue`:
- Line 490: The component cleanup currently only calls observer.disconnect() but
doesn't cancel the pending debounced resize, which can run after unmount; update
the unmount/cleanup routine to call debouncedHandleResize.cancel() in addition
to observer.disconnect() so any queued debouncedHandleResize timers are cleared
(locate the cleanup that currently calls observer.disconnect() and add a call to
debouncedHandleResize.cancel()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5c2c5d65-0c5c-4516-9e97-0fe7e8cc9349

📥 Commits

Reviewing files that changed from the base of the PR and between 50f7377 and 220aa2f.

📒 Files selected for processing (1)
  • streamlit_pdf_viewer/frontend/src/PdfViewer.vue

Comment on lines +468 to +476
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting && !wasVisible.value) {
wasVisible.value = true;
debouncedHandleResize();
} else if (!entry.isIntersecting) {
wasVisible.value = false;
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether IntersectionObserver is already polyfilled/mocked in this repo.
rg -n -C2 'IntersectionObserver|mock.*IntersectionObserver|polyfill' -g '!**/node_modules/**'

Repository: lfoppiano/streamlit-pdf-viewer

Length of output: 545


🏁 Script executed:

#!/bin/bash
# Check package.json for dependencies and browser targets
head -50 package.json

# Check for browserslist or similar browser target config
fd -e '.browserslistrc|browserslist|package.json' . --max-depth 2

# Search for test setup that might use JSDOM or similar
rg -l 'jest|vitest|mocha|testEnvironment' --max-count 5

Repository: lfoppiano/streamlit-pdf-viewer

Length of output: 145


🏁 Script executed:

#!/bin/bash
# Find package.json in the frontend directory
find . -name "package.json" -type f | head -5

# Look for test configuration files
find . -name "vitest.config.*" -o -name "jest.config.*" -o -name "vite.config.*" | head -10

# Check if tests exist and what environment they use
rg -l 'describe|it\(|test\(' --type javascript --type typescript -g '**/*.test.*|**/*.spec.*' | head -5

Repository: lfoppiano/streamlit-pdf-viewer

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Read the frontend package.json
cat ./streamlit_pdf_viewer/frontend/package.json

# Look for vitest/jest config in frontend
find ./streamlit_pdf_viewer/frontend -maxdepth 2 -name "vitest.config.*" -o -name "jest.config.*" -o -name "vite.config.*"

# Check for test files
find ./streamlit_pdf_viewer/frontend -name "*.test.*" -o -name "*.spec.*" | head -5

Repository: lfoppiano/streamlit-pdf-viewer

Length of output: 865


🏁 Script executed:

#!/bin/bash
# Check the main entry point for core-js imports
find ./streamlit_pdf_viewer/frontend/src -name "main.*" -o -name "index.*" | head -5

# Search for core-js imports
rg 'core-js|import.*polyfill' ./streamlit_pdf_viewer/frontend/src

# Check if IntersectionObserver is used elsewhere in the codebase with guards
rg -B3 -A3 'IntersectionObserver' ./streamlit_pdf_viewer/frontend/src

# Look for any browser support documentation
find ./streamlit_pdf_viewer -maxdepth 3 -name "README*" -o -name "BROWSER*" | xargs cat 2>/dev/null | head -100

Repository: lfoppiano/streamlit-pdf-viewer

Length of output: 838


🏁 Script executed:

#!/bin/bash
# Read main.ts to check for polyfill setup
cat ./streamlit_pdf_viewer/frontend/src/main.ts

Repository: lfoppiano/streamlit-pdf-viewer

Length of output: 163


Guard IntersectionObserver initialization to prevent runtime failures in unsupported environments.

Line 468 creates new IntersectionObserver(...) unconditionally. This will crash at component setup time in environments lacking the API (older browsers, server-side rendering, or test runners using JSDOM). Add a feature check before instantiation.

🔧 Proposed fix
-    const observer = new IntersectionObserver((entries) => {
-      const entry = entries[0];
-      if (entry.isIntersecting && !wasVisible.value) {
-        wasVisible.value = true;
-        debouncedHandleResize();
-      } else if (!entry.isIntersecting) {
-        wasVisible.value = false;
-      }
-    });
+    let observer = null;
+    if (typeof window !== "undefined" && "IntersectionObserver" in window) {
+      observer = new IntersectionObserver((entries) => {
+        const entry = entries[0];
+        if (entry.isIntersecting && !wasVisible.value) {
+          wasVisible.value = true;
+          debouncedHandleResize();
+        } else if (!entry.isIntersecting) {
+          wasVisible.value = false;
+        }
+      });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting && !wasVisible.value) {
wasVisible.value = true;
debouncedHandleResize();
} else if (!entry.isIntersecting) {
wasVisible.value = false;
}
});
let observer = null;
if (typeof window !== "undefined" && "IntersectionObserver" in window) {
observer = new IntersectionObserver((entries) => {
const entry = entries[0];
if (entry.isIntersecting && !wasVisible.value) {
wasVisible.value = true;
debouncedHandleResize();
} else if (!entry.isIntersecting) {
wasVisible.value = false;
}
});
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue` around lines 468 - 476, The
IntersectionObserver is created unconditionally which can crash in environments
that don't support it; before instantiating the const observer = new
IntersectionObserver(...), add a feature check for window?.IntersectionObserver
(or typeof IntersectionObserver !== 'undefined') and only create and use
observer when available—otherwise ensure wasVisible/value and any teardown logic
(e.g., observer.disconnect) gracefully skip usage; keep references to
wasVisible.value and debouncedHandleResize inside the guarded block so their
calls only occur when IntersectionObserver exists.

Signed-off-by: Luca Foppiano <luca@foppiano.org>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
streamlit_pdf_viewer/frontend/src/PdfViewer.vue (1)

470-477: ⚠️ Potential issue | 🟠 Major

Guard ResizeObserver construction for unsupported environments.

new ResizeObserver(...) is created unconditionally; this can fail in environments where the API is unavailable (older browsers/test runners). Please gate creation and lifecycle calls.

🔧 Proposed fix
-    let lastContainerWidth = 0;
-    const resizeObserver = new ResizeObserver((entries) => {
-      const entry = entries[0];
-      const newWidth = entry.contentRect.width;
-      if (lastContainerWidth === 0 && newWidth > 0) {
-        debouncedHandleResize();
-      }
-      lastContainerWidth = newWidth;
-    });
+    let lastContainerWidth = 0;
+    const resizeObserver =
+      typeof window !== "undefined" && "ResizeObserver" in window
+        ? new ResizeObserver((entries) => {
+            const entry = entries[0];
+            const newWidth = entry.contentRect.width;
+            if (lastContainerWidth === 0 && newWidth > 0) {
+              debouncedHandleResize();
+            }
+            lastContainerWidth = newWidth;
+          })
+        : null;
@@
-      if (pdfContainer.value) {
+      if (resizeObserver && pdfContainer.value) {
         resizeObserver.observe(pdfContainer.value);
       }
@@
-      resizeObserver.disconnect();
+      resizeObserver?.disconnect();
#!/bin/bash
# Verify whether ResizeObserver is polyfilled/mocked or already guarded in frontend.
rg -n -C2 'ResizeObserver|mock.*ResizeObserver|polyfill' streamlit_pdf_viewer/frontend
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue` around lines 470 - 477, Wrap
creation and use of ResizeObserver in a feature-check so it only runs when the
API exists: guard the new ResizeObserver(...) instantiation and any calls on the
resizeObserver variable (observe, disconnect, etc.) with a conditional like "if
(typeof ResizeObserver !== 'undefined')" around the block where resizeObserver
is defined and where it is used; ensure debouncedHandleResize() and
lastContainerWidth logic remain unchanged inside the guarded block and that the
resizeObserver variable is null/undefined-safe for lifecycle cleanup to avoid
runtime errors in environments without ResizeObserver.
🧹 Nitpick comments (1)
streamlit_pdf_viewer/frontend/src/PdfViewer.vue (1)

473-476: Observer condition is too narrow for non-window layout changes.

Current logic only reacts to 0 -> >0. Consider also reacting to >0 -> different >0 so width changes from parent layout updates can reflow pages reliably.

♻️ Suggested adjustment
-      if (lastContainerWidth === 0 && newWidth > 0) {
+      if (newWidth > 0 && newWidth !== lastContainerWidth) {
         debouncedHandleResize();
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue` around lines 473 - 476, The
current resize observer only triggers debouncedHandleResize when
lastContainerWidth transitions from 0 to >0; update the condition in the
observer so it also triggers when lastContainerWidth > 0 and newWidth differs
(lastContainerWidth !== newWidth) to handle parent layout-driven width changes;
keep assigning lastContainerWidth = newWidth after the check so future changes
are detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue`:
- Around line 470-477: Wrap creation and use of ResizeObserver in a
feature-check so it only runs when the API exists: guard the new
ResizeObserver(...) instantiation and any calls on the resizeObserver variable
(observe, disconnect, etc.) with a conditional like "if (typeof ResizeObserver
!== 'undefined')" around the block where resizeObserver is defined and where it
is used; ensure debouncedHandleResize() and lastContainerWidth logic remain
unchanged inside the guarded block and that the resizeObserver variable is
null/undefined-safe for lifecycle cleanup to avoid runtime errors in
environments without ResizeObserver.

---

Nitpick comments:
In `@streamlit_pdf_viewer/frontend/src/PdfViewer.vue`:
- Around line 473-476: The current resize observer only triggers
debouncedHandleResize when lastContainerWidth transitions from 0 to >0; update
the condition in the observer so it also triggers when lastContainerWidth > 0
and newWidth differs (lastContainerWidth !== newWidth) to handle parent
layout-driven width changes; keep assigning lastContainerWidth = newWidth after
the check so future changes are detected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 69a149da-76c9-435d-a2c0-cd408ad03e0d

📥 Commits

Reviewing files that changed from the base of the PR and between 220aa2f and 857a464.

📒 Files selected for processing (1)
  • streamlit_pdf_viewer/frontend/src/PdfViewer.vue

Signed-off-by: Luca Foppiano <luca@foppiano.org>
Signed-off-by: Luca Foppiano <luca@foppiano.org>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
.github/workflows/ci-build.yml (1)

24-30: Harden action references by pinning immutable SHAs.

Lines 24, 26, and 30 use mutable major tags (@v6). Pin to full commit SHAs to reduce supply-chain risk:

  • Line 24: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
  • Line 26: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405
  • Line 30: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f

Reference: https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci-build.yml around lines 24 - 30, Replace the mutable
action tags with the provided immutable commit SHAs: change the uses: reference
for actions/checkout (currently actions/checkout@v6) to
actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd, actions/setup-python
(currently actions/setup-python@v6) to
actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405, and
actions/setup-node (currently actions/setup-node@v6) to
actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f so the workflow uses
pinned immutable SHAs instead of major tags.
tests/test_unwrap_no_args.py (1)

92-92: Replace fixed sleep with state-based waiting for iframe dimension changes.

Line 92 relies on wait_for_timeout(500), which can be flaky under variable CI load. The variables initial_width and initial_height are already captured at lines 87–88; use wait_for_function() to wait for the iframe's actual dimensions to change instead.

🔧 Suggested refactor
     # Change viewport size
     page.set_viewport_size({"width": 600, "height": 400})
-    page.wait_for_timeout(500)  # Wait for responsive adjustment
+    page.wait_for_function(
+        """([prevW, prevH]) => {
+            const iframe = document.querySelector('iframe[title="streamlit_pdf_viewer.streamlit_pdf_viewer"]');
+            if (!iframe) return false;
+            const r = iframe.getBoundingClientRect();
+            return r.width !== prevW || r.height !== prevH;
+        }""",
+        [initial_width, initial_height],
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_unwrap_no_args.py` at line 92, Replace the hard-coded sleep
page.wait_for_timeout(500) with a state-based wait that polls the iframe's
dimensions until they differ from the previously captured initial_width and
initial_height; use page.wait_for_function() (or frame.evaluate) to query the
iframe element's offsetWidth/offsetHeight and return when width !==
initial_width || height !== initial_height. Update the call site where
initial_width and initial_height are captured (variables initial_width,
initial_height) and remove the fixed timeout, ensuring the selector used to find
the iframe matches the existing code that set those variables.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_interactive_features.py`:
- Around line 10-12: Update the misleading inline comment to match the actual
assertion: change the comment referencing “all three PDF viewers” to reflect
that the test expects a single PDF viewer. Locate the test variable
iframe_components and the locator
'iframe[title="streamlit_pdf_viewer.streamlit_pdf_viewer"]' in
tests/test_interactive_features.py and revise the comment above that assertion
so it accurately describes expect(iframe_components).to_have_count(1).

In `@tests/test_unwrap_no_args.py`:
- Line 72: The loop introduces an unused enumerate index `i` causing lint
warning B007; update the for-loop over canvas_locator.all() to replace the
unused `i` with `_` (i.e., use "for _, canvas in
enumerate(canvas_locator.all()):" or simply "for canvas in
canvas_locator.all():" depending on whether the index is needed) so the variable
`i` is removed and the Ruff warning is resolved.

---

Nitpick comments:
In @.github/workflows/ci-build.yml:
- Around line 24-30: Replace the mutable action tags with the provided immutable
commit SHAs: change the uses: reference for actions/checkout (currently
actions/checkout@v6) to
actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd, actions/setup-python
(currently actions/setup-python@v6) to
actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405, and
actions/setup-node (currently actions/setup-node@v6) to
actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f so the workflow uses
pinned immutable SHAs instead of major tags.

In `@tests/test_unwrap_no_args.py`:
- Line 92: Replace the hard-coded sleep page.wait_for_timeout(500) with a
state-based wait that polls the iframe's dimensions until they differ from the
previously captured initial_width and initial_height; use
page.wait_for_function() (or frame.evaluate) to query the iframe element's
offsetWidth/offsetHeight and return when width !== initial_width || height !==
initial_height. Update the call site where initial_width and initial_height are
captured (variables initial_width, initial_height) and remove the fixed timeout,
ensuring the selector used to find the iframe matches the existing code that set
those variables.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e7d10004-feea-44df-a1cf-1472d8d69822

📥 Commits

Reviewing files that changed from the base of the PR and between 857a464 and 31dcc55.

📒 Files selected for processing (9)
  • .github/workflows/ci-build.yml
  • .github/workflows/ci-bump-version.yml
  • .github/workflows/ci-release.yml
  • tests/test_interactive_features.py
  • tests/test_tabs_setHeight.py
  • tests/test_unwrap_height.py
  • tests/test_unwrap_no_args.py
  • tests/test_unwrap_width.py
  • tests/test_unwrap_width_height.py
✅ Files skipped from review due to trivial changes (2)
  • .github/workflows/ci-bump-version.yml
  • .github/workflows/ci-release.yml
🚧 Files skipped from review as they are similar to previous changes (4)
  • tests/test_unwrap_width_height.py
  • tests/test_unwrap_height.py
  • tests/test_unwrap_width.py
  • tests/test_tabs_setHeight.py

Comment on lines 10 to 12
# Check that all three PDF viewers are present
iframe_components = page.locator('iframe[title="streamlit_pdf_viewer.streamlit_pdf_viewer"]')
expect(iframe_components).to_have_count(1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Update stale inline comment about iframe count.

Line 10 says “all three PDF viewers,” but Line 12 asserts a count of 1. Please align the comment with the actual expectation to avoid confusion during future maintenance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_interactive_features.py` around lines 10 - 12, Update the
misleading inline comment to match the actual assertion: change the comment
referencing “all three PDF viewers” to reflect that the test expects a single
PDF viewer. Locate the test variable iframe_components and the locator
'iframe[title="streamlit_pdf_viewer.streamlit_pdf_viewer"]' in
tests/test_interactive_features.py and revise the comment above that assertion
so it accurately describes expect(iframe_components).to_have_count(1).

Comment thread tests/test_unwrap_no_args.py Outdated
Signed-off-by: Luca Foppiano <luca@foppiano.org>
@lfoppiano lfoppiano merged commit 974f894 into main Mar 21, 2026
24 checks passed
@lfoppiano lfoppiano deleted the feature/test-resilience branch March 21, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant