Portiere PHP-Cronjobs auf Spring-Tasks und ergänze Legacy-Tests
Migriert die weiterhin benötigten Zeitaufgaben einschließlich SOAP-Importen, Betriebsstatus und Wiederanlaufkontrolle. Aktiviert 115 native Spring-Tasks im Docker-Image ohne Cron-Daemon oder PHP und dokumentiert die entfallenen HHA-Aufrufe. Ergänzt PHP-/UI-Testwerkzeuge und aktualisiert den Projektstand auf Version 0.2.0.
This commit is contained in:
1 parent
781a2b82dc
commit
9de2358480
57 files changed
+3251
-7
No files matched your search
@@ -0,0 +1,6 @@
|
||||
/reports/
|
||||
/test-results/
|
||||
/.auth/
|
||||
/node_modules/
|
||||
.env*
|
||||
!.env.example
|
||||
@@ -0,0 +1,95 @@
|
||||
# Oberflächentests der alten Votian-PHP-Anwendung
|
||||
|
||||
Die Tests bedienen die echte Anwendung unter **https://test.sb.assecutor.de**. Sie verwenden echte Anmeldung, Kundenwahl, Preisberechnung und Auftragsformulare; es gibt keine simulierten Geschäftsdaten. Die Anwendung läuft auf `172.16.0.158` aus `/data/0/votian-test/html`. Die Tests liegen daneben unter `/data/0/votian-test/tests/ui`, außerhalb des Webroots. PHP-Produktivcode wird nicht verändert.
|
||||
|
||||
## Ausführen
|
||||
|
||||
Auf dem Server, mit interaktivem Terminal:
|
||||
|
||||
```sh
|
||||
/data/0/votian-test/tests/ui/run-server.sh
|
||||
```
|
||||
|
||||
Benutzername und Passwort werden abgefragt. Der Standardlauf speichert keine Aufträge; der Speichertest wird ausdrücklich übersprungen. Für den vollständigen Lauf einschließlich **eines neuen Testauftrags**:
|
||||
|
||||
```sh
|
||||
/data/0/votian-test/tests/ui/run-server.sh --create-order
|
||||
```
|
||||
|
||||
Einen bereits angelegten Testauftrag erneut prüfen, ohne einen weiteren anzulegen:
|
||||
|
||||
```sh
|
||||
/data/0/votian-test/tests/ui/run-server.sh --verify-order 21891310 UI-TEST-20261026T094639-670e98
|
||||
```
|
||||
|
||||
Der Runner startet einen kurzlebigen Docker-Container mit `mcr.microsoft.com/playwright:v1.63.0-noble`, installiert die drei per Lockfile festgelegten npm-Pakete im Testverzeichnis und startet Firefox. Er bindet nur das Testverzeichnis ein, keine Anwendungs- oder Datenbankverzeichnisse. Docker und HTTPS-Zugriff auf die Testanwendung sind erforderlich. Parallel gestartete Läufe im selben Verzeichnis vermeiden.
|
||||
|
||||
Lokal, mit Node.js 20 oder neuer und Python 3:
|
||||
|
||||
```sh
|
||||
cd legacy-ui-tests
|
||||
npm ci
|
||||
npx playwright install firefox
|
||||
python3 run.py
|
||||
```
|
||||
|
||||
Alternativ im Projektstamm: `npm run test:legacy-ui`. Die Servervariante ist auf diesem Arbeitsplatz die geprüfte Ausführungsumgebung: Der lokale Firefox startete hier wegen eines Profilverzeichnisfehlers nicht.
|
||||
|
||||
## Testumfang
|
||||
|
||||
15 Tests prüfen:
|
||||
|
||||
- JavaScript-Fehler beim Öffnen und Aufbau der vier Frames;
|
||||
- leeres Formular und gesperrte Sendeschaltfläche;
|
||||
- Bezahlerwahl über die echte Kundenliste;
|
||||
- Auftragseingang „Sonstiges“ und Rücksetzen des Zusatzfelds;
|
||||
- Abhängigkeiten zwischen Angebot, manueller Disposition und unvollständigem Status;
|
||||
- Versicherung ein-/ausschalten, Warenwert und drei ungültige Eingaben;
|
||||
- Vertauschen und Entfernen von Stationen;
|
||||
- Vorbereitung mit zwei Testkunden, Besteller, Hinweisen und 10,00 € Festpreis;
|
||||
- optionales Speichern über „Senden“, Serverbestätigung mit Auftragsnummer und erneutes Laden einer frischen Detailansicht. Dort werden Stationsreihenfolge, Hinweise, Festpreis und „UNFERTIG!“ geprüft.
|
||||
|
||||
Es handelt sich um funktionale Tests der Einzelerfassung, nicht um vollständige Abdeckung der Verwaltung, Disposition oder Abrechnung. Die Unit-Tests stehen separat in `../legacy-php-tests/`.
|
||||
|
||||
## Testdaten und Schreibzugriffe
|
||||
|
||||
Der Speichertest nutzt ausschließlich **Testkunde Assecutor 1** (Kostenstelle `878914`) und **Testkunde Assecutor 2** (`878915`). Beide Stammsätze sind als „Testaufträge Assecutor“ gekennzeichnet. Jeder neue Auftrag erhält einen eindeutigen `UI-TEST-…`-Marker im Besteller und in beiden Stationshinweisen, darunter „NICHT AUSFÜHREN“.
|
||||
|
||||
Vor dem Senden validiert eine Netzwerksperre die tatsächlichen Formulardaten: genau zwei markierte Teststationen, Testkunde 1 als Bezahler, 10,00 €, manuelle Disposition, unvollständig, kein Kurier, keine Serien-/Mehrfachaufträge, keine Höherversicherung und `sendmail=0`. Pro Test wird höchstens ein Speichervorgang zugelassen; Wiederholungen und Retries des Speichertests werden abgewiesen. Ein vor dem Versand angelegter Eintrag in `reports/created-orders.jsonl` verhindert nach einem unklaren Ausgang erneutes Speichern, bis das Ergebnis geprüft wurde. Ein Testauftrag wird nicht automatisch gelöscht oder abgeschlossen.
|
||||
|
||||
Andere fachliche Schreibzugriffe werden vor dem Versand blockiert. Freigegeben sind Anmeldung, gelesene Nachschlagefunktionen, Kunden-/Tourensuche und Preisberechnung. Die automatische News-Ticker-Bestätigung und JavaScript-Fehlertelemetrie werden im Browser beantwortet, ohne sie zu speichern. Google-Maps-Telemetrie wird unterdrückt. Normale serverseitige Sitzungen, Zugriffsprotokolle und Folgearbeiten der PHP-Anwendung bleiben möglich; dies ist keine vollständig schreibfreie Datenbank-Sandbox.
|
||||
|
||||
Zugangsdaten werden nur abgefragt bzw. aus `VOTIAN_UI_USERNAME` und `VOTIAN_UI_PASSWORD` gelesen. Sie werden nicht in Dateien gespeichert. Anmeldung und Sitzungszustand bleiben im Speicher; Traces und Videos sind deaktiviert. Berichte und Screenshots sind von Git ausgeschlossen und können Daten der Testanwendung enthalten.
|
||||
|
||||
## Bekannter Fehler und Browserwahl
|
||||
|
||||
`jobs/job_edit.php:13` führt `top.opener.top.job_edit = this` aus, obwohl die über `target="_blank"` geöffnete Seite kein `window.opener` besitzt. Der Test „Auftragserfassung startet ohne JavaScript-Ausnahme“ bleibt deshalb absichtlich rot. Der exakt erkannte Fehler wird in den anderen Tests als Anhang protokolliert; neue JavaScript-Ausnahmen lassen jeden betroffenen Test scheitern. Es gibt kein `test.fail()` oder stilles Überspringen dieses Anwendungsfehlers.
|
||||
|
||||
Im vollständigen Speichertest trat zusätzlich `TypeError: can't access property "currentTSZ", parent.job_options.document.tourOptions is undefined` auf. Die Preisantwort greift auf ein während des Speicherns neu geladenes Formular zu (erzeugter JavaScript-Code aus `jobs/job_options.php:3357`). Das Speichern und die Prüfung der frischen Detailansicht waren trotzdem erfolgreich. Der Test bleibt wegen der erfassten Ausnahme rot; sie wird nicht als erlaubter Fehler unterdrückt. Der zeitliche Zusammenhang spricht für eine Race Condition, deren genauer Auslöser gesondert untersucht werden muss.
|
||||
|
||||
Für die Netzwerksperre mit den alten `document.write`-Popups wird Firefox verwendet. Playwright 1.63 blockiert diese Popups unter Chromium bereits durch aktiviertes Routing; das ist ein [bestätigter Playwright-Fehler](https://github.com/microsoft/playwright/issues/43496), kein Beleg für einen Speicherfehler der PHP-Anwendung. Der Ablauf wurde dafür nicht durch direkte API- oder Datenbank-Schreibzugriffe ersetzt.
|
||||
|
||||
## Ergebnisse
|
||||
|
||||
Vollständiger Serverlauf vom **26.10.2026**, mit `--create-order`: **15 ausgeführt, 13 bestanden, 2 fehlgeschlagen, 0 übersprungen**, Laufzeit 41 Sekunden. Beide Fehlschläge betreffen die oben dokumentierten JavaScript-Ausnahmen. Alle fachlichen Prüfungen des gespeicherten Auftrags waren erfolgreich.
|
||||
|
||||
Angelegte und erhaltene Testaufträge:
|
||||
|
||||
| Auftrag | Marker | Prüfung |
|
||||
| --- | --- | --- |
|
||||
| 21891310 | `UI-TEST-20261026T094639-670e98` | Erstlauf; Speicherung zusätzlich per lesender Datenbankabfrage bestätigt, frische Detailansicht geprüft |
|
||||
| 21891311 | `UI-TEST-20261026T095313-b89f03` | Vollständiger Lauf; Serverbestätigung und Detailansicht geprüft |
|
||||
|
||||
Beide verwenden 10,00 € Festpreis, Testkunde 1 als Bezahler, manuelle Disposition, „unvollständig“ und keine Kurierzuweisung. Sie wurden nicht abgeschlossen oder gelöscht.
|
||||
|
||||
Geprüftes Container-Image: `sha256:2c1f4e0fd6450f43ddb46d60c2a6df30855a8588e165b1f2559fb0eda8d7ff35` (Tag `mcr.microsoft.com/playwright:v1.63.0-noble`). Im vollständigen Lauf wurde die Datenübertragung nicht simuliert; die Zugangsdaten kamen aus der interaktiven Eingabe.
|
||||
|
||||
Nach jedem Lauf: `reports/html/index.html`, `reports/junit.xml`, `reports/results.json` und `test-results/` mit Screenshots. Die Liste angelegter Aufträge wird separat fortgeschrieben und nicht durch den nächsten Testlauf gelöscht.
|
||||
|
||||
HTML-Bericht öffnen:
|
||||
|
||||
```sh
|
||||
npx playwright show-report reports/html
|
||||
```
|
||||
|
||||
Der Rückgabecode bleibt bei einem offenen Anwendungsfehler ungleich null. Maßgeblich ist der letzte vollständige Lauf; ein gefilterter Wiederholungslauf überschreibt die Standardberichte.
|
||||
Generated
+60
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"name": "votian-legacy-ui-tests",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "votian-legacy-ui-tests",
|
||||
"version": "1.0.0",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.63.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz",
|
||||
"integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.63.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
|
||||
"integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.63.0"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.63.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
|
||||
"integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "votian-legacy-ui-tests",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": { "test": "python3 run.py" },
|
||||
"devDependencies": { "@playwright/test": "1.63.0" }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { defineConfig } from '@playwright/test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const report = (name: string) => fileURLToPath(new URL('./reports/' + name, import.meta.url));
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
fullyParallel: false,
|
||||
workers: 1,
|
||||
retries: 0,
|
||||
maxFailures: 0,
|
||||
timeout: 45_000,
|
||||
expect: { timeout: 10_000 },
|
||||
reporter: [
|
||||
['list'],
|
||||
['html', { outputFolder: report('html'), open: 'never' }],
|
||||
['junit', { outputFile: report('junit.xml') }],
|
||||
['json', { outputFile: report('results.json') }],
|
||||
],
|
||||
outputDir: './test-results',
|
||||
use: {
|
||||
baseURL: 'https://test.sb.assecutor.de',
|
||||
// Chromium routing stalls document.write popups in Playwright 1.63 (#43496).
|
||||
browserName: 'firefox',
|
||||
headless: true,
|
||||
viewport: { width: 1600, height: 1200 },
|
||||
locale: 'de-DE',
|
||||
timezoneId: 'Europe/Berlin',
|
||||
serviceWorkers: 'block',
|
||||
screenshot: 'only-on-failure',
|
||||
// A trace could contain login credentials and customer data.
|
||||
trace: 'off',
|
||||
video: 'off',
|
||||
actionTimeout: 10_000,
|
||||
navigationTimeout: 30_000,
|
||||
},
|
||||
});
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
test_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
test_image='mcr.microsoft.com/playwright:v1.63.0-noble'
|
||||
terminal_flags=(-i)
|
||||
if [[ -t 0 && -t 1 ]]; then terminal_flags+=(-t); fi
|
||||
# Only this test directory is mounted. The browser accesses the PHP app via HTTPS.
|
||||
exec docker run --rm --init --shm-size=512m "${terminal_flags[@]}" \
|
||||
--mount "type=bind,source=$test_dir,target=/tests" --workdir /tests \
|
||||
"$test_image" bash -c 'npm ci --ignore-scripts --no-audit --no-fund && exec python3 run.py "$@"' -- "$@"
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prompt credentials without echo; never save them in source or reports."""
|
||||
from pathlib import Path
|
||||
import getpass
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
suite = Path(__file__).resolve().parent
|
||||
env = os.environ.copy()
|
||||
arguments = sys.argv[1:]
|
||||
if '--verify-order' in arguments:
|
||||
position = arguments.index('--verify-order')
|
||||
if len(arguments) < position + 3 or not arguments[position + 1].isdigit() or not arguments[position + 2].startswith('UI-TEST-'):
|
||||
raise SystemExit('--verify-order benötigt AUFTRAGSNUMMER und UI-TEST-MARKER.')
|
||||
env['VOTIAN_UI_VERIFY_ORDER_ID'], env['VOTIAN_UI_VERIFY_ORDER_MARKER'] = arguments[position + 1:position + 3]
|
||||
del arguments[position:position + 3]
|
||||
if '--create-order' in arguments:
|
||||
if env.get('VOTIAN_UI_VERIFY_ORDER_ID'):
|
||||
raise SystemExit('--create-order und --verify-order dürfen nicht kombiniert werden.')
|
||||
arguments.remove('--create-order')
|
||||
env['VOTIAN_UI_CREATE_ORDER'] = '1'
|
||||
print('Speichert einen markierten, unvollständigen Testauftrag ohne Kurier.')
|
||||
if not env.get('VOTIAN_UI_USERNAME'):
|
||||
env['VOTIAN_UI_USERNAME'] = input('Votian-Testbenutzer: ')
|
||||
if not env.get('VOTIAN_UI_PASSWORD'):
|
||||
env['VOTIAN_UI_PASSWORD'] = getpass.getpass('Votian-Passwort (wird nicht gespeichert): ')
|
||||
binary = suite / 'node_modules/.bin/playwright'
|
||||
if not binary.exists():
|
||||
binary = suite.parent / 'node_modules/.bin/playwright'
|
||||
if not binary.exists():
|
||||
raise SystemExit('Playwright fehlt. Zuerst npm ci im Projekt oder im Testverzeichnis ausführen.')
|
||||
command = [str(binary), 'test', '-c', str(suite / 'playwright.config.ts'), *arguments]
|
||||
try:
|
||||
sys.exit(subprocess.call(command, cwd=suite, env=env))
|
||||
finally:
|
||||
env.pop('VOTIAN_UI_PASSWORD', None)
|
||||
@@ -0,0 +1,44 @@
|
||||
import { test as base, expect, type BrowserContext } from '@playwright/test';
|
||||
import { installGuard } from './network';
|
||||
|
||||
type State = Awaited<ReturnType<BrowserContext['storageState']>>;
|
||||
export const test = base.extend<{ guard: Awaited<ReturnType<typeof installGuard>> }, { signedIn: State }>({
|
||||
signedIn: [async ({ browser }, use) => {
|
||||
const username = process.env.VOTIAN_UI_USERNAME;
|
||||
const password = process.env.VOTIAN_UI_PASSWORD;
|
||||
if (!username || !password) throw new Error('Votian UI credentials required. Run python3 legacy-ui-tests/run.py.');
|
||||
const context = await browser.newContext({ locale: 'de-DE', serviceWorkers: 'block' });
|
||||
const guard = await installGuard(context, true);
|
||||
try {
|
||||
const page = await context.newPage();
|
||||
await page.goto('https://test.sb.assecutor.de/admin/login.php');
|
||||
const details = page.locator('details').filter({ has: page.locator('input[name="f_chk_account"]') });
|
||||
if (await details.count() && !(await details.getAttribute('open'))) await details.locator('summary').click();
|
||||
await page.locator('input[name="f_chk_account"]').fill(username);
|
||||
await page.locator('input[name="f_chk_password"]').fill(password);
|
||||
await page.locator('[name="f_submit"]').click();
|
||||
await expect(page).toHaveURL(/\/admin\/start\.php/);
|
||||
expect(guard.errors, 'Legacy JavaScript errors').toEqual([]);
|
||||
expect(guard.denied, 'Unexpected requests during login').toEqual([]);
|
||||
const state = await context.storageState();
|
||||
await use(state);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
}, { scope: 'worker' }],
|
||||
storageState: async ({ signedIn }, use) => { await use(signedIn); },
|
||||
guard: [async ({ context }, use, info) => {
|
||||
const guard = await installGuard(context);
|
||||
await use(guard);
|
||||
if (info.status !== info.expectedStatus) await info.attach('request-paths', { body: JSON.stringify([...new Set(guard.allowed)], null, 2), contentType: 'application/json' });
|
||||
if (guard.consoleErrors.length) await info.attach('console-diagnostics', { body: JSON.stringify(guard.consoleErrors, null, 2), contentType: 'application/json' });
|
||||
if (guard.knownErrors.length) await info.attach('known-opener-bug', { body: JSON.stringify(guard.knownErrors), contentType: 'application/json' });
|
||||
if (guard.errors.length) await info.attach('legacy-js-errors', { body: JSON.stringify(guard.errors), contentType: 'application/json' });
|
||||
if (guard.denied.length) {
|
||||
await info.attach('blocked-request-paths', { body: JSON.stringify([...new Set(guard.denied)], null, 2), contentType: 'application/json' });
|
||||
}
|
||||
expect(guard.errors, 'Legacy JavaScript errors').toEqual([]);
|
||||
expect(guard.denied, 'Unexpected request (blocked before it reached the server)').toEqual([]);
|
||||
}, { auto: true }],
|
||||
});
|
||||
export { expect } from '@playwright/test';
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { BrowserContext } from '@playwright/test';
|
||||
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ledgerDirectory = fileURLToPath(new URL('../reports/', import.meta.url));
|
||||
export function recordOrder(event: Record<string, unknown>) {
|
||||
mkdirSync(ledgerDirectory, { recursive: true });
|
||||
appendFileSync(ledgerDirectory + 'created-orders.jsonl', JSON.stringify({ time: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
|
||||
}
|
||||
|
||||
// The old form uses ISO-8859-1, including superscript station delimiters.
|
||||
function formData(body: string) {
|
||||
const utf8 = new URLSearchParams(body);
|
||||
if (![...utf8.values()].some(value => value.includes('\uFFFD'))) return utf8;
|
||||
const decode = (value: string) => new TextDecoder('windows-1252').decode(Buffer.from(value.replace(/\+/g, ' ').replace(/%([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))), 'latin1'));
|
||||
return new URLSearchParams(body.split('&').map(pair => { const index = pair.indexOf('='); return [decode(pair.slice(0, index)), decode(pair.slice(index + 1))]; }));
|
||||
}
|
||||
|
||||
function validateCreation(data: URLSearchParams, marker: string): string[] {
|
||||
const problems: string[] = [];
|
||||
const required = { toursubmit: 'Senden', getPriceOnly: '', jb_id: '', csc_id_payer: '878914', jb_orderer_hidden: marker, jb_status_manual: '1', jb_incomplete: '1', jb_multi_factor: '1', sendmail: '0', jb_fixprice: '10,00' };
|
||||
for (const [key, expected] of Object.entries(required)) {
|
||||
if ((data.get(key) ?? '') !== expected) problems.push(key);
|
||||
}
|
||||
for (const key of ['cr_id_order', 'cr_id_order_hidden', 'cr_id_orders', 'jb_permanent', 'jb_multi', 'jb_globaljob', 'afterwards', 'jb_offer', 'jb_insurance', 'jb_id_parent', 'jb_id_org', 'jb_tourname', 'jb_tourname_hidden', 'tmp_add_jb_ids', 'tmp_add_sids', 'do_delete_suborder', 'jb_offer_mail_hidden', 'jb_cashpayer_mail_hidden']) {
|
||||
if (!['', '0'].includes(data.get(key) ?? '')) problems.push(key);
|
||||
}
|
||||
if (!data.get('request_token')) problems.push('request_token missing');
|
||||
const stations = (data.get('jobList') ?? '').split('¹').map(row => row.split('²')).filter(row => [0, 1, 3, 4, 6, 7, 8].some(index => row[index]));
|
||||
if (stations.length !== 2 || stations[0][0] !== '878914' || stations[1][0] !== '878915' || !stations.every(row => row.some(cell => cell.includes(marker)))) problems.push('exactly two marked test stations');
|
||||
return problems;
|
||||
}
|
||||
|
||||
const origin = 'https://test.sb.assecutor.de';
|
||||
const pages = new Set([
|
||||
'/', '/index.php', '/admin/login.php', '/admin/start.php',
|
||||
'/jobs/job_edit.php', '/jobs/job_header.php', '/jobs/job_tour.php',
|
||||
'/jobs/job_options.php', '/jobs/job_tour_list.php',
|
||||
'/admin/cs_csc_list.php',
|
||||
'/css/phoenix.css.php', '/css/navigation.css.php',
|
||||
'/include/maps.googleapis.com_maps_api_js_places.php',
|
||||
]);
|
||||
|
||||
export async function installGuard(context: BrowserContext, login = false) {
|
||||
const denied: string[] = [];
|
||||
const allowed: string[] = [];
|
||||
const errors: string[] = [];
|
||||
const knownErrors: string[] = [];
|
||||
const suppressed: string[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
let creationMarker: string | undefined;
|
||||
let saveAttempts = 0;
|
||||
const readableOrders = new Set<string>();
|
||||
context.on('page', page => {
|
||||
page.on('console', message => { if (['error', 'warning'].includes(message.type())) consoleErrors.push(message.text()); });
|
||||
page.on('pageerror', error => {
|
||||
// Still fails the dedicated regression test; do not mask unrelated exceptions.
|
||||
if (["Cannot read properties of null (reading 'top')", 'can\'t access property "top", top.opener is null'].includes(error.message) && /\/jobs\/job_edit\.php[^\n]*:13:\d+/.test(error.stack ?? '')) knownErrors.push(error.stack!);
|
||||
else errors.push(error.stack ?? error.message);
|
||||
});
|
||||
});
|
||||
await context.route('**/*', async route => {
|
||||
const request = route.request();
|
||||
const url = new URL(request.url());
|
||||
const method = request.method();
|
||||
const description = `${method} ${url.origin}${url.pathname} [${[...url.searchParams.keys()].join(',')}]`;
|
||||
if (url.origin === origin && url.pathname === '/include/ajaxReqLib.php' &&
|
||||
url.searchParams.get('mode') === '500' && url.searchParams.get('value_03') === 'usr_state_newsticker') {
|
||||
suppressed.push('News ticker acknowledgement');
|
||||
return route.fulfill({ status: 200, contentType: 'application/javascript', body: "retValue = '1';" });
|
||||
}
|
||||
if (url.pathname === '/include/ajaxJsError.php' && url.origin === origin) {
|
||||
errors.push(url.searchParams.get('message') ?? 'Legacy JavaScript error');
|
||||
return route.fulfill({ status: 204 });
|
||||
}
|
||||
if (url.hostname === 'maps.googleapis.com' && url.pathname.endsWith('/gen_204')) {
|
||||
suppressed.push('Google Maps telemetry');
|
||||
return route.fulfill({ status: 204 });
|
||||
}
|
||||
let ok = false;
|
||||
if (url.origin === origin) {
|
||||
if (method === 'GET') {
|
||||
ok = /\.(?:js|css|png|gif|jpe?g|svg|ico|woff2?|ttf)$/i.test(url.pathname) || pages.has(url.pathname);
|
||||
// Never permit action parameters on otherwise readable pages.
|
||||
for (const key of ['f_act', 'action', 'save', 'delete', 'sendmail', 'toursubmit']) {
|
||||
if (url.searchParams.get(key) && !(url.pathname === '/admin/cs_csc_list.php' && key === 'f_act' && url.searchParams.get(key) === 'search')) ok = false;
|
||||
}
|
||||
if (url.pathname === '/include/ajaxReqJob.php') {
|
||||
ok = [...url.searchParams.keys()].every(key => ['cs_id', 'csc_id', 'cs_id_photo', 'csc_id_ask_mail', 'cs_id_ask_mail', 'hq_id', 'vht_id', 'csc_id_payer_cash', '_'].includes(key)) && ['cs_id', 'csc_id', 'cs_id_photo', 'csc_id_ask_mail'].some(key => url.searchParams.has(key));
|
||||
}
|
||||
if (url.pathname === '/include/ajaxReqDocument.php') ok = url.searchParams.get('mode') === '1';
|
||||
if (url.pathname === '/jobs/get_tournames.php') ok = [...url.searchParams.keys()].every(key => key === 'csc_id') && /^\d+$/.test(url.searchParams.get('csc_id') ?? '');
|
||||
if (url.pathname === '/admin/jb_detail.php') ok = [...url.searchParams.keys()].every(key => key === 'job_id') && readableOrders.has(url.searchParams.get('job_id') ?? '');
|
||||
if (url.pathname === '/include/ajaxReqGeneric.php') ok = [...url.searchParams.keys()].every(key => ['incFile', 'fun', 'par_01', 'retIdx', 'retParName', '_'].includes(key)) && url.searchParams.get('incFile') === 'inc_job.inc' && url.searchParams.get('fun') === 'getJobInternalRemarkItems' && readableOrders.has(url.searchParams.get('par_01') ?? '') && url.searchParams.get('retIdx') === '0' && url.searchParams.get('retParName') === '_RAW_TEXT_';
|
||||
if (url.pathname === '/include/ajaxReqLib.php') {
|
||||
ok = ['2', '6'].includes(url.searchParams.get('mode') ?? '');
|
||||
if (!ok) denied.push('Library mode=' + url.searchParams.get('mode') + ' field=' + url.searchParams.get('value_03'));
|
||||
}
|
||||
} else if (method === 'POST' && url.pathname === '/jobs/job_options.php') {
|
||||
const data = formData(request.postData() ?? '');
|
||||
ok = data.get('getPriceOnly') === '1' && data.get('sendmail') !== '1';
|
||||
if (!ok && creationMarker && saveAttempts === 0) {
|
||||
const problems = validateCreation(data, creationMarker);
|
||||
if (problems.length) denied.push('Order creation rejected: ' + problems.join(', '));
|
||||
else {
|
||||
saveAttempts++;
|
||||
recordOrder({ state: 'submit-attempt', marker: creationMarker });
|
||||
ok = true;
|
||||
}
|
||||
}
|
||||
} else if (method === 'POST' && url.pathname === '/admin/cs_csc_list.php') {
|
||||
const data = new URLSearchParams(request.postData() ?? '');
|
||||
ok = data.get('f_act') === 'search';
|
||||
} else if (login && method === 'POST' && url.pathname === '/admin/login.php') {
|
||||
const data = new URLSearchParams(request.postData() ?? '');
|
||||
ok = data.get('f_act') === '' && data.has('f_chk_account') && data.has('f_chk_password');
|
||||
}
|
||||
} else if (method === 'GET' && ['maps.googleapis.com', 'maps.gstatic.com', 'fonts.googleapis.com', 'fonts.gstatic.com'].includes(url.hostname)) {
|
||||
ok = ['script', 'stylesheet', 'font', 'image'].includes(request.resourceType());
|
||||
}
|
||||
if (ok) {
|
||||
allowed.push(description);
|
||||
await route.continue();
|
||||
} else {
|
||||
denied.push(description);
|
||||
await route.abort('blockedbyclient');
|
||||
}
|
||||
});
|
||||
return {
|
||||
denied, allowed, errors, knownErrors, suppressed, consoleErrors,
|
||||
armCreation(marker: string) {
|
||||
if (process.env.VOTIAN_UI_CREATE_ORDER !== '1' || !/^UI-TEST-\d{8}T\d{6}-[a-f0-9]{6}$/.test(marker) || creationMarker) throw new Error('Creation is not enabled or already armed');
|
||||
const ledger = ledgerDirectory + 'created-orders.jsonl';
|
||||
if (existsSync(ledger)) {
|
||||
const events = readFileSync(ledger, 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line));
|
||||
const unresolved = events.find(event => event.state === 'submit-attempt' && !events.some(later => later.marker === event.marker && later.state === 'server-confirmed'));
|
||||
if (unresolved) throw new Error('Unconfirmed previous save for ' + unresolved.marker + '. Inspect the server before another creation; see reports/created-orders.jsonl.');
|
||||
}
|
||||
creationMarker = marker;
|
||||
},
|
||||
allowOrderRead(id: string) { if (!/^\d+$/.test(id)) throw new Error('Invalid order ID'); readableOrders.add(id); },
|
||||
get saveAttempts() { return saveAttempts; },
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { expect, type Page } from '@playwright/test';
|
||||
|
||||
export async function openOrder(start: Page): Promise<Page> {
|
||||
await start.goto('/admin/start.php');
|
||||
await start.locator('#auftraege a').click();
|
||||
const popup = start.waitForEvent('popup');
|
||||
await start.locator('#erfassung a').click();
|
||||
const order = await popup;
|
||||
await order.waitForLoadState('load');
|
||||
await expect(order).toHaveTitle('AUFTRAGSERFASSUNG');
|
||||
await expect(order.frameLocator('frame[name="job_tour"]').locator('[name="von_cs_comp"]')).toBeVisible();
|
||||
await expect(order.frameLocator('frame[name="job_options"]').locator('[name="toursubmit"]')).toBeVisible();
|
||||
await expect(order.frameLocator('frame[name="job_tour"]').locator('[name="nameOfPayer"]')).toHaveValue(/Bezahler wählen/);
|
||||
await expect(order.frameLocator('frame[name="job_tour_list"]').getByText('Tourendaten (Etappen)', { exact: true })).toBeVisible();
|
||||
await expect.poll(() => order.frame({ name: 'job_options' })!.evaluate(() => (window as any).isCurrentlySubmitting)).toBe(false);
|
||||
return order;
|
||||
}
|
||||
|
||||
export function fields(order: Page) {
|
||||
return {
|
||||
tour: order.frameLocator('frame[name="job_tour"]'),
|
||||
options: order.frameLocator('frame[name="job_options"]'),
|
||||
list: order.frameLocator('frame[name="job_tour_list"]'),
|
||||
};
|
||||
}
|
||||
|
||||
/** Select an existing, explicitly named test cost centre through the real customer picker. */
|
||||
export async function selectTestCustomer(order: Page, button: string, name: 'Testkunde Assecutor 1' | 'Testkunde Assecutor 2') {
|
||||
const popup = order.waitForEvent('popup', { predicate: candidate => candidate.url().includes('/admin/cs_csc_list.php') });
|
||||
await fields(order).tour.locator(`[name="${button}"]`).click();
|
||||
const search = await popup;
|
||||
await search.locator('[name="f_cmp_comp"]').fill(name);
|
||||
await Promise.all([
|
||||
search.waitForNavigation({waitUntil:'domcontentloaded'}),
|
||||
search.getByRole('link', {name:'Suchen',exact:true}).first().click(),
|
||||
]);
|
||||
const match = search.locator('a[href^="javascript:finishPage("]');
|
||||
await expect(match).toHaveCount(1);
|
||||
const href = await match.getAttribute('href');
|
||||
expect(href).toContain(name);
|
||||
const expectedCentre = name.endsWith('1') ? '878914' : '878915';
|
||||
expect(href).toMatch(new RegExp(`finishPage\\('\\d+','${expectedCentre}'`));
|
||||
await Promise.all([search.waitForEvent('close'), match.click()]);
|
||||
await expect.poll(() => order.frame({ name: 'job_options' })!.evaluate(() => (window as any).isCurrentlySubmitting)).toBe(false);
|
||||
}
|
||||
|
||||
export async function fillTestOrder(order: Page, marker: string) {
|
||||
await selectTestCustomer(order, 'vonFirmaButton', 'Testkunde Assecutor 1');
|
||||
await selectTestCustomer(order, 'nachFirmaButton', 'Testkunde Assecutor 2');
|
||||
await selectTestCustomer(order, 'bezahlerFirmaButton', 'Testkunde Assecutor 1');
|
||||
const {tour,options}=fields(order);
|
||||
await expect(tour.locator('[name="nameOfPayer"]')).toHaveValue('Testkunde Assecutor 1');
|
||||
await tour.locator('[name="jb_orderer"]').fill(marker);
|
||||
await tour.locator('[name="von_tr_remark"]').fill(marker + ' - automatischer Test, NICHT AUSFÜHREN');
|
||||
await tour.locator('[name="nach_tr_remark"]').fill(marker + ' - keine echte Beförderung');
|
||||
await options.locator('[name="jb_fixprice"]').fill('10,00');
|
||||
await options.locator('[name="jb_fixprice"]').press('Tab');
|
||||
await options.locator('[name="jb_markup"]').fill('0');
|
||||
await options.locator('[name="jb_markup"]').press('Tab');
|
||||
await options.locator('[name="jb_status_manual"]').check();
|
||||
await options.locator('[name="jb_incomplete"]').check();
|
||||
await expect(options.locator('[name="cr_id_order"]')).toHaveValue('');
|
||||
await expect(options.locator('[name="jb_globaljob"]')).not.toBeChecked();
|
||||
await expect(options.locator('[name="jb_insurance"]')).not.toBeChecked();
|
||||
await expect(options.locator('[name="jb_offer"]')).not.toBeChecked();
|
||||
await expect(options.locator('[name="afterwards"]')).not.toBeChecked();
|
||||
await expect(options.locator('[name="jb_multi_factor"]')).toHaveValue('1');
|
||||
await expect(options.locator('[name="toursubmit"]')).toBeEnabled();
|
||||
await expect.poll(() => order.frame({ name: 'job_options' })!.evaluate(() => ({ busy: (window as any).isCurrentlySubmitting, alert: (window as any).alertIsOpen })), { timeout: 35_000, message: 'Preisberechnung ist vor dem Absenden abgeschlossen' }).toEqual({ busy: false, alert: false });
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { test, expect } from '../support/fixtures';
|
||||
import { openOrder, fields, selectTestCustomer, fillTestOrder } from '../support/order-page';
|
||||
|
||||
test('Auftragserfassung startet ohne JavaScript-Ausnahme', async ({ page, guard }) => {
|
||||
await openOrder(page);
|
||||
expect(guard.knownErrors, 'job_edit.php greift trotz fehlendem window.opener auf top zu').toEqual([]);
|
||||
});
|
||||
|
||||
test('Auftragserfassung öffnet vier Frames und verhindert leeres Absenden', async ({ page }) => {
|
||||
const order = await openOrder(page);
|
||||
expect(order.frames().map(f => f.name())).toEqual(expect.arrayContaining(['job_header', 'job_tour', 'job_options', 'job_tour_list']));
|
||||
const { tour, options } = fields(order);
|
||||
await expect(tour.locator('[name="nameOfPayer"]')).toHaveValue(/Bezahler wählen/);
|
||||
await expect(options.locator('[name="toursubmit"]')).toBeDisabled();
|
||||
await expect(options.locator('[name="jb_permanent"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
test('Bezahlerauswahl übernimmt die explizite Testkostenstelle', async ({ page }) => {
|
||||
const order = await openOrder(page);
|
||||
await selectTestCustomer(order, 'bezahlerFirmaButton', 'Testkunde Assecutor 1');
|
||||
await expect(fields(order).tour.locator('[name="nameOfPayer"]')).toHaveValue('Testkunde Assecutor 1');
|
||||
});
|
||||
|
||||
test('Auftragseingang Sonstiges blendet Zusatzfeld ein und räumt es wieder auf', async ({ page }) => {
|
||||
const { tour } = fields(await openOrder(page));
|
||||
const detail = tour.locator('[name="jb_origin_other"]');
|
||||
await expect(detail).toBeHidden();
|
||||
await tour.locator('[title="Auftragseingang sonstiges"]').check();
|
||||
await expect(detail).toBeVisible();
|
||||
await detail.fill('UI-TEST ohne Speicherung');
|
||||
await tour.locator('[title="Auftragseingang per Telefon"]').check();
|
||||
await expect(detail).toBeHidden();
|
||||
await expect(detail).toHaveValue('');
|
||||
});
|
||||
|
||||
test('Angebot erzwingt manuelle Disposition und unvollständigen Status', async ({ page }) => {
|
||||
const { options } = fields(await openOrder(page));
|
||||
await options.locator('[name="jb_offer"]').check();
|
||||
for (const name of ['jb_status_manual', 'jb_incomplete']) {
|
||||
await expect(options.locator(`[name="${name}"]`)).toBeChecked();
|
||||
await expect(options.locator(`[name="${name}"]`)).toBeDisabled();
|
||||
}
|
||||
for (const name of ['afterwards', 'jb_insurance', 'jb_globaljob']) {
|
||||
await expect(options.locator(`[name="${name}"]`)).not.toBeChecked();
|
||||
await expect(options.locator(`[name="${name}"]`)).toBeDisabled();
|
||||
}
|
||||
await options.locator('[name="jb_offer"]').uncheck();
|
||||
for (const name of ['afterwards', 'jb_insurance', 'jb_status_manual', 'jb_incomplete']) {
|
||||
await expect(options.locator(`[name="${name}"]`)).toBeEnabled();
|
||||
}
|
||||
});
|
||||
|
||||
test('Höherversicherung zeigt Warenwert und Tarif und setzt sie beim Abschalten zurück', async ({ page }) => {
|
||||
const { options } = fields(await openOrder(page));
|
||||
const value = options.locator('[name="jb_value_of_goods"]');
|
||||
await expect(value).toBeHidden();
|
||||
await options.locator('[name="jb_insurance"]').check();
|
||||
await expect(value).toBeVisible();
|
||||
await value.fill('1234,56');
|
||||
await options.locator('[name="jb_insurance_rate"]').selectOption('1');
|
||||
await options.locator('[name="special_case"]').check();
|
||||
await options.locator('[name="jb_insurance"]').uncheck();
|
||||
await expect(value).toBeHidden();
|
||||
await expect(value).toHaveValue('');
|
||||
await expect(options.locator('[name="jb_insurance_rate"]')).toHaveValue('0');
|
||||
await expect(options.locator('[name="special_case"]')).not.toBeChecked();
|
||||
});
|
||||
|
||||
for (const [input, message] of [['abc', /Warenwert bitte/], ['500001', /500\.000/], ['12,345', /Warenwert bitte/]] as const) {
|
||||
test(`Höherversicherung weist ungültigen Warenwert ${input} zurück`, async ({ page }) => {
|
||||
const order = await openOrder(page);
|
||||
const { options } = fields(order);
|
||||
await options.locator('[name="jb_insurance"]').check();
|
||||
const value = options.locator('[name="jb_value_of_goods"]');
|
||||
await value.fill(input);
|
||||
const alert = order.waitForEvent('dialog');
|
||||
const blur = value.press('Tab');
|
||||
const dialog = await alert;
|
||||
expect(dialog.type()).toBe('alert');
|
||||
expect(dialog.message()).toMatch(message);
|
||||
await dialog.accept();
|
||||
await blur;
|
||||
await expect(value).toHaveValue('');
|
||||
await expect(options.locator('[name="toursubmit"]')).toBeDisabled();
|
||||
});
|
||||
}
|
||||
|
||||
test('Stationswechsel vertauscht Firmen, Ansprechpartner und Hinweise', async ({ page }) => {
|
||||
const { tour } = fields(await openOrder(page));
|
||||
for (const [name, value] of Object.entries({von_cs_comp:'UI-TEST Start', nach_cs_comp:'UI-TEST Ziel', von_tr_person:'Kontakt A', nach_tr_person:'Kontakt B', von_tr_remark:'Hinweis A', nach_tr_remark:'Hinweis B'})) {
|
||||
await tour.locator(`[name="${name}"]`).fill(value);
|
||||
}
|
||||
await tour.locator('[name="switchButton"]').click();
|
||||
for (const [name, value] of Object.entries({von_cs_comp:'UI-TEST Ziel', nach_cs_comp:'UI-TEST Start', von_tr_person:'Kontakt B', nach_tr_person:'Kontakt A', von_tr_remark:'Hinweis B', nach_tr_remark:'Hinweis A'})) {
|
||||
await expect(tour.locator(`[name="${name}"]`)).toHaveValue(value);
|
||||
}
|
||||
});
|
||||
|
||||
for (const [side, name] of [['von', 'waste1Button'], ['nach', 'waste2Button']] as const) {
|
||||
test(`Löschen der ${side}-Station erhält die verbleibende Route`, async ({ page }) => {
|
||||
const order = await openOrder(page);
|
||||
const { tour } = fields(order);
|
||||
await tour.locator('[name="von_cs_comp"]').fill('UI-TEST Start');
|
||||
await tour.locator('[name="nach_cs_comp"]').fill('UI-TEST Ziel');
|
||||
await tour.locator(`[name="${name}"]`).click();
|
||||
await expect(tour.locator('[name="von_cs_comp"]')).toHaveValue(side === 'von' ? 'UI-TEST Ziel' : 'UI-TEST Start');
|
||||
await expect(tour.locator('[name="nach_cs_comp"]')).toHaveValue('');
|
||||
});
|
||||
}
|
||||
|
||||
test('Manuelle Disposition und unvollständig sind ohne Speichern auswählbar', async ({ page }) => {
|
||||
const { options } = fields(await openOrder(page));
|
||||
await options.locator('[name="jb_status_manual"]').check();
|
||||
await options.locator('[name="jb_incomplete"]').check();
|
||||
await expect(options.locator('[name="jb_status_manual"]')).toBeChecked();
|
||||
await expect(options.locator('[name="jb_incomplete"]')).toBeChecked();
|
||||
await expect(options.locator('[name="toursubmit"]')).toBeDisabled();
|
||||
});
|
||||
|
||||
|
||||
test('Vollständiger Testauftrag wird absendebereit, aber noch nicht gespeichert', async ({ page }, info) => {
|
||||
const order = await openOrder(page);
|
||||
await fillTestOrder(order, 'UI-TEST-VORSCHAU');
|
||||
const {options,tour}=fields(order);
|
||||
await expect(tour.locator('[name="von_cs_comp"]')).toHaveValue('Testkunde Assecutor 1');
|
||||
await expect(tour.locator('[name="nach_cs_comp"]')).toHaveValue('Testkunde Assecutor 2');
|
||||
await expect(options.locator('[name="jb_fixprice"]')).toHaveValue('10,00');
|
||||
await expect(options.locator('[name="csc_id_payer"]')).toHaveValue('878914');
|
||||
await expect(options.locator('[name="sendmail"]')).toHaveValue('0');
|
||||
await order.screenshot({path:info.outputPath('testauftrag-vorbereitet.png'),fullPage:true});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { test, expect } from '../support/fixtures';
|
||||
import { openOrder, fields, fillTestOrder } from '../support/order-page';
|
||||
import { recordOrder } from '../support/network';
|
||||
|
||||
test('Testauftrag speichern und in frischer Detailansicht wieder öffnen', async ({ page, context, guard }, info) => {
|
||||
test.skip(process.env.VOTIAN_UI_CREATE_ORDER !== '1' && !process.env.VOTIAN_UI_VERIFY_ORDER_ID, 'Speichern nur mit --create-order; vorhandenen Testauftrag mit VERIFY_ORDER_ID prüfen.');
|
||||
test.setTimeout(90_000);
|
||||
let id = process.env.VOTIAN_UI_VERIFY_ORDER_ID;
|
||||
let marker = process.env.VOTIAN_UI_VERIFY_ORDER_MARKER;
|
||||
if (!id) {
|
||||
if (info.retry || info.repeatEachIndex) throw new Error('A save test must never be automatically retried or repeated.');
|
||||
marker = 'UI-TEST-' + new Date().toISOString().replace(/[-:]/g, '').slice(0, 15) + '-' + randomBytes(3).toString('hex');
|
||||
const order = await openOrder(page);
|
||||
await fillTestOrder(order, marker);
|
||||
expect(guard.denied, 'All lookups must finish before enabling a save').toEqual([]);
|
||||
guard.armCreation(marker);
|
||||
const unexpectedDialogs: string[] = [];
|
||||
let savedId: string | undefined;
|
||||
order.on('dialog', async dialog => {
|
||||
const match = dialog.message().match(/^Der Auftrag wurde gespeichert!\s*Auftragsnummer:\s*(\d+)\s*$/);
|
||||
if (dialog.type() === 'alert' && match) {
|
||||
savedId = match[1];
|
||||
recordOrder({ state: 'server-confirmed', marker, id: savedId });
|
||||
await dialog.accept();
|
||||
} else {
|
||||
unexpectedDialogs.push(dialog.type() + ': ' + dialog.message());
|
||||
await dialog.dismiss();
|
||||
}
|
||||
});
|
||||
await fields(order).options.locator('[name="toursubmit"]').click();
|
||||
await expect.poll(() => Boolean(savedId || unexpectedDialogs.length), { timeout: 30_000, message: 'Server bestätigt gespeicherten Auftrag mit Nummer' }).toBe(true);
|
||||
expect(unexpectedDialogs).toEqual([]);
|
||||
expect(guard.denied).toEqual([]);
|
||||
expect(savedId).toMatch(/^\d+$/);
|
||||
expect(guard.saveAttempts).toBe(1);
|
||||
id = savedId;
|
||||
}
|
||||
expect(marker, 'Marker des gespeicherten Testauftrags').toMatch(/^UI-TEST-/);
|
||||
guard.allowOrderRead(id!);
|
||||
const detail = await context.newPage();
|
||||
await detail.goto('/admin/jb_detail.php?job_id=' + id);
|
||||
await expect(detail.locator('body')).toContainText(marker!);
|
||||
await expect(detail.locator('body')).toContainText('Testkunde Assecutor 1');
|
||||
await expect(detail.locator('body')).toContainText('Testkunde Assecutor 2');
|
||||
await expect(detail.locator('body')).toContainText('NICHT AUSFÜHREN');
|
||||
await expect(detail.locator('body')).toContainText('keine echte Beförderung');
|
||||
await expect(detail.locator('tr:has(> td:text-is("Fixpreis"))')).toContainText('10,00');
|
||||
await expect(detail.locator('tr:has(> td:text-is("Firma")) > td')).toHaveText(['Firma', 'Testkunde Assecutor 1', 'Testkunde Assecutor 2']);
|
||||
await expect(detail.getByText('UNFERTIG!', { exact: true })).toBeVisible();
|
||||
await expect(detail.getByRole('link', { name: 'Kurierhistorie', exact: true })).toHaveCount(0);
|
||||
await info.attach('saved-test-order', { body: JSON.stringify({ id, marker, url: detail.url() }, null, 2), contentType: 'application/json' });
|
||||
await detail.screenshot({ path: info.outputPath('testauftrag-gespeichert.png'), fullPage: true });
|
||||
recordOrder({ state: 'ui-verified', marker, id });
|
||||
});
|
||||
Reference in new issue
Block a user