teqo/tests/int/campaignAdvisorManagement.int.spec.ts
Francisco Ignácio Fontoura Solla 812e3a0bee B34: editable município chips on the leaderships list.
The "Municípios" column was dead text; it is now chips that navigate to the
município, with a territory chip when a leadership covers a whole TI, and it is
editable in place. What picks inline vs. Drawer is the `pointer:` media query,
not viewport width: on a fine pointer the editor is always mounted (the "×" sits
at the chip's corner, revealed on hover/focus) and on a coarse pointer a tap
anywhere in the cell opens a Drawer with 44 px targets.

`AdvisorMunicipalityCell` is promoted to `shared/MunicipalityPortfolioCell` and
/campanha/assessores migrates onto it in the same change, so the second call site
is born with the first. One batch action only; the single-id advisor twin is
deleted with its int tests migrated. The anti-goal on always-mounted list inputs
is amended for chip-based relations on fine pointers, not reopened.

Also fixes a latent bug: `CampaignTableHead`'s `description` seam broke any RSC
caller, because its two helpers lived in a 'use client' module.
2026-07-27 22:35:19 -03:00

252 lines
8.1 KiB
TypeScript

// @vitest-environment node
import type { Payload } from 'payload'
import { getPayload } from 'payload'
import { beforeAll, describe, expect, it } from 'vitest'
import {
createAdvisorRecord,
sendAdvisorPasswordResetRecord,
setAdvisorMunicipalitiesBatchRecord,
updateAdvisorProfileRecord,
} from '@/app/(campaign)/campanha/actions/advisor'
import { assignMunicipalityAdvisorsRecord } from '@/app/(campaign)/campanha/actions/municipality'
import config from '@/payload.config'
import { installCampaignFixtures } from '../helpers/campaignFixtures'
let payload: Payload
const campaignFixtures = installCampaignFixtures({
getPayload: () => payload,
setPayload: (nextPayload) => {
payload = nextPayload
},
})
const advisorIdsOf = async (municipalityID: number): Promise<number[]> => {
const municipality = await payload.findByID({
collection: 'municipality',
id: municipalityID,
depth: 0,
select: { advisors: true },
overrideAccess: true,
})
return (municipality.advisors ?? []).map((entry) =>
typeof entry === 'number' ? entry : entry.id,
)
}
describe('campaign advisor management (B19)', () => {
beforeAll(async () => {
payload = await getPayload({ config: await config })
})
it.each(['coordinator', 'candidate'] as const)(
'lets a %s create an advisor and assign a municipality',
async (role) => {
const fixtures = campaignFixtures()
const actor = await fixtures.createCampaignUser(role)
const municipality = await fixtures.getMunicipality()
const email = `${fixtures.value('advisor')}@example.com`
const created = await createAdvisorRecord(payload, actor, {
name: 'Assessor Novo',
email,
phone: fixtures.phone(),
})
expect(created.role).toBe('advisor')
const stored = await payload.findByID({
collection: 'campaignUser',
id: created.id,
depth: 0,
select: { email: true, role: true },
overrideAccess: true,
})
expect(stored.email).toBe(email)
expect(stored.role).toBe('advisor')
await setAdvisorMunicipalitiesBatchRecord(payload, actor, {
advisorId: created.id,
municipalityIds: [municipality.id],
assigned: true,
})
fixtures.touchMunicipality(municipality.id)
expect(await advisorIdsOf(municipality.id)).toContain(created.id)
const updated = await updateAdvisorProfileRecord(payload, actor, {
id: created.id,
name: 'Assessor Atualizado',
})
expect(updated.name).toBe('Assessor Atualizado')
},
)
it('denies advisor and leader from creating or assigning', async () => {
const fixtures = campaignFixtures()
const advisor = await fixtures.createCampaignUser('advisor')
const leader = await fixtures.createCampaignUser('leader')
const municipality = await fixtures.getMunicipality()
const target = await fixtures.createCampaignUser('advisor')
await expect(
createAdvisorRecord(payload, advisor, {
name: 'Bloqueado',
email: `${fixtures.value('blocked')}@example.com`,
}),
).rejects.toThrow(/coordenação geral ou o candidato/i)
await expect(
createAdvisorRecord(payload, leader, {
name: 'Bloqueado',
email: `${fixtures.value('blocked-leader')}@example.com`,
}),
).rejects.toThrow(/coordenação geral ou o candidato/i)
await expect(
setAdvisorMunicipalitiesBatchRecord(payload, advisor, {
advisorId: target.id,
municipalityIds: [municipality.id],
assigned: true,
}),
).rejects.toThrow(/coordenação geral ou o candidato/i)
})
it('rejects duplicate advisor emails with a safe message', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const email = `${fixtures.value('dup')}@example.com`
await createAdvisorRecord(payload, coordinator, {
name: 'Primeiro',
email,
})
await expect(
createAdvisorRecord(payload, coordinator, {
name: 'Segundo',
email,
}),
).rejects.toThrow('Já existe uma conta com este e-mail.')
})
it('blocks password reset for planilha placeholder emails', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const placeholder = await fixtures.createCampaignUser('advisor', {
email: `${fixtures.value('slug')}@planilha.invalid`,
})
await expect(
sendAdvisorPasswordResetRecord(payload, coordinator, { advisorId: placeholder.id }),
).rejects.toThrow(/e-mail placeholder da planilha/i)
})
it('keeps preventAssignedAdvisorDowngrade when the advisor still has municipalities', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const advisor = await fixtures.createCampaignUser('advisor')
const municipality = await fixtures.getMunicipality()
await assignMunicipalityAdvisorsRecord(payload, coordinator, {
municipality: municipality.id,
advisors: [advisor.id],
})
fixtures.touchMunicipality(municipality.id)
await expect(
payload.update({
collection: 'campaignUser',
id: advisor.id,
data: { role: 'leader' },
user: coordinator,
overrideAccess: false,
}),
).rejects.toThrow(/Remova ou substitua este usuário da assessoria/i)
})
it('is idempotent when toggling municipality membership', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const advisor = await fixtures.createCampaignUser('advisor')
const municipality = await fixtures.getMunicipality()
await setAdvisorMunicipalitiesBatchRecord(payload, coordinator, {
advisorId: advisor.id,
municipalityIds: [municipality.id],
assigned: true,
})
fixtures.touchMunicipality(municipality.id)
await setAdvisorMunicipalitiesBatchRecord(payload, coordinator, {
advisorId: advisor.id,
municipalityIds: [municipality.id],
assigned: true,
})
expect(await advisorIdsOf(municipality.id)).toEqual([advisor.id])
await setAdvisorMunicipalitiesBatchRecord(payload, coordinator, {
advisorId: advisor.id,
municipalityIds: [municipality.id],
assigned: false,
})
await setAdvisorMunicipalitiesBatchRecord(payload, coordinator, {
advisorId: advisor.id,
municipalityIds: [municipality.id],
assigned: false,
})
expect(await advisorIdsOf(municipality.id)).toEqual([])
})
it('assigns and removes multiple municipalities in one batch', async () => {
const fixtures = campaignFixtures()
const coordinator = await fixtures.createCampaignUser('coordinator')
const advisor = await fixtures.createCampaignUser('advisor')
const first = await fixtures.getMunicipality()
const second = (
await payload.find({
collection: 'municipality',
where: { id: { not_equals: first.id } },
limit: 1,
depth: 0,
overrideAccess: true,
})
).docs[0]
expect(second).toBeTruthy()
await setAdvisorMunicipalitiesBatchRecord(payload, coordinator, {
advisorId: advisor.id,
municipalityIds: [first.id, second.id],
assigned: true,
})
fixtures.touchMunicipality(first.id)
fixtures.touchMunicipality(second.id)
const assigned = await payload.find({
collection: 'municipality',
where: { advisors: { contains: advisor.id } },
depth: 0,
limit: 0,
pagination: false,
overrideAccess: true,
})
expect(assigned.docs.map((doc) => doc.id).sort((a, b) => a - b)).toEqual(
[first.id, second.id].sort((a, b) => a - b),
)
await setAdvisorMunicipalitiesBatchRecord(payload, coordinator, {
advisorId: advisor.id,
municipalityIds: [first.id, second.id],
assigned: false,
})
const cleared = await payload.find({
collection: 'municipality',
where: { advisors: { contains: advisor.id } },
depth: 0,
limit: 0,
pagination: false,
overrideAccess: true,
})
expect(cleared.docs).toHaveLength(0)
})
})