TestSteps bauen

Komponiere Controls zu Tester-facing TestSteps — den Business-Level-Aktionen, die ein TestCase-Autor beim Namen aufruft, mit einer Beschreibung, die er lesen kann, ohne die Datei zu öffnen.

← Zurück zur Übersicht · 🇬🇧 English · ← Controls hinzufügen · Deinen TestCase komponieren →


Verstehe, was ein TestStep modelliert

Ein TestStep kombiniert einen oder mehrere Control-Aufrufe zu einer einzigen geschäftlich bedeutsamen Aktion und gibt ihr einen Namen, den ein nicht-technischer Leser erkennt — TS_Main_Button_Click, nicht clickTheSubmitButton. Wo ein Control weiß, wie man ein Element findet und bedient, weiß ein TestStep, welche Controls in welcher Reihenfolge für eine Tester-facing Aktion aufzurufen sind; ein TestCase ruft dann nur noch TestSteps auf, nie direkt einen Control. Ein TestStep ist auch die Stelle, an der ein Referenz-Screenshot für Self-Healing gebunden wird — siehe Self-Healing — Setup — weil diese Bindung im selben refId-Argument steckt, das jeder Step unten akzeptiert.


Wähle den TestStep-Typ

Der Tester… Step-Typ Beispiel
interagiert mit einem Element auf der Main-View TS_Main_* TS_Main_Button_Click
löst einen modalen Dialog auf TS_Dialog_* TS_Dialog_Button_Click
reagiert auf eine Bestätigungs- oder Fehlermeldung TS_Message_* TS_Message_Button_Click
startet/stoppt/navigiert die App selbst TS_Execution_* TS_Execution_Start
tut etwas, das noch keine Control×Action-Kombination abdeckt TS_Custom_* siehe Custom Steps

TS_Main/TS_Dialog/TS_Message nutzen alle dieselbe Core.defineTestStep-Factory und dasselbe Control-Set — nur der semantische Scope unterscheidet sich. TS_Execution nutzt eine eigene Factory, Core.defineExecutionStep, weil es die App-Session verwaltet statt eines Screen-Elements.

🛡️ Automatisch durchgesetzt. Der Pre-Commit-Hook prüft jeden TS_*-Export-Namen gegen TS_<Type>_<Element>_<Action> und jeden defineTestStep-run-Callback auf die unten gezeigten führenden Parameter pageLogName/sectionName — siehe Skeleton Conventions — R2 und R3.


Baue einen TS_Main-Step

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

/** Klickt den Button, der über `label` identifiziert wird. */
export const TS_Main_Button_Click = Core.defineTestStep('DeineApp', {
    descriptionI18n: {
        key: 'TS_Main_Button_Click',
        values: (pageLogName: string, _sectionName: string, label: string) => ({ pageLogName, label }),
    },
    run: (_pageLogName: string, _sectionName: string, label: string) => ControlsButton.click(label),
});

Core.defineTestStep(appId, spec) versteckt Inspector.bindReference(refId), das App-Scoping, das zum Auflösen der Controls von appId nötig ist, und Step.numberedStep — du schreibst descriptionI18n (was Tester lesen) und run (was passiert). Die Call-Shape der zurückgegebenen Funktion ist (refId, pageLogName, sectionName, label) => Promise<void>; übergib refId = '', wenn für diesen Aufruf keine Self-Healing-Referenz gebunden ist:

await Project.DeineApp.TS_Main_Button_Click('', 'Registrierung', '', 'Absenden');

runs erste zwei Parameter, pageLogName und sectionName, werden im Funktionskörper nicht genutzt (daher das _-Präfix), sind aber trotzdem Teil der Signatur — sie fließen direkt in descriptionI18n.values, damit der gerenderte Step-Titel “Auf der Seite ‘Registrierung’ auf die Schaltfläche ‘Absenden’ klicken” lauten kann, ohne dass run selbst diesen Kontext braucht.


Baue einen TS_Dialog-Step

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

/** Klickt den Button, der über `label` identifiziert wird, innerhalb des (aktuell offenen) Dialogs. */
export const TS_Dialog_Button_Click = Core.defineTestStep('DeineApp', {
    descriptionI18n: {
        key: 'TS_Dialog_Button_Click',
        values: (pageLogName: string, _sectionName: string, dialogName: string, label: string) => ({ pageLogName, dialogName, label }),
    },
    run: (_pageLogName: string, _sectionName: string, _dialogName: string, label: string) => ControlsButton.click(label),
});

Der einzige strukturelle Unterschied zu TS_Main: ein zusätzlicher dialogName-Parameter, direkt nach pageLogName/sectionName, damit der gerenderte Titel den Dialog benennen kann (“Im Dialog ‘Löschen bestätigen’ auf ‘Ja’ klicken”). Der Control-Aufruf darunter ist unverändert — ein modaler Dialog rendert weiterhin ins Seiten-DOM, also findet ControlsButton.click(label) ihn genauso wie einen Main-View-Button.


Baue einen TS_Message-Step

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

/** Prüft, dass die aktuell gezeigte Meldung `expectedText` enthält. */
export const TS_Message_CheckText = Core.defineTestStep('DeineApp', {
    descriptionI18n: {
        key: 'TS_Message_CheckText',
        values: (pageLogName: string, _sectionName: string, expectedText: string) => ({ pageLogName, expectedText }),
    },
    run: async (_pageLogName: string, _sectionName: string, expectedText: string) => {
        const target = await Core.findLocators(
            undefined,
            [Core.xpath`.//*[contains(@class,'message') and contains(.,'${expectedText}')]`],
            Core.Constant.searchTimeoutNotExists,
            false,
            false,
        );
        await Core.Check.exists(target, true);
    },
});

/** Klickt den Button, der über `label` identifiziert wird, innerhalb der (aktuell sichtbaren) Meldung (z.B. "Schließen"). */
export const TS_Message_Button_Click = Core.defineTestStep('DeineApp', {
    descriptionI18n: {
        key: 'TS_Message_Button_Click',
        values: (pageLogName: string, _sectionName: string, label: string) => ({ pageLogName, label }),
    },
    run: (_pageLogName: string, _sectionName: string, label: string) => ControlsButton.click(label),
});

Eine Bestätigungs-/Fehlermeldung verdient selten einen eigenen Control — ein einmaliger Core.findLocators-Check direkt in run, wie TS_Message_CheckText oben, ist die pragmatische Wahl für eine einzelne Aufrufstelle. Greife zu Core.Check.exists, wenn nur die Existenz zählt, oder fädle den aufgelösten Locator in Core.Check.textOrValueIsSet, wenn auch der exakte Text zählt.


Dynamischen Text mit dem **-Wildcard matchen

Ein label, pageLogName, dialogName oder erwarteter Inhalt kann das Token ** enthalten, um einen stabilen Teil eines Texts zu matchen und dabei einen zur Laufzeit wechselnden Teil zu überspringen — eine laufende Bestellnummer, ein Zeitstempel, eine generierte ID — ohne dafür ein XPath von Hand zu schreiben:

Match-String Matcht
Bestellung bestätigt exakter Text Bestellung bestätigt (kein ** vorhanden — heutiges Exact-Match-Verhalten, unverändert)
Bestellung** beginnt mit Bestellung
**bestätigt endet mit bestätigt
**Bestellung** enthält Bestellung an beliebiger Stelle
Bestellung**2024 beginnt mit Bestellung UND endet mit 2024, in dieser Reihenfolge
await Project.DeineApp.TS_Main_Label_ByXpath_CheckIsEqual('Bestellungen', '', './/tr[1]/td[2]', 'Bestellung #**');
await Project.DeineApp.TS_Main_Button_Click('Bestellungen', '', 'Bestellung ** löschen');

Leerraum rund um den gematchten Text wird vor dem Vergleich normalisiert, sodass zufällige Formatierungsunterschiede allein keinen Match brechen. Ein Match-String ohne ** verhält sich exakt wie bisher — die Erweiterung ist rein additiv.

Der Wildcard greift überall dort, wo ein Control ein Element über sein Label oder seinen Text sucht, und überall dort, wo eine Check.textContent*-/textOrValueIsSet-Assertion einen erwarteten Inhalt vergleicht. Er gilt nicht für Werte, die du in die AUT hinein schreibst — Textfield.fill, Combobox.select und Link.checkHref behandeln ihr Argument immer als Literal.

💡 Ein einzelnes *. Nur das exakte Zwei-Zeichen-Token ** löst den Wildcard-Modus aus — ein einzelnes * hat keine besondere Bedeutung und wird als gewöhnliches Zeichen gematcht.


Gegen die falsche Seite oder den falschen Dialog absichern: checkTitleFromPageExists / checkTitleFromDialogExists

Jeder TS_Main_*-Step akzeptiert ein optionales, nachgestelltes checkTitleFromPageExists: boolean = false; jeder TS_Dialog_*-Step das entsprechende checkTitleFromDialogExists: boolean = false. Setzt du es auf true, prüft der Step zuerst, ob pageLogName (bzw. dialogName) tatsächlich irgendwo auf der aktuellen Seite oder im aktuellen Dialog auftaucht, bevor er seine eigene Control-Aktion ausführt:

await Project.DeineApp.TS_Dialog_Button_Click('Löschen bestätigen', '', 'Ja', true);

Ohne die Prüfung agiert ein Step auf der jeweils gerade sichtbaren Seite oder dem gerade offenen Overlay — meist korrekt, aber still falsch, wenn ein Test unerwartet woanders gelandet ist oder zwei Overlays gleichzeitig offen sind. Mit aktivierter Prüfung wirft ein Mismatch einen klaren, sofort sichtbaren “falsche Seite/falscher Dialog”-Fehler, statt dass ein grüner Test auf dem falschen Screen agiert hat.

Das Flag löst außerdem den Fall zweier gleichzeitig offener Dialoge auf: Ist es true, dient dialogName zugleich als Titel-Wächter, und der Step agiert nur gegen das Overlay, dessen Inhalt tatsächlich dazu passt. dialogName/pageLogName können dabei gleichzeitig den **-Wildcard aus dem Abschnitt oben nutzen — checkTitleFromDialogExists kombiniert mit 'Bestellung ** bestätigt' löst so genau den Bestätigungsdialog der gerade offenen Bestellung auf.

Bleibt das Flag auf seinem Default false, verhält sich jeder bestehende Aufruf unverändert — es läuft keine Titel-Prüfung, und der Step löst die erste passende Seite/das erste passende Overlay auf, exakt wie vor Einführung dieses Flags.


Einen Wert aus einem Get-/Read-Step liefern

Core.defineTestStep<TArgs, TReturn = void> ist generisch über den Wert, den run an den Aufrufer zurückgibt. Jeder Step oben gibt void zurück — eine Aktion führt etwas aus, ein Check*-Step prüft und wirft bei Abweichung, keiner der beiden gibt etwas zurück. Ein Get*-/Read*-/List*-Step ist anders: er ermittelt einen Wert, also fließt runs eigener Rückgabetyp direkt durch — kein Typ-Argument von Hand zu schreiben, TypeScript leitet TReturn aus runs Rückgabetyp ab:

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

/** Liest den aktuellen Wert des Textfelds, das über `label` identifiziert wird. */
export const TS_Main_Textfield_GetText = Core.defineTestStep('DeineApp', {
    descriptionI18n: {
        key: 'TS_Main_Textfield_GetText',
        values: (pageLogName: string, _sectionName: string, label: string) => ({ pageLogName, label }),
    },
    run: (_pageLogName: string, _sectionName: string, label: string) => ControlsTextfield.getText(label),
});

Die Call-Shape der exportierten Funktion wird zu (refId, pageLogName, sectionName, label) => Promise<string> — der Wert wird weiterhin exakt wie zuvor über Core.Step.logParam(...) protokolliert, aber ein TestCase kann ihn jetzt zusätzlich zuweisen und direkt weiterverwenden:

// 3_Cases/TC_MyFlow.spec.ts
import * as Project from '@GlobalRef';

const currentValue = await Project.DeineApp.TS_Main_Textfield_GetText('', 'Profile', '', 'Display name');
await Core.Check.textOrValueIsSet(currentValue, true, 'Jane Doe');
Step-Verb TReturn Verhalten
Get* / Read* / List* Typ des ermittelten Werts gibt den Wert zurück und protokolliert ihn, Call-Shape sonst unverändert
Check* void (Default) bleibt eine Prüfung — wirft bei Abweichung, nichts zurückzugeben
beliebiges Aktions-Verb (Click, Fill, Start, …) void (Default) bleibt rückgabefrei

💡 Nichts zu migrieren. TReturn ist standardmäßig void, daher kompiliert und verhält sich jeder Step, der vor diesem Generic geschrieben wurde, byte-identisch — nur ein Step, dessen run-Callback selbst einen Wert zurückgibt, erhält automatisch einen Nicht-void-Rückgabewert.


Baue einen TS_Execution-Step

// 2_Apps/DeineApp/2_Steps/TS_Execution.ts
import * as Core from '@Core/References';

/** Startet den Browser und navigiert zu `env.url`. */
export const TS_Execution_Start = Core.defineExecutionStep(
    'DeineApp',
    'Start',
    (env) => ({
        descriptionI18n: {
            key: 'TS_Execution_Start',
            values: () => ({ url: env.url }),
        },
        run: () => Core.I_BrowserHandler.start(env.page, env.url, { waitForReady: true }),
    }),
);

/** Schließt die Browser-Session logisch und navigiert zu `about:blank`. */
export const TS_Execution_Close = Core.defineExecutionStep(
    'DeineApp',
    'Close',
    (env) => ({
        descriptionI18n: {
            key: 'TS_Execution_Close',
            values: () => ({}),
        },
        run: () => Core.I_BrowserHandler.close(),
    }),
);

defineExecutionStep(appName, action, factory) ist env-aware und family-aware: factory bekommt { page, url, appConfig } für eine Web-AUT oder { executable, appiumUrl, appConfig } für eine Desktop-AUT — automatisch aus GlobalConfig.apps['DeineApp'] aufgelöst, kein page/url von Hand getippt in dieser Datei. Siehe API-Referenz — Sektion 16 für die vollständige family-aware env-Form.


Füge Step-Beschreibungen hinzu, die deine Tester lesen können

descriptionI18n.key schlägt sich selbst in einem Sibling-.i18n.json-Katalog nach, ein Eintrag pro Step, ${placeholder}-Namen passend zu den an values übergebenen Parametern:

{
  "TS_Main_Button_Click": {
    "en": "On the page '${pageLogName}' click on the button with the label '${label}'",
    "de": "Auf der Seite '${pageLogName}' auf die Schaltfläche mit dem Label '${label}' klicken"
  }
}

Jede Locale-Variante muss exakt dieselben ${placeholder}-Namen wie der en-Eintrag wiederverwenden — ein Validator setzt das zur Commit-Zeit durch, sodass ein Übersetzer, der einen Platzhalter umbenennt oder weglässt, den Build zum Scheitern bringt statt in einer Locale still undefined zu rendern. Passende Platzhalter sind auch, was values als eine einzige, über alle Locales geteilte Funktion erlaubt: sie gibt ein einziges reines Objekt zurück, und der Katalog-String jeder Locale entscheidet, welche seiner Keys sie zeigt und in welcher Reihenfolge. Siehe Step-Description Localization für den vollständigen Platzhalter-Vertrag, das Hinzufügen einer neuen Locale und die Laufzeit-Auflösungsreihenfolge (Core.i18n.setLocale(...)CC_TESTFRAMEWORK_LOCALEGlobalConfig.language'en').


Wo die TestStep-Datei hingehört

2_Apps/DeineApp/2_Steps/TS_Main.ts
2_Apps/DeineApp/2_Steps/TS_Main.i18n.json
2_Apps/DeineApp/2_Steps/TS_Dialog.ts
2_Apps/DeineApp/2_Steps/TS_Dialog.i18n.json

Eine Datei pro Step-Familie, ihr .i18n.json-Katalog direkt daneben — der Pre-Commit-Hook lehnt eine Step-Datei mit mindestens einem defineTestStep-Aufruf, aber ohne Sibling-Katalog ab. Siehe Deinen ersten TestCase schreiben — Re-Export-Konvention dafür, warum 2_Apps/1_Global/References.ts jeden Step flach re-exportiert (kein Steps-präfigierter Namespace, anders als bei Controls) und wie ein TestCase ihn dann aufruft.


Nächster Schritt: deinen TestCase komponieren

Mit TestSteps für jede Aktion, die dein Business-Flow braucht, komponierst du sie zu einem TestCase.

Weiter: Deinen ersten TestCase schreiben — Naming-Konventionen, das Re-Export-Barrel und dein erster grüner Flow.


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

itsbusiness AG · Bern · Schweiz