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-editor and wuic-parametric-dialog.
  • The __wuicCmp handle, 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:

AttributeContent
data-field-namelogical column name (mc_nome_colonna)
data-field-idthe column's mc_id, empty when unavailable
data-widget-typeresolved widget type (mc_ui_column_type)
data-editabletrue/false per metadata and form state
data-hide-in-edittrue/false (mc_hide_in_edit)
data-is-edit-formtrue in an edit form, false in filter/view
data-has-recordtrue once the record object is assigned
data-field-valuethe field's current value, serialized

The anchor is therefore the field name, not the position:

Snippet 1js
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:

SignalMeaning
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:

Snippet 2js
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:

Snippet 3js
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, where window.ng exists anyway;
  • in production only if the page declared globalThis.__wuicE2E = true before the application booted.

The test bootstrap does that with an init script, not the product:

Snippet 4js
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:

Snippet 5ts
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:

Snippet 6js
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_translations and 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 different mcordine changes the test.
  • `window.ng.getComponent` as the only route: development only (see above).

Operational notes

  • The __wuicE2E flag 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 cmp returned by getComponent means "not inspectable", not "no data": telling the two apart in error messages keeps a diagnosis from starting in the wrong direction.