mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
602 lines
23 KiB
TypeScript
602 lines
23 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
buildContentPieceCatalogHref,
|
|
cardCatalogItems,
|
|
CONTENT_PIECE_ASPECT_CLASS,
|
|
contentCatalogItems,
|
|
contentPieceAspectClass,
|
|
contentPieceBoardBucket,
|
|
contentPieceCatalogActiveFilters,
|
|
contentPieceCatalogFacets,
|
|
contentPieceDownloadFilename,
|
|
contentPieceFramePath,
|
|
contentPieceMediaKind,
|
|
contentPieceMediaPath,
|
|
contentPiecePublicPath,
|
|
contentPiecePublicPeople,
|
|
contentPieceTextExcerpt,
|
|
contentPieceThemeMatch,
|
|
contentPieceThemeTerms,
|
|
filterContentPieceCatalogItems,
|
|
interleaveCatalogItems,
|
|
isCardCatalogItem,
|
|
parseContentPieceCatalogParams,
|
|
toContentPiecePublicItem,
|
|
type CardCatalogItem,
|
|
type ContentCatalogItem,
|
|
type ContentPiecePublicItem,
|
|
type ContentPiecePublicSource,
|
|
} from '@/lib/contentPieceCatalog'
|
|
|
|
const source = (patch: Partial<ContentPiecePublicSource> = {}): ContentPiecePublicSource => ({
|
|
id: 1,
|
|
slug: 'fim-da-escala-6x1',
|
|
title: 'Fim da escala 6x1 é saúde',
|
|
type: 'video',
|
|
status: 'publicado',
|
|
origin: 'arquivo',
|
|
topics: ['economia-trabalho'],
|
|
cityLabel: 'Salvador',
|
|
region: 'Metropolitano de Salvador',
|
|
description: 'Solla explica a jornada.',
|
|
transcript: 'A redução da jornada é saúde.',
|
|
durationSeconds: 134,
|
|
searchText: 'fim da escala 6x1 e saude salvador',
|
|
media: { id: 7, filename: 'Fim da Escala.MP4' },
|
|
...patch,
|
|
})
|
|
|
|
const item = (patch: Partial<ContentPiecePublicSource> = {}): ContentPiecePublicItem => {
|
|
const resolved = toContentPiecePublicItem(source(patch))
|
|
if (!resolved) throw new Error('fixture should be public')
|
|
return resolved
|
|
}
|
|
|
|
const pieceRows = (rows: readonly ContentCatalogItem[]): ContentPiecePublicItem[] =>
|
|
rows.filter((row): row is ContentPiecePublicItem => !isCardCatalogItem(row))
|
|
|
|
const cardRows = (rows: readonly ContentCatalogItem[]): CardCatalogItem[] =>
|
|
rows.filter((row): row is CardCatalogItem => isCardCatalogItem(row))
|
|
|
|
describe('content piece catalog params', () => {
|
|
it('parses one valid value per facet and drops unknown values', () => {
|
|
expect(
|
|
parseContentPieceCatalogParams({
|
|
tipo: ['video', 'foto'],
|
|
cidade: 'salvador',
|
|
regiao: 'metropolitano-de-salvador',
|
|
tema: 'economia-trabalho',
|
|
instituicao: 'camara-dos-deputados',
|
|
lideranca: 'maria-silva',
|
|
q: ' escala ',
|
|
mode: 'tema',
|
|
}),
|
|
).toEqual({
|
|
tipo: 'video',
|
|
cidade: 'salvador',
|
|
regiao: 'metropolitano-de-salvador',
|
|
tema: 'economia-trabalho',
|
|
instituicao: 'camara-dos-deputados',
|
|
lideranca: 'maria-silva',
|
|
q: 'escala',
|
|
mode: 'tema',
|
|
})
|
|
|
|
expect(parseContentPieceCatalogParams({ tipo: 'inexistente', tema: 'nada' })).toEqual({
|
|
tipo: null,
|
|
cidade: null,
|
|
regiao: null,
|
|
tema: null,
|
|
instituicao: null,
|
|
lideranca: null,
|
|
q: '',
|
|
mode: null,
|
|
})
|
|
expect(parseContentPieceCatalogParams({ cidade: '../etc/passwd' }).cidade).toBeNull()
|
|
// S37 — the people facet is a display-name slug; spaces/accents never pass.
|
|
expect(parseContentPieceCatalogParams({ lideranca: 'Maria Silva' }).lideranca).toBeNull()
|
|
expect(parseContentPieceCatalogParams({}).q).toBe('')
|
|
})
|
|
|
|
it('parses the theme mode only with a query and drops unknown modes', () => {
|
|
expect(parseContentPieceCatalogParams({ q: 'escala', mode: 'tema' }).mode).toBe('tema')
|
|
// `exato` is the default and never serialized; unknown values mean exact too.
|
|
expect(parseContentPieceCatalogParams({ q: 'escala', mode: 'exato' }).mode).toBeNull()
|
|
expect(parseContentPieceCatalogParams({ q: 'escala', mode: 'semantico' }).mode).toBeNull()
|
|
expect(parseContentPieceCatalogParams({ q: ' ', mode: 'tema' }).mode).toBeNull()
|
|
expect(parseContentPieceCatalogParams({ mode: 'tema' }).mode).toBeNull()
|
|
})
|
|
|
|
it('builds the canonical href with a fixed order and no empty params', () => {
|
|
expect(buildContentPieceCatalogHref({})).toBe('/conteudos')
|
|
expect(buildContentPieceCatalogHref({ q: ' ' })).toBe('/conteudos')
|
|
expect(
|
|
buildContentPieceCatalogHref({
|
|
q: 'escala',
|
|
tema: 'economia-trabalho',
|
|
tipo: 'video',
|
|
cidade: 'salvador',
|
|
}),
|
|
).toBe('/conteudos?tipo=video&cidade=salvador&tema=economia-trabalho&q=escala')
|
|
expect(buildContentPieceCatalogHref({ tipo: 'video', tema: null, q: '' })).toBe(
|
|
'/conteudos?tipo=video',
|
|
)
|
|
// S37 — the people facet is appended after Instituição, before the query.
|
|
expect(
|
|
buildContentPieceCatalogHref({
|
|
instituicao: 'camara-dos-deputados',
|
|
lideranca: 'maria-silva',
|
|
q: 'escala',
|
|
}),
|
|
).toBe('/conteudos?instituicao=camara-dos-deputados&lideranca=maria-silva&q=escala')
|
|
expect(buildContentPieceCatalogHref({ q: 'escala', mode: 'tema' })).toBe(
|
|
'/conteudos?q=escala&mode=tema',
|
|
)
|
|
// A mode without a query has nothing to expand and is not serialized.
|
|
expect(buildContentPieceCatalogHref({ q: '', mode: 'tema' })).toBe('/conteudos')
|
|
})
|
|
|
|
it('lists the active filters with their removal hrefs', () => {
|
|
const items = [
|
|
item(),
|
|
item({ id: 2, slug: 'card', type: 'card', topics: ['saude'], cityLabel: null }),
|
|
]
|
|
const params = parseContentPieceCatalogParams({
|
|
tipo: 'video',
|
|
cidade: 'salvador',
|
|
q: 'escala',
|
|
})
|
|
const filters = contentPieceCatalogActiveFilters(params, contentPieceCatalogFacets(items))
|
|
|
|
expect(filters.map((filter) => `${filter.label}: ${filter.value}`)).toEqual([
|
|
'Tipo: Vídeo',
|
|
'Cidade: Salvador',
|
|
'Busca: escala',
|
|
])
|
|
expect(filters[0]?.removeHref).toBe('/conteudos?cidade=salvador&q=escala')
|
|
expect(filters[2]?.removeHref).toBe('/conteudos?tipo=video&cidade=salvador')
|
|
})
|
|
})
|
|
|
|
describe('content piece catalog facets', () => {
|
|
it('derives options from published items only, in enum/alphabetical order', () => {
|
|
const facets = contentPieceCatalogFacets([
|
|
item({ leaderNames: ['Maria Silva'] }),
|
|
item({
|
|
id: 2,
|
|
slug: 'card-saude',
|
|
type: 'card',
|
|
topics: ['saude'],
|
|
cityLabel: 'Feira de Santana',
|
|
region: 'Portal do Sertão',
|
|
institution: 'Câmara dos Deputados',
|
|
publicFigures: ['Dep. Estadual Exemplo'],
|
|
}),
|
|
])
|
|
|
|
expect(facets.tipo).toEqual([
|
|
{ value: 'video', label: 'Vídeo' },
|
|
{ value: 'card', label: 'Card' },
|
|
])
|
|
expect(facets.tema.map((option) => option.label)).toEqual(['Saúde', 'Economia e Trabalho'])
|
|
expect(facets.cidade.map((option) => option.label)).toEqual(['Feira de Santana', 'Salvador'])
|
|
expect(facets.regiao.map((option) => option.label)).toEqual([
|
|
'Metropolitano de Salvador',
|
|
'Portal do Sertão',
|
|
])
|
|
expect(facets.instituicao).toEqual([
|
|
{ value: 'camara-dos-deputados', label: 'Câmara dos Deputados' },
|
|
])
|
|
// S37 — one facet unions the leader snapshot and the curated figures,
|
|
// alphabetical by the display name.
|
|
expect(facets.lideranca).toEqual([
|
|
{ value: 'dep-estadual-exemplo', label: 'Dep. Estadual Exemplo' },
|
|
{ value: 'maria-silva', label: 'Maria Silva' },
|
|
])
|
|
})
|
|
|
|
it('collapses homonyms and spelling variants into one people option (S37)', () => {
|
|
const facets = contentPieceCatalogFacets([
|
|
item({ leaderNames: ['Dra. Elaine'] }),
|
|
item({ id: 2, slug: 'outra', publicFigures: ['Dra Elaine'] }),
|
|
])
|
|
|
|
expect(facets.lideranca).toEqual([{ value: 'dra-elaine', label: 'Dra Elaine' }])
|
|
// No piece carries people: the facet renders nothing.
|
|
expect(contentPieceCatalogFacets([item()]).lideranca).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('content piece catalog filtering', () => {
|
|
const items = [
|
|
item(),
|
|
item({
|
|
id: 2,
|
|
slug: 'card-feira',
|
|
title: 'Card do giro',
|
|
type: 'card',
|
|
topics: ['saude'],
|
|
cityLabel: 'Feira de Santana',
|
|
searchText: 'card do giro saude feira de santana',
|
|
}),
|
|
item({
|
|
id: 3,
|
|
slug: 'link-yt',
|
|
origin: 'youtube',
|
|
media: null,
|
|
sourceUrl: 'https://youtu.be/X',
|
|
searchText: 'video no youtube',
|
|
}),
|
|
]
|
|
|
|
const idsFor = (raw: Record<string, string>) =>
|
|
pieceRows(filterContentPieceCatalogItems(items, parseContentPieceCatalogParams(raw))).map(
|
|
(row) => row.id,
|
|
)
|
|
|
|
it('combines facets and the accent-insensitive term', () => {
|
|
expect(idsFor({})).toEqual([1, 2, 3])
|
|
expect(idsFor({ tipo: 'video' })).toEqual([1, 3])
|
|
expect(idsFor({ tema: 'saude' })).toEqual([2])
|
|
expect(idsFor({ cidade: 'feira-de-santana' })).toEqual([2])
|
|
expect(idsFor({ q: 'ESCALA' })).toEqual([1])
|
|
expect(idsFor({ q: 'saúde' })).toEqual([1, 2])
|
|
expect(idsFor({ tipo: 'video', cidade: 'feira-de-santana' })).toEqual([])
|
|
expect(idsFor({ tipo: 'video', q: 'escala' })).toEqual([1])
|
|
})
|
|
|
|
it('filters by the people facet union and combines with the other facets (S37)', () => {
|
|
const people = [
|
|
item({ leaderNames: ['Maria Silva'] }),
|
|
item({
|
|
id: 2,
|
|
slug: 'card-feira',
|
|
type: 'card',
|
|
cityLabel: 'Feira de Santana',
|
|
publicFigures: ['Dep. Estadual Exemplo'],
|
|
}),
|
|
]
|
|
const ids = (raw: Record<string, string>) =>
|
|
pieceRows(filterContentPieceCatalogItems(people, parseContentPieceCatalogParams(raw))).map(
|
|
(row) => row.id,
|
|
)
|
|
|
|
expect(ids({ lideranca: 'maria-silva' })).toEqual([1])
|
|
expect(ids({ lideranca: 'dep-estadual-exemplo' })).toEqual([2])
|
|
expect(ids({ lideranca: 'maria-silva', tipo: 'card' })).toEqual([])
|
|
expect(ids({ lideranca: 'ninguem' })).toEqual([])
|
|
})
|
|
|
|
it('ORs the literal query with the expanded theme terms, facets still AND', () => {
|
|
const idsForTerms = (raw: Record<string, string>, terms: string[]) =>
|
|
pieceRows(
|
|
filterContentPieceCatalogItems(items, parseContentPieceCatalogParams(raw), terms),
|
|
).map((row) => row.id)
|
|
|
|
// No terms is byte-identical to the literal search.
|
|
expect(idsForTerms({ q: 'escala' }, [])).toEqual([1])
|
|
// The expanded term surfaces a piece the query alone would not.
|
|
expect(idsForTerms({ q: 'giro' }, ['card'])).toEqual([2])
|
|
expect(idsForTerms({ q: 'zzz' }, ['card', 'giro'])).toEqual([2])
|
|
// Facets keep narrowing the OR result.
|
|
expect(idsForTerms({ q: 'zzz', tipo: 'video' }, ['card'])).toEqual([])
|
|
expect(idsForTerms({ q: 'zzz' }, ['ESCALA'])).toEqual([1])
|
|
})
|
|
|
|
it('mixes the card items into the same facet and order (S38)', () => {
|
|
const rows = filterContentPieceCatalogItems(
|
|
[...items, ...cardCatalogItems()],
|
|
parseContentPieceCatalogParams({ tipo: 'card' }),
|
|
)
|
|
|
|
expect(rows.map((row) => (isCardCatalogItem(row) ? row.modelId : row.slug))).toEqual([
|
|
'card-feira',
|
|
...cardCatalogItems().map((card) => card.modelId),
|
|
])
|
|
expect(contentPieceCatalogFacets([...items, ...cardCatalogItems()]).tipo).toEqual([
|
|
{ value: 'video', label: 'Vídeo' },
|
|
{ value: 'card', label: 'Card' },
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('card models as catalogue items (S38)', () => {
|
|
const cards = cardCatalogItems()
|
|
const modelIdsFor = (raw: Record<string, string>) =>
|
|
cardRows(filterContentPieceCatalogItems(cards, parseContentPieceCatalogParams(raw))).map(
|
|
(card) => card.modelId,
|
|
)
|
|
|
|
it('builds one item per model with the studio deep link and the model copy', () => {
|
|
expect(cards.map((card) => card.modelId)).toEqual([
|
|
'eu-sou-solla',
|
|
'perfil-quadrado',
|
|
'perfil-retangular',
|
|
'time-de-voce',
|
|
'time-do-estadual',
|
|
'minha-colinha',
|
|
])
|
|
expect(cards.every((card) => card.href === `/cards?model=${card.modelId}`)).toBe(true)
|
|
expect(cards.find((card) => card.modelId === 'minha-colinha')?.badge).toBe('NOVO')
|
|
expect(cards.every((card) => card.badge === null || card.modelId === 'minha-colinha')).toBe(
|
|
true,
|
|
)
|
|
})
|
|
|
|
it('carries the real art, or the neutral placeholder of the photo models', () => {
|
|
const artOf = (modelId: string) => cards.find((card) => card.modelId === modelId)?.art
|
|
|
|
expect(artOf('time-de-voce')).toEqual({ kind: 'image', src: '/cards/team-card-example.jpg' })
|
|
expect(artOf('eu-sou-solla')).toEqual({ kind: 'image', src: '/cards/name-card-base.jpg' })
|
|
expect(artOf('perfil-quadrado')).toEqual({ kind: 'placeholder', shape: 'square' })
|
|
expect(artOf('perfil-retangular')).toEqual({ kind: 'placeholder', shape: 'portrait' })
|
|
})
|
|
|
|
it('finds the models by nickname, by name and by description', () => {
|
|
expect(modelIdsFor({ q: 'santinho' })).toEqual(['time-de-voce', 'minha-colinha'])
|
|
expect(modelIdsFor({ q: 'foto de perfil' })).toEqual(['perfil-quadrado', 'perfil-retangular'])
|
|
expect(modelIdsFor({ q: 'colinha' })).toEqual(['minha-colinha'])
|
|
expect(modelIdsFor({ q: 'estadual' })).toEqual(['time-do-estadual'])
|
|
expect(modelIdsFor({ q: 'Moldura quadrada' })).toEqual(['perfil-quadrado'])
|
|
expect(modelIdsFor({ q: 'cola de votação' })).toEqual(['minha-colinha'])
|
|
})
|
|
|
|
it('matches the Tipo facet only as card and never a geographic/theme facet', () => {
|
|
expect(modelIdsFor({ tipo: 'card' })).toHaveLength(6)
|
|
expect(modelIdsFor({ tipo: 'video' })).toEqual([])
|
|
expect(modelIdsFor({ cidade: 'salvador' })).toEqual([])
|
|
expect(modelIdsFor({ regiao: 'metropolitano-de-salvador' })).toEqual([])
|
|
expect(modelIdsFor({ instituicao: 'camara-dos-deputados' })).toEqual([])
|
|
expect(modelIdsFor({ tema: 'saude' })).toEqual([])
|
|
})
|
|
|
|
it('feeds only the Tipo facet, never the geographic ones', () => {
|
|
const facets = contentPieceCatalogFacets(cards)
|
|
|
|
expect(facets.tipo).toEqual([{ value: 'card', label: 'Card' }])
|
|
expect(facets.cidade).toEqual([])
|
|
expect(facets.regiao).toEqual([])
|
|
expect(facets.instituicao).toEqual([])
|
|
expect(facets.tema).toEqual([])
|
|
})
|
|
|
|
it('appends the cards only to a non-empty Central', () => {
|
|
expect(contentCatalogItems([], 0, cards)).toEqual([])
|
|
expect(contentCatalogItems([item()], 1, cards)).toHaveLength(7)
|
|
expect(contentCatalogItems([item()], 1, cards).map((row) => isCardCatalogItem(row))).toEqual([
|
|
false,
|
|
true,
|
|
true,
|
|
true,
|
|
true,
|
|
true,
|
|
true,
|
|
])
|
|
})
|
|
|
|
it('alternates pieces and models, never a block of cards', () => {
|
|
const label = (row: ContentCatalogItem) => (isCardCatalogItem(row) ? row.modelId : row.id)
|
|
const first = item({ id: 1 })
|
|
const second = item({ id: 2, slug: 'outra-peca' })
|
|
|
|
expect(interleaveCatalogItems([first, second], cards.slice(0, 2)).map(label)).toEqual([
|
|
1,
|
|
'eu-sou-solla',
|
|
2,
|
|
'perfil-quadrado',
|
|
])
|
|
// The remainder of the longer side keeps its own committed order.
|
|
expect(interleaveCatalogItems([first], cards.slice(0, 2)).map(label)).toEqual([
|
|
1,
|
|
'eu-sou-solla',
|
|
'perfil-quadrado',
|
|
])
|
|
expect(interleaveCatalogItems([first, second], []).map(label)).toEqual([1, 2])
|
|
})
|
|
})
|
|
|
|
describe('content piece theme terms and provenance (S28)', () => {
|
|
it('drops empty terms, duplicates and the literal query itself', () => {
|
|
expect(contentPieceThemeTerms('escala', ['', ' ', 'Escala', 'fim da escala 6x1'])).toEqual([
|
|
'fim da escala 6x1',
|
|
])
|
|
expect(contentPieceThemeTerms('escala', ['FIM DA ESCALA', 'fim da escala'])).toEqual([
|
|
'FIM DA ESCALA',
|
|
])
|
|
})
|
|
|
|
it('quotes the transcript window that carries the term', () => {
|
|
const match = contentPieceThemeMatch(source({ searchText: 'a reducao da jornada e saude' }), [
|
|
'redução da jornada',
|
|
])
|
|
|
|
expect(match?.term).toBe('redução da jornada')
|
|
expect(match?.evidence?.quoted).toBe(true)
|
|
expect(match?.evidence?.source).toBe('transcript')
|
|
expect(match?.evidence?.parts.some((part) => part.highlighted)).toBe(true)
|
|
})
|
|
|
|
it('falls back to the description window when the transcript does not carry it', () => {
|
|
const match = contentPieceThemeMatch(
|
|
source({
|
|
transcript: null,
|
|
description: 'Solla explica a jornada de trabalho.',
|
|
searchText: 'jornada de trabalho',
|
|
}),
|
|
['jornada de trabalho'],
|
|
)
|
|
|
|
expect(match?.evidence?.source).toBe('description')
|
|
expect(match?.evidence?.quoted).toBe(true)
|
|
})
|
|
|
|
it('shows a real passage without highlight when the match came from elsewhere', () => {
|
|
const match = contentPieceThemeMatch(
|
|
source({ searchText: 'camara dos deputados saude', description: 'Solla explica a jornada.' }),
|
|
['camara dos deputados'],
|
|
)
|
|
|
|
expect(match?.term).toBe('camara dos deputados')
|
|
expect(match?.evidence?.quoted).toBe(false)
|
|
expect(match?.evidence?.source).toBe('description')
|
|
expect(match?.evidence?.parts).toEqual([
|
|
{ text: 'Solla explica a jornada.', highlighted: false },
|
|
])
|
|
})
|
|
|
|
it('never claims a term the haystack does not carry', () => {
|
|
expect(contentPieceThemeMatch(source(), ['escola sem partido'])).toBeNull()
|
|
expect(contentPieceThemeMatch(source(), [])).toBeNull()
|
|
})
|
|
|
|
it('annotates the public item only when the theme mode passes terms', () => {
|
|
const themed = source({ searchText: 'a reducao da jornada e saude' })
|
|
const item = toContentPiecePublicItem(themed, { themeTerms: ['redução da jornada'] })
|
|
expect(item?.themeMatch?.term).toBe('redução da jornada')
|
|
|
|
const literal = toContentPiecePublicItem(themed)
|
|
expect(literal?.themeMatch).toBeNull()
|
|
// The transcript itself never reaches the view model.
|
|
expect(literal && 'transcript' in literal).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('content piece public view model', () => {
|
|
it('resolves the file piece with paths, duration and metadata line', () => {
|
|
const view = item()
|
|
|
|
expect(view.isLink).toBe(false)
|
|
expect(view.typeLabel).toBe('Vídeo')
|
|
expect(view.durationLabel).toBe('02:14')
|
|
expect(view.metaLabel).toBe('Economia e Trabalho · Salvador')
|
|
expect(view.publicPath).toBe('/conteudos/fim-da-escala-6x1')
|
|
expect(view.file?.path).toBe('/conteudos/fim-da-escala-6x1/midia')
|
|
expect(view.file?.mimeType).toBeNull()
|
|
expect(view.file?.downloadFilename).toBe('jorge-solla-1313-fim-da-escala-6x1.mp4')
|
|
})
|
|
|
|
it('resolves a link piece with the platform URL and no media', () => {
|
|
const view = item({
|
|
origin: 'instagram',
|
|
media: null,
|
|
sourceUrl: 'https://www.instagram.com/reel/ABC/',
|
|
transcript: null,
|
|
})
|
|
|
|
expect(view.isLink).toBe(true)
|
|
expect(view.originLabel).toBe('Instagram')
|
|
expect(view.file).toBeNull()
|
|
})
|
|
|
|
it('declares a frame only for a video with an archived file (C226)', () => {
|
|
expect(item().framePath).toBe('/conteudos/fim-da-escala-6x1/frame')
|
|
// Same decision as the card's own media kind, so a piece can never be asked
|
|
// for a frame it cannot produce.
|
|
expect(item({ media: { id: 7, mimeType: 'video/mp4' }, type: 'texto' }).framePath).toBe(
|
|
'/conteudos/fim-da-escala-6x1/frame',
|
|
)
|
|
// Everything else keeps the neutral slot.
|
|
expect(item({ media: { id: 7, mimeType: 'audio/mpeg' }, type: 'audio' }).framePath).toBeNull()
|
|
expect(item({ media: { id: 7, mimeType: 'image/png' }, type: 'foto' }).framePath).toBeNull()
|
|
expect(item({ media: { id: 7, mimeType: 'text/plain' }, type: 'texto' }).framePath).toBeNull()
|
|
expect(
|
|
item({ media: { id: 7, mimeType: 'application/pdf' }, type: 'card' }).framePath,
|
|
).toBeNull()
|
|
// Unknown MIME with a non-video editorial type: the type decides, as everywhere.
|
|
expect(item({ media: { id: 7 }, type: 'foto' }).framePath).toBeNull()
|
|
// A link piece never has an archived file, so it never has a frame.
|
|
expect(
|
|
item({ media: null, sourceUrl: 'https://www.instagram.com/reel/ABC/', type: 'video' })
|
|
.framePath,
|
|
).toBeNull()
|
|
})
|
|
|
|
it('resolves the media kind from the stored MIME with a type fallback', () => {
|
|
expect(contentPieceMediaKind(item({ media: { id: 7, mimeType: 'video/mp4' } }))).toBe('video')
|
|
expect(contentPieceMediaKind(item({ media: { id: 7, mimeType: 'audio/mpeg' } }))).toBe('audio')
|
|
expect(contentPieceMediaKind(item({ media: { id: 7, mimeType: 'image/webp' } }))).toBe('image')
|
|
expect(contentPieceMediaKind(item({ media: { id: 7, mimeType: 'text/plain' } }))).toBe('text')
|
|
expect(contentPieceMediaKind(item({ media: { id: 7, mimeType: 'application/pdf' } }))).toBe(
|
|
'other',
|
|
)
|
|
// No MIME stored: the editorial type decides.
|
|
expect(contentPieceMediaKind(item({ media: { id: 7 } }))).toBe('video')
|
|
expect(contentPieceMediaKind(item({ type: 'card', media: { id: 7 } }))).toBe('image')
|
|
expect(contentPieceMediaKind(item({ media: null, sourceUrl: 'https://youtu.be/X' }))).toBeNull()
|
|
})
|
|
|
|
it('maps the frame of the type, leaving the wide kinds without a class (S45)', () => {
|
|
expect(contentPieceAspectClass('video')).toBe('aspect-[9/16]')
|
|
expect(contentPieceAspectClass('image')).toBe('aspect-[4/5]')
|
|
// The S38 model art is not a media kind: its host reads the table directly.
|
|
expect(CONTENT_PIECE_ASPECT_CLASS.model).toBe('aspect-[3/4]')
|
|
// Audio, link, text and other have no framing to preserve.
|
|
for (const kind of ['audio', 'text', 'other', null] as const) {
|
|
expect(contentPieceAspectClass(kind)).toBeUndefined()
|
|
}
|
|
})
|
|
|
|
it('buckets the board by type: vertical, wide or compact (S45)', () => {
|
|
expect(contentPieceBoardBucket('video')).toBe('vertical')
|
|
expect(contentPieceBoardBucket('audio')).toBe('wide')
|
|
expect(contentPieceBoardBucket(null)).toBe('wide')
|
|
for (const kind of ['image', 'text', 'other'] as const) {
|
|
expect(contentPieceBoardBucket(kind)).toBe('compact')
|
|
}
|
|
})
|
|
|
|
it('leads the metadata line with who appears in the piece (S37)', () => {
|
|
expect(item({ leaderNames: ['Maria Silva'] }).metaLabel).toBe(
|
|
'Maria Silva · Economia e Trabalho · Salvador',
|
|
)
|
|
// A figure takes the slot when there is no leader name.
|
|
expect(item({ publicFigures: ['Dep. Estadual Exemplo'] }).metaLabel).toBe(
|
|
'Dep. Estadual Exemplo · Economia e Trabalho · Salvador',
|
|
)
|
|
// Nobody marked: the previous line stays byte-identical.
|
|
expect(item().metaLabel).toBe('Economia e Trabalho · Salvador')
|
|
})
|
|
|
|
it('unions leaders and figures as people options, deduped by display name (S37)', () => {
|
|
expect(
|
|
contentPiecePublicPeople({
|
|
leaderNames: ['Maria Silva'],
|
|
publicFigures: ['Maria Silva', 'Dep. Estadual Exemplo'],
|
|
}),
|
|
).toEqual([
|
|
{ slug: 'maria-silva', name: 'Maria Silva' },
|
|
{ slug: 'dep-estadual-exemplo', name: 'Dep. Estadual Exemplo' },
|
|
])
|
|
expect(contentPiecePublicPeople({ leaderNames: [], publicFigures: [' '] })).toEqual([])
|
|
})
|
|
|
|
it('fails closed without a slug or without file and link', () => {
|
|
expect(toContentPiecePublicItem(source({ slug: null }))).toBeNull()
|
|
expect(toContentPiecePublicItem(source({ status: 'rascunho' }))).toBeNull()
|
|
expect(toContentPiecePublicItem(source({ media: null }))).toBeNull()
|
|
expect(toContentPiecePublicItem(source({ media: null, sourceUrl: null }))).toBeNull()
|
|
expect(
|
|
toContentPiecePublicItem(source({ media: null, sourceUrl: 'https://youtu.be/X' }))?.isLink,
|
|
).toBe(true)
|
|
})
|
|
|
|
it('keeps the text excerpt short and the download name legible', () => {
|
|
expect(contentPieceTextExcerpt('\n “Saúde é direito.” \nSegunda linha')).toBe(
|
|
'“Saúde é direito.”',
|
|
)
|
|
expect(contentPieceTextExcerpt('')).toBeNull()
|
|
expect(contentPieceTextExcerpt('a'.repeat(200))?.endsWith('…')).toBe(true)
|
|
expect(contentPieceDownloadFilename('card-feira', 'Card Final.PNG')).toBe(
|
|
'jorge-solla-1313-card-feira.png',
|
|
)
|
|
expect(contentPieceDownloadFilename('sem-extensao', null)).toBe(
|
|
'jorge-solla-1313-sem-extensao.bin',
|
|
)
|
|
expect(contentPiecePublicPath('x')).toBe('/conteudos/x')
|
|
expect(contentPieceMediaPath('x')).toBe('/conteudos/x/midia')
|
|
expect(contentPieceFramePath('x')).toBe('/conteudos/x/frame')
|
|
})
|
|
})
|