mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
test(marca): a medição da fachada no navegador, e a sabotagem que a prova
O que está aqui é a superfície 2 do #659 — a tela de entrada — medida por
ferramenta num build de produção, com build ANTES e DEPOIS.
`medir-fachada.mjs` existe ao lado da spec porque o daemon do Docker desta
máquina travou (o socket ACEITA conexão e nunca responde), e sem Supabase local
a spec não carrega — o `loadCreds()` dela semeia pelo banco. A fachada não
precisa de banco: `marcaDaInstalacao()` devolve `null` quando a consulta falha
(`instalacao.ts:362` — ela não lança) e a resolução cai na camada do AMBIENTE,
que é a semente `APP_LOGO_URL` documentada no CLAUDE.md. O caminho de produto é
o mesmo; muda qual camada ganhou.
MEDIDO (tema escuro, logo enviado):
background-color=rgb(255,255,255) padding=[8,12,8,12] box-shadow≠none
a moldura CONTÉM o logo: 64×56 contra 40×40
MEDIDO (tema claro, mesmo logo):
background-color=rgba(0,0,0,0) padding=[0,0,0,0] box-shadow=none
MEDIDO (tema escuro, marca do PRODUTO):
nenhum ancestral com fundo claro — a fronteira da reconciliação vale.
A CONDIÇÃO DO DONO, com dois builds de produção (pré-#659 = 688f798f2 contra o
estado atual): para quem NÃO enviou logo, a marca e o cartão de acesso ocupam
retângulos IDÊNTICOS até a fração de pixel, nos dois temas, antes e depois.
O preço, medido e não escondido: para quem TEM logo enviado, o cartão desce 8px
no escuro (o `dark:py-2` da moldura, num bloco centrado). É a troca que o #659
declara — fica com número para poder ser discutida.
SABOTAGEM, previsão antes de rodar: revertendo a fachada ao pré-#659, 4
asserções vermelhas (pintura, folga, sombra, contenção) e o tema claro intacto.
Observado: exatamente 4, cada uma pelo motivo certo — a da contenção falha pela
ALTURA, porque sem o <div> o pai do logo passa a ser o `flex justify-center`,
que tem a mesma altura do logo.
Duas sondas minhas nasceram cegas e estão registradas no dossiê: uma rodou com
rótulo que não casava com o ramo do script (contagem certa, motivo errado), e a
outra usava "nenhum ancestral claro" no tema CLARO, onde o próprio
`bg-background` é claro por definição.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"dark": {
|
||||
"temaReal": "dark",
|
||||
"temImgEnviada": true,
|
||||
"temMarcaDoProduto": false,
|
||||
"medida": {
|
||||
"tagDoPai": "div",
|
||||
"classeDoPai": "flex justify-center",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"sombra": "none",
|
||||
"raio": "0px",
|
||||
"caixaDoPai": {
|
||||
"x": 448,
|
||||
"y": 241,
|
||||
"largura": 384,
|
||||
"altura": 40
|
||||
},
|
||||
"caixaDoLogo": {
|
||||
"x": 620,
|
||||
"y": 241,
|
||||
"largura": 40,
|
||||
"altura": 40
|
||||
},
|
||||
"naturalWidth": 64
|
||||
},
|
||||
"cartao": {
|
||||
"x": 448,
|
||||
"y": 387,
|
||||
"largura": 384,
|
||||
"altura": 200
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"temaReal": "light",
|
||||
"temImgEnviada": true,
|
||||
"temMarcaDoProduto": false,
|
||||
"medida": {
|
||||
"tagDoPai": "div",
|
||||
"classeDoPai": "flex justify-center",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0
|
||||
],
|
||||
"sombra": "none",
|
||||
"raio": "0px",
|
||||
"caixaDoPai": {
|
||||
"x": 448,
|
||||
"y": 241,
|
||||
"largura": 384,
|
||||
"altura": 40
|
||||
},
|
||||
"caixaDoLogo": {
|
||||
"x": 620,
|
||||
"y": 241,
|
||||
"largura": 40,
|
||||
"altura": 40
|
||||
},
|
||||
"naturalWidth": 64
|
||||
},
|
||||
"cartao": {
|
||||
"x": 448,
|
||||
"y": 387,
|
||||
"largura": 384,
|
||||
"altura": 200
|
||||
}
|
||||
},
|
||||
"deslocamentoDoCartao": {
|
||||
"dx": 0,
|
||||
"dy": 0,
|
||||
"dLargura": 0,
|
||||
"dAltura": 0
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"dark": {
|
||||
"temaReal": "dark",
|
||||
"temImgEnviada": false,
|
||||
"temMarcaDoProduto": true,
|
||||
"medida": {
|
||||
"cadeia": [
|
||||
{
|
||||
"tag": "svg",
|
||||
"classe": "",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"classe": "flex justify-center",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"classe": "w-full max-w-sm space-y-6",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"classe": "flex min-h-screen items-center justify-center bg-background p-6",
|
||||
"fundo": "rgb(22, 21, 16)",
|
||||
"padding": "24px 24px 24px 24px"
|
||||
}
|
||||
],
|
||||
"caixa": {
|
||||
"x": 556.734375,
|
||||
"y": 237,
|
||||
"largura": 166.515625,
|
||||
"altura": 48
|
||||
}
|
||||
},
|
||||
"cartao": {
|
||||
"x": 448,
|
||||
"y": 391,
|
||||
"largura": 384,
|
||||
"altura": 200
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"temaReal": "light",
|
||||
"temImgEnviada": false,
|
||||
"temMarcaDoProduto": true,
|
||||
"medida": {
|
||||
"cadeia": [
|
||||
{
|
||||
"tag": "svg",
|
||||
"classe": "",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"classe": "flex justify-center",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"classe": "w-full max-w-sm space-y-6",
|
||||
"fundo": "rgba(0, 0, 0, 0)",
|
||||
"padding": "0px 0px 0px 0px"
|
||||
},
|
||||
{
|
||||
"tag": "div",
|
||||
"classe": "flex min-h-screen items-center justify-center bg-background p-6",
|
||||
"fundo": "rgb(250, 249, 246)",
|
||||
"padding": "24px 24px 24px 24px"
|
||||
}
|
||||
],
|
||||
"caixa": {
|
||||
"x": 556.734375,
|
||||
"y": 237,
|
||||
"largura": 166.515625,
|
||||
"altura": 48
|
||||
}
|
||||
},
|
||||
"cartao": {
|
||||
"x": 448,
|
||||
"y": 391,
|
||||
"largura": 384,
|
||||
"altura": 200
|
||||
}
|
||||
},
|
||||
"deslocamentoDoCartao": {
|
||||
"dx": 0,
|
||||
"dy": 0,
|
||||
"dLargura": 0,
|
||||
"dAltura": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Mede a moldura do logo na FACHADA (`app/(public)/layout.tsx`) — a superfície 2
|
||||
* do PR #659 — contra o `next start` de produção já buildado.
|
||||
*
|
||||
* Por que este script existe ao lado da spec: o daemon do Docker desta máquina
|
||||
* travou (o socket ACEITA conexão e nunca responde), então Supabase local está
|
||||
* fora e a spec Playwright não carrega — o `loadCreds()` dela semeia pelo banco.
|
||||
* A fachada, porém, não precisa de banco: `marcaDaInstalacao()` devolve `null`
|
||||
* quando a consulta falha (ela não lança), e a resolução cai na camada do
|
||||
* AMBIENTE — que é a semente `APP_LOGO_URL` documentada em CLAUDE.md, não um
|
||||
* atalho de teste. O caminho de produto exercitado é o mesmo; o que muda é a
|
||||
* camada que ganhou a resolução.
|
||||
*
|
||||
* Tudo aqui é medido por ferramenta: `getComputedStyle` e `getBoundingClientRect`.
|
||||
*/
|
||||
import { chromium } from "@playwright/test";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const BASE = process.argv[2] ?? "http://localhost:3007";
|
||||
const ROTULO = process.argv[3] ?? "com-logo";
|
||||
const EVID = "/Users/rafaelmelgaco/wt/qa-logo/evidence/logo-moldura-tema-escuro";
|
||||
mkdirSync(EVID, { recursive: true });
|
||||
|
||||
const px = (s) => Number.parseFloat(s) || 0;
|
||||
|
||||
function canais(cor) {
|
||||
const m = String(cor).match(/rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,/\s]+([\d.]+))?/);
|
||||
if (!m) return null;
|
||||
return { r: +m[1], g: +m[2], b: +m[3], a: m[4] === undefined ? 1 : +m[4] };
|
||||
}
|
||||
const fundoEClaro = (c) => {
|
||||
const k = canais(c);
|
||||
return !!k && k.a >= 0.9 && k.r >= 200 && k.g >= 200 && k.b >= 200;
|
||||
};
|
||||
const fundoETransparente = (c) => {
|
||||
const k = canais(c);
|
||||
return !!k && k.a === 0;
|
||||
};
|
||||
|
||||
const resultados = {};
|
||||
|
||||
const navegador = await chromium.launch();
|
||||
try {
|
||||
for (const tema of ["dark", "light"]) {
|
||||
const ctx = await navegador.newContext({ viewport: { width: 1280, height: 900 } });
|
||||
const p = await ctx.newPage();
|
||||
// O que o navegador de quem escolheu escuro e saiu da conta já faz sozinho:
|
||||
// `THEME_INIT_SCRIPT` (app/layout.tsx) lê esta chave no <head>.
|
||||
await p.addInitScript((t) => window.localStorage.setItem("deskcomm-theme", t), tema);
|
||||
await p.goto(`${BASE}/login`, { waitUntil: "networkidle" });
|
||||
|
||||
const temaReal = await p.evaluate(() => document.documentElement.getAttribute("data-theme"));
|
||||
|
||||
// A âncora: qual ramo a fachada desenhou.
|
||||
const temImgEnviada = (await p.getByTestId("logo-da-fachada").count()) > 0;
|
||||
const temMarcaDoProduto =
|
||||
(await p.getByRole("img", { name: "DeskcommCRM" }).count()) > 0 && !temImgEnviada;
|
||||
|
||||
let medida = null;
|
||||
if (temImgEnviada) {
|
||||
const alvo = p.getByTestId("logo-da-fachada");
|
||||
await alvo.waitFor({ state: "visible", timeout: 15000 });
|
||||
medida = await alvo.evaluate((el) => {
|
||||
const pai = el.parentElement;
|
||||
const cs = getComputedStyle(pai);
|
||||
const rp = pai.getBoundingClientRect();
|
||||
const rl = el.getBoundingClientRect();
|
||||
return {
|
||||
tagDoPai: pai.tagName.toLowerCase(),
|
||||
classeDoPai: pai.className,
|
||||
fundo: cs.backgroundColor,
|
||||
padding: [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft],
|
||||
sombra: cs.boxShadow,
|
||||
raio: cs.borderRadius,
|
||||
caixaDoPai: { x: rp.x, y: rp.y, largura: rp.width, altura: rp.height },
|
||||
caixaDoLogo: { x: rl.x, y: rl.y, largura: rl.width, altura: rl.height },
|
||||
naturalWidth: el.naturalWidth,
|
||||
};
|
||||
});
|
||||
medida.padding = medida.padding.map(px);
|
||||
} else if (temMarcaDoProduto) {
|
||||
// A marca do PRODUTO: a negação é sobre TODA a cadeia até o <body>, não só
|
||||
// o pai — uma moldura em qualquer avô pintaria igual na tela.
|
||||
const alvo = p.getByRole("img", { name: "DeskcommCRM" }).first();
|
||||
await alvo.waitFor({ state: "visible", timeout: 15000 });
|
||||
medida = await alvo.evaluate((el) => {
|
||||
const cadeia = [];
|
||||
let no = el;
|
||||
while (no && no.tagName.toLowerCase() !== "body") {
|
||||
const cs = getComputedStyle(no);
|
||||
cadeia.push({
|
||||
tag: no.tagName.toLowerCase(),
|
||||
classe: typeof no.className === "string" ? no.className : "",
|
||||
fundo: cs.backgroundColor,
|
||||
padding: `${cs.paddingTop} ${cs.paddingRight} ${cs.paddingBottom} ${cs.paddingLeft}`,
|
||||
});
|
||||
no = no.parentElement;
|
||||
}
|
||||
const r = el.getBoundingClientRect();
|
||||
return { cadeia, caixa: { x: r.x, y: r.y, largura: r.width, altura: r.height } };
|
||||
});
|
||||
}
|
||||
|
||||
// A geometria do CARTÃO de acesso — a régua de "não quebrou o que já existia"
|
||||
// nesta superfície: o cartão não pode se mover para quem não enviou logo.
|
||||
const cartao = await p
|
||||
.locator("form")
|
||||
.first()
|
||||
.evaluate((el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: r.x, y: r.y, largura: r.width, altura: r.height };
|
||||
});
|
||||
|
||||
const nome = `fachada-${ROTULO}-${tema}`;
|
||||
await p.screenshot({ path: join(EVID, `${nome}.png`), fullPage: true });
|
||||
resultados[tema] = { temaReal, temImgEnviada, temMarcaDoProduto, medida, cartao };
|
||||
await ctx.close();
|
||||
}
|
||||
} finally {
|
||||
await navegador.close();
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
join(EVID, `fachada-${ROTULO}.json`),
|
||||
JSON.stringify(resultados, null, 2) + "\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
// ── O veredito, com o número ao lado ────────────────────────────────────────
|
||||
const linhas = [];
|
||||
let falhas = 0;
|
||||
const afirmar = (ok, texto) => {
|
||||
if (!ok) falhas++;
|
||||
linhas.push(`${ok ? "VERDE " : "VERMELHO"} ${texto}`);
|
||||
};
|
||||
|
||||
for (const tema of ["dark", "light"]) {
|
||||
const r = resultados[tema];
|
||||
afirmar(r.temaReal === tema, `<html data-theme> = ${r.temaReal} (pedido: ${tema})`);
|
||||
if (ROTULO === "com-logo") {
|
||||
afirmar(r.temImgEnviada, `${tema}: a fachada desenhou o logo ENVIADO`);
|
||||
const m = r.medida;
|
||||
if (!m) continue;
|
||||
afirmar(m.naturalWidth > 0, `${tema}: o bitmap baixou (naturalWidth=${m.naturalWidth})`);
|
||||
if (tema === "dark") {
|
||||
afirmar(fundoEClaro(m.fundo), `dark: moldura pintada — background-color=${m.fundo}`);
|
||||
afirmar(
|
||||
m.padding.every((v) => v > 0),
|
||||
`dark: moldura com folga — padding=[${m.padding}]`,
|
||||
);
|
||||
afirmar(m.sombra !== "none", `dark: moldura com sombra — box-shadow=${m.sombra.slice(0, 60)}`);
|
||||
afirmar(
|
||||
m.caixaDoPai.largura > m.caixaDoLogo.largura &&
|
||||
m.caixaDoPai.altura > m.caixaDoLogo.altura &&
|
||||
m.caixaDoPai.x <= m.caixaDoLogo.x &&
|
||||
m.caixaDoPai.y <= m.caixaDoLogo.y,
|
||||
`dark: a moldura CONTÉM o logo — moldura=${JSON.stringify(m.caixaDoPai)} logo=${JSON.stringify(m.caixaDoLogo)}`,
|
||||
);
|
||||
} else {
|
||||
afirmar(fundoETransparente(m.fundo), `light: SEM moldura — background-color=${m.fundo}`);
|
||||
afirmar(
|
||||
m.padding.every((v) => v === 0),
|
||||
`light: SEM folga — padding=[${m.padding}]`,
|
||||
);
|
||||
afirmar(m.sombra === "none", `light: SEM sombra — box-shadow=${m.sombra}`);
|
||||
}
|
||||
} else {
|
||||
afirmar(r.temMarcaDoProduto, `${tema}: a fachada desenhou a marca do PRODUTO (svg inline)`);
|
||||
afirmar(!r.temImgEnviada, `${tema}: nenhuma <img> enviada na fachada`);
|
||||
// ⚠️ SÓ NO ESCURO, e a primeira versão deste script errou aqui: no tema
|
||||
// CLARO o fundo da própria página (`bg-background`) é `rgb(250,249,246)`, que
|
||||
// é claro por definição — a sonda acusava a página de ser a moldura. A régua
|
||||
// "nenhum ancestral claro" só separa moldura de não-moldura contra um fundo
|
||||
// escuro; no claro ela não mede nada.
|
||||
if (tema === "dark") {
|
||||
const comMoldura = (r.medida?.cadeia ?? []).filter((n) => fundoEClaro(n.fundo));
|
||||
afirmar(
|
||||
comMoldura.length === 0,
|
||||
`dark: NENHUM ancestral da marca do produto tem fundo claro — ${JSON.stringify(comMoldura)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── A regressão, e ela só vale para quem NÃO enviou logo ────────────────────
|
||||
//
|
||||
// ⚠️ Esta asserção estava mal escopada na primeira versão deste script, e o
|
||||
// vermelho que ela deu era MEU, não do produto: com logo enviado, a moldura
|
||||
// acrescenta `dark:py-2` (8px em cima e 8px embaixo) à caixa do logo, o bloco de
|
||||
// acesso é centrado verticalmente, e o cartão desce 8px no tema escuro. Isso é
|
||||
// a TROCA que o #659 declara e que o dono aceitou ("moldura desnecessária" é
|
||||
// melhor que "logo invisível") — não é regressão, é o preço, e medi-lo é o que
|
||||
// permite discuti-lo.
|
||||
//
|
||||
// A condição do dono — "não quebra o visual que já existe" — é sobre quem NUNCA
|
||||
// enviou logo. Para esses, o deslocamento tem de ser ZERO, e é só aí que a
|
||||
// igualdade é exigida.
|
||||
const deslocamento = {
|
||||
dx: resultados.dark.cartao.x - resultados.light.cartao.x,
|
||||
dy: resultados.dark.cartao.y - resultados.light.cartao.y,
|
||||
dLargura: resultados.dark.cartao.largura - resultados.light.cartao.largura,
|
||||
dAltura: resultados.dark.cartao.altura - resultados.light.cartao.altura,
|
||||
};
|
||||
if (ROTULO === "sem-logo") {
|
||||
// A marca do PRODUTO ocupa o MESMO retângulo nos dois temas. É a régua que
|
||||
// funciona nos dois (ao contrário de "nenhum ancestral claro"): se o chip do
|
||||
// #659 tivesse vazado para este ramo, o `dark:py-*` moveria a marca no escuro
|
||||
// e só no escuro.
|
||||
const c = resultados.dark.medida?.caixa;
|
||||
const d = resultados.light.medida?.caixa;
|
||||
afirmar(
|
||||
!!c && !!d && JSON.stringify(c) === JSON.stringify(d),
|
||||
`a marca do PRODUTO ocupa o mesmo retângulo nos dois temas — dark=${JSON.stringify(c)} light=${JSON.stringify(d)}`,
|
||||
);
|
||||
afirmar(
|
||||
deslocamento.dx === 0 &&
|
||||
deslocamento.dy === 0 &&
|
||||
deslocamento.dLargura === 0 &&
|
||||
deslocamento.dAltura === 0,
|
||||
`SEM logo enviado, o cartão de acesso NÃO se move entre os temas — deslocamento=${JSON.stringify(deslocamento)}`,
|
||||
);
|
||||
} else {
|
||||
linhas.push(
|
||||
`MEDIDO com logo enviado, o cartão desce no escuro (o preço declarado do #659) — deslocamento=${JSON.stringify(deslocamento)}`,
|
||||
);
|
||||
}
|
||||
resultados.deslocamentoDoCartao = deslocamento;
|
||||
writeFileSync(
|
||||
join(EVID, `fachada-${ROTULO}.json`),
|
||||
JSON.stringify(resultados, null, 2) + "\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
console.log(`\n── FACHADA (${ROTULO}) ──`);
|
||||
for (const l of linhas) console.log(l);
|
||||
console.log(`\nfalhas=${falhas}`);
|
||||
process.exit(falhas === 0 ? 0 : 1);
|
||||
Reference in New Issue
Block a user