mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-14 17:43:04 +00:00
* deploy: limpeza pós-deploy do build cache e tags locais antigas (INF3/F2) O deploy compila no homeserver e acumulava build cache + tags locais antigas no disco raiz (incidente 19/08: 45G de cache). Após o smoke: docker builder prune -f + remoção das tags locais teqo-1313 exceto o SHA em uso (best-effort, registry preserva o rollback). * feat(S9): captura de novidades da campanha na home pública (form + CTA do hero) Seção 'Receba as novidades da campanha' acima do rodapé: nome + WhatsApp obrigatórios, e-mail/estado/cidade/comentário opcionais, toggle pré-selecionado 'Quero fazer parte do time' gravado como subscription.campaignLevel (time|esporadico). CTA secundário do hero vira o atalho (#novidades, scroll suave). Contact + Subscription em withPayloadTransaction com consentimento novo fail-closed por chave estável 'campanha-novidades' — nada é gravado sem o texto aprovado no admin. Migration 20260819_192605 (state de contact nullable + coluna campaign_level). Impl plan, changelog, int + e2e. * plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil * plans(C141,C143,C142): permissões granulares por assessor + responsáveis de demanda + UI por perfil * feat(S8): seção 'Nossa história' na home — vídeo da trajetória embutido do YouTube (Closes #91) * docs(S8): resolução do conflito com S9 (história → newsletter) + nota do guard e2e dev-only * feat(S10): pixel do Meta na página pública de campanha (campo admin em SiteSettings + PageView na home + Lead no sucesso da captura) Campo tracking.facebookPixelId no global SiteSettings (mesma validação/normalização do precedente Petition, regra centralizada em facebookPixel.ts); home (grupo (home)) injeta o base code + PageView quando o ID está configurado (fail-closed) e o CampaignNewsletterForm dispara exatamente um Lead no sucesso do S9. Migration 20260819_213947_add_site_settings_facebook_pixel_id. Closes #94 * refactor(S10): simplify pass — Lead wiring unit-tested e raças de estado compartilhado do e2e resolvidas e2e: stub hermético de connect.facebook.net no guard compartilhado (specs paralelos renderizam a home com o pixel); advisory lock no Postgres serializando o login do usuário de teste (Payload perde sessão em login concorrente read-modify-write — classe documentada no webauthn); seedTestUser vira create-if-missing (sem janela de usuário ausente nem reset de senha); Lead do form protegido do catch de erro (tracking nunca falha uma captura commitada); descrição do campo compartilhada; unidade nova campaignNewsletterLead cobre exatamente um Lead no sucesso. * feat(C141): perfis de permissão por assessor — Visão × Edição configuráveis, enforcement fail-closed (Closes #105) * chore(C141): regenera changelog agregado após rebase com S10 * plans(C144): options write-scoped para formulários de escrita pós-C141 --------- Co-authored-by: Francisco Solla <admin@solla.dev>
97 lines
4.3 KiB
TypeScript
97 lines
4.3 KiB
TypeScript
import type { APIRequestContext } from '@playwright/test'
|
|
|
|
import { adminHeaders } from '../helpers/adminApi'
|
|
import { seedTestUser } from '../helpers/seedUser'
|
|
import { expect, test } from './fixtures/e2eTest'
|
|
|
|
/**
|
|
* S10 — the Meta pixel on the public campaign page: configured through
|
|
* `SiteSettings.tracking.facebookPixelId` (admin REST), the home renders the
|
|
* base code (inline `next/script` + `fbq` init) and nothing at all without a
|
|
* configured ID (fail-closed). The external `fbevents.js` fetch is fulfilled
|
|
* locally by the shared e2e guard (fixtures/e2eTest.ts), so no spec ever
|
|
* depends on the Meta CDN; the base code itself is inline. The `Lead` wiring
|
|
* (successful S9 capture → exactly one queued `fbq` Lead) is pinned by the
|
|
* unit spec `campaignNewsletterLead.unit.spec.tsx` — a real submission here
|
|
* would share the `campanha-novidades` consent key with the parallel
|
|
* `campaignNewsletter` spec's purge/create lifecycle. Serial mode (precedent
|
|
* `campaignNearestMunicipality`): the tests share one worker — a parallel
|
|
* split would race two `seedTestUser` beforeAlls.
|
|
*/
|
|
test.describe.configure({ mode: 'serial' })
|
|
|
|
test.describe('Campaign home Meta pixel', () => {
|
|
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000'
|
|
const runSuffix = Date.now()
|
|
// Valid 15-digit numeric ID (same class the admin validation accepts).
|
|
const PIXEL_ID = '123456789012345'
|
|
|
|
// `_status: 'published'` publishes through the global's draft pipeline, same
|
|
// pattern the frontend spec uses for versioned documents.
|
|
const setSitePixel = async (
|
|
request: APIRequestContext,
|
|
headers: Record<string, string>,
|
|
pixelId: string | null,
|
|
) => {
|
|
// Insurance against the same-user concurrent-login session race with
|
|
// specs that do NOT route their login through the advisory-locked
|
|
// `adminHeaders` helper (e.g. the frontend spec's inline logins): a 403
|
|
// means the session row was lost to another worker — re-login mints a
|
|
// fresh one. The locked helper already serializes the specs that use it.
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
const response = await request.post(`${baseURL}/api/globals/site-settings`, {
|
|
headers,
|
|
data: { tracking: { facebookPixelId: pixelId }, _status: 'published' },
|
|
})
|
|
if (response.ok()) return
|
|
if (response.status() === 403 && attempt === 0) {
|
|
headers = await adminHeaders(request, baseURL)
|
|
continue
|
|
}
|
|
throw new Error(`setSitePixel(${pixelId}) failed with ${response.status()}`)
|
|
}
|
|
}
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
await seedTestUser()
|
|
// Self-healing lifecycle (S9 precedent): an aborted run could leave the
|
|
// pixel behind and poison the fail-closed assertion — purge first.
|
|
const headers = await adminHeaders(request, baseURL)
|
|
await setSitePixel(request, headers, null)
|
|
})
|
|
|
|
test.afterAll(async ({ request }) => {
|
|
const headers = await adminHeaders(request, baseURL).catch(() => undefined)
|
|
if (headers) {
|
|
await setSitePixel(request, headers, null)
|
|
}
|
|
})
|
|
|
|
test('renders nothing without a configured pixel ID', async ({ page }) => {
|
|
// Fresh load (unique query param misses every HTTP/router cache entry —
|
|
// the content-section trick); the app ignores unknown params. The
|
|
// fail-closed read comes from the global, which beforeAll just reset.
|
|
await page.goto(`/?e2e=${runSuffix}-none`)
|
|
await expect(page.locator('script[id^="meta-pixel-"]')).toHaveCount(0)
|
|
})
|
|
|
|
test('renders the base code when the site settings carry a valid ID', async ({
|
|
page,
|
|
request,
|
|
}) => {
|
|
const headers = await adminHeaders(request, baseURL)
|
|
// The REST update busts `global_site-settings` via afterChange BEFORE the
|
|
// goto, so the fresh render carries the pixel (deterministic, no cache race).
|
|
await setSitePixel(request, headers, PIXEL_ID)
|
|
await page.goto(`/?e2e=${runSuffix}-set`)
|
|
|
|
const pixelScript = page.locator(`script#meta-pixel-${PIXEL_ID}`)
|
|
await expect(pixelScript).toBeAttached()
|
|
|
|
// The base code defines `window.fbq` synchronously (queue-based no-op
|
|
// until fbevents.js processes it) — the seam `trackMetaLead` uses.
|
|
await expect
|
|
.poll(() => page.evaluate(() => typeof (window as { fbq?: unknown }).fbq === 'function'))
|
|
.toBe(true)
|
|
})
|
|
})
|