Controls hinzufügen

Modelliere die interaktiven Elemente eines Screens als Controls — Locators plus Actions — damit jeder darauf aufbauende TestStep dasselbe Element auf dieselbe Weise auflöst.

← Zurück zur Übersicht · 🇬🇧 English · ← Neue App hinzufügen · TestSteps bauen →


Verstehe, was ein Control modelliert

Ein Control kapselt alles, was nötig ist, um eine Art Element auf deinem Screen zu finden und zu bedienen: die Locator-Strategie (eine geordnete Liste von Core.xpath-Fragmenten) plus eine kleine Menge exportierter Funktionen (click, fill, checkVisible, …), die TestSteps per Label aufrufen. Eine Control-Datei deckt jede Instanz dieser Element-Art auf dem Screen ab — eine einzige Button.ts bedient jeden Button, parametrisiert über sein Label, statt eine Datei pro konkretem Button. Siehe Konzepte — 1_Controls/ für die Einordnung ins Drei-Schichten-Bild; diese Seite zeigt, wie man einen wirklich schreibt.


Wähle Locators für deinen Control

Tagge jeden Locator mit Core.xpath statt mit einem rohen Template-Literal-String:

Core.xpath`.//button[.='${label}']`

Core.xpath erzeugt zur Laufzeit exakt denselben String wie das äquivalente rohe Template-Literal — seine einzige zusätzliche Aufgabe ist, den Locator als Writeback-fähig zu markieren, damit eine Self-Healing-Reparatur genau diese Zeile später findet und aktualisiert. Das Taggen kostet nichts und ist immer sicher, egal ob du Self-Healing je aktivierst.

Ein parametrischer Locator interpoliert einen Laufzeit-Wert (${label}, ${rowIndex}) statt ihn fest zu verdrahten — jeder Control oben ist parametrisch, weil eine einzige locate(label)-Funktion jeden Button auf dem Screen bedient. Ein konkreter Locator verdrahtet einen bestimmten Wert fest und matcht nur ein Element. Bevorzuge parametrisch, wann immer dieselbe Element-Form mit unterschiedlichen Labels wiederkehrt — der Normalfall — weil ein geheilter Wert, der zur Form deines Templates passt, als Template-Erweiterung statt als Einweg-Literal re-emittiert wird. Siehe Self-Healing — Erkennen, wenn ein geheilter Wert bereits zu einem parametrischen Locator passt für die Praxis-Bedeutung bei einer Heilung.


Füge einen Button-Control hinzu

// 2_Apps/DeineApp/1_Controls/Button.ts
import * as Core from '@Core/References';

async function locate(
    label: string,
    searchTimeout: number = Core.Constant.searchTimeout,
    checkExists: boolean = true,
    checkVisible: boolean = true,
): Promise<Core.Locator | undefined> {
    return Core.findLocators(
        undefined,
        [
            Core.xpath`.//button[.='${label}']`,
            Core.xpath`.//*[@role='button' and .='${label}']`,
        ],
        searchTimeout,
        checkExists,
        checkVisible,
    );
}

/** Klickt den Button, der über `label` identifiziert wird. */
export async function click(label: string): Promise<void> {
    const target = await locate(label);
    await Core.Action.click(target);
}

/** Prüft, dass der Button, der über `label` identifiziert wird, sichtbar ist. */
export async function checkVisible(label: string): Promise<void> {
    const target = await locate(label, Core.Constant.searchTimeoutNotExists, false, false);
    await Core.Check.isVisible(target, true);
}

Ein locate-Helfer, privat in der Datei, löst das Element auf; jede exportierte Funktion ruft ihn auf und tut dann genau eine Sache — eine Action (Core.Action.*) oder eine Assertion (Core.Check.*). searchTimeoutNotExists ist ein kürzeres Timeout für Assertions, die schnell scheitern dürfen (eine Sichtbarkeitsprüfung sollte nicht das volle Default-Timeout abwarten, bevor sie auf “nicht sichtbar” schließt).


Füge einen Textfield-Control hinzu

// 2_Apps/DeineApp/1_Controls/Textfield.ts
import * as Core from '@Core/References';

async function locate(
    label: string,
    searchTimeout: number = Core.Constant.searchTimeout,
    checkExists: boolean = true,
    checkVisible: boolean = true,
): Promise<Core.Locator | undefined> {
    return Core.findLocators(
        undefined,
        [
            Core.xpath`.//input[@aria-label='${label}']`,
            Core.xpath`.//label[.='${label}']/following::input[1]`,
        ],
        searchTimeout,
        checkExists,
        checkVisible,
    );
}

/** Befüllt das Textfeld, das über `label` identifiziert wird, mit `value`. */
export async function fill(label: string, value: string): Promise<void> {
    const target = await locate(label);
    await Core.Action.fill(target, value);
}

/** Liest den aktuellen Wert des Textfelds, das über `label` identifiziert wird. */
export async function getText(label: string): Promise<string> {
    const target = await locate(label);
    return target instanceof Core.StrategyLocator ? target.getText() : ((await target?.inputValue()) ?? '');
}

label ist der parametrische Teil jedes Locators oben — dieselbe zweizeilige locate-Form deckt jedes Textfeld auf dem Screen ab, adressiert über sein zugängliches Label statt über einen fest verdrahteten Selector pro Feld.


Füge einen Combobox-Control hinzu

// 2_Apps/DeineApp/1_Controls/Combobox.ts
import * as Core from '@Core/References';

async function locate(
    label: string,
    searchTimeout: number = Core.Constant.searchTimeout,
    checkExists: boolean = true,
    checkVisible: boolean = true,
): Promise<Core.Locator | undefined> {
    return Core.findLocators(
        undefined,
        [Core.xpath`.//select[@aria-label='${label}']`],
        searchTimeout,
        checkExists,
        checkVisible,
    );
}

/** Wählt `option` in der Combobox, die über `label` identifiziert wird. */
export async function select(label: string, option: string): Promise<void> {
    const target = await locate(label);
    await Core.Action.selectOption(target, option);
}

/** Prüft, dass die Combobox, die über `label` identifiziert wird, aktuell `expected` ausgewählt hat. */
export async function checkOption(label: string, expected: string): Promise<void> {
    const target = await locate(label, Core.Constant.searchTimeoutNotExists, false, false);
    await Core.Check.textOrValueIsSet(target, expected);
}

Wieder dieselbe Drei-Teile-Form — locate plus eine Action (select) plus eine Assertion (checkOption) — obwohl eine Combobox mehr bewegliche Teile hat als ein Button. Füge getValue() hinzu (spiegelt Textfield.getText() oben), sobald ein TestStep die aktuelle Auswahl zurücklesen statt nur prüfen muss.


Füge Locators hinzu, die UI-Änderungen überstehen

Ordne Fragmente am spezifischsten zuerst und bleibe strukturell — Textinhalt, ARIA-Rolle, zugehöriges <label> — statt zu @id, [@data-testid] oder [@data-cy] zu greifen:

// Vermeiden — brüchig, an ein spezifisches generiertes Build-Attribut gebunden
Core.xpath`.//*[@id='submit-btn-42']`

// Bevorzugen — strukturell, übersteht eine Markup-/Build-Tool-Änderung
Core.xpath`.//button[.='${label}']`

Das ist genau die Regel, die der Pre-Commit-Hook als R4 — XPath-Style durchsetzt; siehe Skeleton Conventions — R4 für den vollständigen Check und wie du eine Verletzung behebst.

💡 Nur Desktop-Controls: Core.SemanticType. Ein Web-Control braucht das nie — das DOM-Tag und die ARIA-Rolle sagen einer Vision-basierten Heilung schon, was für ein Element sie vor sich hat. Ein Desktop-/Appium-Windows-Control profitiert davon, den passenden Core.SemanticType-Wert (SemanticType.Button, SemanticType.Textfield, …) neben seinem Locator zu übergeben: das gibt einem Heil-Versuch denselben “was ist das für ein Element”-Hinweis, den ein Web-Control gratis aus dem Markup bekommt.


Wo die Control-Datei hingehört

2_Apps/DeineApp/1_Controls/Button.ts
2_Apps/DeineApp/1_Controls/Textfield.ts
2_Apps/DeineApp/1_Controls/Combobox.ts

Eine Datei pro Element-Art, benannt nach dieser Art (Button.ts, nicht Control_Button.ts oder ButtonControl.ts), direkt unter 1_Controls/. 2_Apps/1_Global/References.ts re-exportiert jede Datei mit einem Controls-präfigierten Namespace:

// 2_Apps/1_Global/References.ts
export * as ControlsButton from '../DeineApp/1_Controls/Button';
export * as ControlsTextfield from '../DeineApp/1_Controls/Textfield';
export * as ControlsCombobox from '../DeineApp/1_Controls/Combobox';

Ein TestStep importiert und ruft ihn dann als ControlsButton.click(label) auf — siehe als Nächstes TestSteps bauen dafür, wie aus einem Control ein Tester-facing Step wird.


Nächster Schritt: TestSteps bauen

Mit deinen Controls an Ort und Stelle komponierst du sie zu TestSteps, die ein TestCase-Autor beim Namen aufruft — kein Locator in Sicht.

Weiter: TestSteps bauenCore.defineTestStep, Step-Typen und lokalisierte Beschreibungen.


📧 Fragen? Kontakt: jens.szelag@itsbusiness.ch

itsbusiness AG · Bern · Schweiz