Skip to content

ci(playwright): lint rules for UI-in-setup and unjustified page.reload - #33056

Open
chirag-madlani wants to merge 1 commit into
mainfrom
ci-playwright-lint-rules-ui-setup-and-reload
Open

chirag-madlani wants to merge 1 commit into
mainfrom
ci-playwright-lint-rules-ui-setup-and-reload

Conversation

@chirag-madlani

Copy link
Copy Markdown
Collaborator

Summary

Two new om-playwright/* ESLint rules — both gated at error behind the existing eslint-suppressions.json ratchet, so new violations fail lint while existing sites are grandfathered and can only shrink.

Direct follow-up to Pere's #32591 (deterministic-failures-hidden-by-flakiness), #32594 (21% wasted API calls), and #32611 (retry/timeout deadlock): both rules attack the same root cause — the tests are doing enough unnecessary work that they stress the SUT, which slows every subsequent test, which trips timeouts, which we call flakiness.

om-playwright/no-ui-in-test-setup

Bans page.click / fill / press / selectOption / check / uncheck / setInputFiles / hover / dblclick / tap / dragAndDrop / focus / blur inside test.beforeAll / beforeEach / afterAll / afterEach. page.goto in setup is not banned — navigating to the URL under test is legitimate; it's the user-input subset that turns setup into a slow UI journey.

Push state via apiContext.<Entity>.create() or a REST helper — the canonical pattern already used in ClassificationVersionPage, ServiceEntityVersionPage, MetricVersionPage and every other healthy suite.

Baseline: 15 sites across 10 files. Small — this rule is mostly a regression guard.

om-playwright/no-page-reload-without-justification

Bans bare page.reload(). Legit reloads (persistence tests, service-worker upgrades, SSO return flows) pass with // TEST_KEEP_RELOAD: <reason> on the line above or on the same line as the call.

// TEST_KEEP_RELOAD: setting must persist across full reload
await page.reload();

await page.reload(); // TEST_KEEP_RELOAD: SSO callback returns to /

Receiver is Page-scoped by identifier heuristic (page, p, any *Page, this.page, browser.newPage() result), so domain objects like store.reload() don't trigger.

Motivation: measured appBootsPerUIScenario = 2.3, convergence target is ≤1. Every reload boots the SPA entry chunk again (index.tsx's recordPlaywrightAppBoot beacon counts it via a favicon fetch). Bare reloads used as "refresh to see the update" are the dominant contributor — a stale UI after mutation is a product bug the test shouldn't work around.

Baseline: 217 sites across 90 files. Meaningful — every one worked down brings the boot ratio closer to 1, which cuts wall time roughly in proportion.

What the ratchet means in practice

  • New code: any UI action in a setup hook or any bare page.reload() fails lint immediately. The author fixes or explicitly justifies.
  • Existing code: 217 + 15 sites are recorded in eslint-suppressions.json. The file can only shrink — every PR that touches a listed file must fix its listed violations (or the suppression is pruned by yarn lint:playwright:suppressions).
  • Progress signal: the count of suppressions per rule is directly observable — grep no-page-reload-without-justification in eslint-suppressions.json and it's the current backlog.

Non-goals

  • Doesn't touch the 217 existing reload sites. Each is a separate judgement call (legit-and-justify vs. remove-and-replace) that belongs with the file's owner. Ratchet gives them time; new violations don't wait.
  • Doesn't fix the underlying appBootsPerUIScenario ratio. This rule prevents further regression and creates the mechanism to work it down; the actual work-down is follow-up.
  • Doesn't add a storage-state-based auth migration (still an open follow-up for the same appBoot goal — every performAdminLogin today is 2 boots per shard, and there are ~276 files using it).

Testing

  • Unit tests for both rules under playwright/eslint-rules/tests/. RuleTester covers hoisted receivers, TS wrappers (!, as, satisfies), computed member access, and the specific evasion shapes the existing no-positional-locator rule already had to defend against.
  • All 116 existing eslint-rule tests still pass.
  • yarn lint:playwright passes with 0 errors, 214 warnings (unchanged — all require-aggregation-wait-helper warnings already present on main).

Test plan

  • yarn test:eslint-rules — 116 passed, 0 failed
  • yarn lint:playwright — 0 errors, 214 pre-existing warnings
  • Baseline snapshotted into eslint-suppressions.json
  • Merge to observe: next PR that adds an unjustified page.reload() or a UI action in beforeAll should fail lint locally and in CI.

🤖 Generated with Claude Code

Two new om-playwright rules, both wired at error with the existing
eslint-suppressions.json ratchet — new violations fail lint, existing
ones are grandfathered and can only shrink.

no-ui-in-test-setup
  Bans page.click / fill / press / selectOption / check / uncheck /
  setInputFiles / hover / dblclick / tap / dragAndDrop / focus / blur
  inside test.beforeAll / beforeEach / afterAll / afterEach. page.goto
  is intentionally NOT banned — navigating to the URL under test is
  legitimate setup; it is the user-input subset that turns setup into a
  slow UI journey. Push state via apiContext.<Entity>.create() or a REST
  helper, matching the canonical pattern in this codebase (sampled from
  ClassificationVersionPage / ServiceEntityVersionPage / MetricVersionPage
  and every other healthy suite).

  Motivation: PR #32594 measured 21% of API calls wasted, mostly from
  UI-driven setup. Every UI click in setup adds ~30 API calls to the
  SUT; over ~4200 tests this compounds into the timeouts we then call
  flakiness.

  Baseline: 15 sites across 10 files (small, easy to work down).

no-page-reload-without-justification
  Bans bare page.reload(). Legit reloads (persistence tests, service-
  worker upgrades, SSO return flows) pass with a
  `// TEST_KEEP_RELOAD: <reason>` comment on the line above or on the
  same line as the call. Receiver is Page-scoped by identifier heuristic
  (`page`, `p`, any *Page`, `this.page`, `browser.newPage()` result) so
  domain objects like `store.reload()` don't trigger.

  Motivation: measured appBootsPerUIScenario is 2.3, convergence target
  is ≤1. Every reload boots the SPA entry chunk again (index.tsx's
  recordPlaywrightAppBoot beacon counts it via a favicon fetch). Bare
  reloads used as "refresh to see the update" are the dominant
  contributor — a stale UI after mutation is a product bug the test
  shouldn't work around.

  Baseline: 217 sites across 90 files.

Both rules follow the existing plugin conventions in
playwright/eslint-rules/: pure ESM, no external deps, RuleTester tests
under tests/, wired into index.mjs, gated at error in eslint.config.mjs.
Docs in .claude/rules/frontend-playwright.md list both rules alongside
the existing highest-value constraints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added safe to test Add this label to run secure Github workflows on PRs UI UI specific issues labels Sep 9, 2026
Comment on lines +108 to +122
const isUiInputCall = (node) => {
const { callee } = node;

if (callee?.type !== 'MemberExpression') {
return false;
}

const methodName = getMethodName(callee.property, callee.computed);

if (methodName === null || !UI_INPUT_METHODS.has(methodName)) {
return false;
}

// Any receiver — `page`, `this.page`, `newPage`, or a Locator variable —
// is a Playwright surface as long as the method name matches. Narrowing

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Edge Case: no-ui-in-test-setup matches any receiver, not just Page/Locator

isUiInputCall fires on any member call whose method name is in UI_INPUT_METHODS regardless of receiver (return unwrap(callee.object) !== undefined). Several of those names collide with common JS/DOM APIs — notably Array.prototype.fill (new Array(n).fill(0), arr.fill(null)), plus .type, .focus, .blur, .check. Since the rule is registered at error, a legitimate arr.fill(...) inside a beforeAll/beforeEach would fail CI as a bogus UI-in-setup violation. No such call exists in the repo today (so the baseline is clean), but this is a latent false-positive that will block ordinary setup code. Consider narrowing the receiver to a Page/Locator heuristic (as the reload rule does with isPageReceiver) or at least excluding the ambiguous names like fill/type/focus/blur.

Was this helpful? React with 👍 / 👎

Comment on lines +138 to +152
for (const comment of sourceCode.getAllComments()) {
if (!RELOAD_JUSTIFICATION.test(comment.value)) {
continue;
}
const commentStart = comment.loc?.start.line ?? -1;
const commentEnd = comment.loc?.end.line ?? commentStart;
if (commentStart < 0) {
continue;
}

// Leading comment: ends at most 1 line above the reload, and starts
// no more than MAX_LEAD_LINES above it.
if (commentEnd <= callLine && callLine - commentEnd <= MAX_LEAD_LINES) {
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Quality: One TEST_KEEP_RELOAD comment can justify multiple nearby reloads

hasJustificationComment scans all file comments and matches any TEST_KEEP_RELOAD whose end line is within MAX_LEAD_LINES (3) above the reload call. A single justification comment therefore silently justifies up to three subsequent bare page.reload() calls stacked within 3 lines, weakening the ratchet (those reloads never get counted as violations). Consider requiring the comment to be immediately adjacent (within 1 line) to the reload, or associating each justification with exactly one call.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 2 findings

Adds two ESLint rules to prevent UI actions in test setup hooks and unjustified page.reload() calls, both gated behind a ratchet mechanism to grandfathers existing violations while blocking new ones.

The no-ui-in-test-setup rule matches any receiver with method names like fill, focus, and blur, causing false positives on legitimate array/DOM APIs in setup code. The no-page-reload-without-justification rule allows a single TEST_KEEP_RELOAD comment to justify up to three stacked reloads, weakening the ratchet. Both issues should be fixed before merge — narrow the receiver heuristic for UI-in-setup and require justification comments to be immediately adjacent to their reload call.

⚠️ Edge Case: no-ui-in-test-setup matches any receiver, not just Page/Locator

📄 openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/no-ui-in-test-setup.mjs:108-122 📄 openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/no-ui-in-test-setup.mjs:38-52

isUiInputCall fires on any member call whose method name is in UI_INPUT_METHODS regardless of receiver (return unwrap(callee.object) !== undefined). Several of those names collide with common JS/DOM APIs — notably Array.prototype.fill (new Array(n).fill(0), arr.fill(null)), plus .type, .focus, .blur, .check. Since the rule is registered at error, a legitimate arr.fill(...) inside a beforeAll/beforeEach would fail CI as a bogus UI-in-setup violation. No such call exists in the repo today (so the baseline is clean), but this is a latent false-positive that will block ordinary setup code. Consider narrowing the receiver to a Page/Locator heuristic (as the reload rule does with isPageReceiver) or at least excluding the ambiguous names like fill/type/focus/blur.

💡 Quality: One TEST_KEEP_RELOAD comment can justify multiple nearby reloads

📄 openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/no-page-reload-without-justification.mjs:138-152

hasJustificationComment scans all file comments and matches any TEST_KEEP_RELOAD whose end line is within MAX_LEAD_LINES (3) above the reload call. A single justification comment therefore silently justifies up to three subsequent bare page.reload() calls stacked within 3 lines, weakening the ratchet (those reloads never get counted as violations). Consider requiring the comment to be immediately adjacent (within 1 line) to the reload, or associating each justification with exactly one call.

🤖 Prompt for agents
Code Review: Adds two ESLint rules to prevent UI actions in test setup hooks and unjustified `page.reload()` calls, both gated behind a ratchet mechanism to grandfathers existing violations while blocking new ones.
  
  The `no-ui-in-test-setup` rule matches any receiver with method names like `fill`, `focus`, and `blur`, causing false positives on legitimate array/DOM APIs in setup code. The `no-page-reload-without-justification` rule allows a single `TEST_KEEP_RELOAD` comment to justify up to three stacked reloads, weakening the ratchet. Both issues should be fixed before merge — narrow the receiver heuristic for UI-in-setup and require justification comments to be immediately adjacent to their reload call.

1. ⚠️ Edge Case: no-ui-in-test-setup matches any receiver, not just Page/Locator
   Files: openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/no-ui-in-test-setup.mjs:108-122, openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/no-ui-in-test-setup.mjs:38-52

   `isUiInputCall` fires on any member call whose method name is in UI_INPUT_METHODS regardless of receiver (`return unwrap(callee.object) !== undefined`). Several of those names collide with common JS/DOM APIs — notably `Array.prototype.fill` (`new Array(n).fill(0)`, `arr.fill(null)`), plus `.type`, `.focus`, `.blur`, `.check`. Since the rule is registered at `error`, a legitimate `arr.fill(...)` inside a `beforeAll`/`beforeEach` would fail CI as a bogus UI-in-setup violation. No such call exists in the repo today (so the baseline is clean), but this is a latent false-positive that will block ordinary setup code. Consider narrowing the receiver to a Page/Locator heuristic (as the reload rule does with `isPageReceiver`) or at least excluding the ambiguous names like `fill`/`type`/`focus`/`blur`.

2. 💡 Quality: One TEST_KEEP_RELOAD comment can justify multiple nearby reloads
   Files: openmetadata-ui/src/main/resources/ui/playwright/eslint-rules/no-page-reload-without-justification.mjs:138-152

   `hasJustificationComment` scans all file comments and matches any `TEST_KEEP_RELOAD` whose end line is within MAX_LEAD_LINES (3) above the reload call. A single justification comment therefore silently justifies up to three subsequent bare `page.reload()` calls stacked within 3 lines, weakening the ratchet (those reloads never get counted as violations). Consider requiring the comment to be immediately adjacent (within 1 line) to the reload, or associating each justification with exactly one call.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

❌ UI Checkstyle Failed

❌ Playwright - Guardrails + ESLint + Prettier + Organise Imports

Either a Playwright test file has linting/formatting issues, or a guardrail check failed: ESLint rule unit tests, a new guardrail violation, a stale suppression entry (its violation was fixed but the baseline was not pruned), a blanket eslint-disable, or a stale generated rule table. For the guardrail cases run yarn lint:playwright:suppressions in openmetadata-ui/src/main/resources/ui and commit the pruned eslint-suppressions.json, then yarn test:eslint-rules && node scripts/generate-playwright-rule-table.mjs --check.

Affected files

Subtest: const a = 'server.entity-fetch-error';\n const b = 'server.entity-fetch-error';\n const c = 'server.entity-fetch-error';

    ok 2 - const a = 'server.entity-fetch-error';\\n             const b = 'server.entity-fetch-error';\\n             const c = 'server.entity-fetch-error';

not ok 27 - the suppressions baseline matches its recorded state exactly
failureType: 'testCodeFailure'
error: |-
name: 'AssertionError'

fail 1

error Command failed with exit code 1.


Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Playwright Results — workflow succeeded

Validated commit e937891475687310d0c0f785604d468b9fab4536 in Playwright run 34354603242, attempt 1.

✅ 4484 passed · ❌ 0 failed · 🟡 4 flaky · ⏭️ 1 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 58m 15s

⏱️ Max setup 4m 27s · max shard execution 20m 36s · max shard-job elapsed before upload 24m 32s · reporting 17s

🌐 218.24 requests/attempt · 2.31 app boots/UI scenario · 41.10% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 41.1% (convergence target: at most 15%).
  • Browser traffic was 218.24 requests per attempt (convergence target: fewer than 200).
  • Application boot ratio was 2.31 per UI scenario (10925 boots / 4735 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard advanced-search-01 130 0 0 0 0 0
✅ Shard chromium-01 132 0 0 0 0 0
✅ Shard chromium-02 130 0 0 0 0 0
✅ Shard chromium-03 144 0 0 0 0 0
✅ Shard chromium-04 121 0 0 0 0 0
✅ Shard chromium-05 170 0 0 0 0 0
✅ Shard chromium-06 148 0 0 0 0 0
✅ Shard chromium-07 145 0 0 0 0 0
✅ Shard chromium-08 126 0 0 0 0 0
🟡 Shard chromium-09 144 0 1 0 0 0
✅ Shard chromium-10 181 0 0 0 0 0
✅ Shard chromium-11 189 0 0 0 0 0
✅ Shard chromium-12 169 0 0 0 0 0
✅ Shard chromium-13 179 0 0 0 0 0
✅ Shard chromium-14 157 0 0 0 0 0
🟡 Shard chromium-15 179 0 1 0 0 0
✅ Shard chromium-16 147 0 0 0 0 0
✅ Shard chromium-17 155 0 0 0 0 0
✅ Shard chromium-18 156 0 0 0 0 0
✅ Shard chromium-19 174 0 0 0 0 0
✅ Shard chromium-20 140 0 0 0 0 0
✅ Shard chromium-21 189 0 0 0 0 0
✅ Shard chromium-22 178 0 0 0 0 0
✅ Shard chromium-23 183 0 0 0 0 0
🟡 Shard chromium-24 156 0 1 1 0 0
🟡 Shard chromium-25 142 0 1 0 0 0
✅ Shard data-asset-rules-01 65 0 0 0 0 0
✅ Shard domain-isolation-01 16 0 0 0 0 0
✅ Shard global-state-01 34 0 0 0 0 0
✅ Shard import-export-01 97 0 0 0 0 0
✅ Shard import-export-02 22 0 0 0 0 0
✅ Shard import-export-03 31 0 0 0 0 0
✅ Shard ingestion-01 52 0 0 0 0 0
✅ Shard ingestion-02 34 0 0 0 0 0
✅ Shard reindex-01 28 0 0 0 0 0
✅ Shard search-01 12 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0
🟡 4 flaky test(s) (passed on retry)
  • VersionPages/EntityVersionPages.spec.tsTable (shard chromium-09, 1 retry)
  • Features/Dashboards.spec.tsshould be able to toggle between deleted and non-deleted charts (shard chromium-15, 1 retry)
  • Pages/DomainAdvanced.spec.tsRemove multiple assets from domain at once (shard chromium-24, 1 retry)
  • Pages/TasksUIFlow.spec.tsCreate and resolve description task for Pipeline via UI (shard chromium-25, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

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

Labels

safe to test Add this label to run secure Github workflows on PRs UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant