"""Run representative write flows against a disposable deployed test server. Requires CSR_TEST_BASE_URL, CSR_TEST_USER, and CSR_TEST_PASSWORD. Prints the unique record prefix so created test data can be removed after inspection. """ import os import time from pathlib import Path from tempfile import TemporaryDirectory from openpyxl import Workbook from playwright.sync_api import expect, sync_playwright def run(): base = os.environ["CSR_TEST_BASE_URL"].rstrip("/") username = os.environ["CSR_TEST_USER"] password = os.environ["CSR_TEST_PASSWORD"] marker = f"PW{int(time.time())}" print(f"TEST_MARKER={marker}", flush=True) errors = [] with sync_playwright() as playwright, TemporaryDirectory(prefix="csr-server-browser-") as temporary: browser = playwright.chromium.launch(headless=True) context = browser.new_context(ignore_https_errors=True, accept_downloads=True) page = context.new_page() page.set_default_timeout(10000) page.on("pageerror", lambda error: errors.append(str(error))) page.goto(f"{base}/accounts/login/") page.locator('[name="username"]').fill(username) page.locator('[name="password"]').fill(password) page.locator('form button').click() assert page.url.rstrip("/") == base, f"Login failed: {page.url}" for route in ("/", "/uebersicht/", "/ablauf/", "/aufgaben/", "/configuration/", "/users/", "/archive/", "/history/", "/import/"): response = page.goto(f"{base}{route}") assert response.status == 200, f"{route}: {response.status}" print("PASS login and main routes", flush=True) page.goto(f"{base}/devices/new/") page.locator('[name="device_type"]').select_option(label="Laptop") assert page.locator('[data-hardware-field="ram_gb"]').is_visible() page.locator('[name="model"]').fill(f"{marker}-Laptop") page.locator('[name="employee"]').select_option(index=1) page.locator('[name="ram_gb"]').fill("16") page.locator('[name="ssd_gb"]').fill("512") page.locator('form[data-device-type-form] button').click() page.goto(f"{base}/?model_contains={marker}") expect(page.locator('tr[data-device-id]')).to_have_count(1) row = page.locator('tr[data-device-id]').first device_id = row.get_attribute('data-device-id') assert page.locator('[data-filter-panel]').is_hidden() row.locator('td[data-field="model"]').dblclick() field = row.locator('td[data-field="model"] input') field.fill(f"{marker}-Laptop-Edited") field.press('Enter') expect(row.locator('td[data-field="model"]')).to_have_text(f"{marker}-Laptop-Edited") print(f"PASS create, filter, inline edit: device {device_id}", flush=True) page.goto(f"{base}/devices/{device_id}/") task = page.locator(f'form[action="/devices/{device_id}/tasks/"]') task.locator('[name="title"]').fill(f"{marker}-Akku prüfen") task.locator('button').click() stage = page.locator(f'form[action="/devices/{device_id}/stage/"]') stage.locator('[name="stage"]').select_option('INSPECTION') stage.locator('button').click() page.goto(f"{base}/ablauf/") card = page.locator('.workflow-card').filter(has=page.locator(f'a[href="/devices/{device_id}/"]')) inspection = card.locator(f'form[action="/devices/{device_id}/inspection/"]') inspection.locator('[name="defects"]').fill(f"{marker}-Akku schwach") inspection.locator('[name="ram_gb"]').fill('16') inspection.locator('button').click() expect(card).to_contain_text(f"{marker}-Akku schwach") stage = card.locator(f'form[action="/devices/{device_id}/stage/"]') stage.locator('[name="stage"]').select_option('REPAIR') stage.locator('button').click() repair = card.locator(f'form[action="/devices/{device_id}/repairs/"]') repair.locator('[name="work_done"]').fill(f"{marker}-Akku ersetzt") repair.locator('button').click() stage = card.locator(f'form[action="/devices/{device_id}/stage/"]') stage.locator('[name="stage"]').select_option('STORAGE') stage.locator('button').click() expect(repair).to_be_visible() # An open task blocks storage. page.goto(f"{base}/aufgaben/") page.locator(f'form[action^="/devices/{device_id}/tasks/"][action$="/complete/"] button').click() page.goto(f"{base}/ablauf/") stage = card.locator(f'form[action="/devices/{device_id}/stage/"]') stage.locator('[name="stage"]').select_option('STORAGE') stage.locator('button').click() storage = card.locator(f'form[action="/devices/{device_id}/recipient/"]') expect(storage).to_be_visible() storage.locator('[name="recipient"]').fill(f"{marker}-Recipient") storage.locator('button').click() stage = card.locator(f'form[action="/devices/{device_id}/stage/"]') stage.locator('[name="stage"]').select_option('ISSUED') stage.locator('button').click() expect(page.locator('#stage-ISSUED').locator('../..')).to_contain_text(f"{marker}-Laptop-Edited") print("PASS PostgreSQL workflow, records, task gating, recipient, issuance", flush=True) page.goto(f"{base}/devices/{device_id}/") page.get_by_role('button', name='Gerät archivieren').click() page.locator('.confirm-box button').last.click() page.goto(f"{base}/archive/") expect(page.locator('tbody')).to_contain_text(f"{marker}-Laptop-Edited") page.locator(f'form[action="/archive/{device_id}/restore/"] button').click() page.goto(f"{base}/devices/{device_id}/") assert page.url.rstrip('/').endswith(f'/devices/{device_id}') expect(page.locator('h1')).to_contain_text('Gerät #') print("PASS archive and restore", flush=True) page.goto(f"{base}/import/") for label, suffix in (("XLSX herunterladen", '.xlsx'), ("ODS für LibreOffice", '.ods')): with page.expect_download() as event: page.get_by_role('link', name=label).click() assert event.value.suggested_filename.endswith(suffix) workbook = Workbook() workbook.active.append(['Modell', 'Typ', 'Status', 'Druckverfahren', 'Anschlüsse']) workbook.active.append([f"{marker}-Printer", 'Drucker', 'Eingang', 'Laser', 'USB-C']) import_path = Path(temporary) / f"{marker}.xlsx" workbook.save(import_path) page.locator('input[type="file"]').set_input_files(import_path) page.get_by_role('button', name='Datei analysieren').click() assert [page.locator(f'[name="column_{i}"]').input_value() for i in range(5)] == [ 'model', 'device_type', 'status', 'print_technology', 'ports', ] page.get_by_role('button', name='Import bestätigen').click() page.goto(f"{base}/?model_contains={marker}-Printer") expect(page.locator('tr[data-device-id]')).to_have_count(1) imported_id = page.locator('tr[data-device-id]').get_attribute('data-device-id') page.goto(f"{base}/devices/{imported_id}/") expect(page.locator('[name="print_technology"]')).to_have_value('Laser') expect(page.locator('[name="ports"]')).to_have_value('USB-C') print(f"PASS PostgreSQL import/export with printer fields: device {imported_id}", flush=True) page.goto(f"{base}/") page.locator('.sidebar-theme').click() assert page.locator('html').get_attribute('data-theme') == 'dark' page.reload() assert page.locator('html').get_attribute('data-theme') == 'dark' mobile = browser.new_context(ignore_https_errors=True, viewport={'width': 390, 'height': 844}) mobile_page = mobile.new_page() mobile_page.goto(f"{base}/accounts/login/") mobile_page.locator('[name="username"]').fill(username) mobile_page.locator('[name="password"]').fill(password) mobile_page.locator('form button').click() mobile_page.locator('[data-sidebar-toggle]').click() expect(mobile_page.locator('#sidebar')).to_have_class('sidebar open') mobile.close() print("PASS dark mode and mobile navigation", flush=True) assert not errors, f"JavaScript errors: {errors}" context.close() browser.close() if __name__ == '__main__': run()