"""Quick tracking dialogs and responsive cards, using isolated sample orders.""" import json import os import sys import tempfile import threading from datetime import date, datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] RUN = Path(tempfile.mkdtemp(prefix='tracking-ui-', dir=ROOT/'tools/out')) os.environ['PO_DATA_DIR'] = str(RUN) sys.path.insert(0, str(ROOT/'po_app')) from app import app, Session from models import ApprovalLog, Order, User from playwright.sync_api import sync_playwright, expect from waitress.server import create_server from werkzeug.security import generate_password_hash PASSWORD = 'Isolated-Tracking-2026' with Session() as db: users = {} for name, role in [('采购员', 'maker'), ('审批员', 'approver'), ('管理员', 'admin')]: user = User(username=role, name=name, role=role, active=True, must_change_password=False, session_version=1, password_hash=generate_password_hash(PASSWORD)) db.add(user) db.flush() users[role] = user.id samples = [ ('博远精密(测试)', 'unpaid', 'pending', 'pending', True), ('华星电子(测试)', 'partial', 'delivered', 'received', False), ('恒达机电(测试)', 'paid', 'partial', 'received', False), ('新力材料(测试)', 'paid', 'delivered', 'received', False), ('中科配件(测试)', 'unknown', 'unknown', 'unknown', False), ('宏光模具(测试)', 'paid', 'delivered', 'pending', False), ] for i, (supplier, payment, delivery, invoice, prepay) in enumerate(samples, 1): order = Order(order_no='PO20260917-%03d' % i, supplier_name=supplier, created_by=users['maker'], created_by_name='采购员', status='approved', order_date='2026-09-17', total_amount=1200*i, revision=1, approved_by='审批员', approved_at=datetime.now(), payment_status=payment, delivery_status=delivery, invoice_status=invoice, needs_prepayment=prepay, fulfillment_revision=1, payment_revision=1) if i == 4: order.fulfillment_revision = 0 db.add(order) db.commit() server = create_server(app, host='127.0.0.1', port=0) threading.Thread(target=server.run, daemon=True).start() base = 'http://127.0.0.1:%s' % server.effective_port errors = [] try: with sync_playwright() as p: browser = p.chromium.launch(channel='msedge', headless=True) def login(role): page = browser.new_page(viewport={'width': 1440, 'height': 1100}) page.on('pageerror', lambda error: errors.append(str(error))) page.goto(base+'/login') page.locator('[name=username]').fill(role) page.locator('[name=password]').fill(PASSWORD) page.get_by_role('button', name='登 录').click() page.wait_for_url(base+'/') return page maker, approver, admin = [login(role) for role in ('maker', 'approver', 'admin')] admin.screenshot(path=str(RUN/'desktop.png'), full_page=True) assert maker.locator('[data-quick-tracking][href$="/payment"]').count() == 0 assert approver.locator('[data-quick-tracking][href$="/follow-up"]').count() == 0 assert admin.locator('.tracking-table tbody tr').first.get_attribute('data-order-id') == '4' for width in (320, 390, 768, 1024): approver.set_viewport_size({'width': width, 'height': 844}) for path in ('/', '/orders', '/orders/2/view'): approver.goto(base+path) assert approver.evaluate('document.documentElement.scrollWidth <= innerWidth'), (width, path) if width <= 768 and path != '/orders/2/view': assert approver.locator('table.order-cards').evaluate('(el) => el.scrollWidth <= el.clientWidth + 1'), (width, path) approver.set_viewport_size({'width': 390, 'height': 844}) approver.goto(base+'/') approver.screenshot(path=str(RUN/'mobile.png'), full_page=True) # Inline validation retains user input; a stale form cannot overwrite a newer record. approver.locator('tr[data-order-id="2"] [data-quick-tracking]').click() dialog = approver.locator('#tracking-dialog') expect(dialog.locator('[name=payment_status]')).to_have_value('partial') dialog.locator('[name=payment_status]').select_option('unpaid') dialog.get_by_role('button', name='保存付款状态').click() expect(dialog.locator('#tracking-dialog-error')).to_contain_text('更正原因') expect(dialog.locator('[name=payment_status]')).to_have_value('unpaid') dialog.locator('[name=payment_status]').select_option('paid') dialog.locator('[name=payment_date]').fill(date.today().isoformat()) dialog.locator('[name=payment_note]').fill('款项已付清') # Another authorized user changes the same order while this dialog is open. admin.locator('tr[data-order-id="2"] a[href$="/payment"]').click() ad = admin.locator('#tracking-dialog') ad.locator('[name=payment_date]').fill(date.today().isoformat()) ad.locator('[name=payment_note]').fill('另一位经办人已核对首款') ad.get_by_role('button', name='保存付款状态').click() expect(ad).not_to_be_visible() dialog.get_by_role('button', name='保存付款状态').click() expect(dialog.locator('#tracking-dialog-error')).to_contain_text('已经更新') expect(dialog.get_by_role('button', name='保存付款状态')).to_be_disabled() dialog.locator('#tracking-reload').click() expect(dialog.locator('[name=payment_note]')).to_have_value('另一位经办人已核对首款') dialog.locator('[name=payment_status]').select_option('paid') dialog.locator('[name=payment_note]').fill('款项已付清') approver.screenshot(path=str(RUN/'payment-dialog-mobile.png')) dialog.get_by_role('button', name='保存付款状态').click() expect(dialog).not_to_be_visible() expect(approver.locator('.tracking-table tr[data-order-id="2"]')).to_have_count(0) assert approver.url == base+'/' # Maker updates delivery and invoice directly from the order list. maker.goto(base+'/orders?kw=001') maker.locator('a[data-quick-tracking]').click() md = maker.locator('#tracking-dialog') md.locator('[name=invoice_status]').select_option('received') md.locator('[name=delivery_status]').select_option('partial') md.locator('[name=fulfillment_note]').fill('首批货已到,剩余预计周五交齐。') md.get_by_role('button', name='保存采购跟进').click() expect(md).not_to_be_visible() assert maker.url == base+'/orders?kw=001' expect(maker.locator('[data-progress=delivery] [aria-current=step]')).to_have_text('部分交货') maker.goto(base+'/orders/1/view') expect(maker.locator('#order-tracking')).to_contain_text('最近跟进:') expect(maker.locator('#order-tracking')).to_contain_text('首批货已到,剩余预计周五交齐。') maker.locator('#order-tracking').screenshot(path=str(RUN/'detail.png')) # A standard page remains usable when JavaScript is unavailable. fallback = browser.new_context(java_script_enabled=False) fallback.add_cookies(maker.context.cookies()) tab = fallback.new_page() tab.goto(base+'/orders/1/tracking/follow-up') assert tab.locator('#follow-up-form').count() == 1 assert not errors, errors browser.close() with Session() as db: assert db.get(Order, 2).payment_status == 'paid' assert db.get(Order, 1).delivery_status == 'partial' assert db.query(ApprovalLog).filter_by(user_name='采购员', action='更新采购跟进').count() == 1 print(json.dumps({'result': 'PASS', 'output': str(RUN), 'js_errors': errors}, ensure_ascii=False)) finally: server.close()