Skip to content

test: read Monaco content from the model, not the DOM - #3467

Closed
opensource-joe wants to merge 1 commit into
apache:masterfrom
opensource-joe:test/monaco-poll
Closed

opensource-joe wants to merge 1 commit into
apache:masterfrom
opensource-joe:test/monaco-poll

Conversation

@opensource-joe

Copy link
Copy Markdown

Please answer these questions before submitting a pull request, or your PR will get closed.

Why submit this pull request?

  • Bugfix

What changes will this PR take into?

Fixes #3466. Test-only change, no src/ code touched.

The diagnosis changed once I could measure it

I originally suggested in #3466 that the helper should poll until its content parses. That treats it as a race to wait out. Measuring it showed the simpler story: the helper is reading sources that never hold the document.

plugin_metadata.crud-all-fields.spec.ts defined its own local getMonacoEditorValue that tried the hidden textarea first and fell back to joining .view-line elements. Neither is a source of editor content:

  • the hidden textarea holds only a small buffer around the cursor, for IME purposes, never the whole document
  • .view-line elements are virtualised, so only painted lines exist

Sampling all three sources at the instant the Edit Plugin drawer becomes visible, for a 219-character config:

t+0ms     textarea=   0 ""    viewLines= 1 "{"                 model=219
t+50ms    textarea=   0 ""    viewLines=11 "{ \"log_format\"…"   model=219
t+150ms   textarea=   0 ""    viewLines=11 "{ \"log_format\"…"   model=219
t+1000ms  textarea=   0 ""    viewLines=11 "{ \"log_format\"…"   model=219

Two things fall out of that table:

  1. The textarea is empty at every offset, so the first path never contributed anything and the helper always fell through to the fallback.
  2. The fallback returns exactly { until the editor paints, because the first view-line of the JSON is the opening brace. That is precisely the Received string: "{" in the failure.

The model is already complete at t+0. There was never a race worth waiting out, just the wrong source being read. The paint window is roughly 50ms wide, which is why this reproduces on a loaded machine and passes on an idle one.

The fix

Add uiGetMonacoEditorValue next to the existing Monaco helpers and read window.__monacoEditor__.getModel().getValue().

That is not a new mechanism. It is the same source uiFillMonacoEditor and uiClearMonacoEditor already write to, and the same one plugin-metadata.drawer-keeps-edits-on-failed-save.spec.ts already reads. The flaky spec was the outlier for having a private DOM-scraping helper at all.

It polls for non-empty content rather than reading once, because the drawer mounts its editor asynchronously and window.__monacoEditor__ can briefly still point at a previously mounted instance after a new drawer opens.

routes.empty-plugin-config.spec.ts had the same defect, reading .view-lines innerText. Its comment there ("Read Monaco editor content from the visible lines (not the textarea)") shows the textarea problem was already known, and the workaround reached for the other DOM source instead of the model. Converted as well, so the pattern does not survive anywhere in e2e/.

Verification

Against a live APISIX instance from e2e/server:

  • the affected specs (plugin_metadata.crud-all-fields, plugin_metadata.crud-required-fields, plugin_metadata.list, routes.empty-plugin-config) pass 24/24 across 4 repeats
  • plugin_metadata.crud-all-fields alone passes 10/10 with --repeat-each=10
  • pnpm lint clean at --max-warnings=0, tsc -b clean

One honest caveat on the before/after. I cannot show a clean red-to-green locally, because on an idle machine the unpatched spec also passes (10/10 when I tried). The failure needs the paint to lose a roughly 50ms window, which happens under load. So the evidence that this is the right fix is the sampling table above, which is deterministic and reproducible, rather than a flip in pass rate. I also tried forcing it with CDP Emulation.setCPUThrottlingRate; that does not work, because throttling slows the test driver along with the renderer and the relative timing barely moves.

For the same reason, the earlier failure rates I quoted in #3466 (4 of 6 on master) were measured while the machine was busy and should be read as "this fails regularly under load", not as a stable rate.

Related issues

fixes #3466

Checklist:

  • Did you explain what problem does this PR solve? Or what new features have been added?
  • Have you added corresponding test cases? (this is a test-only change; it repairs existing coverage rather than adding new)
  • Have you modified the corresponding document? (no document covers e2e helpers)
  • Is this PR backward compatible?

)

`plugin_metadata.crud-all-fields` was flaky on its final assertion,
failing with `Received string: "{"` when reading the editor back after a
save. It is a test-only race, and CI hides it because
`playwright.config.ts` sets `retries: process.env.CI ? 2 : 0`.

The spec had its own local `getMonacoEditorValue` that read the DOM
through two paths, and neither is a reliable source for editor content:

- the hidden `textarea` holds only a small buffer around the cursor for
  IME purposes, never the whole document
- `.view-line` elements are virtualised, so only painted lines exist

Sampling all three sources at the instant the drawer becomes visible,
for a 219-character config:

    t+0ms     textarea=   0 ""    viewLines= 1 "{"                model=219
    t+50ms    textarea=   0 ""    viewLines=11 "{ \"log_format\"…"  model=219
    t+1000ms  textarea=   0 ""    viewLines=11 "{ \"log_format\"…"  model=219

The textarea is empty at every offset, so the helper always fell through
to the `.view-line` path, and that path returns exactly `{` until the
editor paints. That is the observed failure value. The window is roughly
50ms wide, which is why it reproduces under load and passes on an idle
machine.

The model, meanwhile, is complete at t+0. There is no race to wait out,
only the wrong source being read.

Add `uiGetMonacoEditorValue` alongside the existing helpers and read
`window.__monacoEditor__.getModel().getValue()`, which is the same
source `uiFillMonacoEditor` and `uiClearMonacoEditor` already write to,
and which `plugin-metadata.drawer-keeps-edits-on-failed-save` already
reads. It polls for non-empty content because the drawer mounts its
editor asynchronously and the global can briefly still point at a
previously mounted instance.

`routes.empty-plugin-config` had the same defect, reading `.view-lines`
innerText. Its comment ("not the textarea") shows the textarea problem
was already known; the fix reached for the other DOM source rather than
the model. Converted too, so the pattern does not survive anywhere.

Verified against a live APISIX instance: the affected specs pass 24/24
across 4 repeats, and lint and typecheck are clean.
@opensource-joe

opensource-joe commented Aug 15, 2026

Copy link
Copy Markdown
Author

Withdrawn: this PR is closed and its commit is folded into #3465. @LiteSun please disregard the workflow-approval ask below; it applies to #3465 only, where this change now lives as its own commit. Apologies for the extra notification.

Leaving the rationale below, since it is the reasoning behind that commit.


This is worth running properly because it is a test-only change and CI is the point of it. plugin_metadata.crud-all-fields.spec.ts reads Monaco content back from the DOM, which fails intermittently under machine load on master as well as on the branch, so it is pre-existing rather than anything introduced here. It is timing-sensitive rather than a fixed rate: on a busy machine it failed 4 of 6 runs on master, and on an idle one it passed 10 of 10, so a handful of green runs does not clear it. retries: 2 in the CI config hides it upstream, which is why it may not have been visible.

The clearest evidence that the DOM path was never viable rather than merely slow: the hidden textarea holds 0 characters at every time offset sampled, while the editor model holds the full 219. The fix reads from the model instead of the rendered DOM.

@opensource-joe

Copy link
Copy Markdown
Author

Folding this into #3465 rather than keeping it separate, so there is one PR to review instead of two.

Nothing is dropped: the commit here is cherry-picked onto #3465 unchanged, as its own commit rather than squashed, so the test fix stays independently reviewable. #3465's description now covers both halves separately, and its scope is unchanged otherwise.

@LiteSun apologies for the churn, and please disregard my earlier ask on this one. The workflow-approval request now applies only to #3465.

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.

e2e: plugin_metadata.crud-all-fields is flaky on a half-rendered Monaco editor, masked by CI retries

1 participant