diff --git a/tests/e2e/campaignHomeActions.e2e.spec.ts b/tests/e2e/campaignHomeActions.e2e.spec.ts index 7ce7948b..7ff2b712 100644 --- a/tests/e2e/campaignHomeActions.e2e.spec.ts +++ b/tests/e2e/campaignHomeActions.e2e.spec.ts @@ -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, diff --git a/tests/e2e/campaignWizardChrome.e2e.spec.ts b/tests/e2e/campaignWizardChrome.e2e.spec.ts index 4fb42995..c7256755 100644 --- a/tests/e2e/campaignWizardChrome.e2e.spec.ts +++ b/tests/e2e/campaignWizardChrome.e2e.spec.ts @@ -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() diff --git a/tests/int/homeSearchStateDeputies.int.spec.ts b/tests/int/homeSearchStateDeputies.int.spec.ts index f27691b1..18ee529a 100644 --- a/tests/int/homeSearchStateDeputies.int.spec.ts +++ b/tests/int/homeSearchStateDeputies.int.spec.ts @@ -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, }) diff --git a/tests/int/municipalityPageData.int.spec.ts b/tests/int/municipalityPageData.int.spec.ts index 73dfd09c..d0edfa7b 100644 --- a/tests/int/municipalityPageData.int.spec.ts +++ b/tests/int/municipalityPageData.int.spec.ts @@ -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