mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:05:19 +00:00
554 lines
20 KiB
TypeScript
554 lines
20 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
|
||
import { COLINHA_ESTADUAL_LABEL, COLINHA_ROW_LAYOUT } from '@/lib/cardColinha'
|
||
import {
|
||
NAME_CARD_SLOT,
|
||
TEAM_CARD_LABEL,
|
||
TEAM_CARD_NAME_BANNER,
|
||
TEAM_CARD_NAME_SLOT,
|
||
getCardModel,
|
||
type CardRect,
|
||
} from '@/lib/cardModels'
|
||
import { fitCardName } from '@/lib/cardNameFit'
|
||
import {
|
||
centerCardPhotoTransform,
|
||
frameCardPhotoOnBbox,
|
||
type CardPhotoSize,
|
||
} from '@/lib/cardPhotoTransform'
|
||
import {
|
||
createCardMeasure,
|
||
drawCardName,
|
||
renderColinhaCard,
|
||
renderNameCard,
|
||
renderPhotoCard,
|
||
renderTeamCard,
|
||
resolveCardBannerWidth,
|
||
type CardDrawContext,
|
||
} from '@/lib/cardRender'
|
||
import { getStateDeputyCard, type StateDeputyCatalogEntry } from '@/lib/stateDeputyCatalog'
|
||
|
||
type DrawCall = { image: unknown; dx: number; dy: number; dw: number; dh: number }
|
||
type TextCall = { text: string; x: number; y: number }
|
||
type TextState = { align: CanvasTextAlign; fill: string; font: string; letterSpacing: string }
|
||
type RectCall = { x: number; y: number; width: number; height: number }
|
||
type OpCall = { op: string; args: number[] }
|
||
|
||
const fontSizeFrom = (font: string): number => {
|
||
const match = /(\d+(?:\.\d+)?)px/.exec(font)
|
||
return match ? Number(match[1]) : 0
|
||
}
|
||
|
||
const createFakeContext = () => {
|
||
const drawCalls: DrawCall[] = []
|
||
const textCalls: TextCall[] = []
|
||
const textStates: TextState[] = []
|
||
const rectCalls: RectCall[] = []
|
||
const ops: OpCall[] = []
|
||
|
||
const ctx: CardDrawContext = {
|
||
font: '',
|
||
letterSpacing: '',
|
||
globalAlpha: 1,
|
||
fillStyle: '',
|
||
textAlign: 'start',
|
||
textBaseline: 'alphabetic',
|
||
imageSmoothingEnabled: false,
|
||
imageSmoothingQuality: 'low',
|
||
drawImage: (image, dx, dy, dw, dh) => {
|
||
drawCalls.push({ image, dx, dy, dw, dh })
|
||
},
|
||
fillRect: (x, y, width, height) => {
|
||
rectCalls.push({ x, y, width, height })
|
||
},
|
||
fillText: (text, x, y) => {
|
||
textCalls.push({ text, x, y })
|
||
textStates.push({
|
||
align: ctx.textAlign,
|
||
fill: typeof ctx.fillStyle === 'string' ? ctx.fillStyle : '',
|
||
font: ctx.font,
|
||
letterSpacing: ctx.letterSpacing,
|
||
})
|
||
},
|
||
measureText: (text) => {
|
||
const size = fontSizeFrom(ctx.font)
|
||
return {
|
||
width: text.length * size * 0.84,
|
||
actualBoundingBoxAscent: size * 0.72,
|
||
actualBoundingBoxDescent: size * 0.2,
|
||
}
|
||
},
|
||
save: () => {
|
||
ops.push({ op: 'save', args: [] })
|
||
},
|
||
restore: () => {
|
||
ops.push({ op: 'restore', args: [] })
|
||
},
|
||
translate: (x, y) => {
|
||
ops.push({ op: 'translate', args: [x, y] })
|
||
},
|
||
rotate: (angle) => {
|
||
ops.push({ op: 'rotate', args: [angle] })
|
||
},
|
||
beginPath: () => {
|
||
ops.push({ op: 'beginPath', args: [] })
|
||
},
|
||
arc: (x, y, radius, startAngle, endAngle) => {
|
||
ops.push({ op: 'arc', args: [x, y, radius, startAngle, endAngle] })
|
||
},
|
||
ellipse: (x, y, radiusX, radiusY, rotation, startAngle, endAngle) => {
|
||
ops.push({ op: 'ellipse', args: [x, y, radiusX, radiusY, rotation, startAngle, endAngle] })
|
||
},
|
||
rect: (x, y, width, height) => {
|
||
ops.push({ op: 'rect', args: [x, y, width, height] })
|
||
},
|
||
roundRect: (x, y, width, height, radii) => {
|
||
ops.push({
|
||
op: 'roundRect',
|
||
args: [x, y, width, height, typeof radii === 'number' ? radii : -1],
|
||
})
|
||
},
|
||
clip: () => {
|
||
ops.push({ op: 'clip', args: [] })
|
||
},
|
||
createLinearGradient: (x0, y0, x1, y1) => {
|
||
ops.push({ op: 'createLinearGradient', args: [x0, y0, x1, y1] })
|
||
return {
|
||
addColorStop: (offset: number) => {
|
||
ops.push({ op: 'addColorStop', args: [offset] })
|
||
},
|
||
}
|
||
},
|
||
fill: () => {
|
||
ops.push({ op: 'fill', args: [] })
|
||
},
|
||
}
|
||
|
||
return { ctx, drawCalls, textCalls, textStates, rectCalls, ops }
|
||
}
|
||
|
||
const nameModel = getCardModel('eu-sou-solla')!
|
||
const squareModel = getCardModel('perfil-quadrado')!
|
||
const teamModel = getCardModel('time-de-voce')!
|
||
|
||
describe('renderNameCard', () => {
|
||
it('draws the master base first and then the name on the left border', () => {
|
||
const { ctx, drawCalls, textCalls } = createFakeContext()
|
||
const image = { id: 'base' } as unknown as CanvasImageSource
|
||
const fit = renderNameCard(ctx, nameModel, {
|
||
image,
|
||
name: 'João',
|
||
fontFamily: 'Brexter',
|
||
measure: createCardMeasure(ctx, 'Brexter'),
|
||
})
|
||
|
||
expect(fit.ok).toBe(true)
|
||
expect(drawCalls).toEqual([{ image, dx: 0, dy: 0, dw: 1080, dh: 1440 }])
|
||
expect(textCalls).toHaveLength(1)
|
||
expect(textCalls[0]!.text).toBe('JOÃO')
|
||
expect(textCalls[0]!.x).toBe(NAME_CARD_SLOT.leftX)
|
||
expect(textCalls[0]!.y).toBeCloseTo(NAME_CARD_SLOT.capTop + NAME_CARD_SLOT.capHeight, 5)
|
||
expect(ctx.fillStyle).toBe(NAME_CARD_SLOT.fill)
|
||
expect(ctx.textAlign).toBe('left')
|
||
})
|
||
|
||
it('wraps long names into two lines with baselines inside the free band', () => {
|
||
const { ctx, textCalls } = createFakeContext()
|
||
const fit = renderNameCard(ctx, nameModel, {
|
||
image: { id: 'base' } as unknown as CanvasImageSource,
|
||
name: 'Jorge Solla da Bahia',
|
||
fontFamily: 'Brexter',
|
||
measure: createCardMeasure(ctx, 'Brexter'),
|
||
})
|
||
|
||
expect(fit.ok).toBe(true)
|
||
expect(textCalls.map((call) => call.text)).toEqual(['JORGE SOLLA', 'DA BAHIA'])
|
||
expect(textCalls[0]!.y).toBeLessThan(textCalls[1]!.y)
|
||
expect(textCalls[1]!.y).toBeLessThanOrEqual(NAME_CARD_SLOT.maxBlockBottom)
|
||
})
|
||
|
||
it('draws the base but no text when the name cannot fit (never cuts silently)', () => {
|
||
const { ctx, drawCalls, textCalls } = createFakeContext()
|
||
const fit = renderNameCard(ctx, nameModel, {
|
||
image: { id: 'base' } as unknown as CanvasImageSource,
|
||
name: 'Bartolomeucostajunior',
|
||
fontFamily: 'Brexter',
|
||
measure: createCardMeasure(ctx, 'Brexter'),
|
||
})
|
||
|
||
expect(fit).toEqual({ ok: false, reason: 'too-long' })
|
||
expect(drawCalls).toHaveLength(1)
|
||
expect(textCalls).toHaveLength(0)
|
||
})
|
||
})
|
||
|
||
describe('drawCardName', () => {
|
||
it('draws only the fitted name (no base) on the left border', () => {
|
||
const { ctx, drawCalls, textCalls } = createFakeContext()
|
||
const fit = fitCardName('João', createCardMeasure(ctx, 'Brexter'))
|
||
expect(fit.ok).toBe(true)
|
||
if (!fit.ok) return
|
||
|
||
drawCardName(ctx, { fit, fontFamily: 'Brexter' })
|
||
|
||
expect(drawCalls).toHaveLength(0)
|
||
expect(textCalls).toEqual([
|
||
{ text: 'JOÃO', x: NAME_CARD_SLOT.leftX, y: NAME_CARD_SLOT.capTop + fit.capHeight },
|
||
])
|
||
expect(ctx.textAlign).toBe('left')
|
||
expect(ctx.fillStyle).toBe(NAME_CARD_SLOT.fill)
|
||
})
|
||
|
||
it('left-aligns both wrapped lines inside the free band', () => {
|
||
const { ctx, textCalls } = createFakeContext()
|
||
const fit = fitCardName('Jorge Solla da Bahia', createCardMeasure(ctx, 'Brexter'))
|
||
expect(fit.ok).toBe(true)
|
||
if (!fit.ok) return
|
||
|
||
drawCardName(ctx, { fit, fontFamily: 'Brexter' })
|
||
|
||
expect(textCalls.map((call) => call.text)).toEqual(['JORGE SOLLA', 'DA BAHIA'])
|
||
expect(textCalls.every((call) => call.x === NAME_CARD_SLOT.leftX)).toBe(true)
|
||
expect(textCalls[0]!.y).toBeLessThan(textCalls[1]!.y)
|
||
expect(textCalls[1]!.y).toBeLessThanOrEqual(NAME_CARD_SLOT.maxBlockBottom)
|
||
})
|
||
})
|
||
|
||
describe('renderTeamCard', () => {
|
||
const window = teamModel.photoWindow!
|
||
const photoSize: CardPhotoSize = { width: 400, height: 600 }
|
||
const transform = frameCardPhotoOnBbox(photoSize, window, {
|
||
x: 80,
|
||
y: 40,
|
||
width: 240,
|
||
height: 420,
|
||
})!
|
||
|
||
const render = (name: string) => {
|
||
const fake = createFakeContext()
|
||
const base = { id: 'base' } as unknown as CanvasImageSource
|
||
const overlay = { id: 'overlay' } as unknown as CanvasImageSource
|
||
const photo = { id: 'photo' } as unknown as CanvasImageSource
|
||
const result = renderTeamCard(fake.ctx, teamModel, {
|
||
base,
|
||
overlay,
|
||
subject: { photo, photoSize, transform },
|
||
window,
|
||
name,
|
||
fontFamily: 'Brexter',
|
||
measure: createCardMeasure(fake.ctx, 'Brexter'),
|
||
})
|
||
|
||
return { ...fake, result, base, overlay, photo }
|
||
}
|
||
|
||
it('draws base → framed photo → overlay, then the label and the name banner', () => {
|
||
const { drawCalls, textCalls, rectCalls, ops, result, base, overlay, photo } = render('Maria')
|
||
|
||
expect(result.fit).toMatchObject({ ok: true, lines: ['MARIA'] })
|
||
expect(drawCalls.map((call) => call.image)).toEqual([base, photo, overlay])
|
||
expect(drawCalls[0]).toEqual({ image: base, dx: 0, dy: 0, dw: 1080, dh: 1440 })
|
||
expect(drawCalls[2]).toEqual({ image: overlay, dx: 0, dy: 0, dw: 1080, dh: 1440 })
|
||
|
||
// Both banners are drawn as rotated rectangles: two save/restore pairs.
|
||
expect(rectCalls).toHaveLength(2)
|
||
expect(rectCalls[0]).toMatchObject({
|
||
width: TEAM_CARD_LABEL.width,
|
||
height: TEAM_CARD_LABEL.height,
|
||
})
|
||
// S16 — `MARIA` at the ideal cap (130/0.72) with the 0.84em fake measures
|
||
// ≈758.33 of ink; the banner grows by the model's 39px padding: ≈797.33.
|
||
expect(rectCalls[1]).toMatchObject({
|
||
y: -TEAM_CARD_NAME_BANNER.height / 2,
|
||
height: TEAM_CARD_NAME_BANNER.height,
|
||
})
|
||
expect(rectCalls[1]!.width).toBeCloseTo(797.33, 1)
|
||
expect(rectCalls[1]!.x).toBeCloseTo(-797.33 / 2, 1)
|
||
expect(ops.filter((op) => op.op === 'save')).toHaveLength(2)
|
||
expect(ops.filter((op) => op.op === 'restore')).toHaveLength(2)
|
||
expect(ops.map((op) => op.op)).toEqual([
|
||
'save',
|
||
'translate',
|
||
'rotate',
|
||
'restore',
|
||
'save',
|
||
'translate',
|
||
'rotate',
|
||
'restore',
|
||
])
|
||
expect(ops[1]!.args).toEqual([TEAM_CARD_LABEL.centerX, TEAM_CARD_LABEL.centerY])
|
||
expect(ops[5]!.args).toEqual([TEAM_CARD_NAME_BANNER.centerX, TEAM_CARD_NAME_BANNER.centerY])
|
||
|
||
expect(textCalls.map((call) => call.text)).toEqual(['TIME DE', 'MARIA'])
|
||
expect(textCalls.every((call) => call.x === 0)).toBe(true)
|
||
})
|
||
|
||
it('draws the label but no name banner when the name cannot fit (never cuts silently)', () => {
|
||
const { textCalls, rectCalls, result } = render('Bartolomeucostajunior')
|
||
|
||
expect(result.fit).toEqual({ ok: false, reason: 'too-long' })
|
||
expect(rectCalls).toHaveLength(1)
|
||
expect(textCalls.map((call) => call.text)).toEqual(['TIME DE'])
|
||
})
|
||
|
||
it('returns the photo transform with the zoom clamped and the free position kept (S33)', () => {
|
||
const { result, photo } = render('Maria')
|
||
|
||
expect(result.transform).toEqual(transform)
|
||
|
||
// S33 — `time-de-voce` is `free`: the zoom still clamps to [1, 4] but the
|
||
// position comes back exactly as sent; a re-clamp here would snap the photo
|
||
// back on the next paint (drawing and return share the model's policy).
|
||
const fake = createFakeContext()
|
||
const outOfBounds = { zoom: 0.5, offsetX: 10_000, offsetY: -10_000 }
|
||
const clamped = renderTeamCard(fake.ctx, teamModel, {
|
||
base: { id: 'base' } as unknown as CanvasImageSource,
|
||
overlay: { id: 'overlay' } as unknown as CanvasImageSource,
|
||
subject: { photo, photoSize, transform: outOfBounds },
|
||
window,
|
||
name: 'Maria',
|
||
fontFamily: 'Brexter',
|
||
measure: createCardMeasure(fake.ctx, 'Brexter'),
|
||
})
|
||
|
||
expect(clamped.transform).toEqual({ zoom: 1, offsetX: 10_000, offsetY: -10_000 })
|
||
// The drawing follows the same free policy (second drawImage is the photo).
|
||
expect(fake.drawCalls[1]).toMatchObject({ dx: 10_000, dy: -10_000 })
|
||
})
|
||
|
||
it('leaves the window empty instead of drawing a subject when no photo is ready (S40)', () => {
|
||
const fake = createFakeContext()
|
||
const base = { id: 'base' } as unknown as CanvasImageSource
|
||
const overlay = { id: 'overlay' } as unknown as CanvasImageSource
|
||
const result = renderTeamCard(fake.ctx, teamModel, {
|
||
base,
|
||
overlay,
|
||
subject: null,
|
||
window,
|
||
name: 'Maria',
|
||
fontFamily: 'Brexter',
|
||
measure: createCardMeasure(fake.ctx, 'Brexter'),
|
||
})
|
||
|
||
expect(result.transform).toBeNull()
|
||
expect(result.fit).toMatchObject({ ok: true, lines: ['MARIA'] })
|
||
// Base + overlay only: nothing is drawn inside the photo window.
|
||
expect(fake.drawCalls.map((call) => call.image)).toEqual([base, overlay])
|
||
expect(
|
||
fake.ops.filter((op) => ['beginPath', 'arc', 'ellipse', 'rect', 'fill'].includes(op.op)),
|
||
).toEqual([])
|
||
})
|
||
})
|
||
|
||
describe('resolveCardBannerWidth (S16)', () => {
|
||
const slot = TEAM_CARD_NAME_SLOT
|
||
const maxInkWidth = slot.maxInkWidth
|
||
|
||
it('pins the design table pairs (ink = width − 39: reference 509 → ceiling 1000)', () => {
|
||
const designPairs: [ink: number, width: number][] = [
|
||
[470, 509],
|
||
[527, 566],
|
||
[807, 846],
|
||
[925, 964],
|
||
[951, 990],
|
||
[952, 991],
|
||
[961, 1000],
|
||
]
|
||
|
||
for (const [ink, width] of designPairs) {
|
||
expect(resolveCardBannerWidth(slot, ink)).toBe(width)
|
||
}
|
||
})
|
||
|
||
it('keeps the reference width for short names and clamps past the ceiling', () => {
|
||
expect(resolveCardBannerWidth(slot, 0)).toBe(slot.banner.width)
|
||
expect(resolveCardBannerWidth(slot, 200)).toBe(slot.banner.width)
|
||
expect(resolveCardBannerWidth(slot, 2000)).toBe(slot.banner.maxWidth)
|
||
})
|
||
|
||
it('never grows a fixed banner (the red TIME DE label)', () => {
|
||
const labelSlot = { banner: TEAM_CARD_LABEL, maxInkWidth }
|
||
expect(resolveCardBannerWidth(labelSlot, 0)).toBe(TEAM_CARD_LABEL.width)
|
||
expect(resolveCardBannerWidth(labelSlot, 5000)).toBe(TEAM_CARD_LABEL.width)
|
||
})
|
||
})
|
||
|
||
describe('drawCardName (centered team slot)', () => {
|
||
it('draws the blue banner with the centered name through the same banner helper', () => {
|
||
const { ctx, textCalls, rectCalls, ops } = createFakeContext()
|
||
const fit = fitCardName('Maria', createCardMeasure(ctx, 'Brexter'), TEAM_CARD_NAME_SLOT)
|
||
expect(fit.ok).toBe(true)
|
||
if (!fit.ok) return
|
||
|
||
drawCardName(ctx, { fit, fontFamily: 'Brexter', slot: TEAM_CARD_NAME_SLOT })
|
||
|
||
// S16 — dynamic width resolved from the fitted ink (same ≈797.33 as above).
|
||
expect(rectCalls).toHaveLength(1)
|
||
expect(rectCalls[0]).toMatchObject({
|
||
y: -TEAM_CARD_NAME_BANNER.height / 2,
|
||
height: TEAM_CARD_NAME_BANNER.height,
|
||
})
|
||
expect(rectCalls[0]!.width).toBeCloseTo(797.33, 1)
|
||
expect(rectCalls[0]!.x).toBeCloseTo(-797.33 / 2, 1)
|
||
expect(ops).toEqual([
|
||
{ op: 'save', args: [] },
|
||
{ op: 'translate', args: [TEAM_CARD_NAME_BANNER.centerX, TEAM_CARD_NAME_BANNER.centerY] },
|
||
{ op: 'rotate', args: [(TEAM_CARD_NAME_BANNER.rotationDeg * Math.PI) / 180] },
|
||
{ op: 'restore', args: [] },
|
||
])
|
||
expect(textCalls).toEqual([{ text: 'MARIA', x: 0, y: fit.capHeight / 2 }])
|
||
expect(ctx.fillStyle).toBe('#ffffff')
|
||
})
|
||
})
|
||
|
||
describe('renderPhotoCard', () => {
|
||
it('draws the photo full-bleed under the official overlay', () => {
|
||
const { ctx, drawCalls } = createFakeContext()
|
||
const photo = { id: 'photo' } as unknown as CanvasImageSource
|
||
const frame = { id: 'frame' } as unknown as CanvasImageSource
|
||
const size: CardPhotoSize = { width: 2000, height: 1000 }
|
||
const window: CardRect = squareModel.photoWindow!
|
||
const transform = centerCardPhotoTransform(size, window)
|
||
|
||
const clamped = renderPhotoCard(ctx, {
|
||
photo,
|
||
frame,
|
||
photoSize: size,
|
||
frameSize: { width: squareModel.width, height: squareModel.height },
|
||
window,
|
||
transform,
|
||
})
|
||
|
||
expect(clamped).toEqual(transform)
|
||
expect(drawCalls).toHaveLength(2)
|
||
expect(drawCalls[0]!.image).toBe(photo)
|
||
expect(drawCalls[1]).toEqual({ image: frame, dx: 0, dy: 0, dw: 1000, dh: 1000 })
|
||
expect(drawCalls[0]!.dx).toBeLessThanOrEqual(window.x)
|
||
expect(drawCalls[0]!.dy).toBeLessThanOrEqual(window.y)
|
||
expect(drawCalls[0]!.dx + drawCalls[0]!.dw).toBeGreaterThanOrEqual(window.x + window.width)
|
||
expect(drawCalls[0]!.dy + drawCalls[0]!.dh).toBeGreaterThanOrEqual(window.y + window.height)
|
||
})
|
||
|
||
it('still clamps the visitor adjustment on the bounded photo models (S13/S14)', () => {
|
||
const { ctx, drawCalls } = createFakeContext()
|
||
const photo = { id: 'photo' } as unknown as CanvasImageSource
|
||
const frame = { id: 'frame' } as unknown as CanvasImageSource
|
||
const size: CardPhotoSize = { width: 2000, height: 1000 }
|
||
const window: CardRect = squareModel.photoWindow!
|
||
|
||
const clamped = renderPhotoCard(ctx, {
|
||
photo,
|
||
frame,
|
||
photoSize: size,
|
||
frameSize: { width: squareModel.width, height: squareModel.height },
|
||
window,
|
||
transform: { zoom: 1, offsetX: 9999, offsetY: 9999 },
|
||
})
|
||
|
||
expect(clamped).toEqual({ zoom: 1, offsetX: 0, offsetY: 0 })
|
||
expect(drawCalls[0]).toEqual({ image: photo, dx: 0, dy: 0, dw: 1480, dh: 740 })
|
||
})
|
||
})
|
||
|
||
describe('renderColinhaCard (S34)', () => {
|
||
const colinhaModel = getCardModel('minha-colinha')!
|
||
const art = { id: 'art' } as unknown as CanvasImageSource
|
||
const deputy = getStateDeputyCard('julio')!
|
||
|
||
const render = (chosen: StateDeputyCatalogEntry | null) => {
|
||
const fake = createFakeContext()
|
||
renderColinhaCard(fake.ctx, colinhaModel, {
|
||
image: art,
|
||
deputy: chosen,
|
||
fontFamily: 'Brexter',
|
||
measure: createCardMeasure(fake.ctx, 'Brexter', 900),
|
||
})
|
||
|
||
return fake
|
||
}
|
||
|
||
// The fake face is conservative (0.84em per char): JULIO PINHEIRO shrinks
|
||
// from 36.72 to floor(36.72 × 195.83 / 431.83) = 16 to fit the mask band.
|
||
const nameFontSize = 16
|
||
const baseline = COLINHA_ROW_LAYOUT.copy.top + nameFontSize * 0.72
|
||
const nameWidth = 'JULIO PINHEIRO'.length * nameFontSize * 0.84
|
||
|
||
it('draws the approved art whole at 1080×1920 with high smoothing', () => {
|
||
const { ctx, drawCalls, ops } = render(deputy)
|
||
|
||
expect(drawCalls).toEqual([{ image: art, dx: 0, dy: 0, dw: 1080, dh: 1920 }])
|
||
expect(ctx.imageSmoothingEnabled).toBe(true)
|
||
expect(ctx.imageSmoothingQuality).toBe('high')
|
||
// None of the S31 composition survives: no clip/save/gradient/roundRect.
|
||
expect(ops).toHaveLength(0)
|
||
})
|
||
|
||
it('leaves the art exactly as delivered before a pick (no text, no rect)', () => {
|
||
const { drawCalls, rectCalls, textCalls, ops } = render(null)
|
||
|
||
expect(drawCalls).toEqual([{ image: art, dx: 0, dy: 0, dw: 1080, dh: 1920 }])
|
||
expect(rectCalls).toHaveLength(0)
|
||
expect(textCalls).toHaveLength(0)
|
||
expect(ops).toHaveLength(0)
|
||
})
|
||
|
||
it('fills the estadual line: mask, label at the left, name at the right and five digits', () => {
|
||
const { ctx, rectCalls, textCalls, textStates } = render(deputy)
|
||
|
||
expect(rectCalls).toEqual([
|
||
{
|
||
x: COLINHA_ROW_LAYOUT.mask.x,
|
||
y: COLINHA_ROW_LAYOUT.mask.y,
|
||
width: COLINHA_ROW_LAYOUT.mask.width,
|
||
height: COLINHA_ROW_LAYOUT.mask.height,
|
||
},
|
||
])
|
||
expect(textCalls.map((call) => call.text)).toEqual([
|
||
COLINHA_ESTADUAL_LABEL,
|
||
'JULIO PINHEIRO',
|
||
'1',
|
||
'3',
|
||
'9',
|
||
'9',
|
||
'9',
|
||
])
|
||
|
||
const label = textCalls[0]!
|
||
expect(label.x).toBeCloseTo(
|
||
COLINHA_ROW_LAYOUT.copy.right - nameWidth - COLINHA_ROW_LAYOUT.gap,
|
||
5,
|
||
)
|
||
expect(label.y).toBeCloseTo(baseline, 5)
|
||
expect(textStates[0]).toMatchObject({
|
||
align: 'right',
|
||
fill: COLINHA_ROW_LAYOUT.office.color,
|
||
font: `400 ${COLINHA_ROW_LAYOUT.office.fontSize}px Brexter`,
|
||
letterSpacing: '0px',
|
||
})
|
||
|
||
const name = textCalls[1]!
|
||
expect(name.x).toBeCloseTo(COLINHA_ROW_LAYOUT.copy.right, 5)
|
||
expect(name.y).toBeCloseTo(baseline, 5)
|
||
expect(textStates[1]).toMatchObject({
|
||
align: 'right',
|
||
fill: COLINHA_ROW_LAYOUT.name.color,
|
||
font: `900 ${nameFontSize}px Brexter`,
|
||
letterSpacing: `${COLINHA_ROW_LAYOUT.name.letterSpacingEm * nameFontSize}px`,
|
||
})
|
||
|
||
const { digit } = COLINHA_ROW_LAYOUT
|
||
const centerY = digit.top + digit.height / 2
|
||
textCalls.slice(2).forEach((call, index) => {
|
||
expect(call.x).toBeCloseTo(digit.left + digit.width / 2 + index * digit.step, 5)
|
||
expect(call.y).toBeCloseTo(centerY, 5)
|
||
expect(textStates[2 + index]).toMatchObject({
|
||
align: 'center',
|
||
fill: digit.color,
|
||
font: `900 ${digit.fontSize}px Arial Black, Arial, Helvetica, sans-serif`,
|
||
})
|
||
})
|
||
|
||
// The tracking set for the name never leaks to the next paint.
|
||
expect(ctx.letterSpacing).toBe('0px')
|
||
expect(ctx.globalAlpha).toBe(1)
|
||
})
|
||
})
|