Skip to content

fix: page down/up scrolls by viewport height, not line count#7479

Open
JohnMcLear wants to merge 7 commits intodevelopfrom
fix/pagedown-long-lines-4562
Open

fix: page down/up scrolls by viewport height, not line count#7479
JohnMcLear wants to merge 7 commits intodevelopfrom
fix/pagedown-long-lines-4562

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

Summary

  • Page down/up counted logical lines in the viewport and scrolled by that count
  • When long wrapped lines filled the viewport, only 1-2 logical lines were visible, so page down barely moved
  • Now scrolls by actual viewport height in pixels (minus 40px overlap for context)
  • Cursor moves to the first/last visible line after scrolling

Test plan

  • Type check passes
  • Manual: create 3+ very long wrapped lines, press Page Down from the top, verify it scrolls through the content

Fixes #4562

🤖 Generated with Claude Code

The previous implementation counted logical lines in the viewport,
which failed when long wrapped lines consumed the entire viewport.
Now scrolls by actual pixel height for correct behavior.

Fixes #4562

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Fix Page Down/Up scrolling with long wrapped lines

🐞 Bug fix

Grey Divider

Walkthroughs

Description
• Page Down/Up now scrolls by viewport height in pixels instead of line count
• Fixes issue where long wrapped lines consumed entire viewport, making scrolling ineffective
• Maintains 40px overlap for context preservation between page transitions
• Cursor repositions to first/last visible line after scrolling
Diagram
flowchart LR
  A["Page Down/Up keypress"] --> B["Calculate viewport height in pixels"]
  B --> C["Scroll by viewport height minus 40px overlap"]
  C --> D["Reposition cursor to new visible area"]
  D --> E["Update selection and display"]
Loading

Grey Divider

File Changes

1. src/static/js/ace2_inner.ts 🐞 Bug fix +34/-38

Replace line-count scrolling with viewport height scrolling

• Replaced line-count-based scrolling with pixel-height-based scrolling
• Removed complex caret position calculation logic that relied on DOM traversal
• Simplified cursor repositioning to use visible line range boundaries
• Changed scheduler timeout from 200ms to 0ms for immediate cursor updates

src/static/js/ace2_inner.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 6, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. No regression test for #4562📘 Rule violation ☼ Reliability
Description
This PR changes PageUp/PageDown scrolling/caret behavior but does not add an automated regression
test for the long wrapped-line scenario that triggered the bug. Without a targeted test, the issue
can reappear unnoticed in future refactors.
Code

src/static/js/ace2_inner.ts[R2862-2897]

+          if ((isPageDown && padShortcutEnabled.pageDown) ||
+              (isPageUp && padShortcutEnabled.pageUp)) {
+            // Scroll by actual viewport height in pixels, not by line count.
+            // This fixes the case where very long wrapped lines consume the
+            // entire viewport, making line-count-based scrolling useless.
+            const viewportHeight = outerWin.document.documentElement.clientHeight;
+            // Keep a small overlap so the user doesn't lose context
+            const scrollAmount = viewportHeight - 40;
+            const currentScrollY = scroll.getScrollY();
+
+            if (isPageDown) {
+              scroll.setScrollY(currentScrollY + scrollAmount);
+            } else {
+              scroll.setScrollY(Math.max(0, currentScrollY - scrollAmount));
         }
-            if (isPageDown && padShortcutEnabled.pageDown) {
-              rep.selStart[0] += numberOfLinesInViewport;
-              rep.selEnd[0] += numberOfLinesInViewport;
-            }
+            // Move cursor into the new visible area
+            scheduler.setTimeout(() => {
+              const linesCount = rep.lines.length();
+              const newVisibleRange = scroll.getVisibleLineRange(rep);
-            // clamp to valid line range
-            rep.selStart[0] = Math.max(0, Math.min(rep.selStart[0], linesCount - 1));
-            rep.selEnd[0] = Math.max(0, Math.min(rep.selEnd[0], linesCount - 1));
-            updateBrowserSelectionFromRep();
-            // get the current caret selection, can't use rep. here because that only gives
-            // us the start position not the current
-            const myselection = targetDoc.getSelection();
-            // get the carets selection offset in px IE 214
-            let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
-                myselection.focusNode.offsetTop;
-
-            // sometimes the first selection is -1 which causes problems
-            // (Especially with ep_page_view)
-            // so use focusNode.offsetTop value.
-            if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
-            // set the scrollY offset of the viewport on the document
-            scroll.setScrollY(caretOffsetTop);
-          }, 200);
+              if (isPageDown) {
+                // Place cursor at the first line of the new viewport
+                rep.selStart[0] = newVisibleRange[0];
+                rep.selEnd[0] = newVisibleRange[0];
+              } else {
+                // Place cursor at the last line of the new viewport
+                rep.selEnd[0] = newVisibleRange[1];
+                rep.selStart[0] = newVisibleRange[1];
+              }
+
+              // clamp to valid line range
+              rep.selStart[0] = Math.max(0, Math.min(rep.selStart[0], linesCount - 1));
+              rep.selEnd[0] = Math.max(0, Math.min(rep.selEnd[0], linesCount - 1));
+              updateBrowserSelectionFromRep();
+            }, 0);
Evidence
PR Compliance ID 3 requires bug fixes to include a regression test that would fail without the fix.
The PR implements the PageUp/PageDown behavior change in src/static/js/ace2_inner.ts, but the
existing PageUp/PageDown Playwright test only covers many short lines and is not updated to cover
consecutive very long wrapped lines (the #4562 reproduction).

src/static/js/ace2_inner.ts[2854-2897]
src/tests/frontend-new/specs/page_up_down.spec.ts[12-49]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This bug fix changes PageUp/PageDown behavior, but the change set does not include a regression test for the reported failure mode (consecutive very long wrapped lines at the top, and the "small edit changes wrapping" variant).
## Issue Context
There is an existing Playwright test for PageUp/PageDown, but it only uses many short lines and does not reproduce #4562 conditions (few logical lines that wrap into many visual lines).
## Fix Focus Areas
- src/tests/frontend-new/specs/page_up_down.spec.ts[1-123]
- src/static/js/ace2_inner.ts[2854-2897]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Bad viewport height access🐞 Bug ≡ Correctness
Description
The PageUp/PageDown handler reads outerWin.document.documentElement.clientHeight, but outerWin
is an iframe element (accessed via getElementsByName('ace_outer')[0]) and does not have a
.document property. This will throw at runtime on PageUp/PageDown, breaking paging and cursor
movement.
Code

src/static/js/ace2_inner.ts[2867]

+            const viewportHeight = outerWin.document.documentElement.clientHeight;
Evidence
outerWin is used as an iframe element and its document is accessed via
outerWin.contentWindow.document / outerDoc; there are no other references to outerWin.document
in the repo, so this new access is incorrect and will be undefined at runtime.

src/static/js/ace2_inner.ts[61-70]
src/static/js/ace2_inner.ts[2862-2871]
src/static/js/ace2_inner.ts[3459-3467]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`outerWin.document` is invalid because `outerWin` is the `ace_outer` iframe element. The PageUp/PageDown handler should derive viewport height from the iframe document (`outerDoc`) or reuse the existing `getInnerHeight()` helper.
### Issue Context
`outerWin` is obtained via `document.getElementsByName('ace_outer')[0]` and the codebase consistently uses `outerWin.contentWindow.document` (aliased as `outerDoc`) to access the iframe document.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2862-2871]
- src/static/js/ace2_inner.ts[61-70]
- src/static/js/ace2_inner.ts[3459-3467]
### Suggested change
Replace:
- `const viewportHeight = outerWin.document.documentElement.clientHeight;`
With one of:
- `const viewportHeight = outerDoc.documentElement.clientHeight;` (consistent with existing code), or
- `const viewportHeight = getInnerHeight();` (already handles hidden iframe cases).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Off-by-one visible line count🐞 Bug ≡ Correctness
Description
visibleLogicalLines is computed as visibleEnd - visibleStart even though
scroll.getVisibleLineRange() returns an inclusive end index. This undercounts visible logical
lines (and therefore the page increment) by 1 in the common case.
Code

src/static/js/ace2_inner.ts[R2874-2887]

+            const visibleStart = newVisibleLineRange[0];
+            const visibleEnd = newVisibleLineRange[1];
+            let totalPixelHeight = 0;
+            for (let i = visibleStart; i <= Math.min(visibleEnd, linesCount - 1); i++) {
+              const entry = rep.lines.atIndex(i);
+              if (entry && entry.lineNode) {
+                totalPixelHeight += entry.lineNode.offsetHeight;
+              }
+            }
+            const visibleLogicalLines = visibleEnd - visibleStart;
+            // Use pixel-based count: how many logical lines fit in one viewport
+            const numberOfLinesInViewport = visibleLogicalLines > 0 && totalPixelHeight > 0
+                ? Math.max(1, Math.round(visibleLogicalLines * viewportHeight / totalPixelHeight))
+                : Math.max(1, visibleLogicalLines);
Evidence
scroll.getVisibleLineRange() returns [start, end - 1], so the second element is the last visible
line index (inclusive). Therefore, the count of visible logical lines is `visibleEnd - visibleStart
+ 1, not visibleEnd - visibleStart` as used in the new computation.

src/static/js/scroll.ts[317-330]
src/static/js/ace2_inner.ts[2866-2887]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scroll.getVisibleLineRange()` returns an inclusive end index, but `visibleLogicalLines` is calculated as an exclusive range. This causes PageUp/PageDown to move by too few logical lines.
### Issue Context
The returned range is `[start, endInclusive]` (see `return [start, end - 1]`).
### Fix
Adjust the visible logical line count to be inclusive:
- `const visibleLogicalLines = visibleEnd - visibleStart + 1;`
Also review any other math that assumes an exclusive end.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2873-2887]
- src/static/js/scroll.ts[317-330]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (2)
4. No regression test for #4562📘 Rule violation ☼ Reliability
Description
This PR changes PageUp/PageDown scrolling/caret behavior but does not add an automated regression
test for the long wrapped-line scenario that triggered the bug. Without a targeted test, the issue
can reappear unnoticed in future refactors.
Code

src/static/js/ace2_inner.ts[R2862-2897]

+          if ((isPageDown && padShortcutEnabled.pageDown) ||
+              (isPageUp && padShortcutEnabled.pageUp)) {
+            // Scroll by actual viewport height in pixels, not by line count.
+            // This fixes the case where very long wrapped lines consume the
+            // entire viewport, making line-count-based scrolling useless.
+            const viewportHeight = outerWin.document.documentElement.clientHeight;
+            // Keep a small overlap so the user doesn't lose context
+            const scrollAmount = viewportHeight - 40;
+            const currentScrollY = scroll.getScrollY();
+
+            if (isPageDown) {
+              scroll.setScrollY(currentScrollY + scrollAmount);
+            } else {
+              scroll.setScrollY(Math.max(0, currentScrollY - scrollAmount));
        }
-            if (isPageDown && padShortcutEnabled.pageDown) {
-              rep.selStart[0] += numberOfLinesInViewport;
-              rep.selEnd[0] += numberOfLinesInViewport;
-            }
+            // Move cursor into the new visible area
+            scheduler.setTimeout(() => {
+              const linesCount = rep.lines.length();
+              const newVisibleRange = scroll.getVisibleLineRange(rep);
-            // clamp to valid line range
-            rep.selStart[0] = Math.max(0, Math.min(rep.selStart[0], linesCount - 1));
-            rep.selEnd[0] = Math.max(0, Math.min(rep.selEnd[0], linesCount - 1));
-            updateBrowserSelectionFromRep();
-            // get the current caret selection, can't use rep. here because that only gives
-            // us the start position not the current
-            const myselection = targetDoc.getSelection();
-            // get the carets selection offset in px IE 214
-            let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
-                myselection.focusNode.offsetTop;
-
-            // sometimes the first selection is -1 which causes problems
-            // (Especially with ep_page_view)
-            // so use focusNode.offsetTop value.
-            if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
-            // set the scrollY offset of the viewport on the document
-            scroll.setScrollY(caretOffsetTop);
-          }, 200);
+              if (isPageDown) {
+                // Place cursor at the first line of the new viewport
+                rep.selStart[0] = newVisibleRange[0];
+                rep.selEnd[0] = newVisibleRange[0];
+              } else {
+                // Place cursor at the last line of the new viewport
+                rep.selEnd[0] = newVisibleRange[1];
+                rep.selStart[0] = newVisibleRange[1];
+              }
+
+              // clamp to valid line range
+              rep.selStart[0] = Math.max(0, Math.min(rep.selStart[0], linesCount - 1));
+              rep.selEnd[0] = Math.max(0, Math.min(rep.selEnd[0], linesCount - 1));
+              updateBrowserSelectionFromRep();
+            }, 0);
Evidence
PR Compliance ID 3 requires bug fixes to include a regression test that would fail without the fix.
The PR implements the PageUp/PageDown behavior change in src/static/js/ace2_inner.ts, but the
existing PageUp/PageDown Playwright test only covers many short lines and is not updated to cover
consecutive very long wrapped lines (the #4562 reproduction).

src/static/js/ace2_inner.ts[2854-2897]
src/tests/frontend-new/specs/page_up_down.spec.ts[12-49]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This bug fix changes PageUp/PageDown behavior, but the change set does not include a regression test for the reported failure mode (consecutive very long wrapped lines at the top, and the "small edit changes wrapping" variant).
## Issue Context
There is an existing Playwright test for PageUp/PageDown, but it only uses many short lines and does not reproduce #4562 conditions (few logical lines that wrap into many visual lines).
## Fix Focus Areas
- src/tests/frontend-new/specs/page_up_down.spec.ts[1-123]
- src/static/js/ace2_inner.ts[2854-2897]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Bad viewport height access🐞 Bug ≡ Correctness
Description
The PageUp/PageDown handler reads outerWin.document.documentElement.clientHeight, but outerWin
is an iframe element (accessed via getElementsByName('ace_outer')[0]) and does not have a
.document property. This will throw at runtime on PageUp/PageDown, breaking paging and cursor
movement.
Code

src/static/js/ace2_inner.ts[2867]

+            const viewportHeight = outerWin.document.documentElement.clientHeight;
Evidence
outerWin is used as an iframe element and its document is accessed via
outerWin.contentWindow.document / outerDoc; there are no other references to outerWin.document
in the repo, so this new access is incorrect and will be undefined at runtime.

src/static/js/ace2_inner.ts[61-70]
src/static/js/ace2_inner.ts[2862-2871]
src/static/js/ace2_inner.ts[3459-3467]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`outerWin.document` is invalid because `outerWin` is the `ace_outer` iframe element. The PageUp/PageDown handler should derive viewport height from the iframe document (`outerDoc`) or reuse the existing `getInnerHeight()` helper.
### Issue Context
`outerWin` is obtained via `document.getElementsByName('ace_outer')[0]` and the codebase consistently uses `outerWin.contentWindow.document` (aliased as `outerDoc`) to access the iframe document.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2862-2871]
- src/static/js/ace2_inner.ts[61-70]
- src/static/js/ace2_inner.ts[3459-3467]
### Suggested change
Replace:
- `const viewportHeight = outerWin.document.documentElement.clientHeight;`
With one of:
- `const viewportHeight = outerDoc.documentElement.clientHeight;` (consistent with existing code), or
- `const viewportHeight = getInnerHeight();` (already handles hidden iframe cases).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Negative scroll delta possible🐞 Bug ☼ Reliability
Description
scrollAmount is computed as viewportHeight - 40 with no clamp, so a small/hidden viewport can
yield a negative value and invert PageUp/PageDown direction (PageUp scrolls down and PageDown
scrolls up). This can happen in the already-documented “iframe is hidden” case where height can be
0.
Code

src/static/js/ace2_inner.ts[R2868-2876]

+            // Keep a small overlap so the user doesn't lose context
+            const scrollAmount = viewportHeight - 40;
+            const currentScrollY = scroll.getScrollY();
+
+            if (isPageDown) {
+              scroll.setScrollY(currentScrollY + scrollAmount);
+            } else {
+              scroll.setScrollY(Math.max(0, currentScrollY - scrollAmount));
         }
Evidence
The handler directly subtracts 40px from the viewport height and then subtracts that amount for
PageUp; if the viewport height is ≤40 (including the hidden-iframe case documented in
getInnerHeight()), the computed scroll delta becomes negative and reverses scroll direction.

src/static/js/ace2_inner.ts[2867-2876]
src/static/js/ace2_inner.ts[3460-3467]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scrollAmount = viewportHeight - 40` can be negative when the viewport is small or hidden (height 0), which inverts PageUp/PageDown behavior.
### Issue Context
There is already code that anticipates hidden iframe sizing (returning 0 if no explicit px height is set). Page navigation should remain directionally correct even in these cases.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2867-2876]
- src/static/js/ace2_inner.ts[3460-3467]
### Suggested change
After computing `viewportHeight`, clamp:
- `const scrollAmount = Math.max(0, viewportHeight - 40);`
And optionally bail out if it is 0 (to avoid calling `setScrollY` with unchanged/inverted values).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Viewport height miscomputed 🐞 Bug ≡ Correctness
Description
The pixel-based page size uses getInnerHeight() as the viewport height, but the scroll module’s
viewport calculation subtracts editor offset/padding before determining what is visible. This
mismatch can inflate the computed viewport height and make PageUp/PageDown skip too many lines.
Code

src/static/js/ace2_inner.ts[R2870-2876]

+            // Calculate lines to skip based on viewport pixel height divided by
+            // the average rendered line height. This correctly handles long wrapped
+            // lines that consume multiple visual rows (fixes #4562).
+            const viewportHeight = getInnerHeight();
+            const visibleStart = newVisibleLineRange[0];
+            const visibleEnd = newVisibleLineRange[1];
+            let totalPixelHeight = 0;
Evidence
scroll.getVisibleLineRange() is based on _getViewPortTopBottom(), which uses
doc.documentElement.clientHeight minus editor position top and page-view padding. The new code’s
viewportHeight = getInnerHeight() uses outerDoc.documentElement.clientHeight without subtracting
those extras, so the height used in the scaling calculation is not the same height used to decide
which lines are visible.

src/static/js/scroll.ts[98-112]
src/static/js/scroll.ts[114-118]
src/static/js/ace2_inner.ts[3471-3478]
src/static/js/ace2_inner.ts[2866-2876]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The viewport height used for the PageUp/PageDown pixel math (`getInnerHeight()`) does not match the viewport height used by `scroll.getVisibleLineRange()` (which subtracts editor offset/padding). This can cause the computed page size to be larger than the actual visible area.
### Issue Context
- Visible range uses `Scroll._getViewPortTopBottom()`.
- New paging math uses `getInnerHeight()`.
### Fix
Compute `viewportHeight` using the same logic as the scroll module, e.g.:
- Add a public method on `Scroll` such as `getViewportHeight()` / `getViewportTopBottom()` and use `bottom - top`.
- (Prefer this over calling private methods.)
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2870-2887]
- src/static/js/scroll.ts[98-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Negative scroll delta possible🐞 Bug ☼ Reliability
Description
scrollAmount is computed as viewportHeight - 40 with no clamp, so a small/hidden viewport can
yield a negative value and invert PageUp/PageDown direction (PageUp scrolls down and PageDown
scrolls up). This can happen in the already-documented “iframe is hidden” case where height can be
0.
Code

src/static/js/ace2_inner.ts[R2868-2876]

+            // Keep a small overlap so the user doesn't lose context
+            const scrollAmount = viewportHeight - 40;
+            const currentScrollY = scroll.getScrollY();
+
+            if (isPageDown) {
+              scroll.setScrollY(currentScrollY + scrollAmount);
+            } else {
+              scroll.setScrollY(Math.max(0, currentScrollY - scrollAmount));
        }
Evidence
The handler directly subtracts 40px from the viewport height and then subtracts that amount for
PageUp; if the viewport height is ≤40 (including the hidden-iframe case documented in
getInnerHeight()), the computed scroll delta becomes negative and reverses scroll direction.

src/static/js/ace2_inner.ts[2867-2876]
src/static/js/ace2_inner.ts[3460-3467]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scrollAmount = viewportHeight - 40` can be negative when the viewport is small or hidden (height 0), which inverts PageUp/PageDown behavior.
### Issue Context
There is already code that anticipates hidden iframe sizing (returning 0 if no explicit px height is set). Page navigation should remain directionally correct even in these cases.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2867-2876]
- src/static/js/ace2_inner.ts[3460-3467]
### Suggested change
After computing `viewportHeight`, clamp:
- `const scrollAmount = Math.max(0, viewportHeight - 40);`
And optionally bail out if it is 0 (to avoid calling `setScrollY` with unchanged/inverted values).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

9. Unused topOffset local 🐞 Bug ⚙ Maintainability
Description
topOffset is computed from the old visible line range but never used within the PageUp/PageDown
handler. This dead local increases confusion and suggests leftover logic from the previous
implementation.
Code

src/static/js/ace2_inner.ts[R2862-2865]

+          const oldVisibleLineRange = scroll.getVisibleLineRange(rep);
+          let topOffset = rep.selStart[0] - oldVisibleLineRange[0];
+          if (topOffset < 0) topOffset = 0;
+
Evidence
Within the PageUp/PageDown key handler block, topOffset is assigned but there are no references to
it before the handler returns, so it has no effect.

src/static/js/ace2_inner.ts[2854-2910]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`topOffset` is computed but unused, making the handler harder to understand.
### Fix
Remove `oldVisibleLineRange`/`topOffset` if no longer needed, or (if intended) wire it into the paging behavior.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2862-2865]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread src/static/js/ace2_inner.ts Outdated
Comment thread src/static/js/ace2_inner.ts Outdated
JohnMcLear and others added 2 commits April 6, 2026 11:57
… PageDown/Up

outerWin is an HTMLIFrameElement (returned by getElementsByName), not a
Window object, so it has no .document property. The existing getInnerHeight()
helper already uses outerDoc.documentElement.clientHeight correctly; align
the PageDown/PageUp handler with that pattern.

Adds a Playwright regression test that verifies PageDown scrolls the
viewport when the pad contains long wrapping lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The previous approach tried to scroll the outerWin iframe element
directly which didn't work. Reverted to the original cursor-movement
approach but calculates lines-to-skip using viewport pixel height
divided by actual rendered line heights. This correctly handles long
wrapped lines that consume multiple visual rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JohnMcLear JohnMcLear marked this pull request as draft April 6, 2026 12:47
JohnMcLear and others added 3 commits April 6, 2026 16:10
Replace direct outerDoc.documentElement.clientHeight access with the
existing getInnerHeight() helper which handles edge cases (Opera browser,
hidden iframes). Add regression test for #4562 with consecutive long
wrapped lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The 'PageDown scrolls viewport when pad has long wrapping lines' test
was unreliable — scrollBefore and scrollAfter could be equal when the
viewport was already scrolled. The #4562 regression test below covers
the long-wrapped-lines scenario more robustly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use direct DOM manipulation instead of keyboard.type for creating long
wrapped lines. Typing 2500+ chars per line x10 lines exceeds the 90s
test timeout on Firefox.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@JohnMcLear JohnMcLear marked this pull request as ready for review April 6, 2026 20:05
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Fix PageDown/PageUp scrolling with long wrapped lines

🐞 Bug fix 🧪 Tests

Grey Divider

Walkthroughs

Description
• Fix PageDown/PageUp to scroll by viewport pixel height instead of logical line count
• Calculate lines-to-skip using actual rendered line heights for wrapped lines
• Add regression test for consecutive long wrapped lines (#4562)
• Simplify caret positioning logic and remove outdated comments
Diagram
flowchart LR
  A["PageDown/PageUp Event"] --> B["Calculate viewport height in pixels"]
  B --> C["Sum rendered line heights in viewport"]
  C --> D["Compute lines-to-skip ratio"]
  D --> E["Move cursor by calculated lines"]
  E --> F["Scroll to caret position"]
  G["Regression Test"] --> H["Create pad with long wrapped lines"]
  H --> I["Verify PageDown advances cursor correctly"]
Loading

Grey Divider

File Changes

1. src/static/js/ace2_inner.ts 🐞 Bug fix +24/-17

Implement pixel-based line calculation for PageDown/PageUp

• Replace logical line counting with pixel-based viewport height calculation
• Iterate through visible lines to sum actual rendered heights via offsetHeight
• Calculate numberOfLinesInViewport as ratio of viewport height to average line height
• Simplify caret positioning code by removing redundant comments and consolidating logic

src/static/js/ace2_inner.ts


2. src/tests/frontend-new/specs/page_up_down.spec.ts 🧪 Tests +60/-0

Add regression test for long wrapped lines

• Add regression test for #4562 with consecutive long wrapped lines
• Build test pad with alternating long and short lines via DOM manipulation
• Verify PageDown advances caret position correctly on first and second presses
• Use getCaretLine() helper to track caret movement across multiple PageDown events

src/tests/frontend-new/specs/page_up_down.spec.ts


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects bot commented Apr 6, 2026

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Off-by-one visible line count 🐞 Bug ≡ Correctness ⭐ New
Description
visibleLogicalLines is computed as visibleEnd - visibleStart even though
scroll.getVisibleLineRange() returns an inclusive end index. This undercounts visible logical
lines (and therefore the page increment) by 1 in the common case.
Code

src/static/js/ace2_inner.ts[R2874-2887]

+            const visibleStart = newVisibleLineRange[0];
+            const visibleEnd = newVisibleLineRange[1];
+            let totalPixelHeight = 0;
+            for (let i = visibleStart; i <= Math.min(visibleEnd, linesCount - 1); i++) {
+              const entry = rep.lines.atIndex(i);
+              if (entry && entry.lineNode) {
+                totalPixelHeight += entry.lineNode.offsetHeight;
+              }
+            }
+            const visibleLogicalLines = visibleEnd - visibleStart;
+            // Use pixel-based count: how many logical lines fit in one viewport
+            const numberOfLinesInViewport = visibleLogicalLines > 0 && totalPixelHeight > 0
+                ? Math.max(1, Math.round(visibleLogicalLines * viewportHeight / totalPixelHeight))
+                : Math.max(1, visibleLogicalLines);
Evidence
scroll.getVisibleLineRange() returns [start, end - 1], so the second element is the last visible
line index (inclusive). Therefore, the count of visible logical lines is `visibleEnd - visibleStart
+ 1, not visibleEnd - visibleStart` as used in the new computation.

src/static/js/scroll.ts[317-330]
src/static/js/ace2_inner.ts[2866-2887]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`scroll.getVisibleLineRange()` returns an inclusive end index, but `visibleLogicalLines` is calculated as an exclusive range. This causes PageUp/PageDown to move by too few logical lines.

### Issue Context
The returned range is `[start, endInclusive]` (see `return [start, end - 1]`).

### Fix
Adjust the visible logical line count to be inclusive:
- `const visibleLogicalLines = visibleEnd - visibleStart + 1;`
Also review any other math that assumes an exclusive end.

### Fix Focus Areas
- src/static/js/ace2_inner.ts[2873-2887]
- src/static/js/scroll.ts[317-330]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. No regression test for #4562📘 Rule violation ☼ Reliability
Description
This PR changes PageUp/PageDown scrolling/caret behavior but does not add an automated regression
test for the long wrapped-line scenario that triggered the bug. Without a targeted test, the issue
can reappear unnoticed in future refactors.
Code

src/static/js/ace2_inner.ts[R2862-2897]

+          if ((isPageDown && padShortcutEnabled.pageDown) ||
+              (isPageUp && padShortcutEnabled.pageUp)) {
+            // Scroll by actual viewport height in pixels, not by line count.
+            // This fixes the case where very long wrapped lines consume the
+            // entire viewport, making line-count-based scrolling useless.
+            const viewportHeight = outerWin.document.documentElement.clientHeight;
+            // Keep a small overlap so the user doesn't lose context
+            const scrollAmount = viewportHeight - 40;
+            const currentScrollY = scroll.getScrollY();
+
+            if (isPageDown) {
+              scroll.setScrollY(currentScrollY + scrollAmount);
+            } else {
+              scroll.setScrollY(Math.max(0, currentScrollY - scrollAmount));
         }
-            if (isPageDown && padShortcutEnabled.pageDown) {
-              rep.selStart[0] += numberOfLinesInViewport;
-              rep.selEnd[0] += numberOfLinesInViewport;
-            }
+            // Move cursor into the new visible area
+            scheduler.setTimeout(() => {
+              const linesCount = rep.lines.length();
+              const newVisibleRange = scroll.getVisibleLineRange(rep);
-            // clamp to valid line range
-            rep.selStart[0] = Math.max(0, Math.min(rep.selStart[0], linesCount - 1));
-            rep.selEnd[0] = Math.max(0, Math.min(rep.selEnd[0], linesCount - 1));
-            updateBrowserSelectionFromRep();
-            // get the current caret selection, can't use rep. here because that only gives
-            // us the start position not the current
-            const myselection = targetDoc.getSelection();
-            // get the carets selection offset in px IE 214
-            let caretOffsetTop = myselection.focusNode.parentNode.offsetTop ||
-                myselection.focusNode.offsetTop;
-
-            // sometimes the first selection is -1 which causes problems
-            // (Especially with ep_page_view)
-            // so use focusNode.offsetTop value.
-            if (caretOffsetTop === -1) caretOffsetTop = myselection.focusNode.offsetTop;
-            // set the scrollY offset of the viewport on the document
-            scroll.setScrollY(caretOffsetTop);
-          }, 200);
+              if (isPageDown) {
+                // Place cursor at the first line of the new viewport
+                rep.selStart[0] = newVisibleRange[0];
+                rep.selEnd[0] = newVisibleRange[0];
+              } else {
+                // Place cursor at the last line of the new viewport
+                rep.selEnd[0] = newVisibleRange[1];
+                rep.selStart[0] = newVisibleRange[1];
+              }
+
+              // clamp to valid line range
+              rep.selStart[0] = Math.max(0, Math.min(rep.selStart[0], linesCount - 1));
+              rep.selEnd[0] = Math.max(0, Math.min(rep.selEnd[0], linesCount - 1));
+              updateBrowserSelectionFromRep();
+            }, 0);
Evidence
PR Compliance ID 3 requires bug fixes to include a regression test that would fail without the fix.
The PR implements the PageUp/PageDown behavior change in src/static/js/ace2_inner.ts, but the
existing PageUp/PageDown Playwright test only covers many short lines and is not updated to cover
consecutive very long wrapped lines (the #4562 reproduction).

src/static/js/ace2_inner.ts[2854-2897]
src/tests/frontend-new/specs/page_up_down.spec.ts[12-49]
Best Practice: Repository guidelines

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This bug fix changes PageUp/PageDown behavior, but the change set does not include a regression test for the reported failure mode (consecutive very long wrapped lines at the top, and the "small edit changes wrapping" variant).
## Issue Context
There is an existing Playwright test for PageUp/PageDown, but it only uses many short lines and does not reproduce #4562 conditions (few logical lines that wrap into many visual lines).
## Fix Focus Areas
- src/tests/frontend-new/specs/page_up_down.spec.ts[1-123]
- src/static/js/ace2_inner.ts[2854-2897]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Bad viewport height access🐞 Bug ≡ Correctness
Description
The PageUp/PageDown handler reads outerWin.document.documentElement.clientHeight, but outerWin
is an iframe element (accessed via getElementsByName('ace_outer')[0]) and does not have a
.document property. This will throw at runtime on PageUp/PageDown, breaking paging and cursor
movement.
Code

src/static/js/ace2_inner.ts[2867]

+            const viewportHeight = outerWin.document.documentElement.clientHeight;
Evidence
outerWin is used as an iframe element and its document is accessed via
outerWin.contentWindow.document / outerDoc; there are no other references to outerWin.document
in the repo, so this new access is incorrect and will be undefined at runtime.

src/static/js/ace2_inner.ts[61-70]
src/static/js/ace2_inner.ts[2862-2871]
src/static/js/ace2_inner.ts[3459-3467]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`outerWin.document` is invalid because `outerWin` is the `ace_outer` iframe element. The PageUp/PageDown handler should derive viewport height from the iframe document (`outerDoc`) or reuse the existing `getInnerHeight()` helper.
### Issue Context
`outerWin` is obtained via `document.getElementsByName('ace_outer')[0]` and the codebase consistently uses `outerWin.contentWindow.document` (aliased as `outerDoc`) to access the iframe document.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2862-2871]
- src/static/js/ace2_inner.ts[61-70]
- src/static/js/ace2_inner.ts[3459-3467]
### Suggested change
Replace:
- `const viewportHeight = outerWin.document.documentElement.clientHeight;`
With one of:
- `const viewportHeight = outerDoc.documentElement.clientHeight;` (consistent with existing code), or
- `const viewportHeight = getInnerHeight();` (already handles hidden iframe cases).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Viewport height miscomputed 🐞 Bug ≡ Correctness ⭐ New
Description
The pixel-based page size uses getInnerHeight() as the viewport height, but the scroll module’s
viewport calculation subtracts editor offset/padding before determining what is visible. This
mismatch can inflate the computed viewport height and make PageUp/PageDown skip too many lines.
Code

src/static/js/ace2_inner.ts[R2870-2876]

+            // Calculate lines to skip based on viewport pixel height divided by
+            // the average rendered line height. This correctly handles long wrapped
+            // lines that consume multiple visual rows (fixes #4562).
+            const viewportHeight = getInnerHeight();
+            const visibleStart = newVisibleLineRange[0];
+            const visibleEnd = newVisibleLineRange[1];
+            let totalPixelHeight = 0;
Evidence
scroll.getVisibleLineRange() is based on _getViewPortTopBottom(), which uses
doc.documentElement.clientHeight minus editor position top and page-view padding. The new code’s
viewportHeight = getInnerHeight() uses outerDoc.documentElement.clientHeight without subtracting
those extras, so the height used in the scaling calculation is not the same height used to decide
which lines are visible.

src/static/js/scroll.ts[98-112]
src/static/js/scroll.ts[114-118]
src/static/js/ace2_inner.ts[3471-3478]
src/static/js/ace2_inner.ts[2866-2876]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The viewport height used for the PageUp/PageDown pixel math (`getInnerHeight()`) does not match the viewport height used by `scroll.getVisibleLineRange()` (which subtracts editor offset/padding). This can cause the computed page size to be larger than the actual visible area.

### Issue Context
- Visible range uses `Scroll._getViewPortTopBottom()`.
- New paging math uses `getInnerHeight()`.

### Fix
Compute `viewportHeight` using the same logic as the scroll module, e.g.:
- Add a public method on `Scroll` such as `getViewportHeight()` / `getViewportTopBottom()` and use `bottom - top`.
 - (Prefer this over calling private methods.)

### Fix Focus Areas
- src/static/js/ace2_inner.ts[2870-2887]
- src/static/js/scroll.ts[98-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Negative scroll delta possible🐞 Bug ☼ Reliability
Description
scrollAmount is computed as viewportHeight - 40 with no clamp, so a small/hidden viewport can
yield a negative value and invert PageUp/PageDown direction (PageUp scrolls down and PageDown
scrolls up). This can happen in the already-documented “iframe is hidden” case where height can be
0.
Code

src/static/js/ace2_inner.ts[R2868-2876]

+            // Keep a small overlap so the user doesn't lose context
+            const scrollAmount = viewportHeight - 40;
+            const currentScrollY = scroll.getScrollY();
+
+            if (isPageDown) {
+              scroll.setScrollY(currentScrollY + scrollAmount);
+            } else {
+              scroll.setScrollY(Math.max(0, currentScrollY - scrollAmount));
         }
Evidence
The handler directly subtracts 40px from the viewport height and then subtracts that amount for
PageUp; if the viewport height is ≤40 (including the hidden-iframe case documented in
getInnerHeight()), the computed scroll delta becomes negative and reverses scroll direction.

src/static/js/ace2_inner.ts[2867-2876]
src/static/js/ace2_inner.ts[3460-3467]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scrollAmount = viewportHeight - 40` can be negative when the viewport is small or hidden (height 0), which inverts PageUp/PageDown behavior.
### Issue Context
There is already code that anticipates hidden iframe sizing (returning 0 if no explicit px height is set). Page navigation should remain directionally correct even in these cases.
### Fix Focus Areas
- src/static/js/ace2_inner.ts[2867-2876]
- src/static/js/ace2_inner.ts[3460-3467]
### Suggested change
After computing `viewportHeight`, clamp:
- `const scrollAmount = Math.max(0, viewportHeight - 40);`
And optionally bail out if it is 0 (to avoid calling `setScrollY` with unchanged/inverted values).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Advisory comments

6. Unused topOffset local 🐞 Bug ⚙ Maintainability ⭐ New
Description
topOffset is computed from the old visible line range but never used within the PageUp/PageDown
handler. This dead local increases confusion and suggests leftover logic from the previous
implementation.
Code

src/static/js/ace2_inner.ts[R2862-2865]

+          const oldVisibleLineRange = scroll.getVisibleLineRange(rep);
+          let topOffset = rep.selStart[0] - oldVisibleLineRange[0];
+          if (topOffset < 0) topOffset = 0;
+
Evidence
Within the PageUp/PageDown key handler block, topOffset is assigned but there are no references to
it before the handler returns, so it has no effect.

src/static/js/ace2_inner.ts[2854-2910]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`topOffset` is computed but unused, making the handler harder to understand.

### Fix
Remove `oldVisibleLineRange`/`topOffset` if no longer needed, or (if intended) wire it into the paging behavior.

### Fix Focus Areas
- src/static/js/ace2_inner.ts[2862-2865]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread src/static/js/ace2_inner.ts
Addresses Qodo review: scroll.getVisibleLineRange() returns an inclusive
end index, but visibleLogicalLines was computed as (end - start), which
is one fewer than the logical lines actually summed into
totalPixelHeight. The viewport ratio therefore underestimated the page
increment by one line in the common case. Use (end - start + 1) to
match the loop bounds.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

CI Feedback 🧐

A test triggered by this PR failed. Here is an AI-generated analysis of the failure:

Action: with plugins (22)

Failed stage: Run the frontend admin tests [❌]

Failed test name: tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin

Failure summary:

The GitHub Action failed because the Playwright E2E test run (gnpm run test-admin) had 4 failing
tests, causing the job to exit with code 1 (ELIFECYCLE ... exit code 1).

Key failures:
- tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:
- Plugins page ›
Searches for a plugin failed (firefox) because the UI never showed the expected plugin
ep_font_color; the first row repeatedly contained admin_plugins.available_not-found instead
(assertion at
/home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:23:57).

- Plugins page › Attempt to Install and Uninstall a plugin failed (chromium + firefox):
-
chromium: installed plugins table row count was not the expected 2 (got 1 or 3) at
/home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:50:44.

- firefox: plugin row still showed admin_plugins.available_not-found instead of containing
ep_font_color at
/home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33.

- tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:
- Lists installed hooks failed
(firefox) because it expected the hooks text to contain express but received ep_font_colormain
(assertion at
/home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:37:38).

Additionally, there were repeated connection failures to http://localhost:9001/ during a pre-test
connectivity check (curl: (7) Failed to connect to localhost port 9001), indicating a service
expected on port 9001 was not available within the wait period; however, the immediate job failure
is due to the Playwright test assertions above.

Relevant error logs:
1:  ##[group]Runner Image Provisioner
2:  Hosted Compute Agent
...

1545:  �[36;1mcan_connect() {�[0m
1546:  �[36;1mcurl -sSfo /dev/null http://localhost:9001/ || return 1�[0m
1547:  �[36;1mconnected=true�[0m
1548:  �[36;1m}�[0m
1549:  �[36;1mnow() { date +%s; }�[0m
1550:  �[36;1mstart=$(now)�[0m
1551:  �[36;1mwhile [ $(($(now) - $start)) -le 15 ] && ! can_connect; do�[0m
1552:  �[36;1msleep 1�[0m
1553:  �[36;1mdone�[0m
1554:  �[36;1mcd src�[0m
1555:  �[36;1mgnpm run test-admin --runtimeVersion="22"�[0m
1556:  shell: /usr/bin/bash --noprofile --norc -e -o pipefail {0}
1557:  env:
1558:  PNPM_HOME: ~/.pnpm-store
1559:  ##[endgroup]
1560:  curl: (7) Failed to connect to localhost port 9001 after 0 ms: Couldn't connect to server
1561:  curl: (7) Failed to connect to localhost port 9001 after 0 ms: Couldn't connect to server
1562:  curl: (7) Failed to connect to localhost port 9001 after 0 ms: Couldn't connect to server
1563:  curl: (7) Failed to connect to localhost port 9001 after 0 ms: Couldn't connect to server
1564:  curl: (7) Failed to connect to localhost port 9001 after 0 ms: Couldn't connect to server
1565:  �[34mINFO�[0m	Using node as runtime
...

1587:  ✓  12 [firefox] › tests/frontend-new/admin-spec/adminsettings.spec.ts:11:7 › admin settings › Are Settings visible, populated, does save work (2.3s)
1588:  ✓  13 [firefox] › tests/frontend-new/admin-spec/adminsettings.spec.ts:50:7 › admin settings › restart works (2.3s)
1589:  ✓  14 [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:9:5 › Shows troubleshooting page manager (782ms)
1590:  ✓  15 [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:16:5 › Shows a version number (778ms)
1591:  ✓  16 [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:24:5 › Lists installed parts (803ms)
1592:  ✘  17 [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks (856ms)
1593:  ✘  18 [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks (retry #1) (2.1s)
1594:  ✘  19 [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks (retry #2) (1.7s)
1595:  ✓  20 [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:12:9 › Plugins page › List some plugins (1.6s)
1596:  ✘  21 [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin (1.0m)
1597:  ✘  22 [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin (retry #1) (1.0m)
1598:  ✘  23 [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin (retry #2) (1.0m)
1599:  ✘  24 [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin (1.0m)
1600:  ✘  25 [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin (retry #1) (1.1m)
1601:  ✘  26 [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin (retry #2) (1.0m)
1602:  ##[error]  1) [chromium] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1603:      Error: expect(locator).toHaveCount(expected) failed
1604:  
...

1612:        - waiting for locator('table tbody').first().locator('tr')
1613:          5 × locator resolved to 1 element
1614:            - unexpected value "1"
1615:          14 × locator resolved to 3 elements
1616:             - unexpected value "3"
1617:  
1618:  
1619:        48 |         const installedPlugins = page.locator('table tbody').first()
1620:        49 |         const installedPluginsRows = installedPlugins.locator('tr')
1621:      > 50 |         await expect(installedPluginsRows).toHaveCount(2, {
1622:           |                                            ^
1623:        51 |             timeout: 15000
1624:        52 |         })
1625:        53 |
1626:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:50:44
1627:  ##[error]  1) [chromium] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1628:  
1629:      Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
1630:      Error: expect(locator).toContainText(expected) failed
1631:  
...

1637:      Call log:
1638:        - Expect "toContainText" with timeout 60000ms
1639:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1640:          64 × locator resolved to <tr>…</tr>
1641:             - unexpected value "admin_plugins.available_not-found"
1642:  
1643:  
1644:        40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
1645:        41 |         const pluginRow = pluginTable.locator('tr').first()
1646:      > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
1647:           |                                 ^
1648:        43 |
1649:        44 |         // Select Installation button
1650:        45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
1651:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
1652:  ##[error]  1) [chromium] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1653:  
1654:      Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
1655:      Error: expect(locator).toContainText(expected) failed
1656:  
...

1662:      Call log:
1663:        - Expect "toContainText" with timeout 60000ms
1664:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1665:          63 × locator resolved to <tr>…</tr>
1666:             - unexpected value "admin_plugins.available_not-found"
1667:  
1668:  
1669:        40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
1670:        41 |         const pluginRow = pluginTable.locator('tr').first()
1671:      > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
1672:           |                                 ^
1673:        43 |
1674:        44 |         // Select Installation button
1675:        45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
1676:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
1677:  ##[error]  2) [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks 
1678:      Error: expect(received).toContain(expected) // indexOf
1679:  
1680:      Expected substring: "express"
1681:      Received string:    "ep_font_colormain"
1682:  
1683:        35 |   await page.waitForSelector('.innerwrapper ul')
1684:        36 |   const helper = page.locator('.innerwrapper ul').nth(2);
1685:      > 37 |   expect(await helper.textContent()).toContain('express');
1686:           |                                      ^
1687:        38 | });
1688:        39 |
1689:        40 |
1690:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:37:38
1691:  ##[error]  2) [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks 
1692:  
1693:      Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
1694:      Error: expect(received).toContain(expected) // indexOf
1695:  
1696:      Expected substring: "express"
1697:      Received string:    "ep_font_colormain"
1698:  
1699:        35 |   await page.waitForSelector('.innerwrapper ul')
1700:        36 |   const helper = page.locator('.innerwrapper ul').nth(2);
1701:      > 37 |   expect(await helper.textContent()).toContain('express');
1702:           |                                      ^
1703:        38 | });
1704:        39 |
1705:        40 |
1706:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:37:38
1707:  ##[error]  2) [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks 
1708:  
1709:      Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
1710:      Error: expect(received).toContain(expected) // indexOf
1711:  
1712:      Expected substring: "express"
1713:      Received string:    "ep_font_colormain"
1714:  
1715:        35 |   await page.waitForSelector('.innerwrapper ul')
1716:        36 |   const helper = page.locator('.innerwrapper ul').nth(2);
1717:      > 37 |   expect(await helper.textContent()).toContain('express');
1718:           |                                      ^
1719:        38 | });
1720:        39 |
1721:        40 |
1722:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:37:38
1723:  ##[error]  3) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin 
1724:      Error: expect(locator).toContainText(expected) failed
1725:  
...

1733:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1734:          4 × locator resolved to <tr>…</tr>
1735:            - unexpected value "ep_aa_file_menu_toolbarFile / Menu style toolbar0.2.162025-08-25admin_plugins.available_install.value"
1736:          59 × locator resolved to <tr>…</tr>
1737:             - unexpected value "admin_plugins.available_not-found"
1738:  
1739:  
1740:        21 |         await page.keyboard.type('ep_font_color')
1741:        22 |         const pluginTable =  page.locator('table tbody').nth(1);
1742:      > 23 |         await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color', {timeout: 60000})
1743:           |                                                         ^
1744:        24 |     })
1745:        25 |
1746:        26 |
1747:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:23:57
1748:  ##[error]  3) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin 
1749:  
1750:      Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
1751:      Error: expect(locator).toContainText(expected) failed
1752:  
...

1760:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1761:          4 × locator resolved to <tr>…</tr>
1762:            - unexpected value "ep_aa_file_menu_toolbarFile / Menu style toolbar0.2.162025-08-25admin_plugins.available_install.value"
1763:          59 × locator resolved to <tr>…</tr>
1764:             - unexpected value "admin_plugins.available_not-found"
1765:  
1766:  
1767:        21 |         await page.keyboard.type('ep_font_color')
1768:        22 |         const pluginTable =  page.locator('table tbody').nth(1);
1769:      > 23 |         await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color', {timeout: 60000})
1770:           |                                                         ^
1771:        24 |     })
1772:        25 |
1773:        26 |
1774:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:23:57
1775:  ##[error]  3) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin 
1776:  
1777:      Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
1778:      Error: expect(locator).toContainText(expected) failed
1779:  
...

1787:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1788:          4 × locator resolved to <tr>…</tr>
1789:            - unexpected value "ep_aa_file_menu_toolbarFile / Menu style toolbar0.2.162025-08-25admin_plugins.available_install.value"
1790:          59 × locator resolved to <tr>…</tr>
1791:             - unexpected value "admin_plugins.available_not-found"
1792:  
1793:  
1794:        21 |         await page.keyboard.type('ep_font_color')
1795:        22 |         const pluginTable =  page.locator('table tbody').nth(1);
1796:      > 23 |         await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color', {timeout: 60000})
1797:           |                                                         ^
1798:        24 |     })
1799:        25 |
1800:        26 |
1801:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:23:57
1802:  ##[error]  4) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1803:      Error: expect(locator).toContainText(expected) failed
1804:  
...

1810:      Call log:
1811:        - Expect "toContainText" with timeout 60000ms
1812:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1813:          63 × locator resolved to <tr>…</tr>
1814:             - unexpected value "admin_plugins.available_not-found"
1815:  
1816:  
1817:        40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
1818:        41 |         const pluginRow = pluginTable.locator('tr').first()
1819:      > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
1820:           |                                 ^
1821:        43 |
1822:        44 |         // Select Installation button
1823:        45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
1824:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
1825:  ##[error]  4) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1826:  
1827:      Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
1828:      Error: expect(locator).toContainText(expected) failed
1829:  
...

1835:      Call log:
1836:        - Expect "toContainText" with timeout 60000ms
1837:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1838:          63 × locator resolved to <tr>…</tr>
1839:             - unexpected value "admin_plugins.available_not-found"
1840:  
1841:  
1842:        40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
1843:        41 |         const pluginRow = pluginTable.locator('tr').first()
1844:      > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
1845:           |                                 ^
1846:        43 |
1847:        44 |         // Select Installation button
1848:        45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
1849:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
1850:  ##[error]  4) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1851:  
1852:      Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
1853:      Error: expect(locator).toContainText(expected) failed
1854:  
...

1860:      Call log:
1861:        - Expect "toContainText" with timeout 60000ms
1862:        - waiting for locator('table tbody').nth(1).locator('tr').first()
1863:          63 × locator resolved to <tr>…</tr>
1864:             - unexpected value "admin_plugins.available_not-found"
1865:  
1866:  
1867:        40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
1868:        41 |         const pluginRow = pluginTable.locator('tr').first()
1869:      > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
1870:           |                                 ^
1871:        43 |
1872:        44 |         // Select Installation button
1873:        45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
1874:          at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
1875:  ##[notice]  4 failed
1876:      [chromium] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1877:      [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks 
1878:      [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin 
1879:      [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1880:    14 passed (9.2m)
1881:  1) [chromium] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
1882:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoHaveCount�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
1883:  Locator:  locator('table tbody').first().locator('tr')
...

1887:  Call log:
1888:  �[2m  - Expect "toHaveCount" with timeout 15000ms�[22m
1889:  �[2m  - waiting for locator('table tbody').first().locator('tr')�[22m
1890:  �[2m    5 × locator resolved to 1 element�[22m
1891:  �[2m      - unexpected value "1"�[22m
1892:  �[2m    14 × locator resolved to 3 elements�[22m
1893:  �[2m       - unexpected value "3"�[22m
1894:  48 |         const installedPlugins = page.locator('table tbody').first()
1895:  49 |         const installedPluginsRows = installedPlugins.locator('tr')
1896:  > 50 |         await expect(installedPluginsRows).toHaveCount(2, {
1897:  |                                            ^
1898:  51 |             timeout: 15000
1899:  52 |         })
1900:  53 |
1901:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:50:44
1902:  Error Context: test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-chromium/error-context.md
1903:  Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
1904:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
1905:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

1910:  �[2m  - Expect "toContainText" with timeout 60000ms�[22m
1911:  �[2m  - waiting for locator('table tbody').nth(1).locator('tr').first()�[22m
1912:  �[2m    64 × locator resolved to <tr>…</tr>�[22m
1913:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
1914:  40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
1915:  41 |         const pluginRow = pluginTable.locator('tr').first()
1916:  > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
1917:  |                                 ^
1918:  43 |
1919:  44 |         // Select Installation button
1920:  45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
1921:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
1922:  attachment #1: video (video/webm) ──────────────────────────────────────────────────────────────
1923:  test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-chromium-retry1/video.webm
1924:  ────────────────────────────────────────────────────────────────────────────────────────────────
1925:  Error Context: test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-chromium-retry1/error-context.md
1926:  attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
1927:  test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-chromium-retry1/trace.zip
1928:  Usage:
1929:  pnpm exec playwright show-trace test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-chromium-retry1/trace.zip
1930:  ────────────────────────────────────────────────────────────────────────────────────────────────
1931:  Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
1932:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
1933:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

1935:  Received string:    �[31m"admin_plugins.available_not-found"�[39m
1936:  Timeout: 60000ms
1937:  Call log:
1938:  �[2m  - Expect "toContainText" with timeout 60000ms�[22m
1939:  �[2m  - waiting for locator('table tbody').nth(1).locator('tr').first()�[22m
1940:  �[2m    63 × locator resolved to <tr>…</tr>�[22m
1941:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
1942:  40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
1943:  41 |         const pluginRow = pluginTable.locator('tr').first()
1944:  > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
1945:  |                                 ^
1946:  43 |
1947:  44 |         // Select Installation button
1948:  45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
1949:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
1950:  Error Context: test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-chromium-retry2/error-context.md
1951:  2) [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks 
1952:  Error: �[2mexpect(�[22m�[31mreceived�[39m�[2m).�[22mtoContain�[2m(�[22m�[32mexpected�[39m�[2m) // indexOf�[22m
1953:  Expected substring: �[32m"express"�[39m
1954:  Received string:    �[31m"ep_font_colormain"�[39m
1955:  35 |   await page.waitForSelector('.innerwrapper ul')
1956:  36 |   const helper = page.locator('.innerwrapper ul').nth(2);
1957:  > 37 |   expect(await helper.textContent()).toContain('express');
1958:  |                                      ^
1959:  38 | });
1960:  39 |
1961:  40 |
1962:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:37:38
1963:  Error Context: test-results/admin-spec-admintroubleshooting-Lists-installed-hooks-firefox/error-context.md
1964:  Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
1965:  Error: �[2mexpect(�[22m�[31mreceived�[39m�[2m).�[22mtoContain�[2m(�[22m�[32mexpected�[39m�[2m) // indexOf�[22m
1966:  Expected substring: �[32m"express"�[39m
1967:  Received string:    �[31m"ep_font_colormain"�[39m
1968:  35 |   await page.waitForSelector('.innerwrapper ul')
1969:  36 |   const helper = page.locator('.innerwrapper ul').nth(2);
1970:  > 37 |   expect(await helper.textContent()).toContain('express');
1971:  |                                      ^
1972:  38 | });
1973:  39 |
1974:  40 |
1975:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:37:38
1976:  attachment #1: video (video/webm) ──────────────────────────────────────────────────────────────
1977:  test-results/admin-spec-admintroubleshooting-Lists-installed-hooks-firefox-retry1/video.webm
1978:  ────────────────────────────────────────────────────────────────────────────────────────────────
1979:  Error Context: test-results/admin-spec-admintroubleshooting-Lists-installed-hooks-firefox-retry1/error-context.md
1980:  attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
1981:  test-results/admin-spec-admintroubleshooting-Lists-installed-hooks-firefox-retry1/trace.zip
1982:  Usage:
1983:  pnpm exec playwright show-trace test-results/admin-spec-admintroubleshooting-Lists-installed-hooks-firefox-retry1/trace.zip
1984:  ────────────────────────────────────────────────────────────────────────────────────────────────
1985:  Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
1986:  Error: �[2mexpect(�[22m�[31mreceived�[39m�[2m).�[22mtoContain�[2m(�[22m�[32mexpected�[39m�[2m) // indexOf�[22m
1987:  Expected substring: �[32m"express"�[39m
1988:  Received string:    �[31m"ep_font_colormain"�[39m
1989:  35 |   await page.waitForSelector('.innerwrapper ul')
1990:  36 |   const helper = page.locator('.innerwrapper ul').nth(2);
1991:  > 37 |   expect(await helper.textContent()).toContain('express');
1992:  |                                      ^
1993:  38 | });
1994:  39 |
1995:  40 |
1996:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:37:38
1997:  Error Context: test-results/admin-spec-admintroubleshooting-Lists-installed-hooks-firefox-retry2/error-context.md
1998:  3) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin 
1999:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
2000:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

2004:  Call log:
2005:  �[2m  - Expect "toContainText" with timeout 60000ms�[22m
2006:  �[2m  - waiting for locator('table tbody').nth(1).locator('tr').first()�[22m
2007:  �[2m    4 × locator resolved to <tr>…</tr>�[22m
2008:  �[2m      - unexpected value "ep_aa_file_menu_toolbarFile / Menu style toolbar0.2.162025-08-25admin_plugins.available_install.value"�[22m
2009:  �[2m    59 × locator resolved to <tr>…</tr>�[22m
2010:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
2011:  21 |         await page.keyboard.type('ep_font_color')
2012:  22 |         const pluginTable =  page.locator('table tbody').nth(1);
2013:  > 23 |         await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color', {timeout: 60000})
2014:  |                                                         ^
2015:  24 |     })
2016:  25 |
2017:  26 |
2018:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:23:57
2019:  Error Context: test-results/admin-spec-adminupdateplug-9a044--page-Searches-for-a-plugin-firefox/error-context.md
2020:  Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
2021:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
2022:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

2029:  �[2m    4 × locator resolved to <tr>…</tr>�[22m
2030:  �[2m      - unexpected value "ep_aa_file_menu_toolbarFile / Menu style toolbar0.2.162025-08-25admin_plugins.available_install.value"�[22m
2031:  �[2m    59 × locator resolved to <tr>…</tr>�[22m
2032:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
2033:  21 |         await page.keyboard.type('ep_font_color')
2034:  22 |         const pluginTable =  page.locator('table tbody').nth(1);
2035:  > 23 |         await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color', {timeout: 60000})
2036:  |                                                         ^
2037:  24 |     })
2038:  25 |
2039:  26 |
2040:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:23:57
2041:  attachment #1: video (video/webm) ──────────────────────────────────────────────────────────────
2042:  test-results/admin-spec-adminupdateplug-9a044--page-Searches-for-a-plugin-firefox-retry1/video.webm
2043:  ────────────────────────────────────────────────────────────────────────────────────────────────
2044:  Error Context: test-results/admin-spec-adminupdateplug-9a044--page-Searches-for-a-plugin-firefox-retry1/error-context.md
2045:  attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
2046:  test-results/admin-spec-adminupdateplug-9a044--page-Searches-for-a-plugin-firefox-retry1/trace.zip
2047:  Usage:
2048:  pnpm exec playwright show-trace test-results/admin-spec-adminupdateplug-9a044--page-Searches-for-a-plugin-firefox-retry1/trace.zip
2049:  ────────────────────────────────────────────────────────────────────────────────────────────────
2050:  Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
2051:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
2052:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

2056:  Call log:
2057:  �[2m  - Expect "toContainText" with timeout 60000ms�[22m
2058:  �[2m  - waiting for locator('table tbody').nth(1).locator('tr').first()�[22m
2059:  �[2m    4 × locator resolved to <tr>…</tr>�[22m
2060:  �[2m      - unexpected value "ep_aa_file_menu_toolbarFile / Menu style toolbar0.2.162025-08-25admin_plugins.available_install.value"�[22m
2061:  �[2m    59 × locator resolved to <tr>…</tr>�[22m
2062:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
2063:  21 |         await page.keyboard.type('ep_font_color')
2064:  22 |         const pluginTable =  page.locator('table tbody').nth(1);
2065:  > 23 |         await expect(pluginTable.locator('tr').first()).toContainText('ep_font_color', {timeout: 60000})
2066:  |                                                         ^
2067:  24 |     })
2068:  25 |
2069:  26 |
2070:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:23:57
2071:  Error Context: test-results/admin-spec-adminupdateplug-9a044--page-Searches-for-a-plugin-firefox-retry2/error-context.md
2072:  4) [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
2073:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
2074:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

2076:  Received string:    �[31m"admin_plugins.available_not-found"�[39m
2077:  Timeout: 60000ms
2078:  Call log:
2079:  �[2m  - Expect "toContainText" with timeout 60000ms�[22m
2080:  �[2m  - waiting for locator('table tbody').nth(1).locator('tr').first()�[22m
2081:  �[2m    63 × locator resolved to <tr>…</tr>�[22m
2082:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
2083:  40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
2084:  41 |         const pluginRow = pluginTable.locator('tr').first()
2085:  > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
2086:  |                                 ^
2087:  43 |
2088:  44 |         // Select Installation button
2089:  45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
2090:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
2091:  Error Context: test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-firefox/error-context.md
2092:  Retry #1 ───────────────────────────────────────────────────────────────────────────────────────
2093:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
2094:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

2099:  �[2m  - Expect "toContainText" with timeout 60000ms�[22m
2100:  �[2m  - waiting for locator('table tbody').nth(1).locator('tr').first()�[22m
2101:  �[2m    63 × locator resolved to <tr>…</tr>�[22m
2102:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
2103:  40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
2104:  41 |         const pluginRow = pluginTable.locator('tr').first()
2105:  > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
2106:  |                                 ^
2107:  43 |
2108:  44 |         // Select Installation button
2109:  45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
2110:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
2111:  attachment #1: video (video/webm) ──────────────────────────────────────────────────────────────
2112:  test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-firefox-retry1/video.webm
2113:  ────────────────────────────────────────────────────────────────────────────────────────────────
2114:  Error Context: test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-firefox-retry1/error-context.md
2115:  attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
2116:  test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-firefox-retry1/trace.zip
2117:  Usage:
2118:  pnpm exec playwright show-trace test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-firefox-retry1/trace.zip
2119:  ────────────────────────────────────────────────────────────────────────────────────────────────
2120:  Retry #2 ───────────────────────────────────────────────────────────────────────────────────────
2121:  Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoContainText�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed
2122:  Locator: locator('table tbody').nth(1).locator('tr').first()
...

2124:  Received string:    �[31m"admin_plugins.available_not-found"�[39m
2125:  Timeout: 60000ms
2126:  Call log:
2127:  �[2m  - Expect "toContainText" with timeout 60000ms�[22m
2128:  �[2m  - waiting for locator('table tbody').nth(1).locator('tr').first()�[22m
2129:  �[2m    63 × locator resolved to <tr>…</tr>�[22m
2130:  �[2m       - unexpected value "admin_plugins.available_not-found"�[22m
2131:  40 |         await expect(pluginTable.locator('tr')).toHaveCount(1, {timeout: 60000})
2132:  41 |         const pluginRow = pluginTable.locator('tr').first()
2133:  > 42 |         await expect(pluginRow).toContainText('ep_font_color', {timeout: 60000})
2134:  |                                 ^
2135:  43 |
2136:  44 |         // Select Installation button
2137:  45 |         await pluginRow.locator('td').nth(4).locator('button').first().click()
2138:  at /home/runner/work/etherpad/etherpad/src/tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:42:33
2139:  Error Context: test-results/admin-spec-adminupdateplug-b2cba-tall-and-Uninstall-a-plugin-firefox-retry2/error-context.md
2140:  4 failed
2141:  [chromium] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
2142:  [firefox] › tests/frontend-new/admin-spec/admintroubleshooting.spec.ts:32:5 › Lists installed hooks 
2143:  [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:18:9 › Plugins page › Searches for a plugin 
2144:  [firefox] › tests/frontend-new/admin-spec/adminupdateplugins.spec.ts:27:9 › Plugins page › Attempt to Install and Uninstall a plugin 
2145:  14 passed (9.2m)
2146:  ELIFECYCLE  Command failed with exit code 1.
2147:  �[31mERROR�[0m	Error running command	{"error": "exit status 1"}
2148:  ##[error]Process completed with exit code 1.
2149:  ##[group]Run actions/upload-artifact@v7


const oldVisibleLineRange = scroll.getVisibleLineRange(rep);
let topOffset = rep.selStart[0] - oldVisibleLineRange[0];
if (topOffset < 0) topOffset = 0;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I like more brackets but I can understand both styles. Makes it more visible what is inside the if.

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.

Page down does not work on consecutive very long lines

2 participants