Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SPSH 1131 Playwright-Test des Dateiimports #88

Merged
merged 15 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ Empfohlen wird VS-Code

#### npx playwright codegen https://spsh.staging.spsh.dbildungsplattform.de

#### npx playwright codegen https://test.dev.spsh.dbildungsplattform.de

#### npx playwright codegen https://localhost:8099/ --ignore-https-errors

### Tests lokal ausführen:
Expand Down
8 changes: 4 additions & 4 deletions base/api/testHelperPerson.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { UserInfo } from "./testHelper.page";
import { HeaderPage } from '../../pages/Header.page';
import { LoginPage } from '../../pages/LoginView.page';
import { faker } from '@faker-js/faker';
import { lehrkraftOeffentlichRolle } from '../roles';
import { lehrkraftOeffentlichRolle } from '../rollen';
import { generateNachname, generateVorname, generateKopersNr } from "../testHelperGenerateTestdataNames";
import { testschule } from "../organisation";

Expand Down Expand Up @@ -84,13 +84,13 @@ export async function addSecondOrganisationToPerson(page: Page, personId: string
expect(response.status()).toBe(200);
}

export async function deletePersonen(page: Page, personId: string): Promise<void> {
export async function deletePerson(page: Page, personId: string): Promise<void> {
const response = await page.request.delete(FRONTEND_URL + `api/personen/${personId}`, {});
expect(response.status()).toBe(204);
}

export async function getPersonId(page: Page, Benutzername: string): Promise<string> {
const response = await page.request.get(FRONTEND_URL + `api/personen-frontend?suchFilter=${Benutzername}`, {});
export async function getPersonId(page: Page, searchString: string): Promise<string> {
const response = await page.request.get(FRONTEND_URL + `api/personen-frontend?suchFilter=${searchString}`, {});
expect(response.status()).toBe(200);
const json = await response.json();
return json.items[0].person.id;
Expand Down
File renamed without changes.
49 changes: 27 additions & 22 deletions base/testHelperDeleteTestdata.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,42 @@
import { deleteRolle, getRolleId} from "./api/testHelperRolle.page";
import { deletePersonen, getPersonId } from "./api/testHelperPerson.page";
import { deletePerson, getPersonId } from "./api/testHelperPerson.page";
import { getKlasseId, deleteKlasse } from "./api/testHelperOrganisation.page";


export async function deletePersonById(personId, page){ // personId ist ein array mit allen zu löschenden Benutzern
for (const item in personId){
await deletePersonen(page, personId[item]);
}
for (const item in personId){
await deletePerson(page, personId[item]);
}
}

export async function deleteRolleById(roleId, page){ // roleId ist ein array mit allen zu löschenden Rollen
for (const item in roleId){
await deleteRolle(page, roleId[item]);
}
for (const item in roleId){
await deleteRolle(page, roleId[item]);
}
}

export async function deleteRolleByName(roleName, page){ // roleName ist ein array mit allen zu löschenden Rollen
for (const item in roleName){
const roleId = await getRolleId(page, roleName[item]);
await deleteRolle(page, roleId);
}
for (const item in roleName){
const roleId = await getRolleId(page, roleName[item]);
await deleteRolle(page, roleId);
}
}

export async function deletePersonByUsername(username, page){ // username ist ein array mit allen zu löschenden Benutzern
for (const item in username){
const personId = await getPersonId(page, username[item]);
await deletePersonen(page, personId);
}
export async function deletePersonenBySearchStrings(page, searchStringArray){
for (const item in searchStringArray){
const personId = await getPersonId(page, searchStringArray[item]);
await deletePerson(page, personId);
}
}

export async function deleteClassByName(className, page){ // className ist ein array mit allen zu löschenden Klassen
for (const item in className){
const classId = await getKlasseId(page, className[item]);
await deleteKlasse(page, classId);
}
}
export async function deletePersonBySearchString(page, searchString){
const personId = await getPersonId(page, searchString);
await deletePerson(page, personId);
}

export async function deleteKlasseByName(klassenName, page){ // klassenName ist ein array mit allen zu löschenden Klassen
for (const item in klassenName){
const klassenId = await getKlasseId(page, klassenName[item]);
await deleteKlasse(page, klassenId);
}
}
43 changes: 25 additions & 18 deletions docs/struktur.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Struktur der Playwright-Tests

In diesem Dokument wird beschrieben, wie wir unsere Playwright-Tests strukturieren.

## Zielsetzung

Das Ziel bei unseren Tests ist es, die Benutzerinteraktionen so abstrakt wie möglich zu beschreiben.
Das heißt zum Beispiel, dass wir nicht in jedem Test erneut beschreiben wollen, wie man eine ComboBox bedient.
Stattdessen soll der Test lediglich fordern, dass aus einer ComboBox ein Eintrag ausgewählt wird.
Expand All @@ -19,31 +17,40 @@ Tests werden parallel ausgeführt, wenn sie in verschiedenen Dateien stehen.
Es ist daher ratsam, Testdateien klein zu halten und entsprechend sinnvoll zu schneiden.

## Technische Umsetzung

### Verzeichnisstruktur

`/base`: Helper für Tests

`/elements`: wiederkehrende Seitenelemente (TODO: über Umbenennung in "components" nachdenken)

`/pages`: Seitenobjekte

`/tests`: Die eigentlichen Tests

### Verzeichnis `base`
#### `base`: Helper für Tests
Im `base`-Verzeichnis befinden sich Helper, die von den Tests aufgerufen werden aber nicht direkt zu den Tests gehören.
Darunter fällt zum Beispiel die Erzeugung von Testdaten direkt über API.

### Verzeichnis `elements`
In Elements befinden sich semantische Wrapper um Locators, die wiederkehrende Elemente auf den Seiten testen.
#### `elements`: wiederkehrende Seitenelemente (TODO: über Umbenennung in "components" nachdenken)
In `elements` befinden sich semantische Wrapper um Locators, die wiederkehrende Elemente auf den Seiten testen.
Beispielsweise legen wir hier eine Klasse für "Comboboxen" ab, die die nötigen Schritte zur Auswahl von Elementen kapselt.

### Verzeichnis `pages`
#### `fixtures`: Ablage von Testdaten
Das `fixtures`-Verzeichnis beinhaltet Testdaten, die als JSON oder in beliebigen anderen Formaten abgelegt werden können.

#### `pages`: Seitenobjekte
Im Verzeichnis `pages` liegen Seitenrepräsentationen.
Eine Seite hat dabei high-level-Funktionen, zur Navigation und zum Aufruf von Seitenfunktionalitäten.

### Tags
#### `/tests`: Die eigentlichen Tests

### Tags
Wir verwenden Tags, um diejenigen Tests auszuwählen, die ausgeführt werden sollen.
Zur Zeit unterscheiden wir nach Ausführungslänge (@long, @short).
Das muss aber nicht die einzige Unterscheidungskategorie bleiben.
Das muss aber nicht die einzige Unterscheidungskategorie bleiben.

### Navigation mit FromAnywhere
Für die Verwendung von mehreren Pages in den einzelnen Tests müssen sie nicht in jedem Test importiert und einzeln navigiert werden. Stattdessen ist es sinnvoll, verlinkte Pages in dedizierten Funktionen der einzelnen Pages zu returnen und die Funktionen im Test aufzurufen. Das spart Code und Wartungsaufwand.

Als Einstieg dient dazu die Klasse `FromAnywhere`, über die man von jeder Route aus die LandingPage aufrufen kann, um von dort aus weiter zu navigieren.

#### Beispiel
```
FromAnywhere
.start()
.goToLogin()
.login(ADMIN, PW)
.goToAdminstration()
.goToBenutzerimport();
```
10 changes: 10 additions & 0 deletions elements/ComboBox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,14 @@ export class ComboBox {
await item.waitFor({ state: "visible" });
await item.click();
}

public async searchByTitle(title: string): Promise<void> {
await this.locator.click();
await this.locator.fill(title);
const item = this.itemsLocator.filter({
has: this.page.getByText(title, { exact: true }),
});
await item.waitFor({ state: "visible" });
await item.click();
}
}
6 changes: 6 additions & 0 deletions fixtures/Benutzerimport_Lernrolle_UTF-8.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Nachname;Vorname;Klasse
Mustermann-Importtest;Max;1A
Orton-Importtest;John;2B
Laser-Importtest;Eduard;1B
Cena-Importtest;Randy;1B
Musterfrau-Importtest;Maria;2A
2 changes: 1 addition & 1 deletion pages/LandingView.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class LandingPage {
this.button_Anmelden = page.getByTestId("login-button");
}

public async login(): Promise<LoginPage> {
public async goToLogin(): Promise<LoginPage> {
await this.button_Anmelden.click();
return new LoginPage(this.page);
}
Expand Down
4 changes: 2 additions & 2 deletions pages/LoginView.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export class LoginPage {
}

async login(
username = process.env.USER,
password = process.env.PW,
username: string = process.env.USER as string,
password: string = process.env.PW as string,
): Promise<StartPage> {
await expect(this.text_h1).toBeVisible();
await this.input_username.click();
Expand Down
9 changes: 8 additions & 1 deletion pages/MenuBar.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RolleCreationViewPage } from "./admin/RolleCreationView.page";
import {RolleManagementViewPage} from "./admin/RolleManagementView.page";
import { SchuleManagementViewPage } from "./admin/SchuleManagementView.page";
import { SchuleCreationViewPage } from "./admin/SchuleCreationView.page";
import { HeaderPage } from "./Header.page";
import { PersonImportViewPage } from "./admin/PersonImportView.page";

export class MenuPage {
readonly page: Page;
Expand All @@ -22,6 +22,7 @@ export class MenuPage {
readonly menueItem_AlleSchulenAnzeigen: Locator;
readonly menueItem_SchuleAnlegen: Locator;
readonly label_Schultraegerverwaltung: Locator;
readonly menuItemBenutzerImportieren: Locator;

constructor(page) {
this.page = page;
Expand All @@ -36,6 +37,7 @@ export class MenuPage {
this.menueItem_BenutzerAnlegen = page.getByTestId(
"person-creation-menu-item",
);
this.menuItemBenutzerImportieren = page.getByTestId("person-import-menu-item");
this.label_Klassenverwaltung = page.locator(
'[data-testid="klasse-management-title"] .v-list-item-title',
);
Expand Down Expand Up @@ -85,4 +87,9 @@ export class MenuPage {
await this.menueItem_SchuleAnlegen.click();
return new SchuleCreationViewPage(this.page);
}

public async goToBenutzerImport(): Promise<PersonImportViewPage> {
await this.menuItemBenutzerImportieren.click();
return new PersonImportViewPage(this.page);
}
}
2 changes: 1 addition & 1 deletion pages/StartView.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class StartPage {
this.card_item_schulportal_administration = page.locator('[data-testid^="service-provider-card"]', { hasText: "Schulportal-Administration" });
}

public async administration(): Promise<MenuPage> {
public async goToAdministration(): Promise<MenuPage> {
await this.card_item_schulportal_administration.click();
return new MenuPage(this.page);
}
Expand Down
51 changes: 51 additions & 0 deletions pages/admin/PersonImportView.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type Locator, Page } from '@playwright/test';
import { PersonManagementViewPage } from "./PersonManagementView.page";
import { ComboBox } from '../../elements/ComboBox';

export class PersonImportViewPage {
readonly page: Page;
readonly body: Locator;
readonly headlineBenutzerImport: Locator;
readonly schuleSelectInput: Locator;
readonly schuleSelectCombobox: ComboBox;
readonly rolleSelectInput: Locator;
readonly fileInput: Locator;
readonly discardFileUploadButton: Locator;
readonly submitFileUploadButton: Locator;
readonly uploadSuccessText: Locator;
readonly openConfirmationDialogButton: Locator;
readonly importConfirmationText: Locator;
readonly executeImportButton: Locator;
readonly importSuccessText: Locator;
readonly downloadFileButton: Locator;
readonly closeCardButton: Locator;
readonly confirmUnsavedChangesButton: Locator;

constructor(page) {
// Benutzerimport
this.page = page;
this.body = page.locator('body');
this.headlineBenutzerImport = page.getByTestId('layout-card-headline');
this.schuleSelectInput = page.getByTestId('schule-select').locator('input');
this.schuleSelectCombobox = new ComboBox(this.page, this.schuleSelectInput,);
this.rolleSelectInput = page.getByTestId('rolle-select').locator('input');
this.fileInput = page.getByTestId('file-input').locator('input');
this.discardFileUploadButton = page.getByTestId('person-import-form-discard-button');
this.submitFileUploadButton = page.getByTestId('person-import-form-submit-button');
this.uploadSuccessText = page.getByTestId('person-upload-success-text');
this.openConfirmationDialogButton = page.getByTestId('open-confirmation-dialog-button');
this.importConfirmationText = page.getByTestId('person-import-confirmation-text');
this.executeImportButton = page.getByTestId('execute-import-button');
this.importSuccessText = page.getByTestId('person-import-success-text');
this.downloadFileButton = page.getByTestId('download-file-button');
this.closeCardButton = page.getByTestId('close-layout-card-button');
this.confirmUnsavedChangesButton = page.getByTestId('confirm-unsaved-changes-button');
}

public async navigateToPersonManagementView(): Promise<PersonManagementViewPage> {
await this.closeCardButton.click();
await this.confirmUnsavedChangesButton.click();

return new PersonManagementViewPage(this.page);
}
}
42 changes: 24 additions & 18 deletions pages/admin/PersonManagementView.page.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Locator, Page, expect } from '@playwright/test';
import {PersonDetailsViewPage} from "./PersonDetailsView.page.js";
import { PersonDetailsViewPage } from "./PersonDetailsView.page";

export class PersonManagementViewPage{
readonly page: Page;
Expand All @@ -21,23 +21,29 @@ export class PersonManagementViewPage{
readonly comboboxMenuIcon_Schule_input: Locator;

constructor(page: Page){
this.page = page;
this.text_h1_Administrationsbereich = page.getByTestId('admin-headline');
this.text_h2_Benutzerverwaltung = page.getByTestId('layout-card-headline');
this.input_Suchfeld = page.locator('[data-testid="search-filter-input"] input');
this.button_Suchen = page.getByTestId('apply-search-filter-button');
this.table_header_Nachname = page.getByTestId('person-table').getByText('Nachname', { exact: true });
this.table_header_Vorname = page.getByTestId('person-table').getByText('Vorname', { exact: true });
this.table_header_Benutzername = page.getByText('Benutzername', { exact: true });
this.table_header_KopersNr = page.getByText('KoPers.-Nr.');
this.table_header_Rolle = page.getByTestId('person-table').getByText('Rolle', { exact: true });
this.table_header_Zuordnungen = page.getByText('Zuordnung(en)');
this.table_header_Klasse = page.getByTestId('person-table').getByText('Klasse', { exact: true });
this.comboboxMenuIcon_Schule = page.locator('[data-testid="schule-select"] .mdi-menu-down');
this.comboboxMenuIcon_Schule_input = page.locator('[data-testid="schule-select"] input');
this.comboboxMenuIcon_Rolle = page.locator('[data-testid="rolle-select"] .mdi-menu-down');
this.comboboxMenuIcon_Klasse = page.locator('[data-testid="klasse-select"] .mdi-menu-down');
this.comboboxMenuIcon_Status = page.locator('[data-testid="status-select"] .mdi-menu-down');
this.page = page;
this.text_h1_Administrationsbereich = page.getByTestId('admin-headline');
this.text_h2_Benutzerverwaltung = page.getByTestId('layout-card-headline');
this.input_Suchfeld = page.locator('[data-testid="search-filter-input"] input');
this.button_Suchen = page.getByTestId('apply-search-filter-button');
this.table_header_Nachname = page.getByTestId('person-table').getByText('Nachname', { exact: true });
this.table_header_Vorname = page.getByTestId('person-table').getByText('Vorname', { exact: true });
this.table_header_Benutzername = page.getByText('Benutzername', { exact: true });
this.table_header_KopersNr = page.getByText('KoPers.-Nr.');
this.table_header_Rolle = page.getByTestId('person-table').getByText('Rolle', { exact: true });
this.table_header_Zuordnungen = page.getByText('Zuordnung(en)');
this.table_header_Klasse = page.getByTestId('person-table').getByText('Klasse', { exact: true });
this.comboboxMenuIcon_Schule = page.locator('[data-testid="schule-select"] .mdi-menu-down');
this.comboboxMenuIcon_Schule_input = page.locator('[data-testid="schule-select"] input');
this.comboboxMenuIcon_Rolle = page.locator('[data-testid="rolle-select"] .mdi-menu-down');
this.comboboxMenuIcon_Klasse = page.locator('[data-testid="klasse-select"] .mdi-menu-down');
this.comboboxMenuIcon_Status = page.locator('[data-testid="status-select"] .mdi-menu-down');
}

public async navigateToPersonDetailsViewByNachname(nachname: string): Promise<PersonDetailsViewPage> {
await this.page.getByRole("cell", { name: nachname, exact: true }).click();

return new PersonDetailsViewPage(this.page);
}

public async searchBySuchfeld(name: string) {
Expand Down
Loading