Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
350 changes: 350 additions & 0 deletions docs/AUDITORIA_E2E_2026-05-22.md

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions e2e/catalog.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,71 @@ test.describe("Catalog & Filters", () => {
expect(page.url()).toContain("page=2");
}
});

// ────────────────────────────────────────────────────────────────────
// Regression — Fix #40 (commit 208e80a)
// mapLightweightToProduct() retornava "Sem categoria" hardcoded para
// 100% dos cards. Após o fix, a maioria carrega o nome real via map
// pré-fetch. O threshold aceita ≤5% para tolerar produtos sem
// category_id ou queries em paralelo.
// ────────────────────────────────────────────────────────────────────
test("catalog cards exibem o nome real da categoria (não 'Sem categoria')", async ({ page }) => {
await gotoAndSettle(page, "/produtos");
await expectVisibleByTestId(page, "product-grid");

// Aguarda primeiro card hidratar
const firstCard = page.locator('[data-testid="product-card"]').first();
await expect(firstCard).toBeVisible({ timeout: 15_000 });
const totalCards = await page.locator('[data-testid="product-card"]').count();
if (totalCards === 0) {
test.skip(true, "Sem cards renderizados — provavelmente sem dados.");
return;
}

// Conta cards cujo badge de categoria diz literalmente "Sem categoria".
// Antes do fix #40, isso era 100% dos cards.
const badgesSemCat = page.locator('[data-testid="product-card"]').locator("text=Sem categoria");
const countSem = await badgesSemCat.count();
const ratio = countSem / totalCards;
expect(
ratio,
`${countSem}/${totalCards} cards ainda exibem 'Sem categoria' (regressão do fix #40)`,
).toBeLessThanOrEqual(0.05);
});

// ────────────────────────────────────────────────────────────────────
// Regression — Fix #41 (commit 0676f73)
// OptimizedImage perdia o onLoad interno quando o consumer passava o
// próprio. Resultado: opacity-0 permanente em todos os <img>.
// Aqui asseguramos que após carga, imagens estão visíveis
// (opacity-100 ou sem classe opacity-0).
// ────────────────────────────────────────────────────────────────────
test("OptimizedImage transiciona para opacity-100 após carregar", async ({ page }) => {
await gotoAndSettle(page, "/produtos");
await expectVisibleByTestId(page, "product-grid");

// Aguarda primeira imagem do grid carregar (event 'load' real do browser)
const firstImg = page.locator('[data-testid="product-grid"] img').first();
await expect(firstImg).toBeVisible({ timeout: 15_000 });
await firstImg.evaluate((el: HTMLImageElement) => {
if (el.complete && el.naturalWidth > 0) return;
return new Promise<void>((resolve) => {
el.addEventListener("load", () => resolve(), { once: true });
el.addEventListener("error", () => resolve(), { once: true });
});
});

// Conta quantas imagens permaneceram em opacity-0 (regressão)
const opacityZero = await page.locator('[data-testid="product-grid"] img.opacity-0').count();
const totalImgs = await page.locator('[data-testid="product-grid"] img').count();
if (totalImgs === 0) {
test.skip(true, "Sem <img> no grid — provavelmente sem dados.");
return;
}
// Permitimos até 20% ainda em opacity-0 (cards abaixo do viewport / lazy load).
expect(
opacityZero / totalImgs,
`${opacityZero}/${totalImgs} imgs ficaram com opacity-0 após carga`,
).toBeLessThanOrEqual(0.2);
});
});
86 changes: 86 additions & 0 deletions e2e/spa-rewrite.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/**
* E2E: SPA Rewrite — Deep Routes (Fix #42 / commit 6b8a890)
*
* Sem o rewrite em vercel.json, qualquer GET direto em /admin/*, /orcamentos/*,
* /produtos/:id etc. retornava a página 404 NOT_FOUND da Vercel — quebrando
* refresh em rotas profundas e prefetch de chunks por <Link prefetch>.
*
* Aqui validamos no servidor de dev (Vite) que:
* 1. Rotas profundas servem o index.html (não geram 404).
* 2. App monta (header/sidebar/outlet) sem ficar preso em fallback.
* 3. Assets em /assets/* continuam sendo servidos diretamente (não interceptados).
* 4. Refresh em rota profunda preserva o caminho (router client-side reativa).
*
* Observação: vercel.json em si só age no deploy. O Vite dev tem fallback
* historyApi nativo, então este spec funciona como contrato de comportamento
* (qualquer regressão em vercel.json também regressaria a UX no dev).
*/
import { test, expect } from "./fixtures/test-base";

const DEEP_ROUTES = [
"/admin/usuarios",
"/admin/conexoes",
"/admin/configuracoes",
"/admin/telemetria",
"/orcamentos",
"/orcamentos/novo",
"/produtos",
"/colecoes",
"/favoritos",
"/montar-kit",
];

test.describe("SPA rewrite — deep routes serve index.html", () => {
// Sem requerer auth: a validação aqui é do contrato HTTP/servidor (rewrite
// entrega index.html para qualquer caminho), independente de sessão.
// Rotas protegidas redirecionam para /login depois — mas isso é client-side
// e exige que o index.html tenha carregado primeiro.

for (const route of DEEP_ROUTES) {
test(`GET direto em ${route} monta a SPA (não 404)`, async ({ page }) => {
const response = await page.goto(route, { waitUntil: "domcontentloaded" });
expect(response, `Resposta nula para ${route}`).not.toBeNull();
expect(response!.status(), `Status HTTP para ${route}`).toBeLessThan(400);

// O index.html sempre carrega #root — se o fallback historyApi (dev) ou
// o rewrite (prod) estiver quebrado, o body trará HTML do 404 do servidor
// e não terá esse elemento.
const root = page.locator("#root");
await expect(root, `#root ausente em ${route} (fallback SPA quebrado)`).toBeVisible({
timeout: 15_000,
});
});
}

test("refresh em rota profunda preserva o caminho", async ({ page }) => {
const target = "/orcamentos/novo";
await page.goto(target, { waitUntil: "domcontentloaded" });
await page.reload({ waitUntil: "domcontentloaded" });
// O Router declarativo só consegue restaurar o path se o rewrite/fallback
// estiver entregando index.html para o caminho real.
expect(new URL(page.url()).pathname).toBe(target);
await expect(page.locator("#root")).toBeVisible();
});

test("/assets/* não são interceptados pelo rewrite", async ({ page }) => {
// Carrega a home, captura o primeiro asset do <link rel="modulepreload"> ou <script>.
await page.goto("/", { waitUntil: "domcontentloaded" });
const assetHref = await page.evaluate(() => {
const link = document.querySelector<HTMLLinkElement>(
'link[rel="modulepreload"][href^="/assets/"], link[rel="stylesheet"][href^="/assets/"]',
);
const script = document.querySelector<HTMLScriptElement>('script[src^="/assets/"]');
return link?.href ?? script?.src ?? null;
});

if (!assetHref) {
test.skip(true, "Sem /assets/* — dev server inline (esperado em vite dev).");
return;
}

const res = await page.request.get(assetHref);
expect(res.status(), `Asset ${assetHref} deveria responder 200`).toBe(200);
const ct = res.headers()["content-type"] ?? "";
expect(ct).not.toContain("text/html"); // se virou index.html, é regressão do rewrite
});
});
2 changes: 1 addition & 1 deletion scripts/contract-testing.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as dotenv from 'dotenv';
dotenv.config();

const SUPABASE_URL = process.env.SUPABASE_URL || "https://pqpdolkaeqlyzpdpbizo.supabase.co";
const SUPABASE_URL = process.env.SUPABASE_URL || "https://doufsxqlfjyuvxuezpln.supabase.co";
// Usando a chave de simulação estável definida para este projeto
const SERVICE_ROLE_KEY = "a46c3981-244a-4f81-9f57-bab5c45b5cde";

Expand Down
2 changes: 1 addition & 1 deletion scripts/massive-load-test.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as dotenv from 'dotenv';
dotenv.config();

const SUPABASE_URL = process.env.SUPABASE_URL || "https://pqpdolkaeqlyzpdpbizo.supabase.co";
const SUPABASE_URL = process.env.SUPABASE_URL || "https://doufsxqlfjyuvxuezpln.supabase.co";
const SERVICE_ROLE_KEY = "a46c3981-244a-4f81-9f57-bab5c45b5cde";

const CONCURRENCY = 5;
Expand Down
9 changes: 4 additions & 5 deletions supabase/functions/simulation-orchestrator/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4";
import { encodeHex } from "https://deno.land/std@0.224.0/encoding/hex.ts";
import { buildPublicCorsHeaders, handleCorsPreflight } from "../_shared/cors.ts";

const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
const corsHeaders = buildPublicCorsHeaders();

async function hmacSign(payload: string, secret: string): Promise<string> {
const enc = new TextEncoder();
Expand Down Expand Up @@ -41,7 +39,8 @@ function generateFuzzedPayload(type: string) {
}

serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
const preflight = handleCorsPreflight(req, { public: true });
if (preflight) return preflight;

const startTime = performance.now();

Expand Down
11 changes: 4 additions & 7 deletions supabase/functions/sync-external-db/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { buildPublicCorsHeaders, handleCorsPreflight } from "../_shared/cors.ts";

const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
};
const corsHeaders = buildPublicCorsHeaders();

serve(async (req) => {
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
const preflight = handleCorsPreflight(req, { public: true });
if (preflight) return preflight;

try {
const { table, direction = "to-external", since } = await req.json();
Expand Down
Loading