Overview
Testability and e2e hooks
The framework generates the UI from metadata: there are no hand-written ids for a test to hold on to, and the visible labels come from the translations, so they change with the user's language. To make the application verifiable from the outside, the runtime components expose a stable contract made of DOM attributes and a component inspection handle. This page lists that contract and the readiness signals to wait for before interacting.
Scope
- DOM attributes exposed by
wuic-field-editorandwuic-parametric-dialog. - The
__wuicCmphandle, to read a component's runtime state even in production builds. - Completion signals (record loaded, loading overlay gone).
- Selectors and practices to avoid.
Field editor DOM attributes
Every field rendered in the form is hosted by a wuic-field-editor that writes onto its own host element:
| Attribute | Content |
|---|---|
data-field-name | logical column name (mc_nome_colonna) |
data-field-id | the column's mc_id, empty when unavailable |
data-widget-type | resolved widget type (mc_ui_column_type) |
data-editable | true/false per metadata and form state |
data-hide-in-edit | true/false (mc_hide_in_edit) |
data-is-edit-form | true in an edit form, false in filter/view |
data-has-record | true once the record object is assigned |
data-field-value | the field's current value, serialized |
The anchor is therefore the field name, not the position:
const host = document.querySelector('wuic-field-editor[data-field-name="StateProvinceID"]');
const valore = host.getAttribute('data-field-value');data-field-value is a string: for lookups it exposes the nested value, for objects the JSON, for everything else the string conversion. A test that needs a number or a boolean casts it (Number(v), v === 'true').
The attributes are written with an imperative setAttribute inside syncDomAttrs(), called from ngOnChanges and ngDoCheck, not with @HostBinding: in zoneless Angular the binding getters are re-evaluated only when change detection touches that component, and a test's first read arrived earlier than that.
When the record is really loaded
data-has-record says the record object exists, not that the values have arrived: the form builds the skeleton and fills it when getFlatRecordData responds. The "values are in" signal lives on the dialog:
| Signal | Meaning |
|---|---|
wuic-field-editor[data-has-record="true"] | skeleton ready, values not guaranteed |
wuic-parametric-dialog[data-record-loaded="true"] | the data response arrived and was applied |
When the dialog reloads a record (new opening, edit→edit or detail→detail navigation via URL, which since 1.7.13 actually reloads the record), data-record-loaded goes back to false until the new response arrives: a wait started after the navigation does not read the previous record's values.
Lazy widgets — code editor, lookup — propagate their value one tick after the dialog. A robust wait therefore allows a short window after data-record-loaded for the value to show up, without hanging on fields that are legitimately empty:
await page.waitForFunction((campo) => {
const host = document.querySelector(`wuic-field-editor[data-field-name="${campo}"]`);
if (!host || host.getAttribute('data-has-record') !== 'true') return false;
const dialogo = host.closest('wuic-parametric-dialog, .edit-form-content');
if (dialogo?.getAttribute('data-record-loaded') !== 'true') return false;
const valore = host.getAttribute('data-field-value');
return valore !== null && valore !== '';Component inspection: __wuicCmp
window.ng.getComponent() is Angular's debug tooling and does not exist in production builds: a test that relies on it as its only route does not fail because of a defect, it fails because the tool simply isn't there on the installation customers actually run.
That is why the components tests need to interrogate expose their own instance on the host element as __wuicCmp:
const host = document.querySelector('wuic-chart-list');
const cmp = host.__wuicCmp || window.ng?.getComponent?.(host);
const tipo = cmp?.chartRef?.chart?.config?.type;The handle is not always attached. Exposing a component instance makes it reachable from any script on the page, and on a production installation that serves nobody:
- in development (
isDevMode()) it is always on, wherewindow.ngexists anyway; - in production only if the page declared
globalThis.__wuicE2E = truebefore the application booted.
The test bootstrap does that with an init script, not the product:
await page.addInitScript(() => { globalThis.__wuicE2E = true; });With the flag off the cost is one boolean check per instance; with it on, one property assignment plus a cleanup callback when the component is destroyed.
Components exposing __wuicCmp: list-grid, chart-list, kanban-list, scheduler-list, timeline-list, tree-list, carousel-list, spreadsheet-list-sf, map-list, designer, parametric-dialog, field-editor and the twelve field editors, meta-menu, notification-bell, bounded-repeater, pivot-builder, report-designer, workflow-designer, data-source, code-editor, app-settings-editor, view-builder, import-export-button, route-metrics-dashboard.
Making a new component inspectable takes one line in the class:
import { exposeWuicCmpOnHost } from '../../helpers/wuic-cmp-handle';
export class MioComponente {
private readonly wuicCmpHandle = exposeWuicCmpOnHost(this);
}The loading overlay
While loading, the shell covers the page with <div class="busy-indicator">. A click sent at that moment lands on the overlay, and with Playwright click({ force: true }) does not fail: it skips the actionability check but still delivers the event to whatever sits on top. The click vanishes without an error and the test fails much later — on a dialog that never opened, a card that never moved, a save that seems never to have started.
Before any coordinate-based interaction (forced clicks, drags, mouse.down) wait for the page to be free:
await page.waitForFunction(() => {
const visibile = (n) => {
const s = getComputedStyle(n);
if (s.display === 'none' || s.visibility === 'hidden' || s.opacity === '0') return false;
const r = n.getBoundingClientRect();
return r.width > 0 && r.height > 0;
};A useful diagnostic: document.elementFromPoint(x, y) over the target tells you who would actually receive the click.
What not to use as a selector
- Angular-generated `id`s (
_ngcontent-*,ng-reflect-*): they change with every build and disappear in production. - The visible label text: it comes from
_wuic_translationsand changes with the user's language. On an installation whose translations have not loaded you read the raw key (menu.root.administration), not the label. - Position in a table (
nth-child): column order is metadata, and a differentmcordinechanges the test. - `window.ng.getComponent` as the only route: development only (see above).
Operational notes
- The
__wuicE2Eflag is a test switch: the product never sets it, and no feature depends on its presence. - The DOM attributes are a public contract: if a test relies on a new attribute, that attribute is added to the component and documented here, not inferred from an internal structure.
- A null
cmpreturned bygetComponentmeans "not inspectable", not "no data": telling the two apart in error messages keeps a diagnosis from starting in the wrong direction.