vitrinka.docs
QA a testování

Sada nástrojů pro revize

Review pravidla týmu jako data, auditovaný soudce, mapa textů u každého snímku, kontroly, které umí změřit program, a nálezy ukotvené ke slovům na obrazovce.

Vitrinka čte výstup vašeho runneru

Sada nástrojů pro revize pracuje s tím, co váš testovací harness už tak zanechává: se snímky obrazovky a s volitelnou mapou textů vedle každého z nich. Vitrinka nikdy nespouští prohlížeč, neřídí zařízení a neprovádí OCR. Vše, co pravidlo potřebuje, je soubor, který vaše sada v Playwright, Cypress, Selenium, Appium nebo Maestro zapíše na několik řádků, a každé pravidlo je zde zveřejněno, aby si ho tým mohl místo toho implementovat ve vlastní sadě.

Review brief

Pravidla, podle kterých se reviduje, jsou data týmu na serveru, ne prompt, který někdo pokaždé přepisuje. Každé pravidlo je jeden markdown dokument: frontmatter id, family, check (mechanical, partial nebo judgment), severity, volitelně platform a enabled, pak věta pravidla a seznam ## Verify. Pravidla se podle id skládají ze tří vrstev — vestavěná rubrika, vydestilovaná ze skutečných anotací z revizí, pak pravidla workspace, pak přepisy projektu.

  • V prohlížeči — Nastavení → Workspace → Review a záložka Review projektu vypíšou každé výsledné pravidlo s vrstvou, ze které pochází, přepínačem zapnutí, editorem a návratem k vrstvě pod ním. Měnit pravidla může admin; při uložení se zkontrolují, zverzují a zapíšou do auditu.
  • Z terminálu — vitrinka review rules list, get, lint a put čtou a publikují soubory pravidel; put nejdřív kontroluje a každé put je nová verze.
  • Do revize — vitrinka review brief (MCP review_brief) vykreslí zapnutá pravidla jako jeden kompaktní blok promptu, s volitelným řádkem --persona. Soudce níže ho používá a /vitrinka:review ho předá agentovi, který nástěnku reviduje.

Běhy revize

Revize přes mnoho nástěnek nemá pokaždé číst každý screenshot. /vitrinka:review začíná plánem revize, který připraví server: jedno volání nad nástěnkami rozhodne, co je třeba posoudit, a otevře běh revize, který si vede záznam o tom, co se skutečně vidělo.

  • Rozsah — výchozí je poslední průchod snímání každé cesty. Nástěnky s několika průchody (nebo s mnoha obrazovkami) odpoví cenou každé možnosti — obrazovky, obrazovky k posouzení, archy — a plán počká, než se rozsah zvolí: latest, all nebo pojmenované sekce.
  • Jen to, co se liší — prázdné, skeleton a nedekódovatelné snímky i snímky, kterým runner selhal, jsou mezery ve snímání: nikdy nález, nikdy úspěch. Bajtově shodné obrazovky se posoudí jednou. Téměř shodná obrazovka stejného zařízení a motivu se posoudí výřezem jen těch pixelů, které se liší od zástupce skupiny. Obrazovka posouzená v dřívějším dokončeném běhu podle stejné rubriky se použije znovu; úprava kteréhokoli pravidla vynutí posouzení všeho.
  • Důkaz, že recenzent vidí — první arch běhu nese v pixelech krátký kód, nikdy jako text. Recenzent ho zopakuje dřív, než smí číst arch 2 nebo založit nález, takže hostitel, kterému obrázky nedorazí, skončí po jednom obrázku místo hlášení „žádné nálezy“.
  • Účtenka pro každou kartu — každá obrazovka v rozsahu skončí jako prohlédnutá, zastoupená (výřezem nebo shodným dvojčetem), znovu použitá, mezera ve snímání, nebo blokovaná. Nálezy už otevřené na stejné verzi obrazovky jsou vypsané u její dlaždice a jejich opětovné založení se odmítne jako duplikát.
vitrinka review plan <board> <board> --scope latest
vitrinka review plan --run <id> --cards
vitrinka config reviewer codex   # revize tohoto stroje posuzuje Codex sidekick

Plán uvádí svou obrazovou cenu vedle spočtené základny — každá obrazovka v rozsahu na přehledových arších po 12 — takže úspora je číslo. Přes MCP jsou tytéž dveře review_plan a archy běhu se čtou přes get_card_image {run, sheet}.

Mapa textů: boxes.json

Ke každému obrázku zapište sourozence pojmenovaného <image filename>.boxes.json, včetně přípony obrázku: login.png.boxes.json. Soubor je pole JSON; každá položka nese text, x, y, w, h a volitelné role. Souřadnice jsou pixely zdrojového obrázku s počátkem vlevo nahoře. Šířka a výška musí být kladné a každá souřadnice konečná. Záporné počátky jsou povoleny, aby lint mohl hlásit přetečení.

[
  {"text":"Continue","x":24,"y":320,"w":144,"h":40,"role":"button"},
  {"text":"Email","x":24,"y":200,"w":60,"h":20,"role":"label"}
]

Zachyťte text přesně tak, jak je zobrazen, v pořadí zachycení, včetně duplicit. Limity jsou 2 MiB a 20 000 položek; neznámá pole a chybějící povinná pole jsou odmítnuta.

Mapa cestuje se snímkem všude, kam cestuje snímek: vitrinka qa run publish posílá sourozence vedle původní cesty ke snímku každého reportéru, vitrinka board capture --boxes <path> přijímá explicitní mapu (a sourozence adoptuje automaticky) a push sady zahrnuje sourozence vedle jejích obrázků. Server každou mapu zmrazí do revize karty, takže opětovné publikování sady nikdy nepohne kotvami dřívější revize.

Zápis mapy z vašeho runneru

Webové snímky se pořizují v poměru pixelů zařízení, proto CSS obdélníky vynásobte devicePixelRatio; nativní nástroje už hlásí pixely obrazovky. Každý úryvek sbírá viditelné textové uzly a zapisuje sourozence vedle snímku, který popisuje.

Playwright

const boxes = await page.evaluate(() => {
  const dpr = window.devicePixelRatio || 1;
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
  const out: { text: string; x: number; y: number; w: number; h: number; role?: string }[] = [];
  for (let n = walker.nextNode(); n; n = walker.nextNode()) {
    const text = n.textContent?.trim();
    if (!text) continue;
    const range = document.createRange();
    range.selectNodeContents(n);
    const r = range.getBoundingClientRect();
    if (r.width <= 0 || r.height <= 0) continue;
    const role = (n.parentElement?.closest("[role]") as HTMLElement | null)?.getAttribute("role") ?? undefined;
    out.push({ text, x: r.x * dpr, y: r.y * dpr, w: r.width * dpr, h: r.height * dpr, role });
  }
  return out;
});
await page.screenshot({ path: "login.png" });
fs.writeFileSync("login.png.boxes.json", JSON.stringify(boxes));

Cypress

cy.screenshot pojmenuje soubor; mapu zapište přes task.

cy.screenshot("login");
cy.document().then((doc) => {
  const dpr = doc.defaultView.devicePixelRatio || 1;
  const boxes = [...doc.querySelectorAll("button, a, label, h1, h2, h3, p, span, input")]
    .map((el) => ({ el, r: el.getBoundingClientRect(), text: (el.innerText || el.value || "").trim() }))
    .filter(({ r, text }) => text && r.width > 0 && r.height > 0)
    .map(({ el, r, text }) => ({ text, x: r.x * dpr, y: r.y * dpr, w: r.width * dpr, h: r.height * dpr, role: el.getAttribute("role") || undefined }));
  cy.task("writeFile", { path: "cypress/screenshots/login.png.boxes.json", body: JSON.stringify(boxes) });
});

Selenium

Stejný průchod DOM jako u Playwright, spuštěný přes execute_script.

boxes = driver.execute_script("""
  const dpr = window.devicePixelRatio || 1, out = [];
  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
  for (let n = walker.nextNode(); n; n = walker.nextNode()) {
    const text = (n.textContent || '').trim(); if (!text) continue;
    const range = document.createRange(); range.selectNodeContents(n);
    const r = range.getBoundingClientRect(); if (r.width <= 0 || r.height <= 0) continue;
    out.push({text, x: r.x * dpr, y: r.y * dpr, w: r.width * dpr, h: r.height * dpr});
  }
  return out;
""")
driver.save_screenshot("login.png")
open("login.png.boxes.json", "w").write(json.dumps(boxes))

Appium

Zdroj stránky nese nativní hranice v pixelech obrazovky: atributy iOS jsou x y width height, Android má bounds="[x1,y1][x2,y2]".

import re, xml.etree.ElementTree as ET
root = ET.fromstring(driver.page_source)
boxes = []
for el in root.iter():
    text = (el.get("label") or el.get("text") or el.get("content-desc") or "").strip()
    if not text: continue
    if el.get("bounds"):
        x1, y1, x2, y2 = map(int, re.findall(r"\d+", el.get("bounds")))
        rect = dict(x=x1, y=y1, w=x2 - x1, h=y2 - y1)
    else:
        rect = dict(x=float(el.get("x")), y=float(el.get("y")), w=float(el.get("width")), h=float(el.get("height")))
    if rect["w"] > 0 and rect["h"] > 0:
        boxes.append(dict(text=text, **rect, role=el.tag))
driver.get_screenshot_as_file("home.png")
open("home.png.boxes.json", "w").write(json.dumps(boxes))

Maestro

Uvnitř flow se neskriptuje: vypište hierarchii pohledů vedle snímku a jednou ji převeďte.

maestro screenshot home.png
maestro hierarchy > home.hierarchy.json
jq '[.. | objects | select(.attributes.text? and .attributes.bounds?)
     | (.attributes.bounds | capture("\\[(?<x1>\\d+),(?<y1>\\d+)\\]\\[(?<x2>\\d+),(?<y2>\\d+)\\]")) as $b
     | {text: .attributes.text, x: ($b.x1|tonumber), y: ($b.y1|tonumber),
        w: (($b.x2|tonumber) - ($b.x1|tonumber)), h: (($b.y2|tonumber) - ($b.y1|tonumber))}
     | select(.w > 0 and .h > 0)]' home.hierarchy.json > home.png.boxes.json

Nálezy ukotvené k textu

Agent zakládající nález přes annotate už nehádá pixely. Oblast může být { "text": "Continue" }, nebo { "text": "Continue", "nth": 1 } pro druhý výskyt. Shoda je přesná a rozlišuje velikost písmen, proti mapě deklarované verze karty; nejednoznačný nebo chybějící text odpoví unresolved a nikdy se místo něj nevybere blízký popisek. Pixelové oblasti {x, y, w, h} a normalizované oblasti {nx, ny, nw, nh} zůstávají alternativami; nekombinujte je.

category (id revizního pravidla nebo starší kategorie) a severity jsou u každého agentního nálezu povinné. S dryRun: true dávka rozliší každou kotvu a vrátí obdélník plus URL výřezu připnutého k verzi, aniž by cokoli zapsala, takže se špatná kotva odhalí dřív, než přistane na nástěnce.

GET /api/v1/boards/{slug}/cards/{id}/boxes.json?version=N čte mapu přesně jedné revize. Běžné nahrání bez sourozence má pixely, ale žádnou mapu, a textová kotva na něm zůstává záměrně unresolved.

Volitelné měřicí důkazy

Položka může nést metrics. Vynechejte, co runner nezná; nikdy hodnoty nevymýšlejte. Zachyťte jen porovnatelné prvky, které pravidlo potřebuje, protože přiřadit každému textovému uzlu rodiče nebo skupinu popisuje DOM, ne zamýšlený vztah rozvržení.

PoleVýznam
groupPorovnatelní sourozenci, kteří mají mít stejné svislé rozestupy
parent: {x,y,w,h}Kontejner, vůči němuž má mít obsah vyvážené boční odsazení
id, labelForZachycená identita cíle a výslovný vztah popisku ke vstupu
textInsetVodorovné odsazení textu vstupu v pixelech zdroje
primaryZnámá klasifikace primární akce, true nebo false
textTransformVyhodnocená transformace, například none nebo uppercase
textWidthNeoříznutá šířka primárního popisku v pixelech zdroje; šířka položky je jeho dostupná plocha
foreground, backgroundNeprůhledné vyhodnocené barvy #RRGGBB
backgroundSourceliteral jen tehdy, když inspekce zdroje prokáže doslovnou barvu

Jedenáct mechanických kontrol

vitrinka review lint ./screenshots --viewport 1440x900 --theme dark --locale cs-CZ

Zapíše screenshots/candidates.json; --output zvolí jinou cestu. Podporované obrázky jsou PNG, JPEG, WebP a GIF do 64 milionů pixelů. Viewport se ve výchozím stavu rovná rozměrům obrázku; téma a locale musí popisovat zachycení.

KontrolaPožadované důkazy a testPravidlo / výchozí závažnost
duplicate-textPřesně opakovaný text; kandidát u druhého výskyturedundant / medium
sibling-gap-varianceNejméně tři seskupení sourozenci s levými hranami do 1 px; rozdíl největší a nejmenší svislé mezery přes 4 pxspacing / low
side-paddingExplicitní rodič; rozdíl levého a pravého odsazení přes 4 pxspacing / low
label-input-alignmentVztah popisku, id vstupu a odsazení textu; rozdíl levých hran přes 1 pxspacing / low
uppercase-transformZachycená transformace rovna uppercasecopy / low
cs-ascii-wordsČeské locale; slovo je v omezeném slovníku chybějící diakritikycopy / medium
primary-button-countKlasifikace primárních tlačítek; více než jedno primárníhierarchy / medium
text-contrastVyhodnocené barvy a čtyři neprůhledné vzorky rohů obrázku shodné s pozadím; kontrast pod 4.5:1hierarchy / high
outside-viewportObdélník textu přesahuje deklarovaný viewportlayout-width / medium
primary-label-truncationNeoříznutá šířka primárního popisku přesahuje dostupnou šířku o více než 1 pxlayout-width / medium
dark-literal-whiteTmavé téma a zdrojem potvrzené doslovně bílé pozadídark-theme / medium

Kandidát je měření k ověření člověkem, nikdy automatický požadavek na opravu. Bez mapy běží jen heuristika světlých oblastí v tmavém tématu; každá další kontrola je zaznamenána ve skipped s důvodem. Prázdný seznam kandidátů nikdy nedokazuje, že přeskočené kontroly prošly.

Soudce a audit

Co program změřit neumí, jde k soudci: serverovému průchodu, který každou zachycenou obrazovku posoudí podle pravidel briefu a vrátí verzované výsledky podložené důkazy. Spustíte ho přes run review na nástěnce (nabídka ⋯ nebo review pohled panelu Annotations), příkazem vitrinka review judge <board> nebo přes /vitrinka:review <board> eve. Běží na AI poskytovateli organizace.

  • Připnutý — každá úloha si připne verze karet a verzi rubriky, které posuzovala, takže výsledek vždy říká, na co se díval.
  • Auditovaný — každý navržený nález a během kalibrace i vzorek průchodů nezávisle zkontroluje audit; auditor vidí důkaz a tvrzení, nikdy model, který je vytvořil. Chybějící důkaz zůstává Needs evidence, nikdy pass.
  • Zakládáte vy — výsledky se ukazují v review pohledu panelu Annotations, seskupené podle pravidla, každý s výřezem důkazu. File finding ho odloží na nástěnku; Dismiss ho zahodí; hodnocení užitečnosti (valuable, minor, not useful) se drží odděleně od vlastního posouzení AI. Nic se za vás nezaloží, nevyřeší ani nezamítne.
  • Ověřený po opravě — Verify a fix zkontroluje auditem čerstvý snímek téže obrazovky proti nálezu.
vitrinka review judge <board> --rules spacing,copy --instructions "check the empty states"
vitrinka review judge <board> --status
vitrinka review judge export <job> --json

--section nebo --journey průchod zúží, --set posoudí publikovaný set a export zapíše posudky jako JSON, který projdete a označíte keep před založením tak, jak ukazuje další sekce.

Zrevidovat, založit a učit se

Projděte každého kandidáta, nastavte keep: true jen u nálezů, které stojí za založení, a doplňte cardId a cardVersion karty obrázku z dokumentu nástěnky. CLI nikdy neodvozuje revizi z názvu souboru.

vitrinka review file <board> screenshots/candidates.json --agent codex --dry-run --json
vitrinka review file <board> screenshots/candidates.json --agent codex --json

Oba příkazy používají dávkové dveře annotate. Zkušební běh rozliší stejné kotvy a vrátí URL výřezu pro každou připravenou položku; výřezy si prohlédněte a pak zakládejte. Založené nálezy jsou nativní anotace ve stavu staged, které čekají na Accept nebo Dismiss člověka v panelu Annotations na nástěnce.

vitrinka review stats --project <project> --since 90d

Statistiky počítají založené, přijaté, zamítnuté a dosud čekající nálezy podle pravidla a agenta v rámci vaší viditelnosti nástěnek. Míra zamítnutí je podíl zamítnutých k součtu přijatých a zamítnutých, takže čekající nálezy ji nikdy nevylepší, a je null, dokud nikdo nerozhodl. Tato míra je signál driftu: pravidlo, které lidé stále zamítají, je pravidlo k přepsání.

Na této stránce