mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
Peça de vídeo publicada e com arquivo passa a mostrar um frame estático leve no card público antes do play, sem nenhum request do arquivo antes do gesto (`preload="none"` do S27 intacto). O frame é um derivado privado em `contentMedia` (nome determinístico), gerado por um dono só — `ensureContentPieceFrame` — chamado pelo job C211 (que já tem o arquivo em disco: a extração sai de graça) e pela rota `GET /conteudos/<slug>/frame` em miss, que cura o acervo anterior sem backfill, fila, collection nova ou migration. A porta reusa o gate de slug publicado e o proxy de mídia privada do `/midia`: o kill switch fecha as duas de uma vez. Sem frame possível, o slot mostra o estado neutro honesto com play e o marcador — nunca imagem quebrada, nunca retry. A projeção declara o frame por elegibilidade (vídeo com arquivo), não pela existência do derivado, então a cura é invisível para o cache da listagem e não há tag para bustar. O frame também aparece na página da peça e no card da home (variante compacta de 124px, cena 05). A passagem de design (tier primário) achou dois bugs que só o browser mostra: o glyph de imagem quebrada numa requisição que falha, e — ao esconder a imagem até decodificar — o frame sumindo para sempre no caminho mais rápido (uma imagem que carrega antes da hidratação não dispara `load` nem `error` de novo; o `ref` decide os dois desfechos). Também: o guard de falha de e2e aceita `RegExp` e confere os paths declarados antes do caso do favicon (todo 404 do browser traz a mesma mensagem), o manifest de e2e mapeia os módulos novos, e o job C211 passa o arquivo que já tem em disco em vez de rebaixar o vídeo. Design tier: central-conteudos-preview-frame-video Closes #1352
164 lines
6 KiB
TypeScript
164 lines
6 KiB
TypeScript
import path from 'node:path'
|
|
|
|
import {
|
|
test as base,
|
|
expect,
|
|
type ConsoleMessage,
|
|
type Response,
|
|
type WebError,
|
|
} from '@playwright/test'
|
|
|
|
type FailureKind = 'console.error' | 'pageerror' | 'same-origin 5xx' | 'weberror'
|
|
|
|
type BrowserFailure = {
|
|
kind: FailureKind
|
|
detail: string
|
|
}
|
|
|
|
type E2EFailureGuardFixtures = {
|
|
e2eFailureGuard: void
|
|
/**
|
|
* Same-origin paths whose failed responses this spec provokes on purpose.
|
|
*
|
|
* A `fetch` the app handles in code still makes the browser log "Failed to
|
|
* load resource", so a spec asserting a refusal (B40: signing in with a
|
|
* revoked passkey) would otherwise fail the guard for doing its job. Declaring
|
|
* the path — rather than allowing 4xx broadly — keeps an unhandled request
|
|
* failure in any other spec a failure. It is an option so a spec declares it
|
|
* once with `test.use`, instead of pushing at exactly the right moment
|
|
* mid-test.
|
|
*
|
|
* A `RegExp` is accepted for a door whose path carries an id (C226: the still
|
|
* of a piece is `/conteudos/<slug>/frame` and answers 404 until the self-heal
|
|
* lands, so any spec rendering a video card would trip the guard). A literal
|
|
* is still the default, and a pattern still has to name one surface.
|
|
*/
|
|
expectedRequestFailurePaths: (string | RegExp)[]
|
|
}
|
|
|
|
const loadFailurePrefix = 'Failed to load resource'
|
|
|
|
const missingFaviconConsoleError =
|
|
'Failed to load resource: the server responded with a status of 404 (Not Found)'
|
|
|
|
const errorDetail = (error: Error): string => error.stack ?? error.message
|
|
|
|
const responseDetail = (response: Response): string =>
|
|
`${response.status()} ${response.request().method()} ${response.url()}`
|
|
|
|
const consoleDetail = (message: ConsoleMessage): string => {
|
|
const { url, lineNumber, columnNumber } = message.location()
|
|
const location = url ? ` (${url}:${lineNumber}:${columnNumber})` : ''
|
|
return `${message.text()}${location}`
|
|
}
|
|
|
|
const isAllowedConsoleError = (
|
|
message: ConsoleMessage,
|
|
origin: string | null,
|
|
expectedRequestFailurePaths: (string | RegExp)[],
|
|
): boolean => {
|
|
const { url } = message.location()
|
|
if (!url || origin === null) return false
|
|
|
|
const location = new URL(url)
|
|
if (location.origin !== origin) return false
|
|
|
|
if (message.text().startsWith(loadFailurePrefix)) {
|
|
// The declared paths come FIRST: every 404 carries the exact same browser
|
|
// message as the missing favicon, so a favicon-shaped early return would
|
|
// swallow a refusal the spec declared on purpose.
|
|
const declared = expectedRequestFailurePaths.some((expected) =>
|
|
expected instanceof RegExp
|
|
? expected.test(location.pathname)
|
|
: expected === location.pathname,
|
|
)
|
|
if (declared) return true
|
|
}
|
|
|
|
// The app intentionally has no /favicon.ico asset yet.
|
|
if (message.text() === missingFaviconConsoleError) return location.pathname === '/favicon.ico'
|
|
|
|
return false
|
|
}
|
|
|
|
export const test = base.extend<E2EFailureGuardFixtures>({
|
|
expectedRequestFailurePaths: [[], { option: true }],
|
|
e2eFailureGuard: [
|
|
async ({ baseURL, context, expectedRequestFailurePaths, page }, use) => {
|
|
const failures: BrowserFailure[] = []
|
|
const origin = baseURL ? new URL(baseURL).origin : null
|
|
|
|
const onConsole = (message: ConsoleMessage) => {
|
|
if (
|
|
message.type() !== 'error' ||
|
|
isAllowedConsoleError(message, origin, expectedRequestFailurePaths)
|
|
) {
|
|
return
|
|
}
|
|
failures.push({ kind: 'console.error', detail: consoleDetail(message) })
|
|
}
|
|
const onPageError = (error: Error) => {
|
|
failures.push({ kind: 'pageerror', detail: errorDetail(error) })
|
|
}
|
|
const onResponse = (response: Response) => {
|
|
if (
|
|
response.status() >= 500 &&
|
|
origin !== null &&
|
|
new URL(response.url()).origin === origin
|
|
) {
|
|
failures.push({ kind: 'same-origin 5xx', detail: responseDetail(response) })
|
|
}
|
|
}
|
|
const onWebError = (webError: WebError) => {
|
|
failures.push({ kind: 'weberror', detail: errorDetail(webError.error()) })
|
|
}
|
|
|
|
page.on('console', onConsole)
|
|
page.on('pageerror', onPageError)
|
|
page.on('response', onResponse)
|
|
context.on('weberror', onWebError)
|
|
|
|
// S8 — the home story-video embed is a YouTube iframe; e2e must stay
|
|
// hermetic (CI network must never gate a home-page test on the real
|
|
// player loading — same spirit as the S2/S3 feed stubs).
|
|
await context.route('https://www.youtube-nocookie.com/**', (route) => route.abort())
|
|
|
|
// S10 — the campaign-home pixel spec publishes a Meta Pixel ID into the
|
|
// shared test DB, so concurrent specs may render the home with the base
|
|
// code. Fulfill the external script locally (never a real request to
|
|
// Meta): a blocked network would trip the console-error guard below.
|
|
await context.route('https://connect.facebook.net/**', (route) =>
|
|
route.fulfill({ body: '', contentType: 'application/javascript' }),
|
|
)
|
|
|
|
// S25 — the home radio is an own `<audio preload="none">` pointed at the
|
|
// stream, so nothing is requested with the pageview (the S24 iframe stub
|
|
// is gone: a residual zeno.fm/player request would now be a real failure).
|
|
// The stub serves the short MP3 only when a spec presses play; the player
|
|
// ignores `ended` (a live stream never ends), which keeps "playing" stable.
|
|
await context.route('https://stream.zeno.fm/**', (route) =>
|
|
route.fulfill({
|
|
path: path.resolve(process.cwd(), 'tests/fixtures/jingle-tone.mp3'),
|
|
contentType: 'audio/mpeg',
|
|
}),
|
|
)
|
|
|
|
await use()
|
|
|
|
page.off('console', onConsole)
|
|
page.off('pageerror', onPageError)
|
|
page.off('response', onResponse)
|
|
context.off('weberror', onWebError)
|
|
|
|
if (failures.length) {
|
|
const details = failures
|
|
.map(({ kind, detail }, index) => `${index + 1}. [${kind}] ${detail}`)
|
|
.join('\n\n')
|
|
throw new Error(`Unexpected browser/runtime failures:\n\n${details}`)
|
|
}
|
|
},
|
|
{ auto: true },
|
|
],
|
|
})
|
|
|
|
export { expect }
|