Overview

Testabilità e hook e2e

Il framework genera l'interfaccia dai metadati: non ci sono id scritti a mano su cui un test possa appoggiarsi, e le etichette visibili passano dalle traduzioni, quindi cambiano con la lingua dell'utente. Per rendere l'applicazione verificabile dall'esterno, i componenti runtime espongono un contratto stabile fatto di attributi DOM e di un handle di ispezione del componente. Questa pagina elenca quel contratto e i segnali di "pronto" da attendere prima di interagire.

Scope

  • Attributi DOM esposti da wuic-field-editor e da wuic-parametric-dialog.
  • Handle __wuicCmp per leggere lo stato runtime di un componente anche in build di produzione.
  • Segnali di completamento (record caricato, overlay di caricamento chiuso).
  • Selettori e pratiche da evitare.

Attributi DOM del field editor

Ogni campo renderizzato nella form è ospitato da un wuic-field-editor che scrive sul proprio elemento host:

AttributoContenuto
data-field-namenome logico della colonna (mc_nome_colonna)
data-field-idmc_id della colonna, vuoto se non disponibile
data-widget-typetipo di widget risolto (mc_ui_column_type)
data-editabletrue/false secondo i metadati e lo stato della form
data-hide-in-edittrue/false (mc_hide_in_edit)
data-is-edit-formtrue in edit form, false in filtro/vista
data-has-recordtrue quando l'oggetto record è assegnato
data-field-valuevalore corrente del campo, serializzato

Il selettore di riferimento è quindi il nome del campo, non la posizione:

Snippet 1js
const host = document.querySelector('wuic-field-editor[data-field-name="StateProvinceID"]');
const valore = host.getAttribute('data-field-value');

data-field-value è una stringa: per i lookup espone il value annidato, per gli oggetti il JSON, per tutto il resto la conversione a stringa. Un test che vuole un numero o un booleano fa il cast (Number(v), v === 'true').

Gli attributi sono scritti con setAttribute imperativo dentro syncDomAttrs(), invocato da ngOnChanges e ngDoCheck, non con @HostBinding: in Angular zoneless i getter dei binding vengono rivalutati solo quando il change detection tocca quel componente, e il primo read di un test arrivava prima.

Quando il record è davvero caricato

data-has-record dice che l'oggetto record esiste, non che i valori siano arrivati: la form costruisce lo scheletro e lo riempie quando risponde getFlatRecordData. Il segnale di "valori arrivati" sta sul dialogo:

SegnaleSignificato
wuic-field-editor[data-has-record="true"]scheletro pronto, valori non garantiti
wuic-parametric-dialog[data-record-loaded="true"]la risposta dei dati è arrivata ed è stata applicata

Quando il dialogo ricarica un record (nuova apertura, navigazione edit→edit o detail→detail via URL, che dalla 1.7.13 ricarica davvero il record), data-record-loaded torna a false finché non arriva la nuova risposta: un'attesa che parte dopo la navigazione non legge i valori del record precedente.

I widget lazy — code editor, lookup — propagano il proprio valore un tick più tardi del dialogo. Un'attesa robusta concede quindi una finestra breve dopo data-record-loaded perché il valore compaia, senza bloccarsi sui campi legittimamente vuoti:

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 !== '';

Ispezione del componente: __wuicCmp

window.ng.getComponent() è il debug tooling di Angular e non esiste nelle build di produzione: un test che lo usa come unica via non fallisce per un difetto, fallisce perché lo strumento non c'è proprio sull'installazione che usano i clienti.

Per questo i componenti che i test devono poter interrogare espongono la propria istanza sull'elemento host come __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;

L'handle non è sempre attaccato. Esporre l'istanza di un componente la rende raggiungibile da qualunque script della pagina, e in un'installazione di produzione non serve a nessuno:

  • in sviluppo (isDevMode()) è sempre attivo, dove peraltro c'è anche window.ng;
  • in produzione solo se la pagina ha dichiarato globalThis.__wuicE2E = true prima dell'avvio dell'applicazione.

Il bootstrap dei test lo fa con uno script di init, non il prodotto:

Snippet 4js
await page.addInitScript(() => { globalThis.__wuicE2E = true; });

Quando il flag è spento il costo è un confronto booleano per istanza; quando è acceso, un assegnamento di proprietà più una callback di pulizia alla distruzione del componente.

Espongono __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 e i dodici editor di campo, 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.

Per rendere introspezionabile un componente nuovo basta una riga nella classe:

Snippet 5ts
import { exposeWuicCmpOnHost } from '../../helpers/wuic-cmp-handle';

export class MioComponente {
  private readonly wuicCmpHandle = exposeWuicCmpOnHost(this);
}

L'overlay di caricamento

Mentre carica, la shell copre la pagina con <div class="busy-indicator">. Un click inviato in quel momento finisce sull'overlay, e con Playwright click({ force: true }) non fallisce: salta il controllo di raggiungibilità ma consegna comunque l'evento all'elemento sovrapposto. Il click sparisce senza errore e il test fallisce molto più avanti — su un dialogo che non si è mai aperto, una card che non si è mossa, un salvataggio che sembra non essere mai partito.

Prima di qualunque interazione a coordinate (click forzati, drag, mouse.down) si attende che la pagina sia libera:

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;
  };

Un controllo utile in diagnosi: document.elementFromPoint(x, y) sul bersaglio dice chi riceverebbe davvero il click.

Cosa non usare come selettore

  • Gli `id` generati da Angular (_ngcontent-*, ng-reflect-*): cambiano a ogni build e spariscono in produzione.
  • Il testo visibile delle etichette: passa da _wuic_translations e cambia con la lingua dell'utente. Su un'installazione con traduzioni non ancora caricate si legge la chiave grezza (menu.root.administration), non l'etichetta.
  • La posizione in tabella (nth-child): l'ordine delle colonne è metadato, e un mcordine diverso cambia il test.
  • `window.ng.getComponent` come unica strada: vale solo in sviluppo (vedi sopra).

Note operative

  • Il flag __wuicE2E è un interruttore dei test: il prodotto non lo imposta mai, e nessuna funzionalità dipende dalla sua presenza.
  • Gli attributi DOM sono contratto pubblico: se un test si appoggia a un attributo nuovo, quell'attributo va aggiunto al componente e documentato qui, non dedotto da una struttura interna.
  • Un cmp nullo restituito da getComponent significa "non ispezionabile", non "senza dati": distinguere i due casi nei messaggi d'errore evita diagnosi che partono nella direzione sbagliata.