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;
Comment on lines +85 to +89
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Check empty grid before waiting for first product card

This test tries to skip when there are no products, but the skip branch is unreachable because expect(firstCard).toBeVisible() runs first and will timeout when the catalog is empty. In environments/tenants without seeded products, this turns an intended skip into a hard failure and can make the regression suite flaky. Count cards (or otherwise detect empty state) before the visibility assertion so the test can skip as designed.

Useful? React with 👍 / 👎.

}

// 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);
Comment on lines +96 to +100
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Relax category fallback ratio for small product counts

The asserted threshold (<= 0.05) contradicts the stated intent to tolerate some uncategorized products when the page has fewer than 20 cards: for 1–19 cards, even a single legitimate "Sem categoria" result fails the test. This makes the regression check brittle across tenants with small catalogs and can produce false failures unrelated to fix #40.

Useful? React with 👍 / 👎.

});

// ────────────────────────────────────────────────────────────────────
// 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 });
Comment on lines +116 to +120
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard empty image list before awaiting first image visibility

The same control-flow issue exists here: the test only calls test.skip when totalImgs === 0, but it first asserts firstImg is visible. If the grid renders no images (valid for empty catalogs or filtered datasets), the test fails on timeout instead of skipping, which creates avoidable CI noise. Move the zero-images guard ahead of the visibility/load wait.

Useful? React with 👍 / 👎.

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);
Comment on lines +133 to +136
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude offscreen lazy images from opacity ratio assertion

This assertion divides by all images in the grid, but many images are expected to remain opacity-0 until they enter viewport in lazy-load flows, so the ratio can exceed 20% even when onLoad chaining works correctly. In long grids or small viewports this produces false regressions; scope the denominator to loaded/visible images (or wait for a deterministic loaded subset) before asserting.

Useful? React with 👍 / 👎.

});
});
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).
Comment on lines +14 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Run rewrite regression against Vercel-equivalent server

This suite claims to validate the vercel.json SPA rewrite, but it runs against the Playwright web server (npx vite in playwright.config.ts), where history fallback is handled by Vite itself. As a result, removing or breaking Vercel rewrite rules can still leave these tests green, producing a false sense of protection for the production-only 404 regression.

Useful? React with 👍 / 👎.

*/
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);
Comment on lines +41 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Assert deep-route URL is preserved for each route check

Each route test only checks status < 400 and #root visibility, so a server-side redirect (for example to /) can still pass even though the deep-link contract is broken for that route. The suite should also assert new URL(page.url()).pathname === route per case, otherwise several rewrite regressions remain undetected.

Useful? React with 👍 / 👎.


// 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;
Comment on lines +70 to +73
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid always-skipped asset check under Vite dev

The asset assertion is effectively non-executing in the configured test environment: this code only looks for /assets/* links/scripts, but Vite dev serves entry modules via /@vite/client and /src/*, so assetHref becomes null and the test is skipped every run. That means this test does not currently verify the rewrite-interception behavior it is meant to guard.

Useful? React with 👍 / 👎.

});

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