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-editore dawuic-parametric-dialog. - Handle
__wuicCmpper 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:
| Attributo | Contenuto |
|---|---|
data-field-name | nome logico della colonna (mc_nome_colonna) |
data-field-id | mc_id della colonna, vuoto se non disponibile |
data-widget-type | tipo di widget risolto (mc_ui_column_type) |
data-editable | true/false secondo i metadati e lo stato della form |
data-hide-in-edit | true/false (mc_hide_in_edit) |
data-is-edit-form | true in edit form, false in filtro/vista |
data-has-record | true quando l'oggetto record è assegnato |
data-field-value | valore corrente del campo, serializzato |
Il selettore di riferimento è quindi il nome del campo, non la posizione:
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:
| Segnale | Significato |
|---|---|
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:
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:
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'è anchewindow.ng; - in produzione solo se la pagina ha dichiarato
globalThis.__wuicE2E = trueprima dell'avvio dell'applicazione.
Il bootstrap dei test lo fa con uno script di init, non il prodotto:
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:
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:
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_translationse 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 unmcordinediverso 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
cmpnullo restituito dagetComponentsignifica "non ispezionabile", non "senza dati": distinguere i due casi nei messaggi d'errore evita diagnosi che partono nella direzione sbagliata.