Files
DeskcommCRM/scripts/screenshot-ai-tabs.ts
T
Rafael MelgaçoandClaude Fable 5 3788837cff feat(ai): abas de navegação na área de IA — Credenciais acessível pela UI (#30)
As páginas de Credenciais, Conhecimento e Uso já existiam completas mas
eram órfãs — nenhum link na UI levava até elas; o dropdown de credencial
do agente orientava a digitar a rota na mão.

- AiSectionTabs + layout da área: barra Agentes · Credenciais ·
  Conhecimento · Uso · Inbox nos hubs; some nas telas de edição ([id])
- CredentialPicker: empty-state vira link real pra aba Credenciais
  (antes: rota crua em <code>)
- scripts/screenshot-ai-tabs.ts: prova visual com login real (senha +
  TOTP do seed) — captura as abas, a página de credenciais e o dialog

Prova: typecheck/lint/vitest 335 verdes; screenshots headed contra
next start + Supabase local (login MFA completo).


Claude-Session: https://claude.ai/code/session_01454SGvQ7wBAvah8ZPnnWXv

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 21:39:07 +00:00

85 lines
3.1 KiB
TypeScript

/**
* Prova visual da aba de Credenciais (protocolo execução visível).
* Loga como o admin do seed (com TOTP real) e fotografa as telas novas.
* Uso: pnpm exec tsx scripts/screenshot-ai-tabs.ts <baseURL> <outDir>
*/
import { chromium } from "@playwright/test";
import { createHmac } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const [, , baseURL = "http://localhost:3210", outDir = "/tmp"] = process.argv;
function base32Decode(s: string): Buffer {
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let bits = 0;
let value = 0;
const out: number[] = [];
for (const ch of s.replace(/=+$/, "")) {
value = (value << 5) | alphabet.indexOf(ch);
bits += 5;
if (bits >= 8) {
out.push((value >>> (bits - 8)) & 0xff);
bits -= 8;
}
}
return Buffer.from(out);
}
function totp(secret: string, at = Date.now()): string {
const counter = Math.floor(at / 1000 / 30);
const buf = Buffer.alloc(8);
buf.writeBigUInt64BE(BigInt(counter));
const hmac = createHmac("sha1", base32Decode(secret)).update(buf).digest();
const offset = hmac[hmac.length - 1]! & 0xf;
const code = (hmac.readUInt32BE(offset) & 0x7fffffff) % 1_000_000;
return code.toString().padStart(6, "0");
}
async function main() {
const creds = JSON.parse(readFileSync(".e2e-creds.json", "utf-8"));
const email: string = creds.users.admin.email;
const password: string = creds.password;
const secret: string = creds.admin_totp.secret;
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto(`${baseURL}/login`);
await page.getByLabel("Email").fill(email);
await page.getByLabel("Senha").fill(password);
await page.getByRole("button", { name: "Entrar" }).click();
await page.waitForURL(/\/login\/mfa/, { timeout: 20_000 });
console.log("etapa: /login/mfa alcançado");
// Input OTP com auto-submit no onComplete — digitar via teclado, não fill().
await page.locator("input").first().click();
await page.keyboard.type(totp(secret), { delay: 60 });
await page.waitForURL(/\/(app|onboarding)\//, { timeout: 30_000 });
console.log("etapa: autenticado em", page.url());
await page.goto(`${baseURL}/app/ai/agents`);
await page.getByRole("navigation", { name: "Seções de IA" }).waitFor({ timeout: 15_000 });
await page.screenshot({ path: join(outDir, "ai-tabs-agents.png") });
console.log("url agents:", page.url());
await page.getByRole("link", { name: "Credenciais" }).click();
await page.waitForURL(/\/app\/ai\/credentials/);
await page.waitForLoadState("networkidle");
await page.screenshot({ path: join(outDir, "ai-tabs-credentials.png") });
const addBtn = page.getByRole("button", { name: /credencial/i }).first();
if (await addBtn.isVisible().catch(() => false)) {
await addBtn.click();
await page.waitForTimeout(600);
await page.screenshot({ path: join(outDir, "ai-credentials-dialog.png") });
}
await browser.close();
console.log("SCREENSHOTS_OK");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});