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.
252 lines
8.1 KiB
TypeScript
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)
|
|
})
|
|
})
|