Lint / PHP (phpcs PSR-12) (push) Successful in 33s
Lint / JS (eslint) (push) Successful in 13s
Lint / PHP requirements (version + extensions) (push) Successful in 56s
Lint / Notify on failure (push) Skipped
Security / PHP Security (semgrep) (push) Successful in 2m14s
Lint / Deploy (push) Successful in 3s
175 lines
6.2 KiB
Python
175 lines
6.2 KiB
Python
# flake8: noqa: E501 (demo data and canned output contain long literal lines)
|
|
import asyncio
|
|
import io
|
|
import sys
|
|
from PIL import Image
|
|
from playwright.async_api import async_playwright
|
|
import pathlib
|
|
|
|
OUT = str(pathlib.Path(__file__).resolve().parents[2] / "docs" / "img")
|
|
B = "http://127.0.0.1:8300"
|
|
H = {
|
|
"Remote-User": "alex.rivera",
|
|
"Remote-Name": "Alex Rivera",
|
|
"Remote-Email": "alex@example.com",
|
|
"Remote-Groups": "admin,employee",
|
|
}
|
|
P1 = "152847156"
|
|
CEPH = "524938499"
|
|
JS_TOP = (
|
|
"document.activeElement&&document.activeElement.blur();"
|
|
"document.documentElement.style.scrollBehavior='auto';window.scrollTo(0,0)"
|
|
)
|
|
|
|
|
|
async def ctxmk(p, theme="dark", w=1440, h=900):
|
|
b = await p.chromium.launch()
|
|
c = await b.new_context(viewport={"width": w, "height": h}, extra_http_headers=H)
|
|
await c.add_init_script(f"try{{localStorage.setItem('lt_theme','{theme}')}}catch(e){{}}")
|
|
|
|
async def avatar(route):
|
|
import re
|
|
|
|
uid = int((re.search(r"user_id=(\d+)", route.request.url) or [0, 0])[1] or 0)
|
|
ini = {1: "AR", 2: "SC", 3: "RB", 4: "JK", 5: "HW"}.get(uid, "??")
|
|
col = {1: "#ff6b00", 2: "#00d4ff", 3: "#b44aff", 4: "#00ff88", 5: "#ffc800"}.get(uid, "#888")
|
|
svg = f'<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64"><rect width="64" height="64" fill="#050a10"/><text x="32" y="40" font-family="monospace" font-size="24" font-weight="700" fill="{col}" text-anchor="middle">{ini}</text></svg>'
|
|
await route.fulfill(status=200, content_type="image/svg+xml", body=svg)
|
|
|
|
await c.route("**/api/user_avatar.php*", avatar)
|
|
return b, c
|
|
|
|
|
|
async def go(pg, path, wait=3500):
|
|
await pg.goto(B + path)
|
|
await pg.wait_for_timeout(wait)
|
|
await pg.evaluate(JS_TOP)
|
|
await pg.wait_for_timeout(300)
|
|
|
|
|
|
async def tab(pg, name):
|
|
t = pg.get_by_role("tab", name=name).first
|
|
if await t.count() == 0:
|
|
t = pg.get_by_text(name, exact=False).first
|
|
await t.click(timeout=4000)
|
|
await pg.wait_for_timeout(900)
|
|
|
|
|
|
async def shots():
|
|
async with async_playwright() as p:
|
|
b, c = await ctxmk(p)
|
|
pg = await c.new_page()
|
|
await go(pg, "/", 5000)
|
|
await pg.screenshot(path=f"{OUT}/dashboard.png")
|
|
# table crop
|
|
await pg.set_viewport_size({"width": 1840, "height": 900})
|
|
await pg.wait_for_timeout(600)
|
|
await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})")
|
|
await pg.wait_for_timeout(500)
|
|
await pg.screenshot(path=f"{OUT}/ticket-queue.png")
|
|
await pg.set_viewport_size({"width": 1440, "height": 900})
|
|
await pg.wait_for_timeout(600)
|
|
try:
|
|
await tab(pg, "Kanban")
|
|
await pg.screenshot(path=f"{OUT}/kanban.png")
|
|
print("kanban ok")
|
|
except Exception as e:
|
|
print("kanban fail", str(e)[:80])
|
|
await go(pg, "/ticket/" + P1)
|
|
await pg.screenshot(path=f"{OUT}/ticket-sla.png")
|
|
await go(pg, "/ticket/" + CEPH)
|
|
for name, fn in (
|
|
("Comments", "ticket-comments"),
|
|
("Dependencies", "ticket-dependencies"),
|
|
("Activity", "ticket-activity"),
|
|
):
|
|
try:
|
|
await tab(pg, name)
|
|
await pg.evaluate("document.querySelector('.lt-tabs, [role=tablist]')?.scrollIntoView({block:'start'})")
|
|
await pg.wait_for_timeout(500)
|
|
await pg.screenshot(path=f"{OUT}/{fn}.png")
|
|
print(fn, "ok")
|
|
except Exception as e:
|
|
print(fn, "fail", str(e)[:80])
|
|
await go(pg, "/ticket/create", 3000)
|
|
await pg.screenshot(path=f"{OUT}/new-ticket.png")
|
|
await go(pg, "/")
|
|
await pg.keyboard.press("Control+k")
|
|
await pg.wait_for_timeout(500)
|
|
await pg.keyboard.type("ticket", delay=70)
|
|
await pg.wait_for_timeout(900)
|
|
await pg.screenshot(path=f"{OUT}/command-palette.png", clip={"x": 320, "y": 60, "width": 800, "height": 460})
|
|
await pg.keyboard.press("Escape")
|
|
for path, name in (
|
|
("/admin/workflow", "admin-workflow"),
|
|
("/admin/api-keys", "admin-api-keys"),
|
|
("/admin/audit-log", "admin-audit-log"),
|
|
("/admin/recurring-tickets", "admin-recurring"),
|
|
):
|
|
await go(pg, path, 3500)
|
|
await pg.screenshot(path=f"{OUT}/{name}.png")
|
|
print(name, "ok")
|
|
await b.close()
|
|
b, c = await ctxmk(p, "light")
|
|
pg = await c.new_page()
|
|
await go(pg, "/", 5000)
|
|
await pg.screenshot(path=f"{OUT}/dashboard-light.png")
|
|
await b.close()
|
|
|
|
|
|
async def gif():
|
|
frames = []
|
|
async with async_playwright() as p:
|
|
b, c = await ctxmk(p, "dark", 1280, 720)
|
|
pg = await c.new_page()
|
|
|
|
async def snap(n=1, ms=0):
|
|
if ms:
|
|
await pg.wait_for_timeout(ms)
|
|
im = Image.open(io.BytesIO(await pg.screenshot())).convert("RGB").resize((960, 540), Image.LANCZOS)
|
|
for _ in range(n):
|
|
frames.append(im)
|
|
|
|
await go(pg, "/", 4500)
|
|
await snap(3)
|
|
await pg.mouse.wheel(0, 520)
|
|
await snap(2, 500)
|
|
try:
|
|
await tab(pg, "Kanban")
|
|
await snap(3, 300)
|
|
except Exception:
|
|
pass
|
|
await go(pg, "/ticket/" + P1, 3000)
|
|
await snap(3)
|
|
await go(pg, "/ticket/" + CEPH, 3000)
|
|
await snap(2)
|
|
for name in ("Comments", "Dependencies", "Activity"):
|
|
try:
|
|
await tab(pg, name)
|
|
await snap(3, 300)
|
|
except Exception:
|
|
pass
|
|
await go(pg, "/", 3000)
|
|
await pg.keyboard.press("Control+k")
|
|
await snap(2, 500)
|
|
for ch in "tick":
|
|
await pg.keyboard.type(ch)
|
|
await snap(1, 200)
|
|
await snap(3, 300)
|
|
await go(pg, "/admin/workflow", 3000)
|
|
await snap(3)
|
|
await b.close()
|
|
pal = [f.quantize(colors=96, method=Image.MEDIANCUT, dither=Image.NONE) for f in frames]
|
|
pal[0].save(f"{OUT}/demo.gif", save_all=True, append_images=pal[1:], duration=1100, loop=0, optimize=True)
|
|
print("gif", len(frames))
|
|
|
|
|
|
async def main():
|
|
if "gif" not in sys.argv:
|
|
await shots()
|
|
if "shots" not in sys.argv:
|
|
await gif()
|
|
|
|
|
|
asyncio.run(main())
|