test(miss-73): allocate municipalities, never pin seeded slugs (deadlock class)
Int specs pinning seeded rows (cairu/feira-de-santana/catalog[0..2] or class-derived fixed slugs) and mutating them deadlocked municipality_rels under the parallel suite (40P01); e2e specs deep-linking ?municipio=cairu raced the same row from two workers. - homeSearchStateDeputies: 2 fixed slugs -> 2 allocations. - municipalityPageData classe test: rewritten class-AGNOSTIC — allocates rows until two distinct territorial classes (same pinned behavior: filter narrows, totals honest, classe sort = descending weight), no fixed reduto/marginal slugs. nivel test + facet test: plain allocations. - campaignHomeActions/campaignWizardChrome: 11 hardcoded cairu deep links -> claimMunicipality(); hit clicks disambiguate by href (catalog names are not prefix-unique); caption/URL assertions use ^-anchored full-URL regexes (P3-C template guard). /Cairu/i literals gone. Verified: int 17/17, e2e 18/18 (dev mode), unit 1106/1106.
This commit is contained in:
parent
f27e801e25
commit
6b894a04c5
4 changed files with 94 additions and 43 deletions
|
|
@ -82,17 +82,22 @@ test.describe('Início — busca global (B47)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
// Claimed, never hardcoded: parallel specs sharing one seeded municipality
|
||||
// raced each other's mutations on it (miss #73).
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
const search = page.getByLabel('Buscar na campanha')
|
||||
await search.fill('Cairu')
|
||||
await search.fill(municipality.name)
|
||||
|
||||
const group = page.getByRole('region', { name: 'Resultados da busca' })
|
||||
await expect(group.getByRole('heading', { name: 'Municípios' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
})
|
||||
await group.getByRole('link', { name: /Cairu/i }).click()
|
||||
await page.waitForURL(/\/campanha\/municipios\/cairu/)
|
||||
// Disambiguate by href, not by name: catalog names are not prefix-unique
|
||||
// ("Conde" also matches "Condeúba" — the unanchored-regex lesson).
|
||||
await group.locator(`a[href$="/campanha/municipios/${municipality.slug}"]`).click()
|
||||
await page.waitForURL(new RegExp(`^https?://[^/]+/campanha/municipios/${municipality.slug}$`))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -158,20 +163,26 @@ test.describe('Wizard — busca município (B60)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/atualizar-votos')
|
||||
await expect(page.getByRole('heading', { name: WIZARD_MUNICIPALITY_STEP_TITLE })).toBeVisible()
|
||||
|
||||
const search = page.getByLabel('Buscar município')
|
||||
await search.fill('Cairu')
|
||||
await search.fill(municipality.name)
|
||||
|
||||
const results = page.getByRole('region', { name: 'Resultados da busca' })
|
||||
await expect(results.getByRole('link', { name: /Cairu/i })).toBeVisible({ timeout: 15000 })
|
||||
await results.getByRole('link', { name: /Cairu/i }).click()
|
||||
const hit = results.locator(`a[href*="municipio=${municipality.slug}"]`)
|
||||
await expect(hit).toBeVisible({ timeout: 15000 })
|
||||
await hit.click()
|
||||
|
||||
await page.waitForURL(/\/campanha\/acoes\/atualizar-votos\?municipio=cairu/)
|
||||
await expect(page.getByLabel(/Município em atualização: Cairu/i)).toBeVisible()
|
||||
await page.waitForURL(
|
||||
new RegExp(`^https?://[^/]+/campanha/acoes/atualizar-votos\\?municipio=${municipality.slug}$`),
|
||||
)
|
||||
await expect(
|
||||
page.getByLabel(new RegExp(`^Município em atualização: ${municipality.name}`, 'i')),
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('heading', { name: 'Ajustar votos estimados' })).toBeVisible()
|
||||
})
|
||||
})
|
||||
|
|
@ -188,9 +199,10 @@ test.describe('Wizard — ajuste de votos (B61 / B77)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/atualizar-votos?municipio=cairu')
|
||||
await page.goto(`/campanha/acoes/atualizar-votos?municipio=${municipality.slug}`)
|
||||
|
||||
const step = wizardVoteStep(page)
|
||||
await expect(page.getByRole('heading', { name: 'Ajustar votos estimados' })).toBeVisible({
|
||||
|
|
@ -217,9 +229,10 @@ test.describe('Wizard — ajuste de votos (B61 / B77)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/atualizar-votos?municipio=cairu')
|
||||
await page.goto(`/campanha/acoes/atualizar-votos?municipio=${municipality.slug}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Ajustar votos estimados' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
|
|
@ -239,11 +252,18 @@ test.describe('Wizard — ajuste de votos (B61 / B77)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/atualizar-votos?municipio=cairu&cenario=pessimistic')
|
||||
await page.goto(
|
||||
`/campanha/acoes/atualizar-votos?municipio=${municipality.slug}&cenario=pessimistic`,
|
||||
)
|
||||
|
||||
await page.waitForURL(/\/campanha\/acoes\/atualizar-votos\?municipio=cairu$/)
|
||||
await page.waitForURL(
|
||||
new RegExp(
|
||||
`^https?://[^/]+/campanha/acoes/atualizar-votos\\?municipio=${municipality.slug}$`,
|
||||
),
|
||||
)
|
||||
await expect(page.getByRole('heading', { name: 'Ajustar votos estimados' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
})
|
||||
|
|
@ -256,10 +276,11 @@ test.describe('Wizard — atualizar liderança (B70)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
const suffix = Date.now().toString().slice(-8).padStart(8, '0')
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/atualizar-lideranca?municipio=cairu')
|
||||
await page.goto(`/campanha/acoes/atualizar-lideranca?municipio=${municipality.slug}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Quem coordena por aqui?' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
|
|
@ -293,8 +314,10 @@ test.describe('Wizard — registrar sinal (B63)', () => {
|
|||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/registrar-sinal?municipio=cairu')
|
||||
await page.goto(`/campanha/acoes/registrar-sinal?municipio=${municipality.slug}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Que tipo de sinal?' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
|
|
@ -319,9 +342,10 @@ test.describe('Wizard — registrar sinal (B63)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/registrar-sinal?municipio=cairu')
|
||||
await page.goto(`/campanha/acoes/registrar-sinal?municipio=${municipality.slug}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Que tipo de sinal?' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
|
|
@ -341,9 +365,12 @@ test.describe('Wizard — registrar sinal (B63)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/registrar-sinal?municipio=cairu&entry=update-votes')
|
||||
await page.goto(
|
||||
`/campanha/acoes/registrar-sinal?municipio=${municipality.slug}&entry=update-votes`,
|
||||
)
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Pular registro de sinal' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
|
|
|
|||
|
|
@ -35,9 +35,12 @@ test.describe('Wizard — header mobile (B75)', () => {
|
|||
const coordinator = await fixtures.createCampaignUser('coordinator', {
|
||||
name: fixtures.value('Coordenadora Geral'),
|
||||
})
|
||||
// Claimed, never hardcoded: parallel specs sharing one seeded municipality
|
||||
// raced each other's mutations on it (miss #73).
|
||||
const municipality = await fixtures.claimMunicipality()
|
||||
|
||||
await campaign.login(page, coordinator.email!, coordinator.password)
|
||||
await page.goto('/campanha/acoes/atualizar-votos?municipio=cairu')
|
||||
await page.goto(`/campanha/acoes/atualizar-votos?municipio=${municipality.slug}`)
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Ajustar votos estimados' })).toBeVisible({
|
||||
timeout: 15000,
|
||||
|
|
@ -45,7 +48,9 @@ test.describe('Wizard — header mobile (B75)', () => {
|
|||
|
||||
const topBar = page.locator('[data-slot="campaign-mobile-top-bar"][data-mode="wizard"]')
|
||||
await expect(topBar.getByText('Ajustar votos', { exact: true })).toBeVisible()
|
||||
await expect(topBar.getByLabel(/Município em atualização: Cairu/i)).toBeVisible()
|
||||
await expect(
|
||||
topBar.getByLabel(new RegExp(`^Município em atualização: ${municipality.name}`, 'i')),
|
||||
).toBeVisible()
|
||||
await expect(page.getByRole('link', { name: /Voltar/ })).toBeVisible()
|
||||
|
||||
await page.getByRole('link', { name: /Voltar/ }).click()
|
||||
|
|
|
|||
|
|
@ -68,11 +68,13 @@ describe('searchHomeStateDeputies (B52)', () => {
|
|||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
const stateDeputy = await fixtures.createStateDeputy({ name: 'Carteira Dobradinha' })
|
||||
const cairu = await fixtures.getMunicipality('cairu')
|
||||
const feira = await fixtures.getMunicipality('feira-de-santana')
|
||||
// Allocated, never pinned slugs: parallel specs mutating the same seeded
|
||||
// municipality deadlocked municipality_rels (miss #73).
|
||||
const first = await fixtures.getMunicipality()
|
||||
const second = await fixtures.getMunicipality()
|
||||
await setStateDeputyMunicipalitiesBatchRecord(payload, coordinator, {
|
||||
stateDeputyId: stateDeputy.id,
|
||||
municipalityIds: [cairu.id, feira.id],
|
||||
municipalityIds: [first.id, second.id],
|
||||
assigned: true,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { getPayload } from 'payload'
|
|||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import { getMunicipalityCatalogEntry, municipalityCatalog } from '@/lib/municipalityCatalog'
|
||||
import { territorialClassSortWeight } from '@/lib/territorialClassSortWeight'
|
||||
import config from '@/payload.config'
|
||||
import { loadMunicipalityListPageBundle } from '@/utilities/municipality/municipalityPageData'
|
||||
import { computeMunicipalityTerritorialClass } from '@/utilities/municipality/municipalityTerritorialClass'
|
||||
|
|
@ -287,19 +288,27 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
|
||||
const redutoEntry = municipalityCatalog.find(
|
||||
(entry) => computeMunicipalityTerritorialClass(entry.slug).class === 'reduto',
|
||||
)!
|
||||
const marginalEntry = municipalityCatalog.find(
|
||||
(entry) => computeMunicipalityTerritorialClass(entry.slug).class === 'marginal',
|
||||
)!
|
||||
const reduto = await fixtures.getMunicipality(redutoEntry.slug)
|
||||
const marginal = await fixtures.getMunicipality(marginalEntry.slug)
|
||||
// Two allocated municipalities with DISTINCT territorial classes: the
|
||||
// behavior pinned here (class filter narrows, totals stay honest, classe
|
||||
// sort follows the weight table) does not care WHICH classes they are —
|
||||
// and pinning fixed catalog slugs raced parallel specs mutating the same
|
||||
// seeded rows (deadlock on municipality_rels, miss #73).
|
||||
const first = await fixtures.getMunicipality()
|
||||
const firstClass = computeMunicipalityTerritorialClass(first.slug).class
|
||||
let second = await fixtures.getMunicipality()
|
||||
let secondClass = computeMunicipalityTerritorialClass(second.slug).class
|
||||
for (let attempt = 0; secondClass === firstClass && attempt < 20; attempt++) {
|
||||
second = await fixtures.getMunicipality()
|
||||
secondClass = computeMunicipalityTerritorialClass(second.slug).class
|
||||
}
|
||||
if (secondClass === firstClass) {
|
||||
throw new Error('could not allocate municipalities of two distinct classes')
|
||||
}
|
||||
|
||||
const marker = `e10-classe-${Date.now()}`
|
||||
for (const [municipality, suffix] of [
|
||||
[reduto, 'reduto'],
|
||||
[marginal, 'marginal'],
|
||||
[first, 'filtrada'],
|
||||
[second, 'fora'],
|
||||
] as const) {
|
||||
await payload.update({
|
||||
collection: 'municipality',
|
||||
|
|
@ -313,29 +322,39 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
|
||||
const filtered = await loadMunicipalityListPageBundle(payload, coordinator, {
|
||||
q: marker,
|
||||
class: 'reduto',
|
||||
class: firstClass,
|
||||
})
|
||||
expect(filtered.municipalities.map((row) => row.slug)).toEqual([reduto.slug])
|
||||
expect(filtered.municipalities.map((row) => row.slug)).toEqual([first.slug])
|
||||
expect(filtered.totalDocs).toBe(1)
|
||||
expect(filtered.overview?.municipalityCount).toBe(1)
|
||||
expect(filtered.municipalities[0]!.territorialClass).toBe('reduto')
|
||||
expect(filtered.municipalities[0]!.territorialClass).toBe(firstClass)
|
||||
expect(filtered.municipalities[0]!.territorialClassFactors.length).toBeGreaterThan(0)
|
||||
|
||||
// A native sort key would otherwise let Payload paginate before the class
|
||||
// filter runs, which would hand back a short page with an inflated total.
|
||||
const byName = await loadMunicipalityListPageBundle(payload, coordinator, {
|
||||
q: marker,
|
||||
class: 'reduto',
|
||||
class: firstClass,
|
||||
sort: 'name',
|
||||
})
|
||||
expect(byName.municipalities.map((row) => row.slug)).toEqual([reduto.slug])
|
||||
expect(byName.municipalities.map((row) => row.slug)).toEqual([first.slug])
|
||||
expect(byName.totalDocs).toBe(1)
|
||||
|
||||
const sorted = await loadMunicipalityListPageBundle(payload, coordinator, {
|
||||
q: marker,
|
||||
sort: 'classe',
|
||||
})
|
||||
expect(sorted.municipalities.map((row) => row.slug)).toEqual([reduto.slug, marginal.slug])
|
||||
// `classe` sorts strongest class first (reduto 4 … marginal 1; every
|
||||
// catalog row classes into a weighted class, so no null weight exists).
|
||||
const expectedOrder = [first, second]
|
||||
.map((municipality) => ({
|
||||
slug: municipality.slug,
|
||||
weight:
|
||||
territorialClassSortWeight[computeMunicipalityTerritorialClass(municipality.slug).class],
|
||||
}))
|
||||
.sort((left, right) => (right.weight ?? 0) - (left.weight ?? 0))
|
||||
.map((entry) => entry.slug)
|
||||
expect(sorted.municipalities.map((row) => row.slug)).toEqual(expectedOrder)
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
@ -348,9 +367,9 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
const fixtures = campaignFixtures()
|
||||
const coordinator = await fixtures.createCampaignUser('coordinator')
|
||||
|
||||
const low = await fixtures.getMunicipality(municipalityCatalog[0]!.slug)
|
||||
const high = await fixtures.getMunicipality(municipalityCatalog[1]!.slug)
|
||||
const none = await fixtures.getMunicipality(municipalityCatalog[2]!.slug)
|
||||
const low = await fixtures.getMunicipality()
|
||||
const high = await fixtures.getMunicipality()
|
||||
const none = await fixtures.getMunicipality()
|
||||
|
||||
const marker = `e14-nivel-${Date.now()}`
|
||||
for (const [municipality, suffix, engagementLevel] of [
|
||||
|
|
@ -446,9 +465,7 @@ describe('loadMunicipalityListPageBundle', () => {
|
|||
data: { priority: 'alta' },
|
||||
overrideAccess: true,
|
||||
})
|
||||
const covered = await fixtures.getMunicipality(
|
||||
municipalityCatalog.find((entry) => entry.slug !== priorityMunicipality.slug)!.slug,
|
||||
)
|
||||
const covered = await fixtures.getMunicipality()
|
||||
await fixtures.assignMunicipalityAdvisors(covered.id, [advisor.id])
|
||||
|
||||
// "Prioritária" lives in the Município popover, so it must not hide the
|
||||
|
|
|
|||
Loading…
Reference in a new issue