Bugs
- IndexFieldExplosionIT: SCHEMA_ALIAS was `databaseSchema_search_index`; the
canonical indexMapping.json name is `database_schema_search_index`.
- ExplorePage:
- `tabTestId(GLOSSARY_TERMS)` produced `glossaries-tab`, but the UI builds
the testid from the i18n label (`Glossary Terms` → `glossary terms-tab`).
- `Tab.DASHBOARD_DATA_MODELS` path was `dashboardDataModels`; the Explore
route segment is singular (`dashboardDataModel`).
- Javadoc {@link} now points at the correct `openWithSearch` overload.
- UiSessionExtension: split video lifecycle so the `Video` handles are
snapshotted before `context.close()` (pages() is empty after close) but
`video.path()` is resolved AFTER close (Playwright finalises the file on
close — calling .path() earlier blocks/fails).
- GoogleSsoSignInUIIT: removed the empty alternative from the
`(my-data|explore|)` regex; it matched almost any post-auth URL and
weakened the assertion.
- MockOidcServer: still requires a single fixed port (token `iss` claim has
to match across container/host/browser), but the port is now overridable
via `-Dom.mockOidc.port=NNNN` and a fast pre-flight `ServerSocket` probe
fails clearly when the chosen port is busy. GoogleSsoSignInUIIT now reads
the port from `MockOidcServer.PORT` instead of hard-coding 1080.
Test hygiene
- SearchAvailableDuringReindexUIIT: replaced `Thread.sleep` polling with
Awaitility (`.atMost(REINDEX_TIMEOUT).pollInterval(PROBE_INTERVAL)`),
giving the loop a real deadline and removing the antipattern.
- ClipboardHelper: replaced the fixed `waitForTimeout(300)` with bounded
paste-retries until the hidden textarea has a non-empty value; textarea
cleanup moved to a `finally` block.
- SimpleReindexTriggerUIIT / SearchAvailableDuringReindexUIIT: defaults are
now PR-friendly (200/100/100/100 tables/topics/dashboards/pipelines and
500 tables respectively) overridable via system properties; the nightly
workflow sets the historical 5k stress numbers.
Quality
- DistributedAutoTuneReindexUIIT.distributedAutoTuneConfig now returns
`Map.of(...)` instead of a mutable `HashMap`.
- SearchQueryHelper.SearchProbe defensively copies `ids` / `uniqueIds` to
immutable collections in the canonical constructor.
- EntityLoader: every parameter and local that doesn't change is now
`final`.
- AuthAssumptions: `toLowerCase` calls now pin `Locale.ROOT` to stay stable
under Turkish / other surprising locales.
Docs
- PageObject javadoc: list of rules updated to reflect actual contract
(Page Objects may expose `Locator`-returning accessors, `rawPage()` is a
documented escape hatch).
- UI_TEST_CONVENTIONS.md: layering diagram now lists the real packages
(`playwright.scenarios`, `playwright.ui.pages`, `it.auth`, `it.server`).
Rule about Locator/Page softened to match the real contract. Headed-debug
recipe points at `:openmetadata-integration-tests` (the
`:openmetadata-java-playwright` module was removed). Stale references to
MIGRATION_TRACKING.md and SearchAfterReindexUIIT replaced with
REINDEX_TEST_PLAN.md and SimpleReindexTriggerUIIT.
- REINDEX_TEST_PLAN.md: helpers table now flagged as a planning shape with
an explicit list of what's shipped today vs. what's still aspirational.
5.8 KiB
UI test conventions
Read this before writing or porting any *UIIT.java. It exists so the suite stays
consistent as we migrate 258 specs.
Layering — strict, top-only depends down
playwright.scenarios.<domain>.*UIIT.java — tests
↓
playwright.ui.pages.*Page.java — Page Objects (locators + actions)
↓
playwright.ui.{SessionBrowser,UiSession,UiSessionExtension,TraceRecorder}
it.auth.{AuthBackend,AuthSession,BasicJwtBackend,OidcBackend,…}
↓
it.server.*, it.search.* — server lifecycle + SDK helpers
Rule: prefer keeping Locator / Page / BrowserContext use inside Page Objects.
Page Objects may expose Locator-returning accessors when a test legitimately needs to
make a Playwright-level assertion on a specific element, and PageObject.rawPage() is a
documented escape hatch for URL assertions and SSO redirect flows. If a test reaches for
Playwright primitives for routine interactions (clicks, typing, navigation), promote the
interaction into a Page Object method.
Test skeleton
@ExtendWith({UiSessionExtension.class, TestNamespaceExtension.class})
class FooUIIT {
@BeforeAll
static void setup() {
SdkClients.useFluentApis(UiTestServer.get().sdk());
Apps.setDefaultClient(UiTestServer.get().sdk());
}
@Test
void scenario(final UiSession ui, final TestNamespace ns) {
Table t = TableTestFactory.createSimple(ns); // SDK setup
FooPage page = FooPage.open(ui, t.getFullyQualifiedName()); // page object navigation
page.doSomething(); // page object action
assertThat(page.someResult()).isVisible(); // assertion
}
}
Setup via SDK. Cleanup via TestNamespace. Never click through the UI to seed state.
Page Objects
- One class per page, ≤ 200 lines. Split if bigger.
- Static
open(...)factory navigates and returns a loaded instance. - Actions return
this(or anotherPageObject) for chaining. - Locators:
getByTestId>getByRole/getByLabel>getByPlaceholder> text > CSS. - Define every selector as
private static final— no inline literals. waitForLoaded()overridden per page — the readiness signal must be page-specific (a known testid visible, an API response done) not a genericnetworkidleif a better signal exists.
Auth
The whole suite is parameterized by an AuthBackend. Pick one with -Djpw.auth=<name>
(or JPW_AUTH=<name>); the same UI tests run unchanged regardless of which is active.
jpw.auth |
OM auth provider | Token acquired by |
|---|---|---|
basic (default) |
OM built-in JWT | JwtAuthProvider (admin signing key) |
sso-google-public |
Google + public client (response_type=id_token) |
mock IdP /google/token (password grant) |
sso-google-confidential |
Google + confidential client (response_type=code) |
same |
sso-okta-public |
Okta + public client | mock IdP /okta/token |
sso-okta-confidential |
Okta + confidential client | same |
sso-custom-oidc-public |
Custom OIDC + public | mock IdP /custom-oidc/token |
sso-custom-oidc-confidential |
Custom OIDC + confidential | same |
Per-test override (only when the test explicitly drives the sign-in surface and doesn't want a token preloaded):
@Test
@NoPreloadAuth
void clickingSignInWithGoogleCompletesLogin(UiSession ui) { ... }
Tests that only make sense under one backend gate themselves with
AuthAssumptions.onlyWhenBackendIs("sso-google-confidential"). They skip cleanly when the
suite is running under any other backend.
A daemon TokenRefresher keeps AuthSession.current() valid across long runs; tests
read the current token at @BeforeEach, so refresh is transparent.
Parallelism
- Per-method parallel by default; classes serial. Configured in pom failsafe.
- Tests that mutate global state must be tagged:
@ResourceLock("GLOBAL_SETTINGS")— settings, themes, login config@ResourceLock("SEARCH_INDEX_APP")— re-triggers the global reindex app@ResourceLock("APPS")— install/uninstall apps@ResourceLock("ALERTS")— global alert config@Execution(SAME_THREAD)— inherently sequential UI flows (Tour, SSO renewal)
- Default: assume parallel-safe.
TestNamespacekeeps entity names unique.
Java code rules (recap from project CLAUDE.md)
- Methods ≤ 15 lines, ≤ 3 nesting, ≤ 5 params, ≤ 10 cyclomatic.
finalon every parameter and local that doesn't change.- No comments stating what — only why. Name the code so it reads.
- No magic strings in
equals/contains/switch— define a constant or an enum. - No deep
else ifchains — refactor toswitch,Mapdispatch, or named predicate. - No empty catches, no
printStackTrace, nocatch(Exception)for control flow. mvn spotless:applybefore every commit.
Headed debugging
Knobs (all available as env var or -D<name>=... system property):
| Knob | Effect |
|---|---|
PW_HEADED=true |
Visible Chromium |
PW_SLOWMO=<ms> |
Inter-action delay; defaults to 250ms in headed, 0 in headless |
PW_VIDEO=true |
Records every test, saves to target/playwright-videos/<class>-<test>-<ts>.webm |
Trace zips for failed runs are saved to target/playwright-traces/. Replay any trace:
npx playwright show-trace target/playwright-traces/trace-<class>-<test>-<ts>.zip
Typical local debug recipe — slow it down enough to follow, capture video for review:
PW_HEADED=true PW_SLOWMO=500 PW_VIDEO=true mvn verify -P ui-it \
-pl :openmetadata-integration-tests -Dit.test='SimpleReindexTriggerUIIT'
When in doubt
- Read
REINDEX_TEST_PLAN.mdat the module root for the reindex scenario coverage map. - Read
SimpleReindexTriggerUIITas the canonical reference test.