blob: af81bb2303e54057d20c8109e3c348b27d16fac2 [file] [view]
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
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).
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>/[<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 …', …)`. 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`. 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`. 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.
- 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:
```ts
import { addPageAnnotationBeforeEach, PAGES } from '../../utils';
test.describe('Home Page - Core Elements', () => {
addPageAnnotationBeforeEach(PAGES.WORKSPACE.HOME);
// …
});
```
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` (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 |
| `npm run e2e:debug` | Step-by-step debugger |
| `npm run e2e:report` | Open last HTML report |
| `npm run e2e:report:classic` | Open last classic HTML report |
| `npm run e2e:ci` | CI mode (`CI=true`, baseURL `:8080`), main then classic suite |
| `npm run e2e:codegen` | Record against `:4200` |
| `npm run e2e:cleanup` | Delete leftover test notebooks (`e2e/cleanup-util.ts`) |
## 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.
## 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.
### 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())`).
### 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.
### 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.
### 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.
## 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/`.