mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 14:55:16 +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
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { contentPieceFramePath } from '@/lib/contentPieceCatalog'
|
|
import {
|
|
buildContentPieceFrameFfmpegArgs,
|
|
CONTENT_PIECE_FRAME_MAX_SOURCE_BYTES,
|
|
CONTENT_PIECE_FRAME_SEEK_ATTEMPTS,
|
|
CONTENT_PIECE_FRAME_WAIT_MS,
|
|
contentPieceFrameFilename,
|
|
contentPieceFrameMediaAlt,
|
|
contentPieceFrameSizeAllowed,
|
|
} from '@/lib/contentPieceFrame'
|
|
|
|
describe('contentPieceFrameFilename / contentPieceFramePath', () => {
|
|
it('is deterministic and unique per piece (the idempotence key)', () => {
|
|
expect(contentPieceFrameFilename(42)).toBe('content-piece-frame-42.jpg')
|
|
expect(contentPieceFrameFilename(43)).toBe('content-piece-frame-43.jpg')
|
|
})
|
|
|
|
it('lives beside the media route of the same public piece', () => {
|
|
expect(contentPieceFramePath('peca-de-video')).toBe('/conteudos/peca-de-video/frame')
|
|
})
|
|
})
|
|
|
|
describe('contentPieceFrameMediaAlt', () => {
|
|
it('describes the derived row (contentMedia.alt is required)', () => {
|
|
expect(contentPieceFrameMediaAlt('Voto 1313')).toBe('Frame do vídeo «Voto 1313»')
|
|
})
|
|
})
|
|
|
|
describe('contentPieceFrameSizeAllowed', () => {
|
|
it('refuses a source the self-heal would have to download whole', () => {
|
|
expect(contentPieceFrameSizeAllowed(1024)).toBe(true)
|
|
expect(contentPieceFrameSizeAllowed(CONTENT_PIECE_FRAME_MAX_SOURCE_BYTES)).toBe(true)
|
|
expect(contentPieceFrameSizeAllowed(CONTENT_PIECE_FRAME_MAX_SOURCE_BYTES + 1)).toBe(false)
|
|
})
|
|
|
|
it('does not skip on an unknown size (a projection limit is not a licence)', () => {
|
|
expect(contentPieceFrameSizeAllowed(null)).toBe(true)
|
|
expect(contentPieceFrameSizeAllowed(undefined)).toBe(true)
|
|
expect(contentPieceFrameSizeAllowed(Number.NaN)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('buildContentPieceFrameFfmpegArgs', () => {
|
|
it('seeks to the second and takes a single scaled jpeg frame', () => {
|
|
const args = buildContentPieceFrameFfmpegArgs({
|
|
inputPath: '/tmp/source.mp4',
|
|
outputPath: '/tmp/content-piece-frame-42.jpg',
|
|
atSeconds: 1,
|
|
})
|
|
|
|
expect(args).toEqual([
|
|
'-nostdin',
|
|
'-hide_banner',
|
|
'-y',
|
|
'-ss',
|
|
'1',
|
|
'-i',
|
|
'/tmp/source.mp4',
|
|
'-frames:v',
|
|
'1',
|
|
'-vf',
|
|
'scale=640:-2',
|
|
'-q:v',
|
|
'3',
|
|
'-an',
|
|
'/tmp/content-piece-frame-42.jpg',
|
|
])
|
|
// The seek is an input option: before `-i`, never after — and the command
|
|
// is an argv array, never a shell string (the `toEqual` above is the pin).
|
|
expect(args.indexOf('-ss')).toBeLessThan(args.indexOf('-i'))
|
|
})
|
|
|
|
it('never seeks to a negative instant', () => {
|
|
const args = buildContentPieceFrameFfmpegArgs({
|
|
inputPath: 'in.mp4',
|
|
outputPath: 'out.jpg',
|
|
atSeconds: -3,
|
|
})
|
|
|
|
expect(args[args.indexOf('-ss') + 1]).toBe('0')
|
|
})
|
|
})
|
|
|
|
describe('CONTENT_PIECE_FRAME_SEEK_ATTEMPTS', () => {
|
|
it('avoids the black opening frame and retries once at zero', () => {
|
|
expect([...CONTENT_PIECE_FRAME_SEEK_ATTEMPTS]).toEqual([1, 0])
|
|
})
|
|
})
|
|
|
|
describe('CONTENT_PIECE_FRAME_WAIT_MS', () => {
|
|
it('is shorter than the acervo poster wait (here the heal may download the file)', () => {
|
|
expect(CONTENT_PIECE_FRAME_WAIT_MS).toBeLessThan(20_000)
|
|
})
|
|
})
|