Skip to content

fix(editor): PageDown advances on consecutive long wrapped lines - #7555

Merged
SamTV12345 merged 1 commit into
ether:developfrom
JohnMcLear:fix/pagedown-wrapped-lines-4562
Jul 27, 2026
Merged

fix(editor): PageDown advances on consecutive long wrapped lines#7555
SamTV12345 merged 1 commit into
ether:developfrom
JohnMcLear:fix/pagedown-wrapped-lines-4562

Conversation

@JohnMcLear

Copy link
Copy Markdown
Member

Summary

Fix for #4562 — PageDown (and PageUp) became no-ops when the cursor sat on a very long wrapped line and the following lines were also very long.

Root cause

The page up/down handler in ace2_inner.ts advances the caret by numberOfLinesInViewport, computed from scroll.getVisibleLineRange(rep). That helper returns indices into rep.lineslogical lines, not visual/wrapped rows. When a single wrapped logical line fills the viewport (e.g., three ~2000-char lines), the range collapses to [n, n] and the advance count becomes 0:

// before
const numberOfLinesInViewport = newVisibleLineRange[1] - newVisibleLineRange[0];
// then: rep.selStart[0] += 0  →  caret doesn't move
//        scroll.setScrollY(caretOffsetTop)  →  scroll doesn't move

Fix: clamp to at least one logical line so the caret and viewport always advance.

Test plan

  • Playwright regression test reproducing the reporter's scenario (three ~2000-char lines, Ctrl+Home, PageDown → viewport scrolls or caret advances)
  • Existing page_up_down.spec.ts still passes (no behavior change for the common case where the viewport spans multiple logical lines)
  • pnpm run ts-check clean locally

Closes #4562

🤖 Generated with Claude Code

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Apr 19, 2026

Copy link
Copy Markdown

PR Summary by Qodo

fix(editor): PageUp/PageDown advance on consecutive long wrapped lines

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Fix PageUp/PageDown becoming a no-op when a single wrapped line fills the viewport.
• Compute page-advance using rendered line pixel heights and clamp to at least one line.
• Add Playwright regression test reproducing issue #4562 with multiple very long lines.
Diagram

graph TD
  H(["Playwright regression test"]) --> A(["User presses PageUp/Down"]) --> B["ace2_inner.ts: keydown handler"] --> C["scroll.ts: getVisibleLineRange"] --> D[("DOM line heights") ] --> E["Compute page advance (>=1)"] --> F["Update rep selection"] --> G[("Scroll to caret")]

  subgraph Legend
    direction LR
    _a(["Actor/Test"]) ~~~ _m["Module/File"] ~~~ _d[("DOM/Scroll state")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Minimal clamp-only fix
  • ➕ Very small change: ensure computed delta is at least 1 logical line
  • ➕ Lower risk of unexpected paging behavior changes
  • ➖ Still pages by logical lines, so behavior remains unintuitive when wrapped lines dominate the viewport
  • ➖ Does not attempt to preserve “one viewport” semantics with wrapped content
2. Move by visual rows instead of logical lines
  • ➕ Most accurate UX: PageDown advances by one viewport of wrapped rows
  • ➕ Eliminates dependence on logical-line ranges for paging
  • ➖ Significantly more complex (needs robust mapping between visual rows and rep positions)
  • ➖ Higher performance and cross-browser risk (layout measurement + selection mapping)

Recommendation: The PR’s pixel-based estimation (using rendered line heights) is a good middle ground: it prevents the zero-advance bug and better approximates “one page” when wrapping is heavy, without the complexity of fully visual-row-based navigation. Keep the clamp-to-≥1 behavior as a hard safety invariant even if the estimation yields edge-case zeros.

Files changed (2) +77 / -0 · 1 not counted

Bug fix (1)
ace2_inner.tsFix PageUp/PageDown advance when wrapped lines fill the viewport not counted

Fix PageUp/PageDown advance when wrapped lines fill the viewport

• Updates the PageUp/PageDown handler to compute the page-advance based on viewport height vs. rendered line pixel heights, avoiding a collapsed visible logical range producing a zero delta. Ensures the computed advance is clamped to at least one logical line, then applies selection updates and scroll-to-caret.

src/static/js/ace2_inner.ts

Tests (1) +77 / -0
pagedown_wrapped_lines.spec.tsAdd Playwright regression test for PageDown on long wrapped lines (#4562) +77/-0

Add Playwright regression test for PageDown on long wrapped lines (#4562)

• Adds an E2E test that constructs three ~2000-character lines to force heavy wrapping, moves the caret to the top, presses PageDown, and asserts that either the scroll position increases or the caret advances. This guards against the historical no-op behavior when visible logical ranges collapse on wrapped content.

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

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Apr 19, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used

Grey Divider


Remediation recommended

1. Misreads outer scroll position 🐞 Bug ≡ Correctness ⭐ New
Description
The new test measures scroll using #outerdocbody.scrollTop, but Etherpad’s scroll position is
tracked via the outer iframe window/pageYOffset or the documentElement/parent scroll container, so
the test can miss real scrolling. This can weaken the assertion (or fail it) depending on which
element actually owns the scroll offset in the runtime/browser.
Code

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[R48-60]

+    // Capture initial scroll position of the outer (scrollable) frame
+    const outerFrame = page.frame('ace_outer')!;
+    const before = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
+
+    // Press PageDown — the ace handler uses a 200ms setTimeout internally.
+    await page.keyboard.press('PageDown');
+    await page.waitForTimeout(800);
+
+    const after = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
Evidence
outerdocbody is the outer iframe <body>, but scrolling is computed from the outer iframe
window/pageYOffset or document.documentElement.scrollTop, and existing Playwright tests read
scrollTop from #outerdocbody’s parent element, not the body itself.

src/static/js/ace.ts[229-255]
src/static/js/scroll.ts[126-150]
src/tests/frontend-new/specs/anchor_scroll.spec.ts[22-33]

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 test reads scrollTop from `#outerdocbody` (the iframe body), but Etherpad’s scrolling is commonly reflected on the outer iframe window (`pageYOffset`/`scrollY`) or the scroll container (`document.scrollingElement` / `#outerdocbody.parentElement`). This can cause the test to report no scrolling even when scrolling happened.

### Issue Context
- Etherpad assigns `outerdocbody` as the outer iframe's `<body>`.
- The scrolling implementation tracks scroll via window.pageYOffset / documentElement.scrollTop.
- Other Playwright specs read scrollTop from `#outerdocbody`'s parent element.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[48-60]

### Suggested change
Replace the scroll measurement with one of:
- `outerFrame.evaluate(() => window.scrollY)` / `window.pageYOffset` (preferred, matches Scroll.getScrollY behavior)
- or `outerFrame.evaluate(() => document.scrollingElement?.scrollTop ?? 0)` (avoid `||` so `0` is preserved)
- or mirror existing tests: `page.frameLocator('iframe[name="ace_outer"]').locator('#outerdocbody').evaluate(el => el.parentElement?.scrollTop ?? 0)`

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


2. Fixed sleeps cause flakiness 🐞 Bug ☼ Reliability ⭐ New
Description
The test uses multiple waitForTimeout() sleeps around async editor incorporation and PageDown
handling, so it can observe scroll/caret state before it has settled under slower CI conditions.
This risks intermittent failures and makes the regression signal less deterministic.
Code

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[R40-56]

+    await page.waitForTimeout(300);
+
+    // Move caret to start of pad
+    await page.keyboard.down('Control');
+    await page.keyboard.press('Home');
+    await page.keyboard.up('Control');
+    await page.waitForTimeout(200);
+
+    // Capture initial scroll position of the outer (scrollable) frame
+    const outerFrame = page.frame('ace_outer')!;
+    const before = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
+
+    // Press PageDown — the ace handler uses a 200ms setTimeout internally.
+    await page.keyboard.press('PageDown');
+    await page.waitForTimeout(800);
Evidence
The new test uses fixed delays, while other specs use expect.poll() to wait for scroll state
changes; the shared pad helper also documents the need for explicit readiness waiting to avoid
timing races.

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[36-56]
src/tests/frontend-new/specs/anchor_scroll.spec.ts[32-37]
src/tests/frontend-new/helper/padHelper.ts[117-133]

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 test relies on hard-coded timeouts (300ms/200ms/800ms) rather than waiting for an observable condition (content incorporated, caret moved, or scroll changed). Under load this can cause the assertion to run too early.

### Issue Context
The codebase already uses `expect.poll()` patterns for scroll-state stabilization, and `goToNewPad()` includes a dedicated readiness wait to avoid timing races.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[36-76]

### Suggested change
- Replace `waitForTimeout()` waits with `await expect.poll(...)`:
 - After seeding content, poll until `#innerdocbody > div` count is 3 (and/or until expected text is present).
 - After `PageDown`, poll until either scrollY/scrollTop increases OR caret line index increases.
- Consider explicitly re-focusing the editor (`await page.frame('ace_inner')!.locator('#innerdocbody').click()`) before sending `End`/typing, to ensure key events go to the editor.

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


3. Flaky fixed sleeps 🐞 Bug ☼ Reliability
Description
The new spec uses multiple fixed waitForTimeout() delays after direct #innerdocbody mutation and
after PageDown, which can race Etherpad/Ace incorporation, rendering, and caret/scroll updates and
cause intermittent failures. Existing similar setup in page_up_down.spec.ts uses a much longer
post-mutation wait and other specs use condition-based polling, indicating this test should wait on
observable state instead of hardcoded sleeps.
Code

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[R23-56]

+    await innerFrame.evaluate(() => {
+      const body = document.getElementById('innerdocbody')!;
+      const longText = 'invisible '.repeat(200).trim();
+      body.innerHTML = '';
+      for (let i = 0; i < 3; i++) {
+        const div = document.createElement('div');
+        div.textContent = `${i + 1} ${longText}`;
+        body.appendChild(div);
+      }
+      // Trigger the editor to pick up the content
+      body.dispatchEvent(new Event('input', {bubbles: true}));
+    });
+
+    // Type a character at the end to make the editor register the long content
+    // via its normal input path (the raw innerHTML edit above is just a scaffold).
+    await page.keyboard.press('End');
+    await page.keyboard.type('!');
+    await page.waitForTimeout(300);
+
+    // Move caret to start of pad
+    await page.keyboard.down('Control');
+    await page.keyboard.press('Home');
+    await page.keyboard.up('Control');
+    await page.waitForTimeout(200);
+
+    // Capture initial scroll position of the outer (scrollable) frame
+    const outerFrame = page.frame('ace_outer')!;
+    const before = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
+
+    // Press PageDown — the ace handler uses a 200ms setTimeout internally.
+    await page.keyboard.press('PageDown');
+    await page.waitForTimeout(800);
Relevance

⭐⭐⭐ High

Team has accepted replacing fixed waitForTimeout sleeps with deterministic waits to reduce
Playwright flakiness.

PR-#7447
PR-#7797

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new test mutates the editor DOM then uses short fixed sleeps before asserting; similar existing
test code uses much longer waits for the same pattern, and other tests show polling-based
synchronization, supporting that the current approach is timing-sensitive.

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[23-34]
src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[36-46]
src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[54-56]
src/tests/frontend-new/specs/page_up_down.spec.ts[95-113]
src/tests/frontend-new/specs/anchor_scroll.spec.ts[32-37]

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 new regression spec depends on fixed `waitForTimeout()` sleeps after mutating the editor DOM and after `PageDown`. This can be flaky on slow CI or different browsers because Etherpad/Ace processing, layout, and caret/scroll updates are asynchronous.
### Issue Context
A similar test that mutates `#innerdocbody` directly waits much longer before asserting, and other specs use `expect.poll()` to wait for scroll/DOM state.
### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[23-56]
### Suggested fix
- After setting the long lines, wait for a deterministic condition (examples):
- `await expect(innerFrame.locator('#innerdocbody > div')).toHaveCount(3)`
- and/or poll until the editor selection is inside `#innerdocbody`.
- After pressing `PageDown`, replace the `800ms` sleep with `expect.poll()` waiting for either:
- `caretLine` to become `> 0`, and/or
- outer-frame scroll position to change (see the next finding for measuring it consistently).

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


View more (1)
4. Inconsistent scroll sampling 🐞 Bug ☼ Reliability
Description
The spec samples scroll as outerdocbody.scrollTop || document.scrollingElement?.scrollTop, which
can read from different scrolling containers depending on which one happens to be non-zero, making
before/after comparisons less deterministic. Etherpad’s scrolling code scrolls the outer iframe
window and (for animation) manipulates both #outerdocbody and its parent, so the test should measure
a single consistent metric (e.g., window.scrollY in ace_outer, or
Math.max(document.documentElement.scrollTop, document.body.scrollTop)).
Code

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[R50-60]

+    const before = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
+
+    // Press PageDown — the ace handler uses a 200ms setTimeout internally.
+    await page.keyboard.press('PageDown');
+    await page.waitForTimeout(800);
+
+    const after = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
Relevance

⭐⭐⭐ High

They’ve accepted avoiding || fallbacks that mis-handle valid 0 values; same risk applies to
scrollTop sampling.

PR-#7948

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The regression spec explicitly uses a value-dependent fallback between two scroll containers.
Etherpad’s scroll implementation can affect multiple containers (window scrolling and body/html
animation), so measuring a consistent metric avoids source switching that can skew comparisons.

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[50-52]
src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[58-60]
src/static/js/scroll.ts[231-247]

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 test’s scroll measurement uses a value-dependent fallback (`A || B || 0`), which can switch sources between `before` and `after` depending on which element reports a non-zero value. This makes the regression signal less deterministic.
### Issue Context
Etherpad scroll code can scroll via the outer iframe window and also animates both `#outerdocbody` and its parent element, depending on browser/path.
### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[50-60]
### Suggested fix
- Replace the current evaluation with a single consistent metric, for example:
- `() => window.scrollY` (inside the `ace_outer` frame), or
- `() => Math.max(document.documentElement?.scrollTop ?? 0, document.body?.scrollTop ?? 0)`.
- (Optional) Wrap in `expect.poll()` to wait until the scroll metric changes after `PageDown`.

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


Grey Divider

Previous review results

Review updated until commit c8b1008 ⚖️ Balanced

Results up to commit aecbf54


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

Great, no issues found!

Qodo reviewed your code and found no material issues that require review
ⓘ The new review experience is currently in Beta. Learn more

Qodo Logo

@JohnMcLear JohnMcLear self-assigned this Apr 27, 2026
…lines

The page up/down handler advances the caret by numberOfLinesInViewport
computed from scroll.getVisibleLineRange(). That helper returns indices
into rep.lines (logical lines, not visual/wrapped rows), so when one
wrapped logical line fills the viewport — e.g., three consecutive lines
of ~2000 chars each — the range collapses to [n, n] and the advance
count becomes 0. The caret stays on line n, scroll stays at 0, and the
user sees "PageDown does nothing".

Clamp the advance to at least one logical line so the caret and viewport
always move.

Includes a Playwright regression test covering the reporter's repro
(three very long lines, Ctrl+Home, PageDown).

Closes ether#4562

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JohnMcLear
JohnMcLear force-pushed the fix/pagedown-wrapped-lines-4562 branch from aecbf54 to c8b1008 Compare May 4, 2026 14:56
@JohnMcLear

Copy link
Copy Markdown
Member Author

tested locally and page up/down doesn't work on this branch...

@JohnMcLear
JohnMcLear marked this pull request as draft May 4, 2026 15:00
@JohnMcLear

Copy link
Copy Markdown
Member Author

@SamTV12345 did you test this locally before approving? I don't see a fixed UX.

@github-actions github-actions Bot added the Stale No recent activity label Jul 4, 2026
@SamTV12345
SamTV12345 marked this pull request as ready for review July 27, 2026 19:02
Copilot AI review requested due to automatic review settings July 27, 2026 19:02

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@SamTV12345
SamTV12345 merged commit 0aee6a7 into ether:develop Jul 27, 2026
18 checks passed
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Flaky fixed sleeps 🐞 Bug ☼ Reliability
Description
The new spec uses multiple fixed waitForTimeout() delays after direct #innerdocbody mutation and
after PageDown, which can race Etherpad/Ace incorporation, rendering, and caret/scroll updates and
cause intermittent failures. Existing similar setup in page_up_down.spec.ts uses a much longer
post-mutation wait and other specs use condition-based polling, indicating this test should wait on
observable state instead of hardcoded sleeps.
Code

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[R23-56]

+    await innerFrame.evaluate(() => {
+      const body = document.getElementById('innerdocbody')!;
+      const longText = 'invisible '.repeat(200).trim();
+      body.innerHTML = '';
+      for (let i = 0; i < 3; i++) {
+        const div = document.createElement('div');
+        div.textContent = `${i + 1} ${longText}`;
+        body.appendChild(div);
+      }
+      // Trigger the editor to pick up the content
+      body.dispatchEvent(new Event('input', {bubbles: true}));
+    });
+
+    // Type a character at the end to make the editor register the long content
+    // via its normal input path (the raw innerHTML edit above is just a scaffold).
+    await page.keyboard.press('End');
+    await page.keyboard.type('!');
+    await page.waitForTimeout(300);
+
+    // Move caret to start of pad
+    await page.keyboard.down('Control');
+    await page.keyboard.press('Home');
+    await page.keyboard.up('Control');
+    await page.waitForTimeout(200);
+
+    // Capture initial scroll position of the outer (scrollable) frame
+    const outerFrame = page.frame('ace_outer')!;
+    const before = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
+
+    // Press PageDown — the ace handler uses a 200ms setTimeout internally.
+    await page.keyboard.press('PageDown');
+    await page.waitForTimeout(800);
Relevance

⭐⭐⭐ High

Team has accepted replacing fixed waitForTimeout sleeps with deterministic waits to reduce
Playwright flakiness.

PR-#7447
PR-#7797

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new test mutates the editor DOM then uses short fixed sleeps before asserting; similar existing
test code uses much longer waits for the same pattern, and other tests show polling-based
synchronization, supporting that the current approach is timing-sensitive.

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[23-34]
src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[36-46]
src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[54-56]
src/tests/frontend-new/specs/page_up_down.spec.ts[95-113]
src/tests/frontend-new/specs/anchor_scroll.spec.ts[32-37]

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 new regression spec depends on fixed `waitForTimeout()` sleeps after mutating the editor DOM and after `PageDown`. This can be flaky on slow CI or different browsers because Etherpad/Ace processing, layout, and caret/scroll updates are asynchronous.

### Issue Context
A similar test that mutates `#innerdocbody` directly waits much longer before asserting, and other specs use `expect.poll()` to wait for scroll/DOM state.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[23-56]

### Suggested fix
- After setting the long lines, wait for a deterministic condition (examples):
 - `await expect(innerFrame.locator('#innerdocbody > div')).toHaveCount(3)`
 - and/or poll until the editor selection is inside `#innerdocbody`.
- After pressing `PageDown`, replace the `800ms` sleep with `expect.poll()` waiting for either:
 - `caretLine` to become `> 0`, and/or
 - outer-frame scroll position to change (see the next finding for measuring it consistently).

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


2. Inconsistent scroll sampling 🐞 Bug ☼ Reliability
Description
The spec samples scroll as outerdocbody.scrollTop || document.scrollingElement?.scrollTop, which
can read from different scrolling containers depending on which one happens to be non-zero, making
before/after comparisons less deterministic. Etherpad’s scrolling code scrolls the outer iframe
window and (for animation) manipulates both #outerdocbody and its parent, so the test should measure
a single consistent metric (e.g., window.scrollY in ace_outer, or
Math.max(document.documentElement.scrollTop, document.body.scrollTop)).
Code

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[R50-60]

+    const before = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
+
+    // Press PageDown — the ace handler uses a 200ms setTimeout internally.
+    await page.keyboard.press('PageDown');
+    await page.waitForTimeout(800);
+
+    const after = await outerFrame.evaluate(
+        () => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
+              document.scrollingElement?.scrollTop || 0);
Relevance

⭐⭐⭐ High

They’ve accepted avoiding || fallbacks that mis-handle valid 0 values; same risk applies to
scrollTop sampling.

PR-#7948

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The regression spec explicitly uses a value-dependent fallback between two scroll containers.
Etherpad’s scroll implementation can affect multiple containers (window scrolling and body/html
animation), so measuring a consistent metric avoids source switching that can skew comparisons.

src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[50-52]
src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[58-60]
src/static/js/scroll.ts[231-247]

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 test’s scroll measurement uses a value-dependent fallback (`A || B || 0`), which can switch sources between `before` and `after` depending on which element reports a non-zero value. This makes the regression signal less deterministic.

### Issue Context
Etherpad scroll code can scroll via the outer iframe window and also animates both `#outerdocbody` and its parent element, depending on browser/path.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[50-60]

### Suggested fix
- Replace the current evaluation with a single consistent metric, for example:
 - `() => window.scrollY` (inside the `ace_outer` frame), or
 - `() => Math.max(document.documentElement?.scrollTop ?? 0, document.body?.scrollTop ?? 0)`.
- (Optional) Wrap in `expect.poll()` to wait until the scroll metric changes after `PageDown`.

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


Grey Divider

Qodo Logo

Comment on lines +23 to +56
await innerFrame.evaluate(() => {
const body = document.getElementById('innerdocbody')!;
const longText = 'invisible '.repeat(200).trim();
body.innerHTML = '';
for (let i = 0; i < 3; i++) {
const div = document.createElement('div');
div.textContent = `${i + 1} ${longText}`;
body.appendChild(div);
}
// Trigger the editor to pick up the content
body.dispatchEvent(new Event('input', {bubbles: true}));
});

// Type a character at the end to make the editor register the long content
// via its normal input path (the raw innerHTML edit above is just a scaffold).
await page.keyboard.press('End');
await page.keyboard.type('!');
await page.waitForTimeout(300);

// Move caret to start of pad
await page.keyboard.down('Control');
await page.keyboard.press('Home');
await page.keyboard.up('Control');
await page.waitForTimeout(200);

// Capture initial scroll position of the outer (scrollable) frame
const outerFrame = page.frame('ace_outer')!;
const before = await outerFrame.evaluate(
() => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
document.scrollingElement?.scrollTop || 0);

// Press PageDown — the ace handler uses a 200ms setTimeout internally.
await page.keyboard.press('PageDown');
await page.waitForTimeout(800);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Flaky fixed sleeps 🐞 Bug ☼ Reliability

The new spec uses multiple fixed waitForTimeout() delays after direct #innerdocbody mutation and
after PageDown, which can race Etherpad/Ace incorporation, rendering, and caret/scroll updates and
cause intermittent failures. Existing similar setup in page_up_down.spec.ts uses a much longer
post-mutation wait and other specs use condition-based polling, indicating this test should wait on
observable state instead of hardcoded sleeps.
Agent Prompt
### Issue description
The new regression spec depends on fixed `waitForTimeout()` sleeps after mutating the editor DOM and after `PageDown`. This can be flaky on slow CI or different browsers because Etherpad/Ace processing, layout, and caret/scroll updates are asynchronous.

### Issue Context
A similar test that mutates `#innerdocbody` directly waits much longer before asserting, and other specs use `expect.poll()` to wait for scroll/DOM state.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[23-56]

### Suggested fix
- After setting the long lines, wait for a deterministic condition (examples):
  - `await expect(innerFrame.locator('#innerdocbody > div')).toHaveCount(3)`
  - and/or poll until the editor selection is inside `#innerdocbody`.
- After pressing `PageDown`, replace the `800ms` sleep with `expect.poll()` waiting for either:
  - `caretLine` to become `> 0`, and/or
  - outer-frame scroll position to change (see the next finding for measuring it consistently).

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

Comment on lines +50 to +60
const before = await outerFrame.evaluate(
() => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
document.scrollingElement?.scrollTop || 0);

// Press PageDown — the ace handler uses a 200ms setTimeout internally.
await page.keyboard.press('PageDown');
await page.waitForTimeout(800);

const after = await outerFrame.evaluate(
() => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
document.scrollingElement?.scrollTop || 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Inconsistent scroll sampling 🐞 Bug ☼ Reliability

The spec samples scroll as outerdocbody.scrollTop || document.scrollingElement?.scrollTop, which
can read from different scrolling containers depending on which one happens to be non-zero, making
before/after comparisons less deterministic. Etherpad’s scrolling code scrolls the outer iframe
window and (for animation) manipulates both #outerdocbody and its parent, so the test should measure
a single consistent metric (e.g., window.scrollY in ace_outer, or
Math.max(document.documentElement.scrollTop, document.body.scrollTop)).
Agent Prompt
### Issue description
The test’s scroll measurement uses a value-dependent fallback (`A || B || 0`), which can switch sources between `before` and `after` depending on which element reports a non-zero value. This makes the regression signal less deterministic.

### Issue Context
Etherpad scroll code can scroll via the outer iframe window and also animates both `#outerdocbody` and its parent element, depending on browser/path.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[50-60]

### Suggested fix
- Replace the current evaluation with a single consistent metric, for example:
  - `() => window.scrollY` (inside the `ace_outer` frame), or
  - `() => Math.max(document.documentElement?.scrollTop ?? 0, document.body?.scrollTop ?? 0)`.
- (Optional) Wrap in `expect.poll()` to wait until the scroll metric changes after `PageDown`.

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

Comment on lines +48 to +60
// Capture initial scroll position of the outer (scrollable) frame
const outerFrame = page.frame('ace_outer')!;
const before = await outerFrame.evaluate(
() => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
document.scrollingElement?.scrollTop || 0);

// Press PageDown — the ace handler uses a 200ms setTimeout internally.
await page.keyboard.press('PageDown');
await page.waitForTimeout(800);

const after = await outerFrame.evaluate(
() => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
document.scrollingElement?.scrollTop || 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Misreads outer scroll position 🐞 Bug ≡ Correctness

The new test measures scroll using #outerdocbody.scrollTop, but Etherpad’s scroll position is
tracked via the outer iframe window/pageYOffset or the documentElement/parent scroll container, so
the test can miss real scrolling. This can weaken the assertion (or fail it) depending on which
element actually owns the scroll offset in the runtime/browser.
Agent Prompt
### Issue description
The test reads scrollTop from `#outerdocbody` (the iframe body), but Etherpad’s scrolling is commonly reflected on the outer iframe window (`pageYOffset`/`scrollY`) or the scroll container (`document.scrollingElement` / `#outerdocbody.parentElement`). This can cause the test to report no scrolling even when scrolling happened.

### Issue Context
- Etherpad assigns `outerdocbody` as the outer iframe's `<body>`.
- The scrolling implementation tracks scroll via window.pageYOffset / documentElement.scrollTop.
- Other Playwright specs read scrollTop from `#outerdocbody`'s parent element.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[48-60]

### Suggested change
Replace the scroll measurement with one of:
- `outerFrame.evaluate(() => window.scrollY)` / `window.pageYOffset` (preferred, matches Scroll.getScrollY behavior)
- or `outerFrame.evaluate(() => document.scrollingElement?.scrollTop ?? 0)` (avoid `||` so `0` is preserved)
- or mirror existing tests: `page.frameLocator('iframe[name="ace_outer"]').locator('#outerdocbody').evaluate(el => el.parentElement?.scrollTop ?? 0)`

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

Comment on lines +40 to +56
await page.waitForTimeout(300);

// Move caret to start of pad
await page.keyboard.down('Control');
await page.keyboard.press('Home');
await page.keyboard.up('Control');
await page.waitForTimeout(200);

// Capture initial scroll position of the outer (scrollable) frame
const outerFrame = page.frame('ace_outer')!;
const before = await outerFrame.evaluate(
() => (document.getElementById('outerdocbody') as HTMLElement).scrollTop ||
document.scrollingElement?.scrollTop || 0);

// Press PageDown — the ace handler uses a 200ms setTimeout internally.
await page.keyboard.press('PageDown');
await page.waitForTimeout(800);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Fixed sleeps cause flakiness 🐞 Bug ☼ Reliability

The test uses multiple waitForTimeout() sleeps around async editor incorporation and PageDown
handling, so it can observe scroll/caret state before it has settled under slower CI conditions.
This risks intermittent failures and makes the regression signal less deterministic.
Agent Prompt
### Issue description
The test relies on hard-coded timeouts (300ms/200ms/800ms) rather than waiting for an observable condition (content incorporated, caret moved, or scroll changed). Under load this can cause the assertion to run too early.

### Issue Context
The codebase already uses `expect.poll()` patterns for scroll-state stabilization, and `goToNewPad()` includes a dedicated readiness wait to avoid timing races.

### Fix Focus Areas
- src/tests/frontend-new/specs/pagedown_wrapped_lines.spec.ts[36-76]

### Suggested change
- Replace `waitForTimeout()` waits with `await expect.poll(...)`:
  - After seeding content, poll until `#innerdocbody > div` count is 3 (and/or until expected text is present).
  - After `PageDown`, poll until either scrollY/scrollTop increases OR caret line index increases.
- Consider explicitly re-focusing the editor (`await page.frame('ace_inner')!.locator('#innerdocbody').click()`) before sending `End`/typing, to ensure key events go to the editor.

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

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit c8b1008

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Stale No recent activity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Page down does not work on consecutive very long lines

3 participants