This is an automated email from the ASF dual-hosted git repository.

tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 63609aa30d [ZEPPELIN-6560] Lint the new UI e2e suite and document its 
conventions
63609aa30d is described below

commit 63609aa30df368daecfcbc075a04c76fb5c34479
Author: YONGJAE LEE (이용재) <[email protected]>
AuthorDate: Mon Jul 27 23:26:06 2026 +0900

    [ZEPPELIN-6560] Lint the new UI e2e suite and document its conventions
    
    ### What is this PR for?
    
    The new UI e2e suite has never been linted. `angular.json` sets 
`lintFilePatterns` to `src/**/*.ts` and `src/**/*.html`, so nothing under 
`e2e/` is checked beyond what the TypeScript compiler catches. 
`zeppelin-web-angular` has no unit or component test target either: no `test` 
target in `angular.json`, no `npm test` script, no spec files under `src/`. The 
e2e suite is the app's only automated coverage, so a check in it that quietly 
passes goes unnoticed. Two are in the tree today. `e [...]
    
    ```ts
    await expect(page.locator('.paragraph-control .fa-spin')).not.toBeVisible({ 
timeout: 15000 });
    ```
    
    `.fa-spin` is defined in the vendored FontAwesome stylesheet but applied by 
no Angular template, so the locator matches no element, and `not.toBeVisible` 
passes when a locator resolves to no DOM node. Separately, 
`e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts:25` attributes coverage 
to `PAGES.SHARE.SHARE_RESULT`, a key that does not exist: `SHARE_RESULT` lives 
under `PAGES.WORKSPACE`. Playwright transpiles without type checking, so the 
expression is `undefined` and that descr [...]
    
    This PR adds `e2e/**/*.ts` to `lintFilePatterns` and applies 
`eslint-plugin-playwright`'s recommended flat config to that path. The 
pre-commit hook runs `eslint --fix` through lint-staged, and `--fix` applies 
fixes at any severity including warnings, so an autofixable rule rewrites test 
code without review. `playwright/no-useless-not` rewrites `.not.toBeVisible()` 
into `.toBeHidden()`; both forms pass when the locator matches nothing, but the 
rewritten one reads as a deliberate hidden [...]
    
    Auditing the tree against the rules the doc states turned up a few more, 
fixed here: an unused catch binding in `login-page.util.ts`, `eslint-disable` 
markers carrying the tracking key on the four ZEPPELIN-6379 skips, reasons on 
the `waitForTimeout` calls in `utils.ts` and `react-footer.spec.ts`, reasons on 
the two `eslint-disable` lines in `notebook-keyboard-page.ts`, a message on the 
unexplained WebKit skip in `dark-mode.spec.ts`, and the missing `PAGES` 
annotation in `user-menu-nav [...]
    
    `e2e/AGENTS.md` is updated too. It drops the `Tooling: Use e2e-skills` 
section, which pointed contributors at an external personal repository, along 
with the vendor-specific setup notes in the preamble. In their place it gains 
an escape-hatch contract (`// JUSTIFIED:` and `// eslint-disable-next-line ... 
-- <why>`) and a locator hierarchy that explains why `exact: true` matters and 
when a CSS selector is acceptable. Four rules that claimed more than the tree 
delivers were brought back [...]
    
    ### What type of PR is it?
    Improvement
    
    ### Todos
    * [x] Run `npm run lint` with the e2e patterns added
    * [x] Check every autofixable rule for one that could change a test's 
meaning through the pre-commit hook
    * [x] Compare each override against the preset severity so the config 
states only real deviations
    * [x] Confirm every `PAGES` reference resolves
    
    ### What is the Jira issue?
    ZEPPELIN-6560
    
    ### How should this be tested?
    
    This is a lint and documentation change; no test behaviour changes.
    
    ```bash
    cd zeppelin-web-angular
    npm run lint
    ```
    
    Expect 0 errors. The warnings are the debt described above. To see the new 
coverage on its own, run `npx eslint 'e2e/**/*.ts'`; before this PR `ng lint` 
reported nothing for `e2e/` because the path was not in `lintFilePatterns`. 
Adding a violation of any error-level rule, for example 
`test.describe.skip('x', () => {})` in a spec, should now fail the lint.
    
    The coverage-key fix is visible in the run's annotations: `npm run e2e:fast 
-- tests/notebook/paragraph/copy-to-clipboard.spec.ts` attributes that describe 
to the `share/result` component instead of `undefined`. That spec needs a 
running shell interpreter, so it skips on CI.
    
    ### Screenshots (if appropriate)
    
    ### Questions:
    * Does the license files need to update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    
    Closes #5348 from voidmatcha/fix/e2e-locator-conventions.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 zeppelin-web-angular/angular.json                  |   2 +-
 zeppelin-web-angular/e2e/AGENTS.md                 | 187 +++++++--------------
 zeppelin-web-angular/e2e/models/login-page.util.ts |   2 +-
 .../e2e/models/notebook-keyboard-page.ts           |   4 +-
 .../keyboard/notebook-keyboard-shortcuts.spec.ts   |  10 +-
 .../notebook/paragraph/copy-to-clipboard.spec.ts   |   2 +-
 .../tests/notebook/paragraph/react-footer.spec.ts  |   2 +
 .../e2e/tests/theme/dark-mode.spec.ts              |   4 +-
 .../tests/workspace/user-menu-navigation.spec.ts   |   4 +-
 zeppelin-web-angular/e2e/utils.ts                  |   1 +
 zeppelin-web-angular/eslint.config.js              |  21 +++
 zeppelin-web-angular/package-lock.json             |  30 ++++
 zeppelin-web-angular/package.json                  |   1 +
 13 files changed, 133 insertions(+), 137 deletions(-)

diff --git a/zeppelin-web-angular/angular.json 
b/zeppelin-web-angular/angular.json
index 089cc07ce6..fa7d20ec48 100644
--- a/zeppelin-web-angular/angular.json
+++ b/zeppelin-web-angular/angular.json
@@ -151,7 +151,7 @@
         "lint": {
           "builder": "@angular-eslint/builder:lint",
           "options": {
-            "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
+            "lintFilePatterns": ["src/**/*.ts", "src/**/*.html", "e2e/**/*.ts"]
           }
         }
       }
diff --git a/zeppelin-web-angular/e2e/AGENTS.md 
b/zeppelin-web-angular/e2e/AGENTS.md
index 900756358a..af81bb2303 100644
--- a/zeppelin-web-angular/e2e/AGENTS.md
+++ b/zeppelin-web-angular/e2e/AGENTS.md
@@ -17,81 +17,67 @@ limitations under the License.
 
 # AGENTS.md
 
-> E2E (Playwright) conventions for `zeppelin-web-angular/e2e/`. A scoped 
companion
-> to the repository-root AGENTS.md, loaded only when working under `e2e/`.
-> See [AGENTS.md specification](https://github.com/agentsmd/agents.md).
+> E2E (Playwright) conventions for `zeppelin-web-angular/e2e/`. A scoped 
companion to the repository-root AGENTS.md, loaded only when working under 
`e2e/`. See [AGENTS.md specification](https://github.com/agentsmd/agents.md).
 
-Config: `zeppelin-web-angular/playwright.config.js` (Angular UI) and
-`playwright.classic.config.js` (legacy classic UI), sharing 
`playwright.shared.js`.
-This document is the shared source of truth for E2E conventions; Codex and
-agents.md-native tools read it directly.
-Claude Code / Gemini users can symlink `CLAUDE.md` / `GEMINI.md` to it locally
-(both gitignored, personal, not committed).
-
-## Tooling: Use e2e-skills
-
-Generate, review, and debug with 
[e2e-skills](https://github.com/voidmatcha/e2e-skills)
-instead of ad-hoc prompts. It encodes the rules below and adds a deterministic
-silent-pass scanner.
-
-```bash
-npx skills add voidmatcha/e2e-skills -g --all   # or -a <agent>
-```
-
-| Task | Skill |
-| --- | --- |
-| Generate new Playwright coverage | `playwright-test-generator` |
-| Review specs for silent-pass smells | `e2e-reviewer` |
-| Debug a failed Playwright report | `playwright-debugger` |
-| Deterministic local scan | `bash skills/e2e-reviewer/scripts/scan.sh e2e/` |
-
-Always run `e2e-reviewer` on generated specs. It catches always-passing
-assertions (`toBeDefined()`, `not.toBeNull()`) that pass while the feature is 
broken.
+Config: `zeppelin-web-angular/playwright.config.js` (Angular UI) and 
`playwright.classic.config.js` (legacy classic UI), sharing 
`playwright.shared.js`. This document is the source of truth for E2E 
conventions, for contributors and for coding agents alike.
 
 ## Layout
 
-- Specs: `e2e/tests/<area>/<feature>.spec.ts` (areas: `authentication`, `home`,
-  `login`, `notebook`, `share`, `theme`, `workspace`).
+- Specs: `e2e/tests/<area>/[<group>/]<feature>.spec.ts` (areas: 
`authentication`, `home`, `login`, `notebook`, `share`, `theme`, `workspace`). 
Larger areas group specs one level deeper, as in `notebook/keyboard/` and 
`workspace/notebook-repos/`. `tests/app.spec.ts` covers the app shell and sits 
outside any area.
 - Page Objects (POM), split by role:
   - `e2e/models/<name>.ts`: locators + primitive actions (click, fill, 
navigate, simple state checks).
   - `e2e/models/<name>.util.ts`: workflows, composite verification, scenario 
helpers.
+  - Most existing POMs are a single file. Split a new one by role, and split 
an existing one when its workflow code outgrows its locators.
 - Shared helpers: `e2e/utils.ts`.
 
 ## Style
 
 - English only. No unnecessary comments.
-- BDD via `test.step('Given/When/Then …', …)`, as in existing specs.
-- One `test.describe` per feature; construct the POM in `beforeEach`.
+- BDD via `test.step('Given/When/Then …', …)`. Steps show up in traces and 
reports; `// Given:` comments do not. Some specs still use comments; migrate a 
test's comments to steps when you touch it.
+- One `test.describe` per feature; construct the feature's own POM in 
`beforeEach`. A secondary POM that only one test needs, such as the second 
viewer in a collaboration test or a page reached mid-test, can be built in the 
test body.
+- `test.describe.serial` is a last resort: one failure skips every later test 
in the group, which hides the rest instead of reporting them. Playwright 
recommends against it (https://playwright.dev/docs/test-parallel#serial-mode). 
Prefer making each test set up its own state.
+
+## Escape hatches
+
+Two comment forms mark a deliberate rule violation, and both require a reason:
+
+- `// JUSTIFIED: <why>` for the conventions in this document, either trailing 
the offending line or in the comment block directly above it. It is a contract 
with the reviewer: the marker says the deviation is deliberate and the reason 
says why. A `test.describe.serial` group needs one too.
+- `// eslint-disable-next-line <rule> -- <why>` for a lint rule. Give a reason 
after the `--`; if the violation is tracked elsewhere, the ticket key is that 
reason.
+
+When a rule is both a convention here and a lint rule, `// JUSTIFIED:` is the 
one to use: an `eslint-disable` silences the linter but leaves the convention 
unmet.
+
+Neither hatch is a way to opt out of thinking. One without a concrete reason 
will be challenged in review.
 
 ## Locators
 
 Prefer user-facing, in this order:
 
-1. `getByRole('button' | 'link' | 'textbox', { name })`, `getByLabel`, 
`getByText`.
-2. Last resort: `data-testid` (attribute selector) when a role/label is 
unavailable
-   and a CSS chain would be brittle.
-3. Forbidden: raw CSS chains and XPath.
+1. `getByRole('button' | 'link' | 'textbox', { name })`, `getByLabel`, 
`getByText`. Pass `exact: true` alongside `name`. The default matches the 
accessible name as a case-insensitive substring, which collides with note 
titles and other page content and fails strict mode.
+2. `data-testid` (attribute selector) when a role or label is unavailable. 
Adding one to the Angular or React template is allowed, and is better than 
reaching into component internals with a CSS selector.
+3. A CSS selector only when the element offers neither, which is common for 
ng-zorro internals and icon-only controls. It belongs in the Page Object, named 
for what it does (`cancelButton`, not `.cancel-para`), never inline in a spec. 
An accessible name that is really an icon glyph (`pause-circle`) is not an 
improvement; it rots on the next icon swap.
+
+XPath is forbidden outright.
+
+Much of the suite predates this section: it inlines CSS and mostly omits 
`exact: true`. The ratchet is that new or modified code complies. When you 
touch a test that inlines a selector, move it into the Page Object as part of 
that change.
 
 ## Assertions
 
-- Web-first, auto-waiting assertions only: `toBeVisible`, `toHaveURL`,
-  `toHaveText`, `toHaveCount`.
-- No `waitForTimeout`. When waiting on a count, use `toHaveCount`.
-- No one-shot boolean checks (`expect(await el.isVisible())`) and no
-  always-true assertions (`toBeDefined`, `not.toBeNull`).
+- Web-first, auto-waiting assertions only: `toBeVisible`, `toHaveURL`, 
`toHaveText`, `toHaveCount`. The suite still asserts on values it extracted 
first in a few places, which `playwright/prefer-web-first-assertions` reports; 
the ratchet applies here too.
+- No `waitForTimeout` without a `// JUSTIFIED:` rationale. When waiting on a 
count, use `toHaveCount`.
+- No one-shot boolean checks (`expect(await el.isVisible())`) and no 
always-true assertions on a locator (`toBeDefined`, `not.toBeNull`). A Locator 
is always a defined, non-null object, so those pass whether or not the element 
exists. Asserting a non-locator value is not this smell: 
`expect.poll(...).not.toBeNull()` and a null-guard on a regex match are both 
legitimate.
+- A conditional may gate a setup action on dual-mode UI (auth vs anonymous, 
the optional welcome modal), which is why `playwright/no-conditional-in-test` 
is off. Do not put an `expect` inside one: an assertion that runs on only one 
branch passes by skipping the check it exists to make. 
`playwright/no-conditional-expect` reports those and the suite still carries 
some, so the ratchet applies here too.
+- A network wait is synchronization, not proof. 
`waitForLoadState('networkidle')` is discouraged by Playwright and the suite 
still has several, one of them inside `waitForZeppelinReady`; in new code wait 
on a user-visible signal instead. When you do wait on the network, assert the 
rendered result afterwards.
+- The lint config covers part of this section, not all of it. 
`eslint-plugin-playwright` has no rule for always-true assertions, so those are 
a review responsibility.
 
 ## Readiness & Auth
 
-- After navigation, wait with `waitForZeppelinReady(page)` from `e2e/utils.ts`
-  (not fixed sleeps).
-- Auth is programmatic: the `setup` project logs in once and writes
-  `playwright/.auth/user.json`; browser projects consume it via `storageState`.
-  Do not add per-test login races. For logged-out scenarios use a fresh 
context.
+- After navigation, wait with `waitForZeppelinReady(page)` from `e2e/utils.ts` 
(not fixed sleeps).
+- Auth is programmatic: the `setup` project logs in once and writes 
`playwright/.auth/user.json`; browser projects consume it via `storageState`. 
Do not add per-test login races. For logged-out scenarios use a fresh context.
+- A skip says why it skipped. `playwright/no-skipped-test` errors on the 
declaration forms (`test.skip('title', fn)`, `test.describe.skip`) and on a 
bare `test.skip()` outside an `if`; those need the `eslint-disable` hatch and a 
tracking key. Every other skip passes lint whatever its message says, so the 
message is a convention, not a gate: name the missing capability (auth mode, 
interpreter, environment feature) or the tracking key.
 
 ## Coverage Annotation (Required)
 
-Every `describe` must declare the page/component it exercises so coverage is
-attributed:
+Every `describe` must declare the page/component it exercises so coverage is 
attributed:
 
 ```ts
 import { addPageAnnotationBeforeEach, PAGES } from '../../utils';
@@ -102,23 +88,20 @@ test.describe('Home Page - Core Elements', () => {
 });
 ```
 
-Use an existing key from the `PAGES` object in `e2e/utils.ts`; add a new one
-there if the page is missing. `PAGES` is also the coverage-instrumentation set
-(`getCoverageTransformPaths`), so it defines the coverage denominator. Purely
-structural / non-page components (lifecycle hooks, shared UI primitives like 
the
-spinner or resize handle) are intentionally omitted from `PAGES`. They are
-exercised transitively and are not counted.
+Use an existing key from the `PAGES` object in `e2e/utils.ts`; add a new one 
there if the page is missing. `PAGES` is also the coverage-instrumentation set 
(`getCoverageTransformPaths`), so it defines the coverage denominator. Purely 
structural / non-page components (lifecycle hooks, shared UI primitives like 
the spinner or resize handle) are intentionally omitted from `PAGES`. They are 
exercised transitively and are not counted.
 
 ## Running
 
-- Node: `nvm use` (pinned in `.nvmrc`, currently 22.21.1).
-- Dev server: `npm run start` at `http://localhost:4200` (Playwright reuses a
-  running one via `webServer.reuseExistingServer`).
+- Node: `nvm use` (version pinned in `.nvmrc`).
+- Dev server: `npm run start` at `http://localhost:4200` (Playwright reuses a 
running one via `webServer.reuseExistingServer`).
 
 | Command | Purpose |
 | --- | --- |
 | `npm run e2e` | Full suite |
 | `npm run e2e:fast` | Chromium only (fast) |
+| `npm run e2e:fast -- tests/<area>/<feature>.spec.ts` | One spec (path is 
relative to `e2e/`) |
+| `npm run e2e:fast -- -g '<test title>'` | One test, matched by title |
+| `npx eslint e2e/tests/<area>/<feature>.spec.ts` | Lint one file; `npm run 
lint` covers the whole app |
 | `npm run e2e:classic` | Classic `/classic` UI suite against `:8080` (needs 
`-Pweb-classic`) |
 | `npm run e2e:ui` | Playwright Test UI |
 | `npm run e2e:headed` | Headed run |
@@ -132,90 +115,40 @@ exercised transitively and are not counted.
 ## Adding a Test (Agents Start Here)
 
 1. Pick/confirm the target route and the `PAGES` key.
-2. Copy the shape of an existing spec in the same `<area>`; reuse or extend the
-   matching POM (`models/<name>.ts` + `.util.ts`). Do not inline selectors the
-   POM already owns.
-3. Annotate the page (`addPageAnnotationBeforeEach`), navigate, then
-   `waitForZeppelinReady`.
-4. Run `npm run e2e:fast` and iterate until green; then run `e2e-reviewer`.
+2. Copy the shape of an existing spec in the same `<area>`; reuse or extend 
the matching POM (`models/<name>.ts` + `.util.ts`). Do not inline selectors the 
POM already owns.
+3. Annotate the page (`addPageAnnotationBeforeEach`), navigate, then 
`waitForZeppelinReady`.
+4. Run `npm run e2e:fast` and iterate until green.
 
 ## Migration (Angular to React Microfrontend)
 
-Pages are moving from Angular to React fragments incrementally. Today this is
-narrow: the published paragraph route reads a `?react=true` flag
-(`published/paragraph/paragraph.component`), and the notebook footer swaps via 
a
-`?reactFooter=true` flag (read into the notebook component's `useReactFooter`
-input). Both are query params inside the hash. There is no app-wide "flip this
-route to React" flag, and
-no cross-framework parity project in this config. Write specs so they survive a
-route being reimplemented, but do not build parity infrastructure ahead of 
need.
+Pages are moving from Angular to React fragments incrementally. Today this is 
narrow: the published paragraph route reads a `?react=true` flag 
(`published/paragraph/paragraph.component`), and the notebook footer swaps via 
a `?reactFooter=true` flag (read into the notebook component's `useReactFooter` 
input). Both are query params inside the hash. There is no app-wide "flip this 
route to React" flag, and no cross-framework parity project in this config. 
Write specs so they survive a route [...]
 
 ### Write Framework-Neutral Specs
 
-- Assert observable behavior only: what the user sees, the URL, network 
effects.
-  Avoid asserting framework internals (`[ng-version]`, Angular component 
classes,
-  `zeppelin-*` custom-element tags) except in a deliberate feature-flag test.
-- Keep the locator order from the Locators section (role/label/text first). At 
a
-  seam that will flip frameworks, prefer a shared `data-testid` that both
-  implementations render.
-- Never use fixed waits at a fragment seam. Wait on a user-visible post-mount
-  signal or the specific remote response (`page.waitForResponse` on the 
fragment
-  chunk), then assert the rendered result. `react-footer.spec.ts` shows the
-  fallback pattern (`page.route('**/remoteEntry.js', route => route.abort())`).
+- Assert observable behavior only: what the user sees, the URL, network 
effects. Avoid asserting framework internals (`[ng-version]`, Angular component 
classes, `zeppelin-*` custom-element tags) except in a deliberate feature-flag 
test.
+- Keep the locator order from the Locators section (role/label/text first). At 
a seam that will flip frameworks, prefer a shared `data-testid` that both 
implementations render.
+- Never use fixed waits at a fragment seam. Wait on a user-visible post-mount 
signal or the specific remote response (`page.waitForResponse` on the fragment 
chunk), then assert the rendered result. `react-footer.spec.ts` shows the 
fallback pattern (`page.route('**/remoteEntry.js', route => route.abort())`).
 
 ### When a Route Gains a React Flag
 
-- The flag is a route query param read via `ActivatedRoute.queryParams`, so 
with
-  the hash router it goes INSIDE the hash: 
`/#/notebook/<id>/paragraph/<id>?react=true`,
-  not before the `#`. Popups opened by app code (`window.open`) will not carry 
a
-  flag added only to `page.goto`.
-- To exercise both frameworks, follow the existing precedent and toggle the 
flag
-  in-spec: navigate the same spec with and without the flag across tests, as
-  `published-paragraph.spec.ts` does. A separate flag-appending Playwright 
project
-  is an alternative, but scope it (its own `testMatch`) to routes that read the
-  flag rather than running the whole suite twice.
+- The flag is a route query param read via `ActivatedRoute.queryParams`, so 
with the hash router it goes INSIDE the hash: 
`/#/notebook/<id>/paragraph/<id>?react=true`, not before the `#`. Popups opened 
by app code (`window.open`) will not carry a flag added only to `page.goto`.
+- To exercise both frameworks, follow the existing precedent and toggle the 
flag in-spec: navigate the same spec with and without the flag across tests, as 
`published-paragraph.spec.ts` does. A separate flag-appending Playwright 
project is an alternative, but scope it (its own `testMatch`) to routes that 
read the flag rather than running the whole suite twice.
 
 ### Coverage
 
-- Coverage is tracked by `PAGES` key, not source file. The key is the stable
-  identity; the path behind it is an implementation detail. When a page moves 
to
-  React, update its path in `PAGES` rather than deleting the key (deleting 
drops
-  it from the coverage denominator). Specs keep the same
-  `addPageAnnotationBeforeEach(PAGES.KEY)` call across the migration.
+- Coverage is tracked by `PAGES` key, not source file. The key is the stable 
identity; the path behind it is an implementation detail. When a page moves to 
React, update its path in `PAGES` rather than deleting the key (deleting drops 
it from the coverage denominator). Specs keep the same 
`addPageAnnotationBeforeEach(PAGES.KEY)` call across the migration.
 
 ### Suite Shape
 
-- Keep the composed suite focused on real cross-seam user flows. Behavior that
-  lives entirely inside one fragment belongs in that fragment's own tests; do 
not
-  grow the composed suite into a per-fragment unit suite.
+- Keep the composed suite focused on real cross-seam user flows. Behavior that 
lives entirely inside one fragment belongs in that fragment's own tests; do not 
grow the composed suite into a per-fragment unit suite.
 
 ## Classic UI Tests (`e2e/tests/classic/`)
 
-`e2e/tests/classic/` runs Playwright against the legacy AngularJS app served at
-`/classic`, ported from the retired `zeppelin-web` Protractor suite. Treat it 
as
-a frozen legacy surface: keep it at parity coverage and test new features only 
in
-the Angular/React suites.
-
-- **Locators (classic exception):** the classic templates predate roles and
-  `data-testid`, so the role/label/text-first rule cannot apply. Sanctioned 
here:
-  element ids (`#findInput`), `ng-click="..."` / `ng-controller="..."` 
attribute
-  selectors, class selectors the legacy templates already expose (`.username`,
-  `.interpreterHead`), and Ace/Select2 internals. Do not add `data-testid` to 
the
-  frozen `zeppelin-web` sources.
-- **Readiness:** `waitForZeppelinReady` is Angular-specific (`[ng-version]`) 
and
-  does not resolve on `/classic`; gate on a classic-visible signal instead 
(e.g.
-  the first `ParagraphCtrl` paragraph, or `.ace_text-input` attached).
-- **Coverage:** `PAGES` is the Angular coverage denominator; classic pages are
-  intentionally outside it, so `addPageAnnotationBeforeEach` is not used here.
-- **Running:** the classic suite has its own config, 
`playwright.classic.config.js`
-  (Desktop Chrome only, targets `http://localhost:8080`), and needs a Zeppelin
-  server built with `-Pweb-classic` — the `:4200` dev server does not serve
-  `/classic`, so a plain `npm run e2e` never includes it. Run it with
-  `npm run e2e:classic` (single spec: `npm run e2e:classic -- 
tests/classic/<spec>`).
-  In CI the workflow enables it on the anonymous matrix leg only
-  (`-Dweb.e2e.classic.disabled=false`), matching the anonymous-only legacy
-  Protractor suite.
-- **POM:** inlining locators/helpers is acceptable while the suite is this 
small;
-  if it grows, move them behind `models/classic-*.ts` / `*.util.ts`.
-- The React-migration / framework-neutral-spec guidance does not apply to
-  `tests/classic/`.
+`e2e/tests/classic/` runs Playwright against the legacy AngularJS app served 
at `/classic`, ported from the retired `zeppelin-web` Protractor suite. Treat 
it as a frozen legacy surface: keep it at parity coverage and test new features 
only in the Angular/React suites.
+
+- **Locators (classic exception):** the classic templates predate roles and 
`data-testid`, so the role/label/text-first rule cannot apply. Sanctioned here: 
element ids (`#findInput`), `ng-click="..."` / `ng-controller="..."` attribute 
selectors, class selectors the legacy templates already expose (`.username`, 
`.interpreterHead`), and Ace/Select2 internals. Do not add `data-testid` to the 
frozen `zeppelin-web` sources.
+- **Readiness:** `waitForZeppelinReady` is Angular-specific (`[ng-version]`) 
and does not resolve on `/classic`; gate on a classic-visible signal instead 
(e.g. the first `ParagraphCtrl` paragraph, or `.ace_text-input` attached).
+- **Coverage:** `PAGES` is the Angular coverage denominator; classic pages are 
intentionally outside it, so `addPageAnnotationBeforeEach` is not used here.
+- **Running:** the classic suite has its own config, 
`playwright.classic.config.js` (Desktop Chrome only, targets 
`http://localhost:8080`), and needs a Zeppelin server built with 
`-Pweb-classic`. The `:4200` dev server does not serve `/classic`, so a plain 
`npm run e2e` never includes it. Run it with `npm run e2e:classic` (single 
spec: `npm run e2e:classic -- tests/classic/<spec>`). In CI the workflow 
enables it on the anonymous matrix leg only 
(`-Dweb.e2e.classic.disabled=false`), match [...]
+- **POM:** inlining locators/helpers is acceptable while the suite is this 
small; if it grows, move them behind `models/classic-*.ts` / `*.util.ts`.
+- The React-migration / framework-neutral-spec guidance does not apply to 
`tests/classic/`.
diff --git a/zeppelin-web-angular/e2e/models/login-page.util.ts 
b/zeppelin-web-angular/e2e/models/login-page.util.ts
index e5cce7fee5..825c7111b0 100644
--- a/zeppelin-web-angular/e2e/models/login-page.util.ts
+++ b/zeppelin-web-angular/e2e/models/login-page.util.ts
@@ -42,7 +42,7 @@ export class LoginTestUtil {
     try {
       await access(this.SHIRO_CONFIG_PATH);
       this._isShiroEnabled = true;
-    } catch (error) {
+    } catch {
       this._isShiroEnabled = false;
     }
 
diff --git a/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts 
b/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
index fb4a24a3a6..2ffedd0b64 100644
--- a/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
+++ b/zeppelin-web-angular/e2e/models/notebook-keyboard-page.ts
@@ -574,7 +574,7 @@ export class NotebookKeyboardPage extends BasePage {
       .waitForFunction(
         // waitForFunction executes in browser context, not Node.js context.
         // Browser cannot access Node.js variables like 
PARAGRAPH_RESULT_SELECTOR.
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- args 
cross the browser boundary untyped
         ([index, selector]: any[]) => {
           const paragraphs = 
document.querySelectorAll('zeppelin-notebook-paragraph'); // JUSTIFIED: 
index-based paragraph lookup with sub-element checks not expressible via 
Playwright locator API
           const targetParagraph = paragraphs[index];
@@ -622,7 +622,7 @@ export class NotebookKeyboardPage extends BasePage {
       .waitForFunction(
         // waitForFunction executes in browser context, not Node.js context.
         // Browser cannot access Node.js variables like 
PARAGRAPH_RESULT_SELECTOR.
-        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any -- args 
cross the browser boundary untyped
         ([index, selector]: any[]) => {
           const paragraphs = 
document.querySelectorAll('zeppelin-notebook-paragraph'); // JUSTIFIED: 
index-based paragraph lookup with sub-element checks not expressible via 
Playwright locator API
           const targetParagraph = paragraphs[index];
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
 
b/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
index 6f3029fa51..f4e584d215 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/keyboard/notebook-keyboard-shortcuts.spec.ts
@@ -25,7 +25,7 @@ import {
  * (src/app/key-binding/shortcuts-map.ts). The page object gates on Monaco's 
`focused`
  * class before dispatching shortcuts; effects are asserted with web-first 
expectations.
  */
-// Serial ordering prevents cross-test editor state corruption within the 
shared notebook.
+// JUSTIFIED: serial ordering prevents cross-test editor state corruption 
within the shared notebook.
 test.describe.serial('Comprehensive Keyboard Shortcuts (ShortcutsMap)', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.NOTEBOOK);
   addPageAnnotationBeforeEach(PAGES.SHARE.SHORTCUT);
@@ -89,7 +89,9 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
   // TODO: Fix the previously skipped tests - ZEPPELIN-6379
   test.describe('ParagraphActions.RunAbove: Control+Shift+ArrowUp', () => {
+    // eslint-disable-next-line playwright/no-skipped-test -- tracked by 
ZEPPELIN-6379
     test.skip();
+
     test('should run all paragraphs above current with Control+Shift+ArrowUp', 
async () => {
       // Given: Multiple paragraphs
       await keyboardPage.tryFocusCodeEditor(0);
@@ -121,7 +123,9 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
   // TODO: Fix the previously skipped tests - ZEPPELIN-6379
   test.describe('ParagraphActions.RunBelow: Control+Shift+ArrowDown', () => {
+    // eslint-disable-next-line playwright/no-skipped-test -- tracked by 
ZEPPELIN-6379
     test.skip();
+
     test('should run current and all paragraphs below with 
Control+Shift+ArrowDown', async () => {
       // Given: Multiple paragraphs with content
       await keyboardPage.tryFocusCodeEditor(0);
@@ -666,7 +670,9 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
   // TODO: Fix the previously skipped tests - ZEPPELIN-6379
   test.describe('ParagraphActions.CutLine: Control+K', () => {
+    // eslint-disable-next-line playwright/no-skipped-test -- tracked by 
ZEPPELIN-6379
     test.skip();
+
     test('should cut line with Control+K', async () => {
       // Given: Code editor with content
       await keyboardPage.tryFocusCodeEditor();
@@ -708,7 +714,9 @@ test.describe.serial('Comprehensive Keyboard Shortcuts 
(ShortcutsMap)', () => {
 
   // TODO: Fix the previously skipped tests - ZEPPELIN-6379
   test.describe('ParagraphActions.PasteLine: Control+Y', () => {
+    // eslint-disable-next-line playwright/no-skipped-test -- tracked by 
ZEPPELIN-6379
     test.skip();
+
     test('should paste line with Control+Y', async () => {
       // Given: Content in the editor
       await keyboardPage.tryFocusCodeEditor();
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts
index dc13297dd4..93a9b7029e 100644
--- 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/copy-to-clipboard.spec.ts
@@ -22,7 +22,7 @@ import {
 } from '../../../utils';
 
 test.describe('Copy table result to clipboard', () => {
-  addPageAnnotationBeforeEach(PAGES.SHARE.SHARE_RESULT);
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.SHARE_RESULT);
 
   let paragraphPage: NotebookParagraphPage;
   let testNotebook: { noteId: string; paragraphId: string };
diff --git 
a/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts 
b/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
index d76ff4438b..fd102b36d4 100644
--- a/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/notebook/paragraph/react-footer.spec.ts
@@ -102,6 +102,8 @@ test.describe('React Paragraph Footer', () => {
     await page.goto('/#/');
     await waitForZeppelinReady(page);
 
+    // JUSTIFIED: outlives the 1500 ms stub so a destroy-time error has room 
to surface.
+    // The assertion is that nothing happened, so there is no UI state to wait 
on.
     await page.waitForTimeout(2500);
 
     expect(consoleErrors).toEqual([]);
diff --git a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts 
b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
index 49bbdf92cb..25f69daeb4 100644
--- a/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/theme/dark-mode.spec.ts
@@ -20,9 +20,7 @@ test.describe('Dark Mode Theme Switching', () => {
 
   test.beforeEach(async ({ page, browserName }) => {
     // TODO: This crash occurs only on WebKit. The root cause should be 
investigated and addressed.
-    if (browserName === 'webkit') {
-      test.skip();
-    }
+    test.skip(browserName === 'webkit', 'The theme toggle crashes the page on 
WebKit');
     darkModePage = new DarkModePage(page);
     await page.goto('/#/');
     await waitForZeppelinReady(page);
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts 
b/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts
index c4ee882cf1..656dcd5c61 100644
--- a/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts
+++ b/zeppelin-web-angular/e2e/tests/workspace/user-menu-navigation.spec.ts
@@ -12,7 +12,7 @@
 
 import { expect, test } from '@playwright/test';
 import { HeaderPage } from '../../models/header-page';
-import { waitForZeppelinReady } from '../../utils';
+import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../utils';
 
 /**
  * Regression guard for the header user-menu navigation.
@@ -35,6 +35,8 @@ const MENU_ITEMS = [
 ];
 
 test.describe('Header user menu - full-row navigation', () => {
+  addPageAnnotationBeforeEach(PAGES.SHARE.HEADER);
+
   let header: HeaderPage;
 
   test.beforeEach(async ({ page }) => {
diff --git a/zeppelin-web-angular/e2e/utils.ts 
b/zeppelin-web-angular/e2e/utils.ts
index 25e6d4304b..7ccae56156 100644
--- a/zeppelin-web-angular/e2e/utils.ts
+++ b/zeppelin-web-angular/e2e/utils.ts
@@ -517,6 +517,7 @@ export const createTestNotebookWithName = async (
       if (attempt === 3 || !isRetryableError(message)) {
         throw new Error(`Failed to create test notebook: ${message}. Current 
URL: ${page.url()}`);
       }
+      // JUSTIFIED: backoff between REST create retries; no UI state to wait 
on.
       await page.waitForTimeout(1000 * attempt);
     }
   }
diff --git a/zeppelin-web-angular/eslint.config.js 
b/zeppelin-web-angular/eslint.config.js
index ba289b6519..b6bb00f695 100644
--- a/zeppelin-web-angular/eslint.config.js
+++ b/zeppelin-web-angular/eslint.config.js
@@ -23,6 +23,7 @@ const prettier = require('eslint-config-prettier');
 // from the former TSLint rule (ZEPPELIN-6372).
 const localRules = require('./eslint-rules');
 const perfectionist = require('eslint-plugin-perfectionist');
+const playwright = require('eslint-plugin-playwright');
 
 module.exports = tseslint.config(
   {
@@ -166,6 +167,26 @@ module.exports = tseslint.config(
       'perfectionist/sort-exports': 'error'
     }
   },
+  {
+    files: ['e2e/**/*.ts'],
+    ...playwright.configs['flat/recommended'],
+    rules: {
+      ...playwright.configs['flat/recommended'].rules,
+      // Conditionals here gate setup actions for dual-mode UI (auth vs 
anonymous, an optional welcome modal).
+      // no-conditional-expect stays on: an assertion reached on only one 
branch passes without running.
+      'playwright/no-conditional-in-test': 'off',
+      // Interpreter-backed specs skip on process.env.CI deliberately; only an 
unconditional skip is a leak.
+      'playwright/no-skipped-test': ['error', { allowConditional: true }],
+      // Autofixable, and the pre-commit hook applies fixes without review.
+      // It rewrites `.not.toBeVisible()` into `.toBeHidden()`; both pass when 
the locator matches nothing,
+      // but the rewrite reads as a deliberate check, so a dead selector gets 
harder to spot.
+      'playwright/no-useless-not': 'off',
+      // Lowered from the preset's error: 13 networkidle waits and 9 
extracted-value assertions already exist,
+      // and `ng lint` exits 0 on warnings.
+      'playwright/prefer-web-first-assertions': 'warn',
+      'playwright/no-networkidle': 'warn'
+    }
+  },
   {
     files: ['**/*.html'],
     // == legacy `plugin:@angular-eslint/template/recommended`
diff --git a/zeppelin-web-angular/package-lock.json 
b/zeppelin-web-angular/package-lock.json
index 9d0ccbc33e..544270d737 100644
--- a/zeppelin-web-angular/package-lock.json
+++ b/zeppelin-web-angular/package-lock.json
@@ -70,6 +70,7 @@
         "eslint-plugin-import": "^2.32.0",
         "eslint-plugin-jsdoc": "^50.8.0",
         "eslint-plugin-perfectionist": "^5.10.0",
+        "eslint-plugin-playwright": "^2.10.5",
         "eslint-plugin-prefer-arrow": "^1.2.3",
         "https-proxy-agent": "^2.2.1",
         "husky": "9.1.7",
@@ -12562,6 +12563,35 @@
         "url": "https://opencollective.com/eslint";
       }
     },
+    "node_modules/eslint-plugin-playwright": {
+      "version": "2.10.5",
+      "resolved": 
"https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.10.5.tgz";,
+      "integrity": 
"sha512-4k+ml4cEd55kePUIW1WsCiOLDwZkAdPZPKMFulR83XpplCaVOTKHF6yvXLYixLtdVbkMjmKV5F3BaGuI3aH8aQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "globals": "^17.3.0"
+      },
+      "engines": {
+        "node": ">=16.9.0"
+      },
+      "peerDependencies": {
+        "eslint": ">=8.40.0"
+      }
+    },
+    "node_modules/eslint-plugin-playwright/node_modules/globals": {
+      "version": "17.7.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz";,
+      "integrity": 
"sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus";
+      }
+    },
     "node_modules/eslint-plugin-prefer-arrow": {
       "version": "1.2.3",
       "resolved": 
"https://registry.npmjs.org/eslint-plugin-prefer-arrow/-/eslint-plugin-prefer-arrow-1.2.3.tgz";,
diff --git a/zeppelin-web-angular/package.json 
b/zeppelin-web-angular/package.json
index e9d2b2ac15..756dab3127 100644
--- a/zeppelin-web-angular/package.json
+++ b/zeppelin-web-angular/package.json
@@ -97,6 +97,7 @@
     "eslint-plugin-import": "^2.32.0",
     "eslint-plugin-jsdoc": "^50.8.0",
     "eslint-plugin-perfectionist": "^5.10.0",
+    "eslint-plugin-playwright": "^2.10.5",
     "eslint-plugin-prefer-arrow": "^1.2.3",
     "https-proxy-agent": "^2.2.1",
     "husky": "9.1.7",


Reply via email to